avatar


4.集合

这一章我们讨论集合。
这个名词不仅仅出现在计算机中,在数学中也有。但数学中的集合和计算机中的集合,除了都是"一堆东西在一起"外,没有任何的关系。
(类似的还有计算机中的二叉树和金融中的二叉树,除了都有两个叉之外,没有任何关系。)

集合

《3.最常用的Java自带的类》,我们讨论了ArrayList,当时我们说:ArrayList是集合的一种,集合是指一种存储空间可变的存储模型,存储的数据容量可以发生改变。

但当时我们只讨论了这些,现在我做更详细的讨论。

首先,是集合的体系结构。
集合可以分为两类,分别是单列和双列、单列又有List和Set两种。
集合类的体系图

  • 注意,Collection、Map、List和Set都是接口。

我们一个一个讨论。

Collection

概述

Collection是单列集合的顶层接口,JDK不提供此接口的任何直接实现,但为该接口的子接口(Set和List)提供了具体实现。

我们举一个例子,来说明Collection的基本使用。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.ArrayList;
import java.util.Collection;

public class M {
public static void main(String[] args) {
//创建Collection集合的对象
Collection<String> c = new ArrayList<String>();

//添加元素:boolean add(E e)
c.add("hello");
c.add("world");
c.add("java");

//输出集合对象
System.out.println(c);
}
}

运行结果:

1
[hello, world, java]
  • 这是一种典型的向上转型的多态,从子到父,父类引用指向子类对象。

常用方法

Collection的常用方法有:

方法名 说明
boolean add(E e) 添加元素
boolean remove(Object o) 从集合中移除指定的元素
void clear() 清空集合中的元素
boolean contains(Object o) 判断集合中是否存在指定的元素
boolean isEmpty() 判断集合是否为空
int size() 集合的长度,也就是集合中元素的个数
  • 注意,没有set方法和get方法。没有不是因为不常用,是因为本身就没有。

迭代器遍历

在上一章《3.最常用的Java自带的类》讨论ArrayList的时候,我们说ArrayList有size方法有set和get方法,那么就可以做遍历了。
但是在这里没有set方法和get方法,可以做遍历呢?
也可以。
迭代器

迭代器,集合的专有的遍历方式。
方法如下:

1
Iterator<E> iterator = 集合对象.iterator()

我们来看个例子。
示例代码:

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
package com.kakawanyifan;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class IteratorDemo {
public static void main(String[] args) {
//创建Collection集合的对象
Collection<String> c = new ArrayList<String>();

//添加元素:boolean add(E e)
c.add("hello");
c.add("world");
c.add("java");

//Iterator<E> iterator():返回此集合中元素的迭代器,通过集合的iterator()方法得到
Iterator<String> it = c.iterator();

//用while循环改进元素的判断和获取
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
}
}
}

运行结果:

1
2
3
hello
world
java

迭代器源码

it.next()运行一次就输出下次的结果,就像有指针一直记录着已经next多少记录了一样。
实际上,还真是有指针在记。

我们来看源码。

示例代码:

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
private class Itr implements Iterator<E> {
int cursor; // index of next element to return

【部分代码略】

Itr() {}

public boolean hasNext() {
return cursor != size;
}

@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}

【部分代码略】

}

每调用一次next方法,cursor变量都会加一。

题外话
类似效果的函数,还有Python中读取文件的readline

示例代码:

1
2
3
4
5
6
7
8
with open(fileName, 'r') as fr:
while True:
line = fr.readline()
# 在文件中,如果遇到一个空白行,readline()并不会返回一个空串
# 因为每一行的末尾还有一个或多个分隔符,所以"空白行"至少会有一个换行符或者系统使用的其他符号。
# 只有当真的读到文件末尾时,才会读到空串""。
if not line:
break

List

Collection接口有两个子接口,List和Set。我们先讨论List。

概述

List是有序集合,用户可以精确控制列表中每个元素的插入位置,可以通过整数索引访问元素。
(与Set集合不同,List中允许重复的元素。)

特有方法

List继承自Collection,除了Collection有的方法,List中还有一些特有的方法。

方法名 描述
void add(int index,E element) 在此集合中的指定位置插入指定的元素。
E remove(int index) 删除指定索引处的元素,返回被删除的元素。
E set(int index,E element) 修改指定索引处的元素,返回被修改的元素。
E get(int 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
package com.kakawanyifan;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class IteratorDemo {
public static void main(String[] args) {
// 创建集合对象
List<String> list = new ArrayList<String>();

// 添加元素
list.add("hello");
list.add("world");
list.add("java");

// 遍历集合,得到每一个元素,看有没有"world"这个元素,如果有,就添加一个"javaee"元素。
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
if (s.equals("world")) {
list.add("javaee");
}
}

}
}

运行结果:

1
2
3
4
5
6
hello
world
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at com.kakawanyifan.IteratorDemo.main(IteratorDemo.java:20)

出现异常了。

ArrayList源码分析

我们根据at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911),点击ArrayList.java:911,找到这么一段代码。
示例代码:

1
2
3
4
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

如果modCountexpectedModCount不相等,就抛出异常。

checkForComodification方法属于ArrayList的一个内部类Itr,在这个Itr中,就定义了int expectedModCount = modCount;

示例代码:

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
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

Itr() {}

public boolean hasNext() {
return cursor != size;
}

@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}

public void remove() {

【部分代码略】

}

@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {

【部分代码略】

}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

有很多方法都会调用checkForComodification这个方法,但是在这里调用checkForComodification这个方法的就是next方法。next的第一行就是checkForComodification();,这也印证了报错的第二行at java.util.ArrayList$Itr.next(ArrayList.java:861)
expectedModCount属于Itr这个内部类的成员变量,如果代码规范的话,不应该有方法去直接修改内部类的expectedModCount,所以比较有可能是modCount被修改了。或许是add方法的时候,修改了modCount

右键modCount,Go to,Declaration or Usage,找到modCount的定义,是在AbstractList这个类中被定义的,而且是AbstractList这个类的成员变量。而ArrayList继承了这个类。

来看看add方法。

1
2
3
4
5
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

再来看看被add方法调用的ensureCapacityInternal方法。

1
2
3
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

再来看看ensureExplicitCapacity方法。

1
2
3
4
5
6
7
private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

找到原因了!modCount++;

小结一下,ArrayList的部分源码结构如图所示。
ArrayList

并发修改方案

那么,怎么解决呢?
用for循环遍历,然后用集合对象做对应的操作。

示例代码:

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
package com.kakawanyifan;

import java.util.ArrayList;
import java.util.List;

public class ForDemo {
public static void main(String[] args) {
//创建集合对象
List<String> list = new ArrayList<String>();

//添加元素
list.add("hello");
list.add("world");
list.add("java");

for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
System.out.println(s);
if (s.equals("world")) {
list.add("javaee");
}
}

//输出集合对象
System.out.println(list);
}
}

运行结果:

1
2
3
4
5
hello
world
java
javaee
[hello, world, java, javaee]

列表迭代器

在ArrayList中还有一个内部类:ListIterator。该内部类通过listIterator方法实例化。其特点是可以沿着next和previous两个方向进行遍历,并且提供了add和set方法。

示例代码:

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
package com.kakawanyifan;

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

