【Java】集合

集合框架的概述

集合、数组都是对多个数据进行存储操作的结构,简称Java容器。

Java 数组

数组在存储多个数据方面的特点:

  • 一旦初始化以后,其长度就确定了。
  • 数组一旦定义好,其元素的类型也就确定了。我们也就只能操作指定类型的数据了。比如:String[] arr;int[] arr1;Object[] arr2;

数组在存储多个数据方面的缺点:

  • 一旦初始化以后,其长度就不可修改。
  • 数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。
  • 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用

数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。

Java 集合

Java集合类可以用于存储数量不等的多个对象 ,还可用于保存具有映射关系的关联数组。其可分为 CollectionMap两种体系。

  • Collection接口:单列集合,用来存储一个一个的对象
    • List接口:存储有序的、可重复的数据。包含ArrayListLinkedListVector
    • Set接口:存储无序的、不可重复的数据。包含HashSetLinkedHashSetTreeSet
  • Map接口:双列集合,用来存储一对(key-value)一对的数据。包含HashMapLinkedHashMapTreeMapHashtableProperties

Collection接口继承树

image-20210617161218475

Map接口继承树

image-20210617161305507

Collection 接口

Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals()方法。

Collection接口常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class CollectionTest {

@Test
public void test1(){
Collection coll = new ArrayList();

//add(Object e):将元素e添加到集合coll中
coll.add("AA");
coll.add("BB");
coll.add(123);//自动装箱
coll.add(new Date());

//size():获取添加的元素的个数
System.out.println(coll.size());//4

//addAll(Collection coll1):将coll1集合中的元素添加到当前的集合中
Collection coll1 = new ArrayList();
coll1.add(456);
coll1.add("CC");
coll.addAll(coll1);

System.out.println(coll.size());//6
System.out.println(coll);

//clear():清空集合元素
coll.clear();

//isEmpty():判断当前集合是否为空
System.out.println(coll.isEmpty());

}

}

Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
public class CollectionTest {

@Test
public void test1(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new String("Tom"));
coll.add(false);

//1.contains(Object obj):判断当前集合中是否包含obj
//我们在判断时会调用obj对象所在类的equals()。
boolean contains = coll.contains(123);
System.out.println(contains);
System.out.println(coll.contains(new String("Tom")));

//2.containsAll(Collection coll1):判断形参coll1中的所有元素是否都存在于当前集合中。
Collection coll1 = Arrays.asList(123,4567);
System.out.println(coll.containsAll(coll1));
}

@Test
public void test2(){
//3.remove(Object obj):从当前集合中移除obj元素。
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));

coll.remove(123);
System.out.println(coll);

coll.remove(new Person("Jerry",20));
System.out.println(coll);

//4. removeAll(Collection coll1):差集:从当前集合中移除coll1中所有的元素。
Collection coll1 = Arrays.asList(123,456);
coll.removeAll(coll1);
System.out.println(coll);
}

@Test
public void test3(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);

//5.retainAll(Collection coll1):交集:获取当前集合和coll1集合的交集,并返回给当前集合
// Collection coll1 = Arrays.asList(123,456,789);
// coll.retainAll(coll1);
// System.out.println(coll);

//6.equals(Object obj):要想返回true,需要当前集合和形参集合的元素都相同。
Collection coll1 = new ArrayList();
coll1.add(456);
coll1.add(123);
coll1.add(new Person("Jerry",20));
coll1.add(new String("Tom"));
coll1.add(false);

System.out.println(coll.equals(coll1));
}

@Test
public void test4(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);

//7.hashCode():返回当前对象的哈希值
System.out.println(coll.hashCode());

//8.集合 --->数组:toArray()
Object[] arr = coll.toArray();
for(int i = 0;i < arr.length;i++){
System.out.println(arr[i]);
}

//拓展:数组 --->集合:调用Arrays类的静态方法asList()
List<String> list = Arrays.asList(new String[]{"AA", "BB", "CC"});
System.out.println(list);

