java面试/笔试题目之Java常见集合(持续更新中)

声明:题目大部分来源于Java后端公众号,有些个人整理,但答案皆为个人整理,仅供参考。

目录

Java中的集合

List 和 Set 区别

1.Set:集合中的对象不按特定方式排序(针对内存地址来说,即非线性),并且没有重复对象。它的有些实现类能对集合中的对象按特定方式排序。

2.List:集合中的对象线性方式储存,可以有重复对象,允许按照对象在集合中的索引位置检索对象。有序可重复。

Set和hashCode以及equals方法的联系

List 和 Map 区别

1.Map:通过键值对进行取值,key-value一一对应的,其中key不可以重复,而value可以重复

区别:

Arraylist 与 LinkedList 区别

1.Arraylist(线程不安全):

2.LinkedList(线程不安全):

区别:

ArrayList 与 Vector 区别

1.Vector(线程安全):

区别:

HashMap 的工作原理及代码实现,什么时候用到红黑树

1.HashMap(线程不安全,基于jdk1.7):

注意:

2.Hashtable(线程安全):

HashMap 和 Hashtable 的区别:

HashSet 和 HashMap 区别:

1.HashSet(线程不安全):

区别:

ConcurrentHashMap 的工作原理及代码实现,如何统计所有的元素个数

1.ConcurrentHashMap(线程安全):

总结

HashMap 和 ConcurrentHashMap 的区别

多线程情况下HashMap死循环的问题

介绍一下LinkedHashMap

HashMap出现Hash DOS攻击的问题

手写简单的HashMap

看过那些Java集合类的源码

什么是快速失败的故障安全迭代器?

Iterator和ListIterator的区别

什么是CopyOnWriteArrayList,它与ArrayList有何不同?

迭代器和枚举之间的区别

总结:


 

Java中的集合

Java中的集合主要分为value,key-value(Collection,Map)两种,存储值分为List和Set,存储为key-value得失Map。

Collection接口中主要有这些方法:

boolean add(Object o)  :向集合中加入一个对象的引用   
void clear():删除集合中所有的对象,即不再持有这些对象的引用  
boolean isEmpty()    :判断集合是否为空   
boolean contains(Object o) : 判断集合中是否持有特定对象的引用   
Iterartor iterator()  :返回一个Iterator对象,可以用来遍历集合中的元素   
boolean remove(Object o) :从集合中删除一个对象的引用   
int size()       :返回集合中元素的数目   
Object[] toArray()    : 返回一个数组,该数组中包括集合中的所有元素
boolean equals(Object o):判断值是否相等
int hashCode(): 返回当前集合的hash值,可以作为判断地址是否想相等

Collection接口继承 Iterable<T> 接口,这个接口可以返回一个迭代器,主要有一下三个方法:

List和Set都是继承Collection接口。

List 和 Set 区别

1.Set:集合中的对象不按特定方式排序(针对内存地址来说,即非线性),并且没有重复对象。它的有些实现类能对集合中的对象按特定方式排序。

  1. 不允许重复对象,只允许一个 null 元素,根据equals和hashcode判断,一个对象要存储在set中,必须重写equals和hashcode方法;
  2. 无序容器,你无法保证每个元素的存储顺序,TreeSet通过 Comparator  或者 Comparable 维护了一个排序顺序。
  3. Set 接口最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基于 HashMap 实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其 compare() 和 compareTo() 的定义进行排序的有序容器。

2.List:集合中的对象线性方式储存,可以有重复对象,允许按照对象在集合中的索引位置检索对象。有序可重复。

  1. 可以允许重复的对象,可以插入多个null元素。

  2. 是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。

  3. 常用的实现类有 ArrayList、LinkedList 和 Vector。ArrayList 最为流行,它提供了使用索引的随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适。

Set和hashCode以及equals方法的联系

因为set接口中是不允许存在重复的对象或者值的,所以需要对存入set中的对象或者值进行判断,而hashCode和equals就是用来对这些对象和值进行判断的。

List 和 Map 区别

1.Map:通过键值对进行取值,key-value一一对应的,其中key不可以重复,而value可以重复

区别:

  • Map用 put(k,v) / get(k),还可以使用containsKey()/containsValue()来检查其中是否含有某个key/value。
  • List通过get()方法来一次取出一个元素。使用数字来选择一堆对象中的一个,get(0)...。(add/get)
  • Collection没有get()方法来取得某个元素。只能通过iterator()遍历元素。