public class ListIteratorDemo {
public static void main(String[] args) {
//创建集合对象
List<String> list = new ArrayList<String>();

//添加元素
list.add("hello");
list.add("world");
list.add("java");

//获取列表迭代器
ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
String s = lit.next();
System.out.println(s);
if (s.equals("world")) {
lit.add("javaee");
}
}

System.out.println("------");

while (lit.hasPrevious()) {
String s = lit.previous();
System.out.println(s);
}

System.out.println(list);

}
}

运行结果:

1
2
3
4
5
6
7
8
9
hello
world
java
------
java
javaee
world
hello
[hello, world, javaee, java]

特别注意,这时候并发修改并没有引起异常。我们来看看add方法都做了什么。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void add(E e) {
checkForComodification();

try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
  • 注意:expectedModCount = modCount

增强for循环

还有一种for循环,这种for循环相比之前的for循环更方便。
格式如下:

1
2
3
for(元素数据类型 变量名 : 数组/集合对象名) {
循环体;
}

但是,相比之前的for循环,也有不足。我们来看例子。

示例代码:

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
package com.kakawanyifan;

import java.util.ArrayList;
import java.util.List;

public class ForDemo {
public static void main(String[] args) {
//创建集合对象
List<String> list = new ArrayList<String>();

//添加元素
list.add("hello");
list.add("world");
list.add("java");

for (String s : list) {
System.out.println(s);
if (s.equals("world")) {
list.add("javaee");
}
}

System.out.println(list);

}
}

运行结果:

1
2
3
4
5
6
hello
world
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at com.kakawanyifan.ForDemo.main(ForDemo.java:16)

原因是,这种For循环的内部原理是一个Iterator迭代器。所以,同样有并发修改异常。

LinkedList的特有方法

除了ArrayList,还有一种是LinkeList。顾名思义,基于链表实现的。

关于数组和链表,我们在《算法入门经典(Java与Python描述):2.数组、链表》有非常详细的讨论,这里不赘述。

数组:查询快、增删慢
链表:查询慢、增删快

ArrayList:基于数组实现,查询快、增删慢
LinkedList:基于链表实现,查询慢、增删快

LinkedList有一些特有的方法。

方法名 说明
public void addFirst(E e) 在该列表开头插入指定的元素
public void addLast(E e) 将指定的元素追加到此列表的末尾
public E getFirst() 返回此列表中的第一个元素
public E getLast() 返回此列表中的最后一个元素
public E removeFirst() 从此列表中删除并返回第一个元素
public E removeLast() 从此列表中删除并返回最后一个元素

等等!
刚刚说了,LinkedList,基于链表实现,怎么LinkedList还专门提供了针对last的操作?链表,last,那我不得一个一个指过去?这还能快吗?

LinkedList源码分析

因为LinkedList有两个成员变量first和last,在add和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
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{

transient Node<E> first;

transient Node<E> last;

public boolean add(E e) {
linkLast(e);
return true;
}

void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}

public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}

E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;

if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}

if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}

x.item = null;
size--;
modCount++;
return element;
}
  • 该示例代码来自JDK的源码,在入选本博客的时候,有删改。

Set

Set的特点

第二种单列的Collection是Set。
其特点是:

  • 元素存取无序
  • 没有索引、只能通过迭代器或增强for循环遍历
  • 不能存储重复元素

我们来看个例子。
示例代码:

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
package com.kakawanyifan;

import java.util.HashSet;
import java.util.Set;

public class SetDemo {
public static void main(String[] args) {
// 创建集合对象
Set<String> set = new HashSet<String>();

// 添加元素
set.add("hello");
set.add("world");
set.add("java");
// 不包含重复元素的集合
set.add("world");

// 遍历
for(String s : set) {
System.out.println(s);
}

System.out.println(set);
}
}

运行结果:

1
2
3
4
world
java
hello
[world, java, hello]

执行了两次set.add("world");,但只记录一条world

哈希值

那么HashSet是如何做到数据唯一的呢?这就得从哈希值开始说起了。

哈希值是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。
在Object类中有一个方法public int hashCode(),该方法返回对象的哈希值。
哈希值的特点:

  1. 同一个对象多次调用hashCode()方法返回的哈希值是相同的。
  2. 默认情况下,不同对象的哈希值是不同的。
  3. 如果重写hashCode()方法,可以实现让不同对象的哈希值相同。
    比如在下面的例子中,"通话"和"重地"拥有相同的哈希值,是因为String类重写了hashCode方法。

示例代码:

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
package com.kakawanyifan;
/**
* 学生类
*/
public class Student {
private String name;
private int age;

public Student(String name,int age) {
this.name = name;
this.age = age;
}

public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setAge(int age) {
this.age = age;
}

public int getAge() {
return age;
}
}
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
package com.kakawanyifan;

public class HashDemo {
public static void main(String[] args) {
//创建学生对象
Student s1 = new Student("林青霞",30);

//同一个对象多次调用hashCode()方法返回的哈希值是相同的
System.out.println(s1.hashCode());
System.out.println(s1.hashCode());
System.out.println("--------");

Student s2 = new Student("林青霞",30);

//默认情况下,不同对象的哈希值是不相同的
//通过方法重写,可以实现不同对象的哈希值是相同的
System.out.println(s2.hashCode());
System.out.println("--------");

System.out.println("hello".hashCode());
System.out.println("world".hashCode());
System.out.println("java".hashCode());
System.out.println("world".hashCode());
System.out.println("--------");

System.out.println("重地".hashCode());
System.out.println("通话".hashCode());

System.out.println("通话".equals("重地"));
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
460141958
460141958
--------
1163157884
--------
99162322
113318802
3254818
113318802
--------
1179395
1179395
false

注意"重地"和"通话",拥有相同的哈希值。
但是"重地".equals("通话"),返回的是false。我们可以看String类源码的equals方法,并没有利用哈希值,这个我们在上一章《3.最常用的Java自带的类》提到过。

HashSet源码分析

那么,HashSet是怎么保证元素唯一性的呢?
利用哈希值?哈希值一样的就认为是一样的元素?
来,看个反例。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.kakawanyifan;

import java.util.HashSet;
import java.util.Set;

public class SetDemo {
public static void main(String[] args) {
String s1 = "重地";
String s2 = "通话";

System.out.println(s1.hashCode());
System.out.println(s2.hashCode());

Set<String> set = new HashSet<>();
set.add(s1);
set.add(s2);

System.out.println(set);
}
}

运行结果:

1
2
3
1179395
1179395
[重地, 通话]

居然"重地"和"通话"都被加进去了?
难道和String的equals方法一样?就没有利用哈希值?
没有利用哈希值,那叫什么"HashSet"?
利用的哈希值,但是对于"重地"和"通话",做了if-else的特殊处理?
不会吧,这么糟糕的代码?

不要猜了!直接看源码!

我们找到HashSet的add方法的源码。

1
2
3
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

发现数据是通过map.put(e, PRESENT)添加的,再来看看map.put(e, PRESENT)的源码。

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

发现数据是通过putVal(hash(key), key, value, false, true)添加的。此外,还有一个hash(key)方法。我们来看看两个方法的源码

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

到这里,其实可以解答我们的疑惑了。利用了哈希值,但不是在哈希值的基础上还做了操作。

最后,再看一下putVal()方法都做了啥。

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
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
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);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

该部分的代码,其逻辑结构如图。
唯一性

提到了HashSet,就不得不提哈希表,描述了元素的存储方法,有"链表法",有"开放地址法"。
这个我们在《算法入门经典(Java与Python描述):7.哈希表》中有过专题讨论,这里就不追述了。

LinkedHashSet的概述和特点

除了HashSet,还有LinkedHashSet。

LinkedHashSet的特点:

  • 哈希表和链表实现的Set接口。
  • 元素的存储和取出顺序是一致的。
  • 元素唯一,没有重复的元素。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.kakawanyifan;

import java.util.LinkedHashSet;

public class LinkedHashSetDemo {
public static void main(String[] args) {
//创建集合对象
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<String>();

//添加元素
linkedHashSet.add("hello");
linkedHashSet.add("world");
linkedHashSet.add("java");

linkedHashSet.add("world");

//遍历集合
for(String s : linkedHashSet) {
System.out.println(s);
}
}
}

运行结果:

1
2
3
hello
world
java

TreeSet

TreeSet的特点

《算法入门经典(Java与Python描述):7.哈希表》中,我们讨论过TreeMap,特点是所有的key都是排序好的。
与之对应的有TreeSet,特点是:

  • 元素有序,此外可以通过构造方法自定义排序。
    • 构造方法一 TreeSet():根据其元素的自然排序进行排序
    • 构造方法二 TreeSet(Comparator comparator) :根据指定的比较器进行排序
  • 没有带索引的方法,所以不能使用普通for循环遍历
  • 不包含重复元素的集合

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.TreeSet;

public class M {
public static void main(String[] args) {
//创建集合对象
TreeSet<Integer> ts = new TreeSet<Integer>();

//添加元素
ts.add(10);
ts.add(40);
ts.add(30);
ts.add(50);
ts.add(20);

ts.add(30);

//遍历集合
for(Integer i : ts) {
System.out.println(i);
}
}
}

运行结果:

1
2
3
4
5
10
20
30
40
50

接下来,我们利用TreeSet的特点,来做一件事情。
按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序

自然排序

示例代码:

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
package com.kakawanyifan;

public class Student {

private String name;
private int age;

public Student(String name, int age) {
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;
}
}
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
package com.kakawanyifan;

import java.util.TreeSet;

public class TreeSetDemo {
public static void main(String[] args) {

TreeSet<Student> ts = new TreeSet<Student>();

//创建学生对象
Student s1 = new Student("x", 29);
Student s2 = new Student("y", 28);
Student s3 = new Student("z", 30);
Student s4 = new Student("za", 33);

Student s5 = new Student("a", 33);
Student s6 = new Student("a", 33);

//把学生添加到集合
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
ts.add(s6);

//遍历集合
for (Student s : ts) {
System.out.println(s.getName() + "," + s.getAge());
}

}
}

运行结果:

1
2
3
4
5
Exception in thread "main" java.lang.ClassCastException: com.kakawanyifan.Student cannot be cast to java.lang.Comparable
at java.util.TreeMap.compare(TreeMap.java:1294)
at java.util.TreeMap.put(TreeMap.java:538)
at java.util.TreeSet.add(TreeSet.java:255)
at com.kakawanyifan.TreeSetDemo.main(TreeSetDemo.java:20)

居然报错了?
因为没有实现Comparable接口。

那我们就实现一下Comparable的接口。

示例代码:

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
package com.kakawanyifan;

public class Student implements Comparable<Student>{

private String name;
private int age;

public Student(String name, int age) {
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;
}

@Override
public int compareTo(Student o) {
return 0;
}
}

运行结果:

1
x,29

compareTo(Student o)这个方法默认return 0,我们不修改,试一下。居然之输出了一个?
因为0代表元素是相等的,所以不会添加。

如果是1呢?

1
2
3
4
5
6
x,29
y,28
z,30
za,33
a,33
a,33

和存储顺序一样?
s1存储不需要比,s2和s1比,返回的是正数,s2比s1大,所以s2在s1后面,所以s3在s2后面。

同理如果是-1

1
2
3
4
5
6
a,33
a,33
za,33
z,30
y,28
x,29

小结一下,A.compareTo(B)

  • 0代表相等。
  • 正数代表B在后。
  • 负数代表B在前。

所以呢,为了实现目标,我们修改compareTo方法如下即可
示例代码:

1
2
3
4
5
6
@Override
public int compareTo(Student o) {
int num = this.getAge() - o.getAge();
int num2 = num == 0 ? this.getName().compareTo(o.getName()) : num;
return num2;
}

运行结果:

1
2
3
4
5
y,28
x,29
z,30
a,33
za,33
  • String类中已经有了compareTo方法,直接利用即可。

比较器排序

关于比较器,我们在《算法入门经典(Java与Python描述):9.堆》也有过讨论,这里我们复习一下。

示例代码:

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
package com.kakawanyifan;

import java.util.Comparator;
import java.util.TreeSet;

public class TreeSetDemo {
public static void main(String[] args) {
//创建集合对象
TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
int num = s1.getAge() - s2.getAge();
int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
return num2;
}
});

//创建学生对象
Student s1 = new Student("x", 29);
Student s2 = new Student("y", 28);
Student s3 = new Student("z", 30);
Student s4 = new Student("za", 33);

Student s5 = new Student("a", 33);
Student s6 = new Student("a", 33);

//把学生添加到集合
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
ts.add(s6);

//遍历集合
for (Student s : ts) {
System.out.println(s.getName() + "," + s.getAge());
}
}
}

运行结果:

1
2
3
4
5
y,28
x,29
z,30
a,33
za,33

拼音排序

拼音排序有两种方法:

  1. 基于java.util.Locale.CHINA,该方法存在准确率的问题,主要是对于多音字不够准确。
    示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package com.kakawanyifan;

    import java.text.Collator;
    import java.util.Arrays;
    import java.util.Locale;

    public class main {
    public static void main(String[] args) {
    Collator cmp = Collator.getInstance(Locale.CHINA);
    String[] arr = { "张三", "李四", "王五"};
    Arrays.sort(arr, cmp);
    System.out.println(Arrays.toString(arr));

    String[] arr2 = { "上海", "重庆"};
    Arrays.sort(arr2, cmp);
    System.out.println(Arrays.toString(arr2));
    }
    }

    运行结果:

    1
    2
    [李四, 王五, 张三]
    [上海, 重庆]
  2. 基于第三方的com.ibm.icu.util.ULocale.SIMPLIFIED_CHINESE,该方法更准确。
    示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.kakawanyifan;

    import com.ibm.icu.text.Collator;
    import com.ibm.icu.util.ULocale;

    import java.util.Arrays;

    public class main {
    public static void main(String[] args) {
    Collator cmp = Collator.getInstance(ULocale.SIMPLIFIED_CHINESE);
    String[] arr = { "张三", "李四", "王五"};
    Arrays.sort(arr, cmp);
    System.out.println(Arrays.toString(arr));

    String[] arr2 = { "上海", "重庆"};
    Arrays.sort(arr2, cmp);
    System.out.println(Arrays.toString(arr2));
    }
    }

    运行结果:

    1
    2
    [李四, 王五, 张三]
    [重庆, 上海]

    附,方法二的包:

    1
    2
    3
    4
    5
    6
    7
    <dependencies>
    <dependency>
    <groupId>com.ibm.icu</groupId>
    <artifactId>icu4j</artifactId>
    <version>71.1</version>
    </dependency>
    </dependencies>

