只是不愿随波逐流 ...|

lidongdongdong~

园龄:2年7个月粉丝:14关注:8

12、容器工具类

内容来自王争 Java 编程之美

上一节,我们罗列了 JCF 中的容器,实际上,JCF 还提供了一个工具类 Collections,是开发中非常常用的一个类
Collections 类中包含大量操作容器的静态函数,比如 sort() 函数、emptyList() 函数等
其中涉及的一些知识点,也在面试中经常被考到,比如 sort() 函数底层的 TimSort 和 DualPivotQuickSort 两种排序算法的实现原理和区别是什么

本节,我们从函数定义、使用方法、底层原理三个方面,详细讲讲 Collections 工具类中常用的并且比较复杂的函数,它们分别是

  • sort()
  • binarySearch()
  • emptyXXX()
  • synchronizedXXX()
  • unmodifiableXXX()

其中,XXX 可以替换为 Colleciton、List、Set、Map 等
对于其他比较简单的函数,比如 copy() 函数、addAll() 函数等,你可以自行查阅学习

1、sort()

1.1、函数定义

Colletions 类中的 sort() 函数用来对 List 进行排序,默认排序方式为从小到大排序
当然,我们也可以通过向 sort() 函数,传入 Comparator 接口的匿名类对象,改变排序方式
sort() 函数的定义如下所示

public static <T extends Comparable<? super T>> void sort(List<T> list);
public static <T> void sort(List<T> list, Comparator<? super T> c);

前面讲到,但凡实现中要涉及到元素比较操作的类或函数,几乎都会用到 Comparable 和 Comparator 这两个接口
所以,如果 List 容器中的元素对应的类,没有实现 Comparable 接口,并且没有在 sort() 函数中传入 Comparator 接口的匿名类对象
那么,我们将无法使用 sort() 函数对这个List 容器进行排序

1.2、使用方法

sort() 函数的定义非常简单,我们再来看一下 sort() 函数的使用方法,示例如下所示

List<Integer> list = new LinkedList<>(); // 或 new ArrayList<>();
list.add(3);
list.add(6);
list.add(2);
Collections.sort(list);
for (Integer i : list) System.out.println(i); // 输出 2、3、6
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
for (Integer i : list) System.out.println(i); // 输出 6、3、2

1.3、底层原理

Collections

我们知道,绝大部分排序算法都是运行在数组之上的,因为排序的过程需要用到数组按照下标快速访问元素的特性
而在上述代码中,我们使用 sort() 函数,对链表(LinkedList)进行了排序,这又是如何做到的呢?

实际上,不管是 ArrayList 容器还是 LinkedList 容器,使用 sort() 函数进行排序时,底层都会先将其转换为数组,对数组进行排序之后,再将排好序的数据存入容器中

// 位于 Collections 类中
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
// 位于 List 接口中
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}

在上述源码中,我们看到,Collections 类的 sort() 函数最终调用 Arrays 类的 sort() 函数,完成了对数组中数据的排序
Arrays 类的作用跟 Collections 类类似,Collections 类是容器的工具类,Arrays 类是数组的工具类
Arrays 类中定义了一系列处理数组的静态函数,比如 sort()、binarySearch()、equals() 等

实际上,从上述源码,我们还可以发现,我们不仅可以使用 Collections 类的 sort() 函数对 List 容器进行排序,还可以直接调用 List 容器自身的 sort() 函数进行排序
不管使用哪种方法排序,底层都是依赖 Arrays 的 sort() 函数

List<Integer> list = new LinkedList<>();
list.add(3);
list.add(6);
list.add(2);
// Comparator 接口的匿名类对象为 null
// 使用 Integer 实现的 Comparable 接口进行元素大小比较
list.sort(null);
for (Integer i : list) System.out.println(i); // 输出 2、3、6

Arrays

学过数据结构和算法的同学都知道,常用的排序算法有:选择排序、插入排序、冒泡排序、归并排序、快速排序、堆排序等
那么,Collections 类的 sort() 函数底层依赖的 Arrays 类的 sort() 函数,是用哪种算法实现的呢?

