2020JAVA集合容器面试题

集合和数组的区别

数组是固定长度的;集合可变长度的。
数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

collection集合和map集合结构图

在这里插入图片描述

Collection集合主要有List和Set两大接口

List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。

Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复,常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap

集合框架底层数据结构

Collection

List

Arraylist: Object数组
Vector: Object数组
LinkedList: 双向循环链表

Set

HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
LinkedHashSet: LinkedHashSet 继承自HashSet,并且其内部是通过 LinkedHashMap 来实现的。
TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)

Map

HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
TreeMap: 红黑树(自平衡的排序二叉树)

哪些集合类是线程安全的?

vector:使用了 Synchronized 来实现线程同步,因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。

statck:堆栈类,先进后出。

hashtable:就比hashmap多了个线程安全。

enumeration:枚举,相当于迭代器。

Java集合的快速失败机制 “fail-fast”?

是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。

例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hasNext()/next()遍历下一个元素之前,都会检测modCount变量是否expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

解决办法
在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。

使用CopyOnWriteArrayList来替换ArrayList

fail-fast原理分析

ArrayList

public class ArrayListTest {
    
    

    public static void main(String[] args) {
    
    
        ArrayList<Integer> arrayList = new ArrayList<>();
        arrayList.add(10);
        arrayList.add(11);

        Iterator<Integer> iterator = arrayList.iterator();
        while (iterator.hasNext()) {
    
    
            Integer next = iterator.next();		//遍历,产生错误
            if (next == 11) {
    
    
                arrayList.remove(next);
            }
        }
    }

}

结果是:
在这里插入图片描述从结果看出,在单线程下,在使用迭代器进行遍历的情况下,如果调用 ArrayList 的 remove 方法,此时会报 ConcurrentModificationException 的错误,从而产生 fail-fast 机制

错误信息告诉我们,发生在 iterator.next() 这一行,继续点进去,定位到 checkForComodification() 这一行

public E next() {
    
    
    checkForComodification();		// 错误在这儿
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

继续点进去,可以在 ArrayList 的 Itr 这个内部类中找到该方法的详细定义,这里涉及到两个变量,modCount 和 expectedModCount,modCount 是在 ArrayList 的父类 AbstractList 中进行定义的,初始值为 0,而 expectedModCount 则是在 ArrayList 的 内部类中进行定义的,在执行 arrayList.iterator() 的时候,首先会实例化 Itr 这个内部类,在实例化的同时也会对 expectedModCount 进行初始化,将 modCount 的值赋给 expectedModCount

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    
    
    
    // modCount 初始值为 0
    protected transient int modCount = 0;
    
}

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    
    
	public Iterator<E> iterator() {
    
    
		// 实例化内部类 Itr
        return new Itr();
    }

	private class Itr implements Iterator<E> {
    
    
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        // expectedModCount 一开始等于 modCount
        int expectedModCount = modCount;

		// 检查是否发生异常
        final void checkForComodification() {
    
    
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }	
}

知道了这两个变量是从何而来之后,我们来看 checkForComodification() 这个方法,如果 modCount 和 expectedModCount 不等,就会抛出 ConcurrentModificationException 这个异常,换句话说,一般情况下,这两个变量是相等的,那么啥时候起?这两个变量会不等呢?

经过观察,发现 ArrayList 在增加、删除(根据对象删除集合元素)、清除等操作中,都有 modCount++ 这一步骤,即代表着,每次执行完相应的方法,modCount 这一变量就会加 1

public boolean add(E e) {
    
    
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

// 根据传入的对象来删除,而不是根据位置
public boolean remove(Object o) {
    
    
    if (o == null) {
    
    
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
    
    
                fastRemove(index);
                return true;
            }
    } else {
    
    
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
    
    
                fastRemove(index);		// modCount++
                return true;
            }
    }
    return false;
}

public void clear() {
    
    
    modCount++;

    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;
    
    size = 0;
}

分析到这儿,似乎有些明白了,我们来完整的分析一下整个过程,在没有执行删除操作之前,ArrayList 中的 modCount 变量和迭代器中的 expectedModCount 的值一直都是相同的。在迭代的过程中,调用了 ArrayList 的 remove(Object o) 方法,使得 ArrayList 的 modCount 这个变量发生变化(删除成功一次加1),一开始和 modCount 相等的 expectedModCount 是属于内部类的,它直到迭代结束都没能发生变化。在迭代器执行下一次迭代的时候,因为这两个变量不等,所以便会抛出 ConcurrentModificationException 异常,即产生了 fail-fast 异常

HashMap

public class HashMapTest {
    
    