List arr1 = Arrays.asList(new int[]{123, 456});
System.out.println(arr1.size());//1

List arr2 = Arrays.asList(new Integer[]{123, 456});
System.out.println(arr2.size());//2

//9.iterator():返回Iterator接口的实例,用于遍历集合元素。放在IteratorTest.java中测试
}
}

集合元素的遍历操作,使用迭代器Iterator接口。获取方法:collection.iterator();

  • 内部的方法:hasNext()next()
  • 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
  • 内部定义了remove(),可以在遍历的时候,删除集合中的元素。此方法不同于集合直接调用remove()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class IteratorTest {

@Test
public void test1(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));

Iterator iterator = coll.iterator();

//hasNext():判断是否还有下一个元素
while(iterator.hasNext()){
//next():①指针下移 ②将下移以后集合位置上的元素返回
System.out.println(iterator.next());
}

}

@Test
public void test2(){

Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);

//错误方式一:
// Iterator iterator = coll.iterator();
// while((iterator.next()) != null){
// System.out.println(iterator.next());
// }

//错误方式二:
//集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
while (coll.iterator().hasNext()){
System.out.println(coll.iterator().next());
}
}

//测试Iterator中的remove()
//如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,
// 再调用remove都会报IllegalStateException。
@Test
public void test3(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);

//删除集合中"Tom"
Iterator iterator = coll.iterator();
while (iterator.hasNext()){
// iterator.remove();
Object obj = iterator.next();
if("Tom".equals(obj)){
iterator.remove();
// iterator.remove();
}

}
//遍历集合
iterator = coll.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}

调用next()方法时,先将iterator的指针下移,再将下移后位置上的元素返回。

增强for循环:

1
2
3
4
5
//for(集合元素的类型 局部变量 : 集合对象)
//内部仍然调用了迭代器。
for(Object obj : coll){
System.out.println(obj);
}

List 接口

List接口:存储有序的、可重复的数据。是一种“动态”数组。能够替换原有的数组。其实现类有三种:

  • ArrayList:作为List接口的主要实现类线程不安全的(可使用Collections工具类返回线程安全的ArrayList),效率高;底层使用Object[] elementData数组存储,随机查找效率高,删除和插入效率低;数组长度动态扩容时每次扩容1.5倍
  • LinkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高(但随机查找的效率低);底层使用双向链表存储。不仅实现了 List 接口,还实现了 Deque 接口,可用作双端队列和栈
  • Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData数组存储;数组长度动态扩容时每次扩容2倍。不常使用

img

List接口中的常用方法

1
2
3
4
5
6
7
8
void add(int index, Object ele) //在index位置插入ele元素
boolean addAll(int index, Collection eles) //从index位置开始将eles中的所有元素添加进来
Object get(int index) //获取指定index位置的元素
int indexOf(Object obj) //返回obj在集合中首次出现的位置
int lastIndexOf(Object obj) //返回obj在当前集合中末次出现的位置
Object remove(int index) //移除指定index位置的元素,并返回此元素
Object set(int index, Object ele) //设置指定index位置的元素为ele
List subList(int fromIndex, int toIndex) //返回从fromIndex到toIndex位置的子集合

总结:

  • 增:add(Object obj)
  • 删:remove(int index) / remove(Object obj)
  • 改:set(int index, Object ele)
  • 查:get(int index)
  • 插:add(int index, Object ele)
  • 长度:size()
  • 遍历:
    • Iterator迭代器方式
    • 增强for循环
    • 普通的循环

注意:remove方法有两个重载,一个是删除指定索引位置,一个是删除指定对象

ArrayList

ArrayList的源码分析

jdk 7情况下:

1
2
3
4
ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData
list.add(123);//elementData[0] = new Integer(123);
...
list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容1.5倍。

默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity);

jdk 8中ArrayList的变化:

1
2
3
ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没有创建长度为10的数组
list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0]
...

