Java之集合
摘自廖雪峰的java教程,有部分删改
Java集合简介
Java的集合类定义在java.util包中,支持泛型,主要提供了3种集合类,包括List,Set和Map。Java集合使用统一的Iterator遍历,尽量不要使用遗留接口。
使用List
- 分为
ArrayList和LinkedList- 通常情况下,我们总是优先使用
ArrayList
- 通常情况下,我们总是优先使用
List的特点
List接口允许我们添加重复的元素,即List内部的元素可以重复:1
2
3
4
5
6
7
8
9
10
11
12import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("apple"); // size=1
list.add("pear"); // size=2
list.add("apple"); // 允许重复添加元素,size=3
System.out.println(list.size());
}
}List允许添加null:1
2
3
4
5
6
7
8
9
10
11
12
13import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("apple"); // size=1
list.add(null); // size=2
list.add("pear"); // size=3
String second = list.get(1); // null
System.out.println(second);
}
}
创建List
1 | List<Integer> list = List.of(1, 2, 5); |
List.of()方法不接受null值,如果传入null,会抛出NullPointerException异常。
遍历List
- 不推荐,用
for循环根据索引配合get(int)方法遍历1
2
3
4
5
6
7
8
9
10
11import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
for (int i=0; i<list.size(); i++) {
String s = list.get(i);
System.out.println(s);
}
}
} - 使用
Iterator遍历List1
2
3
4
5
6
7
8
9
10// Iterator对象有两个方法:boolean hasNext()判断是否有下一个元素,E next()返回下一个元素。
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
for (String s : list) {
System.out.println(s);
}
}
}
List和Array转换
- 调用
toArray()方法直接返回一个Object[]数组1
2
3
4
5
6
7
8
9
10
11import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
Object[] array = list.toArray();
for (Object s : array) {
System.out.println(s);
}
}
}这种方法会丢失类型信息,所以实际应用很少。
- 给
toArray(T[])传入一个类型相同的Array,List内部自动把元素复制到传入的Array中1
2
3
4
5
6
7
8
9
10
11import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> list = List.of(12, 34, 56);
Integer[] array = list.toArray(new Integer[3]);//由于方法泛型参数是<T>,所以传number返回number也可以...
for (Integer n : array) {
System.out.println(n);
}
}
}- 但是,如果我们传入类型不匹配的数组,例如,
String[]类型的数组,由于List的元素是Integer,所以无法放入String数组,这个方法会抛出ArrayStoreException - 如果传入的数组不够大,那么
List内部会创建一个新的刚好够大的数组,填充后返回;如果传入的数组比List元素还要多,那么填充完元素后,剩下的数组元素一律填充null。
- 但是,如果我们传入类型不匹配的数组,例如,
编写equals方法
List内部并不是通过==判断两个元素是否相等,而是使用equals()方法判断两个元素是否相等- 因此,要正确使用
List的contains()、indexOf()这些方法,放入的实例必须正确覆写equals()方法,否则,放进去的实例,查找不到。我们之所以能正常放入String、Integer这些对象,是因为Java标准库定义的这些类已经正确实现了equals()方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import java.util.List;
public class Main {
public static void main(String[] args) {
List<Person> list = List.of(
new Person("Xiao Ming"),
new Person("Xiao Hong"),
new Person("Bob")
);
System.out.println(list.contains(new Person("Bob"))); // false
}
}
class Person {
String name;
public Person(String name) {
this.name = name;
}
}不出意外,虽然放入了new Person(“Bob”),但是用另一个new Person(“Bob”)查询不到,原因就是Person类没有覆写equals()方法。
编写equals
equals()方法必须满足以下条件
- 自反性(Reflexive):对于非
null的x来说,x.equals(x)必须返回true; - 对称性(Symmetric):对于非
null的x和y来说,如果x.equals(y)为true,则y.equals(x)也必须为true; - 传递性(Transitive):对于非
null的x、y和z来说,如果x.equals(y)为true,y.equals(z)也为true,那么x.equals(z)也必须为true; - 一致性(Consistent):对于非
null的x和y来说,只要x和y状态不变,则x.equals(y)总是一致地返回true或者false; - 对
null的比较:即x.equals(null)永远返回false。
eg:
以Person类为例
1 | public class Person { |
简化引用类型的比较,我们使用Objects.equals()静态方法
1 | public boolean equals(Object o) { |
总结
- 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
- 用
instanceof判断传入的待比较的Object是不是当前类型,如果是,继续比较,否则,返回false; - 对引用类型用
Objects.equals()比较,对基本类型直接用==比较。
- 使用
Objects.equals()比较两个引用类型是否相等的目的是省去了判断null的麻烦。两个引用类型都是null时它们也是相等的。 - 如果不调用
List的contains()、indexOf()这些方法,那么放入的元素就不需要实现equals()方法。
使用Map
Map构建
Map有关于哈希表,这里不多赘述,感兴趣可以搜索了解
1 | import java.util.HashMap; |
Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉。
遍历Map
- 对
Map来说,要遍历key可以使用for each循环遍历Map实例的keySet()方法返回的Set集合,它包含不重复的key的集合1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 123);
map.put("pear", 456);
map.put("banana", 789);
for (String key : map.keySet()) {
Integer value = map.get(key);
System.out.println(key + " = " + value);
}
}
} - 同时遍历
key和value可以使用for each循环遍历Map对象的entrySet()集合,它包含每一个key-value映射1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 123);
map.put("pear", 456);
map.put("banana", 789);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}
}
}遍历的时候,每个key会保证被遍历一次且仅遍历一次,但顺序完全没有保证,甚至对于不同的JDK版本,相同的代码遍历的输出顺序都是不同的
这就意味着,使用map时,任何依赖顺序的逻辑都是不可靠的
编写hashCode方法
hashCode()作用
- 快速定位与高效查找:通过取模运算将
HashCode映射到哈希表的特定存储桶(Bucket),时间复杂度为O(1)。 - 减少equals调用次数:当多个对象
HashCode相同时(哈希冲突),仅需在冲突的存储桶内使用equals()比较元素,而非遍历整个集合。
equals方法和hashcode的关系
- 如果两个对象
equals相等,那么这两个对象的HashCode一定也相同 - 如果两个对象的
HashCode相同,不代表两个对象就相同,只能说明这两个对象在散列存储结构中,存放于同一个位置
重写哈希函数
1 |
|
hashCode()方法编写得越好,HashMap工作的效率就越高。
使用EnumMap
如果作为key的对象是enum类型,那么,还可以使用Java集合库提供的一种EnumMap,它在内部以一个非常紧凑的数组存储value,并且根据enum类型的key直接定位到内部数组的索引,并不需要计算hashCode(),不但效率最高,而且没有额外的空间浪费。
举个例子
1 | //我们以DayOfWeek这个枚举类型为例,为它做一个“翻译”功能: |
使用EnumMap的时候,我们总是用Map接口来引用它,因此,实际上把HashMap和EnumMap互换,在客户端看来没有任何区别。
使用TreeMap
SortedMap接口的实现类,在内部对Key进行排序
SortedMap保证遍历时以Key的顺序来进行排序。例如,放入的Key是"apple"、"pear"、"orange",遍历的顺序一定是"apple"、"orange"、"pear",因为String默认按字母排序:1
2
3
4
5
6
7
8
9
10
11
12
13
14import java.util.*;
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new TreeMap<>();
map.put("orange", 1);
map.put("apple", 2);
map.put("pear", 3);
for (String key : map.keySet()) {
System.out.println(key);
}
// apple, orange, pear
}
}- 使用
TreeMap时,放入的Key必须实现Comparable接口。String、Integer这些类已经实现了Comparable接口,因此可以直接作为Key使用。作为Value的对象则没有任何要求。
- 使用
- 如果作为Key的
class没有实现Comparable接口,那么,必须在创建TreeMap时同时指定一个自定义排序算法: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
29import java.util.*;
public class Main {
public static void main(String[] args) {
Map<Person, Integer> map = new TreeMap<>(new Comparator<Person>() {
public int compare(Person p1, Person p2) {
return p1.name.compareTo(p2.name);
}
});
map.put(new Person("Tom"), 1);
map.put(new Person("Bob"), 2);
map.put(new Person("Lily"), 3);
for (Person key : map.keySet()) {
System.out.println(key);
}
// {Person: Bob}, {Person: Lily}, {Person: Tom}
System.out.println(map.get(new Person("Bob"))); // 2
}
}
class Person {
public String name;
Person(String name) {
this.name = name;
}
public String toString() {
return "{Person: " + name + "}";
}
}- eg:定义了
Student类,并用分数score进行排序,高分在前: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
30import java.util.*;
public class Main {
public static void main(String[] args) {
Map<Student, Integer> map = new TreeMap<>(new Comparator<Student>() {
public int compare(Student p1, Student p2) {
return p1.score > p2.score ? -1 : 1;
}
});
map.put(new Student("Tom", 77), 1);
map.put(new Student("Bob", 66), 2);
map.put(new Student("Lily", 99), 3);
for (Student key : map.keySet()) {
System.out.println(key);
}
System.out.println(map.get(new Student("Bob", 66))); // null?
}
}
class Student {
public String name;
public int score;
Student(String name, int score) {
this.name = name;
this.score = score;
}
public String toString() {
return String.format("{%s: score=%d}", name, score);
}
}在for循环中,我们确实得到了正确的顺序。但是,且慢!根据相同的Key:new Student(“Bob”, 66)进行查找时,结果为null!
- 原因其实出在这个Comparator上:在
p1.score和p2.score不相等的时候,它的返回值是正确的,但是,在p1.score和p2.score相等的时候,它并没有返回0!这就是为什么TreeMap工作不正常的原因:TreeMap在比较两个Key是否相等时,依赖Key的compareTo()方法或者Comparator.compare()方法。在两个Key相等时,必须返回0。因此,修改代码如下:1
2
3
4
5
6public int compare(Student p1, Student p2) {
if (p1.score == p2.score) {
return 0;
}
return p1.score > p2.score ? -1 : 1;
}
- 原因其实出在这个Comparator上:在
- eg:定义了
使用Properties
由于历史遗留原因,Properties内部本质上是一个Hashtable,所以不要去调用这些从Hashtable继承下来的方法。
读取配置文件
- 用
Properties读取配置文件非常简单。Java默认配置文件以.properties为扩展名,每行以key=value表示,以#课开头的是注释。以下是一个典型的配置文件:1
2
3
4# setting.properties
last_open_file=/data/hello.txt
auto_save_interval=60 - 读取:
1
2
3
4
5
6String f = "setting.properties";
Properties props = new Properties();
props.load(new java.io.FileInputStream(f));
String filepath = props.getProperty("last_open_file");
String interval = props.getProperty("auto_save_interval", "120");
可见,用Properties读取配置文件,一共有三步:
- 创建
Properties实例; - 调用
load()读取文件; - 调用
getProperty()获取配置。调用getProperty()获取配置时,如果key不存在,将返回null。我们还可以提供一个默认值,这样,当key不存在的时候,就返回默认值。
- 也可以从classpath读取
.properties文件:1
2Properties props = new Properties();
props.load(getClass().getResourceAsStream("/common/setting.properties")); - 从内存读取一个字节流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// properties
import java.io.*;
import java.util.Properties;
public class Main {
public static void main(String[] args) throws IOException {
String settings = "# test" + "\n" + "course=Java" + "\n" + "last_open_date=2019-08-07T12:35:01";
ByteArrayInputStream input = new ByteArrayInputStream(settings.getBytes("UTF-8"));
Properties props = new Properties();
props.load(input);
System.out.println("course: " + props.getProperty("course"));
System.out.println("last_open_date: " + props.getProperty("last_open_date"));
System.out.println("last_open_file: " + props.getProperty("last_open_file"));
System.out.println("auto_save: " + props.getProperty("auto_save", "60"));
}
}
写入配置文件
如果通过setProperty()修改了Properties实例,可以把配置写入文件,以便下次启动时获得最新配置。写入配置文件使用store()方法:
1 | Properties props = new Properties(); |
编码
- 从JDK9开始,Java的
.properties文件可以使用UTF-8编码了。 - 不过,需要注意的是,由于
load(InputStream)默认总是以ASCII编码读取字节流,所以会导致读到乱码。我们需要用另一个重载方法load(Reader)读取:1
2Properties props = new Properties();
props.load(new FileReader("settings.properties", StandardCharsets.UTF_8));
使用Set
如果我们只需要存储不重复的
key,并不需要存储映射的value,那么就可以使用Set因此,我们经常用Set用于去除重复元素。
Set用于存储不重复的元素集合,它主要提供以下几个方法:- 将元素添加进
Set<E>:add(E e) - 将元素从
Set<E>删除:remove(Object e) - 判断是否包含元素:
contains(Object e)
- 将元素添加进
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14import java.util.*;
public class Main {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
System.out.println(set.add("abc")); // true
System.out.println(set.add("xyz")); // true
System.out.println(set.add("xyz")); // false,添加失败,因为元素已存在
System.out.println(set.contains("xyz")); // true,元素存在
System.out.println(set.contains("XYZ")); // false,元素不存在
System.out.println(set.remove("hello")); // false,删除失败,因为元素不存在
System.out.println(set.size()); // 2,一共两个元素
}
}常见的
Set:HashSet:无序,实际上,HashSet仅仅是对HashMap的一个简单封装TreeSet:有序,它实现了SortedSet接口。
Queue
- 先进先出(FIFO)的数据结构
- 常用方法如下:

- 要避免把
null添加到队列
PriorityQueue
- 从队首获取元素时,总是获取优先级最高的元素;
- 默认按元素比较的顺序排序(必须实现
Comparable接口),也可以通过Comparator自定义排序算法(元素就不必实现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
34
35
36
37
38
39
40
41
42
43
44
45
46import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
Queue<User> q = new PriorityQueue<>(new UserComparator());
// 添加3个元素到队列:
q.offer(new User("Bob", "A1"));
q.offer(new User("Alice", "A2"));
q.offer(new User("Boss", "V1"));
System.out.println(q.poll()); // Boss/V1
System.out.println(q.poll()); // Bob/A1
System.out.println(q.poll()); // Alice/A2
System.out.println(q.poll()); // null,因为队列为空
}
}
class UserComparator implements Comparator<User> {
public int compare(User u1, User u2) {
if (u1.number.charAt(0) == u2.number.charAt(0)) {
// 如果两人的号都是A开头或者都是V开头,比较号的大小:
return u1.number.compareTo(u2.number);
}
if (u1.number.charAt(0) == 'V') {
// u1的号码是V开头,优先级高:
return -1;
} else {
return 1;
}
}
}
class User {
public final String name;
public final String number;
public User(String name, String number) {
this.name = name;
this.number = number;
}
public String toString() {
return name + "/" + number;
}
}
Deque
允许两头都进,两头都出,这种队列叫双端队列(Double Ended Queue),学名
Deque。比较queue与deque