前面的章节中讲到,数组可以分为基本类型数组和对象数组
基本类型数组中存储的是 int、long 等基本类型元素,对象数组中存储的是 Integer、String、Student 等对象
对于基本类型数组排序,在 JDK 8 及其以后版本中,Arrays 类使用 DualPivotQuickSort 来实现,在 JDK 7 及其之前版本中,Arrays 类使用快速排序来实现
对于对象数组,在 JDK 8 及其以后版本中,Arrays 使用 TimSort 来实现,在 JDK 7 及其之前版本中,Arrays 类使用归并排序来实现

在 JDK 7 及其之前版本中

  • 对于基本类型数组排序:快速排序
  • 对于对象数组排序:归并排序

在 JDK 8 及其以后版本中

  • 对于基本类型数组排序:DualPivotQuickSort
  • 对于对象数组排序:TimSort

Arrays 类中跟 sort() 函数相关的源码在 JDK 8 中如下所示

// 基本类型数组排序
// Arrays 中还定义了参数为 long[]、short[]、char[]、float[]、double[] 数组
// 的 sort() 函数, 定义和实现跟 sort(int[] a) 类似, 这里就不重复罗列了
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
// 对象数组排序
public static void sort(Object[] a) {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a);
else
ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
}
public static <T> void sort(T[] a, Comparator<? super T> c) {
if (c == null) {
sort(a);
} else {
if (LegacyMergeSort.userRequested)
legacyMergeSort(a, c);
else
TimSort.sort(a, 0, a.length, c, null, 0, 0);
}
}

因为 Collections 类中的 sort() 函数是对 List 容器进行排序,而容器只能存储对象,不能存储基本类型数据(跟容器的设计有关,在后面讲到泛型时会讲解)
所以,Collections 类中的 sort() 函数调用的是 Arrays 类中对对象数组排序的 sort() 函数,所以,Collections 类的 sort() 函数是使用 TimSort 排序算法来实现的
只有下面这样,直接使用 Arrays 类的 sort() 函数对基本类型数组排序,才会使用 DualPivotQuickSort 排序算法

int[] a = {1, 4, 2, 3};
Arrays.sort(a);
for (int i = 0; i < a.length; ++i) {
System.out.println(a[i]);
}

那么,新的问题又来了,为什么要区别对待基本类型数组和对象数组,分别设计不同的排序算法呢?

在《数据结构与算法之美》中,我们讲到,评价一个排序算法的维度有很多,其中一个重要的维度便是稳定性,对于对象数组来说,我们一般会按照对象中的某个属性来排序
在数据结构中,我们把用于构建数据结构的属性叫做键,对象中的其他属性叫做卫星数据,作为附属数据存储在数据结构中
键相同的对象,数据未必相同,稳定性在某些场景中是必须的

所以,针对对象数组的排序,Arrays 类使用稳定的 TimSort 来实现
对于基本类型数组来说,相等的数据谁在前谁在后,毫无差别,所以,针对基本类型数据的排序,Arrays 类使用对稳定性没有要求的 DualPivotQuickSort 来实现

接下来,我们简单看一下这两种排序算法的实现原理

DualPivotQuickSort

DualPivotQuickSort 中文翻译为 "双轴快速排序算法" 或者 "双分区点快速排序算法",名字有点以偏概全
因为在其代码实现中,不仅仅用到了 "双轴快速排序算法",还用到了插入排序、计数排序、归并排序等多种排序算法

插入排序、计数排序、归并排序在《数据结构与算法之美》中都有介绍,我们不再赘述,现在,我们来看下双轴快速排序算法

我们知道,快速排序算法使用一个分区点(pivot)对数组进行分区,双轴快速排序算法使用两个分区点来对数组进行分区,这也是双轴快速排序算法名称的由来
双轴快速排序算法根据两个分区点(LP、RP),将整个待排序区间,分为三个小区间
如图所示,最左边的元素都小于 LP,最右边的元素都大于 RP,中间的元素大于等于 LP 小于等于 RP
image
分区点的选择有多种方法,可以使用区间的首元素作为 LP,尾元素作为 RP,如果首元素大于尾元素,交换两个值
当然,在 DualPivotQuickSort 中,分区方式更加复杂,采用 5 数取 2 法,你可以自己去阅读源码了解

