Java集合容器面试题3

简介: Java集合容器面试题3

插入数据时,ArrayList、LinkedList、Vector谁速度较快?

在插入数据时,LinkedList 的速度相对较快,因为它的底层是一个链表结构,插入一个元素只需要修改相邻节点的指针即可,时间复杂度为 O(1)。而 ArrayList 和 Vector 的底层是一个数组结构,插入一个元素需要将后面的元素向后移动一位,时间复杂度为 O(n),其中 n 是数组的长度。因此在插入数据时,LinkedList 的性能比 ArrayList 和 Vector 更好。Vector 中的方法由于加了 synchronized 修饰,因此 Vector是线程安全容器,性能上较ArrayList差。


插入速度快到慢排序:LinkedList>ArrayList>Vector


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

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

    List<String> list = new ArrayList<>();
    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));
    }


为什么 ArrayList 的 elementData 加上 transient 修饰?

为什么 ArrayList 的 elementData 加上 transient 修饰?

ArrayList中将elementData修饰成transient是为了节省空间,ArrayList的自动扩容机制,elementData数组相当于容器,当容器不足时就会再扩充容量,但是容器的容量往往都是大于或者等于ArrayList所存元素的个数。所以ArrayList的设计者将elementData设计为transient,这样这个数组就不会被序列化,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个elementData数组。


比如,现在实际有了8个元素,那么elementData数组的容量可能是8x1.5=12,如果直接序列化elementData数组,那么就会浪费4个元素的空间,特别是当元素个数非常多时,这种浪费是非常不合算的。


序列化和反序列化的定义

Java序列化就是指把Java对象转换为字节序列的过程

Java反序列化就是指把字节序列恢复为Java对象的过程。

transient关键字

被transient修饰的成员变量不会被序列化。

代码解析

ArrayList 中的数组定义如下:


private transient Object[] elementData;


再 看 一 下 ArrayList 的 定 义 :


public class ArrayListextends AbstractListimplementsList, RandomAccess, Cloneable, java.io.Serializable


可以看到 ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。


transient 的作用是希望 elementData 数组不被序列化


重写了 writeObject 实现:


private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException{
    int expectedModCount = modCount;
    s.defaultWriteObject();
    s.writeInt(elementData.length);
    for(int i=0; i<size; i++)
        s.writeObject(elementData[i]);
        if(modCount != expectedModCount){
        thrownewConcurrentModificationException();
        }
   }

骚戴理解:从源码中,可以观察到 循环时是使用i

Array (数组)和 ArrayList 有何区别?

Array 可以存储基本数据类型和引用类型,ArrayList 只能存储引用类型。

Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。

Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有ArrayList 有。

Array 数组存储的元素必须是同一个数据类型;ArrayList 存储的对象可以是不同数据类型。


CopyOnWriteArrayList 是什么,可以用于什么应用场景?有哪些优缺点?

什么是CopyOnWrite ?

CopyOnWrite 容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以 CopyOnWrite 容器也是一种读写分离的思想,读和写不同的容器。


CopyOnWriteArrayList 是什么?

CopyOnWriteArrayList 是线程安全的集合,它的实现方式是在写操作时创建一个新的数组,并将原数组中的元素复制到新数组中,然后在新数组中进行写操作,最后将新数组替换为原数组。


读操作则直接在原数组中进行,因此读操作不需要进行同步处理,可以实现读写分离。由于每次写操作都会创建一个新的数组,因此 CopyOnWriteArrayList 的写操作相对较慢,但是读操作的性能很高。


需要注意的是,CopyOnWriteArrayList 的迭代器是弱一致性的,也就是说,在迭代过程中,如果有其他线程对 List 进行了修改,迭代器不会抛出 ConcurrentModificationException 异常,但是不保证迭代器能够遍历到所有的元素。CopyOnWriteArrayList 的使用场景是合适读多写少的场景。


骚戴理解:不能保证迭代器能够遍历到所有元素是为什么呢?例如我有个集合,里面有十个元素,我已经遍历了五个,这个时候我在前五个元素之间添加了一个新元素,但是由于已经遍历到第五个了,所以只会往后继续遍历,而遍历之前的改变就无法遍历到!


CopyOnWriteArrayList 有什么优点?

