java集合详解
Posted ~无关风月~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java集合详解相关的知识,希望对你有一定的参考价值。
List接口的实现类
ArrayList
非线程安全,同样使用Fail-Fast机制
允许包括 null 在内的所有元素
其内部实现也是数组。删除元素会将后边位置的元素向前移动一位,最后一个置为null。当被添加的元素超出数组的容纳极限时,ArrayList会对内部数组进行一次“扩容”,从而可以添加新的元素,每次数组容量的增长大约是其原容量的1.5倍, 数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中。 应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,以减少递增式再分配的数量。
transient Object[] elementData;
transient
用来表示一个域不是该对象序行化的一部分,但是ArrayList又是可序行化的类,elementData 是 ArrayList具体存放元素的成员,为什么用 transient
修饰?
ArrayList 在序列化的时候会调用 writeObject,直接将 size 和 element写入 ObjectOutputStream;反序列化时调用 readObject,从 ObjectInputStream获取 size 和 element,再恢复到 elementData。为什么不直接用 elementData 来序列化,而采用上诉的方式来实现序列化呢?原因在于 elementData 是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
LinkedList
LinkedList 是非同步的。
底层的数据结构是基于双向循环链表。
按下标访问元素—get(i)/set(i,e) 要悲剧的遍历链表将指针移动到位(如果i>数组大小的一半,会从末尾移起)。节点访问的复杂度由O(n)变为O(n/2)。
插入、删除元素时修改前后节点的指针即可,但还是要遍历部分链表的指针才能移动到下标所指的位置,只有在链表两头的操作—add(), addFirst(),removeLast() 或 用iterator()上的 remove()能省掉指针的移动。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
...
LinkedList 是一个继承于AbstractSequentialList
的 双向循环链表。它也可以被当作栈、队列 或 双端队列 进行操作。
访问和操作链表头尾元素块,随机访问慢,插入删除不需要移动元素,快。
LinkedList 实现 List
接口,能对它进行队列操作。
queue.peek(); 获取头元素,不移除
queue.poll(); 获取头元素,移除
LinkedList 实现 Deque
接口,即能将LinkedList当作双端队列使用。
deque.getFirst();
deque.getLast();
deque.pollLast();
LinkedList 实现了Cloneable
接口,即覆盖了函数clone(),能克隆。
LinkedList 实现 java.io.Serializable
接口,这意味着 LinkedList支持序列化,能通过序列化去传输。
Vector
Vector是一个线程安全的列表,采用数组实现。线程安全的实现方式是对所有操作都加上了synchronized关键字,这种方式严重影响效率,因此,不再推荐使用Vector。
Set接口的实现类
HashSet
插入性能高,散列法机制,不保证顺序
使用null元素
基于HashMap实现的,HashSet底层使用HashMap来保存所有元素
HashSet中的元素,只是存放在了底层HashMap的key上,value使用一个static final的Object对象标识
正确重写其equals和hashCode方法,以保证放入的对象的唯一性
LinkedHashSet
此链接列表定义了迭代顺序,该迭代顺序可为插入顺序或是访问顺序。
线程不安全
继承自HashSet
基于LinkedHashMap来实现,哈希表和链接列表实现
TreeSet
TreeSet 底层基于 TreeMap实现。
TreeSet 添加元素的时候,如果元素本身不具备自然顺序的特性,那么该元素所属的类必须要实现Comparable接口,把元素的比较规则定义在compareTo(T o)方法上(自然排序),或必须要在创建TreeSet的时候传入一个比较器(定制排序)。
Map接口的实现类
HashMap
快速查找
基于哈希表的 Map 接口的实现
允许使用 null 值和 null 键
不保证映射的顺序
数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的
Node[] table,即哈希桶数组
Node这个内部类的属性有 hash值,key,value和指向下一个Node结点的next指针。
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V>
final int hash;
final K key;
V value;
Node<K,V> next;
HashMap就是使用哈希表来存储的,采用了链地址法解决冲突。就是数组加链表的结合。
在每个数组元素上都有一个链表结构,当数据被Hash后,与数组长度取模,得到数组下标,把数据放在对应下标元素的链表上。JDK1.7之前是直接将hash冲突的Node插入链表头,如果hash冲突较多,链表过长,会严重影响HashMap的性能。
在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能。
HashMap有 threshold,loadFactor,modCount,size 等属性。
transient Node<K,V>[] table;
transient Set<Map.Entry<K,V>> entrySet;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
public HashMap(int initialCapacity, float loadFactor)...
public HashMap(int initialCapacity)...
Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold = length * Load factor,数据量超过threshold 就重新 2倍resize(扩容)
size这个字段其实就是HashMap中实际存在的键值对数量。
modCount字段主要用来记录HashMap内部结构发生变化的次数,主要用于迭代的快速失败,(添加新元素,某个key对应的value值被覆盖不属于结构变化)。
Fail-Fast 机制:HashMap不是线程安全的,如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException。通过 modCount域,记录修改的次数。迭代器初始化过程中会将这个值赋给迭代器的expectedModCount, 在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map,就抛出异常。
为什么HashMap底层数组的长度总是2的n次方,如果指定大小不符合条件时,capacity 进行移位运算,到刚好大于指定大小的那个2的N次幂的数?
1、当length总是2的n次方时,h & (length-1)
运算等价于对length
取模,也就是h % length
,用位运算代替取模运算,用空间换了时间,速度更快。
2、扩容为原来2倍时,需要使用一个新的数组代替已有的容量小的数组,不需要像JDK1.7的实现那样重新模运算,只需要看看原来的hash值对应新数组长度减一的二进制新增的那一位上是1还是0就好了,是0的话索引没变,是1的话索引变成对原位置+老数组长度。
n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希值(也就是根据key1算出来的hashcode值)与高位与运算的结果。
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。
扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。
LinkedHashMap
它是一个将所有Entry节点链入一个双向链表的 HashMap,LinkedHashMap 是 HashMap的子类。
继承HashMap,底层使用哈希表与双向链表来保存所有元素。
重新定义了数组中保存的元素Entry,该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而在哈希表的基础上又构成了双向链接列表。
迭代顺序默认为插入顺序,private final boolean accessOrder; 为true的时候,可以将最近访问的元素双向链表的尾部。
所有 put 进来的 Entry 都保存在HashMap中,但由于它又额外定义了一个以head为头结点的空的双向链表,因此对于每次put进来Entry还会将其插入到双向链表的尾部。
LinkedHashMap还可以用来实现LRU (Least recently used, 最近最少使用)算法
当accessOrder为true时,get方法和put方法都会调用recordAccess方法使得最近使用的Entry移到双向链表的末尾;当accessOrder为默认值false时,从源码中可以看出recordAccess方法什么也不会做。
双向链表的第一个有效节点(header后的那个节点)为最近最少使用的节点。
使用LinkedHashMap实现LRU算法:
重写removeEldestEntry()方法
@Override
protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest)
// TODO Auto-generated method stub
if(size() > 6)
return true;
return false;
Hashtable
线程安全,put方法是synchronized,不允许有null值和键,其他同HashMap
ConcurrentHashMap
同步容器类是java5 新增的一个线程安全的哈希表,效率介于 HashMap 和 Hashtable之间。get操作无锁,put操作采用“锁分段”机制,粒度小于Hashtable,所以整体性能优于Hashtable。不允许有 null值 和 键。
在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设为16),及任意数量线程的读操作。
ConcurrentHashMap 本质上是一个Segment数组,充当锁的角色,每一个Segment实例又包含若干个桶,每个桶中都包含一条由若干个 HashEntry 对象链接起来的链表。总的来说,ConcurrentHashMap 的高效并发机制是通过以下三方面来保证的:
- 通过锁分段技术保证并发环境下的写操作;
- 通过 HashEntry的不变性、Volatile变量的内存可见性和加锁重读机制保证高效、安全的读操作;
- 通过不加锁和加锁两种方案控制跨段操作的安全性。
Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。
ConcurrentHashMap 允许多个修改(写)操作并发进行,其关键在于使用了锁分段技术,它使用了不同的锁来控制对哈希表的不同部分进行的修改(写),不影响客户端对其它段的访问。
HashEntry用来封装具体的键值对,是个典型的四元组。与HashMap中的Entry类似,HashEntry也包括同样的四个域,分别是key、hash、value 和 next。不同的是,在 HashEntry类中,key,hash 和 next域都被声明为 final的,value域被volatile所修饰,因此HashEntry对象几乎是不可变的,这是 ConcurrentHashmap读操作并不需要加锁的一个重要原因。
根据 key的 hash值的高 n位就可以确定元素到底在哪一个Segment中。
ConcurrentHashMap 的跨段操作,比如说size操作、containsValaue操作等,Segment里的全局变量 count是一个volatile变量。size方法主要思路是先在没有锁的情况下对所有段大小求和,这种求和策略最多执行RETRIES_BEFORE_LOCK次(默认是两次):在没有达到RETRIES_BEFORE_LOCK之前,求和操作会不断尝试执行(这是因为遍历过程中可能有其它线程正在对已经遍历过的段进行结构性更新);在超过RETRIES_BEFORE_LOCK之后,如果还不成功就在持有所有段锁的情况下再对所有段大小求和。ConcurrentHashMap根据Segment里的modCount 成员变量(这个变量在put、remove、clean操作都会加1),前后是否改变来得知容器的大小是否发生变化。
TreeMap
比HashMap更强大。
实现了 SortedMap 接口,可以对元素排序。
采用一种被称为“红黑树”的排序二叉树(链式存储)来保存 Map 中每个 Entry —— 每个 Entry 都被当成“红黑树”的一个节点对待。可以保证当需要快速检索指定节点。
TreeMap是唯一的带有subMap()方法的Map,它可以返回一个子树。
PriorityQueue
默认容量 11,最大容量 Integer.MAX_VALUE - 8;
基于二叉堆来实现优先队列,queue[i]的子节点为queue[2i+1]/queue[2i+2];
数组实现 transient Object[] queue;
并发容器类
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
SynchronizedList 对部分操作加上了synchronized关键字以保证线程安全。但其 iterator()操作还不是线程安全的。
CopyOnWriteArrayList的写操作性能较差,读操作性能较好。适用于以读为主,如缓存。
Collections.synchronizedList 写操作性较好,读操作因为是采用了synchronized关键字的方式,较差。应用在读写操作都比较均匀的地方,如同步列表。
SynchronousQueue:是一种无缓冲的等待队列,是一个缓存值为1的阻塞队列。
LinkedBlockingQueue:是一个无界缓存的等待队列。内部维持着一个数据缓冲队列(该队列由链表构成)。只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒。对于生产者端和消费者端分别采用了独立的锁来控制数据同步。
ArrayBlockingQueue:是一个有界缓存的等待队列。队列由数组构成,在生产者放入数据和消费者获取数据,都是共用同一个锁对象。
集合排序
自然排序:
1、需要被排序的类实现Comparable接口
2、重写其中的 compareTo()方法
定制排序:
1、需要一个新的类实现Comparator接口,常用匿名内部类
2、重写其中的Compare 方法
List 类用Collections.sort(List,Comparator)
TreeSet 和 TreeMap 用: o1.get().compareTo(o2.get())
o1.get-o2.get 是升序,相反是降序
HashMap排序,通过map.entrySet() 获得 Entry 集合,外面套List,然后通过Collections.sort(List,Comparator)排序。如果要返回HashMap ,应将排序结果放到 LinkedHashMap 实现类中。
TreeMap默认按键升序排列,如果想降序,使用compare(T o1,To2)方法,按值排序,同HashMap。
集合在迭代过程中,能否添加、删除元素
for 循环可以,增强for 循环不行,注意可以根据元素删,不要根据下标 i 删,因为删除后,当一个元素被删除时,列表的大小缩小并且下标变化,可能会错删。所以虽然不会报错,但不推荐使用for循环删。
Iterator进行遍历的时候不能使用集合本身的add或者remove方法来增减元素。但是使用Iterator的remove方法是可以的
【强制】 ArrayList 的 subList 结果不可强转成 ArrayList ,否则会抛出 ClassCastException 异常,
即 java.util.RandomAccessSubList cannot be cast to java.util.ArrayList
说明: subList 返回的是 ArrayList 的内部类 SubList ,并不是 ArrayList ,而是 ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。
【强制】在 subList 场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均会产生 ConcurrentModificationException 异常。
【强制】使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法,它的 add / remove / clear 方法会抛出 UnsupportedOperationException 异常。
说明: asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。 Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。
String[] str = new String[] “you”, “wu” ;
List list = Arrays.asList(str);
第一种情况: list.add(“yangguanbao”); 运行时异常。
第二种情况: str[0] = “gujin”; 那么 list.get(0) 也会随之修改。
以上是关于java集合详解的主要内容,如果未能解决你的问题,请参考以下文章
集合-Java中Arrays.sort()自定义数组的升序和降序排序