"双轴快速排序算法" 是对快速排序算法的改进,尽管平均情况下的时间复杂度仍然是 O(N * logN),最差情况下的时间复杂度仍然是 O(n ^ 2)
但在执行过程中,双轴快速排序算法扫描一遍未排序区间,会将区间划分为 3 个小区间,而快速排序算法只能划分为 2 个小区间
所以,双轴快速排序算法递归的深度更浅,累计总扫描元素个数,要比快速排序少,所以,执行效率更高
image
你可能会说,是不是分区点越多性能越好呢?如果采用更多分区点,将一个区间划分为更多小区间,递归深度会更浅,累计总扫描元素个数会更少,这样是不是效率就更高了呢?
实际上,分区操作本身也是有成本的,如果分区点太多,每个元素都要跟多个分区点比较才能将其放入合适的分区中,这个操作将会消耗比较多的时间,也会影响到整体性能

DualPivotQuickSort 算法的实现,位于 java.util.DualPivotQuickSort 类中
因为基本类型无法使用泛型,所以,针对不同基本类型的数组,DualPivotQuickSort 类定义了不同的 sort() 函数,如下所示
每个函数都要重复实现了一遍,所以,DualPivotQuickSort 类中的代码行数非常多,有 2000 多行,但核心逻辑并不复杂

// DualPivotQuickSort 类中 sort() 函数定义
static void sort(int[] a, int left, int right, int[] work, int workBase, int workLen);
static void sort(long[] a, int left, int right, long[] work, int workBase, int workLen);
static void sort(float[] a, int left, int right, float[] work, int workBase, int workLen);
static void sort(double[] a, int left, int right, double[] work, int workBase, int workLen);
static void sort(short[] a, int left, int right, short[] work, int workBase, int workLen);
static void sort(char[] a, int left, int right, char[] work, int workBase, int workLen);
static void sort(byte[] a, int left, int right);

其中,针对 int、long、float、double 这 4 种类型的数组,其排序过程几乎相同,对 char、short、byte 这 3 类型的数组,其排序过程稍有不同,是在前者基础上的改进
我们先来看下如何排序 int、long、float、double 这 4 种类型的数组

如果待排序数组长度小于预先定义的阈值 QUICKSORT_THRESHOLD(值为 286),DualPivotQuickSort 执行改进后的双轴快速排序算法
在改进后的双轴快速排序算法中,在递归处理过程中,当待排序区间长度小于 INSERTION_SORT_THRESHOLD(值为 47)时,改为使用插入排序算法
因为对于小规模数据的排序,时间复杂度不再起决定作用,尽管插入排序算法的时间复杂度比较高,但因为处理过程简单,反倒会比快速排序算法执行得快

如果待排序数组长度大于等于预先定义的阈值 QUICKSORT_THRESHOLD(值为 286),DualPivotQuickSort 会先考察待排序数组的数据的有序性,是杂乱无章还是基本有序?
考察方法比较特别:从头到尾扫描数组,统计数组中连续增或连续减的区间个数
如果区间个数超过预先定义的阈值 MAX_RUN_COUNT(值为 67),则说明待排序数组中的数据偏杂乱无章一些,于是就选择执行改进后的双轴快速排序
否则,就选择执行非递归版的归并排序
之所以这样选择,是因为对于快速排序来说,待排序数组中数据的有序性越高,排序耗时越多
有点反直觉,如有疑惑,可以参看《数据结构与算法之美》书中对快速排序算法的讲解

当待排序数组中数据的有序性较高时,DualPivotQuickSort 选择执行非递归版的归并排序
相对于递归版的归并排序,非递归版的归并排序只有合并过程,没有递归分解过程,省掉了函数调用栈对空间的开销和函数调用对时间的开销,具体的实现原理大体如下示例所示
image
DualPivotQuickSort 类中的代码实现比较复杂,为了让你对上述处理过程有比较清晰的了解,我们抛开 Java 源码实现,我们将上述处理过程,重新实现了一遍,如下代码所示
实际上,以下实现思路也可以应用到链表上的非递归归并排序