总是调用xxxFirst()/xxxLast()以便与Queue的方法区分开;
实际Deque接口实际上扩展自Queue,但为了区分避免写queue的方法
避免添加
null
Stack
- 后进先出(LIFO:Last In First Out)的数据结构
- 操作栈的方法有:
- 把元素压栈:
push(E); - 把栈顶的元素“弹出”:
pop(E); - 取栈顶元素但不弹出:
peek(E)。在Java中,我们用Deque可以实现Stack的功能,注意只调用push()/pop()/peek()方法,避免调用Deque的其他方法;
不要使用遗留类Stack。
- 把元素压栈:
Iterator
- 一种抽象的数据访问模型
- 好处:
- 对任何集合都采用同一种访问模型;
- 调用者对集合内部结构一无所知;
- 集合类返回的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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47// Iterator
import java.util.*;
public class Main {
public static void main(String[] args) {
ReverseList<String> rlist = new ReverseList<>();
rlist.add("Apple");
rlist.add("Orange");
rlist.add("Pear");
for (String s : rlist) {
System.out.println(s);
}
}
}
class ReverseList<T> implements Iterable<T> {
private List<T> list = new ArrayList<>();
public void add(T t) {
list.add(t);
}
public Iterator<T> iterator() {
return new ReverseIterator(list.size());
}
class ReverseIterator implements Iterator<T> {
int index;
ReverseIterator(int index) {
this.index = index;
}
public boolean hasNext() {
return index > 0;
}
public T next() {
index--;
return ReverseList.this.list.get(index);
}
}
}
Collections
注意:Collections结尾多了一个s,不是Collection!
以下涉及版本JDK≥9
创建空集合
使用List.of()、Map.of()、Set.of()来创建空集合。
创建单元素集合
使用List.of(T...)、Map.of(T...)、Set.of(T...)来创建任意个元素的集合。
排序
1 | import java.util.*; |
洗牌
Collections提供了洗牌算法,即传入一个有序的List,可以随机打乱List内部元素的顺序,效果相当于让计算机洗牌:
1 | import java.util.*; |
不可变集合
Collections还提供了一组方法把可变集合封装成不可变集合:
- 封装成不可变List:
List<T> unmodifiableList(List<? extends T> list) - 封装成不可变Set:
Set<T> unmodifiableSet(Set<? extends T> set) - 封装成不可变Map:
Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m)实际上是通过创建一个代理对象,拦截掉所有修改方法实现的
线程安全集合
Collections还提供了一组方法,可以把线程不安全的集合变为线程安全的集合:
- 变为线程安全的List:
List<T> synchronizedList(List<T> list) - 变为线程安全的Set:
Set<T> synchronizedSet(Set<T> s) - 变为线程安全的Map:
Map<K,V> synchronizedMap(Map<K,V> m)
从Java 5开始,引入了更高效的并发集合类,所以上述这几个同步方法已经没有什么用了。






