秋招—java集合

1. 集合概述

1.1 Java 集合概览

Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 Collection接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于Collection 接口,下面又有三个主要的子接口:ListSetQueue

Java 集合可分为 Collection 和 Map 两大体系:

  • Collection接口:用于存储一个一个的数据,也称单列数据集合
    • List子接口:用来存储有序的、可以重复的数据(主要用来替换数组,"动态"数组)
      • 实现类:ArrayList(主要实现类)、LinkedList、Vector
    • Set子接口:用来存储无序的、不可重复的数据(类似于高中讲的"集合")
      • 实现类:HashSet(主要实现类)、LinkedHashSet、TreeSet
  • Map接口:用于存储具有映射关系“key-value对”的集合,即一对一对的数据,也称双列数据集合。(类似于高中的函数、映射。(x1,y1),(x2,y2) —> y = f(x) )
    • HashMap(主要实现类)、LinkedHashMap、TreeMap、Hashtable、Properties
  • JDK提供的集合API位于java.util包内
  • 图示:集合框架全图
    在这里插入图片描述
    简图1:Collection接口继承树
    在这里插入图片描述
    简图2:Map接口继承树
    在这里插入图片描述
1.2 List, Set, Queue, Map 四者的区别
  • List(对付顺序的好帮手): 存储的元素是有序的、可重复的。
  • Set(注重独一无二的性质): 存储的元素是无序的、不可重复的。
  • Queue(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
  • Map(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),“x” 代表 key,“y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
1.3 集合框架底层数据结构总结

List(有序可重复)

  • ArrayList(主要实现类):线程不安全,效率高。底层使用Object[]数组存储在添加数据,查找数据时,效率较高;在插入,删除数据时,效率较低。
  • LinkedList:底层使用双向链表的方式进行存储;在对集合中的数据进行频繁删除插入时建议使用。在插入,删除数据时,效率较高;在添加数据,查找数据时,效率较低。
  • Vector(List的古老实现类),线程安全,效率低。底层使用Object[]数组存储

Set(无序,唯一)

  • HashSet(主要实现类):底层使用的是HashMap,即使用数组+单向链表+红黑树结构进行存储的。(jdk8)
  • LinkedHashSet:是HashSet的子类,内部是通过 LinkedHashMap来实现的,在现有的数组+单向链表+红黑树结构的基础上,又添加了一组双向链表,用于记录添加元素的先后顺序。即:我们可以按照添加元素的顺序实现遍历。便于频繁的查询操作。
  • TreeSet:底层使用红黑树存储。可以按照添加的元素的指定的属性大小顺序进行遍历

Queue

  • PriorityQueue: Object[] 数组来实现二叉堆
  • ArrayQueue: Object[] 数组 + 双指针

Map

  • HashMap:主要实现类;线程不安全的,效率高;可以添加null的key和value值;底层使用数组+单向链表+红黑树结构存储(jdk8)。当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
  • LinkedHashMap:是HashMap的子类;在HashMap使用的数据结构的基础上,增加了一对双向链表,用于记录添加的元素的先后顺序, 进而我们在遍历元素时,就可以按照添加的顺序显示。开发中,对于频繁的遍历操作,建议使用此类。
  • TreeMap:底层使用红黑树存储;可以按照添加的key-value中的key元素的指定的属性的大小顺序进行遍历。需要考虑使用①自然排序 ②定制排序。
  • Hashtable:古老实现类;线程安全的,效率低;**不可以添加null的key或value值;**底层使用数组+单向链表结构存储(jdk8)。
  • Properties:其key和value都是String类型。常用来处理属性文件。
1.4 为什么使用集合(数组的特点和弊端)
  • 一方面,面向对象语言对事物的体现都是以对象的形式,为了方便对多个对象的操作,就要对对象进行存储。
  • 另一方面,使用数组存储对象方面具有一些弊端,而Java 集合就像一种容器,可以动态地把多个对象的引用放入容器中。
  • 数组在内存存储方面的特点
    • 数组初始化以后,长度就确定了。
    • 数组中的添加的元素是依次紧密排列的,有序的,可以重复的。
    • 数组声明的类型,就决定了进行元素初始化时的类型。不是此类型的变量,就不能添加。
    • 可以存储基本数据类型值,也可以存储引用数据类型的变量
  • 数组在存储数据方面的弊端
    • 数组初始化以后,长度就不可变了,不便于扩展
    • 数组中提供的属性和方法少,不便于进行添加、删除、插入、获取元素个数等操作,且效率不高。
    • 数组存储数据的特点单一,只能存储有序的、可以重复的数据
  • Java 集合框架中的类可以用于存储多个对象,还可用于保存具有映射关系的关联数组。

2. Collection 子接口之 List

2.1 ArrayList 和 Vector 的区别
  • ArrayListList 的主要实现类,底层使用 Object[]存储,适用于频繁的查找工作,线程不安全 ;当添加第11个元素时,需要扩容。默认扩容为原来的1.5倍。
  • VectorList 的古老实现类,底层使用Object[] 存储,线程安全的。当添加第11个元素时,需要扩容。默认扩容为原来的2倍。
2.2 ArrayList 与 LinkedList 区别

是否保证线程安全: ArrayListLinkedList 都是不同步的,也就是不保证线程安全

底层数据结构: ArrayList 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。)

插入和删除是否受元素位置的影响:

  • ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
  • LinkedList 采用链表存储,所以,如果是在头尾插入或者删除元素不受元素位置的影响(add(E e)addFirst(E e)addLast(E e)removeFirst()removeLast()),时间复杂度为 O(1),如果是要在指定位置 i 插入和删除元素的话(add(int index, E element)remove(Object o)), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入。
  • 若增删元素的位置是在链表头,则LinkedList较快。
  • 如果增删都是在末尾来操作【每次调用的都是 remove() 和 add() 】,此时 ArrayList 就不需要移动和复制数组 来进行操作了。如果数据量有百万级的时,速度是会比 LinkedList 要快的。
  • 如果删除操作的位置是在中间。由于 LinkedList 的消耗主要是在遍历上, ArrayList 的消耗主要是在移动和复 制上(底层调用的是 arrayCopy() 方 法,是 native 方 法)。 LinkedList 的遍历速度是要慢于 ArrayList 的复制 移动速度的如果数据量有百万级的时,还是 ArrayList 要快。

是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList(实现了RandomAccess接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。

内存空间占用: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

2.3 ArrayList 实现 RandomAccess 接口有何作用?为何 LinkedList 却没实现这个接口

​RandomAccess 接口只是⼀个标志接口,只要 List 集合实现这个接口,就能支持快速随机访问。通过 Collections 类中的 binarySearch() 方法,可以看出,判断 List 是否实现 RandomAccess 接口来执行indexedBinarySerach(list, key) 或 iteratorBinarySerach(list, key)方法。

public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
    if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
        return Collections.indexedBinarySearch(list, key);
    else
        return Collections.iteratorBinarySearch(list, key);
}