public void mergeSort(int[] arr) {
int n = arr.length;
int step = 1;
// 存放两个相邻区间的起始下标
int[][] intervals = new int[2][2];
while (step < n) {
int seq = 0; // 值为 0 或 1, 标记相邻区间中的前一个和后一个
int i = 0; // 区间起始下标
while (i < n) {
int s = i;
int e = i + step; // 分离出区间 [s, e)
if (e > n) {
e = n;
}
// 将区间[s, e)放入到intervals
intervals[seq][0] = s;
intervals[seq][1] = e;
if (seq == 1) { // 两个相邻区间凑够了
seq = 0;
merge(arr, intervals[0][0], intervals[0][1],
intervals[1][0], intervals[1][1]);
} else { // 相邻区间中的前一个找到了, 该找下一个了
seq++;
}
i += step;
}
step *= 2; // 待 merge 区间长度变大
}
}
// 合并两个有序区间 [start1, end1) 和 [start2, end2)
private void merge(int[] arr, int start1, int end1, int start2, int end2) {
int[] tmp = new int[end2 - start1];
int i = start1;
int j = start2;
int k = 0;
while (i < end1 && j < end2) {
if (arr[i] <= arr[j]) {
tmp[k++] = arr[i++];
} else {
tmp[k++] = arr[j++];
}
}
while (i < end1) {
tmp[k++] = arr[i++];
}
while (j < end2) {
tmp[k++] = arr[j++];
}
for (int ti = 0; ti < tmp.length; ++ti) {
arr[start1 + ti] = tmp[ti];
}
}

刚刚讲的是对 int、long、float、double 这 4 种类型数组的排序过程,现在来看对 char、short、byte 这 3 种类型

  • 对于 char 类型和 short 类型的数组,如果数组元素个数大于 COUNTING_SORT_THRESHOLD_FOR_SHORT_OR_CHAR(值为 3200),将执行计数排序
    否则执行跟上述对 int 等 4 种类型的数组相同的排序过程
  • 对于 byte 类型的数组,如果数组元素个数大于 COUNTING_SORT_THRESHOLD_FOR_BYTE(值为 29),将执行计数排序,否则执行插入排序

TimSort

TimSort 用来排序对象数组,其使用非递归版本归并排序算法,在归并排序的过程中,大的待排序区间不断被分解为小的待排序区间
如果待排序区间的长度小于MIN_MERGE(值为 32),就不再继续分解,转而执行二分插入排序算法

我们来看下 "二分插入排序" 算法是如何工作的

"二分插入排序" 将数组分为两部分:已排序区间和未排序区间,初始化已排序区间为空,未排序区间为整个数组
每次从未排序区间中取出一个数,插入到已排序区间中,并保持已排序区间继续有序

  • "普通插入排序" 是通过 "遍历已排序区间",查找未排序区间中的数据在已排序区间中的插入位置
  • "二分插入排序" 是通过 "二分查找算法",查找未排序区间中的数据在已排序区间中的插入位置
    当查找到插入位置之后,通过调用 system.arraycopy() 函数,将插入点之后的数据整体快速后移一位,腾出位置给要插入的数据

2、binarySearch()

2.1、函数定义

binarySearch() 函数用来对已排序的 List 容器进行二分查找,binarySearch() 函数的定义如下所示
二分查找也会涉及到元素的比较大小,所以,跟 sort() 函数类似
要么 List 容器中的元素对应的类实现 Comparable 接口,要么在使用 binarySearch() 函数时,我们主动传入 Comparator 接口的匿名类对象

public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key);
public static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c);

2.2、使用方法

binarySearch() 函数的使用方式,如下代码所示

List<Integer> list = new LinkedList<>(); // 也可 new ArrayList();
list.add(3);
list.add(6);
list.add(2);
Collections.sort(list);
int idx = Collections.binarySearch(list, 3);
System.out.println(idx); // 返回下标 1