    public static void main(String[] args) {
    
    
        HashMap<Integer, String> hashMap = new HashMap<>();
        hashMap.put(1, "QQQ");
        hashMap.put(2, "JJJ");
        hashMap.put(3, "EEE");

        Set<Map.Entry<Integer, String>> entries = hashMap.entrySet();
        Iterator<Map.Entry<Integer, String>> iterator = entries.iterator();

        while (iterator.hasNext()) {
    
    
            Map.Entry<Integer, String> next = iterator.next(); //错误产生地方
            if (next.getKey() == 2) {
    
    
                hashMap.remove(next.getKey());
            }
        }

        System.out.println(hashMap);
    }

}

输出结果是:
在这里插入图片描述根据错误的提示,找到出错的位置,也是在 Map.Entry<Integer, String> next = iterator.next() 这一行,继续寻找源头,定位到了 HashMap 中的内部类 EntryIterator 下的 next() 方法

private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
    
    
    public Map.Entry<K,V> next() {
    
    
        return nextEntry();
    }
}

继续往下找,来到了 HashMap 下的另一个私有内部类 HashIterator,该内部类也有 expectedModCount,modCount 是直接定义在 HashMap 中的,初始值为 0,expectedModCount 直接定义在 HashMap 的内部类中,当执行 arrayList.iterator() 这段代码的时候,便会初始化 HashIterator 这个内部类,同时调用构造函数 HashIterator(),将 modCount 的值赋给 expectedModCount

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
{
    
    
	// 初始值为 0
	transient int modCount;

	// HashMap 的内部类 HashIterator
	private abstract class HashIterator<E> implements Iterator<E> {
    
    
        Entry<K,V> next;        // next entry to return
        // 期待改变的值,初始值为 0
        int expectedModCount;   // For fast-fail
        int index;              // current slot
        Entry<K,V> current;     // current entry

        HashIterator() {
    
    
            // expectedModCount 和 modCount 一样,初始值为 0
            expectedModCount = modCount;
            if (size > 0) {
    
     // advance to first entry
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        }

        public final boolean hasNext() {
    
    
            return next != null;
        }

        final Entry<K,V> nextEntry() {
    
    
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();

            if ((next = e.next) == null) {
    
    
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
            current = e;
            return e;
        }

  		...
    }
}

来看抛出异常的 nextEntry() 这个方法,只要 modCount 和 expectedModCount 不等,便会抛出 ConcurrentModificationException 这个异常,即产生 fast-fail 错误

同样,我们看一下 modCount 这个变量在 HashMap 的哪些方法中使用到了,和 ArrayList 类似,也是在增加、删除、修改等方法中,对 modCount 这个变量进行了加 1 操作

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;
            e.recordAccess(this);
            return oldValue;
        }
    }

    // 将 modCount 加 1
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

public V remove(Object key) {
    
    
    Entry<K,V> e = removeEntryForKey(key);		//该方法里面,如果删除成功,则将 modCount 加 1
    return (e == null ? null : e.value);
}

public void clear() {
    
    
    // 将 modCount 加 1
    modCount++;
    Arrays.fill(table, null);
    size = 0;
}

我们来捋一下整个过程,在对 HashMap 和 Iterator 进行初始化之后,没有执行 remove 方法之前,HashMap 中的 modCount 和内部类 HashIterator 中的 expectedModCount 一直是相同的。在 HashMap 调用 remove(Object key) 方法时,如果删除成功,则会将 modCount 这个变量加 1,而 expectedModCount 是在内部类中的,一直没有发生变化,当进行到下一次迭代的时候(执行 next 方法),因为 modCount 和 expectedModCount 不同,所以抛出 ConcurrentModificationException 这个异常

怎么确保一个集合不能被修改?

可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。
示例代码如下:

List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());

Collection接口

List接口

迭代器 Iterator 是什么?

Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。

Iterator 怎么使用?有什么特点?

Iterator 使用代码如下:

List<String> list = new ArrayList<>();
Iterator<String> it = list. iterator();
while(it. hasNext()){
    
    
  String obj = it. next();
  System. out. println(obj);
}

Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModificationException 异常。

如何边遍历边移除 Collection 中的元素?

边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:

Iterator<Integer> it = list.iterator();
while(it.hasNext()){
    
    
   *// do something*
   it.remove();
}

一种最常见的错误代码如下:

for(Integer i : list){
    
    
   list.remove(i)
}

运行以上错误代码会报 ConcurrentModificationException 异常。这是因为当使用foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被 Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。

Iterator 和 ListIterator 有什么区别?

Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
ListIterator 继承 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?

遍历方式有以下几种:
for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。

迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。

foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如增加、删除、替换、清除。

说一下 ArrayList 的优缺点

ArrayList的优点如下:

ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
ArrayList 在顺序添加一个元素的时候非常方便。

ArrayList 的缺点如下:
删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
插入元素的时候,也需要做一次元素复制操作,缺点同上。

ArrayList 比较适合顺序添加、随机访问的场景。