再通过查看这两个方法的源码发现:实现 RandomAccess 接口的 List 集合采⽤⼀般的 for 循环遍历,而未实现这接口则采用迭代器,即 ArrayList ⼀般采用 for 循环遍历,而 LinkedList ⼀般采用迭代器遍历;ArrayList 用 for 循环遍历比 iterator 迭代器遍历快, LinkedList 用 iterator 迭代器遍历比 for 循环遍历快。

ArrayList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。

链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。

2.4 ArrayList 的扩容机制

ArrayList 是一种变长的集合类,基于定长数组实现,使用默认构造方法初始化出来的容量是10(1.7之后都是延迟初始化,即第一次调用add方法添加元素的时候才将elementData容量初始化为10)

//jdk1.8
//如下代码的执行:底层会初始化数组,即:Object[] elementData = new Object[]{};
ArrayList<String> list = new ArrayList<>();
//首次添加元素时,会初始化数组elementData = new Object[10];elementData[0] = "AA";
list.add("AA"); 
//jdk1.7
Object[] elementData = new Object[10];

ArrayList 允许空值和重复元素,当要添加第11个元素的时候,底层的elementData数组已满,则需要扩容。默认扩容为原来长度的1.5倍。并将原有数组中的元素复制到新的数组中

由于 ArrayList 底层基于数组实现,所以其可以保证在 O(1) 复杂度下完成随机查找操作。

具体为:

  1. 当使用add方 法的时候首先调用ensureCapacityInternal方法,传⼊ size+1 进去,检查是否需要扩充 elementData 数组的大小;
  2. newCapacity = 扩充数组为原来的 1.5 倍 ( 不能⾃定义 ),如果还不够,就使用它指定要扩充的大小minCapacity ,然后判断 minCapacity 是否⼤于 MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8) ,如果大于,就 取 Integer.MAX_VALUE ;
  3. 扩容的主要方法: grow
  4. ArrayList 中copy 数组的核心就是System.arraycopy方 法,将 original 数组的所有数据复制到 copy 数组中,这是⼀个本地方法。

为什么扩容因子为1.5

k=1.5时,就能充分利用前面已经释放的空间。如果k >= 2,新容量刚刚好永远大于过去所有废弃的数组容量。可以充分利用移位操作,减少浮点数或者运算时间和运算次数。

  1. 扩容容量不能太小,防止频繁扩容,频繁申请内存空间 + 数组频繁复制
  2. 扩容容量不能太大,需要充分利用空间,避免浪费过多空间;
//底层会初始化数组,即:Object[] elementData = new Object[]{};
ArrayList<String> list = new ArrayList<>();
//首次添加元素
list.add("AA"); //elementData[0] = "AA";
public boolean add(E e) {
    //因为要添加元素,所以添加之后可能导致容量不够,所以需要在添加之前进行判断(扩容)
    ensureCapacityInternal(size + 1);  // size + 1 为 1;
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {//minCapacity为1
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //判断elementData数组是不是为空数组
    //(使用的无参构造的时候,elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
    //如果是,那么比较size+1(第一次调用add的时候size+1=1)和DEFAULT_CAPACITY,
    //那么显然容量为10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)// 10-0>0需要扩容
        grow(minCapacity);
}

下面来看一下扩容的主要方法grow。

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;//0
    int newCapacity = oldCapacity + (oldCapacity >> 1);//0
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;//10
    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);//复制到一个新数组里
}

在这里插入图片描述

5 Array 和 ArrayList 有何区别?什么时候更适合用 Array ?
  1. Array 可以容纳基本类型和对象,而 ArrayList 只能容纳对象;
  2. Array 是指定大小的,而 ArrayList 大小 是不固定的。

什么时候更适合用 Array ?

如果列表的大小已经指定,⼤部分情况下是存储和遍历它们;

对于遍历基本数据类型,尽管 Collections 使用自动装箱来减轻编码任务,在指定大小的基本类型的列表上工作也会变得很慢;

如果你要使用多维数组,比 List 更容易

2.6 遍历ArrayList的时候,删除一个元素

如果直接使用for循环的话会导致漏删,因为在for循环时,数组会调整数组的下标,导致漏删。下标位置在不断调整,整个数组就和集体往前移动,而i也在++ 。正确的做法是利用for循环从后往前遍历,或者使用Iterator遍历,并调用自身的remove方法删除。Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性!

for(int i=0;i<list.size();i++) 

如上述代码,i++在不断地+1,而list.size()也有可能会减少(如果满足添加删除元素就会减少),从而导致漏掉。

需要注意的是,使用Iterator遍历的时候,不能不允许并发调用ArrayList的remove/add操作进行修改,否则会抛出异常。原理是:Iterator是工作在一个独立的线程中,而且拥有一个mutex锁。Iterator在建立后会创建一个指向原来对象的单索引链表。当原来的对象元素发生改变(增加或者删除一个元素),这个索引表是不会同步改变的。所以当索引指针往后移动的时候就找不到要迭代的对象,这时候就会触发fast-fail快速失败机制

public class ListTest {

    public static void main(String[] args) {

        List list = new ArrayList();
        list.add(3);
        list.add(4);
        list.add(5);

        Iterator iterator = list.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
            list.add(3);
        }
    }
}

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
	at java.util.ArrayList$Itr.next(ArrayList.java:861)
	at ListTest.main(ListTest.java:21)

Process finished with exit code 1

fail-fast 机制,即快速失败机制,是java集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出 ConcurrentModificationException异常。那么,如果你在遍历的时候,调用list的add()或者remove(),使得集合的结构发生改变。Iterator 会马上抛出 java.util.ConcurrentModificationException 异常。如果并发操作,需要对 Iterator 对象加锁。

3. Collection 子接口之 Set

3.1 无序性和不可重复性的含义是什么

无序性: != 随机性
与添加的元素的位置有关,存储的数据在底层数组中并非按照数组索引的顺序添加,不像ArrayList一样是依次紧密相连的。
这里是根据添加的元素的哈希值,计算其在数组中的存储位置。此位置不是依次排列的,表现为无序性。
不可重复性:
添加到Set元素是不能相同的。比较的标准是判断hashCode()得到的哈希值以及equals()得到的boolean型结果

3.2 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
  • HashSetLinkedHashSetTreeSet 都是 Set 接口的实现类,都能保证元素唯一,并且都不是线程安全的。
  • HashSetLinkedHashSetTreeSet 的主要区别在于底层数据结构不同。HashSet 的底层数据结构是哈希表(基于 HashMap 实现)即数组+单向链表+红黑树结构LinkedHashSet 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFOTreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。
  • 底层数据结构不同又导致这三者的应用场景不同。HashSet 用于不需要保证元素插入和取出顺序的场景,LinkedHashSet 用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet 用于支持对元素自定义排序规则的场景