2.3、底层原理

我们知道,二分查找算法运行在数组之上,因为查找过程需要用到数组支持按照下标快速访问的特性
但是,在上述示例代码中,binarySearch() 函数实现的二分查找,也可以运行在链表(LinkedList)之上,这是怎么做到的呢?我们来剖析一下 binarySearch() 函数的源码

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);
}

从上述源码中,我们可以发现,如果要查找的 List 容器实现了 RandomAccess 接口,比如 ArrayList,那就表明容器支持随机访问,即支持按照下标快速访问元素
这种情况下,binarySearch() 函数就使用普通的二分查找算法来实现,代码如下所示

private static <T> int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key) {
int low = 0;
int high = list.size() - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
Comparable<? super T> midVal = list.get(mid);
int cmp = midVal.compareTo(key);
if (cmp < 0)
low = mid + 1;
else if (cmp > 0)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found
}

如果要应用二分查找的 List 容器没有实现 RandomAccess 接口,比如 LinkedList,这时又分两种情况来看

  • 如果容器中的元素个数少于 BINARYSEARCH_THRESHOLD(值为 5000),这时就使用跟 ArrayList 相同的二分查找实现代码,如上 indexedBinarySearch() 代码所示
    不过,在 LinkedList 容器上调用 get(mid) 函数查找中间节点,需要遍历整个链表,时间复杂度为 O(n)
    每次 while 循环都会将区间大小变为原来的 1 / 2,最差情况下,直到区间大小为 0 时,while 循环才停止
    此时总共进行了 logN 次 while 循环,也就是执行了 logN 次 get(mid) 函数
    综上所述,indexedBinarySearch() 函数运行在 LinkedList 容器上的时间复杂度是 O(NlogN)
    关于二分查找的复杂度分析,你可以参考《数据结构与算法之美》
    当 LinkedList 容器中元素个数小于 5000 个时,尽管 indexedBinarySearch() 的时间复杂度有点高,但因为数据量比较小,而且代码实现也比较简单,所以,执行效率可以接受
  • 但当容器中的元素个数比较多,大于等于 5000 个时,我们就需要对 indexedBinarySearch() 代码进行优化了,转而使用新的二分查找代码实现,如下所示