CopyOnWriteArrayList 的优点主要包括以下几点:


1. 线程安全:CopyOnWriteArrayList 是线程安全的 List 实现,可以在多线程环境中使用,无需进行额外的同步处理。


2. 读写分离:CopyOnWriteArrayList 的读操作和写操作是分离的,读操作直接在原数组中进行,不需要进行同步处理,因此读操作的性能很高。


3. 弱一致性迭代器:CopyOnWriteArrayList 的迭代器是弱一致性的,可以在迭代过程中进行修改,不会抛出 ConcurrentModificationException 异常。


4. 适用于读多写少的场景:由于每次写操作都需要创建一个新的数组,因此适用于读多写少的场景,如果写操作比较频繁,可能会导致性能下降。


需要注意的是,CopyOnWriteArrayList 的写操作相对较慢,因为每次写操作都需要创建一个新的数组,因此适用于读多写少的场景。如果读写操作的频率相当,或者写操作比较频繁,可能会导致性能下降。此外,由于每次写操作都会创建一个新的数组,因此会占用较多的内存空间,需要根据具体的场景和需求进行选择。


CopyOnWriteArrayList 的缺点是什么?

CopyOnWriteArrayList 的缺点主要包括以下几点:


1. 内存占用较高:由于每次写操作都会创建一个新的数组,因此会占用较多的内存空间。


2. 写操作性能较低:由于每次写操作都需要创建一个新的数组,因此写操作的性能较低,适用于读多写少的场景。


3. 数据一致性问题:由于 CopyOnWriteArrayList 的迭代器是弱一致性的,也就是说,在迭代过程中,如果有其他线程对 List 进行了修改,迭代器不会抛出 ConcurrentModificationException 异常,但是不保证迭代器能够遍历到所有的元素。


4. 不适用于实时性要求高的场景:由于每次写操作都需要创建一个新的数组,因此不适用于实时性要求高的场景,可能会导致写操作的延迟


CopyOnWriteArrayList 是如何保证写时线程安全的?

CopyOnWriteArrayList 是通过“写时复制”(Copy-On-Write)策略来保证写时线程安全的。当一个线程需要进行写操作时,CopyOnWriteArrayList 会先创建一个新的数组,然后将原数组中的元素复制到新数组中。由于创建新数组时只有当前线程能够访问,因此不需要进行同步处理。在新数组中进行写操作时,其他线程仍然可以访问原数组,不会被当前线程的写操作所影响。当写操作完成后,CopyOnWriteArrayList 将新数组替换为原数组,从而保证写操作的线程安全性。


Set

List 和 Set 的区别

List 和 Set 都是 Java 集合框架中的接口,它们的主要区别在于以下几个方面:


1. 元素的顺序:List 中的元素是按照插入顺序排列的,可以根据索引访问元素;而 Set 中的元素是无序的,不能根据索引访问元素。


2. 元素的唯一性:List 中的元素可以重复,可以插入多个null元素。而 Set 中的元素不能重复,每个元素只能出现一次,只允许存入一个null元素,必须保证元素唯一性


3. 实现方式:List 的常见实现方式有 ArrayList、LinkedList 等;而 Set 的常见实现方式有 HashSet、TreeSet、LinkedHashSet 等。


4. 应用场景:List 适用于需要按照插入顺序访问元素的场景,如需要维护一个有序的列表;而 Set 适用于需要保证元素唯一性的场景,如去重、查找等。


骚戴理解:如果需要按照插入顺序访问元素,可以选择 List;如果需要保证元素唯一性,可以选择 Set。

说一下 HashSet 的实现原理?

HashSet 的底层其实就是HashMap,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。


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

当向HashSet 中添加一个对象的时候(底层就是把这个对象存到HashMap的 key中),会调用hashCode方法得到一个hash值,然后看有没有发生hash冲突,如果没有发生hash冲突,那就是直接插入,如果发生了hash冲突,那就调用equals方法进一步判断,如果这两个的hashCode()返回值相等,通过equals比较也返回true的话(说明加入的是重复的对象)那就放弃添加这个重复的元素,这也就满足了Set中元素不重复的特性。如果发生了hash冲突但是equals方法返回结果是false那就把新加入的这个对象散列到其他的位置上去。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。如果不用hashcode函数的话那就要遍历set中所有元素然后调用equals方法一个个的去比较是否相同,很明显,这需要大量调用equals方法


