文章目录
- 一 简介
- 二 底层数据结构总结
- 1 List
- 2 Set
- 3 Queue
- 4 Map
- 三 Collection 的子接口 List
- 1 ArrayList 与 Vector
- 2 ArrayList 与 LinkedList
- 3 ArrayList 的 JDK 7/8 差异
- 4 ArrayList 的构造方法与扩容机制*
- 四 Collection 的子接口 Set
- 1 HashSet、LinkedHashSet 和 TreeSet
- 2 HashSet / HashMap 加入实例时,查重的方式**
- 五 Collection 的子接口 Queue
- 1 Queue 与 Deque
- 2 PriorityQueue【Java堆实现】
- 3 ArrayDeque 与 LinkedList【Java栈和队列实现】
- 六 Collection 的子接口 Map
- 1 HashMap 和 Hashtable
- 2 HashMap 的长度为什么是 2 的幂次方
- 3 ConcurrentHashMap 和 Hashtable
- 4 为什么JDK1.8的 ConcurrentHashMap 使用 CAS+Synchronized 代替 Segment*
- 5 CAS (Compare And Swap)
- 6 JDK8 相较于 JDK7 在底层实现方面的不同
- 7 HashMap 底层实现的参数
- 9 JDK 8 HashMap 的扩容
- 七 Iterator
- 1 类与接口
- 2 只读使用方法
- 3 迭代的问题:在循环中增加或删除元素
- 八 Collections 工具类
- 1 对传入的容器接口对象进行操作:查找/替换/排序/添加/修改
- 2 返回一个容器接口对象
一 简介
Java容器可大致分为 List、Queue、Set、Map 四种。
List:存储的元素是有序的、可重复的Set:存储的元素是无序的、不可重复的Queue:按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的Map:使用键值对(key-value)存储,类似于数学上的函数 y=f(x),“x” 代表 key,“y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值

二 底层数据结构总结
1 List
| 类 | 数据结构 | 备注 |
|---|---|---|
ArrayList | Object[] | |
Vector | Object[] | |
LinkedList | 双向链表 | JDK1.6 之前为循环链表,JDK1.7 取消了循环,常用的Java栈、队列实现 |
- 栈采用
LinkedList或ArrayDeque的Deque(双端队列)接口 - 队列使用
LinkedList的Queue接口 - 栈和队列都是双端队列的特殊情况
// LinkedList节点,被定义为私有的静态内部类
private static class Node<E> {E item;Node<E> next;Node<E> prev;
}
2 Set
Set是Map的特殊形式,只有key有意义,其value均为相同的固定值
| 类 | 数据结构 | 备注 |
|---|---|---|
HashSet | HashMap | 存储的元素无序 |
LinkedHashSet | LinkedHashMap | 存储的元素可以选择按插入顺序或访问顺序排序,默认是插入顺序 |
TreeSet | TreeMap | 存储的元素有序 |
3 Queue
| 类 | 数据结构 | 备注 |
|---|---|---|
PriorityQueue | Object[] 实现二叉堆 | 存储的元素按照指定的权值和顺序组织,首个元素有序,其它元素无序(常用的Java堆实现) |
ArrayDeque | Object[] + 双指针 |
4 Map
| 类 | 数据结构 | 备注 |
|---|---|---|
HashMap | 数组+链表/红黑树 | 存储的元素无序 |
LinkedHashMap | 数组+链表/红黑树,且 Entry 存在双向的引用 | 存储的元素可以选择按插入顺序或访问顺序排序 |
TreeMap | 红黑树(自平衡的排序二叉树) | 存储的元素有序 |
HashTable | 数组+链表 | 存储的元素无序 |
HashMap- JDK1.8 之前
HashMap由 数组+链表 组成的,数组是HashMap的主体,链表则是主要为了使用拉链法解决哈希冲突而存在的 - JDK1.8 以后当链表长度大于阈值(默认为 8)且键值对个数大于64时,将链表转化为红黑树,以减少搜索时间
- 如果键值对个数小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)
- 相较于 AVL 树,红黑树减弱了对平衡的要求,降低了保持平衡需要的开销
- JDK1.8 之前
// HashMap数据结构
class HashMap<K, V> extends ... {Entry<K, V>[] table;int size; // 键值对个数int threshold; // 扩容阈值 = table.length * loadFactorfloat loadFactor; // 负载因子,默认0.75// HashMap Entrystatic class Entry<K, V> implements Map.Entry<K, V> {K key;V value;Entry<K, V> next; // 用于拉链法解决哈希冲突int hash; // key哈希值}
}