3.3 comparable 和 Comparator 的区别
  • comparable 接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序
  • comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序

一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()方法或compare()方法,当我们需要对某一个集合实现两种排序方式,比如一个 song 对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo()方法和使用自制的Comparator方法或者以两个 Comparator 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 Collections.sort()

comparatorcomparable
排序代码实现在调用时实现在需排序对象类中实现被称为自然排序
实现实现comparator接口的compare方法实现comparator接口的compareTo方法
所在包java.utiljava.lang
触发排序Collections.sort(list,new Comparator(){…})Collections.sort(list)

他们之间的区别在于用法上。Comparator 是一个专用的比较器,当这个对象不支持自比较或者自比较函数不能满足你的要求时,你可以写一个比较器来完成两个对象之间大小的比较。Comparable 是一个对象本身就已经支持自比较所需要实现的接口(如 String、Integer 自己就可以完成比较大小操作,已经实现了Comparable接口),对自己写的类实现这一接口来实现按自己规则排序。总结起来就是comparator在外,comparable在内。

自然排序:java.lang.Comparable

  • Comparable接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序。
  • 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。如果当前对象this大于形参对象obj,则返回正整数,如果当前对象this小于形参对象obj,则返回负整数,如果当前对象this等于形参对象obj,则返回零。
package java.lang;

public interface Comparable{
    int compareTo(Object obj);
}
  • 实现Comparable接口的对象列表(和数组)可以通过 Collections.sort 或 Arrays.sort进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
  • 对于类 C 的每一个 e1 和 e2 来说,当且仅当 e1.compareTo(e2) == 0 与 e1.equals(e2) 具有相同的 boolean 值时,类 C 的自然排序才叫做与 equals 一致。建议(虽然不是必需的)最好使自然排序与 equals 一致
  • Comparable 的典型实现:(默认都是从小到大排列的)
    • String:按照字符串中字符的Unicode值进行比较
    • Character:按照字符的Unicode值来进行比较
    • 数值类型对应的包装类以及BigInteger、BigDecimal:按照它们对应的数值大小进行比较
    • Boolean:true 对应的包装类实例大于 false 对应的包装类实例
    • Date、Time等:后面的日期时间比前面的日期时间大

定制排序:java.util.Comparator

  • 思考
    • 当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码(例如:一些第三方的类,你只有.class文件,没有源文件)
    • 如果一个类,实现了Comparable接口,也指定了两个对象的比较大小的规则,但是此时此刻我不想按照它预定义的方法比较大小,但是我又不能随意修改,因为会影响其他地方的使用,怎么办?
  • JDK在设计类库之初,也考虑到这种情况,所以又增加了一个java.util.Comparator接口。强行对多个对象进行整体排序的比较。
    • 重写compare(Object o1,Object o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
    • 可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。
package java.util;

public interface Comparator{
    int compare(Object o1,Object o2);
}
Arrays.sort(all, new Comparator() {

    @Override
    public int compare(Object o1, Object o2) {
        Goods g1 = (Goods) o1;
        Goods g2 = (Goods) o2;

        return g1.getName().compareTo(g2.getName());
    }
});
3.4 添加到HashSet/LinkedHashSet中元素的要求:

要求元素所在的类重写两个方法:equals()和hashCode()
同时要求equals()和hashCode()保持一致。

3.5 TreeSet的使用

可以按照添加的元素的指定的属性大小顺序进行遍历。

向TreeSet中添加的元素的要求:

向TreeSet中添加的元素必须是同一个类型的对象,否则会报ClassCastException。
添加的元素需要考虑排序:自然排序和定制排序

判断数据是否相同的标准

不在是考虑hashCode()和equals()方法了。也就意味着添加到TreeSet的元素所在的类不需要重写hashCode()和equals()方法
比较元素大小的或者比较元素是否相等的标准就是考虑自然排序或定制排序,compareTo()或compare()的返回值。
如果compareTo()或compare()的返回值为0,则认为两个对象是相等的。由于TreeSet中不能存放相同的元素,则后一个相等的元素就不能 添加到TreeSet中。

4. Collection 子接口之 Queue

4.1 Queue 与 Deque 的区别

Queue 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。

Queue 扩展了 Collection 的接口,根据 因为容量问题而导致操作失败后处理方式的不同 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值

Queue 接口抛出异常返回特殊值
插入队尾add(E e)offer(E e)
删除队首remove()poll()
查询队首元素element()peek()

Deque 是双端队列,在队列的两端均可以插入或删除元素。

Deque 扩展了 Queue 的接口, 增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类:

Deque 接口抛出异常返回特殊值
插入队首addFirst(E e)offerFirst(E e)
插入队尾addLast(E e)offerLast(E e)
删除队首removeFirst()pollFirst()
删除队尾removeLast()pollLast()
查询队首元素getFirst()peekFirst()
查询队尾元素getLast()peekLast()

事实上,Deque 还提供有 push()pop() 等其他方法,可用于模拟栈。

4.2 ArrayDeque 与 LinkedList 的区别
  • ArrayDequeLinkedList 都实现了 Deque 接口,两者都具有队列的功能。
  • ArrayDeque 是基于可变长的数组和双指针来实现,而 LinkedList 则通过链表来实现。
  • ArrayDeque 不支持存储 NULL 数据,但 LinkedList 支持。
  • ArrayDeque 是在 JDK1.6 才被引入的,而LinkedList 早在 JDK1.2 时就已经存在。
  • ArrayDeque 插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 LinkedList 不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。

从性能的角度上,选用 ArrayDeque 来实现队列要比 LinkedList 更好。此外,ArrayDeque 也可以用于实现栈。

4.3 说一说 PriorityQueue

PriorityQueue 是在 JDK1.5 中被引入的, 其与 Queue 的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队。

这里列举其相关的一些要点:

  • PriorityQueue 利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据
  • PriorityQueue 通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。
  • PriorityQueue 是非线程安全的,且不支持存储 NULLnon-comparable 的对象。
  • PriorityQueue 默认是小顶堆,但可以接收一个 Comparator 作为构造参数,从而来自定义元素优先级的先后。

PriorityQueue 在面试中可能更多的会出现在手撕算法的时候,典型例题包括堆排序、求第K大的数、带权图的遍历等,所以需要会熟练使用才行。

5. Map

5.1 HashMap中key-value特点

HashMap中的所有的key彼此之间是不可重复的、无序的。所有的key就构成一个Set集合。—>key所在的类要重写hashCode()和equals()

HashMap中的所有的value彼此之间是可重复的、无序的。所有的value就构成一个Collection集合。—>value所在的类要重写equals()

HashMap中的一个key-value,就构成了一个entry。

HashMap中的所有的entry彼此之间是不可重复的、无序的。所有的entry就构成了一个Set集合。
在这里插入图片描述

5.2 HashMap 和 Hashtable 的区别

线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);