骚戴理解:不同对象的hashCode方法执行的结果可能会相同,也就是所谓的hash冲突,但是不同对象调用equals后返回的结果一定是false,只有两个相同的对象的hashCode()返回值相等,通过equals比较也返回true


hashCode()与equals()的相关规定

hashCode() 和 equals() 是 Object 类中定义的两个方法,它们在 Java 中被广泛使用,用于判断对象是否相等。在使用这两个方法时,需要注意以下规定:


1. 如果两个对象相等(equals() 方法返回 true),那么它们的 hashCode() 值必须相等。这是因为 hashCode() 方法的实现通常依赖于对象的内容,如果两个对象相等,那么它们的内容也应该相等,因此它们的 hashCode() 值也应该相等。


2. 如果两个对象的 hashCode() 值相等,它们并不一定相等(equals() 方法返回 true)。这是因为 hashCode() 方法可能存在哈希冲突,即不同的对象可能会产生相同的 hashCode() 值。因此,当 hashCode() 值相等时,还需要调用 equals() 方法进行比较,以确定它们是否相等。


3. 如果一个类重写了 equals() 方法,那么它也应该重写 hashCode() 方法,以保证上述规定的正确性。这是因为 Object 类中的 hashCode() 方法是根据对象的地址计算得到的,如果不重写 hashCode() 方法,可能会导致相等的对象具有不同的 hashCode() 值,从而违反了第一个规定。


4. 如果一个类重写了 hashCode() 方法,那么它也应该重写 equals() 方法,以保证上述规定的正确性。这是因为 hashCode() 方法可能存在哈希冲突,如果不重写 equals() 方法,可能会导致不相等的对象具有相同的 hashCode() 值,从而违反了第二个规定。


需要注意的是,hashCode() 和 equals() 的实现应该保证效率和正确性,不应该过于复杂或者耗时。此外,hashCode() 方法的返回值应该尽可能地分布均匀,以提高哈希表的性能。


HashSet与HashMap的区别

HashSet 和 HashMap 都是 Java 集合框架中的实现类,它们的主要区别在以下几个方面:


1. 存储方式:HashSet 存储的是一组唯一的、无序的元素,而 HashMap 存储的是一组键值对。


2. 底层实现:HashSet 的底层是基于 HashMap 实现的,它使用 HashMap 存储元素,只是将元素的值作为键,键对应的值为一个固定的 Object 对象;而 HashMap 则是使用哈希表实现的。


3. 元素的唯一性:HashSet 中的元素是唯一的,每个元素只能出现一次;而 HashMap 中的键是唯一的,但值可以重复。


4. 访问方式:HashSet 中的元素不能根据索引访问,只能通过迭代器进行访问;而 HashMap 中的元素可以根据键进行访问。


需要根据具体的需求选择合适的集合类型。如果需要存储一组唯一的、无序的元素,可以选择 HashSet;如果需要存储一组键值对,可以选择 HashMap。如果需要同时存储键值对并保证键的唯一性,可以选择 LinkedHashMap。

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

TreeMap 和 TreeSet 在排序时如何比较元素?

TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。


TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进行排序。


骚戴理解:都是通过实现Comparable接口来比较元素,该接口提供了比较元素的compareTo()


方法,当插入元素时会调用该方法比较元素的大小

Collections 工具类中的 sort()方法如何比较元素?

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


要求传入的待排序容器中存放的对象必须实现 Comparable 接口,该接口提供了比较元素的compareTo()方法,当插入元素时会调用该方法比较元素的大小

可以传入集合中的元素没有实现Comparable接口的对象,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),也就是你需要定义一个比较器,然后sort()方法比较实际上就是调用这个比较器的 compare 方法来进行比较


第二点举例

Queue

BlockingQueue是什么?

BlockingQueue是什么?


BlockingQueue 是 Java 并发编程中的一个接口,它继承自 Queue 接口,表示一个支持阻塞的队列,用于在多线程之间传递数据。当队列满时,会阻塞入队的线程,直到队列不满;当队列为空时,会阻塞出队的线程,直到队列中有元素。


