「Java集合篇」Collection
2022/4/12 1:43:08
本文主要是介绍「Java集合篇」Collection,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
Java集合
知识体系结构
概述
容器,就是可以容纳其他Java对象的对象。Java Collections Framework(JCF)为Java开发者提供了通用的容器,其始于JDK 1.2,优点是:
- 降低编程难度
- 提高程序性能
- 提高API间的互操作性
- 降低学习难度
- 降低设计和实现相关API的难度
- 增加程序的重用性
Java容器里只能放对象,对于基本类型(int, long, float, double等),需要将其包装成对象类型后(Integer, Long, Float, Double等)才能放到容器里。很多时候拆包装和解包装能够自动完成。这虽然会导致额外的性能和空间开销,但简化了设计和编程。
Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 Collection
接口,主要用于存放单一元素;另一个是 Map
接口,主要用于存放键值对。对于Collection
接口,下面又有三个主要的子接口:List
、Set
和 Queue
。
集合框架底层数据结构总结
Collection
容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
Set
TreeSet
基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。(自平衡的排序二叉树)
HashSet
支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。(无序,唯一)基于 HashMap
实现的,底层采用 HashMap
来保存元素
LinkedHashSet
具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。 LinkedHashSet
是 HashSet
的子类,并且其内部是通过 LinkedHashMap
来实现的。有点类似于我们之前说的 LinkedHashMap
其内部是基于 HashMap
实现一样,不过还是有一点点区别的
List
ArrayList
基于动态数组实现,支持随机访问。
Vector
和 ArrayList 类似,但它是线程安全的。
LinkedList
基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)
Queue
ArrayQueue
Object[]
数组 + 双指针
LinkedList
可以用它来实现双向队列。
PriorityQueue
基于堆结构实现,可以用它来实现优先队列。PriorityQueue
: Object[]
数组来实现二叉堆
Map
TreeMap
基于红黑树实现。
HashMap
JDK1.8 之前 HashMap
由数组+链表组成的,数组是 HashMap
的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
HashTable
和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。数组+链表组成的,数组是 Hashtable
的主体,链表则是主要为了解决哈希冲突而存在的
LinkedHashMap
使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 LinkedHashMap
继承自 HashMap
,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:《LinkedHashMap 源码详细分析(JDK1.8)》
ArrayList
简介
ArrayList
的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity
操作来增加 ArrayList
实例的容量。这可以减少递增式再分配的数量。
ArrayList
继承于 AbstractList
,实现了 List
, RandomAccess
, Cloneable
, java.io.Serializable
这些接口。
查看代码
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{ }
RandomAccess
是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在ArrayList
中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。ArrayList
实现了Cloneable
接口 ,即覆盖了函数clone()
,能被克隆。ArrayList
实现了java.io.Serializable
接口,这意味着ArrayList
支持序列化,能通过序列化去传输。
源码解读
接口
查看代码
package java.util; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.UnaryOperator; public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
RandomAccess 接口
查看代码
public interface RandomAccess { }
查看源码我们发现实际上 RandomAccess
接口中什么都没有定义。所以,在我看来 RandomAccess
接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。
在 binarySearch()
方法中,它要判断传入的 list 是否 RandomAccess
的实例,如果是,调用indexedBinarySearch()
方法,如果不是,那么调用iteratorBinarySearch()
方法
查看代码
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); }
ArrayList
实现了 RandomAccess
接口, 而 LinkedList
没有实现。为什么呢?我觉得还是和底层数据结构有关!ArrayList
底层是数组,而 LinkedList
底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,ArrayList
实现了 RandomAccess
接口,就表明了他具有快速随机访问功能。 RandomAccess
接口只是标识,并不是说 ArrayList
实现 RandomAccess
接口才具有快速随机访问功能的!
Serializable标记性接口
Serializable adj. 可串行化的、可序列化的
类的序列化由实现java.io.Serializable
接口的类启用。不实现此接口的类将不会使任何状态序列化或反序列化。可序列化类的所有子类都是可序列化的。序列化接口没有方法或字段,仅用于标识可串行化的语义
-
序列化:将对象的数据写入到文件(写对象)
-
反序列化:将文件中对象的数据读取出来 (读对象)
private static final long serialVersionUID = 8683452581122892189L;
Cloneable 标记性接口
一个类实现Cloneable接口来指示Object.clone()方法,该方法对于该类的实例进行字段的复制是合法的。
-
克隆就是依据已有数据,创造一份新的完全一样的数据拷贝
-
前提条件
- 被克隆对象所在的类必须实现Cloneable接口
- 必须重写clone方法
核心参数
查看代码
/** * 默认初始容量大小 */ private static final int DEFAULT_CAPACITY = 10; /** * 空数组(用于空实例)。 */ private static final Object[] EMPTY_ELEMENTDATA = {}; //用于默认大小空实例的共享空数组实例。 //我们把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * 保存ArrayList数据的数组 */ transient Object[] elementData; // non-private to simplify nested class access /** * ArrayList 所包含的元素个数 */ private int size;
构造方法
查看代码
/** * 带初始容量参数的构造函数(用户可以在创建ArrayList对象时自己指定集合的初始大小) */ public ArrayList(int initialCapacity) { if (initialCapacity > 0) { //如果传入的参数大于0,创建initialCapacity大小的数组 this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { //如果传入的参数等于0,创建空数组 this.elementData = EMPTY_ELEMENTDATA; } else { //其他情况,抛出异常 throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } /** *默认无参构造函数 *DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为0.初始化为10,也就是说初始其实是空数组 当添加第一个元素的时候数组容量才变成10 */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } /** * 构造一个包含指定集合的元素的列表,按照它们由集合的迭代器返回的顺序。 */ public ArrayList(Collection<? extends E> c) { //将指定集合转换为数组 elementData = c.toArray(); //如果elementData数组的长度不为0 if ((size = elementData.length) != 0) { // 如果elementData不是Object类型数据(c.toArray可能返回的不是Object类型的数组所以加上下面的语句用于判断) if (elementData.getClass() != Object[].class) //将原来不是Object类型的elementData数组的内容,赋值给新的Object类型的elementData数组 elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // 其他情况,用空数组代替 this.elementData = EMPTY_ELEMENTDATA; } }
常用方法
trimToSize()
将容量修改为当前实际存储容量
查看代码
/** * 修改这个ArrayList实例的容量是列表的当前大小。 应用程序可以使用此操作来最小化ArrayList实例的存储。 */ public void trimToSize() { modCount++; if (size < elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } }
size()
获得元素个数
查看代码
/** *返回此列表中的元素数。 */ public int size() { return size; }
isEmpty()
判断ArrayList是否为空
查看代码
/** * 如果此列表不包含元素,则返回 true 。 */ public boolean isEmpty() { //注意=和==的区别 return size == 0; }
contains()
查询是否包含元素
查看代码
/** * 如果此列表包含指定的元素,则返回true 。 */ public boolean contains(Object o) { //indexOf()方法:返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 return indexOf(o) >= 0; }
indexOf()
查看代码
/** *返回此列表中指定元素的首次出现的索引,如果此列表不包含此元素,则为-1 */ public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) //equals()方法比较 if (o.equals(elementData[i])) return i; } return -1; }
lastIndexOf()
查看代码
/** * 返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1。. */ public int lastIndexOf(Object o) { if (o == null) { for (int i = size-1; i >= 0; i--) if (elementData[i]==null) return i; } else { for (int i = size-1; i >= 0; i--) if (o.equals(elementData[i])) return i; } return -1; }
clone()
查看代码
/** * 返回此ArrayList实例的浅拷贝。 (元素本身不被复制。) */ public Object clone() { try { ArrayList<?> v = (ArrayList<?>) super.clone(); //Arrays.copyOf功能是实现数组的复制,返回复制后的数组。参数是被复制的数组和复制的长度 v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // 这不应该发生,因为我们是可以克隆的 throw new InternalError(e); } }
toArray()
查看代码
/** *以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。 *返回的数组将是“安全的”,因为该列表不保留对它的引用。 (换句话说,这个方法必须分配一个新的数组)。 *因此,调用者可以自由地修改返回的数组。 此方法充当基于阵列和基于集合的API之间的桥梁。 */ public Object[] toArray() { return Arrays.copyOf(elementData, size); } /** * 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); *返回的数组的运行时类型是指定数组的运行时类型。 如果列表适合指定的数组,则返回其中。 *否则,将为指定数组的运行时类型和此列表的大小分配一个新数组。 *如果列表适用于指定的数组,其余空间(即数组的列表数量多于此元素),则紧跟在集合结束后的数组中的元素设置为null 。 *(这仅在调用者知道列表不包含任何空元素的情况下才能确定列表的长度。) */ @SuppressWarnings("unchecked") public <T> T[] toArray(T[] a) { if (a.length < size) // 新建一个运行时类型的数组,但是ArrayList数组的内容 return (T[]) Arrays.copyOf(elementData, size, a.getClass()); //调用System提供的arraycopy()方法实现数组之间的复制 System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; }
ArrayList的扩容机制
默认容量 = 10,当有设置容量时按照设置的容量,当超过设置容量时会以1.5倍增加,判断容量是否超过Integer.MAX_VALUE
查看代码
//下面是ArrayList的扩容机制 //ArrayList的扩容机制提高了性能,如果每次只扩充一个, //那么频繁的插入会导致频繁的拷贝,降低性能,而ArrayList的扩容机制避免了这种情况。 /** * 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量 * @param minCapacity 所需的最小容量 */ public void ensureCapacity(int minCapacity) { //如果是true,minExpand的值为0,如果是false,minExpand的值为10 int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) // any size if not default element table ? 0 // larger than default for default empty table. It's already // supposed to be at default size. : DEFAULT_CAPACITY; //如果最小容量大于已有的最大容量 if (minCapacity > minExpand) { ensureExplicitCapacity(minCapacity); } } //1.得到最小扩容量 //2.通过最小容量扩容 private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 获取“默认的容量”和“传入参数”两者之间的最大值 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } //判断是否需要扩容 private void ensureExplicitCapacity(int minCapacity) { // 底层结构被修改的次数 即扩容次数等 modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) //调用grow方法进行扩容,调用此方法代表已经开始扩容了 grow(minCapacity); } /** * 要分配的最大数组大小 */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /** * ArrayList扩容的核心方法。 */ private void grow(int minCapacity) { // oldCapacity为旧容量,newCapacity为新容量 int oldCapacity = elementData.length; //将oldCapacity 右移一位,其效果相当于oldCapacity /2, //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍, int newCapacity = oldCapacity + (oldCapacity >> 1); //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量, if (newCapacity - minCapacity < 0) newCapacity = minCapacity; //再检查新容量是否超出了ArrayList所定义的最大容量, //若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE, //如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。 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); } //比较minCapacity和 MAX_ARRAY_SIZE private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
增删改查
查看代码
// Positional Access Operations @SuppressWarnings("unchecked") E elementData(int index) { return (E) elementData[index]; } /** * 返回此列表中指定位置的元素。 */ public E get(int index) { rangeCheck(index); return elementData(index); } /** * 用指定的元素替换此列表中指定位置的元素。 */ public E set(int index, E element) { //对index进行界限检查 rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; //返回原来在这个位置的元素 return oldValue; } /** * 将指定的元素追加到此列表的末尾。 */ public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! //这里看到ArrayList添加元素的实质就相当于为数组赋值 elementData[size++] = e; return true; } /** * 在此列表中的指定位置插入指定的元素。 *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 */ public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! //arraycopy()这个实现数组之间复制的方法一定要看一下,下面就用到了arraycopy()方法实现数组自己复制自己 System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } /** * 删除该列表中指定位置的元素。 将任何后续元素移动到左侧(从其索引中减去一个元素)。 */ public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work //从列表中删除的元素 return oldValue; } /** * 从列表中删除指定元素的第一个出现(如果存在)。 如果列表不包含该元素,则它不会更改。 *返回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); return true; } } return false; } /* * Private remove method that skips bounds checking and does not * return the value removed. */ private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work } /** * 从列表中删除所有元素。 */ public void clear() { modCount++; // 把数组中所有的元素的值设为null for (int i = 0; i < size; i++) elementData[i] = null; size = 0; } /** * 按指定集合的Iterator返回的顺序将指定集合中的所有元素追加到此列表的末尾。 */ public boolean addAll(Collection<? extends E> c) { Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew); // Increments modCount System.arraycopy(a, 0, elementData, size, numNew); size += numNew; return numNew != 0; } /** * 将指定集合中的所有元素插入到此列表中,从指定的位置开始。 */ public boolean addAll(int index, Collection<? extends E> c) { rangeCheckForAdd(index); Object[] a = c.toArray(); int numNew = a.length; ensureCapacityInternal(size + numNew); // Increments modCount int numMoved = size - index; if (numMoved > 0) System.arraycopy(elementData, index, elementData, index + numNew, numMoved); System.arraycopy(a, 0, elementData, index, numNew); size += numNew; return numNew != 0; } /** * 从此列表中删除所有索引为fromIndex (含)和toIndex之间的元素。 *将任何后续元素移动到左侧(减少其索引)。 */ protected void removeRange(int fromIndex, int toIndex) { modCount++; int numMoved = size - toIndex; System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved); // clear to let GC do its work int newSize = size - (toIndex-fromIndex); for (int i = newSize; i < size; i++) { elementData[i] = null; } size = newSize; } /** * 检查给定的索引是否在范围内。 */ private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } /** * add和addAll使用的rangeCheck的一个版本 */ private void rangeCheckForAdd(int index) { if (index > size || index < 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } /** * 返回IndexOutOfBoundsException细节信息 */ private String outOfBoundsMsg(int index) { return "Index: "+index+", Size: "+size; } /** * 从此列表中删除指定集合中包含的所有元素。 */ public boolean removeAll(Collection<?> c) { Objects.requireNonNull(c); //如果此列表被修改则返回true return batchRemove(c, false); } /** * 仅保留此列表中包含在指定集合中的元素。 *换句话说,从此列表中删除其中不包含在指定集合中的所有元素。 */ public boolean retainAll(Collection<?> c) { Objects.requireNonNull(c); return batchRemove(c, true); } /** * 从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。 *指定的索引表示初始调用将返回的第一个元素为next 。 初始调用previous将返回指定索引减1的元素。 *返回的列表迭代器是fail-fast 。 */ public ListIterator<E> listIterator(int index) { if (index < 0 || index > size) throw new IndexOutOfBoundsException("Index: "+index); return new ListItr(index); } /** *返回列表中的列表迭代器(按适当的顺序)。 *返回的列表迭代器是fail-fast 。 */ public ListIterator<E> listIterator() { return new ListItr(0); } /** *以正确的顺序返回该列表中的元素的迭代器。 *返回的迭代器是fail-fast 。 */ public Iterator<E> iterator() { return new Itr(); }
System.arraycopy()
和 Arrays.copyOf()
方法
阅读源码的话,我们就会发现 ArrayList 中大量调用了这两个方法。比如:我们上面讲的扩容操作以及add(int index, E element)
、toArray()
等方法中都用到了该方法!
System.arraycopy()
方法
源码:
查看代码
// 我们发现 arraycopy 是一个 native 方法,接下来我们解释一下各个参数的具体意义 /** * 复制数组 * @param src 源数组 * @param srcPos 源数组中的起始位置 * @param dest 目标数组 * @param destPos 目标数组中的起始位置 * @param length 要复制的数组元素的数量 */ public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
场景:
查看代码
/** * 在此列表中的指定位置插入指定的元素。 *先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 方法保证capacity足够大; *再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。 */ public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! //arraycopy()方法实现数组自己复制自己 //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;index + 1:目标数组中的起始位置; size - index:要复制的数组元素的数量; System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
我们写一个简单的方法测试以下:
查看代码
public class ArraycopyTest { public static void main(String[] args) { // TODO Auto-generated method stub int[] a = new int[10]; a[0] = 0; a[1] = 1; a[2] = 2; a[3] = 3; System.arraycopy(a, 2, a, 3, 3); a[2]=99; for (int i = 0; i < a.length; i++) { System.out.print(a[i] + " "); } } }
结果:
</details> 0 1 99 2 3 0 0 0 0 0
Arrays.copyOf()
方法
源码:
查看代码
public static int[] copyOf(int[] original, int newLength) { // 申请一个新的数组 int[] copy = new int[newLength]; // 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组 System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; }
场景:
查看代码
/** 以正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素); 返回的数组的运行时类型是指定数组的运行时类型。 */ public Object[] toArray() { //elementData:要复制的数组;size:要复制的长度 return Arrays.copyOf(elementData, size); }
个人觉得使用 Arrays.copyOf()
方法主要是为了给原有数组扩容,测试代码如下:
查看代码
public class ArrayscopyOfTest { public static void main(String[] args) { int[] a = new int[3]; a[0] = 0; a[1] = 1; a[2] = 2; int[] b = Arrays.copyOf(a, 10); System.out.println("b.length"+b.length); } }
结果:
</details> 10
两者联系和区别
联系:
看两者源代码可以发现 copyOf()
内部实际调用了 System.arraycopy()
方法
区别:
arraycopy()
需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf()
是系统自动在内部新建一个数组,并返回该数组。
HashMap
简介
HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是非线程安全的。
HashMap
可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个
JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 以后的 HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, HashMap
总是使用 2 的幂作为哈希表的大小。
底层数据结构分析
JDK1.8 之前
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。
HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash
判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
JDK 1.8 HashMap 的 hash 方法源码:
JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
查看代码
static final int hash(Object key) { int h; // key.hashCode():返回散列值也就是hashcode // ^ :按位异或 // >>>:无符号右移,忽略符号位,空位都以0补齐 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
对比一下 JDK1.7 的 HashMap 的 hash 方法源码.
查看代码
static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8 之后
相比于之前的版本,JDK1.8 以后在解决哈希冲突时有了较大的变化。
当链表长度大于阈值(默认为 8)时,会首先调用 treeifyBin()
方法。这个方法会根据 HashMap 数组来决定是否转换为红黑树。只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是执行 resize()
方法对数组扩容。相关源码这里就不贴了,重点关注 treeifyBin()
方法即可!
Node 节点类源码:
查看代码
// 继承自 Map.Entry<K,V> static class Node<K,V> implements Map.Entry<K,V> { final int hash;// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较 final K key;//键 V value;//值 // 指向下一个节点 Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } // 重写hashCode()方法 public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // 重写 equals() 方法 public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
树节点类源码:
查看代码
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // 父 TreeNode<K,V> left; // 左 TreeNode<K,V> right; // 右 TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; // 判断颜色 TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); } // 返回根节点 final TreeNode<K,V> root() { for (TreeNode<K,V> r = this, p;;) { if ((p = r.parent) == null) return r; r = p; }
核心参数
查看代码
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { // 序列号 private static final long serialVersionUID = 362498820763181265L; // 默认的初始容量是16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默认的填充因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 当桶(bucket)上的结点数大于这个值时会转成红黑树 static final int TREEIFY_THRESHOLD = 8; // 当桶(bucket)上的结点数小于这个值时树转链表 static final int UNTREEIFY_THRESHOLD = 6; // 桶中结构转化为红黑树对应的table的最小容量 static final int MIN_TREEIFY_CAPACITY = 64; // 存储元素的数组,总是2的幂次倍 transient Node<k,v>[] table; // 存放具体元素的集 transient Set<map.entry<k,v>> entrySet; // 存放元素的个数,注意这个不等于数组的长度。 transient int size; // 每次扩容和更改map结构的计数器 transient int modCount; // 临界值(容量*填充因子) 当实际大小超过临界值时,会进行扩容 int threshold; // 加载因子 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的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。
构造方法
HashMap 中有四个构造方法,它们分别如下:
查看代码
// 默认构造函数。 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } // 包含另一个“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); } // 指定“容量大小”和“加载因子”的构造函数 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } 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); } } } /** * Returns a power of two size for the given target capacity. * 返回给定目标容量的 2 次方 */ 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; }
put 方法
HashMap 只提供了 put 用于添加元素,putVal 方法只是给 put 方法调用的一个方法,并没有提供给用户使用。
对 putVal 方法添加元素的分析如下:
- 如果定位到的数组位置没有元素 就直接插入。
- 如果定位到的数组位置有元素就和要插入的 key 比较,如果 key 相同就直接覆盖,如果 key 不相同,就判断 p 是否是一个树节点,如果是就调用
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)
将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。
说明:上图有两个小问题:
- 直接覆盖之后应该就会 return,不会有后续操作。参考 JDK8 HashMap.java 658 行(issue#608)。
- 当链表长度大于阈值(默认为 8)并且 HashMap 数组长度超过 64 的时候才会执行链表转红黑树的操作,否则就只是对数组扩容。参考 HashMap 的
treeifyBin()
方法(issue#1087)。
查看代码
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } // 第四个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度 // 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量 // table未初始化或者长度为0,进行扩容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 桶中已经存在元素 else { Node<K,V> e; K k; // 比较桶中第一个元素(数组中的结点)的hash值相等,key相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 将第一个元素赋值给e,用e来记录 e = p; // hash值不相等,即key不相等;为红黑树结点 else if (p instanceof TreeNode) // 放入树中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 为链表结点 else { // 在链表最末插入结点 for (int binCount = 0; ; ++binCount) { // 到达链表的尾部 if ((e = p.next) == null) { // 在尾部插入新结点 p.next = newNode(hash, key, value, null); // 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法 // 这个方法会根据 HashMap 数组来决定是否转换为红黑树。 // 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); // 跳出循环 break; } // 判断链表中结点的key值与插入的元素的key值是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 相等,跳出循环 break; // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 p = e; } } // 表示在桶中找到key值、hash值与插入元素相等的结点 if (e != null) { // 记录e的value V oldValue = e.value; // onlyIfAbsent为false或者旧值为null if (!onlyIfAbsent || oldValue == null) //用新值替换旧值 e.value = value; // 访问后回调 afterNodeAccess(e); // 返回旧值 return oldValue; } } // 结构性修改 ++modCount; // 实际大小大于阈值则扩容 if (++size > threshold) resize(); // 插入后回调 afterNodeInsertion(evict); return null; }
我们再来对比一下 JDK1.7 put 方法的代码
对于 put 方法的分析如下:
- ① 如果定位到的数组位置没有元素 就直接插入。
- ② 如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和插入的 key 比较,如果 key 相同就直接覆盖,不同就采用头插法插入元素。
查看代码
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++; addEntry(hash, key, value, i); // 再插入 return null; }
get 方法
- 计算 key 的 hash 值,根据 hash 值找到对应数组下标: hash & (length-1)
- 判断数组该位置处的元素是否刚好就是我们要找的,如果不是,走第三步
- 判断该元素类型是否是 TreeNode,如果是,用红黑树的方法取数据,如果不是,走第四步
- 遍历链表,直到找到相等(==或equals)的 key
查看代码
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 数组元素相等 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 桶中不止一个节点 if ((e = first.next) != null) { // 在树中get if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); // 在链表中get do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
remove()
remove(Object key)
的作用是删除key
值对应的entry
,该方法的具体逻辑是在removeEntryForKey(Object key)
里实现的。removeEntryForKey()
方法会首先找到key
值对应的entry
,然后删除该entry
(修改链表的相应引用)。查找过程跟getEntry()
过程类似。
查看代码
//removeEntryForKey() final Entry<K,V> removeEntryForKey(Object key) { ...... int hash = (key == null) ? 0 : hash(key); int i = indexFor(hash, table.length);//hash&(table.length-1) Entry<K,V> prev = table[i];//得到冲突链表 Entry<K,V> e = prev; while (e != null) {//遍历冲突链表 Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {//找到要删除的entry modCount++; size--; if (prev == e) table[i] = next;//删除的是冲突链表的第一个entry else prev.next = next; return e; } prev = e; e = next; } return e; }
扩容方法 resize()
进行扩容,会伴随着一次重新 hash 分配,并且会遍历 hash 表中所有的元素,是非常耗时的。在编写程序中,要尽量避免 resize。
查看代码
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { // 超过最大值就不再扩充了,就只好随你碰撞去吧 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 没超过最大值,就扩充为原来的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候 newCap = oldThr; else { // 对应使用 new HashMap() 初始化后,第一次 put 的时候 // signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 计算新的resize上限 if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; // 用新的数组大小初始化新的数组 @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 如果是初始化数组,到这里就结束了,返回 newTab 即可 if (oldTab != null) { // 把每个bucket都移动到新的buckets中 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; // 如果该数组位置上只有单个元素,那就简单了,简单迁移这个元素就可以了 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; // 如果是红黑树 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // 这块是处理链表的情况, // 需要将此链表拆成两个链表,放到新的数组中,并且保留原来的先后顺序 // loHead、loTail 对应一条链表,hiHead、hiTail 对应另一条链表,代码还是比较简单的 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; // 原索引 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 原索引+oldCap else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 原索引放到bucket里 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 原索引+oldCap放到bucket里 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
ConcurrentHashMap
存储结构
Java 7 中 ConcurrentHashMap
的存储结构如上图,ConcurrnetHashMap
由很多个 Segment
组合,而每一个 Segment
是一个类似于 HashMap 的结构,所以每一个 HashMap
的内部可以进行扩容。但是 Segment
的个数一旦初始化就不能改变,默认 Segment
的个数是 16 个,你也可以认为 ConcurrentHashMap
默认支持最多 16 个线程并发。
可以发现 Java8 的 ConcurrentHashMap 相对于 Java7 来说变化比较大,不再是之前的 Segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表 / 红黑树。当冲突链表达到一定长度时,链表会转换成红黑树。
初始化
jdk1.8前
通过 ConcurrentHashMap 的无参构造探寻 ConcurrentHashMap 的初始化流程。
查看代码
/** * Creates a new, empty map with a default initial capacity (16), * load factor (0.75) and concurrencyLevel (16). */ public ConcurrentHashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL); }
无参构造中调用了有参构造,传入了三个参数的默认值,他们的值是。
查看代码
/** * 默认初始化容量 */ static final int DEFAULT_INITIAL_CAPACITY = 16; /** * 默认负载因子 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 默认并发级别 */ static final int DEFAULT_CONCURRENCY_LEVEL = 16;
接着看下这个有参构造函数的内部实现逻辑。
查看代码
@SuppressWarnings("unchecked") public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) { // 参数校验 if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); // 校验并发级别大小,大于 1<<16,重置为 65536 if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; // Find power-of-two sizes best matching arguments // 2的多少次方 int sshift = 0; int ssize = 1; // 这个循环可以找到 concurrencyLevel 之上最近的 2的次方值 while (ssize < concurrencyLevel) { ++sshift; ssize <<= 1; } // 记录段偏移量 this.segmentShift = 32 - sshift; // 记录段掩码 this.segmentMask = ssize - 1; // 设置容量 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; // c = 容量 / ssize ,默认 16 / 16 = 1,这里是计算每个 Segment 中的类似于 HashMap 的容量 int c = initialCapacity / ssize; if (c * ssize < initialCapacity) ++c; int cap = MIN_SEGMENT_TABLE_CAPACITY; //Segment 中的类似于 HashMap 的容量至少是2或者2的倍数 while (cap < c) cap <<= 1; // create segments and segments[0] // 创建 Segment 数组,设置 segments[0] Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]); Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize]; UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0] this.segments = ss; }
总结一下在 Java 7 中 ConcurrnetHashMap 的初始化逻辑。
- 必要参数校验。
- 校验并发级别 concurrencyLevel 大小,如果大于最大值,重置为最大值。无参构造默认值是 16.
- 寻找并发级别 concurrencyLevel 之上最近的 2 的幂次方值,作为初始化容量大小,默认是 16。
- 记录 segmentShift 偏移量,这个值为【容量 = 2 的N次方】中的 N,在后面 Put 时计算位置时会用到。默认是 32 - sshift = 28.
- 记录 segmentMask,默认是 ssize - 1 = 16 -1 = 15.
- 初始化 segments[0],默认大小为 2,负载因子 0.75,扩容阀值是 2*0.75=1.5,插入第二个值时才会进行扩容。
jdk1.8
查看代码
/** * Initializes table, using the size recorded in sizeCtl. */ private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { // 如果 sizeCtl < 0 ,说明另外的线程执行CAS 成功,正在进行初始化。 if ((sc = sizeCtl) < 0) // 让出 CPU 使用权 Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }
从源码中可以发现 ConcurrentHashMap 的初始化是通过自旋和 CAS 操作完成的。里面需要注意的是变量 sizeCtl
,它的值决定着当前的初始化状态。
- -1 说明正在初始化
- -N 说明有N-1个线程正在进行扩容
- 表示 table 初始化大小,如果 table 没有初始化
- 表示 table 容量,如果 table 已经初始化。
put()
查看代码
/** * Maps the specified key to the specified value in this table. * Neither the key nor the value can be null. * * <p> The value can be retrieved by calling the <tt>get</tt> method * with a key that is equal to the original key. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @return the previous value associated with <tt>key</tt>, or * <tt>null</tt> if there was no mapping for <tt>key</tt> * @throws NullPointerException if the specified key or value is null */ public V put(K key, V value) { Segment<K,V> s; if (value == null) throw new NullPointerException(); int hash = hash(key); // hash 值无符号右移 28位(初始化时获得),然后与 segmentMask=15 做与运算 // 其实也就是把高4位与segmentMask(1111)做与运算 int j = (hash >>> segmentShift) & segmentMask; if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment // 如果查找到的 Segment 为空,初始化 s = ensureSegment(j); return s.put(key, hash, value, false); } /** * Returns the segment for the given index, creating it and * recording in segment table (via CAS) if not already present. * * @param k the index * @return the segment */ @SuppressWarnings("unchecked") private Segment<K,V> ensureSegment(int k) { final Segment<K,V>[] ss = this.segments; long u = (k << SSHIFT) + SBASE; // raw offset Segment<K,V> seg; // 判断 u 位置的 Segment 是否为null if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { Segment<K,V> proto = ss[0]; // use segment 0 as prototype // 获取0号 segment 里的 HashEntry<K,V> 初始化长度 int cap = proto.table.length; // 获取0号 segment 里的 hash 表里的扩容负载因子,所有的 segment 的 loadFactor 是相同的 float lf = proto.loadFactor; // 计算扩容阀值 int threshold = (int)(cap * lf); // 创建一个 cap 容量的 HashEntry 数组 HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap]; if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck // 再次检查 u 位置的 Segment 是否为null,因为这时可能有其他线程进行了操作 Segment<K,V> s = new Segment<K,V>(lf, threshold, tab); // 自旋检查 u 位置的 Segment 是否为null while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // 使用CAS 赋值,只会成功一次 if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s)) break; } } } return seg; }
上面的源码分析了 ConcurrentHashMap 在 put 一个数据时的处理流程,下面梳理下具体流程。
-
计算要 put 的 key 的位置,获取指定位置的 Segment。
-
如果指定位置的 Segment 为空,则初始化这个 Segment.
初始化 Segment 流程:
- 检查计算得到的位置的 Segment 是否为null.
- 为 null 继续初始化,使用 Segment[0] 的容量和负载因子创建一个 HashEntry 数组。
- 再次检查计算得到的指定位置的 Segment 是否为null.
- 使用创建的 HashEntry 数组初始化这个 Segment.
- 自旋判断计算得到的指定位置的 Segment 是否为null,使用 CAS 在这个位置赋值为 Segment.
-
Segment.put 插入 key,value 值。
上面探究了获取 Segment 段和初始化 Segment 段的操作。最后一行的 Segment 的 put 方法还没有查看,继续分析。
查看代码
final V put(K key, int hash, V value, boolean onlyIfAbsent) { // 获取 ReentrantLock 独占锁,获取不到,scanAndLockForPut 获取。 HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value); V oldValue; try { HashEntry<K,V>[] tab = table; // 计算要put的数据位置 int index = (tab.length - 1) & hash; // CAS 获取 index 坐标的值 HashEntry<K,V> first = entryAt(tab, index); for (HashEntry<K,V> e = first;;) { if (e != null) { // 检查是否 key 已经存在,如果存在,则遍历链表寻找位置,找到后替换 value K k; if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { oldValue = e.value; if (!onlyIfAbsent) { e.value = value; ++modCount; } break; } e = e.next; } else { // first 有值没说明 index 位置已经有值了,有冲突,链表头插法。 if (node != null) node.setNext(first); else node = new HashEntry<K,V>(hash, key, value, first); int c = count + 1; // 容量大于扩容阀值,小于最大容量,进行扩容 if (c > threshold && tab.length < MAXIMUM_CAPACITY) rehash(node); else // index 位置赋值 node,node 可能是一个元素,也可能是一个链表的表头 setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { unlock(); } return oldValue; }
由于 Segment 继承了 ReentrantLock,所以 Segment 内部可以很方便的获取锁,put 流程就用到了这个功能。
-
tryLock() 获取锁,获取不到使用
scanAndLockForPut
方法继续获取。 -
计算 put 的数据要放入的 index 位置,然后获取这个位置上的 HashEntry 。
-
遍历 put 新元素,为什么要遍历?因为这里获取的 HashEntry 可能是一个空元素,也可能是链表已存在,所以要区别对待。
如果这个位置上的 HashEntry 不存在:
- 如果当前容量大于扩容阀值,小于最大容量,进行扩容。
- 直接头插法插入。
如果这个位置上的 HashEntry 存在:
- 判断链表当前元素 Key 和 hash 值是否和要 put 的 key 和 hash 值一致。一致则替换值
- 不一致,获取链表下一个节点,直到发现相同进行值替换,或者链表表里完毕没有相同的。
- 如果当前容量大于扩容阀值,小于最大容量,进行扩容。
- 直接链表头插法插入。
-
如果要插入的位置之前已经存在,替换后返回旧值,否则返回 null.
这里面的第一步中的 scanAndLockForPut 操作这里没有介绍,这个方法做的操作就是不断的自旋 tryLock()
获取锁。当自旋次数大于指定次数时,使用 lock()
阻塞获取锁。在自旋时顺表获取下 hash 位置的 HashEntry。
查看代码
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) { HashEntry<K,V> first = entryForHash(this, hash); HashEntry<K,V> e = first; HashEntry<K,V> node = null; int retries = -1; // negative while locating node // 自旋获取锁 while (!tryLock()) { HashEntry<K,V> f; // to recheck first below if (retries < 0) { if (e == null) { if (node == null) // speculatively create node node = new HashEntry<K,V>(hash, key, value, null); retries = 0; } else if (key.equals(e.key)) retries = 0; else e = e.next; } else if (++retries > MAX_SCAN_RETRIES) { // 自旋达到指定次数后,阻塞等到只到获取到锁 lock(); break; } else if ((retries & 1) == 0 && (f = entryForHash(this, hash)) != first) { e = first = f; // re-traverse if entry changed retries = -1; } } return node; }
jdk1.8
查看代码
public V put(K key, V value) { return putVal(key, value, false); } /** Implementation for put and putIfAbsent */ final V putVal(K key, V value, boolean onlyIfAbsent) { // key 和 value 不能为空 if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { // f = 目标位置元素 Node<K,V> f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值 if (tab == null || (n = tab.length) == 0) // 数组桶为空,初始化数组桶(自旋+CAS) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出 if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))) break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; // 使用 synchronized 加锁加入节点 synchronized (f) { if (tabAt(tab, i) == f) { // 说明是链表 if (fh >= 0) { binCount = 1; // 循环加入新的或者覆盖节点 for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; 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) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null; }
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
- 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
- 如果当前位置的
hashcode == MOVED == -1
,则需要进行扩容。 - 如果都不满足,则利用 synchronized 锁写入数据。
- 如果数量大于
TREEIFY_THRESHOLD
则要执行树化方法,在treeifyBin中会首先判断当前数组长度≥64时才会将链表转换为红黑树。
rehash() 扩容
ConcurrentHashMap 的扩容只会扩容到原来的两倍。老数组里的数据移动到新的数组时,位置要么不变,要么变为 index+ oldSize,参数里的 node 会在扩容之后使用链表头插法插入到指定位置。
查看代码
private void rehash(HashEntry<K,V> node) { HashEntry<K,V>[] oldTable = table; // 老容量 int oldCapacity = oldTable.length; // 新容量,扩大两倍 int newCapacity = oldCapacity << 1; // 新的扩容阀值 threshold = (int)(newCapacity * loadFactor); // 创建新的数组 HashEntry<K,V>[] newTable = (HashEntry<K,V>[]) new HashEntry[newCapacity]; // 新的掩码,默认2扩容后是4,-1是3,二进制就是11。 int sizeMask = newCapacity - 1; for (int i = 0; i < oldCapacity ; i++) { // 遍历老数组 HashEntry<K,V> e = oldTable[i]; if (e != null) { HashEntry<K,V> next = e.next; // 计算新的位置,新的位置只可能是不便或者是老的位置+老的容量。 int idx = e.hash & sizeMask; if (next == null) // Single node on list // 如果当前位置还不是链表,只是一个元素,直接赋值 newTable[idx] = e; else { // Reuse consecutive sequence at same slot // 如果是链表了 HashEntry<K,V> lastRun = e; int lastIdx = idx; // 新的位置只可能是不便或者是老的位置+老的容量。 // 遍历结束后,lastRun 后面的元素位置都是相同的 for (HashEntry<K,V> last = next; last != null; last = last.next) { int k = last.hash & sizeMask; if (k != lastIdx) { lastIdx = k; lastRun = last; } } // ,lastRun 后面的元素位置都是相同的,直接作为链表赋值到新位置。 newTable[lastIdx] = lastRun; // Clone remaining nodes for (HashEntry<K,V> p = e; p != lastRun; p = p.next) { // 遍历剩余元素,头插法到指定 k 位置。 V v = p.value; int h = p.hash; int k = h & sizeMask; HashEntry<K,V> n = newTable[k]; newTable[k] = new HashEntry<K,V>(h, p.key, v, n); } } } } // 头插法插入新的节点 int nodeIndex = node.hash & sizeMask; // add the new node node.setNext(newTable[nodeIndex]); newTable[nodeIndex] = node; table = newTable; }
有些同学可能会对最后的两个 for 循环有疑惑,这里第一个 for 是为了寻找这样一个节点,这个节点后面的所有 next 节点的新位置都是相同的。然后把这个作为一个链表赋值到新位置。第二个 for 循环是为了把剩余的元素通过头插法插入到指定位置链表。这样实现的原因可能是基于概率统计,有深入研究的同学可以发表下意见。
get
1.7
到这里就很简单了,get 方法只需要两步即可。
- 计算得到 key 的存放位置。
- 遍历指定位置查找相同 key 的 value 值。
查看代码
public V get(Object key) { Segment<K,V> s; // manually integrate access methods to reduce overhead HashEntry<K,V>[] tab; int h = hash(key); long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; // 计算得到 key 的存放位置 if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); e != null; e = e.next) { // 如果是链表,遍历查找到相同 key 的 value。 K k; if ((k = e.key) == key || (e.hash == h && key.equals(k))) return e.value; } } return null; }
1.8
get 流程比较简单,直接过一遍源码。
查看代码
public V get(Object key) { Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; // key 所在的 hash 位置 int h = spread(key.hashCode()); if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { // 如果指定位置元素存在,头结点hash值相同 if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) // key hash 值相等,key值相同,直接返回元素 value return e.val; } else if (eh < 0) // 头结点hash值小于0,说明正在扩容或者是红黑树,find查找 return (p = e.find(h, key)) != null ? p.val : null; while ((e = e.next) != null) { // 是链表,遍历查找 if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null; }
总结一下 get 过程:
- 根据 hash 值计算位置。
- 查找到指定位置,如果头节点就是要找的,直接返回它的 value.
- 如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之。
- 如果是链表,遍历查找之。
总结:
总的来说 ConcurrentHashMap 在 Java8 中相对于 Java7 来说变化还是挺大的
总结
Java7 中 ConcurrentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。
Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。
有些同学可能对 Synchronized 的性能存在疑问,其实 Synchronized 锁自从引入锁升级策略后,性能不再是问题,有兴趣的同学可以自己了解下 Synchronized 的锁升级。
Stack & Queue
概述
Java里有一个叫做Stack的类,却没有叫做Queue的类(它是个接口名字)。当需要使用栈时,Java已不推荐使用Stack,而是推荐使用更高效的ArrayDeque;既然Queue只是一个接口,当需要使用队列时也就首选ArrayDeque了(次选是LinkedList)。
Queue
Queue接口继承自Collection接口,除了最基本的Collection的方法之外,它还支持额外的insertion, extraction和inspection操作。这里有两组格式,共6个方法,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)。
Throws exception | Returns special value | |
---|---|---|
Insert | add(e) | offer(e) |
Remove | remove() | poll() |
Examine | element() | peek() |
Deque
Deque
是"double ended queue", 表示双向的队列,英文读作"deck". Deque 继承自 Queue接口,除了支持Queue的方法之外,还支持insert
, remove
和examine
操作,由于Deque是双向的,所以可以对队列的头和尾都进行操作,它同时也支持两组格式,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)。共12个方法如下:
First Element - Head | Last Element - Tail | |||
---|---|---|---|---|
Throws exception | Special value | Throws exception | Special value | |
Insert | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
Remove | removeFirst() | pollFirst() | removeLast() | pollLast() |
Examine | getFirst() | peekFirst() | getLast() | peekLast() |
当把Deque
当做FIFO的queue
来使用时,元素是从deque
的尾部添加,从头部进行删除的; 所以deque
的部分方法是和queue
是等同的。具体如下:
Queue Method | Equivalent Deque Method |
---|---|
add(e) | addLast(e) |
offer(e) | offerLast(e) |
remove() | removeFirst() |
poll() | pollFirst() |
element() | getFirst() |
peek() | peekFirst() |
Deque的含义是“double ended queue”,即双端队列,它既可以当作栈使用,也可以当作队列使用。下表列出了Deque与Queue相对应的接口:
Queue Method | Equivalent Deque Method | 说明 |
---|---|---|
add(e) |
addLast(e) |
向队尾插入元素,失败则抛出异常 |
offer(e) |
offerLast(e) |
向队尾插入元素,失败则返回false |
remove() |
removeFirst() |
获取并删除队首元素,失败则抛出异常 |
poll() |
pollFirst() |
获取并删除队首元素,失败则返回null |
element() |
getFirst() |
获取但不删除队首元素,失败则抛出异常 |
peek() |
peekFirst() |
获取但不删除队首元素,失败则返回null |
下表列出了Deque与Stack对应的接口:
Stack Method | Equivalent Deque Method | 说明 |
---|---|---|
push(e) |
addFirst(e) |
向栈顶插入元素,失败则抛出异常 |
无 | offerFirst(e) |
向栈顶插入元素,失败则返回false |
pop() |
removeFirst() |
获取并删除栈顶元素,失败则抛出异常 |
无 | pollFirst() |
获取并删除栈顶元素,失败则返回null |
peek() |
getFirst() |
获取但不删除栈顶元素,失败则抛出异常 |
无 | peekFirst() |
获取但不删除栈顶元素,失败则返回null |
上面两个表共定义了Deque的12个接口。添加,删除,取值都有两套接口,它们功能相同,区别是对失败情况的处理不同。一套接口遇到失败就会抛出异常,另一套遇到失败会返回特殊值(false
或null
)。除非某种实现对容量有限制,大多数情况下,添加操作是不会失败的。虽然*Deque*的接口有12个之多,但无非就是对容器的两端进行操作,或添加,或删除,或查看。明白了这一点讲解起来就会非常简单。
ArrayDeque和LinkedList是Deque的两个通用实现,由于官方更推荐使用AarryDeque用作栈和队列,加之上一篇已经讲解过LinkedList,本文将着重讲解ArrayDeque的具体实现。
从名字可以看出ArrayDeque底层通过数组实现,为了满足可以同时在数组两端插入或删除元素的需求,该数组还必须是循环的,即循环数组(circular array),也就是说数组的任何一点都可能被看作起点或者终点。ArrayDeque是非线程安全的(not thread-safe),当多个线程同时使用的时候,需要程序员手动同步;另外,该容器不允许放入null
元素。
上图中我们看到,head
指向首端第一个有效元素,tail
指向尾端第一个可以插入元素的空位。因为是循环数组,所以head
不一定总等于0,tail
也不一定总是比head
大。
方法剖析
addFirst()
addFirst(E e)
的作用是在Deque的首端插入元素,也就是在head
的前面插入元素,在空间足够且下标没有越界的情况下,只需要将elements[--head] = e
即可。
实际需要考虑: 1.空间是否够用,以及2.下标是否越界的问题。上图中,如果head
为0
之后接着调用addFirst()
,虽然空余空间还够用,但head
为-1
,下标越界了。下列代码很好的解决了这两个问题。
查看代码
//addFirst(E e) public void addFirst(E e) { if (e == null)//不允许放入null throw new NullPointerException(); elements[head = (head - 1) & (elements.length - 1)] = e;//2.下标是否越界 if (head == tail)//1.空间是否够用 doubleCapacity();//扩容 }
上述代码我们看到,空间问题是在插入之后解决的,因为tail
总是指向下一个可插入的空位,也就意味着elements
数组至少有一个空位,所以插入元素的时候不用考虑空间问题。
下标越界的处理解决起来非常简单,head = (head - 1) & (elements.length - 1)
就可以了,这段代码相当于取余,同时解决了head
为负值的情况。因为elements.length
必需是2
的指数倍,elements - 1
就是二进制低位全1
,跟head - 1
相与之后就起到了取模的作用,如果head - 1
为负数(其实只可能是-1),则相当于对其取相对于elements.length
的补码。
下面再说说扩容函数doubleCapacity()
,其逻辑是申请一个更大的数组(原数组的两倍),然后将原数组复制过去。过程如下图所示:
图中我们看到,复制分两次进行,第一次复制head
右边的元素,第二次复制head
左边的元素。
查看代码
//doubleCapacity() private void doubleCapacity() { assert head == tail; int p = head; int n = elements.length; int r = n - p; // head右边元素的个数 int newCapacity = n << 1;//原空间的2倍 if (newCapacity < 0) throw new IllegalStateException("Sorry, deque too big"); Object[] a = new Object[newCapacity]; System.arraycopy(elements, p, a, 0, r);//复制右半部分,对应上图中绿色部分 System.arraycopy(elements, 0, a, r, p);//复制左半部分,对应上图中灰色部分 elements = (E[])a; head = 0; tail = n; }
addLast()
addLast(E e)
的作用是在Deque的尾端插入元素,也就是在tail
的位置插入元素,由于tail
总是指向下一个可以插入的空位,因此只需要elements[tail] = e;
即可。插入完成后再检查空间,如果空间已经用光,则调用doubleCapacity()
进行扩容。
查看代码
public void addLast(E e) { if (e == null)//不允许放入null throw new NullPointerException(); elements[tail] = e;//赋值 if ( (tail = (tail + 1) & (elements.length - 1)) == head)//下标越界处理 doubleCapacity();//扩容 }
下标越界处理方式addFirt()
中已经讲过,不再赘述。
pollFirst()
pollFirst()
的作用是删除并返回Deque首端元素,也即是head
位置处的元素。如果容器不空,只需要直接返回elements[head]
即可,当然还需要处理下标的问题。由于ArrayDeque
中不允许放入null
,当elements[head] == null
时,意味着容器为空。
查看代码
public E pollFirst() { E result = elements[head]; if (result == null)//null值意味着deque为空 return null; elements[h] = null;//let GC work head = (head + 1) & (elements.length - 1);//下标越界处理 return result; }
pollLast()
pollLast()
的作用是删除并返回Deque尾端元素,也即是tail
位置前面的那个元素。
查看代码
public E pollLast() { int t = (tail - 1) & (elements.length - 1);//tail的上一个位置是最后一个元素 E result = elements[t]; if (result == null)//null值意味着deque为空 return null; elements[t] = null;//let GC work tail = t; return result; }
peekFirst()
peekFirst()
的作用是返回但不删除Deque首端元素,也即是head
位置处的元素,直接返回elements[head]
即可。
查看代码
public E peekFirst() { return elements[head]; // elements[head] is null if deque empty }
peekLast()
peekLast()
的作用是返回但不删除Deque尾端元素,也即是tail
位置前面的那个元素。
查看代码
public E peekLast() { return elements[(tail - 1) & (elements.length - 1)]; }
集合相关面试题
说说 List, Set, Queue, Map 四者的区别?
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。Set
(注重独一无二的性质): 存储的元素是无序的、不可重复的。Queue
(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。Map
(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
如何选用集合?
主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用 Map
接口下的集合,需要排序时选择 TreeMap
,不需要排序时就选择 HashMap
,需要保证线程安全就选用 ConcurrentHashMap
。
当我们只需要存放元素值时,就选择实现Collection
接口的集合,需要保证元素唯一时选择实现 Set
接口的集合比如 TreeSet
或 HashSet
,不需要就选择实现 List
接口的比如 ArrayList
或 LinkedList
,然后再根据实现这些接口的集合的特点来选用。
为什么要使用集合?
当我们需要保存一组类型相同的数据的时候,我们应该是用一个容器来保存,这个容器就是数组,但是,使用数组存储对象具有一定的弊端, 因为我们在实际开发中,存储的数据的类型是多种多样的,于是,就出现了“集合”,集合同样也是用来存储多个数据的。
数组的缺点是一旦声明之后,长度就不可变了;同时,声明数组时的数据类型也决定了该数组存储的数据的类型;而且,数组存储的数据是有序的、可重复的,特点单一。 但是集合提高了数据存储的灵活性,Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据。
Arraylist 和 Vector 的区别?
ArrayList
是List
的主要实现类,底层使用Object[ ]
存储,适用于频繁的查找工作,线程不安全 ;Vector
是List
的古老实现类,底层使用Object[ ]
存储,线程安全的。
Arraylist 与 LinkedList 区别?
- 是否保证线程安全:
ArrayList
和LinkedList
都是不同步的,也就是不保证线程安全; - 底层数据结构:
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)
方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i
插入和删除元素的话((add(int index, E element)
) 时间复杂度近似为o(n))
因为需要先移动到指定位置再插入。 - 是否支持快速随机访问:
LinkedList
不支持高效的随机元素访问,而ArrayList
支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)
方法)。 - 内存空间占用:
ArrayList
的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而LinkedList
的空间花费则体现在它的每一个元素都需要消耗比ArrayList
更多的空间(因为要存放直接后继和直接前驱以及数据)。
comparable 和 Comparator 的区别
comparable
接口实际上是出自java.lang
包 它有一个compareTo(Object obj)
方法用来排序comparator
接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)
方法用来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()
方法或compare()
方法,当我们需要对某一个集合实现两种排序方式,比如一个 song 对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo()
方法和使用自制的Comparator
方法或者以两个 Comparator 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 Collections.sort()
.
Comparator 定制排序
查看代码
ArrayList<Integer> arrayList = new ArrayList<Integer>(); arrayList.add(-1); arrayList.add(3); arrayList.add(3); arrayList.add(-5); arrayList.add(7); arrayList.add(4); arrayList.add(-9); arrayList.add(-7); System.out.println("原始数组:"); System.out.println(arrayList); // void reverse(List list):反转 Collections.reverse(arrayList); System.out.println("Collections.reverse(arrayList):"); System.out.println(arrayList); // void sort(List list),按自然排序的升序排序 Collections.sort(arrayList); System.out.println("Collections.sort(arrayList):"); System.out.println(arrayList); // 定制排序的用法 Collections.sort(arrayList, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2.compareTo(o1); } }); System.out.println("定制排序后:"); System.out.println(arrayList);
Output:
</details> 原始数组: [-1, 3, 3, -5, 7, 4, -9, -7] Collections.reverse(arrayList): [-7, -9, 4, 7, -5, 3, 3, -1] Collections.sort(arrayList): [-9, -7, -5, -1, 3, 3, 4, 7] 定制排序后: [7, 4, 3, 3, -1, -5, -7, -9]
重写 compareTo 方法实现按年龄来排序
查看代码
// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列 // 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他 // 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了 public class Person implements Comparable<Person> { private String name; private int age; public Person(String name, int age) { super(); this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } /** * T重写compareTo方法实现按年龄来排序 */ @Override public int compareTo(Person o) { if (this.age > o.getAge()) { return 1; } if (this.age < o.getAge()) { return -1; } return 0; } } public static void main(String[] args) { TreeMap<Person, String> pdata = new TreeMap<Person, String>(); pdata.put(new Person("张三", 30), "zhangsan"); pdata.put(new Person("李四", 20), "lisi"); pdata.put(new Person("王五", 10), "wangwu"); pdata.put(new Person("小红", 5), "xiaohong"); // 得到key的值的同时得到key所对应的值 Set<Person> keys = pdata.keySet(); for (Person key : keys) { System.out.println(key.getAge() + "-" + key.getName()); } }
Output:
</details> 5-小红 10-王五 20-李四 30-张三
无序性和不可重复性的含义是什么
1、什么是无序性?无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
2、什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重写 equals()方法和 HashCode()方法。
比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
-
HashSet
、LinkedHashSet
和TreeSet
都是Set
接口的实现类,都能保证元素唯一,并且都不是线程安全的。 -
HashSet
、LinkedHashSet
和TreeSet
的主要区别在于底层数据结构不同。HashSet
的底层数据结构是哈希表(基于HashMap
实现)。LinkedHashSet
的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。 -
底层数据结构不同又导致这三者的应用场景不同。
HashSet
用于不需要保证元素插入和取出顺序的场景,LinkedHashSet
用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet
用于支持对元素自定义排序规则的场景。
ArrayDeque 与 LinkedList 的区别
ArrayDeque
和 LinkedList
都实现了 Deque
接口,两者都具有队列的功能,但两者有什么区别呢?
ArrayDeque
是基于可变长的数组和双指针来实现,而LinkedList
则通过链表来实现。ArrayDeque
不支持存储NULL
数据,但LinkedList
支持。ArrayDeque
是在 JDK1.6 才被引入的,而LinkedList
早在 JDK1.2 时就已经存在。ArrayDeque
插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然LinkedList
不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。
从性能的角度上,选用 ArrayDeque
来实现队列要比 LinkedList
更好。此外,ArrayDeque
也可以用于实现栈。
说一说 PriorityQueue
PriorityQueue
是在 JDK1.5 中被引入的, 其与 Queue
的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队。
这里列举其相关的一些要点:
PriorityQueue
利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据PriorityQueue
通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。PriorityQueue
是非线程安全的,且不支持存储NULL
和non-comparable
的对象。PriorityQueue
默认是小顶堆,但可以接收一个Comparator
作为构造参数,从而来自定义元素优先级的先后。
PriorityQueue
在面试中可能更多的会出现在手撕算法的时候,典型例题包括堆排序、求第K大的数、带权图的遍历等,所以需要会熟练使用才行。
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+1。HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而HashMap
会将其扩充为 2 的幂次方大小(HashMap
中的tableSizeFor()
方法保证,下面给出了源代码)。也就是说HashMap
总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 - 底层数据结构: JDK1.8 以后的
HashMap
在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
HashMap
中带有初始容量的构造函数:
查看代码
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } 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; }
HashMap 和 HashSet 区别
如果你看过 HashSet
源码的话就应该知道:HashSet
底层就是基于 HashMap
实现的。(HashSet
的源码非常非常少,因为除了 clone()
、writeObject()
、readObject()
是 HashSet
自己不得不实现之外,其他方法都是直接调用 HashMap
中的方法。
HashMap |
HashSet |
---|---|
实现了 Map 接口 |
实现 Set 接口 |
存储键值对 | 仅存储对象 |
调用 put() 向 map 中添加元素 |
调用 add() 方法向 Set 中添加元素 |
HashMap 使用键(Key)计算 hashcode |
HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals() 方法用来判断对象的相等性 |
HashMap 和 TreeMap 区别
TreeMap
和HashMap
都继承自AbstractMap
,但是需要注意的是TreeMap
它还实现了NavigableMap
接口和SortedMap
接口。
实现 NavigableMap
接口让 TreeMap
有了对集合内元素的搜索的能力。
实现SortedMap
接口让 TreeMap
有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。示例代码如下:
查看代码
/** * @author shuang.kou * @createTime 2020年06月15日 17:02:00 */ 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<Person, String> treeMap = new TreeMap<>(new Comparator<Person>() { @Override public int compare(Person person1, Person 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.entrySet().stream().forEach(personStringEntry -> { System.out.println(personStringEntry.getValue()); }); } }
输出:
</details> person1 person4 person2 person3
可以看出,TreeMap
中的元素已经是按照 Person
的 age 字段的升序来排列了。
上面,我们是通过传入匿名内部类的方式实现的,你可以将代码替换成 Lambda 表达式实现的方式:
查看代码
TreeMap<Person, String> treeMap = new TreeMap<>((person1, person2) -> { int num = person1.getAge() - person2.getAge(); return Integer.compare(num, 0); });
综上,相比于HashMap
来说 TreeMap
主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。
HashSet 如何检查重复
以下内容摘自我的 Java 启蒙书《Head first java》第二版:
当你把对象加入HashSet
时,HashSet
会先计算对象的hashcode
值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode
值作比较,如果没有相符的 hashcode
,HashSet
会假设对象没有重复出现。但是如果发现有相同 hashcode
值的对象,这时会调用equals()
方法来检查 hashcode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让加入操作成功。
在openjdk8中,HashSet
的add()
方法只是简单的调用了HashMap
的put()
方法,并且判断了一下返回值以确保是否有重复元素。直接看一下HashSet
中的源码:
查看代码
// Returns: true if this set did not already contain the specified element // 返回值:当set中没有包含add的元素时返回真 public boolean add(E e) { return map.put(e, PRESENT)==null; }
而在HashMap
的putVal()
方法中也能看到如下说明:
查看代码
// Returns : previous value, or null if none // 返回值:如果插入位置没有元素返回null,否则返回上一个元素 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { ... }
也就是说,在openjdk8中,实际上无论HashSet
中是否已经存在了某元素,HashSet
都会直接插入,只是会在add()
方法的返回值处告诉我们插入前是否存在相同元素。
hashCode()
与 equals()
的相关规定:
- 如果两个对象相等,则
hashcode
一定也是相同的 - 两个对象相等,对两个
equals()
方法返回 true - 两个对象有相同的
hashcode
值,它们也不一定是相等的 - 综上,
equals()
方法被覆盖过,则hashCode()
方法也必须被覆盖 hashCode()
的默认行为是对堆上的对象产生独特值。如果没有重写hashCode()
,则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
==与 equals 的区别
对于基本类型来说,== 比较的是值是否相等;
对于引用类型来说,== 比较的是两个引用是否指向同一个对象地址(两者在内存中存放的地址(堆内存地址)是否指向同一个地方);
对于引用类型(包括包装类型)来说,equals 如果没有被重写,对比它们的地址是否相等;如果 equals()方法被重写(例如 String),则比较的是地址里的内容。
HashMap 的底层实现
JDK1.8 之前
JDK1.8 之前 HashMap
底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
JDK 1.8 HashMap 的 hash 方法源码:
JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
查看代码
static final int hash(Object key) { int h; // key.hashCode():返回散列值也就是hashcode // ^ :按位异或 // >>>:无符号右移,忽略符号位,空位都以0补齐 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
对比一下 JDK1.7 的 HashMap 的 hash 方法源码.
查看代码
static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8 之后
相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
HashMap 的长度为什么是 2 的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash
”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。
HashMap 多线程操作导致死循环问题
主要原因在于并发下的 Rehash 会造成元素之间会形成一个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。
详情请查看:https://coolshell.cn/articles/9606.html
HashMap 有哪几种常见的遍历方式?
HashMap 的 7 种遍历方式与性能分析!
ConcurrentHashMap 和 Hashtable 的区别
ConcurrentHashMap
和 Hashtable
的区别主要体现在实现线程安全的方式上不同。
- 底层数据结构: JDK1.7 的
ConcurrentHashMap
底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8
的结构一样,数组+链表/红黑二叉树。Hashtable
和 JDK1.8 之前的HashMap
的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; - 实现线程安全的方式(重要): ① 在 JDK1.7 的时候,
ConcurrentHashMap
(分段锁) 对整个桶数组进行了分割分段(Segment
),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment
的概念,而是直接用Node
数组+链表+红黑树的数据结构来实现,并发控制使用synchronized
和 CAS 来操作。(JDK1.6 以后 对synchronized
锁做了很多优化) 整个看起来就像是优化过且线程安全的HashMap
,虽然在 JDK1.8 中还能看到Segment
的数据结构,但是已经简化了属性,只是为了兼容旧版本;②Hashtable
(同一把锁) :使用synchronized
来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
两者的对比图:
Hashtable:
https://www.cnblogs.com/chengxiao/p/6842045.html>
JDK1.7 的 ConcurrentHashMap:
https://www.cnblogs.com/chengxiao/p/6842045.html>
JDK1.8 的 ConcurrentHashMap:
JDK1.8 的 ConcurrentHashMap
不再是 Segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表 / 红黑树。不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode
。当冲突链表达到一定长度时,链表会转换成红黑树。
ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
JDK1.7
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap
是由 Segment
数组结构和 HashEntry
数组结构组成。
Segment 实现了 ReentrantLock
,所以 Segment
是一种可重入锁,扮演锁的角色。HashEntry
用于存储键值对数据。
查看代码
static class Segment<K,V> extends ReentrantLock implements Serializable { }
一个 ConcurrentHashMap
里包含一个 Segment
数组。Segment
的结构和 HashMap
类似,是一种数组和链表结构,一个 Segment
包含一个 HashEntry
数组,每个 HashEntry
是一个链表结构的元素,每个 Segment
守护着一个 HashEntry
数组里的元素,当对 HashEntry
数组的数据进行修改时,必须首先获得对应的 Segment
的锁。
JDK1.8
ConcurrentHashMap
取消了 Segment
分段锁,采用 CAS 和 synchronized
来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))
synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。
Collections 工具类
Collections 工具类常用方法:
- 排序
- 查找,替换操作
- 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合)
排序操作
查看代码
void reverse(List list)//反转 void shuffle(List list)//随机排序 void sort(List list)//按自然排序的升序排序 void sort(List list, Comparator c)//定制排序,由Comparator控制排序逻辑 void swap(List list, int i , int j)//交换两个索引位置的元素 void rotate(List list, int distance)//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面
查找,替换操作
查看代码
int binarySearch(List list, Object key)//对List进行二分查找,返回索引,注意List必须是有序的 int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll) int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c) void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素 int frequency(Collection c, Object o)//统计元素出现次数 int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引,找不到则返回-1,类比int lastIndexOfSubList(List source, list target) boolean replaceAll(List list, Object oldVal, Object newVal)//用新元素替换旧元素
同步控制
Collections
提供了多个synchronizedXxx()
方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。
我们知道 HashSet
,TreeSet
,ArrayList
,LinkedList
,HashMap
,TreeMap
都是线程不安全的。Collections
提供了多个静态方法可以把他们包装成线程同步的集合。
最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。
方法如下:
查看代码
synchronizedCollection(Collection<T> c) //返回指定 collection 支持的同步(线程安全的)collection。 synchronizedList(List<T> list)//返回指定列表支持的同步(线程安全的)List。 synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(线程安全的)Map。 synchronizedSet(Set<T> s) //返回指定 set 支持的同步(线程安全的
参考
- CarpenterLee/JCFInternals
这篇关于「Java集合篇」Collection的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23Springboot应用的多环境打包入门
- 2024-11-23Springboot应用的生产发布入门教程
- 2024-11-23Python编程入门指南
- 2024-11-23Java创业入门:从零开始的编程之旅
- 2024-11-23Java创业入门:新手必读的Java编程与创业指南
- 2024-11-23Java对接阿里云智能语音服务入门详解
- 2024-11-23Java对接阿里云智能语音服务入门教程
- 2024-11-23JAVA对接阿里云智能语音服务入门教程
- 2024-11-23Java副业入门:初学者的简单教程
- 2024-11-23JAVA副业入门:初学者的实战指南