如何实现数组和 List 之间的转换?

数组转 List:使用 Arrays. asList(array) 进行转换。
List 转数组:使用 List 自带的 toArray() 方法。
代码示例:

// list to array
List<String> list = new ArrayList<String>();
list.add("123");
list.add("456");
list.toArray();

// array to list
方法一:
Integer[] nums1= new Integer[]{
    
    4,1,2};
 List<Integer> list1 = Arrays.asList(nums1);
  strings.add("aaa");
 使用这种方法,对集合进行操作后会报UnsupportedOperationException,比如:增删改等。下面这种方法不会报此错误,推荐使用下面方法
 
 方法二:
  ArrayList<Integer> list = new ArrayList<>(Arrays.asList(nums1));
  list.add("aaa");
  注意:这里数组nums1类型必须是Integer,不能是int类型,否则报错

ArrayList 和 LinkedList 的区别是什么?

数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。

随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。

增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。

内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。

线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;

综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

ArrayList 和 Vector 的区别是什么?

这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合

线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
性能:ArrayList 在性能方面要优于 Vector。 扩容:它们都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。

Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。

多线程场景下如何使用 ArrayList?

ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:

List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");

for (int i = 0; i < synchronizedList.size(); i++) {
    
    
    System.out.println(synchronizedList.get(i));
}

List 和 Set 的区别

List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和Vector。

Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。

Set接口

说一下 HashSet 的实现原理?

HashSet 底层是基于 HashMap 实现的,使用HashMap进行存储。HashSet的值存放于HashMap的key上,HashMap的value统一为PRESENT(private static final Object PRESENT = new Object();)。因此 HashSet 的实现比较简单,HashSet 的相关操作,基本上都是直接调用底层 HashMap 的相关方法来完成。HashSet 不允许重复的值。

HashSet如何检查重复?HashSet是如何保证数据不可重复的?

以下是HashSet 部分源码:

private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;

public HashSet() {
    
    
    map = new HashMap<>();
}

public boolean add(E e) {
    
    
    // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
	return map.put(e, PRESENT)==null;
}

HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key

HashSet与HashMap的区别

HashMap HashSet
实现了Map接口 实现Set接口
存储键值对 仅存储对象
调用put()向map中添加元素 调用add()方法向Set中添加元素
HashMap使用键(Key)计算Hashcode,来查找value的位置 HashSet使用成员对象来计算hashcode值
HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 HashSet较HashMap来说比较慢

Map接口

说一下 HashMap 的实现原理?

HashMap概述: HashMap是基于哈希表的Map接口的非同步实现,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

HashMap的数据结构:HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

HashMap 基于 Hash 算法实现的:

当我们往Hashmap中put元素时,利用key的hashCode重新hash,计算出当前对象在数组中的下标。

存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中

获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。

理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。

需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现(重点)

JDK1.8之前
JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
在这里插入图片描述JDK1.8之后
相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
在这里插入图片描述

JDK1.7 VS JDK1.8 比较

JDK1.8主要解决或优化了一下问题:
resize 进行扩容优化

引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考

解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
在这里插入图片描述

HashMap是怎么解决哈希冲突的?

答:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希才行;

什么是哈希?

Hash,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值)。

所有散列函数都有如下一个基本特性:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。

什么是哈希冲突?

当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。

HashMap的数据结构

在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做链地址法的方式可以解决哈希冲突:
在这里插入图片描述这样我们就可以将拥有相同哈希值的对象组织成一个链表放在hash值所对应的bucket下,但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化

hash()函数

上面提到的问题,主要是因为如果使用hashCode取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下:

static final int hash(Object key) {
    
    
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与自己右移16位进行异或运算(高低位异或)
}

hashmap的存储和获取数据原理(重点)

  • 往hashmap中插入数据时,先计算key的hash值。key.hash = (h = k.hashCode()) ^ (h >>>16)
  • 计算bucket数组的位置,index = key.hash & (table.length - 1),找到元素被存放在bucket数组中的位置。
  • 如果两个key的hash值相等,发了hash冲突,可以在bucket后面接一个链表(JDK8及以后的版本中还会加上红黑树)来解决,将新存储的键值对放在表头(也就是bucket中)。

当调用get方法获取存储的值时,首先根据键的hashCode找到对应的bucket,然后根据equals方法来在链表和红黑树中找到对应的值。

hashmap的时间复杂度(重点)

理想情况下,hashmap的时间复杂度为O(1),根据key.hash = (h = k.hashCode()) ^ (h >>>16)可以得到key在数组中的位置,这刚好就是数组的高效查询时间复杂度O(1)。

JDK1.8新增红黑树

