整理 Java 中的容器
作者:互联网
整理学习的 Java 容器,也是面试中常问的问题。****
一、 Java 容器介绍
Java 容器主要分为 Collection
** 和 Map
两大类。这里主要讲 List
、Set
** 、Map
这三个容器,这也是常常被一起提到的容器。Queue
就自行搜索。
Collection: 存放独立元素的序列。
Map:存放key-value型的元素。
下面展示 Java 容器主要结构:
二、List,Set,Map 三者的区别?
-
List
(对付顺序的好帮手) : 存储的元素是有序的、可重复的。 -
Set
(注重独一无二的性质) :存储的元素是无序的、不可重复的。 -
Map
(用 Key 来搜索的专家) :使用键值对(key-value)存储,Key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
三、List 接口
1. ArrayList
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
List 接口的主要实现类,基于数组实现。数组移动,复制(扩容)成本较高,适合随机查找和遍历,不适合插入和删除。线程不安全。
-
默认容量: 10 (即未指定初始容量)
private static final int DEFAULT_CAPACITY = 10;
-
默认扩容方式:50%
int newCapacity = oldCapacity + (oldCapacity >> 1);
2. LinkedList
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。)。 适合数据插入和删除,随机访问和遍历速度比较慢。线程不安全。
另外 LinkedList
实现了 Deque
接口,专门用于操作表头和表尾元素,所以常被用于堆栈、队列和双向队列。
3. Vector
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
List 的旧实现类,也是基于数组实现,但不同的是 Vector
是线程安全的,所以效率比 ArrayList
低。也是适合随机查找和遍历,不适合插入和删除。
-
默认容量:10
public Vector() { this(10); }
-
默认扩容方式:1倍 可以自定义传入扩容容量(
capacityIncrement
),int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
3.1 Stack
public class Stack<E> extends Vector<E>
Stack
,常用于模拟"栈"这种数据结构(LIFO后进先出)。线程安全
四、Set 接口
1. TreeSet
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable
TreeSet
实现了 NavigableSet
接口(JDK1.6,该接口为排序标准接口,是 SortedSet
(JDK1.2)的子类),排序方式有自然排序和定制排序。
底层是 TreeMap
使用 key 来存储,所以值不可重复,且不能为 null,线程不安全
-
默认排序:升序 优先级:数字排序 > 字母排序 > 汉字排序
-
自然排序:被存储的元素需要实现
Comparable
接口,覆写compareTo()
方法 -
定制排序:直接传入
Comparator
,支持 Lambda 表达式。可以不需要实现Comparable
接口参考: Arrays.sort() 的一些用法 中的写法
2. HashSet
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable
Set 接口的主要实现类。底层是 HashMap
,线程不安全,可以存储一个 null 值;
public HashSet() { map = new HashMap<>(); }
因为存储方式,其元素需要重写 hashCode()
方法和 equal()
方法。以保证数据的准确性。
2.1. LinkedHashSet
public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable
- LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但和HashSet不同的是,它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。
- 当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。
- LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时(遍历)将有很好的性能(链表很适合进行遍历)
五、Queue 接口
1. PriorityQueue
public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable
优先队列,其实它并没有按照插入的顺序来存放元素,而是按照队列中某个属性的大小来排列的。故而叫优先队列。
2. ArrayDeque
public class ArrayDeque<E> extends AbstractCollection<E> implements Deque<E>, Cloneable, Serializable
基于数组的双端队列,类似于ArrayList有一个Object[] 数组。
3. LinkedList
上面已写
六 、Map 接口
1. TreeMap
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable
TreeMap 是一个红黑树结构,每个键值对都作为红黑树的一个节点。
TreeMap 存储键值对时,需要根据 key 对节点进行排序(实现了 NavigableSMap
接口(JDK1.6,该接口为排序标准接口,是 SortedMap
(JDK1.2)的子类)),TreeMap 可以保证所有的 key-value对处于有序状态。
同时,TreeMap 也有两种排序方式:自然排序、定制排序(参考上面的重写 CompareTo() 方法)
key 不能为 null,value 可以为 null
2. HashMap
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
JDK1.8 之前 HashMap
底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同(通过 equals() 方法),如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
从JDK1.8 之后 hash 方法更加简化,原理不变,但扰动次数减少。
JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法
因为头插法扩容时会造成死链,参考:hashmap扩容时死循环问题
HashMap
线程不安全,线程安全可以使用 ConcurrentHashMap
。
HashMap
可以存储 null 的 key 和 value,但 null 作为键( key )只能有一个,null 作为值( value )可以有多个;
-
阈值: 8
static final int TREEIFY_THRESHOLD = 8;
-
数组 最小树形化阈值:64
static final int MIN_TREEIFY_CAPACITY = 64;
-
链表的树形化阈值:8
static final int TREEIFY_THRESHOLD = 8;
-
红黑树链化阙值:6
static final int UNTREEIFY_THRESHOLD = 6;
-
默认初始容量:16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
-
默认负载因子:0.75f 参考: Load Factor in HashMap in Java with Examples
static final float DEFAULT_LOAD_FACTOR = 0.75f;
-
数组扩容机制: 创建一个为原先2倍的数组,然后对原数组进行遍历以及 rehash ;
2.1 LinkedHashMap
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
LinkedHashMap也使用双向链表来维护 key-value 对的次序,该链表负责维护 Map 的迭代顺序,与 key-value 对的插入顺序一致(注意和 TreeMap 对所有的 key-value 进行排序区分)。同时允许保存的 Key 或 value 内容为 null 。
3. HashTable
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable
底层是 数组+链表, 线程安全,不允许有 null 键和 null 值,否则会抛出 NullPointerException
。
-
扩容机制: 创建一个原长的 2n+1 容量的数组,使用头插法将链表进行反序。
int newCapacity = (oldCapacity << 1) + 1;
-
默认初始容量:11 默认负载因子:0.75f
public Hashtable() { this(11, 0.75f); }
4. WeakHashMap
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>
WeakHashMap 的 key 只保留了对实际对象的弱引用,这意味着如果 WeakHashMap 对象的 key 所引用的对象没有被其他强引用变量所引用,则这些 key 所引用的对象可能被垃圾回收,当垃圾回收了该 key 所对应的实际对象之后,WeakHashMap 也可能自动删除这些 key 所对应的 key-value 对。
支持 null 值和 null 键 HashMap
类相似的性能特征
,具有初始容量(16)和负载因子(0.75f)的相同效率参数 。
线程不安全,无序的
PS: 更多参考:一文搞懂WeakHashMap工作原理
5. IdentityHashMap
public class IdentityHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, java.io.Serializable, Cloneable
IdentityHashMap 的实现不同于 HashMap,虽然也是数组不过 IdentityHashMap 中没有用到链表,解决冲突的方式是计算下一个有效索引,并且将数据 key 和value 紧挨着存在 map 中,即table[i]=key、table[i+1]=value 。
IdentityHashMap 允许 key、value 都为 null,当 key 为 null 的时候,默认会初始化一个 Object 对象作为 key;
线程不安全,无序的
PS: 更多参考:深入浅出分析 IdentityHashMap
七、补充
1. 使用哪些容器时需要重写 hashcode() 和 equals() , 为什么?
以上所述中 HashSet , LinkedHashSet , Hashmap , LinkedHashMap , WeakHashMap 需要重写 hashcode() 和 equal() 方法。原因看 HashMap 写的存储原理。
为什么 IdentityHashMap 不需要,因为其源码使用的是 identityHashCode 方法 。
int h = System.identityHashCode(x);
而 TreeSet , TreeMap 通过比较器比较元素,即重写 Comparable 接口的 compareTo() 方法或者是传入实现好的 Comparator 接口,具体看上面的 TreeSet 。
2. NavigableSet , NavigableMap 及 SortedSet,SortedMap 是什么?
NavigableSet 扩展了 SortedSet,具有了为给定搜索目标报告最接近匹配项的导航方法。方法 lower、floor、ceiling 和 higher 分别返回小于、小于等于、大于等于、大于给定元素的元素,如果不存在这样的元素,则返回 null。类似地,方法 lowerKey、floorKey、ceilingKey 和 higherKey 只返回关联的键。所有这些方法是为查找条目而不是遍历条目而设计的。
SortedSet,SortedMap 这两个接口提供排序操作,实现他们的子类都具有接口中定义的功能。Set和Map本身不具备排序功能,提供了SortedMap和SortedSet接口之后可以在提供排序方案的同时,增加更多的获取集合特定位置元素的方法。类似:结合的第一个元素,最后一个元素,位于特定元素之间的元素等。
更多参考:Java集合之NavigableMap与NavigableSet接口 和 Java 集合SortedSet&SortedMap讲解
3. RandomAccess 接口 (摘自 JavaGuide)
RandomAccess
是标记型的接口, 标识实现这个接口的类具有随机访问功能。
在 binarySearch()
方法中,它要判断传入的 list 是否 RamdomAccess
的实例,如果是,调用indexedBinarySearch()
方法,如果不是,那么调用iteratorBinarySearch()
方法
ArrayList
实现了 RandomAccess
接口, 而 LinkedList
没有实现。为什么呢?我觉得还是和底层数据结构有关!ArrayList
底层是数组,而 LinkedList
底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,ArrayList
实现了 RandomAccess
接口,就表明了他具有快速随机访问功能。 RandomAccess
接口只是标识,并不是说 ArrayList
实现 RandomAccess
接口才具有快速随机访问功能的!
4. Cloneable 接口
Cloneable
是标记型的接口,它们内部都没有方法和属性,实现 Cloneable
来表示该对象能被克隆,能使用 Object.clone()
方法。如果没有实现 Cloneable
的类对象调用 clone()
就会抛出 CloneNotSupportedException
。
更多参考:Java中Cloneable的使用
5. modCount 是什么,什么作用
modCount 是这个集合被结构性修改的次数。
modCount主要是为了防止在迭代过程中某些原因改变了原集合,导致出现不可预料的情况,从而抛出并发修改异常( ConcurrentModificationException
),这可能也与Fail-Fast机制有关
八、总结
待定
九、参考
标签:容器,Java,Cloneable,元素,接口,链表,key,整理,public 来源: https://www.cnblogs.com/dongjiao/p/14615233.html