BlockingQueue 是线程安全的队列,它是在多线程环境下使用的,具有线程安全的特性。


BlockingQueue 的实现通常使用了同步机制,如锁、条件变量等,来保证多个线程之间的安全访问。在多线程环境下,多个线程可以同时对 BlockingQueue 进行读写操作,而不会产生数据竞争等线程安全问题。


例如,在使用 ArrayBlockingQueue 时,put() 和 take() 方法都需要先获取锁,然后在条件变量上等待或者唤醒其他线程,以保证线程安全。而在使用 LinkedBlockingQueue 时,不同的线程可以同时对队列进行读写操作,因为它使用了两个锁,一个用于入队操作,一个用于出队操作,以避免竞争和死锁问题。


需要注意的是,虽然 BlockingQueue 是线程安全的队列,但在实际使用时,仍需要注意一些细节问题,如队列容量大小、线程池大小等,以避免因为不当使用而导致的性能问题或者死锁问题。


BlockingQueue 接口提供了多种阻塞方法,包括 put()、take()、offer()、poll() 等,可以用于实现生产者-消费者模型、线程池等多种并发场景。


BlockingQueue 接口的主要特点是:当队列为空时,take() 方法会阻塞等待元素的到来;当队列已满时,put() 方法会阻塞等待队列空闲。这种阻塞等待的机制可以避免多线程之间的竞争和死锁问题,提高了程序的健壮性和可靠性。


BlockingQueue主要提供了四类方法,如下表所示:


1686470100050.png

除了抛出异常返回特定值方法与Queue接口定义相同外,BlockingQueue还提供了两类阻塞方法:一种是当队列没有空间/元素时一直阻塞,直到有空间/有元素;另一种是在特定的时间尝试入队/出队,等待时间可以自定义。



主要实现类

BlockingQueue 接口的常见实现类包括 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等。其中,ArrayBlockingQueue 和 LinkedBlockingQueue 是基于数组和链表实现的阻塞队列,SynchronousQueue 是一种特殊的阻塞队列,它不存储元素,只是在生产者和消费者之间进行同步。


1686470162366.png

其中在日常开发中用的比较多的是ArrayBlockingQueue和LinkedBlockingQueue

在 Queue 中 poll()和 remove()有什么区别?

在 Queue 接口中,poll() 和 remove() 都是用于获取并移除队列头部元素的方法,但它们的行为略有不同。


1. poll() 方法:从队列头部获取并移除一个元素,如果队列为空,则返回 null。


2. remove() 方法:从队列头部获取并移除一个元素,如果队列为空,则抛出 NoSuchElementException 异常。


简单来说,poll() 方法在队列为空时返回 null,而 remove() 方法在队列为空时抛出异常。


因此,在使用 Queue 接口时,如果不确定队列是否为空,可以使用 poll() 方法来获取并移除队列头部元素,如果队列为空,则返回 null;如果确定队列不为空,可以使用 remove() 方法来获取并移除队列头部元素,如果队列为空,则抛出异常。


需要注意的是,在使用 remove() 方法时,如果队列为空,会抛出 NoSuchElementException 异常,因此需要进行异常处理或者使用 try-catch 语句来避免程序崩溃。


HashMap

HashMap求桶的位置

jdk1.7

在 JDK 1.7 中,HashMap 的桶位置的计算方式与 JDK 1.8 有所不同。具体地,JDK 1.7 中 HashMap 的桶位置的计算方式如下:


1. 先计算键的哈希值 `h`,使用的是 `key.hashCode()` 方法。


2. 然后通过以下公式计算桶位置 `i`:   i = (n - 1) & h


  其中,`n` 表示 `table` 数组的长度。由于在 JDK 1.7 中,`table` 的长度不一定是 2 的幂次方,因此需要使用 `n - 1` 来保证 `i` 的值在 `table` 的下标范围内。


3. 如果桶位置 `i` 上已经有元素了,那么会使用键的 `equals()` 方法来比较键是否相等。如果键相等,那么就将该键对应的值替换为新值;如果键不相等,那么就将该键值对插入到链表的末尾。