后续的添加和扩容操作与jdk 7无异。

小结:jdk 7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk 8中的ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。

LinkedList

LinkedList不仅实现了List接口,还实现了Deque接口,可以用作双端队列LinkedList 内部维护了一个Node类型的firstlast属性,用于快速进行头插和尾插。仅插入到队列尾部和头部的时间复杂度为 O(1)。

LinkedList的源码分析:

1
2
LinkedList list = new LinkedList();  // 内部声明了Node类型的first和last属性,默认值为null
list.add(123); // 将123封装到Node中,创建了Node对象。

其中,Node定义为:体现了LinkedList的双向链表的说法

1
2
3
4
5
6
7
8
9
10
11
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

Vector

Vector的源码分析:jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。在扩容方面,默认扩容为原来的数组长度的2倍。

问:ArrayList、LinkedList、Vector三者的异同?

  • 同:三个类都是实现了List接口,存储数据的特点相同:存储有序的、可重复的数据
  • 异:
    • ArrayListVector区别:ArrayList效率高,线程不安全,Vector效率低,线程安全;数组长度动态扩容倍数不同;
    • ArrayListLinkedList的区别:ArrayList使用数组结构存储,LinkedList使用双向链表结构存储;LinkedList适用于频繁插入删除数据,Array适用于频繁查询某个位置数据;

Set 接口

Set接口:存储无序的、不可重复的数据。Set接口中没有额外定义新的方法,使用的都是Collection中声明过的方法。其实现类有三个:

  • HashSet:作为Set接口的主要实现类线程不安全的(可使用Collections工具类返回线程安全的HashSet;可以存储null值;遍历时无法按照添加时的顺序遍历(HashSet底层是通过HashMap实现的,HashSet中的值就是HashMap中的key)
    • LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历。对于频繁的遍历操作,LinkedHashSet效率高于HashSet
  • TreeSet:可以按照添加对象的指定属性,进行排序。

使用要求:向Set(主要指:HashSetLinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()和equals()方法。并且重写的hashCode()equals()尽可能保持一致性:相等的对象必须具有相等的hashCode 值。重写两个方法的小技巧:对象中用作 equals()方法比较的 Field,都应该用来计算 hashCode 值。


https://cloud.tencent.com/developer/article/1622192

  • OpenJDK默认的hashCode()方法实现和对象内存地址无关,在版本6和7中,它是随机生成的数字,在版本8中,它是基于线程状态的数字。(AZUL-ZING的hashcode是基于地址的)
  • 在Hotspot中,hash值会存在对象头中。
  • hashCode()方法和System.identityHashCode()会让对象不能使用偏向锁,所以如果想使用偏向锁,那就最好重写hashCode方法。

HashSet

HashSet底层:数组+链表的结构(HashSet本质上是 HashMap)。

  • 无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。
  • 不可重复性:保证添加的元素按照equals()判断时,不能返回true。即:相同的元素只能添加一个。

添加元素的过程:

  • HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置);
  • 判断数组此位置上是否已经有元素:如果此位置上没有其他元素,则元素a添加成功。(情况1)
  • 如果此位置上有其他元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
    • 如果hash值不相同,则元素a添加成功。(情况2)
    • 如果hash值相同,进而需要调用元素a所在类的equals()方法:equals()返回true,元素a添加失败;equals()返回false,则元素a添加成功。(情况3)

对于添加成功的情况2和情况3而言:元素a与已经存在指定索引位置上数据以链表的方式存储。jdk 7:元素a放到数组中,指向原来的元素。jdk 8:原来的元素在数组中,指向元素a

image-20210617201737187

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test1(){
Set set = new HashSet();
set.add(456);
set.add(123);
set.add(123);
set.add("AA");
set.add("CC");
set.add(new User("Tom",12));
set.add(new User("Tom",12));
set.add(129);

Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}

HashSet本质上是 HashMap

HashSetadd() 方法调用的就是 HashMapput() 方法,add() 方法传入的值就是 HashMap里的 keyvalue为一个共用的 Object 常量 PRESENT(不放 null是因为 remove() 时要判断是否为空):

image-20210918132643499

image-20210918132711870

LinkedHashSet

LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据(类似于双向链表结构)。优点:对于频繁的遍历操作,LinkedHashSet效率高于HashSet
image-20210617202602059

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test2(){
Set set = new LinkedHashSet();
set.add(456);
set.add(123);
set.add(123);
set.add("AA");
set.add("CC");
set.add(new User("Tom",12));
set.add(new User("Tom",12));
set.add(129);

Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}

TreeSet

TreeSetSortedSet接口的实现类,TreeSet可以确保集合元素处于排序状态。TreeSet底层使用红黑树结构存储数据。特点:有序,查询速度比List快。

TreeSet中添加的数据,要求是相同类的对象。两种排序方式:自然排序(实现Comparable接口)和定制排序(Comparator)。自然排序中,比较两个对象是否相同的标准为:compareTo()返回0,不再使用equals()。定制排序中,比较两个对象是否相同的标准为:compare()返回0,不再使用equals()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Test
public void test2(){
Comparator com = new Comparator() {
//按照年龄从小到大排列
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof User && o2 instanceof User){
User u1 = (User)o1;
User u2 = (User)o2;
return Integer.compare(u1.getAge(),u2.getAge());
}else{
throw new RuntimeException("输入的数据类型不匹配");
}
}
};