Arraylist 与 LinkedList 区别

1.Arraylist(线程不安全):

  • 底层是数组(数组在内存中是一块连续的内存,如果插入或删除元素需要移动内存),可以插入空数据
    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    

    实现了 RandomAccess 接口,所以支持随机访问

    private static final int DEFAULT_CAPACITY = 10;

    数组的默认大小为 10。

  • 插入数据的时候,会先进行扩容校验,添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍。

    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    • 首先进行扩容校验。
    • 将插入的值放到尾部,并将 size + 1 。
  • 如果是调用 add(index,e) 在指定位置添加的话:
    public void add(int index, E element) {
            rangeCheckForAdd(index);
    
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            //复制,向后移动
            System.arraycopy(elementData, index, elementData, index + 1,
                             size - index);
            elementData[index] = element;
            size++;
        }
    • 也是首先扩容校验。
    • 接着对数据进行复制,目的是把 index 位置空出来放本次插入的数据,并将后面的数据向后移动一个位置。
      private void grow(int minCapacity) {
              // overflow-conscious code
              int oldCapacity = elementData.length;
              int newCapacity = oldCapacity + (oldCapacity >> 1);
              if (newCapacity - minCapacity < 0)
                  newCapacity = minCapacity;
              if (newCapacity - MAX_ARRAY_SIZE > 0)
                  newCapacity = hugeCapacity(minCapacity);
              // minCapacity is usually close to size, so this is a win:
              elementData = Arrays.copyOf(elementData, newCapacity);
          }

      扩容最终调用的代码,也是一个数组复制的过程。由此可见 ArrayList 的主要消耗是数组扩容以及在指定位置添加数据,在日常使用时最好是指定大小,尽量减少扩容。更要减少在指定位置插入数据的操作。

  • 删除元素

    public E remove(int index) {
        rangeCheck(index);
        modCount++;
        E oldValue = elementData(index);
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index, numMoved);
        elementData[--size] = null; // clear to let GC do its work
        return oldValue;
    }

    需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。

  • 由于 ArrayList 是基于动态数组实现的,所以并不是所有的空间都被使用。因此使用了 transient 修饰,可以防止被自动序列化。

    transient Object[] elementData;

    保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。

     private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException{
            // Write out element count, and any hidden stuff
            int expectedModCount = modCount;
            s.defaultWriteObject();
    
            // Write out size as capacity for behavioural compatibility with clone()
            s.writeInt(size);
    
            // Write out all elements in the proper order.
            //只序列化了被使用的数据
            for (int i=0; i<size; i++) {
                s.writeObject(elementData[i]);
            }
    
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
        }
    
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            elementData = EMPTY_ELEMENTDATA;
    
            // Read in size, and any hidden stuff
            s.defaultReadObject();
    
            // Read in capacity
            s.readInt(); // ignored
    
            if (size > 0) {
                // be like clone(), allocate array based upon size not capacity
                ensureCapacityInternal(size);
    
                Object[] a = elementData;
                // Read in all elements in the proper order.
                for (int i=0; i<size; i++) {
                    a[i] = s.readObject();
                }
            }
        }

    当对象中自定义了 writeObject 和 readObject 方法时,JVM 会调用这两个自定义方法来实现序列化与反序列化。序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。

2.LinkedList(线程不安全):

  • 底层是基于双向链表实现的,(JDK1.7/8 之后取消了循环,修改为双向链表),不要求内存是连续的,在当前元素存放下一个或上一个元素的地址。
  • 每次插入都是移动指针,改变引用指向即可,效率较高;
  • 查询的时候使用二分法,利用了双向链表的特性,如果index离链表头比较近,就从节点头部遍历。否则就从节点尾部开始遍历。使用空间(双向链表)来换取时间。node()会以O(n/2)的性能去获取一个结点;如果索引值大于链表大小的一半,那么将从尾结点开始遍历。
    public E get(int index) {
            checkElementIndex(index);
            return node(index).item;
        }
        
        Node<E> node(int index) {
            // assert isElementIndex(index);
    
            if (index < (size >> 1)) {
                Node<E> x = first;
                for (int i = 0; i < index; i++)
                    x = x.next;
                return x;
            } else {
                Node<E> x = last;
                for (int i = size - 1; i > index; i--)
                    x = x.prev;
                return x;
            }
        }

    这样的效率是非常低的,特别是当 index 越接近 size 的中间值时。

