一、集合介绍
1.为什么需要集合(Collection)
- Java是一门面向对象的语⾔,就免不了处理对象
- 为了⽅便操作多个对象,那么我们就得把这多个对象存储起来
- 想要存储多个对象(变量),很容易就能想到⼀个容器
- 常⽤的容器我们知道有-->StringBuffered,数组(虽然有对象数组,但是数组的⻓度是不可变的)
- 所以,Java就为我们提供了集合(Collection)~
2.数组和集合的区别
- 长度的区别: 数组的⻓度固定,集合的⻓度可变
- 元素的数据类型:
数组可以存储基本数据类型,也可以存储引⽤类型
集合只能存储引⽤类型(你存储的是简单的int,它会⾃动装箱成Integer)
3.集合大致结构体系
如果是集合类型,有List和Set供我们选择。List的特点是插⼊有序的,元素是可重复的。Set的特
点是插⼊⽆序的,元素不可重复的。⾄于选择哪个实现类来作为我们的存储容器,我们就得看具体
的应⽤场景。是希望可重复的就得⽤List,选择List下常⻅的⼦类。是希望不可重复,选择Set下常
⻅的⼦类
如果是Key-Value型,那我们会选择Map。如果要保持插⼊顺序的,我们可以选择
LinkedHashMap,如果不需要则选择HashMap,如果要排序则选择TreeMap
4.迭代器(Iterator)介绍
遍历集合(Collection)的元素都可以使⽤Iterator,它的具体实现是在ArrayList以内部类的⽅式实现的
Iterator也是一个接口,它只有三个方法:
•hasNext()
•next()
•remove()
Collection c=new ArrayList();
c.add("Hello");
Iterator it=c.iterator();//通过集合获取迭代器对象
while(it.hasNext()){//通过hasNext()方法判断集合是否有元素
String s=(String)in.next();通过next()方法获取元素并移动到下一个位置
System.out.printyln(s);
}
二、List集合
List集合的特点就是:有序(存储顺序和取出顺序⼀致),可重复
List集合常⽤的⼦类有三个:
•ArrayList
底层数据结构是数组,线程不安全
•LinkedList
底层数据结构是链表,线程不安全
•Vector [ˈvɛktə]
底层数据结构是数组,线程安全
1.ArrayList
ArrayList底层其实就是⼀个数组,ArrayList中有扩容这么⼀个概念,正因为它扩容,所以它能够实现“动态”增长
2.ArrayList的方法
add(E e)
⾸先检查数组的容量是否⾜够 :确认list容量,尝试容量加1,看看有⽆必要
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
--足够:直接添加
--不足够:扩容
•扩容到原来的1.5倍 grow( )、copyOf( )
•第一次扩容后,如果容量还是小于minCapacity,就将容量扩充为minCapacity
add(int index, E element)
步骤:
•检查角标是否越界
•空间检查,如果有需要进行扩容
•插入元素
我们发现,与扩容相关ArrayList的add⽅法其底层都是arraycopy( )来实现的
get(int index)⽅法
步骤:
•检查⻆标
•返回元素
set(int index,E element)⽅法
步骤:
•检查⻆标
•替代元素
•返回旧值
remove(int index)⽅法
步骤:
•检查⻆标
•删除元素
•计算出需要移动的个数,并移动
•设置为null,让GC(Garbage Collection)回收
3.Vector与ArrayList区别
Vector是jdk1.2的类,属于⽐较⽼旧的⼀个集合类。
Vector底层也是数组,与ArrayList最⼤的区别就是:同步(线程安全) synchronized['sɪŋkrənaɪzd]
还有一个区别:ArrayList在底层数组不够⽤时在原来的基础上扩展0.5倍,Vector是扩展1倍。
4.LinkedList集合
LinkedList底层是双向链表。LinkedList实现了Deque接口,因此,我们可以操作LinkedList像操作队列和栈一样
5.LinkedList的方法
add(E e)
实际上就是往链表的最后添加元素
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
remove(Object o)⽅法
get(int index)方法
返回当前链表中第index个节点的对象
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
set(int index,E element)⽅法
set⽅法和get⽅法其实差不多,根据下标来判断是从头遍历还是从尾遍历
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
6.List集合总结
ArrayList:
•底层实现是数组
•ArrayList的默认初始化容量是10,每次扩容时候增加原先容量的一半,也就是变为原来的1.5倍
• 在增删时候,需要数组的拷贝复制用到arraycopy( )(该方法是由C/C++来编写的)
LinkedList:
•底层实现是双向链表(双向链表方便实现往前遍历)
Vector:
•底层是数组,现在已少用,被ArrayList替代,原因有两个:
--Vector所有方法都是同步,有性能损失
--Vector初始化容量length是10,超过length时,扩容时变成原先容量的2倍,相比于ArrayList消耗更多内存
总的来说:查询多用ArrayList,增删多用LinkedList
ArrayList增删慢不是绝对的(在数量大的情况下,已测试):
• 如果增加元素一直是使用add()(增加到末尾)的话,那是ArrayList要快
• 一直删除末尾的元素也是ArrayList要快(不用复制移动位置)
• 至于如果删除的是中间的位置的话,还是LinkedList要快
但一般来说:增删多还是用LinkedList,因为上面的情况是极端的~
三、Map集合
1.预备知识
1.1Map与Collection的区别:
•Map集合存储的元素是成对出现的,Map的键是唯一的,但值是可以重复的
•Collection集合存储的元素是单独出现的
1.2常用的Map功能
1.3散列表
•链表和数组都可以按照人们的意愿来排列元素的次序,他们可以说是有序的(存储的顺序和取出的顺序是一致的)
•但同时,这会带来缺点:想要获取某个元素,就要访问所有的元素,直到找到为止
•这会让我们消耗很多的时间在里边遍历访问元素~
而还有另外的一些存储结构:不在意元素的顺序,能够快速的查找元素的数据,其中就有一种非常常见的:散列表
散列表的工作原理:
散列表为每个对象计算出⼀个整数,称为散列码。根据这些计算出来的整数(散列码)保存在对应的位置上。在Java中,散列表用的是链表数组实现的,每个列表称之为桶
散列(哈希)冲突
⼀个桶上可能会遇到被占⽤的情况(hashCode散列码相同,就存储在同⼀个位置上),这种情况是⽆法避免的,这种现象称之为:散列冲突
•在JDK1.8中,桶满时会将链表转成红黑树
解决散列(哈希)冲突的2种方法:开放寻址法、链表法
扩容:
如果散列表太满,是需要对散列表再散列即扩容,创建一个桶数更多的散列表,并将原有的元素插入到新表中,丢弃原来的表
•装填因子(load factor)决定了何时对散列表扩容
•装填因子默认为0.75,如果表中超过了75%的位置已经填入了元素,那么这个表就会用双倍的桶数自动进行扩容
2.HashMap剖析
• 底层是由散列表(哈希表)+红黑树实现
•初始容量为16,装载引子默认0.75,每次扩容2倍
•允许为null,存储无序
•非同步
•散列表容量大于64且链表大于8时,转成红黑树
2.1 HashMap构造方法
HashMap的构造方法有4个:
在上面的构造方法最后一行,发现调用了tableSizeFor();
•threshold这个成员变量是阈值,决定了是否要将散列表扩容。它的值应该是:capacity * load factor,其实这⾥仅仅是⼀个初始化,当创建哈希表的时候,它会重新赋值的
put方法
put方法可以说是HashMap的核心
Key的哈希值会与该值的高16位做异或运算,增加了随机性,减少了碰撞冲突的可能性
get方法
getNode( ) 的实现:
remove方法
removeNode( )的实现:
2.2 HashMap与Hashtable对比
从存储结构和实现来讲基本上都是相同的。
Hashtable和HashMap的最大的不同是:
• Hashtable是线程安全的
• Hashtable不允许key和value为null
Hashtable是个过时的集合类,不建议在新代码中使用
不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换
2.3 HashMap总结
•在JDK8中HashMap的底层是: 数组+链表(散列表)+红黑树
•在散列表中有装载因子这么一个属性,当装载因子*初始容量小于散列表元素时,该散列表会再散列,扩容2倍
• 装载因子的默认值是0.75,无论是默认值大了还是小了对我们HashMap的性能都不好
--装载因子初始值大了,可以减少散列表再散列(扩容的次数),但同时会导致散列冲突的可能性变大(散列冲突也是耗性能的一个操作,要得操作链表(红黑树)
--装载因子初始值小了,可以减小散列冲突的可能性,但同时扩容的次数可能就会变多
• 初始容量的默认值是16,无论初始大了还是小了,对HashMap都是有影响的
--初始容量过大,那么遍历时我们的速度就会受影响
--初始容量过小,散列表再散列(扩容的次数)可能就变得多,扩容也是一件非常耗费性能的一件事
•从源码上我们可以发现: HashMap并不是直接拿key的哈希值来用的,它会将key的哈希值的高16位进行异或操作,使得我们将元素放入哈希表的时候增加了一定的随机性
•并不是桶子上有8位元素的时候它就能变成红黑树,它得同时满足散列表容量大于64才行
2. LinkedHashMap剖析
•底层是散列表+双向链表
•允许为null,不同步
•插入的顺序是有序的(底层链表致使有序)
•装载因子和初始容量对LinkedHashMap影响是很大
2.1 LinkedHashMap重写的方法
这就印证了我们的LinkedHashMap底层确确实实是散列表和双向链表
从构造方法上我们可以知道:LinkedHashMap默认使用的是插入顺序
put方法
LinkedHashMap和HashMap的put方法是一样的!LinkedHashMap继承于HashMap,LinkedHashMap没有重写HashMap的put方法
get方法
LinkedHashMap提供插入顺序和访问顺序两种,默认是插入顺序,访问顺序如果不重写用处并不大,一般用于扩展
remove方法
在LinkedHashMap中没有重写remove方法,它调用的是父类HashMap的remove()方法,在LinkedHashMap中重写的是:afterNodeRemoval(Node<K,V> e)这个方法
遍历的方法
Set<Map.Entry<K,V>> entrySet()是被重写的了
因为它遍历的是LinkedHashMap内部维护的一个双向链表,所以初始容量对遍历没有影响
2.2 LinkedHashMap总结
•LinkedHashMap大多使用HashMap的API,只不过在内部重写了某些方法,LinkedHashMap⽐HashMap多了⼀个双向链表的维护
•LinkedHashMap可以设置两种遍历顺序:
--插入顺序(insertion-ordered)(默认)
--访问顺序(access-ordered)
对于访问顺序,它是LRU算法的实现,要使用它,要么重写LinkedListMap的几个方法:(removeEldestEntry(Map.Entry<K,V> eldest)和afterNodeInsertion(boolean evict)),要么是扩展成LRUMap来使用,不然设置为访问顺序(access-ordered)的用处不大~
•LinkedHashMap遍历的是内部维护的双向链表,所以说初始容量对LinkedHashMap的遍历是不受影响
的
3. TreeMap剖析
类继承图:
•TreeMap实现了NavigableMap接口,而NavigableMap接口继承着SortedMap接口,致使TreeMap是有序的
•TreeMap底层是红黑树,它方法的时间复杂度log(n)
•非同步
•使用Comparator或者Comparable来比较key是否相等与排序的问题
TreeMap构造方法
可以发现,TreeMap的构造方法大多数与comparator有关
put方法
get方法
remove方法
删除节点的时候调用的是
deleteEntry(Entry<K,V> p)方法,这个方法主要是删除节点并且平衡红黑树
遍历方法
在看源码的时候可能不知道哪个是核心的遍历方法,因为Iterator有非常非常多
•TreeMap遍历是使用EntryIterator 这个内部类的
3.1 TreeMap总结
•TreeMap底层是红黑树,能够实现该Map集合有序
•如果在构造方法中传递了Comparator对象,那么就会以Comparator对象的方法进行比较。否则,则使用Comparable的compareTo()方法来比较
--值得说明的是:如果使用的是compareTo()方法来比较,key一定是不能为null,并且得实现了Comparable接口的
--即使是传入了Comparator对象,不用compareTo()方法来比较,key也是不能为null
•Comparator和Comparable出现的频率是很高的,因为TreeMap实现有序要么就是外界传递进来Comparator对象,要么就使用默认key的Comparable接口(实现自然排序)
1.由于底层是红黑树,那么时间复杂度可以保证为log(n)
2.key不能为null,为null为抛出NullPointException的
3.想要自定义比较,在构造方法中传入Comparator对象,否则使用key的自然排序来进行比较
4.TreeMap非同步的,想要同步可以使用Collections来进行封装
4. ConcurrentHashMap剖析[kənˈkɜrənt]
•JDK1.8 ConCurrentHashMap的底层是散列表+红黑树,与HashMap是一样的
•ConCurrentHashMap支持高并发的访问和更新,它是线程安全的
•检索操作不用加锁,get方法是非阻塞的
•key和value都不允许为null
4.1 JDK1.7ConcurrentHashMap的底层是:segments+HashEntry数组
Segment继承了ReentrantLock,每个片段都有了一个锁,叫做“锁分段”
4.2 有了Hashtable为啥需要ConCurrentHashMap
•Hashtable是在每个方法上都加上了Synchronized完成同步,效率低下
•ConcurrentHashMap通过在部分加锁和利用CAS算法来实现同步
4.3 CAS算法和volatile简单介绍
CAS(比较与交换,Compare and swap)是一种有名的无锁算法
CAS有3个操作数:
1、内存值V
2、旧的预期值A
3、要修改的新值B
•当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做
•当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值(A和内存值V相同时,将内存值V修改为B),而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试
总结:(CAS算法)先比较是否相等,如果相等则替换
4.4 volatile关键字[ˈvɒlətʌɪl]
volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性
•保证该变量对所有线程的可见性:
--在多线程的环境下:当这个变量修改时,所有的线程都会知道该变量被修改了,也就是所谓的“可见性”
•不保证原子性:
--修改变量(赋值)实质上是在JVM中分了好几步,而在这几步内(从装载变量到修改),它是不安全的
4.5 ConCurrentHashMap构造方法
put方法
初始化散列表
initTable();
get方法
get方法是不用加锁的,是非阻塞的
可以发现,Node节点是重写的,设置了volatile关键字修饰,致使它每次获取的都是最新设置的值
4.6 ConCurrentHashMap的核心要点
• 底层结构是哈希表(数组+链表)+红黑树,这一点和HashMap是一样的
• Hashtable是将所有的方法进行同步,效率低下。而ConcurrentHashMap作为一个高并发的容器,它是通过部分锁定+CAS算法来进行实现线程安全的。CAS算法也可以认为是乐观锁的一种~
• 在高并发环境下,统计数据(计算size…等等)其实是无意义的,因为在下一时刻size值就变化了
• get方法是非阻塞,无锁的。重写Node类,通过volatile修饰next来实现每次获取都是最新设置的值
• ConcurrentHashMap的key和Value都不能为null
四、Set集合
1. HashSet剖析
1.1 归纳HashSet的要点:
• 实现Set接口(无序,元素不可重复)
• 允许元素为null
• 底层实际上是一个HashMap实例
• 非同步
• 初始容量非常影响迭代性能
HashSet实际上就是封装了HashMap,操作HashSet元素实际上就是操作HashMap。这也是面向对象的一种体现
2. TreeSet剖析
2.1 归纳TreeSet的要点
• 实现NavigableSet接口
• 可以实现排序功能
• 底层实际上是一个TreeMap实例
• 非同步
3. LinkedHashSet剖析
归纳LinkedHashSet的要点:
• 迭代是有序的
• 允许为null
• 底层实际上是一个HashMap+双向链表实例(其实就是LinkedHashMap)
• 非同步
• 性能比HashSet差一丢丢,因为要维护一个双向链表
• 初始容量与迭代无关,LinkedHashSet迭代的是双向链表
4. Set集合总结
• 实现了Collection接口
• Set集合特性:无序的,元素不可重复
• 底层大多是Map结构的实现
• 常用的3个子类都是非同步的
HashSet:
--无序,允许为null,底层是HashMap(散列表+红黑树),非线程同步
TreeSet:
--有序,不允许为null,底层是TreeMap(红黑树),非线程同步
LinkedHashSet:
--迭代有序,允许为null,底层是HashMap+双向链表,非线程同步
五、CopyOnWriteArrayList(Set)介绍
一般来说,我们会认为:CopyOnWriteArrayList是同步List的替代品,CopyOnWriteArraySet是同步Set的替代品
JUC(Java5.0提供了java.util.concurrent(简称JUC)包)下支持并发的容器与老一代的线程安全类相比,总结起来就是加锁粒度的问题
• Hashtable、Vector加锁的粒度大(直接在方法声明处使用synchronized)
• ConcurrentHashMap、CopyOnWriteArrayList加锁粒度小(用各种的方式来实现线程安全,比如我们知道的ConcurrentHashMap用了cas锁、volatile等方式来实现线程安全)
• JUC下的线程安全容器在遍历的时候不会抛出ConcurrentModificationException异常
所以一般来说,我们都会使用JUC包下给我们提供的线程安全容器,而不是使用老一代的线程安全容器
1.1 CopyOnWriteArrayList源码注释概括
• CopyOnWriteArrayList是线程安全容器(相对于ArrayList),底层通过复制数组的方式来实现
• CopyOnWriteArrayList在遍历的使用不会抛出ConcurrentModificationException异常,并且遍历的时候不用额外加锁
• 元素可以为null
CopyOnWriteArrayList底层就是数组,加锁就交由ReentrantLock来完成
1.2 常见方法的实现
我们知道如果遍历Vector/SynchronizedList是需要自己手动加锁的,CopyOnWriteArrayList使用迭代器遍历时不需要显示加锁
add( )方法
size( )方法
get( )方法
set( )方法
结论:
• 在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组交由array变量指向
• 写加锁,读不加锁
1.3 为什么遍历时不用调用者显式加锁
看一下CopyOnWriteArrayList的迭代器:
可以发现,CopyOnWriteArrayList在使用迭代器遍历的时候,操作的都是原数组!
1.4 CopyOnWriteArrayList缺点
• 内存占用:
--如果CopyOnWriteArrayList经常要增删改里面的数据,经常要执行add()、set()、remove()的话,那是比较耗费内存的
因为我们知道每次add()、set()、remove()这些增删改操作都要复制一个数组出来。
• 数据一致性:
--CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性
从上面的例子也可以看出来,比如线程A在迭代CopyOnWriteArrayList容器的数据。线程B在线程A迭代的间隙中将CopyOnWriteArrayList部分的数据修改了(已经调用setArray()了)。但是线程A迭代出来的是原有的数据。
1.5 CopyOnWriteSet
CopyOnWriteArraySet的原理就是CopyOnWriteArrayList