LinkedHashMap- 继承自
HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成 - 每个
Entry增加了双向引用,构成一条双向链表,可以 保持键值对的插入顺序或访问顺序 - 访问顺序指的是,对一个
key执行put/get后,将键值对移动到链表末尾(便于实现 LRU) - 指定按访问顺序组织链表,需要调用构造方法
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)并指定accessOrder = true
- 继承自
TreeMap- 存放的元素必须是全序的:构造时元素需要实现
Comparable接口,或者指定Comparator - 迭代时输出按键排序
- 存放的元素必须是全序的:构造时元素需要实现
// TreeMap数据结构
class TreeMap<K, V> extends... {Comparator<? super K> comparator;Entry<K, V> root;int size;// TreeMap Entry(红黑树节点)static final class Entry<K, V> implements Map.Entry<K, V> {K key;V value;Entry<K, V> left;Entry<K, V> right;Entry<K, V> parent;boolean color; // 标志节点红或黑}
}*
三 Collection 的子接口 List
1 ArrayList 与 Vector
ArrayList和Vector的底层都是Object[]存储的,即对数组进行封装ArrayList是List的主要实现类,适用于频繁的查找工作,线程不安全Vector是List的古老实现类,线程安全
2 ArrayList 与 LinkedList
- 都是线程不安全的
ArrayList底层使用Object[]存储(支持随机访问),LinkedList使用双向链表存储(不支持随机访问)ArrayList的空间浪费主要体现在列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)- 获取/插入/删除元素的时间复杂度不同,体现在数组与链表的区别
3 ArrayList 的 JDK 7/8 差异
- JDK 7:
ArrayList的对象的创建类似于单例的饿汉式
ArrayList list = new ArrayList();//底层创建了长度是*10*的Object[]数组elementData
list.add(123);//elementData[0] = new Integer(123);
list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。/*
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)
*/
- JDK 8:
ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存
ArrayList list = new ArrayList(); // 底层Object[] elementData初始化为{}.并没有创建长度为10的数组
list.add(123); // 第一次调用add()时,底层才创建了长度10的数组,并将数据123添加elementData[0]
// 后续的添加和扩容操作与jdk 7 无异
4 ArrayList 的构造方法与扩容机制*
参考链接
- 三种构造方法:无参、传入指定长度、传入
Collection
// 属性
private static final int DEFAULT_CAPACITY = 10; // 默认的容量大小(常量)
private static final Object[] EMPTY_ELEMENTDATA = {}; // 定义的空数组(final修饰,大小固定为0)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 定义的默认空容量的数组(final修饰,大小固定为0)
transient Object[] elementData; // 定义的不可被序列化的数组,实际存储元素的数
private int size; // 数组中元素的个数// 无参的构造方法
"""
容量此时是0,元素个数size为默认值0
"""
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 只有调用无参构造器才会获得 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
}// 传容量的构造方法
"""
initialCapacity > 0时,容量为initialCapacity,元素个数size为默认值0
initialCapacity = 0时,容量为0,元素个数size为默认值0
initialCapacity < 0时,抛出异常
"""
public ArrayList(int initialCapacity) {if (initialCapacity > 0) {this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA; // 调用有参构造器传入0,获得EMPTY_ELEMENTDATA(区别于无参构造器)} else {throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);}
}// 传入Collection元素列表的构造方法
"""
如果传入的Collection不包含元素,容量是0,元素个数size为0
如果传入的Collection包含元素,容量为传入序列的长度,元素个数size也为序列长度,此时的ArrayList是满的
"""
public ArrayList(Collection<? extends E> c) {// 将列表转化为对应的数组elementData = c.toArray();if ((size = elementData.length) != 0) {// 原对象数组的数组类型转化为 Object对象数组的数组类型if (elementData.getClass() != Object[].class)elementData = Arrays.copyOf(elementData, size, Object[].class);} else {// 赋予空数组this.elementData = EMPTY_ELEMENTDATA;}
}
- 扩容:
add & grow
// 单参数的add方法,用户使用的方法
public boolean add(E e) {modCount++;add(e, elementData, size);return true;
}// 重载的多参数add方法
private void add(E e, Object[] elementData, int s) {// 判断元素个数是否等于当前容量,如果相等则调用无参的grow方法if (s == elementData.length)elementData = grow();elementData[s] = e;size = s + 1;
}// 无参的grow方法
private Object[] grow() {return grow(size + 1); // 传递参数为需求的最小容量,也就是当前容量+1
}// 重载的有参grow方法
"""
if语句中不会处理 用默认 __无参构造方法__ 创建的数组的 __初始扩容__ 情况,其余扩容情况都是由if语句处理ArraysSupport.newLength函数的作用是创建一个大小为oldCapacity + max(minimum growth, preferred growth)的数组
minCapacity是传入的参数,上面显示它的值是当前容量+1,那么minCapacity - oldCapacity的值就恒为1,minimum growth的值也就恒为1
oldCapacity >> 1 右移一位,也就是减半,preferred growth的值即为oldCapacity大小的一半(当oldCapacity为0时,右移后还是0)
"""
private Object[] grow(int minCapacity) {// 当前容量int oldCapacity = elementData.length;// 如果当前容量大于0 或者 数组不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(创建时调用的是无参构造器)if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {int newCapacity = ArraysSupport.newLength(oldCapacity,minCapacity - oldCapacity, /* 最小增长量 */oldCapacity >> 1 /* 倾向增长量 */);return elementData = Arrays.copyOf(elementData, newCapacity);// 如果 数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(容量等于0的话,只剩这一种情况了)} else {return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];}
}
- 容量增加 1 的情况:
- 原来的容量为 0,而且是有参构造器创建的
ArrayList(传入 0 或者是空 Collection ,不能是无参构造器创建)
- 原来的容量为 0,而且是有参构造器创建的
- 容量变为原来1.5倍的情况:
- 原来的容量大于 0
- 容量变为
max(DEFAULT_CAPACITY, minCapacity)的情况:- 原来的容量为 0,而且是无参构造器创建的
ArrayList(其中DEFAULT_CAPACITY = 10) - 用默认无参构造方法创建的数组在添加元素前,
ArrayList的容量为0,添加一个元素后,ArrayList的容量就变为 10
- 原来的容量为 0,而且是无参构造器创建的
四 Collection 的子接口 Set
1 HashSet、LinkedHashSet 和 TreeSet
HashSet- 底层数据结构是
HashMap
- 底层数据结构是
LinkedHashSet- 底层数据结构是
LinkedHashMap,元素的插入和取出顺序满足 FIFO 或 LRU - 是
HashSet的子类,在添加数据的同时,每个Entry还维护了两个引用,记录此数据前一个数据和后一个数据 - 对于频繁的遍历操作,
LinkedHashSet效率高于HashSet
- 底层数据结构是
TreeSet- 底层数据结构是
TreeMap,元素是有序的,排序的方式有自然排序和定制排序 - 比较两个对象是否相同的标准为:
compare()返回 0,而不再是equals() - 不能放入无法排序的对象
- 底层数据结构是
- 使用场景
- 都不是线程安全的
HashSet用于不需要保证元素插入和取出顺序的场景LinkedHashSet用于保证元素的插入和取出顺序满足 FIFO 或 LUR 的场景TreeSet用于支持对元素自定义排序规则的场景
// TreeSet的自定排序(TreeMap同理)public void test2(){Comparator com = new Comparator() {@Overridepublic int compare(Object o1, Object o2) {// 定制比较方法...} else {// 类型不匹配...}}};TreeSet set = new TreeSet(com);}
2 HashSet / HashMap 加入实例时,查重的方式**
- 当对象加入
HashSet时,HashSet会先计算对象的 hashcode值来判断对象加入的位置- 如果该位置无对象,则 HashSet 中无重复元素,加入成功
- 如果该位置有对象(下标相同的两个对象, hashcode不一定相同),与相同位置的其他的对象的 hashcode 值作比较
- 如果没有相符的 hashcode,则无重复元素,加入成功
- 有相同 hashcode 值的对象,这时会调用
equals()方法来检查 hashcode 相等的对象是否真的相同,如果两者相同,HashSet就不会让加入操作成功
public void test3(){HashSet set = new HashSet();Person p1 = new Person(1001,"AA");Person p2 = new Person(1002,"BB");set.add(p1);set.add(p2);System.out.println(set);p1.name = "CC";set.remove(p1);System.out.println(set); // BB, CCset.add(new Person(1001,"CC"));System.out.println(set); // BB, CC, CCset.add(new Person(1001,"AA"));System.out.println(set); // BB, CC, CC, AA}
五 Collection 的子接口 Queue
1 Queue 与 Deque
-
Queue是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 FIFO 规则 -
Queue的两套方法

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

2 PriorityQueue【Java堆实现】
PriorityQueue利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据PriorityQueue通过堆元素的上浮和下沉,实现了在O(logn)的时间复杂度内插入元素和删除堆顶元素。PriorityQueue是非线程安全的,且不支持存储无法比较的对象PriorityQueue同样要求元素实现Comparable接口,或传入Comparator对象- 遍历输出时,只有首个元素是有序的
3 ArrayDeque 与 LinkedList【Java栈和队列实现】
ArrayDeque和LinkedList都实现了Deque接口,两者都具有双端队列的功能,可以实现栈和队列ArrayDeque是基于可变长的数组和双指针来实现,而LinkedList则通过链表来实现ArrayDeque插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)- 虽然
LinkedList不需要扩容,但是每次插入数据时需要申请空间,均摊性能相比更慢 - 从性能的角度上,选用
ArrayDeque来实现队列要比LinkedList更好
- 虽然
六 Collection 的子接口 Map
1 HashMap 和 Hashtable
Hashtable- 线程安全,内部的方法基本都经过
synchronized修饰 - 不允许有 null 键和 null 值,否则会抛出
NullPointerException - 创建时如果给定了容量初始值,会直接使用给定的大小
- 基本被淘汰,不要在代码中使用它
- 线程安全,内部的方法基本都经过
HashMap- 非线程安全
- 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个
- 总是使用 2 的整数次幂作为数组长度,创建时如果给定了容量初始值,会将其扩充为 2 的幂次方大小
2 HashMap 的长度为什么是 2 的幂次方
- Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的
- 问题是一个 40 亿长度的数组,内存是放不下的,用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标
- 位运算比求模更高效,
hash%length == hash&(length-1)的前提是length是 2 的整数次幂
3 ConcurrentHashMap 和 Hashtable
- 均线程安全
- 数据结构
| 类型 | 数据结构 | 使用的锁 |
|---|---|---|
ConcurrentHashMap JDK1.7 | Segment 数组 + HashEntry 数组 + 链表 | Segment(本质是 ReentrantLock),每次锁若干 HashEntry |
ConcurrentHashMap JDK1.8 | Node 数组 + 链表/红黑树 | synchronized,每次锁一个 Node |
Hashtable | 数组+链表 | synchronized,每次锁全表 |
- 在 JDK1.7 的时候,
ConcurrentHashMap采用分段锁机制,对整个桶数组进行了分割分段(Segment,每个Segment都是一个可重入锁),每一个Segment只锁容器其中一部分数据,多线程访问容器里不同数据段的数据不会存在锁竞争,提高并发访问率
static class Segment<K,V> extends ReentrantLock implements Serializable {...}

- JDK1.8 的时候已经摒弃了
Segment的概念,synchronized只锁定当前链表或红黑二叉树的首节点,并发控制使用synchronized和 CAS 来操作- 虽然在 JDK1.8 中还能看到
Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本
- 虽然在 JDK1.8 中还能看到

Hashtable(同一把锁) :使用synchronized来保证线程安全,效率非常低下- 当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低

4 为什么JDK1.8的 ConcurrentHashMap 使用 CAS+Synchronized 代替 Segment*
-
Segment数组本质上是ReentrantLock的数组,其中的每一个ReentrantLock锁的是HashEntry数组的若干个位置 -
Synchronized在优化后有偏向锁、轻量级锁和重量级锁三个等级 -
如果把每个
ReentrantLock锁的范围细化为一个位置,是否能与synchronized锁一个位置的效果相同?答案是否定的:- 锁被细化到一个哈希桶,出现并发争抢的可能性就很低了。对于一个哈希桶,在没有多线程竞争时,使用
Synchronized的偏向锁机制的效率是最高的 - 出现争抢时,
Synchronized轻量级锁具有自旋机制,避免线程状态切换引起的开销;而ReentrantLock倾向于将获取不到锁的线程挂起
- 锁被细化到一个哈希桶,出现并发争抢的可能性就很低了。对于一个哈希桶,在没有多线程竞争时,使用
5 CAS (Compare And Swap)
V:要更新的变量(var)
E:预期值(expected)
N:新值(new)
- 比较并交换的过程:判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做
- 当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作
6 JDK8 相较于 JDK7 在底层实现方面的不同
new HashMap()时,底层没有创建数组,首次调用put()方法时,底层创建长度为16的数组- JDK7 底层结构是 HashEntry 数组+链表;JDK8 中底层结构:Node 数组 + 链表 / 红黑树(数组元素类型的改变是因为支持了链表转红黑树)
- 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前键值对个数 > 64时,此时此索引位置上的所数据改为使用红黑树存储
7 HashMap 底层实现的参数
DEFAULT_INITIAL_CAPACITY: HashMap的默认容量,16DEFAULT_LOAD_FACTOR:HashMap的默认装载因子:0.75,是对空间和时间效率的一个平衡选择threshold:扩容的临界值,数值上等于 容量*填充因子:16 * 0.75 => 12TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树,默认8MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量,默认64
9 JDK 8 HashMap 的扩容
HashMap的数组长度一定是 2 的幂次,目的是方便计算哈希值对数组长度取模HashMap只有在插入元素时才会初始化(创建长为16的Node数组),或者扩容- 具体地,扩容还要满足两个条件之一
- 当前存入数据大于阈值
- 存入数据到某一条链表时,该链表数据个数大于 8,且键值对个数小于 64
- 具体地,扩容还要满足两个条件之一
- 扩容的具体操作:
- 将 Node 数组长度变为原来的 2 倍
- 调整原数据在新数组中的索引,调用
resize方法,根据原来的 hash 值新增的 bit 是0还是1,0=索引不变,1=原来的索引 + 原来哈希表的长度
七 Iterator
1 类与接口
Iterable接口有方法iterator()返回Iterator对象,使用该对象的方法进行遍历- 对象实现了
Iterable,就可以使用for each语法 - 不实现
Iterable的类也可以创建Iterator对象
public interface Iterable<T> {/*** 获取迭代器**/Iterator<T> iterator();
}public interface Iterator() {/*** 是否有下个元素**/boolean hasNext();/*** 迭代器游标下移,并返回指向的元素**/E next();/*** 删除最后返回的元素**/void remove();
}
2 只读使用方法
Iterator iterator = coll.iterator();while(iterator.hasNext()){// next():①指针下移 ②将下移以后集合位置上的元素返回System.out.println(iterator.next());}
3 迭代的问题:在循环中增加或删除元素
- 只能使用
Iterator,因为迭代器内部会维护一些索引位置相关的数据,迭代过程中容器不能发生结构性变化,否则这些数据会失效 - 如果还未调用
next()或在上一次调用next()方法之后已经调用了remove()方法,再调用iterator.remove()都会抛出IllegalStateException - iterator 调用的
remove(),和集合对象的remove()不同
// 删除集合中"Tom"Iterator iterator = coll.iterator();while (iterator.hasNext()){Object obj = iterator.next();if("Tom".equals(obj)){iterator.remove();}}
八 Collections 工具类
- 核心思想是面向接口编程:
Collections提供了很多针对容器接口的通用算法和功能,只要实现了指定接口,就能调用其中的功能 - 提供的功能大概分为如下两类
1 对传入的容器接口对象进行操作:查找/替换/排序/添加/修改
// 二分查找
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);// 查找元素出现的次数
public static int frequency(Collection<?> c, Object o);// 最大值/最小值
...
2 返回一个容器接口对象
- 两种情况
- 适配器:将其它类型的数据转换成容器接口对象(输入其它类型,输出容器)
// 1.空容器方法:返回静态不可变的空容器接口对象,只读,用于节省创建新对象的开销
public static final <T> List<T> emptyList();
public static final <T> Set<T> emptySet();
public static final <K, V> Map<K, V> emptyMap();
public static <T> Iterator<T> emptyIterator();// 2.单一对象方法:将一个单独的对象转换为不可变的容器接口对象,同样只读
public static <T> Set<T> singleton(T o);
public static <T> List<T> singletonList(T o);
public static <K, V> Map<K, V> singletonMap(K key, V value);
- 装饰器:修饰给定的容器接口对象,对其增强(输入容器,输出容器)
// 1.写安全:将修改容器的方法重写为抛出异常,返回只读容器
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> set));
public static <K, V> Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m);// 2.类型安全:在泛型失效的情况下(和老版本JDK代码交互时),确保容器元素类型正确
public static <E> List<E> checkedList(List<E> list, Class<E> type);
...// 3.线程安全:向方法加锁,使容器变成线程安全的,不推荐使用
public static <T> List<T> synchronizedList(List<T> list));
...