在这里插入图片描述通过上面的链地址法(使用散列表)和扰动函数我们成功让我们的数据分布更平均,哈希碰撞减少,但是当我们的HashMap中存在大量数据时,加入我们某个bucket下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8在HashMap中新增了红黑树的数据结构,进一步使得遍历复杂度降低至O(logn);
总结
总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:
使用链地址法(使用散列表)来链接拥有相同hash值的数据;
使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;

能否使用任何类作为 Map 的 key?

可以使用任何类作为 Map 的 key

为什么HashMap中String、Integer这样的包装类适合作为K?

都是final类型,具有不可变性,保证key的不可更改性,不会存在获取hash值不同的情况。
内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范。

如果使用Object作为HashMap的Key,应该怎么办呢?

答:重写hashCode()和equals()方法
重写hashCode()是因为需要计算value的存储位置
重写equals()方法,目的是为了保证key在哈希表中的唯一性;

HashMap 的长度为什么是2的幂次方

为了能让 HashMap 存取高效,尽量减少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。

那为什么是两次扰动呢?

加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突,两次就够了,已经达到了高位低位同时参与运算的目的;

HashMap 与 HashTable 有什么区别?(重点)

线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
效率: HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
对Null key 和Null value的支持: HashMap 中,只能有一个null键,可以有多个键所对应的null值。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
初始容量大小和每次扩充容量大小的不同:①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小,HashMap 总是使用2的幂作为哈希表的大小。
底层数据结构:在JDK1.8之前,hashmap使用链表+数组的数据结构。在JDK1.8 以后, HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
推荐使用:在 Hashtable虽然是线程安全的, 但是效率很低,已经被淘汰了,不建议使用。推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。
在这里插入图片描述

ConcurrentHashMap 和 Hashtable 的区别?(重要)

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
底层数据结构: JDK1.8之前的 ConcurrentHashMap 底层采用 segment数组实现,每个segment类似于hashmap,里面包含一个hashentry数组,hashentry既是key-value对也是链表的头结点,JDK1.8之后采用的数据结构Node数组+链表(长度>8转变为)红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;

实现线程安全的方式(重要): ① 在JDK1.8之前,ConcurrentHashMap使用分段锁实现线程安全,每个segment相当于一把锁,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
到了 JDK1.8之后已经摒弃了Segment的概念,而是直接用 Node 数组+链表(长度>8转变为)红黑树的数据结构来实现,并发控制使用 synchronized +CAS 来操作。② Hashtable使用 synchronized锁住整个容器的来保证线程安全,执行效率非常低下,只允许有一个线程进行同步操作。

在这里插入图片描述
在这里插入图片描述

如何决定使用 HashMap 还是 TreeMap?

对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择.然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择

HashMap 和 ConcurrentHashMap 的区别

数据结构上:1.8之前hahsmap采用数组+链表实现、1.8之后hashmap使用数组+链表(长度>8)转为红黑树实现;1.8之前ConcurrentHashMap采用segments数组实现,每个segment相当于一个hashmap,里面有个hashentry数组,它既是key-value对也是一个链表的首结点、1.8之后的ConcurrentHashMap摒弃了segemnts数组的概念,而是使用node数组+链表(长度>8)转变为红黑树。
线程安全上:hashmap不是线程安全的;ConcurrentHashMap 1.8之前使用segment分段锁实现线程安全,每个segment都是一把锁,多个线程获取相应的segment锁进行高效率操作,1.8之后摒弃了segment概念,使用Node数组+链表(长度>8)转变为红黑树,同步机制采用synchronized+CAS实现。

ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?(重点)

ConcurrentHashMap锁的方式是稍微细粒度的,ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁上当前需要用到的桶。

JDK1.8之前
ConcurrentHashMap采用segment+hashentry实现,segment充当锁的角色,hashentry用来封装映射表的键值对。

一个ConcurrentHashMap有一个segment数组,每个segment数组下面有一个hashentry数组,它守护着hashentry数组里的元素,当要对hashentry数组里的元素进行操作时,需要先获得segment锁。
在这里插入图片描述

JDK1.8之后
在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node数组 + CAS + Synchronized来保证多线程并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

结构如下:
在这里插入图片描述

辅助工具类

comparable 和 comparator的区别?

  • comparable接口通常是实现它然后重写 compareTo(Object obj)方法用来排序。
  • comparator接口通常是使用在匿名内部类中实现compare(Object obj1, Object obj2)方法用来排序

Collection 和 Collections 有什么区别?

  • .Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法
  • Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。

TreeMap 和 TreeSet 在排序时如何比较素?Collections 工具类中的 sort()方法如何比较元素?

TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进 行排 序。

Collections 工具类的 sort 方法有两种重载的形式,

第一种要求传入的待排序容器中存放的对象必须实现 Comparable 接口以实现元素的比较;

第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。

猜你喜欢

转载自blog.csdn.net/weixin_44421869/article/details/108319856
今日推荐