元素的内容被改变,不会重排

其实希望元素内容被改变还重排,更像我们的一厢情愿。
元素内容被改变,还要重排?
这个TreeSet的确不好做,如果一定要做,有两个思路。

  • 元素内容被改变了,去触发TreeSet重排
  • TreeSet有一个很巧妙的办法主动监控

同时,还需要考虑性能问题,以及安全问题。(比如,重排期间被读取。)
总之,不好做,是我们的一厢情愿。

现象

假设存在一批数据,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[{
"cnt": 1,
"name": "标签一",
}, {
"cnt": 2,
"name": "标签二"
}, {
"cnt": 3,
"name": "标签三"
}, {
"cnt": 4,
"name": "标签一"
}]

现在需要对这一批数据进行合并,相同name的合为一个,其cnt进行相加。并按照cnt从大到小进行排序。
即,如下形式:

1
2
3
4
5
6
7
8
9
10
[{
"cnt": 5,
"name": "标签一",
}, {
"cnt": 3,
"name": "标签三"
}, {
"cnt": 2,
"name": "标签二"
}]

因为需要从大到小排序,这里考虑TreeSet,这个自带排序。
但是在TreeSet中如果元素内容被改变,不会重排。

示例代码:

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
package com.kakawanyifan;

public class Label implements Comparable<Label> {
private String name;
private Integer cnt;

public Label(String name, Integer cnt) {
super();
this.name = name;
this.cnt = cnt;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Integer getCnt() {
return cnt;
}

public void setCnt(Integer cnt) {
this.cnt = cnt;
}

@Override
public int compareTo(Label o) {
return o.cnt - cnt;
}

@Override
public String toString() {
return "Label [name=" + name + ", cnt=" + cnt + "]";
}

}
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
package com.kakawanyifan;

import java.util.Set;
import java.util.TreeSet;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;

public class TreeSetDemo {

public static void main(String[] args) {
Set<Label> labelSet = new TreeSet<Label>();

String input = "[{\"cnt\":1,\"name\":\"标签一\",},{\"cnt\":2,\"name\":\"标签二\"},{\"cnt\":3,\"name\":\"标签三\"},{\"cnt\":4,\"name\":\"标签一\"}]";
JSONArray ja = JSONArray.parseArray(input);
for (int i = 0; i < ja.size(); i++) {
JSONObject jo = ja.getJSONObject(i);
String name = jo.getString("name");
Integer cnt = jo.getInteger("cnt");

boolean contain = false;
Label conLabel = null;
for(Label tempLabel : labelSet) {
if (tempLabel.getName().equals(name)) {
conLabel = tempLabel;
contain = true;
break;
}
}

if (contain) {
conLabel.setCnt(conLabel.getCnt() + cnt);
}else {
Label newLabel = new Label(name, cnt);
labelSet.add(newLabel);
}
}
System.out.println(labelSet.toString());
}
}

运行结果:

1
[Label [name=标签三, cnt=3], Label [name=标签二, cnt=2], Label [name=标签一, cnt=5]]

解释说明:

  • name标签一Label被放在第三个位置,因为后期其cnt被修改了,但是并不会重排。

解决

可以让其先去除再加入,以此触发重排。但是这样代码的确不雅。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13

【部分代码略】

if (contain) {
labelSet.remove(conLabel);
conLabel.setCnt(conLabel.getCnt() + cnt);
labelSet.add(conLabel);
}else {
Label newLabel = new Label(name, cnt);
labelSet.add(newLabel);
}

【部分代码略】

运行结果:

1
[Label [name=标签一, cnt=5], Label [name=标签三, cnt=3], Label [name=标签二, cnt=2]]

或者先用List,最后统一加入到Set中。

示例代码:

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
package com.kakawanyifan;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;

public class TreeSetDemo {

public static void main(String[] args) {
Set<Label> labelSet = new TreeSet<Label>();
List<Label> labelList = new ArrayList<Label>();

String input = "[{\"cnt\":1,\"name\":\"标签一\",},{\"cnt\":2,\"name\":\"标签二\"},{\"cnt\":3,\"name\":\"标签三\"},{\"cnt\":4,\"name\":\"标签一\"}]";
JSONArray ja = JSONArray.parseArray(input);
for (int i = 0; i < ja.size(); i++) {
JSONObject jo = ja.getJSONObject(i);
String name = jo.getString("name");
Integer cnt = jo.getInteger("cnt");

boolean contain = false;
Label conLabel = null;
for(Label tempLabel : labelList) {
if (tempLabel.getName().equals(name)) {
conLabel = tempLabel;
contain = true;
break;
}
}

if (contain) {
conLabel.setCnt(conLabel.getCnt() + cnt);
}else {
Label newLabel = new Label(name, cnt);
labelList.add(newLabel);
}
}

for (Label label:labelList) {
labelSet.add(label);
}

System.out.println(labelSet.toString());
}
}

运行结果:

1
[Label [name=标签一, cnt=5], Label [name=标签三, cnt=3], Label [name=标签二, cnt=2]]

compareTo方法的注意

现象

我们修改一下输入,添加两个{"cnt": 1,"name": "标签三"},如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[{
"cnt": 1,
"name": "标签一",
}, {
"cnt": 2,
"name": "标签二"
}, {
"cnt": 3,
"name": "标签三"
}, {
"cnt": 4,
"name": "标签一"
}, {
"cnt": 1,
"name": "标签三"
}, {
"cnt": 1,
"name": "标签三"
}]

这时候"标签一"和"标签三"的cnt,都是5了。

示例代码:

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
package com.kakawanyifan;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;

public class TreeSetDemo {

public static void main(String[] args) {
Set<Label> labelSet = new TreeSet<Label>();
List<Label> labelList = new ArrayList<Label>();

String input = "[{\"cnt\":1,\"name\":\"标签一\",},{\"cnt\":2,\"name\":\"标签二\"},{\"cnt\":3,\"name\":\"标签三\"},{\"cnt\":4,\"name\":\"标签一\"},{\"cnt\":1,\"name\":\"标签三\"},{\"cnt\":1,\"name\":\"标签三\"}]";
JSONArray ja = JSONArray.parseArray(input);
for (int i = 0; i < ja.size(); i++) {
JSONObject jo = ja.getJSONObject(i);
String name = jo.getString("name");
Integer cnt = jo.getInteger("cnt");

boolean contain = false;
Label conLabel = null;
for(Label tempLabel : labelList) {
if (tempLabel.getName().equals(name)) {
conLabel = tempLabel;
contain = true;
break;
}
}

if (contain) {
conLabel.setCnt(conLabel.getCnt() + cnt);
}else {
Label newLabel = new Label(name, cnt);
labelList.add(newLabel);
}
}

for (Label label:labelList) {
labelSet.add(label);
}

System.out.println(labelSet.toString());
}
}

运行结果:

1
[Label [name=标签一, cnt=5], Label [name=标签二, cnt=2]]

解释说明:

  • 标签三没了?

原因分析

会是需要重写equals和hashCode方法吗?
不会!

所有的类都会继承自java.lang.Object

  • java.lang.Object的hashCode值是根据内存地址换算出来的一个值。
  • java.lang.Object的equals方法是判断两个对象是否严格相等(object1 == object2),有时候,我们需要的不是一种严格意义上的相等,而是一种业务上的对象相等。在这种情况下,就需要重写equals方法。

但在这里,{name=标签三, cnt=5}{name=标签一, cnt=5}hashCode必定不相等,equals也必定为false

解决

修改label类的compareTo方法,即可解决问题。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13

【部分代码略】

@Override
public int compareTo(Label o) {
if (o.cnt - cnt > 0) {
return 1;
}else {
return -1;
}
}

【部分代码略】

运行结果:

1
[Label [name=标签三, cnt=5], Label [name=标签一, cnt=5], Label [name=标签二, cnt=2]]

解释说明:

  • 返回1,那么当前的值会排在被比较者后面。
  • 返回0,那么当前的值【不会被加入到 TreeSet 中】,因为当前的值【被认为和现有的某一个值相等】。
  • 返回-1,会被添加到被比较者的前边。

Collections

在讨论了Collection的两个子接口List和Set之后,我们来讨论一个工具类,Collections。

  • 注意是Collections,不是Collection,多了一个s

Collections的常用方法:

方法名 说明
public static void sort(List list) 将指定的列表按升序排序
public static void reverse(List<?> list) 反转指定列表中元素的顺序
public static void shuffle(List<?> list) 使用默认的随机源随机排列指定的列表

示例代码:

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
package com.kakawanyifan;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CollectionsDemo {
public static void main(String[] args) {
//创建集合对象
List<Integer> list = new ArrayList<Integer>();

//添加元素
list.add(30);
list.add(20);
list.add(50);
list.add(10);
list.add(40);

Collections.sort(list);
System.out.println(list);

Collections.reverse(list);
System.out.println(list);

Collections.shuffle(list);
System.out.println(list);
}
}

运行结果:

1
2
3
[10, 20, 30, 40, 50]
[50, 40, 30, 20, 10]
[10, 30, 40, 20, 50]

Collections中同样可以利用比较器。
示例代码:

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
package com.kakawanyifan;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class CollectionsDemo {
public static void main(String[] args) {
//创建ArrayList集合对象
List<Student> array = new ArrayList<Student>();

//创建学生对象
Student s1 = new Student("x", 29);
Student s2 = new Student("y", 28);
Student s3 = new Student("z", 30);
Student s4 = new Student("za", 33);

Student s5 = new Student("a", 33);
Student s6 = new Student("a", 33);

//把学生添加到集合
array.add(s1);
array.add(s2);
array.add(s3);
array.add(s4);
array.add(s5);
// array.add(s6);

//使用Collections对ArrayList集合排序
//sort(List<T> list, Comparator<? super T> c)
Collections.sort(array, new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
//按照年龄从小到大排序,年龄相同时,按照姓名的字母顺序排序
int num = s1.getAge() - s2.getAge();
int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
return num2;
}
});

//遍历集合
for (Student s : array) {
System.out.println(s.getName() + "," + s.getAge());
}
}
}

运行结果:

1
2
3
4
5
y,28
x,29
z,30
a,33
za,33

Map

接下来,我们讨论集合的双列,Map。

概述和特点

Map是一个接口

1
interface Map<K,V>
  • K:键的类型
  • V:值的类型

描述的是键值对映射关系,一个键对应一个值,键不能重复,值可以重复。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.kakawanyifan;

import java.util.HashMap;
import java.util.Map;

public class MapDemo {
public static void main(String[] args) {
//创建集合对象
Map<String,String> map = new HashMap<String,String>();

// V put(K key, V value) 将指定的值与该映射中的指定键相关联
map.put("001","林青霞");
map.put("002","张曼玉");
map.put("003","王祖贤");

System.out.println(map);

map.put("003","柳岩");

//输出集合对象
System.out.println(map);
}
}

运行结果:

1
2
{001=林青霞, 002=张曼玉, 003=王祖贤}
{001=林青霞, 002=张曼玉, 003=柳岩}
  • map.put("003","柳岩")覆盖了map.put("003","王祖贤")

Map的常用方法

方法名 说明
V put(K key,V value) 添加元素。
V get(Object key) 获取元素。
V remove(Object key) 根据键删除键值对元素。
void clear() 移除所有的键值对元素。
boolean containsKey(Object key) 判断集合是否包含指定的键。
boolean containsValue(Object value) 判断集合是否包含指定的值。
boolean isEmpty() 判断集合是否为空。
int size() 集合的长度,也就是集合中键值对的个数。

示例代码:

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
package com.kakawanyifan;

import java.util.HashMap;
import java.util.Map;

public class MapDemo {
public static void main(String[] args) {
//创建集合对象
Map<String,String> map = new HashMap<String,String>();

//V put(K key,V value):添加元素
map.put("张无忌","赵敏");
map.put("郭靖","黄蓉");
map.put("杨过","小龙女");

System.out.println(map);

//V remove(Object key):根据键删除键值对元素
System.out.println(map.remove("郭靖"));
System.out.println(map.remove("郭襄"));

//void clear():移除所有的键值对元素
map.clear();

//boolean containsKey(Object key):判断集合是否包含指定的键
System.out.println(map.containsKey("郭靖"));
System.out.println(map.containsKey("郭襄"));

//boolean isEmpty():判断集合是否为空
System.out.println(map.isEmpty());

//int size():集合的长度,也就是集合中键值对的个数
System.out.println(map.size());

//输出集合对象
System.out.println(map);
}
}

运行结果:

1
2
3
4
5
6
7
8
{杨过=小龙女, 郭靖=黄蓉, 张无忌=赵敏}
黄蓉
null
false
false
true
0
{}

Map的获取方法

方法名 说明
V get(Object key) 根据键获取值。
Set keySet() 获取所有键的集合。
Collection values() 获取所有值的集合。
Set<Map.Entry<K,V>> entrySet() 获取所有键值对对象的集合。

我们可以获取所有的键,可以根据键获取值。
我们还可以获取所有的键值对对象的集合。
另外,如果我们只想要值的话,可以直接获取值的集合。

所以,我们有三种遍历方式。