效率: 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它;

对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException

初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,HashMap 会将其扩充为 2 的幂次方大小HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。

底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。Hashtable 没有这样的机制。

HashMap 中带有初始容量的构造函数:

/**
*有参
*/
public HashMap(int initialCapacity, float loadFactor) {
    //校验initialCapacity合法性
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //校验initialCapacity合法性 
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //校验loadFactor合法性
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    //确定加载因子
    this.loadFactor = loadFactor;
    // threshold 初始为默认容量
    this.threshold = tableSizeFor(initialCapacity);
}

/**
 * 默认
 */
public HashMap(int initialCapacity) {
    //DEFAULT_INITIAL_CAPACITY:默认初始容量16
  	//DEFAULT_LOAD_FACTOR:默认加载因子0.75
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 包含另一个“Map”的构造函数
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);//下面会分析到这个方法
}

// 指定“容量大小”的构造函数
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

下面这个方法保证了 HashMap 总是使用 2 的幂作为哈希表的大小。

/**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
5.3 HashMap 和 HashSet 区别

HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone()writeObject()readObject()HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。

HashMapHashSet
实现了 Map 接口实现 Set 接口
存储键值对仅存储对象
调用 put()向 map 中添加元素调用 add()方法向 Set 中添加元素
HashMap 使用键(Key)计算 hashcodeHashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals()方法用来判断对象的相等性
5.4 HashMap 和 TreeMap 区别

TreeMapHashMap 都继承自AbstractMap ,但是需要注意的是TreeMap它还实现了NavigableMap接口和SortedMap 接口。
在这里插入图片描述
实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。

实现SortedMap接口让 TreeMap 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序。

综上,相比于HashMap来说 TreeMap 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。

5.5 HashSet 在添加元素的时候如何检查重复?

​ 当你把对象加入HashSet时,HashSet 会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcodeHashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。

实际上无论HashSet中是否已经存在了某元素,HashSet都会直接插入,只是会在add()方法的返回值处告诉我们插入前是否存在相同元素。

5.6 HashMap 的底层实现

JDK1.8 之前

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。创建对象的过程中,底层会初始化数组Entry[] table = new Entry[16];

HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

JDK1.8 之后

相比于之前的版本,JDK1.8 以后在解决哈希冲突时有了较大的变化。

当链表长度大于阈值(默认为 8)时,会首先调用 treeifyBin()方法。这个方法会根据 HashMap 数组来决定是否转换为红黑树。只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是执行 resize() 方法对数组扩容。重点关注 treeifyBin()方法!

下面说明是JDK8相较于JDK7的不同之处:
①使用HashMap()的构造器创建对象时,并没有在底层初始化长度为16的table数组。
②jdk8中添加的key,value封装到了HashMap.Node类的对象中。而非jdk7中的HashMap.Entry。
③jdk8中新增的元素所在的索引位置如果有其他元素。在经过一系列判断后,如果能添加,则是旧的元素指向新的元素。而非jdk7中的新的元素指向旧的元素。“七上八下”
④jdk7时底层的数据结构是:数组+单向链表。 而jdk8时,底层的数据结构是:数组+单向链表+红黑树。
红黑树出现的时机:当某个索引位置i上的链表的长度达到8,且数组的长度超过64时,此索引位置上的元素要从单向链表改为红黑树。
如果索引i位置是红黑树的结构,当不断删除元素的情况下,当前索引i位置上的元素的个数低于6时,要从红黑树改为单向链表。

在这里插入图片描述
HashMap的属性:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的初始容量 16
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量  1 << 30
static final float DEFAULT_LOAD_FACTOR = 0.75f;  //默认加载因子
static final int TREEIFY_THRESHOLD = 8; //默认树化阈值8,当链表的长度达到这个值后,要考虑树化
static final int UNTREEIFY_THRESHOLD = 6;//默认反树化阈值6,当树中结点的个数达到此阈值后,要考虑变为链表

//当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。
//当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容
static final int MIN_TREEIFY_CAPACITY = 64; //最小树化容量64

transient Node<K,V>[] table; //数组
transient int size;  //记录有效映射关系的对数,也是Entry对象的个数
int threshold; //阈值,当size达到阈值时,考虑扩容
final float loadFactor; //加载因子,影响扩容的频率
  • loadFactor 加载因子

    loadFactor 加载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。

    loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值

    给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

  • threshold

    threshold = capacity * loadFactor当 Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准
    扩容条件:

随着不断的添加元素,在满足如下的条件的情况下,会考虑扩容:
(size >= threshold) && (null != table[i])
当元素的个数达到临界值(-> 数组的长度 * 加载因子)时,就考虑扩容。默认的临界值 = 16 * 0.75 --> 12.
默认扩容为原来的2倍。

HashMap的添加/修改过程(jdk7):

添加/修改的过程:
将(key1,value1)添加到当前的map中:
首先,需要调用key1所在类的hashCode()方法,计算key1对应的哈希值1,此哈希值1经过某种算法(hash())之后,得到哈希值2。
哈希值2再经过某种算法(indexFor())之后,就确定了(key1,value1)在数组table中的索引位置i。
  1.1 如果此索引位置i的数组上没有元素,则(key1,value1)添加成功。  ---->情况1
  1.2 如果此索引位置i的数组上有元素(key2,value2),则需要继续比较key1和key2的哈希值2  --->哈希冲突
         2.1 如果key1的哈希值2与key2的哈希值2不相同,则(key1,value1)添加成功。   ---->情况2
         2.2 如果key1的哈希值2与key2的哈希值2相同,则需要继续比较key1和key2的equals()。要调用key1所在类的equals(),将key2作为参数传递进去。
               3.1 调用equals(),返回false:(key1,value1)添加成功。   ---->情况3
               3.2 调用equals(),返回true: 则认为key1和key2是相同的。默认情况下,value1替换原有的value2。

HashMap的添加/修改过程(jdk8):

添加/修改的过程:
将(key1,value1)添加到当前的map中:
首先,需要调用key1所在类的hashCode()方法,计算key1对应的哈希值1,此哈希值1经过某种算法(hash())之后,得到哈希值2。
哈希值2再经过某种算法(indexFor())之后,就确定了(key1,value1)在数组table中的索引位置i。
  1.1 如果此索引位置i的数组上没有元素,则(key1,value1)添加成功。  ---->情况1
  1.2 如果此索引位置i的数组上有元素(key2,value2),则需要继续比较key1和key2的哈希值2  --->哈希冲突
         2.1 如果key1的哈希值2与key2的哈希值2不相同,则(key1,value1)直接覆盖value2。   ---->情况2
         2.2 如果key1的哈希值2与key2的哈希值2相同,则需要继续比较key1和key2的equals()3.1 判断table[key1]是否为红黑树,如果是红黑树,直接在树中插入。
               3.2 如果不是红黑树,则遍历链表,判断key是否存在。
                    4.1 调用equals(),返回false:(key1,value1)添加成功。   ---->情况3
                    4.2 调用equals(),返回true: 则认为key1和key2是相同的。默认情况下,value1替换原有的value2。

putMapEntries 方法:

//将一个map,批量添加到当前map中,此方法,在构造方法入参为map时,调用putAll时,调用clone时调用。
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        // 判断table是否已经初始化
        if (table == null) { // pre-size
            // 未初始化,s为m的实际元素个数
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                    (int)ft : MAXIMUM_CAPACITY);
            // 计算得到的t大于阈值,则初始化阈值
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        // 已初始化,并且m元素个数大于阈值,进行扩容处理
        else if (s > threshold)
            resize();
        // 将m中的所有元素添加至HashMap中
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

当使用:

HashMap<String,Integer> map = new HashMap<>();

底层会:调用构造器完成初始化,将加载因子赋值进去。

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted (其他字段都是默认值)
}

当执行:

map.put("AA",123);

底层调用put方法:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

其中,hash(key)为:

static final int hash(Object key) {
    int h;
    //如果key是null,hash是0
	//如果key非null,用key的hashCode值 与 key的hashCode值高16进行异或
	//		即就是用key的hashCode值高16位与低16位进行了异或的干扰运算
		
	/*
	index = hash & table.length-1
	如果用key的原始的hashCode值  与 table.length-1 进行按位与,那么基本上高16没机会用上。
	这样就会增加冲突的概率,为了降低冲突的概率,把高16位加入到hash信息中。
	*/
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