// 构造函数传入Comparator类的对象后将不再使用原先的compareTo方法判断
TreeSet set = new TreeSet(com);
set.add(new User("Tom",12));
set.add(new User("Jerry",32));
set.add(new User("Jim",2));
set.add(new User("Mike",65));
set.add(new User("Mary",33));
set.add(new User("Jack",33));
set.add(new User("Jack",56));

Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}

List和Set插入数据时判断是否重复的方法:

  • List:遍历每个元素,调用其equals()方法判断是否相等。效率较低。
  • Set:计算插入数据的哈希值,判断是否已经存在该哈希值,不存在说明没有重复;若存在则调用equals()方法判断是否相等。效率较高。

Set 的使用

List实现类对象和Set实现类对象可以相互转换,用于过滤List中重复的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//练习:在List内去除重复数字值,要求尽量简单
//方法:将List对象赋给Set对象,其会过滤掉重复的元素,再将其转回List
public static List duplicateList(List list) {
HashSet set = new HashSet();
set.addAll(list);
return new ArrayList(set);
}

@Test
public void test2(){
List list = new ArrayList();
list.add(new Integer(1));
list.add(new Integer(2));
list.add(new Integer(2));
list.add(new Integer(4));
list.add(new Integer(4));
List list2 = duplicateList(list);
for (Object integer : list2) {
System.out.println(integer);
}
}

考察Set接口的底层原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
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);
set.add(new Person(1001,"CC"));
System.out.println(set);
set.add(new Person(1001,"AA"));
System.out.println(set);
}

Map 接口

详解:https://imlql.cn/post/cbc5672a.html

Map:双列数据,存储key-value对的数据

  • HashMap:作为Map的主要实现类;线程不安全的(可使用Collections工具类返回线程安全的HashMap),效率高;可以存储null的key和value;不可以按照添加的顺序实现遍历(HashSet底层是通过HashMap实现的,HashSet中的值就是HashMap中的key)
    • LinkedHashMap:继承自HashMap,保证在遍历map元素时,可以按照添加的顺序实现遍历。原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于HashMap
  • TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序。底层使用红黑树
  • Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value;其他实现细节和HashMap一致
    • Properties:常用来处理配置文件。key和value都是String类型

HashMap的底层:数组+链表 (jdk7及之前);数组+链表+红黑树 (jdk 8)

问:

  1. HashMap的底层实现原理?
  2. HashMapHashtable的异同?
    • HashMap是线程不安全的,效率高,可以存储null的key和value
    • Hashtable是线程安全的,效率低,不可以存储null的key和value
  3. CurrentHashMapHashtable的异同?

