整理 Java 中的容器

整理学习的 Java 容器,也是面试中常问的问题。****

一、 Java 容器介绍

Java 容器主要分为 Collection ** 和 Map 两大类。这里主要讲 ListSet** 、Map 这三个容器,这也是常常被一起提到的容器。Queue就自行搜索。

Collection: 存放独立元素的序列。

Map:存放key-value型的元素。

下面展示 Java 容器主要结构:

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 写的存储原理。

参考: hashCode()与 equals() 之间的关系

为什么 IdentityHashMap 不需要,因为其源码使用的是 identityHashCode 方法 。

int h = System.identityHashCode(x);

PS: 参考: hashCode和identityHashCode的区别你知道吗?

而 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中modCount的用法,fail-fast机制 modCount作用

八、总结

待定

九、参考

Java集合框架常见面试题

深入理解Java中的容器

Java类集框架详细汇总-底层

JAVA常见容器

posted @ 2021-04-03 22:41  东郊  阅读(218)  评论(0编辑  收藏  举报