参考文章:Java集合常见面试题总结
从上图可以知道,Java 集合框架主要包括两种类型的容器:
① Collection,用于存放单一元素;
② Map,用于存储键/值对映射。
注意:上图中只列举了主要的继承派生关系,并没有列举完全,如果想要深入了解,可以自行去查看源码。
(1)Collection 接口下面的集合:
List
① ArrayList:底层实现为 Object[] 数组;
② Vector:底层实现为 Object[] 数组;
③ LinkedList:底层实现为双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环);
Queue
① PriorityQueue: 元素出队顺序与优先级相关,即总是优先级最高的元素先出队,其底层是通过 Object[] 数组实现的二叉堆来完成的;
② ArrayQueue: 底层通过 Object[] 数组 + 双指针来实现;
Set
① HashSet:基于 HashMap 实现的,底层采用 HashMap 来保存元素,元素无序且唯一;
② LinkedHashSet:HashSet 的子类,其内部是通过 LinkedHashMap 来实现的;
③ TreeSet:有序集合,可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现,其中排序是通过红黑树来完成的;
(2)Map 接口下面的集合:
(1)Collection 是 JDK 中集合层次结构中的最根本的接口,定义了集合类的基本方法。Collection 接口在 Java 类库中有很多具体的实现,其意义是为各种具体的集合提供了最大化的统一操作方式。
(2)Collections 是一个包装类,是 Collection 集合框架的工具类。它包含有各种有关集合操作的静态多态方法,不能实例化,比如排序方法: Collections.sort(list)。
(1)List:存储的元素是有序的、可重复的。
(2)Set:存储的元素是无序的、不可重复的,LinkedHashSet 按照插入排序,SortedSet 可排序,HashSet 无序。
(3)Queue:按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。
(4)Map:存储键值对 (key-value),其中要求 key 无序且唯一,而 value 则不要求有序,允许重复。
(1)当我们需要保存一组类型相同的数据时,可以选用数组,但是数组存在着一定的弊端,例如当数组声明后,其长度以及存储的数据类型也就固定了,并且存储的数据的特点单一。因此为了提高数据存储的灵活性、数据特点的多样性,Java 中使用了集合。
(2)在选用集合时,我们应主要根据需求和集合特点来选用:
(1)底层数据结构
① ArrayList 底层使用的是Object 数组,在一片连续的内存空间中存储数据。
② LinkedList 底层使用的是双向链表(JDK1.6 之前为双向循环链表,JDK1.7 取消了循环),数据可以存储在分散的内存空间中。
(2)扩容
① ArrayList 底层使用的是 Object 数组,在无参构造函数中默认初始化长度为 10,当需要扩容时会将原数组中的元素重新拷贝到长度为原数组的 1.5 倍的新数组中,扩容代价比较高;
② LinkedList 通过链表实现,新增元素根据要求插入到链表中即可。
有关 ArrayList 扩容的具体细节,可查看本节的 2.3。
(3)是否支持快速随机访问
① ArrayList 实现了 RandomAccess 接口且底层是通过 Object 数组实现的,故支持快速随机访问。
② LinkedList 的底层是通过链表实现的,故不支持快速随机访问,查找某一元素的时间复杂度为 O(n)。
快速随机访问就是通过元素的下标快速获取元素,即对应于 ArrayList 中的
get(int index)
方法,其时间复杂度为 O(1)。
(4)插入和删除操作
① 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) ,因为需要先移动到指定位置再插入。
(3)遍历操作
ArrayList 和 LinkedList 常见的遍历方式有 3 种:for(结合 get(int index)
方法)、foreach、iterator。
① 在数据量比较小时,不同遍历方式的性能差别不大。
② 但是在数据量比较大时:
1)对于 ArrayList 来说,3 种遍历方式差距不是很大,其中 for 循环的效率最高,因为使用的是快速随机访问方式。
2)对于 LinkedList 来说,迭代器效率最高,因为其相当于维护一个当前状态指针,遍历只需要扫描一遍双向链表即可,而 for 效率最低,因为需要扫描 n 遍链表。
① 在 List 遍历方式中,foreach 的底层就是由迭代器实现的,只不过为了方便书写,做了简单的封装。
② 在遍历 LinkedList 时,尽量不要使用get(int index)
方法,因为每次都要从链表头或者表尾去遍历,时间复杂度较高。
具体细节可参考 ArrayList 和 LinkedList 的三种遍历方式 这篇文章。
(4)内存空间占用
① ArrayList 除了存储数据所占用的内存空间外,列表结尾往往会预留一定的容量空间,这可能会造成一定的内存空间的浪费。
② LinkedList 的内存空间占用则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接前驱和直接后继以及数据)。
注意:在项目中一般不会使用 LinkedList,因为大部分需用到 LinkedList 的场景都可以使用 ArrayList 来代替,并且性能通常会更好!
(1)联系
① Vector 是早期 JDK 版本提供,ArrayList 是新版本中用来替代 Vector 的;
② 底层都是通过数组实现的;
③ 功能相同,实现增删改查等操作的方法相似;
(2)区别
① Vector 是线程安全的,而 ArrayList 是非线程安全的;
② Vector 类中的方法很多有 synchronized 进行修饰,以保证线程安全,这样就导致了 Vector 在效率上无法与 ArrayList 相比;
③ 默认初始化容量都是10,但扩容时 Vector 默认会翻倍,也可指定扩容的大小,而 ArrayList 的大小则是扩容为原来的 1.5 倍;
先来看 ArrayList 的 3 种创建方式,即对应 3 个构造函数:
//默认初始容量大小
private static final int DEFAULT_CAPACITY = 10;private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};//创建时指定容量大小
public ArrayList(int initialCapacity) {if (initialCapacity > 0) {this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA;} else {throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);}
}//无参构造函数,使用初始容量 10 来构造一个空列表
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}//构造包含指定 collection 元素的列表,这些元素利用该集合的迭代器按顺序返回
public ArrayList(Collection extends E> c) {elementData = c.toArray();if ((size = elementData.length) != 0) {// c.toArray might (incorrectly) not return Object[] (see 6260652)if (elementData.getClass() != Object[].class)elementData = Arrays.copyOf(elementData, size, Object[].class);} else {// replace with empty array.this.elementData = EMPTY_ELEMENTDATA;}
}
注意:
① 以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。
② JDK6 new 无参构造的 ArrayList 对象时,直接创建了长度是 10 的 Object[] 数组 elementData。
(1)先来看 add(E e)
方法
//向列表末尾添加元素,添加成功时返回 true
public boolean add(E e) {//调用了 ensureCapacityInternal 方法,确保数组下标不越界ensureCapacityInternal(size + 1); // Increments modCount!!//添加元素elementData[size++] = e;return true;
}
(2)再来看看 ensureCapacityInternal()
方法
private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//返回默认的容量和传入参数的较大值return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;
}
当要向列表中添加第 1 个元素时,minCapacity 为 1,在 Math.max()方法比较后,minCapacity 变为 10。
(3)ensureExplicitCapacity()
方法如下:
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)//调用 grow 方法进行扩容grow(minCapacity);
}
具体分析如下:
(4)grow(int minCapacity)
方法是 ArrayList 扩容时的核心方法,其代码如下:
//要分配的最大数组大小
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//扩容的核心代码
private void grow(int minCapacity) {// oldCapacity 为旧容量,newCapacity 为新容量int oldCapacity = elementData.length;//将 oldCapacity 右移一位,即相当于 oldCapacity / 2,整句运算式的结果就是将新容量更新为旧容量的 1.5 倍int newCapacity = oldCapacity + (oldCapacity >> 1);//检查新容量 newCapacity 是否大于最小需要容量 minCapacity,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量if (newCapacity - minCapacity < 0)newCapacity = minCapacity;//如果 minCapacity 大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8。if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win:/*(1) Arrays.copyOf()方法返回的数组是新的数组对象,原数组对象仍是原数组对象不变;(2) 该拷贝不会影响原来的数组, copyOf()的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值;*/elementData = Arrays.copyOf(elementData, newCapacity);
}
(5)hugeCapacity(int minCapacity)
方法如下:
private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) // overflowthrow new OutOfMemoryError();return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;
}
class Test {//通过反射获取 list 的容量,即 elementData 数组的长度public static Integer getCapacity(ArrayList list) {Integer length = null;Class clazz = list.getClass();Field field;try {field = clazz.getDeclaredField("elementData");field.setAccessible(true);Object[] object = (Object[]) field.get(list);length = object.length;return length;} catch (Exception e) {e.printStackTrace();}return length;}public static void main(String[] args) {ArrayList list = new ArrayList<>();//记录 list 的容量,初始值为 0int preCap = 0;for (int i = 0; i < 100; i++) {list.add(i);int curCap = getCapacity(list);// curCap > preCap 说明上次添加元素过程中发生了扩容if (curCap > preCap) {System.out.println("capacity: " + curCap + ", size: " + list.size());preCap = getCapacity(list);}}System.out.println("添加 100 个元素后,capacity: " + getCapacity(list) + ", size: " + list.size());}
}
输出结果如下:
capacity: 10, size: 1
capacity: 15, size: 11
capacity: 22, size: 16
capacity: 33, size: 23
capacity: 49, size: 34
capacity: 73, size: 50
capacity: 109, size: 74
添加 100 个元素后,capacity: 109, size: 100
(1)扩容因子的大小选择,需要考虑如下情况:
① 扩容容量不能太小,防止频繁扩容,频繁申请内存空间 + 数组频繁复制;
② 扩容容量不能太大,需要充分利用空间,避免浪费过多空间;
(2)为了能充分使用之前分配的内存空间,最好把增长因子设为 1< k < 2,当 k = 1.5 时,就能充分利用前面已经释放的空间。如果 k >= 2,新容量刚刚好永远大于过去所有废弃的数组容量。除此之外,并且充分利用移位操作(右移一位,在不溢出的情况下相当于除以 2),减少了浮点数运算,提高了效率。
(1)Queue 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循先进先出 (FIFO) 规则。Queue 扩展了 Collection 的接口,根据失败后处理方式的不同可以分为以下两类方法:
Queue 接口 | 抛出异常 | 返回特殊值 |
---|---|---|
在队尾插入元素 | add(E e) | offer(E e) |
删除队首元素 | remove() | poll() |
查询队首元素 | element() | peek() |
(2)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() 等其他方法,可用于模拟栈。
ArrayDeque 和 LinkedList 均实现了 Deque 接口,即都具有队列的功能,但两者具有以下这些区别:
从性能的角度上考虑,选用 ArrayDeque 来实现队列更好。此外,ArrayDeque 也可以用于实现栈。
PriorityQueue 中元素出队顺序与优先级相关,即总是优先级最高的元素先出队。这里列举其相关的一些要点:
(1)线程是否安全
HashMap 是非线程安全的,Hashtable 是线程安全的(Hashtable 中的方法基本都使用 synchronized 修饰,同时在效率上不如 HashMap)。如果要保证线程安全,可以使用 ConcurrentHashMap,因为目前 Hashtable 使用的频率较低。
(2)对 null 键和 null 值的支持
HashMap | Hashtable | |
---|---|---|
null 键 | 支持,但只允许有一个 | 支持,允许有多个 |
null 值 | 不支持 | 不支持 |
注意:如果 HashTable 中存在 null 键或 null 值,会抛出 NullPointerException。
(3)初始容量和扩容操作
HashMap | Hashtable | |
---|---|---|
初始容量 | 16,也可自己指定 | 11,也可自己指定 |
扩容操作 | 每次扩充容量变为原来的 2 倍 | 每次扩充容量变为 2n+1,n 为上一次的容量 |
(4)计算 hash 值的方式
① HashMap 计算 hash 值的方式为先调用 hashCode() 计算出来一个 hash 值,再将 hash 与 hash 右移 16 位后的值进行异或操作,从而得到最终的 hash 值,具体的代码实现如下:
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
② Hashtable 则通过计算 key 的 hashCode() 来得到最终的 hash 值。
(5)解决 hash 冲突的机制
① HashMap:在 JDK 1.8 之前,HashMap 底层由数组 + 链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(通过“拉链法”解决冲突)。而 JDK 1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8,并且将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间;
② HashTable:底层也是由数组 + 链表组成的,但没有像 HashMap 那样的机制。
具体细节可参考这篇文章。
(1)HashSet 底层就是基于 HashMap 实现的,具体见下面 HashSet 的几个构造函数:
public HashSet() {map = new HashMap<>();
}public HashSet(Collection extends E> c) {map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {map = new HashMap<>(initialCapacity, loadFactor);
}public HashSet(int initialCapacity) {map = new HashMap<>(initialCapacity);
}
(2)但 HashMap 和 HashSet 之间也存在着一些区别:
HashMap | HashSet | |
---|---|---|
实现接口 | 实现了 Map 接口 | 实现了 Set 接口 |
添加方式 | 调用 put() 向 map 中添加键值对 | 调用 add() 向 set 中添加元素 |
计算 hashcode | 通过键 (key) 来计算 | 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以 equals() 方法用来判断对象的相等性 |
(1)TreeMap 和HashMap 都继承自 AbstractMap ,但是 TreeMap 还实现了 NavigableMap 接口和 SortedMap 接口:
public class Person {private Integer age;public Person(Integer age) {this.age = age;}public Integer getAge() {return age;}public static void main(String[] args) {//重写排序规则TreeMap treeMap = new TreeMap<>((person1, person2) -> {int num = person1.getAge() - person2.getAge();return Integer.compare(num, 0);});treeMap.put(new Person(3), "person1");treeMap.put(new Person(18), "person2");treeMap.put(new Person(35), "person3");treeMap.put(new Person(16), "person4");treeMap.forEach((key, value) -> System.out.println(value));}
}
输出结果如下:
person1
person4
person2
person3
(2)总之,相比于 HashMap 来说,TreeMap 主要多了对集合中的元素根据键排序的能力以及对集合内元素进行搜索的能力。
(1)JDK1.8 之前 HashMap 底层是数组 + 链表结合在一起使用,也就是链表散列。HashMap 通过 key 的 hashcode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法,换句话说使用扰动函数之后可以减少 hash 冲突,提高效率。
(2)拉链法指将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。如果遇到哈希冲突,则将冲突的值加添加到链表中即可,如下图所示:
相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
(1)为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。
(2)我们首先可能会想到通过取模操作来实现。但是与 (&) 的运算速度比取余 (%) 取模运算快,并且在取余 (%) 操作中如果除数是 2 的幂次,则等价于与其除数减一的与 (&) 操作,即 hash % length == hash & (length - 1) 的前提是 length 是 2 的 n 次方。所以 HashMap 的长度为 2 的 n 次方。
注:HashMap 的初始默认长度是 16。
(3)此外,如果 HashMap 的长度为 2 的 n 次方,那么在扩容迁移时不需要再重新定位新的位置了,因为扩容后元素新的位置,要么在原下标位置,要么在原下标 + 扩容长度的位置。
(4)HashMap 源码中的 tableSizeFor(int cap) 可以保证其长度永远是是 2 的 n 次方。
/*** Returns a power of two size for the given target capacity.*/
//返回大于且最靠近 cap 的 2^n,例如 cap = 17 时,返回 32
static final int tableSizeFor(int cap) {// n = cap - 1 使得 n 的二进制表示的最后一位和 cap 的最后一位一定不一样int n = cap - 1;//无符号右移,在移动期间,使用 | 运算保证低位全部是 1n |= 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;
}
无符号右移 >>>:忽略符号位,空位均用 0 补齐,最终结果必为非负数。
具体分析可参考这篇文章。
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
具体细节可参考这篇文章
参考文章:HashMap 的 7 种遍历方式与性能分析!「修正篇」
HashMap 遍历从大的方向来说,可分为以下 4 类:
但每种类型下又有不同的实现方式,因此具体的遍历方式又可以分为以下 7 种:
(1)使用迭代器 (Iterator) EntrySet 的方式进行遍历:
class HashMapTest {public static void main(String[] args) {Map hashMap = new HashMap<>();hashMap.put(1, "Java");hashMap.put(2, "C++");hashMap.put(3, "Python");// Iterator entrySetIterator> iterator = hashMap.entrySet().iterator();while (iterator.hasNext()) {Map.Entry entry = iterator.next();System.out.println("(" + entry.getKey() + ", " + entry.getValue() + ")");}}
}
输出结果如下,下面几种遍历方式的结果于下面的一样,因此将不再展示。
(1, Java)
(2, C++)
(3, Python)
(2)使用迭代器 (Iterator) KeySet 的方式进行遍历:
class HashMapTest {public static void main(String[] args) {Map hashMap = new HashMap<>();hashMap.put(1, "Java");hashMap.put(2, "C++");hashMap.put(3, "Python");// Iterator keySetIterator iterator = hashMap.keySet().iterator();while (iterator.hasNext()) {Integer key = iterator.next();System.out.println("(" + key + ", " + hashMap.get(key) + ")");}}
}
(3)使用 foreach EntrySet 的方式进行遍历:
class HashMapTest {public static void main(String[] args) {Map hashMap = new HashMap<>();hashMap.put(1, "Java");hashMap.put(2, "C++");hashMap.put(3, "Python");// foreach entrySetfor (Map.Entry entry : hashMap.entrySet()) {System.out.println("(" + entry.getKey() + ", " + entry.getValue() + ")");}}
}
(4)使用 foreach KeySet 的方式进行遍历:
class HashMapTest {public static void main(String[] args) {Map hashMap = new HashMap<>();hashMap.put(1, "Java");hashMap.put(2, "C++");hashMap.put(3, "Python");// foreach keySetfor (Integer key : hashMap.keySet()) {System.out.println("(" + key + ", " + hashMap.get(key) + ")");}}
}
(5)使用 Lambda 表达式的方式进行遍历:
class HashMapTest {public static void main(String[] args) {Map hashMap = new HashMap<>();hashMap.put(1, "Java");hashMap.put(2, "C++");hashMap.put(3, "Python");// LambdahashMap.forEach((key, value) -> {System.out.println("(" + key + ", " + value + ")");});}
}
(6)使用 Streams API 单线程的方式进行遍历:
class HashMapTest {public static void main(String[] args) {Map hashMap = new HashMap<>();hashMap.put(1, "Java");hashMap.put(2, "C++");hashMap.put(3, "Python");// Streams API 单线程hashMap.entrySet().stream().forEach((entry) -> {System.out.println("(" + entry.getKey() + ", " + entry.getValue() + ")");});}
}
(7)使用 Streams API 多线程的方式进行遍历:
class HashMapTest {public static void main(String[] args) {Map hashMap = new HashMap<>();hashMap.put(1, "Java");hashMap.put(2, "C++");hashMap.put(3, "Python");// Streams API 多线程hashMap.entrySet().parallelStream().forEach((entry) -> {System.out.println("(" + entry.getKey() + ", " + entry.getValue() + ")");});}
}
(1)当把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()
方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。
(2)在 JDK1.8 中,HashSet 的 add()
方法只是简单的调用了 HashMap 的 put()
方法,并且判断了一下返回值以确保是否有重复元素。直接看一下 HashSet 中的源码:
//@return true if this set did not already contain the specified element
public boolean add(E e) {return map.put(e, PRESENT)==null;
}
而在 HashMap 的 putVal()
方法中也能看到如下说明:
// Returns : previous value, or null if none
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {...
}
也就是说,在 JDK1.8 中,实际上无论 HashSet 中是否已经存在了某元素,HashSet都会直接插入,只是会在 add()
方法的返回值处告诉我们插入前是否存在相同元素。
(1)相同点:poll() 和 remove() 都是从队列中取出一个元素;
(2)不同点:poll() 在获取元素失败的时候会返回空,但 remove() 失败的时候会抛出异常。
下一篇:内核经典数据结构list 剖析