Map 结构的理解:

  • Map中的key:无序的、不可重复的,使用Set存储所有的key。key所在的类要重写equals()hashCode()方法(以HashMap为例)
  • Map中的value:无序的、可重复的,使用Collection存储所有的value。value所在的类要重写equals()方法。

一个键值对:key-value构成了一个Node对象。Map中的Node无序的、不可重复的,使用Set存储所有的Node

HashMap 的底层实现原理

https://www.bilibili.com/video/BV1Kb411W75N?p=552

HashMap底层是: Node类型的数组 + Node类型的单向链表 + 红黑树(jdk1.8)

以jdk7为例说明:

1
HashMap map = new HashMap();

在实例化以后,底层创建了长度是16的一维数组Entry[] table。经过执行过多次put()方法…

1
map.put(key1,value1);

首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后(一些位运算操作),得到在Entry数组中的存放位置。如果此位置上的数据为空,此时的key1-value1添加成功。 ---- 情况1

如果此位置上的数据不为空(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:

  • 如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。---- 情况2
  • 如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较:
    • 如果equals()返回false:此时key1-value1添加成功。----情况3
    • 如果equals()返回true:使用value1替换value2。

补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。

在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来

image-20210625091437538

jdk 8 相较于 jdk 7 在底层实现方面的不同:

  1. new HashMap(); 底层没有立刻创建一个长度为16的数组
  2. jdk 8底层的数组是:Node[],而非Entry[]
  3. 首次调用put()方法时,底层创建长度为16的数组
  4. jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树
    • 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
    • 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储(若数组长度 < 64,则数组扩容,不改用红黑树)。

image-20210625091347684

HashMap 内重要常量(背诵):

  • DEFAULT_INITIAL_CAPACITY: HashMap的默认容量,16
  • DEFAULT_LOAD_FACTORHashMap的默认加载因子:0.75
  • threshold:扩容的临界值,等于容量*填充因子:16 * 0.75 => 12,大于该值时扩容
  • TREEIFY_THRESHOLDBucket中链表长度大于该默认值,转化为红黑树:8
  • MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64

问:负载因子值的大小,对HashMap有什么影响?

  • 负载因子的大小决定了HashMap的数据密度。
  • 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
  • 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
  • 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。

LinkedHashMap 的底层实现原理

继承自HashMap。修改了Entry的内容,增加了两个Entry类型对象代表链表结构中当前对象指向的前后对象。

1
2
3
4
5
6
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;//能够记录添加的元素的先后顺序
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}

HashMap 常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
public class MapTest {

/*
元视图操作的方法:
Set keySet():返回所有key构成的Set集合
Collection values():返回所有value构成的Collection集合
Set entrySet():返回所有key-value对构成的Set集合
*/

@Test
public void test5(){
Map map = new HashMap();
map.put("AA",123);
map.put(45,1234);
map.put("BB",56);

//遍历所有的key集:keySet()
Set set = map.keySet();
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
System.out.println();

//遍历所有的value集:values()
Collection values = map.values();
for(Object obj : values){
System.out.println(obj);
}
System.out.println();

//遍历所有的key-value
//方式一:entrySet()
Set entrySet = map.entrySet();
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()){
Object obj = iterator1.next();
//entrySet集合中的元素都是entry
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + "---->" + entry.getValue());

}
System.out.println();

//方式二:
Set keySet = map.keySet();
Iterator iterator2 = keySet.iterator();
while(iterator2.hasNext()){
Object key = iterator2.next();
Object value = map.get(key);
System.out.println(key + "=====" + value);
}
}