private static <T> int iteratorBinarySearch(List<? extends Comparable<? super T>> list, T key) {
int low = 0;
int high = list.size() - 1;
ListIterator<? extends Comparable<? super T>> i = list.listIterator();
while (low <= high) {
int mid = (low + high) >>> 1;
Comparable<? super T> midVal = get(i, mid);
int cmp = midVal.compareTo(key);
if (cmp < 0)
low = mid + 1;
else if (cmp > 0)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found
}

在上述 iteratorBinarySearch() 函数中,get(i, mid) 函数是通过迭代器来查找中间节点,get(i, mid) 函数的代码实现如下所示
get(i, mid) 函数每次都从上一个 mid 的位置向前或向后查找,查找的范围不再是整个链表,而是当前二分查找的区间 [low, high]
所以,get(i, mid) 函数的查找范围变小了,执行效率相较于 indexedBinarySearch() 的 get(mid) 函数变高了

  • 第一次 while 循环,get(i, mid) 函数遍历的区间长度为 n / 2
  • 第二次 while 循环,get(i, mid) 函数遍历的区间长度为 n / 4
  • 依次类推,最差情况下,遍历元素的总个数为 n / 2 + n / 4 + ... + 1 ,其值约等于 n

综上所述,在 LinkedList 容器上执行 iteratorBinarySearch() 函数的时间复杂度为 O(n)

private static <T> T get(ListIterator<? extends T> i, int index) {
T obj = null;
int pos = i.nextIndex();
if (pos <= index) {
do {
obj = i.next();
} while (pos++ < index);
} else {
do {
obj = i.previous();
} while (--pos > index);
}
return obj;
}

3、emptyXXX()

3.1、函数定义

emptyXXX() 函数用来返回一个空的容器,其中的 XXX 可以替换为 List、Set、Map,函数定义如下所示

public static final <T> List<T> emptyList();
public static final <T> Set<T> emptySet();
public static final <K,V> Map<K,V> emptyMap();

3.2、使用方法

emptyXXX() 函数一般用来替代返回 null 值,因为返回 null 值有可能导致外部代码在调用函数时发生 NullPointerException 异常,如下示例代码所示

public class Demo11_1 {
public static List<String> queryNames(List<Integer> ids) {
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
// ...
}
public static void main(String[] args) {
List<Integer> ids = new ArrayList<>();
List<String> names = queryNames(ids);
// 如果 queryNames 返回 null 而不是 emptyList()
// 此处将抛出 NullPointerException
for (String name : names) {
// ...
}
}
}

不过,使用 emptyXXX() 函数也要相当注意,有时候会产生意想不到的运行结果,如下代码,你觉得运行结果是什么?

List<Integer> ids = new ArrayList<>();
List<String> names1 = queryNames(ids);
List<String> names2 = queryNames(null);
System.out.println(names1 == names2); // true
names1.add("wangzheng"); // UnsupportedOperationException 异常
System.out.println(names1.size());

上述代码的运行结果是,第一个 print 语句打印结果为 true
第二个 print 语句未执行,在执行 names.add("wangzheng") 语句时,代码抛出 UnsupportedOperationException 运行时异常

3.3、底层原理

很多人对 emptyList() 函数有所误解,以为执行 return Collections.emptyList() 就相当于执行了 return new ArrayList() 或 return new LinkedList(),实际上并不是
Collections.emptyList() 函数返回的是一个新类(EmptyList)的对象,并且这个对象是 Collections 的静态成员变量,如下代码所示

// Collections 类中的部分代码
public static final List EMPTY_LIST = new EmptyList<>();
public static final <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}
private static class EmptyList<E> extends AbstractList<E> implements RandomAccess, Serializable {
public Iterator<E> iterator() {
return emptyIterator();
}
public ListIterator<E> listIterator() {
return emptyListIterator();
}
public int size() {
return 0;
}
public boolean isEmpty() {
return true;
}
public boolean contains(Object obj) {
return false;
}
public boolean containsAll(Collection<?> c) {
return c.isEmpty();
}
public Object[] toArray() {
return new Object[0];
}
public <T> T[] toArray(T[] a) {
if (a.length > 0)
a[0] = null;
return a;
}
public E get(int index) {
throw new IndexOutOfBoundsException("Index: " + index);
}
public boolean equals(Object o) {
return (o instanceof List) && ((List<?>) o).isEmpty();
}
public int hashCode() {
return 1;
}
// ... 省略部分代码 ...
}

因此,两次调用 Collections.emptyList() 函数获取的值相同,也就是说,names1 和 names2 指向相同的 EmptyList 对象,第一条 println 语句打印结果为 true
因为 EmptyList 类并未重新实现 add() 函数,add() 函数继承自 AbstractList 类
而 AbstractList 类中的 add() 函数的代码实现如下所示,所以,当执行 names1.add("wangzheng") 语句时会抛出异常

public void add(E element) {
throw new UnsupportedOperationException();
}

为了避免以上问题,我们一般会在外部代码中重新定义一个容器,将调用函数返回的结果通过 addAll() 添加到容器中,示例代码如下所示

List<Integer> ids = new ArrayList<>();
List<String> names = new ArrayList<>();
names.addAll(queryNames(ids));
names.add("wangzheng");
System.out.println(names.size()); // 打印结果 1

4、synchronizedXXX()

4.1、函数定义

上一节讲到,JCF 中的容器都是非线程安全的,那么如果我们要使用线程安全的容器,该怎么办呢?
当然,我们首选使用不管是性能还是功能都更优的 JUC(java.util.concurrent)并发容器,如 ConcurrentHashMap、CopyOnWriteArrayList 等
当没有合适的 JUC 并发容器可以使用时,我们可以使用 Collections 类中的 synchronizedXXX() 函数来创建线程安全的容器
synchronizedXXX() 函数的定义如下所示

public static <T> Collection<T> synchronizedCollection(Collection<T> c);
public static <T> List<T> synchronizedList(List<T> list);
public static <T> Set<T> synchronizedSet(Set<T> s);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);

