集合:
List:
List集合具有以下特点:
- 集合中的元素允许重复
- 集合中的元素是有顺序的,各元素插入的顺序就是各元素的顺序
- 集合中的元素可以通过索引来访问或者设置
List接口常用的实现类有:ArrayList、LinkedList、Vector。
Arraylist(线程不安全):
ArrayList 实现了 List 接口,继承了 AbstractList 抽象类,底层是基于数组实现的,并且实现了动态扩容。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
private static final int DEFAULT_CAPACITY = 10;
transient Object[] elementData;
private int size;
}
RandomAccess 接口 : 随机访问不需要遍历,就可以通过下标(索引)直接访问到内存地址。
Cloneable 接口 : 这表明 ArrayList 是支持拷贝的。ArrayList 内部的确也重写了 Object 类的 clone()
方法
Serializable 接口 :序列化转二进制,内部提供了两个私有方法 writeObject 和 readObject 来完成序列化和反序列化
动态扩容: ArrayList ,
优点: 底层数据结构是数组,查询快,增删慢。
缺点: 线程不安全,效率高
遍历ArrayList的元素主要有以下3种方式:
ArrayList<String> list = new ArrayList<>();
list.add("我");
list.add("adwj");
list.add("我");
-
迭代器iterator遍历
Iterator<String> iterator = list.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); }
-
for循环
for(int i=0;i<list.size();i++){ System.out.println(list.get(i)); }
-
foreach循环
for (String s : list) { System.out.println(s); }
LinkedList :
LinkedList 是一个继承自 AbstractSequentialList 的双向链表,因此它也可以被当作堆栈、队列或双端队列进行操作
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
}
优点: 底层数据结构是链表,查询慢,增删快。
缺点: 线程不安全,效率高
vector(线程安全):
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
}
常问:
相同点
ArrayList、LinkedList、Vector都实现了List接口,所以使用方式很类似
不同点
但是ArrayList、LinkedList、Vector的内部实现方式不同,也就导致了它们之间是有区别的。
存储结构
ArrayList和Vector是基于数组实现的,LinkedList是基于双向链表实现的。
这也就导致ArrayList适合随机查找和遍历,而LinkedList适合动态插入和删除元素。
线程安全性
ArrayList和LinkedList是线程不安全的,Vector是线程安全的。
Vector可以看做是ArrayList在多线程环境下的另一种实现方式,这也导致了Vector的效率没有ArraykList和LinkedList高。
如果要在并发环境下使用ArrayList或者LinkedList,可以调用Collections类的synchronizedList()方法(分情况)
如果读大于写 那么可以使用CopyOnWriteArrayList<>() 不过会在写操作复制一个list进行写操作 会占用大量内存
CopyOnWriteArrayList
中, 定义了一个可重入锁:
final transient ReentrantLock lock = new ReentrantLock();
该锁用于对所有修改集合的方法 (add
, remove
等) 进行同步, 在进行实际修改操作时, 会先复制原来的数组, 再进行修改, 最后替换原来的
但也会因此引入 "弱一致性" 问题; 所谓 "弱一致性" 是指当一个线程正在读取数据时, 若此时有另一个线程同时在修改该区域的数据, 读取的线程将无法读取最新的数据, 即该读取线程只能读取到它读取时刻以前的最新数据;
ArrayList初始容量是10,添加第11个元素会扩容到16,之后扩容因子是1.5+1
Vector初始容量是10,添加到第11个元素扩容为20,21个元素扩容为40,接着80.扩容因子2
Set:
Set集合包括Set接口以及Set接口的所有实现类。Set集合具有以下特点:
- 集合中不包含重复元素(你可以重复添加,但只会保留第1个)
- 集合中的元素不一定保证有序
Set接口常用的实现类有:HashSet、LinkedHashSet、TreeSet。
Hashset:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
底层数据结构是哈希表。(无序,唯一)如何来保证元素唯一性?
1.依赖两个方法:hashCode()和equals()
hashCode()判断哈希值是否相同,相同后调用equals() 判断是否是一个元素
遍历元素用迭代器或者foreach循环
TreeSet:
底层数据结构是红黑树。(唯一,有序)
- 如何保证元素排序的呢?
自然排序 实现接口Comparable,重写该接口的方法compareTo()
比较器排序 - 如何保证元素唯一性的呢?
根据比较的返回值是否是0来决定 :
LinkedHashSet:
底层数据结构是链表和哈希表。(FIFO插入有序,唯一)
1.由链表保证元素有序
2.由哈希表保证元素唯一
加载因子为0.75 , 即当 元素个数 超过 容量长度的0.75倍 时,进行扩容
扩容增量:原容量的 1 倍
如 HashSet的容量为16,一次扩容后是容量为32
Map:
HashMap(线程不安全)
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
......
}
HashMap的父类是AbstractMap ,继承了Map<K,V>Cloneable, Serializable
使用entrySet()获得一个有Key+Value的Set集合 可以使用迭代器iterator()或者foreach()循环遍历
使用value()获得一个Value的值,可以使用迭代器iterator()或者foreach()循环遍历
JDK8中hashmap底层是通过数组+链表+红黑树来实现的
JDK8中的因为使用了红黑树保证了插入和查询了效率,所以实际上JDK8中的Hash算法实现的复杂度降低了
JDK8中数组扩容的条件也发了变化,只会判断是否当前元素个数是否超过了阈值,而不再判断当前put进来的元素对应的数组下标位置是否有值。
JDK7中是先扩容再添加新元素,JDK8中是先添加新元素然后再扩容
HashMap中PUT方法的流程?
- 通过key计算出个hashcode (key%table.length,提高效率key&table.length-1,前提数组长度是2的幂)
- 通过hashcode与数组表长与操作计算出一个数组下标
- 在把put进来的key,value封装为一个node对象
- 判断数组下标对应的位置,是不是空,如果是空则把node直接存在该数组位置
- 如果该下标对应的位置不为空,则需要把node插入到链表中
- 并且还需要判断该链表中是否存在相同的key,如果存在,则更新value
- 如果是JDK7,则使用头插法
- 如果是JDK8,则会遍历链表,并且在遍历链表的过程中,统计当前链表的元素个数,如果超过8个,则先把链表转变为红黑树,并且把元素插入到红黑树中
JDK8中链表转变为红黑树的条件?
- 链表中的元素的个数为8个或超过8个
- 同时,还要满足当前数组的长度大于或等于64才会把链表转变为红黑树。为什么?因为链表转变为红黑树的目的是为了解决链表过长,导致查询和插入效率慢的问题,而如果要解决这个问题,也可以通过数组扩容,把链表缩短也可以解决这个问题。所以在数组长度还不太长的情况,可以先通过数组扩容来解决链表过长的问题。
HashMap扩容流程是怎样的?
- HashMap的扩容指的就是数组的扩容, 因为数组占用的是连续内存空间,所以数组的扩容其实只能新开一个新的数组,然后把老数组上的元素转移到新数组上来,这样才是数组的扩容
- 在HashMap中也是一样,先新建一个2被数组大小的数组
- 然后遍历老数组上的没一个位置,如果这个位置上是一个链表,就把这个链表上的元素转移到新数组上去
- 在这个过程中就需要遍历链表,当然jdk7,和jdk8在这个实
- 现时是有不一样的,jdk7就是简单的遍历链表上的没一个元素,然后按每个元素的hashcode结合新数组的长度重新计算得出一个下标,而重新得到的这个数组下标很可能和之前的数组下标是不一样的,这样子就达到了一种效果,就是扩容之后,某个链表会变短,这也就达到了扩容的目的,缩短链表长度,提高了查询效率
- 而在jdk8中,因为涉及到红黑树,这个其实比较复杂,jdk8中其实还会用到一个双向链表来维护红黑树中的元素,所以jdk8中在转移某个位置上的元素时,会去判断如果这个位置是一个红黑树,那么会遍历该位置的双向链表,遍历双向链表统计哪些元素在扩容完之后还是原位置,哪些元素在扩容之后在新位置,这样遍历完双向链表后,就会得到两个子链表,一个放在原下标位置,一个放在新下标位置,如果原下标位置或新下标位置没有元素,则红黑树不用拆分,否则判断这两个子链表的长度,如果超过八,则转成红黑树放到对应的位置,否则把单向链表放到对应的位置。
- 元素转移完了之后,在把新数组对象赋值给HashMap的table属性,老数组会被回收到。
为什么HashMap的数组的大小是2的幂次方数?
JDK7的HashMap是数组+链表实现的
JDK8的HashMap是数组+链表+红黑树实现的
当某个key-value对需要存储到数组中时,需要先生成一个数组下标index,并且这个index不能越界。
在HashMap中,先得到key的hashcode,hashcode是一个数字,然后通过 hashcode & (table.length - 1) 运算得到一个数组下标index,是通过与运算计算出来一个数组下标的,而不是通过取余,与运算相比于取余运算速度更快,就是数组的长度得是一个2的幂次方数。2的幂次方数二进制值都是1,10,100,1000.......2的幂次方数-1的二进制值都是11,111,1111......&运算计算出来的值都会小于表长。不然会增加哈希碰撞概率,浪费内存
Hashtable:
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
{
......
}
HashMap的父类是 Dictionary,继承了Map<K,V>Cloneable, Serializable
HashTable类的使用方法和HashMap基本一样
LinkedHashMap:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
......
}
inkedHashMap类继承了HashMap类。
LinkedHashMap类的使用方法和HashMap基本一样
TreeMap:
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
......
}
TreeMap中的元素是有序的,默认的排序规则是按照key的字典顺序升序排序
TreeMap类的使用方法和HashMap基本一样
4类区别:
相同点
1)HashMap、Hashtable、LinkedHashMap、TreeMap都实现了Map接口
2)四者都保证了Key的唯一性,即不允许Key重复
排序
HashMap不保证元素的顺序
Hashtable不保证元素的顺序
LinkHashMap保证FIFO即按插入顺序排序(链表)
TreeMap保证元素的顺序,支持自定义排序规则(树)