/*
元素查询的操作:
Object get(Object key):获取指定key对应的value
boolean containsKey(Object key):是否包含指定的key
boolean containsValue(Object value):是否包含指定的value
int size():返回map中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(Object obj):判断当前map和参数对象obj是否相等
*/
@Test
public void test4(){
Map map = new HashMap();
map.put("AA",123);
map.put(45,123);
map.put("BB",56);
// Object get(Object key)
System.out.println(map.get(45));
//containsKey(Object key)
boolean isExist = map.containsKey("BB");
System.out.println(isExist);

isExist = map.containsValue(123);
System.out.println(isExist);

map.clear();

System.out.println(map.isEmpty());
}

/*
添加、删除、修改操作:
Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
void putAll(Map m):将m中的所有key-value对存放到当前map中
Object remove(Object key):移除指定key的key-value对,并返回value
void clear():清空当前map中的所有数据
*/
@Test
public void test3(){
Map map = new HashMap();
//添加
map.put("AA",123);
map.put(45,123);
map.put("BB",56);
//修改
map.put("AA",87);

System.out.println(map);

Map map1 = new HashMap();
map1.put("CC",123);
map1.put("DD",123);

map.putAll(map1);

System.out.println(map);

//remove(Object key)
Object value = map.remove("CC");
System.out.println(value);
System.out.println(map);

//clear()
map.clear();//与map = null操作不同
System.out.println(map.size());
System.out.println(map);
}

@Test
public void test2(){
Map map = new HashMap();
map = new LinkedHashMap();
map.put(123,"AA");
map.put(345,"BB");
map.put(12,"CC");

System.out.println(map);
}


@Test
public void test1(){
Map map = new HashMap();
// map = new Hashtable();
map.put(null,123);

}
}

TreeMap

向TreeMap中添加key-value,要求key必须是由同一个类创建的对象。因为要按照key进行排序:自然排序 、定制排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class TreeMapTest {
//自然排序
@Test
public void test1(){
TreeMap map = new TreeMap();
User u1 = new User("Tom",23);
User u2 = new User("Jerry",32);
User u3 = new User("Jack",20);
User u4 = new User("Rose",18);

map.put(u1,98);
map.put(u2,89);
map.put(u3,76);
map.put(u4,100);

Set entrySet = map.entrySet();
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()){
Object obj = iterator1.next();
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + "---->" + entry.getValue());
}
}

//定制排序
@Test
public void test2(){
TreeMap map = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof User && o2 instanceof User){
User u1 = (User)o1;
User u2 = (User)o2;
return Integer.compare(u1.getAge(),u2.getAge());
}
throw new RuntimeException("输入的类型不匹配!");
}
});
User u1 = new User("Tom",23);
User u2 = new User("Jerry",32);
User u3 = new User("Jack",20);
User u4 = new User("Rose",18);

map.put(u1,98);
map.put(u2,89);
map.put(u3,76);
map.put(u4,100);

Set entrySet = map.entrySet();
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()){
Object obj = iterator1.next();
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + "---->" + entry.getValue());
}
}
}

Collections 工具类

