Collection结构概览
Collection
list和set接口都是继承了collection接口,当然可以使用父类的一些方法
这里就探讨下collection里一些常用的方法
1、添加
add(Object obj)
addAll(Collection coll)
2、获取有效元素的个数
int size()
3、清空集合
void clear()
4、是否是空集合
boolean isEmpty()
5、是否包含某个元素boolean contains(Object obj)
:是通过元素的equals方法来判断是否
是同一个对象boolean containsAll(Collection c)
:也是调用元素的equals方法来比
较的。拿两个集合的元素挨个比较。
6、删除boolean remove(Object obj)
:通过元素的equals方法判断是否是
要删除的那个元素。只会删除找到的第一个元素boolean removeAll(Collection coll)
:取当前集合的差集
7、取两个集合的交集boolean retainAll(Collection c)
:把交集的结果存在当前集合中,不
影响c
8、集合是否相等
boolean equals(Object obj)
9、转成对象数组
Object[] toArray()
10、获取集合对象的哈希值
hashCode()
11、遍历iterator()
:返回迭代器对象,用于集合遍历
collection集合的遍历方式
collection是个接口没法实例化我们用多态的形式,通过它的子类arrarylist来展示collection的遍历方式
- 迭代器
public static void main(String[] args) {
List<Integer> list=new ArrayList<>();
Collections.addAll(list,5,3,1,4);
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
System.out.print(iterator.next());
}
}
- 增强for循环
public static void main(String[] args) {
List<Integer> list=new ArrayList<>();
Collections.addAll(list,5,3,1,4);
for(Integer num:list){
System.out.print(num);
}
}
lambda表达式
public static void main(String[] args) {
List<Integer> list=new ArrayList<>();
Collections.addAll(list,5,3,1,4);
list.forEach(System.out::println);
}
iterator迭代器接口
- Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
- Iterator 仅用于遍历集合,Iterator 本身并不提供承装对象的能力。如果需要创建Iterator 对象,则必须有一个被迭代的集合。
- 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
对于iterator迭代器
iterator迭代器的方法
boolean hasNext()
如果迭代具有更多的元素,则返回true 。 (换句话说,如果next()返回一个元素而不是抛出一个异常,则返回true )E next()
返回迭代中的下一个元素。
结果
迭代中的下一个元素
异常NoSuchElementException
- 如果迭代没有更多的元素
可能对于迭代器的指针有很多的看法,大致分为,指针是在第一个元素前边,或者在第一个元素上
我认为的是指针是在第一个元素之前,根据上边的iterator 的两个方法可以看出,在调用it.next()方法之前必须要调用it.hasNext()进行检测。若不调用,且
下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常,如果此时iterator的指针是在第一个元素的时候,集合的第一个元素可能就无法被遍历
default void remove()
Iterator可以删除集合的元素,但是是遍历过程中通过迭代器对象的remove方法,不是集合对象的remove方法。如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,再调用remove都会报IllegalStateException。
- remove()将会删除上次调用next()时返回的元素,也就是说先调用next()方法,再调用remove方法才会删除元素。next()和romove方法具有依赖性,必须先用next,再使用romove。如果先用remove方法会出现IllegalStateException异常。
- 使用remove()方法必须紧跟在next()之后执行,如果在remove和next中间,集合出现了结构性变化(删除或者是增加)则会出现异常IllegalStateException。
List系列
List系列集合特点
- ArrayList、LinekdList:有序,可重复,有索引。
- 有序:存储和取出的元素顺序一致
- 有索引:可以通过索引操作元素
- 可重复:存储的元素可以重复
List的实现类的底层原理
- ArrayList 的底层实现
它是一个动态数组,实现了 List 接口以及 list相关的所有方法,它允许所有元素的插入,包括 null。
- 属性
//默认容量的大小
private static final int DEFAULT_CAPACITY = 10;
//空数组常量
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认的空数组常量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMEN
TDATA = {};
//存放元素的数组,从这可以发现 ArrayList 的底层实现就是一个 Object
数组
transient Object[] elementData;
//数组中包含的元素个数
private int size;
//数组的最大上限
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALU
E - 8;
ArrayList 的属性非常少,就只有这些。其中最重要的莫过于 elementData 了,ArrayList所有的方法都是建立在 elementData 之上。接下来,我们就来看一下一些主要的方法吧。
- 构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacit
y: "+initialCapacity);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
可以看出当使用空参构造器的时候就会初始化一个空的object数组,当调用有参构造器的时候就会初始化为我们参数的大小
- get方法
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg
(index));
}
E elementData(int index) {
return (E) elementData[index];
}
ArrayList的底层使用数组结构来实现的,所以get方法特别简单,先判断索引是否越界,没有越界的话直接通过索引获取就好了
- add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCou
nt!!
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCou
nt!!
//调用一个 native 的复制方法,把 index 位置开始的元素都往后挪一位
System.arraycopy(elementData, index, elementData, inde
x + 1, size - index);
elementData[index] = element;
size++;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacit
y);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
ArrayList 的 add 方法也很好理解,在插入元素之前,它会先检查是否需要扩容,然后再把元素添加到数组中最后一个元素的后面ensureCapacityInternal 方法中,我们可以看见,如果当 elementData 为空数组时,它会使用默认的大小去扩容。所以说,通过无参构造方法来创建 ArrayList 时,它的大小其实是为 0 的,只有在使用到的时候,才会通过 grow 方法去创建一个大小为 10 的数组。第一个 add 方法的复杂度为 O(1),虽然有时候会涉及到扩容的操作,但是扩容的次数是非常少的,所以这一部分的时间可以忽略不计。如果使用的是带指定下标的 add方法,则复杂度为 O(n),因为涉及到对数组中元素的移动,这一操作是非常耗时的。
- set方法
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
这个方法也比较简单,先去检查索引是否越界,然后将旧值保存起来,将新值赋给旧值,在将旧值返回
- remove方法
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData,
index, numMoved);
elementData[--size] = null; // clear to let GC do its wo
rk
return oldValue;
}
同样的也是根据索引将数组别的元素移动,然后将末尾的元素置空
- grow()
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a wi
n:
elementData = Arrays.copyOf(elementData, newCapacity);
}
grow 方法是在数组进行扩容的时候用到的,从中我们可以看见,ArrayList 每次扩容都是扩 1.5 倍,然后调用 Arrays 类的 copyOf 方法,把元素重新拷贝到一个新的数组
中去。
- indexof() lastindexof()
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
即为遍历数组需寻找,返回索引值,lastindexof就是从后往前寻找罢了
- LinkedList
linkedlist底层是双向链表,链表不同于数组,数组通过索引查找元素很快,插入删除操作会速度略差,因为涉及到元素的移动,链表的插入删除效率就好点,且在头尾结点都有一个指针,所以对头尾结点的操作很快,很适合用来模拟队列或者栈
List集合特有的方法
List集合的遍历
由于list集合是支持索引的所以对于list集合的遍历,除了collection中的遍历方式,也可以通过for循环
public static void main(String[] args) {
List<Integer> list=new ArrayList<>();
Collections.addAll(list,5,3,1,4);
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i));
}
}
LinkedList的特点
双向链表即一个结点中会有前一个结点的地址,data,后一个结点的地址,头尾的操作是很快的,这里就不在赘述
并发修改异常问题
list集合在进行删除操作的时候会伴随着数组的移动操作,所以才会出现这样的错误,所以在删除的时候我们可以用上文所诉的iterator的remove方法,或者倒叙遍历删除,同样可以避免这个异常问题。
底层是list在删除的时候会检测size,如果变动的话就会报这个错误的,底层源码这里就不在论述了
Set系列
Set系列集合特点:
- 无序:存取顺序不一致
- 不重复:可以去除重复
Set集合实现类特点:
- HashSet:无序、不重复、无索引
- LinkedHashSet:有序、不重复、无索引
*TreeSet:排序、不重复、无索引
hashset底层原理
hashset底层是基于哈希表来实现的
在jdk1.8之前哈希表是数组加链表的形式
1.8之后变为数组+链表+红黑树的形式 当链表长度为8时自动转换为红黑树
红黑树是avl树的一种变体,也有研究过avl树的代码,增删改查的效率很高,红黑树自然不会差,相比于链表来说必然是很强的
附上哈希表的初始化步骤吧:
再有不懂的俺的数据结构专栏有哈希表的简单用法,虽然根本不能和jdk的比,但还是可以感受下简单的哈希表,是用数组加链表实现的
LinkedHashSet
大致相同于hashset的底层原理,不同得到是linkedhashset根据元素的插入顺序会有指针指向,用来记录插入的顺序
即它是有序的、不重复的、同样的也没有索引
TreeSet
一般来说使用有参构造器多一点,如果去将类实现comparable接口,会将类的比较固化。
这里放下有参构造器的示例代码
public static void main(String[] args) {
//默认是将数字从小到大排序,因为integer类实现了comperable接口
Set<Integer> list=new TreeSet<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
Collections.addAll(list,5,3,1,4);
System.out.println(list);//5413
}
使用场景的大概总结
但排序的话其实还是很少用treeset的,collections工具类的方法同样可以实现对list的排序,也是比较常用的
Map接口
map集合的概述
map集合体系特点
map集合常用api
map集合的遍历
public static void main(String[] args) {
Map<String,Integer> map=new HashMap<>();
map.put("cxf",18);
map.put("xx",19);
map.put("ff",20);
//方式一
Set<String> strings = map.keySet();
for (String key:strings){
System.out.println(key+"="+map.get(key));
}
//方式二
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for (Map.Entry<String, Integer> entry : entries) {
System.out.println(entry.getKey()+"="+entry.getValue());
}
//方式三
map.forEach((k,v)-> System.out.println(k+"="+v));
}
实现类hashmap
HashMap 的大致结构如下图所示,其中哈希表是一个数组,我们经常把数组中的每一个节点称为一个桶,哈希表中的每个节点都用来存储一个键值对。在插入元素时,如果发生冲突(即多个键值对映射到同一个桶上)的话,就会通过链表的形式来解决冲突。因为一个桶上可能存在多个键值对,所以在查找的时候,会先通过 key 的哈希值先定位到桶,再遍历桶上的所有键值对,找出 key 相等的键值对,从而来获取 value。同样的jdk8以前和jdk8以后如果过一个桶中出现多个结点,挂载方式也是有所不同的,jdk8及以前是将旧元素挂载在新元素的下边,而jdk8及以后是将新元素,挂载在旧元素的下边,当链表的长度超过8时,链表转换为红黑树
底层的一些源码
- 属性
//默认的初始容量为 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大的容量上限为 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的负载因子为 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//变成树型结构的临界值为 8
static final int TREEIFY_THRESHOLD = 8;
//恢复链式结构的临界值为 6
static final int UNTREEIFY_THRESHOLD = 6;
//哈希表
transient Node<K,V>[] table;
//哈希表中键值对的个数
transient int size;
//哈希表被修改的次数
transient int modCount;
//它是通过 capacity*load factor 计算出来的,当 size 到达这个值时,
就会进行扩容操作
int threshold;
//负载因子
final float loadFactor;
//当哈希表的大小超过这个阈值,才会把链式结构转化成树型结构,否则仅采
取扩容来尝试减少冲突
static final int MIN_TREEIFY_CAPACITY = 64;
即初始数组的长度是16,默认的加载因子是0.75,变成树形结构的临界值时8
- Node定义
node是hashmap的一个静态内部类,属性中有hash key value 和next指针
内部代码也是一些常规代码放下边了就
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + valu
e; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(val
ue);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
- get方法
get 方法主要调用的是 getNode 方法,所以重点要看 getNode 方法的
实现
public V get(Object key) {
Node<K,V> e;
return (e = getNode(key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods.
*
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & (hash = hash(key))]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
实现步骤大致如下:
1、通过 hash 值获取该 key 映射到的桶。
2、桶上的 key 就是要查找的 key,则直接命中。
3、桶上的 key 不是要查找的 key,则查看后续节点:
(1)如果后续节点是树节点,通过调用树的方法查找该 key。
(2)如果后续节点是链式节点,则通过循环遍历链查找该 key。
简单点的就是通过hash值定位到,该结点在数组中的存储位置,然后通过树或者链表本身的getnode方法来进行查找,找到就返回。
- put方法
//put 方法的具体实现也是在 putVal 方法中,所以我们重点看下面的 pu
tVal 方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIf
Absent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果哈希表为空,则先创建一个哈希表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果当前桶没有碰撞冲突,则直接把键值对插入,完事
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果桶上节点的 key 与当前 key 重复,那你就是我要找的节点
了
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equ
als(k))))
e = p;
//如果是采用红黑树的方式处理冲突,则通过红黑树的 putTree
Val 方法去插入这个键值对
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab,
hash, key, value);
//否则就是传统的链式结构
else {
//采用循环遍历的方式,判断链中是否有重复的 key
for (int binCount = 0; ; ++binCount) {
//到了链尾还没找到重复的 key,则说明 HashMap 没有
包含该键
if ((e = p.next) == null) {
//创建一个新节点插入到尾部
p.next = newNode(hash, key, value, nul
l);
//如果链的长度大于 TREEIFY_THRESHOLD 这个
临界值,则把链变为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
// -1 for 1st
treeifyBin(tab, hash);
break;
}
//找到了重复的 key
if (e.hash == hash &&
((k = e.key) == key || (key != null &&
key.equals(k))))
break;
p = e;
}
}
//这里表示在上面的操作中找到了重复的键,所以这里把该键的
值替换为新值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//判断是否需要进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
put 方法比较复杂,实现步骤大致如下:
1、先通过 hash 值计算出 key 映射到哪个桶。
2、如果桶上没有碰撞冲突,则直接插入。
3、如果出现碰撞冲突了,则需要处理冲突:
(1)如果该桶使用红黑树处理冲突,则调用红黑树的方法插入。
(2)否则采用传统的链式方法插入。如果链的长度到达临界值,则把链转变为红黑树。
4、如果桶中存在重复的键,则为该键替换新值。
5、如果 size 大于阈值,则进行扩容。
- remove方法
//remove 方法的具体实现在 removeNode 方法中,所以我们重点看下面的 r
emoveNode 方法
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, tru
e)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object va
lue,
boolean matchValue, boolean movabl
e) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//如果当前 key 映射到的桶不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//如果桶上的节点就是要找的 key,则直接命中
if (p.hash == hash && ((k = p.key) == key || (key !=
null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
//如果是以红黑树处理冲突,则构建一个树节点
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash,
key);
//如果是以链式的方式处理冲突,则通过遍历链表来寻找节点
else {
do {
if (e.hash == hash && ((k = e.key) == key
|| (key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//比对找到的 key 的 value 跟要删除的是否匹配
if (node != null && (!matchValue || (v = node.value)
== value ||
(value != null && value.equals
(v)))) {
//通过调用红黑树的方法来删除节点
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, t
ab, movable);
//使用链表的操作来删除节点
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
步骤也很简单
1.先判断当前key映射的桶是否为空
2.如果桶上的结点就是要找的结点,则直接命中
3.判断是链式冲突,还是红黑树冲突,红黑树冲突的话要构建一个红黑树结点
4.如果是链式的就调用链式的查找方式去查找
5.通过equals判断key和value是否相同
6.如果是红黑树就调用红黑树的remove方法
7.如果是链表就调用链表的删除的方法
总结
1.数组加链表的方式当链表过长时对效率影响很大
2.数组加链表加红黑树 的方式大大提高了效率,属于用空间换时间了属于是。
实现类linkedhashmap
linkedhashmap大体上继承自hashmap多了些对链表的操作
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
相比于hashmap的node 它这里多了记录插入顺序的前、后指针
仔细看过hashmap源码的话你会发现有这三个空方法
同时
final boolean accessOrder;
该属性的打开或者关闭也表示linkefhashmap是否实现最近最少使用的算法
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
实这三个方法表示的是在访问、插入、删除某个节点之后,进行一些处理,它们在 LinkedHashMap 都有各自的实现。LinkedHashMap 正是通过重写这三个方法来保证链表的插入、删除的有序性。
这里不在详细展开说
实现类treemap