接着执行puVal方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; //数组
    Node<K,V> p;  //一个结点
    int n, i; //n是数组的长度   i是下标
    
    //tab和table等价
	  //如果table是空的
    if ((tab = table) == null || (n = tab.length) == 0){
      //如果table是空的,resize()完成了①创建了一个长度为16的数组②threshold = 12 n = 16
       /*
				tab = resize();
				n = tab.length;
				*/
        n = (tab = resize()).length;
	}
    //i = (n - 1) & hash ,下标 = 数组长度-1 & hash
	  //p = tab[i] 第1个结点
	  //if(p==null) 条件满足的话说明 table[i]还没有元素
    if ((p = tab[i = (n - 1) & hash]) == null){
        //把新的映射关系直接放入table[i]
        tab[i] = newNode(hash, key, value, null);
        //newNode()方法就创建了一个Node类型的新结点,新结点的next是null
    }else {
        Node<K,V> e; K k;
        //p是table[i]中第一个结点
		    //if(table[i]的第一个结点与新的映射关系的key重复)
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;//用e记录这个table[i]的第一个结点
        else if (p instanceof TreeNode){ //如果table[i]第一个结点是一个树结点
            //单独处理树结点
            //如果树结点中,有key重复的,就返回那个重复的结点用e接收,即e!=null
            //如果树结点中,没有key重复的,就把新结点放到树中,并且返回null,即e=null
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            }else {
                //table[i]的第一个结点不是树结点,也与新的映射关系的key不重复
                //binCount记录了table[i]下面的结点的个数
                for (int binCount = 0; ; ++binCount) {
                    //如果p的下一个结点是空的,说明当前的p是最后一个结点
                    if ((e = p.next) == null) {
                        //把新的结点连接到table[i]的最后
                        p.next = newNode(hash, key, value, null);
                        //如果binCount>=8-1,达到7个时
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //要么扩容,要么树化
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果key重复了,就跳出for循环,此时e结点记录的就是那个key重复的结点
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;//下一次循环,e=p.next,就类似于e=e.next,往链表下移动
                }
            }
        //如果这个e不是null,说明有key重复,就考虑替换原来的value
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e); //什么也没干
            return oldValue;
        }
    }
    ++modCount;
    
    //元素个数增加
	//size达到阈值
    if (++size > threshold)
        resize(); //一旦扩容,重新调整所有映射关系的位置
    afterNodeInsertion(evict); //什么也没干
    return null;
}

其中,resize()方法为:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table; //oldTab原来的table
    //oldCap:原来数组的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //oldThr:原来的阈值
    int oldThr = threshold;//最开始threshold是0
    
    //newCap,新容量
	//newThr:新阈值
    int newCap, newThr = 0;
    if (oldCap > 0) { //说明原来不是空数组
        if (oldCap >= MAXIMUM_CAPACITY) { //是否达到数组最大限制
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //newCap = 旧的容量*2 ,新容量<最大数组容量限制
			//新容量:32,64,...
			//oldCap >= 初始容量16
			//新阈值重新算 = 24,48 ....
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY; //新容量是默认初始化容量16
        //新阈值= 默认的加载因子 * 默认的初始化容量 = 0.75*16 = 12
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr; //阈值赋值为新阈值12,24.。。。
    //创建了一个新数组,长度为newCap,16,32,64.。。
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) { //原来不是空数组
        //把原来的table中映射关系,倒腾到新的table中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {//e是table下面的结点
                oldTab[j] = null; //把旧的table[j]位置清空
                if (e.next == null) //如果是最后一个结点
                    newTab[e.hash & (newCap - 1)] = e; //重新计算e的在新table中的存储位置,然后放入
                else if (e instanceof TreeNode) //如果e是树结点
                    //把原来的树拆解,放到新的table
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    //把原来table[i]下面的整个链表,重新挪到了新的table中
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

newNode()方法为:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    //创建一个新结点
    return new Node<>(hash, key, value, next);
}

treeifyBin()方法为:树化

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; 
    Node<K,V> e;
    //MIN_TREEIFY_CAPACITY:最小树化容量64
    //如果table是空的,或者  table的长度没有达到64
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();//先扩容
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //用e记录table[index]的结点的地址
        TreeNode<K,V> hd = null, tl = null;
        /*
			do...while,把table[index]链表的Node结点变为TreeNode类型的结点
			*/
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;//hd记录根结点
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);

        //如果table[index]下面不是空
        if ((tab[index] = hd) != null)
            hd.treeify(tab);//将table[index]下面的链表进行树化
    }
}	

treeifyBin:两种情况:

如果数组长度没有达到64 先扩容数组

//MIN_TREEIFY_CAPACITY:最小树化容量64       16 32 64
    //如果table是空的,或者  table的长度没有达到64
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();//先扩容

如果数组长度达到64:

else if ((e = tab[index = (n - 1) & hash]) != null) {
        //用e记录table[index]的结点的地址
        TreeNode<K,V> hd = null, tl = null;
        /*
			do...while,把table[index]链表的Node结点变为TreeNode类型的结点
			*/
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;//hd记录根结点
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);