需要注意的是,在 JDK 1.7 中,HashMap 的扩容机制与 JDK 1.8 有所不同。具体来说,当 HashMap 中的元素个数超过了负载因子与容量的乘积时,HashMap 会将容量扩大为原来的两倍,并将所有元素重新分配到新的桶中。这种做法可能会导致在扩容时,多个键映射到同一个桶中,从而降低 HashMap 的性能。


jdk1.8

HashMap求桶的位置一共分为三个过程

  • 求key的hashcode
  • 调用hash函数得到hash值,将hashcode的高16位和低16位进行异或操作。


static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// >>> 是无符号右移符号位
}
  • 通过(length- 1) & hash ,将hash值与length-1进行与操作,求桶的位置(其中hash值是上面hash函数得到的值,length是table数组的长度)
 if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);


骚戴理解:不是得到hash值就知道把元素存在数组中的哪个位置,而是还要经过(length- 1) & hash之后才可以得到数组的位置,这样是为了减少hash冲突

说一下 HashMap 的实现原理?

JDK1.7

HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。(其实所谓Map其实就是保存了两个对象之间的映射关系的一种集合),其中Key 和 Value 允许为null。这些个键值对(Entry)分散存储在一个数组当中,HashMap数组每一个元素的初始值都是Null。


骚戴理解:HashMap不能保证映射的顺序,插入后的数据顺序也不能保证一直不变(如1.8以前的扩容操作会导致顺序变化)

Entry是HashMap中的一个静态内部类.

  static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
        int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算
        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        } 

HashMap的总体结构如下


骚戴理解:简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。


JDK1.8

JDK 1.8 中 HashMap 的底层原理是采用数组+链表+红黑树的结构实现的,主要包括以下几个方面:


1. 数组:HashMap 内部维护了一个数组,用于存储键值对。数组的长度是固定的,且必须是 2 的幂次方。


2. 链表:数组中的每个元素都是一个链表的头结点,用于存储哈希值相同的键值对。当链表长度超过8(TREEIFY_THRESHOLD - 阈值)时,链表就自行转为红黑树,以提高查找效率。


3. 红黑树:当链表长度超过一定阈值时,HashMap 会将链表转换为红黑树,以提高查找效率。红黑树是一种自平衡二叉查找树,它的查找、插入、删除等操作的时间复杂度都是 O(log n)。


4. 扩容:当 HashMap 中的元素个数超过数组长度的 75% 时,HashMap 会进行扩容操作,即将数组长度扩大一倍,并重新计算每个元素在新数组中的位置。扩容操作会导致所有元素的位置发生变化,因此需要重新计算每个元素在新数组中的位置,这个过程比较耗时。


5. hash 函数:HashMap 会使用键对象的 hashCode() 方法计算出键的哈希值,然后根据哈希值计算出键值对在数组中的位置。


6. 线程安全:JDK 1.8 中的 HashMap 是线程不安全的,如果多个线程同时对 HashMap 进行写操作,可能会导致数据丢失或者出现死循环等问题。如果需要在多线程环境下使用 HashMap,可以使用 ConcurrentHashMap。


hashmap常说的桶是什么

HashMap 中的桶指的是存储键值对的数组元素,也称为哈希桶或哈希表节点。


在 HashMap 内部,键值对会被存储在一个数组中,这个数组被称为哈希表。每个数组元素都是一个桶,它可以存储一个或多个键值对。当 HashMap 中的键值对被添加到哈希表中时,会根据键的哈希值计算出该键值对应该存储在哪个桶中。如果桶中已经有一个或多个键值对,那么新的键值对会被添加到这个桶中,成为这个桶中的一个元素。


为了解决哈希冲突问题,HashMap 的每个桶不仅可以存储一个键值对,还可以存储多个键值对。当多个键值对的哈希值相同时,它们会被添加到同一个桶中,成为这个桶中的一个链表或红黑树。HashMap 会根据链表长度或红黑树的节点数来决定采用哪种数据结构来存储键值对,从而提高查找效率。


因此,桶是 HashMap 中存储键值对的基本单位,它是实现 HashMap 内部数据结构的核心。


骚戴理解:简单来说,所谓的桶指的就是entry数组的元素,每一个entry都是一个桶


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

HashMap 是 Java 中非常常用的一种数据结构,它可以用来存储键值对,并且可以根据键快速地查找对应的值。在 JDK 1.7 和 JDK 1.8 中,HashMap 的底层实现有以下几点不同:


1. 底层数组的初始化方式不同。在 JDK 1.7 中,底层数组的初始化是在第一次插入元素时进行的,而在 JDK 1.8 中,底层数组的初始化是在创建 HashMap 对象时进行的。


2. 扩容机制不同。在 JDK 1.7 中,当元素个数超过容量的 75% 时,HashMap 会进行扩容操作,即将容量扩大为原来的两倍,并将所有元素重新计算位置。这种扩容策略可能会导致大量元素在同一个桶中,从而导致链表过长,查询效率降低。而在 JDK 1.8 中,当某个桶中的链表长度大于等于 8 时,会判断当前的 HashMap 的容量是否大于等于 64。如果小于 64,则会进行 2 倍扩容;如果大于等于 64,则将链表转换为红黑树,以提高查询效率。


3. hash 函数的计算方式不同。在 JDK 1.7 中,HashMap 的 hash 函数直接使用键对象的 hashCode() 方法计算出键的哈希值,然后根据哈希值计算出键值对在数组中的位置。这种计算方式可能会导致哈希冲突,即不同的键对象计算出的哈希值相同,从而导致元素存储在同一个桶中,链表过长,查询效率降低。而在 JDK 1.8 中,HashMap 的 hash 函数进行了优化。它首先调用键对象的 hashCode() 方法计算出键的哈希值,然后将哈希值的高 16 位和低 16 位进行异或操作,得到一个 int 类型的哈希值。这种计算方式可以避免只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,使元素分布更均匀。


4. 在 JDK 1.8 中,当发生哈希冲突时,采用的是尾插法,即将新的节点插入到链表的尾部。这样做可以保持链表中节点的顺序,使得链表的遍历顺序和插入顺序一致,从而提高了查询效率。在扩容时,1.8 会保持原链表的顺序,即将链表中的节点按照插入顺序重新排列,而不是颠倒链表的顺序。在 JDK 1.7 中,当发生哈希冲突时,采用的是头插法,即将新的节点插入到链表的头部。这样做可以保证在遍历链表时,先遍历到最近插入的节点,从而提高了查询效率。在扩容时,1.7 会颠倒链表的顺序,即将链表中的节点按照相反的顺序重新排列。


5. 在 JDK 1.8 中,HashMap 会在元素插入后检测是否需要扩容,即先插入元素,再检测是否需要扩容。而在 JDK 1.7 中,HashMap 会在元素插入前检测是否需要扩容,即先检测是否需要扩容,再插入元素。这样做的目的是为了避免在插入元素时频繁地进行扩容操作,从而提高了插入元素的效率。


目录
相关文章
|
8天前
|
安全 Java 大数据
|
5天前
|
Java
Java面向对象实践小结(含面试题)(下)
Java面向对象实践小结(含面试题)(下)
15 1
|
3天前
|
安全 Java
循环的时候去删除集合中的元素 java.util.ConcurrentModificationException
循环的时候去删除集合中的元素 java.util.ConcurrentModificationException
|
5天前
|
设计模式 Java
Java面向对象实践小结(含面试题)(上)
Java面向对象实践小结(含面试题)
12 1
|
7天前
|
JavaScript 前端开发 Java
【JAVA面试题】什么是引用传递?什么是值传递?
【JAVA面试题】什么是引用传递?什么是值传递?
|
7天前
|
Java 程序员
【JAVA面试题】基本类型的强制类型转换是否会丢失精度?引用类型的强制类型转换需要注意什么?
【JAVA面试题】基本类型的强制类型转换是否会丢失精度?引用类型的强制类型转换需要注意什么?
|
7天前
|
Java
【JAVA面试题】什么是深拷贝?什么是浅拷贝?
【JAVA面试题】什么是深拷贝?什么是浅拷贝?
|
7天前
|
安全 Java 测试技术
Java并发容器总结(下)
Java并发容器总结(下)
9 0
|
7天前
|
存储 安全 Java
Java并发容器总结(上)
Java并发容器总结(上)
7 0
|
7天前
|
算法 安全 搜索推荐
Java集合常见工具类
Java集合常见工具类
6 0

热门文章

最新文章