Collections 是一个操作 SetListMap 等集合的工具类(操作数组的工具类:Arrays

Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作, 还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。

其提供的线程安全功能是通过 synchronized 方式加锁,其效率不高,也不常用。通常使用 JUC 包下的 CopyOnWriteArrayList 类实现集合类的线程安全功能。

排序操作:(均为static方法)

  • reverse(List):反转List中元素的顺序
  • shuffle(List):对List集合元素进行随机排序
  • sort(List):根据元素的自然顺序对指定List集合元素按升序排序
  • sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
  • swap(List,int, int):将指定List集合中的 i 处元素和 j 处元素进行交换

查找、替换

  • Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
  • Object max(Collection,Comparator):根据 Comparator指定的顺序,返回给定集合中的最大元素
  • Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素
  • Object min(Collection,Comparator):根据 Comparator指定的顺序,返回给定集合中的最小元素
  • int frequency(Collection,Object):返回指定集合中指定元素的出现次数
  • void copy(List dest,List src):将src中的内容复制到dest
  • boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List对象的所有旧值

同步控制Collections类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。使用 synchronizedXxx() 方法,将返回一个新的集合类对象,其各个方法都添加了同步代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int hashCode() {
synchronized (mutex) {return list.hashCode();}
}

public E get(int index) {
synchronized (mutex) {return list.get(index);}
}

public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class CollectionsTest {
@Test
public void test2(){
List list = new ArrayList();
list.add(123);
list.add(43);
list.add(765);
list.add(-97);
list.add(0);

//报异常:IndexOutOfBoundsException("Source does not fit in dest")
// List dest = new ArrayList();
// Collections.copy(dest,list);
//正确的:
List dest = Arrays.asList(new Object[list.size()]);
System.out.println(dest.size());//list.size();
Collections.copy(dest,list);

System.out.println(dest);

/*
Collections 类中提供了多个 synchronizedXxx() 方法,
该方法可使将指定集合包装成线程同步的集合,从而可以解决
多线程并发访问集合时的线程安全问题
*/
//返回的list1即为线程安全的List
List list1 = Collections.synchronizedList(list);
}

@Test
public void test1(){
List list = new ArrayList();
list.add(123);
list.add(43);
list.add(765);
list.add(765);
list.add(765);
list.add(-97);
list.add(0);
System.out.println(list);

// Collections.reverse(list);
// Collections.shuffle(list);
// Collections.sort(list);
// Collections.swap(list,1,2);
int frequency = Collections.frequency(list, 123);

System.out.println(list);
System.out.println(frequency);
}
}

问:CollectionCollections 的区别?

  • Collection:集合中的一种接口,其实现类有ArrayListLinkedArrayList等。
  • Collections:集合工具类,用于对集合进行排序、查找等操作。

集合类的线程安全分析

ArrayList 线程不安全

ArrayList 是线程不安全的,并发条件下对 ArrayListadd()remove() 操作会抛出异常: java.util.ConcurrentModificationException

ArrayList 在迭代的时候如果同时对其进行修改就会抛出异常 ConcurrentModificationException。原因在于调用 list.remove() 方法导致 modCountexpectedModCount 的值不一致。注意,像使用for-each进行迭代实际上也会出现这种问题。

解决方案一:使用 Vector 集合(较为古老,不常用)

VectorList 接口的古老实现类,其特点是线程安全的,效率低,不太常用(底层是 synchronized 加锁方式的效率都比较低,因为不论是读还是写都会阻塞,而读操作没必要加锁)

解决方案二:使用 Collections 工具类(不常用)

Collections 是一个操作 SetListMap 等集合的工具类(操作数组的工具类:Arrays)。Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。

其提供的线程安全功能是通过 synchronized 方式加锁,其效率不高,也不常用。

1
List<String> list = Collections.synchronizedList(new ArrayList<>());

解决方案三:使用 JUC 包下的 CopyOnWriteArrayList 类(常用)

它相当于线程安全的 ArrayList。和 ArrayList 一样,它是个可变数组;但是和 ArrayList 不同的是,它具有以下特性:

  1. 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
  2. 它是线程安全的
  3. 因为通常需要复制整个基础数组,所以可变操作(add()set()remove() 等等)的开销很大。
  4. 迭代器支持 hasNext()next() 等不可变操作,但不支持可变remove() 等操作。
  5. 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。

CopyOnWriteArrayList 不使用独占锁,而是使用写时复制技术,将读写分离。读操作不加锁,写操作才加锁。

写时复制思路:当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样读和写的是不同的容器,因此实现了读写分离,写时不耽误并发读。

add() 方法源码:

image-20210916205340151

拷贝时使用的是 Arrays 工具类的 copyOf() 方法。

总结Vector 读写时都上锁,因此性能较差,不能同时读和写。但 CopyOnWriteArrayList 可以在读的时候进行写,效率较高。

HashSet 线程不安全

解决方案:使用 CopyOnWriteArraySet

image-20210923215559576

HashMap 线程不安全

解决方案:使用 ConcurrentHashMap