整体流程图:
在这里插入图片描述

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

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。

我们首先可能会想到采用%取余的操作来实现。但是,重点来了:取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。

因为如果数组的长度为2的n次幂,那么table.length-1的二进制就是一个高位全是0,低位全是1的数字,这样才能保证每一个下标位置都有机会被用到。方便计算要添加的元素的底层的索引i。

举例:

hashCode值是   ?
table.length是16
table.length-115????????
15	 00001111
&_____________
	 00000000	[0]
	 00000001	[1]
	 00000010	[2]
	 00000011	[3]
	 ...
	 00001111    [15]
	 范围是[0,15],一定在[0,table.length-1]范围内
5.8 HashMap线程不安全

无论在 JDK7 还是 JDK8 的版本中,HashMap 都是线程不安全的,HashMap 的线程不安全主要体现在以下两个方面:

  • 在JDK7及以前的版本,表现为在多线程环境下进行扩容,由于采用头插法,位于同一索引位置的节点顺序会反掉,导致可能出现死循环的情况。
  • 在JDK8及以后的版本,表现为在多线程环境下添加元素,可能会出现数据丢失的情况。比如两个线程同时添加元素,都判断位置为空,都进入该位置添加;第一个线程插入成功后,第二个判断有元素,则会覆盖第一个插入的元素。原本应该是两个元素,最后只添加了一个元素。

在这里插入图片描述
假设T1,T2同时扩容,他们的都指向头结点A,他们的next都指向B。此时T2休眠,T1在采用头插法把元素放入新的HashMap中,此时,B的next变成指向A了。但是T2还是指向A,T2的next还是指向B,导致循环。在查找元素的时候,可能会一直查找下去。

5.9 ConcurrentHashMap底层实现

ConcurrentHashMap相对于HashMap来说,是线程安全的。

JDK1.8之前

底层数据结构:Segments数组+HashEntry数组+链表,采用分段锁保证安全性
在这里插入图片描述
1.7中ConcurrnetHashMap由很多个 Segment 组合,而每一个 Segment 是一个类似于 HashMap 的结构,所以每一个HashMap的内部可以进行扩容。但是Segment的个数一旦初始化就不能改变,默认Segment的个数是 16 个,你也可以认为ConcurrentHashMap 默认支持最多 16 个线程并发。

线程安全:Segment 继承自 ReentrantLock,是一种可重入锁;其中,HashEntry 是用于真正存储数据的地方。当对某个 HashEntry 数组中的元素进行修改时,必须首先获得该元素所属 HashEntry 数组对应的 Segment 锁。

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    // 真正存放数据的地方
    transient volatile HashEntry<K,V>[] table;
    // 键值对数量
    transient int count;
    // 阈值
    transient int threshold;
    // 负载因子
    final float loadFactor;
 
    Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
        this.loadFactor = lf;
        this.threshold = threshold;
        this.table = tab;
    }
}

1.java1.7中,在创建ConcurrentHashMap对象时,会默认调用无参构造器,创建一个数组长度16,加载因子0.75,并发度(Segment)个数16个线程的数组。

2.当进行put操作时,会进入put()方法内,调用putVal方法:

  • 将key传入put方法中,先根据key的hashcode的值找到对应的segment段。
  • 再根据segment中的put方法,尝试获取锁,如果获取失败则利用 scanAndLockForPut() 进行自旋。
  • 再次hash确定存放的hashEntry数组中的位置
  • 在链表中根据hash值和equals方法进行比较,如果相同就直接覆盖,如果不同就插入在链表中。

scanAndLockForPut这个方法做的操作就是不断的自旋 tryLock() 获取锁。当自旋次数大于指定次数时,使用 lock() 阻塞获取锁。在自旋时顺表获取下 hash 位置的 HashEntry。

简单总结一下,put 方法首先定位到 Segment,尝试获取锁,如果失败则自旋然后在 Segment 里进行插入操作,插入操作需要经历两个步骤,第一步判断是否需要对 Segment 里的 HashEntry 数组进行扩容,第二步定位添加元素的位置,然后将其放在 HashEntry 数组里。

3.get方法,效率非常高,因为整个过程都不需要加锁:

  • 将 Key 通过 Hash 定位到具体的 Segment
  • 再通过一次 Hash 定位到具体的元素上,成功就返回,不成功就返回null。

JDK1.8之后
底层数据结构:Synchronized + CAS +Node +红黑树。Node的val和next都用volatile保证,保证可见性,查找,替换,赋值操作都使用CAS

为什么在有Synchronized 的情况下还要使用CAS

因为CAS是乐观锁,在一些场景中(并发不激烈的情况下)它比Synchronized和ReentrentLock的效率要高,当CAS保障不了线程安全的情况下(扩容或者hash冲突的情况下)转成Synchronized 来保证线程安全,大大提高了低并发下的性能。synchronized锁住的只是发生hash冲突的链表的头节点或红黑树的节点,提高了并发性能。而Segment可重入锁锁住的是一个Segment数组

在这里插入图片描述
1.java1.8中,在创建ConcurrentHashMap对象时,会默认调用无参构造器,这个构造器什么事也没有做。在创建对象时,并没有在底层初始化长度为16的table数组

2.当进行put操作时,会进入put()方法内,调用putVal方法:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    //key和value不允许为空
    if (key == null || value == null) throw new NullPointerException();
    //基于key计算hash值
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //如果数组为null,或者数组长度为0
        if (tab == null || (n = tab.length) == 0)
            //初始化数组
            tab = initTable();
        //数组已经初始化了,将数据插入到map中,判断当前索引数据f是否为null
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //当前索引数据f为null,加入到当前索引位置
            //tabAt(数组,i) - 获取数组中i索引位置的数据(table[i])
            //casTabAt(数组,i,2,3) - 以CAS的方式,将数组i位置的数据从2修改到3
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
    //省略部分代码