区别:

  • LinkedList 插入,删除都是移动指针效率很高;查找需要进行遍历查询,效率较低。
  • LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。

  • ArrayList是可改变大小的数组,而LinkedList是双向链接串列

  • 在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的

ArrayList 与 Vector 区别

1.Vector(线程安全):

  • 底层也是基于数组实现的,但是add方法的时候使用了synchronized进行同步
    public synchronized boolean add(E e) {
            modCount++;
            ensureCapacityHelper(elementCount + 1);
            elementData[elementCount++] = e;
            return true;
        }
    public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
    
        return elementData(index);
    }
    这样的话,开销比较大,所以 Vector 是一个同步容器并不是一个并发容器。

区别:

  • ArrayList和Vector都采用线性连续存储空间,当存储空间不足的时候,Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。
  • ArrayList线程不安全,Vector线程安全
  • Vector可以设置capacityIncrement,而ArrayList不可以,从字面理解就是capacity容量,Increment增加,容量增长的参数。

HashMap 的工作原理及代码实现,什么时候用到红黑树

1.HashMap(线程不安全,基于jdk1.7):

  • hashmap是无序的,因为每次根据 key 的 hashcode 映射到 Entry 数组上,所以遍历出来的顺序并不是写入的顺序
  • HashMap 底层是基于数组和链表实现的,如图所示,其中两个重要的参数:容量和负载因子;容量的默认大小是 16,负载因子是 0.75,当 HashMap 的 size > 16*0.75 时就会发生扩容(容量和负载因子都可以自由调整)。 
  • 内部包含了一个 Entry 类型的数组 table。

    transient Entry[] table;
    

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
    
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    
        public final K getKey() {
            return key;
        }
    
        public final V getValue() {
            return value;
        }
    
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
    
        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }
    
        public final int hashCode() {
            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }
    
        public final String toString() {
            return getKey() + "=" + getValue();
        }
    }

    Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值相同的 Entry

  • 拉链法的工作原理

    HashMap<String, String> map = new HashMap<>();
    map.put("K1", "V1");
    map.put("K2", "V2");
    map.put("K3", "V3");

    下面的桶对应数组的一个元素,即数组中的每个位置被当成一个桶,一个桶放一个链表。

    • 新建一个 HashMap,默认大小为 16;
    • 插入 <K1,V1> 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。
    • 插入 <K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。
    • 插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 前面。
    • 应该注意到链表的插入是以头插法方式进行的,例如上面的 <K3,V3> 不是插在 <K2,V2> 后面,而是插入在链表头部。
    • 查找需要分成两步进行:

      • 计算键值对所在的桶;
      • 在链表上顺序查找,时间复杂度显然和链表的长度成正比。
  • put方法:首先会将传入的 Key 做hash运算计算出 hashcode,然后根据数组长度取模计算出在数组中的 index 下标。

    由于在计算中位运算比取模运算效率高的多,所以 HashMap 规定数组的长度为 2^n 。这样用 2^n - 1 做位运算与取模效果一致,并且效率还要高出许多。

    由于数组的长度有限,所以难免会出现不同的 Key 通过运算得到的 index 相同,这种情况可以利用链表来解决,HashMap 会在 table[index]处形成链表,采用头插法将数据插入到链表中。

    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        // 键为 null 单独处理
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        // 确定桶下标
        int i = indexFor(hash, table.length);
        // 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
    
        modCount++;
        // 插入新键值对
        addEntry(hash, key, value, i);
        return null;
    }

    HashMap 允许插入键为 null 的键值对。但是因为无法调用 null 的 hashCode() 方法,也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。

    private V putForNullKey(V value) {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

    使用链表的头插法,也就是新的键值对插在链表的头部,而不是链表的尾部。

    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
    
        createEntry(hash, key, value, bucketIndex);
    }
    
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        // 头插法,链表头部指向新的键值对
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }
    

    resize是扩容,默认扩为原来的2倍大小。