4.2、使用方法

我们可以按照如下代码所示,创建线程安全的容器

List list = Collections.synchronizedList(new LinkedList<>());

4.3、底层原理

synchronizedXXX() 函数的底层实现原理非常简单,仅仅通过对每个方法进行加锁,来避免了并发访问,如下源码所示,这也是导致其性能不高的原因

public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}
public boolean equals(Object o) {
if (this == o)
return true;
synchronized (mutex) {
return list.equals(o);
}
}
public int hashCode() {
synchronized (mutex) {
return list.hashCode();
}
}
public E get(int index) {
synchronized (mutex) {
return list.get(index);
}
}
public E set(int index, E element) {
synchronized (mutex) {
return list.set(index, element);
}
}
public void add(int index, E element) {
synchronized (mutex) {
list.add(index, element);
}
}
public E remove(int index) {
synchronized (mutex) {
return list.remove(index);
}
}
// ... 省略其他方法 ...
}

5、unmodifiableXXX()

5.1、函数定义

unmodifiableXXX() 函数用来返回不可变容器,这里的不可变指的是容器内的数据只能访问,不可增删,unmodifiableXXX() 函数的定义如下所示

public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c);
public static <T> List<T> unmodifiableList(List<? extends T> list);
public static <T> Set<T> unmodifiableSet(Set<? extends T> s);
public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m);

5.2、使用方法

使用场景示例如下所示,查询商品列表的 listItems() 函数返回的容器不能被更改,外部程序只能通过类暴露的接口来添加删除商品

public class ShoppingCart {
public static class Item {
private int id;
private int price;
public Item(int id, int price) {
this.id = id;
this.price = price;
}
}
private List<Item> items = new ArrayList<>();
public void addItem(Item item) {
items.add(item);
}
public void removeItem(Item item) {
items.remove(item);
}
// 查询商品列表的 listItems() 函数返回的容器不能被更改
// 外部程序只能通过类暴露的接口来添加删除商品
public List<Item> listItems() {
return Collections.unmodifiableList(items);
}
}
public class Demo11_1 {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.addItem(new Item(1, 10));
cart.addItem(new Item(2, 13));
List<Item> items = cart.listItems();
// 抛出 UnsupportedOperationException 异常
items.add(new Item(3, 12));
}
}

5.3、底层原理

实际上,unmodifiableXXX() 函数的实现也比较简单,其实现原理跟 synchronizedXXX() 函数的实现原理类似
如下代码所示,定义新的 UnmodifiableXXX 类,重写 add()、remove() 等增删操作,让其抛出 UnsupportedOperationException 异常

public static <T> List<T> unmodifiableList(List<? extends T> list) {
return (list instanceof RandomAccess ?
new UnmodifiableRandomAccessList<>(list) :
new UnmodifiableList<>(list));
}
static class UnmodifiableList<E> extends UnmodifiableCollection<E> implements List<E> {
final List<? extends E> list;
UnmodifiableList(List<? extends E> list) {
super(list);
this.list = list;
}
public boolean equals(Object o) {
return o == this || list.equals(o);
}
public int hashCode() {
return list.hashCode();
}
public E get(int index) {
return list.get(index);
}
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
// ... 省略其他方法和属性 ...
}

6、课后思考题

1、Collections 为什么设计成工具类?是否可以将其中的方法转移到相关的 List 类中?

2、请简单总结一下 Collections.sort()、List.sort()、Arrays.sort() 三者之间的关系,底层使用什么排序算法来实现?

Collections.sort() 底层依赖 List.sort() 来实现
List.sort() 底层依赖 Arrays.sort() 来实现
针对对象数组的 Arrays.sort() 底层采用 TimSort 排序算法来实现
针对基本类型数组的 Arrays.sort() 底层采用 DualPivotQuickSort 来实现
posted @   lidongdongdong~  阅读(42)  评论(0编辑  收藏  举报
评论
收藏
关注
推荐
深色
回顶
展开
点击右上角即可分享
微信分享提示