示例代码:

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
package com.kakawanyifan;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class MapDemo {
public static void main(String[] args) {
//创建集合对象
Map<String, String> map = new HashMap<String, String>();

//添加元素
map.put("张无忌", "赵敏");
map.put("郭靖", "黄蓉");
map.put("杨过", "小龙女");

//V get(Object key):根据键获取值
System.out.println(map.get("张无忌"));
System.out.println(map.get("张三丰"));
System.out.println("------");

//Set<K> keySet():获取所有键的集合
Set<String> keySet = map.keySet();
for(String key : keySet) {
System.out.println(key + "," + map.get(key));
}
System.out.println("------");


//Collection<V> values():获取所有值的集合
Collection<String> values = map.values();
for(String value : values) {
System.out.println(value);
}
System.out.println("------");

//获取所有键值对对象的集合
Set<Map.Entry<String, String>> entrySet = map.entrySet();
//遍历键值对对象的集合,得到每一个键值对对象
for (Map.Entry<String, String> me : entrySet) {
//根据键值对对象获取键和值
String key = me.getKey();
String value = me.getValue();
System.out.println(key + "," + value);
}
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
赵敏
null
------
杨过,小龙女
郭靖,黄蓉
张无忌,赵敏
------
小龙女
黄蓉
赵敏
------
杨过,小龙女
郭靖,黄蓉
张无忌,赵敏

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
package com.kakawanyifan;

import java.util.HashMap;
import java.util.Set;

public class HashMapDemo {
public static void main(String[] args) {
//创建HashMap集合对象
HashMap<Student, Integer> hm = new HashMap<Student, Integer>();

//创建学生对象
Student s1 = new Student("林青霞", 30);
Student s2 = new Student("张曼玉", 35);
Student s3 = new Student("王祖贤", 33);
Student s4 = new Student("王祖贤", 33);

//把学生添加到集合
hm.put(s1, 99);
hm.put(s2, 98);
hm.put(s3, 87);
hm.put(s4, 90);

//遍历集合
Set<Student> keySet = hm.keySet();
for (Student key : keySet) {
int value = hm.get(key);
System.out.println(key.getName() + "," + key.getAge() + "," + value);
}
}
}

运行结果:

1
2
3
4
王祖贤,33,87
张曼玉,35,98
林青霞,30,99
王祖贤,33,90

出BUG了!

我们用的是HashMap啊,怎么出现了重复的键?两个王祖贤,33

到底怎么回事?

源码分析

我们来看看这个put方法到底做了什么。

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

先来看看hash方法。

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

哦,利用了hashCode,所以我们的Student类继承自Object类,我们要重写Student类的hashCode方法。

再来看看putVal方法。

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
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
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);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

还利用了equals方法,我们还要重写Student类的equals方法。

即,需要重写Student类的hashCode方法和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
package com.kakawanyifan;

import java.util.Objects;

/**
* 学生类
*/
public class Student {
private String name;
private int age;

public Student(String name, int age) {
this.name = name;
this.age = age;
}

public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setAge(int age) {
this.age = age;
}

public int getAge() {
return age;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

Student student = (Student) o;

if (age != student.age) return false;
return Objects.equals(name, student.name);
}

@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
}

为什么是31?
因为31在JVM中是一个神奇的数字。
任何数n*31都可以被jvm优化为(n<<5)-n,移位和减法的操作效率比乘法的操作效率高很多。

关于Map,两个知识点。

  1. “TreeMap”,这个我们在《算法入门经典(Java与Python描述):7.哈希表》中有过讨论,不赘述。
  2. “Properties”,这个我们会在下一章《5.IO流》中进行讨论。

集合嵌套

ArrayList嵌套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
package com.kakawanyifan;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;

public class ArrayListIncludeHashMapDemo {
public static void main(String[] args) {
//创建ArrayList集合
ArrayList<HashMap<String, String>> array = new ArrayList<HashMap<String, String>>();

//创建HashMap集合,并添加键值对元素
HashMap<String, String> hm1 = new HashMap<String, String>();
hm1.put("孙策", "大乔");
hm1.put("周瑜", "小乔");
//把HashMap作为元素添加到ArrayList集合
array.add(hm1);

HashMap<String, String> hm2 = new HashMap<String, String>();
hm2.put("郭靖", "黄蓉");
hm2.put("杨过", "小龙女");
//把HashMap作为元素添加到ArrayList集合
array.add(hm2);

HashMap<String, String> hm3 = new HashMap<String, String>();
hm3.put("令狐冲", "任盈盈");
hm3.put("林平之", "岳灵珊");
//把HashMap作为元素添加到ArrayList集合
array.add(hm3);

//遍历ArrayList集合
for (HashMap<String, String> hm : array) {
Set<String> keySet = hm.keySet();
for (String key : keySet) {
String value = hm.get(key);
System.out.println(key + "," + value);
}
}
}
}

运行结果:

1
2
3
4
5
6
孙策,大乔
周瑜,小乔
杨过,小龙女
郭靖,黄蓉
令狐冲,任盈盈
林平之,岳灵珊

HashMap嵌套ArrayList

示例代码:

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
package com.kakawanyifan;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;

public class ArrayListIncludeHashMapDemo {
public static void main(String[] args) {
//创建HashMap集合
HashMap<String, ArrayList<String>> hm = new HashMap<String, ArrayList<String>>();

//创建ArrayList集合,并添加元素
ArrayList<String> sgyy = new ArrayList<String>();
sgyy.add("诸葛亮");
sgyy.add("赵云");
//把ArrayList作为元素添加到HashMap集合
hm.put("三国演义",sgyy);

ArrayList<String> xyj = new ArrayList<String>();
xyj.add("唐僧");
xyj.add("孙悟空");
//把ArrayList作为元素添加到HashMap集合
hm.put("西游记",xyj);

ArrayList<String> shz = new ArrayList<String>();
shz.add("武松");
shz.add("鲁智深");
//把ArrayList作为元素添加到HashMap集合
hm.put("水浒传",shz);

//遍历HashMap集合
Set<String> keySet = hm.keySet();
for(String key : keySet) {
System.out.println(key);
ArrayList<String> value = hm.get(key);
for(String s : value) {
System.out.println("\t" + s);
}
}
}
}

运行结果:

1
2
3
4
5
6
7
8
9
水浒传
武松
鲁智深
三国演义
诸葛亮
赵云
西游记
唐僧
孙悟空

泛型

最后一个话题,泛型。
《3.最常用的Java自带的类》这么一章,讨论ArrayList的时候,我们提到了泛型。

1
ArrayList<String> array = new ArrayList<String>();

这里有一个<>,里面写着String。这个含义是说这个ArrayList的元素的数据类型是String。
除了String,还可以是其他各种各样的引用类型,比如ArryList<Student>,表示每一个元素的数据类型都是Student。
这就是泛型的使用,用于约束集合中存储元素的数据类型。

在本章,我们其实也大量用到了泛型。
现在我们系统的讨论一下泛型。

泛型概述

根据之前的讨论,我们知道泛型的本质是一个参数化类型,也就是说所操作的数据类型被指定为一个参数。

根据参数的个数,泛型的定义有两种格式,两种定义格式,其实我们都已经用过了。

第一种是格式是:

1
<类型>

这种格式,我们在Collection中用过。

第二种格式是

1
<类型1,类型2...>

这种格式,我们在Map中用过。

泛型类

泛型类初步

泛型类的定义格式

1
修饰符 class 类名<类型> {  }

泛型类的使用格式

1
类名<具体的数据类型> 对象名 = new 类名<具体的数据类型>();

在JDK8及之后,后面的<>中的具体的数据类型可以省略不写。

1
类名<具体的数据类型> 对象名 = new 类名<>();

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kakawanyifan;

public class Generic<T> {
private T t;

public T getT() {
return t;
}

public void setT(T t) {
this.t = t;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.kakawanyifan;

public class GenericDemo {
public static void main(String[] args) {
Generic<String> g1 = new Generic<String>();
g1.setT("林青霞");
System.out.println(g1.getT());

Generic<Integer> g2 = new Generic<Integer>();
g2.setT(30);
System.out.println(g2.getT());

Generic<Boolean> g3 = new Generic<Boolean>();
g3.setT(true);
System.out.println(g3.getT());
}
}

运行结果:

1
2
3
林青霞
30
true

特别的,如果实例化了泛型,但是又没有指定数据类型呢?
那么就是Object。
没有指定数据类型

泛型标识

在刚刚的例子中,泛型类的类型参数是T,这里的T也被称为称为泛型标识。
泛型标识有如下几种,以及墨守成规的是惯例。

  • E,含义是Element,在集合中使用,因为集合中存放的是元素。
  • T,含义是Type,表示类。
  • K,含义是Key,表示键,指的是Map中的key。
  • V,含义是Value,表示值,值得是Map种的Value。
  • N,含义是Number,表示数值类型。
  • ?,表示不确定的类。
  • S、U、V,也是类,在多个泛型的时候,以示区分。

泛型类的继承

泛型类也有其继承方法。
根据子类是不是泛型类分为两种。

子类是泛型类

子类是泛型类,子类和父类的泛型标识要一致。

1
class Zi<T> extends Fu<T>
  • Zi和Fu的<>内的内容要一致。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kakawanyifan;

public class Fu<E> {
private E e;

public E getE() {
return e;
}

public void setE(E e) {
this.e = e;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.kakawanyifan;

public class Zi<T> extends Fu<T>{
private T t;

public T getT() {
return t;
}

public void setT(T t) {
this.t = t;
}
}

子类不是泛型类

子类不是泛型类,父类要明确泛型的数据类型。

1
class Zi extends Fu<Integer>

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.kakawanyifan;

public class Zi extends Fu<Integer>{

@Override
public Integer getE() {
return super.getE();
}

@Override
public void setE(Integer integer) {
super.setE(integer);
}
}

泛型接口

泛型接口定义格式

1
修饰符 interface 接口名<类型> {  }

与继承类似,根据泛型接口的实现类是不是泛型类,在接口实现上分为两种情况。

  1. 实现类也是泛型类,实现类和接口的泛型标识要一致。
  2. 实现类不是泛型类,接口要明确数据类型。

示例代码:

1
2
3
4
5
package com.kakawanyifan;

public interface Generic<T> {
void show(T t);
}
1
2
3
4
5
6
7
8
package com.kakawanyifan;

public class GenericImpl<T> implements Generic<T> {
@Override
public void show(T t) {
System.out.println(t);
}
}
1
2
3
4
5
6
7
8
9
10
11
package com.kakawanyifan;

public class GenericDemo {
public static void main(String[] args) {
Generic<String> g1 = new GenericImpl<String>();
g1.show("林青霞");

Generic<Integer> g2 = new GenericImpl<Integer>();
g2.show(30);
}
}

运行结果:

1
2
林青霞
30

泛型方法

泛型方法的定义格式

1
修饰符 <类型> 返回值类型 方法名(类型 变量名) {  }
  • <类型>这一部分是泛型方法和其他方法主要区别,只有有这一部分的方法才能被称为泛型方法。
    所以,在上文泛型类的成员方法,不可以被称为泛型方法。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.kakawanyifan;

public class GenericDemo {

private static <T> void show(T t) {
System.out.println(t);
}

public static void main(String[] args) {
show("林青霞");
show(30);
show(true);
show(12.34);
}
}

运行结果:

1
2
3
4
林青霞
30
true
12.34

类型通配符

类型通配符的分类:

  1. <?>
    例如List<?>:表示元素类型未知的List,它的元素可以匹配任何的类型。
  2. <? extends 类型>
    例如List<? extends Number>:表示的类型是Number或者其子类型。
  3. <? super 类型>
    例如:List<? super Number>:它表示的类型是Number或者其父类型。

示例代码:

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
package com.kakawanyifan;

import java.util.ArrayList;
import java.util.List;

public class GenericDemo {
public static void main(String[] args) {
//类型通配符:<?>
List<?> list1 = new ArrayList<Object>();
List<?> list2 = new ArrayList<Number>();
List<?> list3 = new ArrayList<Integer>();
System.out.println("--------");

// 类型通配符上限:<? extends 类型>
// List<? extends Number> list4 = new ArrayList<Object>();
List<? extends Number> list5 = new ArrayList<Number>();
List<? extends Number> list6 = new ArrayList<Integer>();
System.out.println("--------");

// 类型通配符下限:<? super 类型>
List<? super Number> list7 = new ArrayList<Object>();
List<? super Number> list8 = new ArrayList<Number>();
// List<? super Number> list9 = new ArrayList<Integer>();
}
}

泛型的特点

泛型的特点有:

  1. 泛型的类型参数只能是引用数据类型,不能是基本数据类型。
  2. 泛型的类型在逻辑上可以看成是多个不同的类型,但实际上都是相同类型。

第二个特点或许不好理解,我们来看一个例子。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.kakawanyifan;

public class GenericDemo {
public static void main(String[] args) {
Generic<String> g1 = new Generic<String>();
System.out.println(g1.getClass());

Generic<Integer> g2 = new Generic<Integer>();
System.out.println(g2.getClass());

Generic<Boolean> g3 = new Generic<Boolean>();
System.out.println(g3.getClass());
}
}

运行结果:

1
2
3
class com.kakawanyifan.Generic
class com.kakawanyifan.Generic
class com.kakawanyifan.Generic

那么,现在问题来了,其实都是同一种类型吧,那是怎么做到又能支持"String",又能支持"Integer"的呢?
这就涉及到了泛型的原理,类型擦除。

泛型原理:类型擦除

泛型是Java 1.5版本才引进的概念,在这之前是没有泛型的,但是泛型代码能够很好地和之前版本的代码兼容。那是因为,泛型信息只存在于代码编译阶段,在进入JVM之前,与泛型相关的信息会被擦除掉,我们称之为:类型擦除。

类型擦除有四种。

  1. 无限制类型擦除
  2. 有限制类型擦除
  3. 擦除方法中类型定义的参数
  4. 桥接方法

我们分别讨论。

无限制类型擦除

无限制类型擦除,在编译的时候,把范型擦除为Object类型。

无限制类型擦除

我们可以通过反射看看,到底是不是这样。

(关于反射,我们在《9.类的加载与反射》有做专题讨论。)

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.kakawanyifan;

public class Erasure<T> {

private T t;

public T getT() {
return t;
}

public void setT(T t) {
this.t = t;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.kakawanyifan;

import java.lang.reflect.Field;

public class ErasureDemo {
public static void main(String[] args) {
Erasure<Integer> erasure = new Erasure<>();
// 利用反射获取字节码文件的对象
Class<? extends Erasure> clz = erasure.getClass();
// 获取所有的成员变量
Field[] declaredFields = clz.getDeclaredFields();
for (Field declareField:declaredFields) {
// 打印成员变量的名称和类型
System.out.println(declareField.getName() + " : " + declareField.getType().getName());
}
}
}

运行结果:

1
t : java.lang.Object

成员变量的名字依旧是t,但是类型已经成为了java.lang.Object,符合我们上文的描述,证明完毕。

也是因为这个原因,在反射中还有一个神奇的现象,越过泛型检查,我们会在《9.类的加载与反射》讨论反射的时候讨论。

有限制类型擦除

有限制类型擦除,把类型擦除为其extend的类型。

有限制类型擦除

证明过程略。

擦除方法中类型定义的参数

擦除方法中类型定义的参数,该种擦除同样存在有限和无限两种情况。
擦除方法中类型定义的参数

我们利用反射来看一下。

示例代码:

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
package com.kakawanyifan;

import java.util.List;

public class Erasure<T extends Number> {

private T t;

public T getT() {
return t;
}

public void setT(T t) {
this.t = t;
}

/**
* 泛型方法
* @param t
* @param <T>
* @return
*/
public <T extends List> T rtn (T t){
return t;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.kakawanyifan;

import java.lang.reflect.Method;

public class ErasureDemo {
public static void main(String[] args) {
Erasure<Integer> erasure = new Erasure<>();
// 利用反射获取字节码文件的对象
Class<? extends Erasure> clz = erasure.getClass();
// 获取所有的成员方法
Method[] declaredMethods = clz.getDeclaredMethods();
for (Method declaredMethod:declaredMethods) {
// 打印成员方法的名称和返回类型
System.out.println(declaredMethod.getName() + " : " + declaredMethod.getReturnType().getName());
}
}
}

运行结果:

1
2
3
setT : void
rtn : java.util.List
getT : java.lang.Number

确实存在一个方法,方法名是rtn,返回类型是java.util.List,符合我们上文的描述,证明完毕。

桥接方法

最后一种是桥接方法,这种主要存在与接口的实现类中。其特点是利用一个桥接方法,保持接口和类的联系。

桥接方法

我们利用反射来看一下。

示例代码:

1
2
3
4
5
package com.kakawanyifan;

public interface Info<T> {
T info(T t);
}
1
2
3
4
5
6
7
8
9
package com.kakawanyifan;

public class InfoImpl implements Info<Integer> {

@Override
public Integer info(Integer integer) {
return integer;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.kakawanyifan;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class InfoImplDemo {
public static void main(String[] args) {
InfoImpl infoImpl = new InfoImpl();
// 利用反射获取字节码文件的对象
Class<? extends InfoImpl> clz = infoImpl.getClass();
// 获取所有的成员方法
Method[] declaredMethods = clz.getDeclaredMethods();
for (Method declaredMethod:declaredMethods) {
// 打印成员方法的名称和返回类型
System.out.println(declaredMethod.getName() + " : " + declaredMethod.getReturnType().getName());
Parameter[] parameters = declaredMethod.getParameters();
for (Parameter parameter:parameters) {
System.out.println(parameter.getType().getName());
}
System.out.println();
}
}
}

运行结果:

1
2
3
4
5
info : java.lang.Integer
java.lang.Integer

info : java.lang.Object
java.lang.Object

确实存在两个方法,其中一个是原始的逻辑处理,另一个是桥接方法。符合我们上文的描述,证明完毕。证明完毕。

可变参数

可变参数即参数个数可变,用作方法的形参出现,指该方法参数个数就是可变的。

可变参数定义格式

1
修饰符 返回值类型 方法名(数据类型... 变量名) {  }

可变参数的注意事项

  1. 这里的变量其实是一个数组
  2. 如果一个方法有多个参数,包含可变参数,可变参数要放在最后

我们来看个具体的例子。

示例代码:

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
package com.kakawanyifan;

public class ArgsDemo {

public static void main(String[] args) {
System.out.println(sum(0));
System.out.println(sum(0, 1));
System.out.println(sum(0, 1, 2));
System.out.println(sum(0, 1, 2, 3));
System.out.println(sum(0, 1, 2, 3, 4));
System.out.println(sum(0, 1, 2, 3, 4, 5));
System.out.println(sum(0, 1, 2, 3, 4, 5, 6));
System.out.println(sum(0, 1, 2, 3, 4, 5, 6, 7));
System.out.println(sum(0, 1, 2, 3, 4, 5, 6, 7, 8));
System.out.println(sum(0, 1, 2, 3, 4, 5, 6, 7, 8,9));
}

public static int sum(int... a) {
int sum = 0;
for(int i : a) {
sum += i;
}
return sum;
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
0
1
3
6
10
15
21
28
36
45

Arrays源码分析

这不是我们第一次分析Arrays的源码,在《3.最常用的Java自带的类》这一章我们也分析过Arrays的源码,当时我们以Arrays的源码为例,说设计工具类时候,非常建议的两个准则。

  1. 构造方法用private,防止外界创建对象。
  2. 成员方法用public static,为了使用类名访问成员方法。

这次,我们来看Arrays的一个似乎不是特别好的地方。

asList的注意

在Arrays类的asList方法中,我们可以看到可变参数,还可以看到泛型。

1
2
3
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
  • 修饰符之后紧跟着的<T>,说明这是一个泛型方法。
  • T...,说明这是一个可变参数方法。

我们来看个例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.kakawanyifan;

import java.util.Arrays;
import java.util.List;

public class ArgsDemo {

public static void main(String[] args) {
List<String> a = Arrays.asList("a", "b", "c");
System.out.println(a);
a.set(0, "d");
System.out.println(a);
a.add("e");
System.out.println(a);
}
}

运行结果:

1
2
3
4
5
6
[a, b, c]
[d, b, c]
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)
at java.util.AbstractList.add(AbstractList.java:108)
at com.kakawanyifan.ArgsDemo.main(ArgsDemo.java:13)

可以修改,但是不能新增?不支持的操作?

看看asList方法的源代码。

1
2
3
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}

调用的是ArrayList的构造方法,再来看看ArrayList的构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;

ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}

【部分代码略】

}

注意看!a居然是用final修饰的。在《2.面向对象》,讨论继承的访问权限的时候,我们讨论过。“fianl修饰类:该类不能被继承(不能有子类,但是可以有父类)”

此外,我们还讨论了
只要是final修饰的变量,就是不能再次进行赋值了,但是对于修饰的是基本数据类型的变量还是引用数据类型的变量,还有区别:

  • fianl修饰基本数据类型变量:final修饰指的是基本类型的数据值不能发生改变。
  • final修饰引用数据类型变量:final修饰指的是引用类型的地址值不能发生改变,但是地址里面的内容是可以发生改变的。

这样就可以解释,为什么我们不可以增加删除,但是可以修改了。

解决方法

那么,我们怎么把一个数组转为ArrayList,同时还能对其进行增删操作呢?
两种方法。

  1. 遍历数组,然后一个一个加进去。
  2. 利用Collections工具类addAll()方法。

第一种方法不举例子,第二种方法,我们举个例子。
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.kakawanyifan;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ExceptionDemo {
public static void main(String[] args) {
String[] array = {"a", "b", "c"};
List<String> arrayList = new ArrayList<>();
Collections.addAll(arrayList,array);
System.out.println(arrayList);
arrayList.set(0,"d");
System.out.println(arrayList);
arrayList.add("e");
System.out.println(arrayList);

}
}

运行结果:

1
2
3
[a, b, c]
[d, b, c]
[d, b, c, e]
文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/10804
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

评论区