注意:

  • 在并发环境下使用 HashMap 容易出现死循环。

  • 并发场景发生扩容,调用 resize() 方法里的 rehash() 时,容易出现环形链表。这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标时就会出现死循环。

  • 在 JDK1.8 中对 HashMap 进行了优化: 当 hash 碰撞之后写入链表的长度超过了阈值(默认为8),链表将会转换为红黑树。假设 hash 冲突非常严重,一个数组后面接了很长的链表,此时重新的时间复杂度就是 O(n) 。如果是红黑树,时间复杂度就是 O(logn) 。

2.Hashtable(线程安全):

  • 也是实现了Map接口,底层是链表和数组;
  • 继承了Dictionary<K,V>
  • Hashtable的synchronized是对整张hash表进行锁定即让线程独享整张hash表,在安全同时造成了浪费。

HashMap 和 Hashtable 的区别:

  • HashMap线程不安全,Hashtable因为很多地方加了synchronized,所以它是线程安全的;
  • HashTable使用Enumeration,HashMap 使用Iterator。
  • HashMap不能保证元素的顺序,HashMap能够将键设为null,也可以将值设为null,但是只有一个键为null,值可以多个为null,当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。Hashtable不能将键和值设为null,否则运行时会报空指针异常错误;
  • hash值的使用方式不同,Hashtable直接使用对象的hashCode,对table数组的长度直接进行取模;而HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取模;
  • Hashtable
    
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    
    HashMap
    
    static int hash(int h) {
         h ^= (h >>> 20) ^ (h >>> 12);
         return h ^ (h >>> 7) ^ (h >>> 4);
     }
    
    static int indexFor(int h, int length) {
         return h & (length-1);
     }
  • HashMap没有contains方法,而Hashtabl有contains方法。
    //以下是Hashtable的方法
    public synchronized boolean contains(Object value)
    public synchronized boolean containsKey(Object key)
    public boolean containsValue(Object value)
    
    //以下是HashMap中的方法,注意,没有contains方法
    public boolean containsKey(Object key)
    public boolean containsValue(Object value)
    
  • Hashtable中hash默认数组大小是11,增加的方式是old*2+1;HashMap中hash数组大小默认是16,而且一定是2的倍数,HashMap会将其扩充为2的幂次方大小
  • Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。
  • 两者存储规则不一样:
    • HashMap的存储规则:优先使用数组存储, 如果出现Hash冲突, 将在数组的该位置拉伸出链表进行存储(在链表的尾部进行添加), 如果链表的长度大于设定值后, 将链表转为红黑树.

    • HashTable的存储规则:优先使用数组存储, 存储元素时, 先取出下标上的元素(可能为null), 然后添加到数组元素Entry对象的next属性中(在链表的头部进行添加).出现Hash冲突时, 新元素next属性会指向冲突的元素. 如果没有Hash冲突, 则新元素的next属性就是null。

      Entry<K,V> e = (Entry<K,V>) tab[index];
      tab[index] = new Entry<>(hash, key, value, e);

参照:https://blog.csdn.net/wangxing233/article/details/79452946

HashSet 和 HashMap 区别:

1.HashSet(线程不安全):

  • 不允许存储重复元素的集合
  • 基于哈希表实现,支持快速查找,但不支持有序性操作。
  • 使用 Iterator 遍历 HashSet 得到的结果是不确定的。
  • 成员变量:
    private transient HashMap<E,Object> map;
    
        // Dummy value to associate with an Object in the backing Map
        private static final Object PRESENT = new Object();

    两个变量:

    • map :用于存放最终数据的。
    • PRESENT :是所有写入 map 的 value 值。
  • 构造函数:利用了 HashMap 初始化了 map 

    public HashSet() {
            map = new HashMap<>();
        }
        
        public HashSet(int initialCapacity, float loadFactor) {
            map = new HashMap<>(initialCapacity, loadFactor);
        }    

 

  •  add方法:

    public boolean add(E e) {
            return map.put(e, PRESENT)==null;
        }

    Hashtable将存放的对象当做了 HashMap 的健,value 都是相同的 PRESENT 。由于 HashMap 的 key 是不能重复的,所以每当有重复的值写入到 HashSet 时,value 会被覆盖,但 key 不会受到影响,这样就保证了 HashSet 中只能存放不重复的元素。

HashSet 的原理比较简单,几乎全部借助于 HashMap 来实现的。所以 HashMap 会出现的问题 HashSet 依然不能避免。