3.tab = initTable() 初始化数组

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    //判断数组是否初始化了
    while ((tab = table) == null || tab.length == 0) {
        //sizeCtl:是一个控制标识符。
        //-1代表正在初始化,-N表示有N-1个线程正在进行扩容操作
        //0代表还没有被初始化,正数代表如果没有初始化,代表初始化的长度;如果初始化了,代表扩容的阈值。
        if ((sc = sizeCtl) < 0)
            //挂起这个线程,如果多个线程进来,有在执行初始化的,sizeCtl为-1,等线程初始化完成在执             行,就可以跳出while循环了。
            Thread.yield(); // lost initialization race; just spin
        //sizeCtl >= 0 以CAS的方式,将sizeCtl设置为-1 
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                //开始初始化
                //再次做一次判断,在并发下,下面得到sc后是 >= 0的,另外的线程会进来在做一次初始化。
                //类似与单例模式的DCL,指令重排了。
                if ((tab = table) == null || tab.length == 0) {
                    //获取数组的初始化长度,如果sc>0,以sc作为长度;如果sc为0,默认16.
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    //table初始化完毕
                    table = tab = nt;
                    //得到下次扩容的阈值,赋值给sc
                    sc = n - (n >>> 2);
                }
            } finally {
                //将sc下次扩容的阈值,赋值给sc
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

4.putVal方法的另一些情况:出现hash冲突

final V putVal(K key, V value, boolean onlyIfAbsent) {
    //省略部分代码。。。。。
    for (Node<K,V>[] tab = table;;) {
        //省略部分代码。。。。。
        //MOVED = -1 当前hash位置的数据正在扩容 TREEBIN = -2 红黑树 RESERVED = -3预留当前索引位置
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        //出现了hash冲突,需要将数据挂到链表上,或者添加到红黑树中
        else {
            V oldVal = null;
            //锁住当前数组位置
            synchronized (f) {
                //再次拿到i索引位置的数据,判断跟锁是不是一个
                if (tabAt(tab, i) == f) {
                    //当前数组下是链表,或者为空
                    if (fh >= 0) {
                        //标识设置为1
                        binCount = 1;
                        //插入
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //如果值一样,不是添加,是修改
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                //获取当前位置的value
                                oldVal = e.val;
                                //是否是IfAbsent,不是则覆盖数据,是的话什么都不做
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            //追加操作
                            //如果值不一样,添加
                            Node<K,V> pred = e;
                            //如果next指向的是null,直接插入到后面
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    ///当前数组下是红黑树
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                //如果binCount>=8,需要判断将链表转化为红黑树。
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

总结:

1.先判断Node数组有没有初始化,如果没有初始化先初始化initTable();

2.根据key的进行hash操作,找到Node数组中的位置,如果不存在hash冲突,即该位置是null或者hash值不同,直接用CAS插入,有可能会失败,因为其他线程可能抢占添加,则接着进行自旋;

3.当可以进行添加时,首先判断是否正在扩容,如果是,则此线程就会帮助扩容。如果不是,就先对链表的头节点或者红黑树的头节点加synchronized锁写入数据;

4.如果是链表,就遍历链表,如果key相同就执行覆盖操作,如果不同就将元素插入到链表的尾部, 并且在链表长度大于8, Node数组的长度超过64时,会将链表的转化为红黑树。

5.最后addCount方法也会检验当前ConcurrentHashMap是否需要扩容

ConcurrentHashMap没有办法保证并发读写情况下线程的安全,只能保证内部的数据不被破坏。

扩容的流程
扩容触发的三种情况:

  • 链表转红黑树的时候,如果数组长度不够的话,需要扩容数组。
  • 执行putAll方法时,如果传入的map比较长,原map放不下,则会扩容。
  • 在执行addCount方法时,即数据量达到阈值,也会扩容。
private final void tryPresize(int size) {
        //n:数组长度
        while ((sc = sizeCtl) >= 0) {
            //判断当前的tab是否和table一致
            else if (tab == table) {
                //计算扩容表示戳,根据当前数据的长度计算一个16位的扩容戳
                //第一个作用是为了保证后面的sizeCtl赋值时,保证sizeCtl为小于-1的负数
                //第二个作用是记录当前是从什么时候开始扩容的
                int rs = resizeStamp(n);
                //代表有线程正在扩容
                if (sc < 0) {
                    Node<K,V>[] nt;
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    //如果两个线程的老数组长度是一样的,可以进行协助扩容
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        //迁移数据
                        transfer(tab, nt);
                }
                //是第一个来扩容的线程
                //基于CAS将sizeCtl修改。把扩容表示戳左移16位,得到最高位符号位1。
                //1代表负数,SIZECTL<-1.
                //低16位表示正在扩容的线程有多少个。
                //为什么低位值为2时,代表有一个线程在扩容
                //每一个线程扩容完毕后,会对低16位进行-1操作,当最后一个线程扩容完毕后,-1的结果还是1
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    //迁移数据,第二个设置为null,代表第一个来扩容的
                    transfer(tab, null);
            }
        }
    }

总结:

  • 确认什么时候触发扩容;
  • 触发扩容后,首先计算扩容表示戳,左移16位+2,代表是第一个进来扩容的;
  • 计算每一个线程要迁移数据的步长,最小值16;
  • 初始化一个全新的数组,线程开始领取任务,从多少索引位置迁移到多少索引位置;
  • 开始迁移,最后做一个判断,是否是最后一个完成扩容的线程,如果是,则从头到尾检查一下遗漏数据。
5.10 ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMapHashtable 的区别主要体现在实现线程安全的方式上不同。

底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;

实现线程安全的方式(重要):

  • 在 JDK1.7 的时候,ConcurrentHashMap 对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
  • 到了 JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
  • Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
5.11 JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?

底层数据结构:Synchronized + CAS +Node +红黑树(logn)。Node的val和next都用volatile保证,保证可见性,查找,替换,赋值操作都使用CAS
synchronized锁住的只是发生hash冲突的链表的头节点或红黑树的节点,提高了并发性能,锁粒度细。而Segment可重入锁锁住的是一个Segment数组
在这里插入图片描述

在扩容方面,如果发现某一线程在扩容,其他线程会协助扩容。

计数器:addCount方法记录了ConcurrentHashMap中元素的个数。

两个作用:

  • 如果添加元素成功,对计数器+1;
  • 检验当前ConcurrentHashMap是否需要扩容。

在并发效率特别高的情况下,它不会一直等待,用CAS长时间占用CPU资源,它会根据CPU的情况,分出来很多位置进行计数,这些位置不会有冲突,当查询整个ConcurrentHashMap中元素的个数时,再进行汇总。类似于分段锁。

ConcurrentHashMap没有办法保证并发读写情况下线程的安全,只能保证内部的数据不被破坏。

6.集合常见方法

6.1 Collection常用方法
1.Collection常用方法
(Collection中定义了15个抽象方法。这些方法需要大家熟悉!)
(1add(E obj):添加元素对象到当前集合中
(2addAll(Collection other):添加other集合中的所有元素对象到当前集合中,即this = this ∪ other
(3int size():获取当前集合中实际存储的元素个数
(4boolean isEmpty():判断当前集合是否为空集合
(5boolean contains(Object obj):判断当前集合中是否存在一个与obj对象equals返回true的元素
(6boolean containsAll(Collection coll):判断coll集合中的元素是否在当前集合中都存在。即coll集合是否是当前集合的“子集”
(7boolean equals(Object obj):判断当前集合与obj是否相等
(8void clear():清空集合元素
(9boolean remove(Object obj) :从当前集合中删除第一个找到的与obj对象equals返回true的元素。
(10boolean removeAll(Collection coll):从当前集合中删除所有与coll集合中相同的元素。即this = this - this ∩ coll
(11boolean retainAll(Collection coll):从当前集合中删除两个集合中不同的元素,使得当前集合仅保留与coll集合中的元素相同的元素,即当前集合中仅保留两个集合的交集,即this  = this ∩ coll;
(12Object[] toArray():返回包含当前集合中所有元素的数组
(13hashCode():获取集合对象的哈希值
(14iterator():返回迭代器对象,用于集合遍历
2. 集合与数组的相互转换:
集合 ---> 数组:toArray()
数组 ---> 集合:调用Arrays的静态方法asList(Object...objs)
3.Collection中添加元素的要求:
   要求元素所属得类一定要重写equals()!
   因为Collection中的相关方法(比如:contains()/remove())在使用时,调用元素所在类的equals().
6.2 迭代器和增强for
1. 迭代器(Iterator)的作用?
用来遍历集合元素的。

2. 如何获取迭代器(Iterator)对象?
Iterator iterator = coll.iterator();

3. 如何实现遍历(代码实现)
while (iterator.hasNext()){
     System.out.println(iterator.next()); //next():①指针下移 ②将下移以后集合位置上的元素返回
   }

4. 增强for循环(foreach循环)的使用(jdk5.0新特性)
4.1 作用
用来遍历数组,集合

4.2 格式:
for (要遍历的集合或数组元素的类型 临时变量 : 要遍历的集合或数组变量){
     System.out.println(临时变量);
  }

4.3 说明:
针对于集合来讲,增强for循环的底层仍然使用的是迭代器
增强for循环的执行过程,是将集合或数组中的元素依次赋值给临时变量,注意,循环体中对临时变量的修改,可能
不会导致原有集合或数组中元素的修改。
6.3 List方法
1.Collection中声明的15个方法
2.增:
       add(E obj)
       addAll(Collection other)
   删:
       remove(Object obj)
       remove(int index)
   改:
       set(int index, Object ele)
   查:
       get(int index)
   插:
       add(int index, Object ele)
       addAll(int index, Collection eles)
   长度:
       size()
   遍历
       iterator():使用迭代器进行遍历
       增强for
       一般for
6.4 Set方法
Collection15个方法
6.5 Map方法
- 添加、修改操作:
  - Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
  - void putAll(Map m):将m中的所有key-value对存放到当前map中
- 删除操作:
  - Object remove(Object key):移除指定key的key-value对,并返回value
  - void clear():清空当前map中的所有数据
- 元素查询的操作:
  - Object get(Object key):获取指定key对应的value
  - boolean containsKey(Object key):是否包含指定的key
  - boolean containsValue(Object value):是否包含指定的value
  - int size():返回map中key-value对的个数
  - boolean isEmpty():判断当前map是否为空
  - boolean equals(Object obj):判断当前map和参数对象obj是否相等
- 元视图操作的方法:
  - Set keySet():返回所有key构成的Set集合
  - Collection values():返回所有value构成的Collection集合
  - Set entrySet():返回所有key-value对构成的Set集合
6.6 集合转数组和数组转集合

使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。

toArray(T[] array) 方法的参数是一个泛型数组,如果 toArray 方法中没有传递任何参数的话返回的是 Object类 型数组。

使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法, 它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。

注意事项

Arrays.asList()是泛型方法,传递的数组必须是对象数组,而不是基本类型。

int[] myArray = {1, 2, 3};
List myList = Arrays.asList(myArray);
System.out.println(myList.size());//1
System.out.println(myList.get(0));//数组地址值
System.out.println(myList.get(1));//报错:ArrayIndexOutOfBoundsException
int[] array = (int[]) myList.get(0);
System.out.println(array[0]);//1

当传入一个原生数据类型数组时,Arrays.asList() 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时 List 的唯一元素就是这个数组,这也就解释了上面的代码。

使用包装类型数组就可以解决这个问题。

使用集合的修改方法: add()、remove()、clear()会抛出异常。

List myList = Arrays.asList(1, 2, 3);
myList.add(4);//运行时报错:UnsupportedOperationException
myList.remove(1);//运行时报错:UnsupportedOperationException
myList.clear();//运行时报错:UnsupportedOperationException

Arrays.asList() 方法返回的并不是 java.util.ArrayList ,而是 java.util.Arrays 的一个内部类,这个内部类并没有实现集合的修改方法或者说并没有重写这些方法。

可以通过以下形式:

List list = new ArrayList<>(Arrays.asList("a", "b", "c"))

或使用 Java8 的 Stream(推荐)

Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本类型也可以实现转换(依赖boxed的装箱操作)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());

番外:HashMap 遍历

HashMap 遍历从大的方向来说,可分为以下 4 类

  1. 迭代器(Iterator)方式遍历;
  2. For Each 方式遍历;
  3. Lambda 表达式遍历(JDK 1.8+);
  4. Streams API 遍历(JDK 1.8+)。

但每种类型下又有不同的实现方式,因此具体的遍历方式又可以分为以下 7 种:

  1. 使用迭代器(Iterator)EntrySet 的方式进行遍历;
  2. 使用迭代器(Iterator)KeySet 的方式进行遍历;
  3. 使用 For Each EntrySet 的方式进行遍历;
  4. 使用 For Each KeySet 的方式进行遍历;
  5. 使用 Lambda 表达式的方式进行遍历;
  6. 使用 Streams API 单线程的方式进行遍历;
  7. 使用 Streams API 多线程的方式进行遍历。

接下来我们来看每种遍历方式的具体实现代码。

1.迭代器 EntrySet
public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Integer, String> entry = iterator.next();
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
    }
}
2.迭代器 KeySet
public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        Iterator<Integer> iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            Integer key = iterator.next();
            System.out.println(key);
            System.out.println(map.get(key));
        }
    }
}
3.ForEach EntrySet
public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        for (Map.Entry<Integer, String> entry : map.entrySet()) {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
    }
}
4.ForEach KeySet
public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        for (Integer key : map.keySet()) {
            System.out.println(key);
            System.out.println(map.get(key));
        }
    }
}
5.Lambda
public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        map.forEach((key, value) -> {
            System.out.println(key);
            System.out.println(value);
        });
    }
}
6.Streams API 单线程
public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        map.entrySet().stream().forEach((entry) -> {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        });
    }
}
7.Streams API 多线程
public class HashMapTest {
    public static void main(String[] args) {
        // 创建并赋值 HashMap
        Map<Integer, String> map = new HashMap();
        map.put(1, "Java");
        map.put(2, "JDK");
        map.put(3, "Spring Framework");
        map.put(4, "MyBatis framework");
        map.put(5, "Java中文社群");
        // 遍历
        map.entrySet().parallelStream().forEach((entry) -> {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        });
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值