区别:

  • HashMap实现了Map接口,而Hashtable实现Set接口;

  • HashMap存储键值对,Hashtable仅存储对象;
  • HashMap调用put()向map中添加元素;Hashtable调用add()方法向Set中添加元素;
  • HashMap比较快,因为是使用唯一的键来获取对象

ConcurrentHashMap 的工作原理及代码实现,如何统计所有的元素个数

1.ConcurrentHashMap(线程安全):

  • 存储结构:
    static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;
    }
    ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。
  • 数据结构(JDK1.7):

如图所示,是由 Segment 数组、HashEntry 数组组成,和 HashMap 一样,仍然是数组加链表组成ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment

  • 默认的并发级别为 16,也就是说默认创建 16 个 Segment。
  • get 方法:ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。

    只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。

  • put 方法:

    static final class HashEntry<K,V> {
            final int hash;
            final K key;
            volatile V value;
            volatile HashEntry<K,V> next;
    
            HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
                this.hash = hash;
                this.key = key;
                this.value = value;
                this.next = next;
            }
        }

虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。首先也是通过 Key 的 Hash 定位到具体的 Segment,在 put 之前会进行一次扩容校验。这里比 HashMap 要好的一点是:HashMap 是插入元素之后再看是否需要扩容,有可能扩容之后后续就没有插入就浪费了本次扩容(扩容非常消耗性能)。而 ConcurrentHashMap 不一样,它是在将数据插入之前检查是否需要扩容,之后再做插入操作。

  • size 方法:每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。
    /**
     * The number of elements. Accessed only either within locks
     * or among other volatile reads that maintain visibility.
     */
    transient int count;

    在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。

    /**
     * Number of unsynchronized retries in size and containsValue
     * methods before resorting to locking. This is used to avoid
     * unbounded retries if tables undergo continuous modification
     * which would make it impossible to obtain an accurate result.
     */
    static final int RETRIES_BEFORE_LOCK = 2;
    
    public int size() {
        // Try a few times to get accurate count. On failure due to
        // continuous async changes in table, resort to locking.
        final Segment<K,V>[] segments = this.segments;
        int size;
        boolean overflow; // true if size overflows 32 bits
        long sum;         // sum of modCounts
        long last = 0L;   // previous sum
        int retries = -1; // first iteration isn't retry
        try {
            for (;;) {
                // 超过尝试次数,则对每个 Segment 加锁
                if (retries++ == RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                        ensureSegment(j).lock(); // force creation
                }
                sum = 0L;
                size = 0;
                overflow = false;
                for (int j = 0; j < segments.length; ++j) {
                    Segment<K,V> seg = segmentAt(segments, j);
                    if (seg != null) {
                        sum += seg.modCount;
                        int c = seg.count;
                        if (c < 0 || (size += c) < 0)
                            overflow = true;
                    }
                }
                // 连续两次得到的结果一致,则认为这个结果是正确的
                if (sum == last)
                    break;
                last = sum;
            }
        } finally {
            if (retries > RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    segmentAt(segments, j).unlock();
            }
        }
        return overflow ? Integer.MAX_VALUE : size;
    }

    每个 Segment 都有一个 modCount 变量,每当进行一次 put remove 等操作,modCount 将会 +1。只要 modCount 发生了变化就认为容器的大小也在发生变化。

  • JDK1.8的实现:

抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。其中的 val next 都用了 volatile 修饰,保证了可见性。

put方法:

  • 根据 key 计算出 hashcode 。
  • 判断是否需要进行初始化。
  • f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  • 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  • 如果都不满足,则利用 synchronized 锁写入数据。
  • 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

get方法:

  • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  • 如果是红黑树那就按照树的方式获取值。
  • 都不满足那就按照链表的方式遍历获取值。

总结

1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。

HashMap 和 ConcurrentHashMap 的区别

  • HashMap线程不安全,而ConcurrentHashMap线程安全;

多线程情况下HashMap死循环的问题

  • 容量大于  总量*负载因子  发生扩容时会出现环形链表从而导致死循环。
  • 并发场景发生扩容,调用 resize() 方法里的 rehash() 时,容易出现环形链表。这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标时就会出现死循环。

https://www.cnblogs.com/dongguacai/p/5599100.html

https://blog.csdn.net/linsongbin1/article/details/54708694

介绍一下LinkedHashMap

  • 这是一个有序的,底层是继承于 HashMap 实现的,由一个双向链表所构成,具有和 HashMap 一样的快速查找特性。

  • LinkedHashMap 的排序方式有两种:

    • 根据写入顺序排序。
    • 根据访问顺序排序,每次 get 都会将访问的值移动到链表末尾,这样重复操作就能得到一个按照访问顺序排序的链表。
  • 数据结构,通过以下代码调试可以看到 map 的组成:
    @Test
    	public void test(){
    		Map<String, Integer> map = new LinkedHashMap<String, Integer>();
    		map.put("1",1) ;
    		map.put("2",2) ;
    		map.put("3",3) ;
    		map.put("4",4) ;
    		map.put("5",5) ;
    		System.out.println(map.toString());
    	}

 /**
     * The head of the doubly linked list.
     */
    private transient Entry<K,V> header;

    /**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *
     * @serial
     */
    private final boolean accessOrder;
    
    private static class Entry<K,V> extends HashMap.Entry<K,V> {
        // These fields comprise the doubly linked list used for iteration.
        Entry<K,V> before, after;

        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }
    }  

其中 Entry 继承于 HashMap 的 Entry,并新增了上下节点的指针,也就形成了双向链表。还有一个 header 的成员变量,是这个双向链表的头结点。

第一个类似于 HashMap 的结构,利用 Entry 中的 next 指针进行关联。下边则是 LinkedHashMap 如何达到有序的关键。就是利用了头节点和其余的各个节点之间通过 Entry 中的 after 和 before 指针进行关联。其中还有一个 accessOrder 成员变量,默认是 false,默认按照插入顺序排序,为 true 时按照访问顺序排序,也可以调用:

这个构造方法可以显示的传入 accessOrder

public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
  • 构造方法就是调用HashMap 的构造方法:

    public LinkedHashMap() {
            super();
            accessOrder = false;
        }
    // HashMap实现:
     public HashMap(int initialCapacity, float loadFactor) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal initial capacity: " +
                                                   initialCapacity);
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new IllegalArgumentException("Illegal load factor: " +
                                                   loadFactor);
    
            this.loadFactor = loadFactor;
            threshold = initialCapacity;
            //HashMap 只是定义了改方法,具体实现交给了 LinkedHashMap
            init();
        }
    

    可以看到里面有一个空的 init(),具体是由 LinkedHashMap 来实现的:

     @Override
        void init() {
            header = new Entry<>(-1, null, null, null);
            header.before = header.after = header;
        }

    其实也就是对 header 进行了初始化,从这个方法可以看出,实现了双向。

  • put() 方法:主体的实现都是借助于 HashMap 来完成的,只是对其中的 recordAccess(), addEntry(), createEntry() 进行了重写。如下是HashMap的put方法:

    public V put(K key, V value) {
            if (table == EMPTY_TABLE) {
                inflateTable(threshold);
            }
            if (key == null)
                return putForNullKey(value);
            int hash = hash(key);
            int i = indexFor(hash, table.length);
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                Object k;
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    //空实现,交给 LinkedHashMap 自己实现
                    e.recordAccess(this);
                    return oldValue;
                }
            }
    
            modCount++;
            // LinkedHashMap 对其重写
            addEntry(hash, key, value, i);
            return null;
        }
        
        // LinkedHashMap 对其重写
        void addEntry(int hash, K key, V value, int bucketIndex) {
            if ((size >= threshold) && (null != table[bucketIndex])) {
                resize(2 * table.length);
                hash = (null != key) ? hash(key) : 0;
                bucketIndex = indexFor(hash, table.length);
            }
    
            createEntry(hash, key, value, bucketIndex);
        }
        
        // LinkedHashMap 对其重写
        void createEntry(int hash, K key, V value, int bucketIndex) {
            Entry<K,V> e = table[bucketIndex];
            table[bucketIndex] = new Entry<>(hash, key, value, e);
            size++;
        }       
    //就是判断是否是根据访问顺序排序,如果是则需要将当前这个 Entry 移动到链表的末尾
            void recordAccess(HashMap<K,V> m) {
                LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
                if (lm.accessOrder) {
                    lm.modCount++;
                    remove();
                    addBefore(lm.header);
                }
            }
            
            
        //调用了 HashMap 的实现,并判断是否需要删除最少使用的 Entry(默认不删除)    
        void addEntry(int hash, K key, V value, int bucketIndex) {
            super.addEntry(hash, key, value, bucketIndex);
    
            // Remove eldest entry if instructed
            Entry<K,V> eldest = header.after;
            if (removeEldestEntry(eldest)) {
                removeEntryForKey(eldest.key);
            }
        }
        
        void createEntry(int hash, K key, V value, int bucketIndex) {
            HashMap.Entry<K,V> old = table[bucketIndex];
            Entry<K,V> e = new Entry<>(hash, key, value, old);
            //就多了这一步,将新增的 Entry 加入到 header 双向链表中
            table[bucketIndex] = e;
            e.addBefore(header);
            size++;
        }
        
            //写入到双向链表中
            private void addBefore(Entry<K,V> existingEntry) {
                after  = existingEntry;
                before = existingEntry.before;
                before.after = this;
                after.before = this;
            }  

    以上是LinkedHashMap 的实现。

  • get 方法,LinkedHashMap 的 get() 方法也重写了:

    public V get(Object key) {
            Entry<K,V> e = (Entry<K,V>)getEntry(key);
            if (e == null)
                return null;
                
            //多了一个判断是否是按照访问顺序排序,是则将当前的 Entry 移动到链表头部。   
            e.recordAccess(this);
            return e.value;
        }
        
        void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) {
                lm.modCount++;
                
                //删除
                remove();
                //添加到头部
                addBefore(lm.header);
            }
        }

    clear()方法:

    //只需要把指针都指向自己即可,原本那些 Entry 没有引用之后就会被 JVM 自动回收。
        public void clear() {
            super.clear();
            header.before = header.after = header;
        }

    总的来说 LinkedHashMap 其实就是对 HashMap 进行了拓展,使用了双向链表来保证了顺序性。因为是继承与 HashMap 的,所以一些 HashMap 存在的问题 LinkedHashMap 也会存在,比如不支持并发等。

HashMap出现Hash DOS攻击的问题

手写简单的HashMap

看过那些Java集合类的源码

什么是快速失败的故障安全迭代器?

快速失败的Java迭代器可能会引发ConcurrentModifcationException在底层集合迭代过程中被修改。故障安全作为发生在实例中的一个副本迭代是不会抛出任何异常的。快速失败的故障安全范例定义了当遭遇故障时系统是如何反应的。例如,用于失败的快速迭代器ArrayList和用于故障安全的迭代器ConcurrentHashMap。

Iterator和ListIterator的区别

●ListIterator有add()方法,可以向List中添加对象,而Iterator不能。

●ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。

●ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。

●都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改

什么是CopyOnWriteArrayList,它与ArrayList有何不同?

CopyOnWriteArrayList是ArrayList的一个线程安全的变体,其中所有可变操作(add、set等等)都是通过对底层数组进行一次新的复制来实现的。相比较于ArrayList它的写操作要慢一些,因为它需要实例的快照。

CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差,但是读操作因为操作的对象和写操作不是同一个对象,读之间也不需要加锁,读和写之间的同步处理只是在写完后通过一个简单的"="将引用指向新的数组对象上来,这个几乎不需要时间,这样读操作就很快很安全,适合在多线程里使用,绝对不会发生ConcurrentModificationException ,因此CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。

迭代器和枚举之间的区别

如果面试官问这个问题,那么他的意图一定是让你区分Iterator不同于Enumeration的两个方面:

●Iterator允许移除从底层集合的元素。

●Iterator的方法名是标准化的。

https://blog.csdn.net/helongzhong/article/details/52869981

总结:

1. 如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。
2. 如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高,如果多个线程可能同时操作一个类,应该使用同步的类。
3. 在除需要排序时使用TreeSet,TreeMap外,都应使用HashSet,HashMap,因为他们 的效率更高。
4. 要特别注意对哈希表的操作,作为key的对象要正确复写equals和hashCode方法。
5. 容器类仅能持有对象引用(指向对象的指针),而不是将对象信息copy一份至数列某位置。一旦将对象置入容器内,便损失了该对象的型别信息。
6. 尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,客户端代码不用改变。这就是针对抽象编程。

猜你喜欢

转载自blog.csdn.net/striveb/article/details/82593768