集合
Collection
在java中,如果一个Java对象可以在内部持有若干其他java对象,并对外提供访问接口,把这种java对象称为集合。显然,java数组可以看作一种集合
String[] ss = new String[10]; // 可以持有10个String对象
ss[0] = "Hello"; // 可以放入String对象
String first = ss[0]; // 可以获取String对象
数组的限制
- 数组初始化后大小不变
- 数组只能按索引顺序存取
Collection它是除Map外所有其他集合类的根接口,java的java.util包主要提供了以下三种类型的集合:
List:一种有序列表的集合,其中元素可以重复,典型的包括ArrayList和LinkedListList提供了基于索引的访问方式,可以方便的获取,更新和插入元素
Set:一种保证没有重复元素的无序集合,常见的实现有HashSet、LinkedHashSet和TreeSetHashSet使用哈希表存储元素,提供快速存取。TreeSet实现了SortedSet接口,元素以自然顺序或者指定的比较器排序。
Map:一种通过键值(key-value)查找的映射表集合Map键是唯一的,但值可以重复,典型的实现包括HashMap和TreeMapHashMap提供了快速的键值对存取,是最常用的Map实现之一。TreeMap实现了NavigableMap,键按自然顺序或指定的比较器排序 有序的。
java集合的设计特点:
- 实现了接口与实现类相分离,例如,有序表的接口是
List,具体的实现类有ArrayList,LinkedList等 - 支持泛型,可以限制在一个集合中只能放入同一种类型的元素
- java集合使用统一的
Iterator遍历
遗留类:
Hashtable:一种线程安全的Map实现;Vector:一种线程安全的List实现;Stack:基于Vector实现的LIFO的栈。
遗留接口:
Enumeration<E>:已被Iterator<E>取代。
List
是一种有序列表
List的行为和数组几乎完全相同:内部按照放入元素的先后顺序存放,每个元素都可以通过索引确定自己的位置,
List的索引和数组一样,从0开始
List允许添加null
常用的API
- 在末尾添加一个元素:
boolean add(E e) - 在指定索引添加一个元素:
boolean add(int index,E e) - 删除指定索引的元素:
E remove(int index) - 删除某个元素:
boolean remove(Object e) - 获取指定索引的元素:
E get(int index) - 获取链表大小(包含元素的个数):
int size() - 判断List是否包含某个指定元素 :
boolean contains(Object o) - 返回某个元素的索引:
int indexOf(Object o)如果元素不存在,就返回-1
对比ArrayList 和 LinkedList
| ArrayList | LinkedList | |
|---|---|---|
| 获取指定元素 | 速度很快 | 需要从头开始查找元素 |
| 添加元素到末尾 | 速度很快 | 速度很快 |
| 在指定位置添加/删除 | 需要移动元素 | 不需要移动元素 |
| 内存占用 | 少 | 较大 |
通常情况下,总是优先使用ArrayList
创建List
除了使用ArrayList 和 LinkedList,还可以通过List接口提供的of()方法创建List
List<Integer> list = List.of(1, 2, 5);
List.of()方法不接受null值,如果传入null,会抛出NullPointerException异常
遍历List
可以使用for循环根据索引配合get(int)方法遍历,但是不推荐,一是代码复杂,二是因为get(int)方法只有ArrayList的实现是高效的,换成LinkedList后,索引越大,访问速度越慢。
推荐使用Iterator 迭代器的方式遍历List,效率更高,简写方式for each
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
//hasNext() 判断是否还有下一个元素
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next(); // 返回下一个元素
System.out.println(s);
}
}
}
equals方法
equals()方法的正确编写
-
先确定实例相等的逻辑,即哪些字段相等,就认为实例相等
-
用
instanceof判断传入的待比较的object是不是当前类型,如果是,继续比较,否则,返回false -
对引用类型用
Objects.equals()比较,对基本类型直接用==比较。使用
Objects.equals()比较两个引用类型是否相等的目的是省去了判断null的麻烦。两个引用类型都是null时它们也是相等的。 -
如果不调用
List的contains()、indexOf()这些方法,那么放入的元素就不需要实现equals()方法。
Map
Map<K, V>是一种键-值映射表,当我们调用put(K key, V value)方法时,就把key和value做了映射并放入Map。当我们调用V get(K key)时,就可以通过key获取到对应的value。如果key不存在,则返回null。和List类似,Map也是一个接口,最常用的实现类是HashMap。
如果只想知道这个key是否存在,可以调用boolean containsKey(K key)f方法
Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉
遍历Map
Map 存储的是映射关系,并且不保证顺序,在遍历的时候即不一定是put()时放入的key的顺序,也不一定是key的排列顺序。
使用for each循环遍历Map实例的keySet()方法返回的Set集合,它包含不重复的key的集合
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);
}
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);
}
hashCode方法
HashMap之所以能根据key直接拿到value,原因是它内部通过空间换时间的方法,用一个大数组存储所有value,并根据key直接计算出value应该存储在哪个索引
正确使用Map必须保证:
- 作为
key的对象必须正确覆写equals()方法。相等的两个key实例调用equals方法必须返回true - 必须正确重写
hashCode()方法,且hashcode方法严格遵循以下规范:- 如果两个对象相等,则两个对象的
hashCode()必须相等; - 如果两个对象不相等,则两个对象的
hashCode()尽量不要相等。
- 如果两个对象相等,则两个对象的
- 即对应两个实例
a和b:- 如果
a和b相等,那么a.equals(b)一定为true,则a.hashCode()必须等于b.hashCode(); - 如果
a和b不相等,那么a.equals(b)一定为false,则a.hashCode()和b.hashCode()尽量不要相等。
- 如果
如果添加超过16个
key-value到HashMap,数组不够用了怎么办?实际上
HashMap初始化时默认的数组大小只有16,任何key,无论它的hashCode()有多大,都可以简单地通过:int index = key.hashCode() & 0xf; // 0xf = 15把索引确定在0 ~ 15,即永远不会超出数组范围,上述算法只是一种最简单的实现。
如果添加超过16个
key-value到HashMap,数组不够用了怎么办?添加超过一定数量的
key-value时,HashMap会在内部自动扩容,每次扩容一倍,即长度为16的数组扩展为长度32,相应地,需要重新确定hashCode()计算的索引位置。例如,对长度为32的数组计算hashCode()对应的索引,计算方式要改为:int index = key.hashCode() & 0x1f; // 0x1f = 31由于扩容会导致重新分布已有的
key-value,所以,频繁扩容对HashMap的性能影响很大。如果我们确定要使用一个容量为10000个key-value的HashMap,更好的方式是创建HashMap时就指定容量:Map<String, Integer> map = new HashMap<>(10000);虽然指定容量是
10000,但HashMap内部的数组长度总是2n,因此,实际数组长度被初始化为比10000大的16384(214)。
EnumMap
因为HashMap是一种通过对key计算hashCode(),通过空间换时间的方式,直接定位到value所在的内部数组的索引,因此,查找效率非常高。
如果key的对象是enum类型,那么,还可以使用java集合库提供的一种EnumMap,它在内部以一个非常紧凑的数组存储value,并且根据enum类型的key直接定位到内部数组的索引,并不需要计算hashCode(),不但效率最高,而且没有额外的空间浪费。
Map<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class);
map.put(DayOfWeek.MONDAY, "星期一");
map.put(DayOfWeek.TUESDAY, "星期二");
map.put(DayOfWeek.WEDNESDAY, "星期三");
map.put(DayOfWeek.THURSDAY, "星期四");
map.put(DayOfWeek.FRIDAY, "星期五");
map.put(DayOfWeek.SATURDAY, "星期六");
map.put(DayOfWeek.SUNDAY, "星期日");
System.out.println(map);
System.out.println(map.get(DayOfWeek.MONDAY));
TreeMap
SortedMap在内部会对Key进行排序,SortedMap是接口,它的实现类是TreeMap
作为
SortedMap的Key必须实现Comparable接口,或者传入Comparator;要严格按照
compare()规范实现比较逻辑,否则,TreeMap将不能正常工作。
使用TreeMap时,放入的Key必须实现Comparable接口。String、Integer这些类已经实现了Comparable接口,因此可以直接作为Key使用。作为Value的对象则没有任何要求。
如果作为Key的class没有实现Comparable接口,那么,必须在创建TreeMap时同时指定一个自定义排序算法:
import 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 + "}";
}
// TreeMap 不需要重写 equals 和 hashCode ,因为TreeMap不使用
}
注意到Comparator接口要求实现一个比较方法,它负责比较传入的两个元素a和b,如果a<b,则返回负数,通常是-1,如果a==b,则返回0,如果a>b,则返回正数,通常是1。TreeMap内部根据比较结果对Key进行排序。
使用Properties
# 上次最后打开的文件:
last_open_file=/data/hello.txt
# 自动保存文件的时间间隔:
auto_save_interval=60
配置文件的特点是,它的key-value一般都是String-String类型的,因此我们完全可以用Map<Stirng,String>来表示
properties内部本质是一个Hashtable,但我们只需要用到Properties自身关于读写配置的接口
读取配置文件
使用Properties读取配置文件非常简单,java默认配置文件以.Properties为扩展名,每一行以key -- value表示,以 # 开头的是注释
# setting.properties
last_open_file=/data/hello.txt
auto_save_interval=60
可以从文件系统读取这个.properties文件
String 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"); // 提供默认值 key不存在时返回默认值
可见,用properties读取配置文件,一共三步:
- 创建
properties实例 - 调用
load()读取文件 - 调用
getProperty()获取配置,如果key不存在则返回null,可以提供默认值,key不存在时返回默认值
也可以从classpath读取.properties文件,因为load(InputStream)方法接收一个InputStream实例,表示一个字节流,它不一定是文件流,也可以是从jar包中读取的资源流:
Properties props = new Properties();
props.load(getClass().getResourceAsStream("/common/setting.properties"));
// 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"));
}
}
Properties设计的目的是存储String类型的key-value,但Properties实际上是从Hashtable派生的,它的设计实际上是有问题的,但是为了保持兼容性,现在已经没法修改了。除了getProperty()和setProperty()方法外,还有从Hashtable继承下来的get()和put()方法,这些方法的参数签名是Object,我们在使用Properties的时候,不要去调用这些从Hashtable继承下来的方法。
写入配置文件
如果通过setProperty()修改了Properties实例,可以把配置写入文件,以便下次启动时获得最新配置。写入配置文件使用store()方法:
Properties props = new Properties();
props.setProperty("url", "http://www.liaoxuefeng.com");
props.setProperty("language", "Java");
props.store(new FileOutputStream("C:\\conf\\setting.properties"), "这是写入的properties注释");
编码
早期版本的Java规定.properties文件编码是ASCII编码(ISO8859-1),如果涉及到中文就必须用name=\u4e2d\u6587来表示,非常别扭。从JDK9开始,Java的.properties文件可以使用UTF-8编码了。
不过,需要注意的是,由于load(InputStream)默认总是以ASCII编码读取字节流,所以会导致读到乱码。我们需要用另一个重载方法load(Reader)读取:
Properties props = new Properties();
props.load(new FileReader("settings.properties", StandardCharsets.UTF_8));
就可以正常读取中文。InputStream和Reader的区别是一个是字节流,一个是字符流。字符流在内存中已经以char类型表示了,不涉及编码问题。
Set
Set相当于只存储key,不存储value的Map,我们经常用set用于去除重复元素
放入Set的元素和Map的key类似,都要正确实现equals()和hashCode()方法,否则该元素无法正确地放入Set
最常见的实现类是HashSet
Set用于存储不重复的元素集合,它主要提供以下几个方法:
- 将元素添加进
Set<E>:boolean add(E e) - 将元素从
Set<E>删除:boolean remove(Object e) - 判断是否包含元素:
boolean contains(Object e)
import 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接口并不保证有序,而SortedSet接口则保证元素是有序的:
HashSet是无序的,因为它实现了Set接口,并没有实现SortedSet接口;TreeSet是有序的,因为它实现了SortedSet接口。
使用TreeSet和TreeMap的要求一样,添加的元素必须正确实现Comparable接口,如果没有实现Comparable接口,那么创建TreeSet时必须传入一个Comparator对象。
Queue
Queue实际上是实现了一个先进先出(FIFO:First In First Out)的有序表。它和List的区别在于,List可以在任意位置添加和删除元素,而Queue只有两个操作:
- 把元素添加到队列末尾;
- 从队列头部取出元素。
在java的标准库中,队列接口Queue定义了以下几个方法:
int size():获取队列长度;boolean add(E)/boolean offer(E):添加元素到队尾;E remove()/E poll():获取队首元素并从队列中删除;E element()/E peek():获取队首元素但并不从队列中删除。
对于具体的实现类,有的Queue有最大队列长度限制,有的Queue没有。注意到添加、删除和获取队列元素总是有两个方法,这是因为在添加或获取元素失败时,这两个方法的行为是不同的。总结如下:
| throw Exception | 返回false或null | |
|---|---|---|
| 添加元素到队尾 | add(E e) | boolean offer(E e) |
| 取队首元素并删除 | E remove() | E poll() |
| 取队首元素但不删除 | E element() | E peek() |
举个栗子,假设我们有一个队列,对它做一个添加操作,如果调用add()方法,当添加失败时(可能超过了队列的容量),它会抛出异常:
Queue<String> q = ...
try {
q.add("Apple");
System.out.println("添加成功");
} catch(IllegalStateException e) {
System.out.println("添加失败");
}
如果我们调用offer()方法来添加元素,当添加失败时,它不会抛异常,而是返回false:
Queue<String> q = ...
if (q.offer("Apple")) {
System.out.println("添加成功");
} else {
System.out.println("添加失败");
}
当我们需要从Queue中取出队首元素时,如果当前Queue是一个空队列,调用remove()方法,它会抛出异常:
Queue<String> q = ...
try {
String s = q.remove();
System.out.println("获取成功");
} catch(IllegalStateException e) {
System.out.println("获取失败");
}
如果我们调用poll()方法来取出队首元素,当获取失败时,它不会抛异常,而是返回null:
Queue<String> q = ...
String s = q.poll();
if (s != null) {
System.out.println("获取成功");
} else {
System.out.println("获取失败");
}
因此,两套方法可以根据需要来选择使用。
注意:不要把
null添加到队列中,否则poll()方法返回null时,很难确定是取到了null元素还是队列为空。
import java.util.LinkedList;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
Queue<String> q = new LinkedList<>();
// 添加3个元素到队列:
q.offer("apple");
q.offer("pear");
q.offer("banana");
// 从队列取出元素:
System.out.println(q.poll()); // apple
System.out.println(q.poll()); // pear
System.out.println(q.poll()); // banana
System.out.println(q.poll()); // null,因为队列是空的
}
}
从上面的代码中,我们还可以发现,LinkedList即实现了List接口,又实现了Queue接口,但是,在使用的时候,如果我们把它当作List,就获取List的引用,如果我们把它当作Queue,就获取Queue的引用:
// 这是一个List:
List<String> list = new LinkedList<>();
// 这是一个Queue:
Queue<String> queue = new LinkedList<>();
PriorityQueue 优先级队列
PriorityQueue和Queue的区别在于,它的出队顺序与元素的优先级有关,对PriorityQueue调用remove()或poll()方法,返回的总是优先级最高的元素。
要使用PriorityQueue,我们就必须给每个元素定义“优先级”。我们以实际代码为例,先看看PriorityQueue的行为:
import java.util.PriorityQueue;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
Queue<String> q = new PriorityQueue<>();
// 添加3个元素到队列:
q.offer("apple");
q.offer("pear");
q.offer("banana");
System.out.println(q.poll()); // apple
System.out.println(q.poll()); // banana
System.out.println(q.poll()); // pear
System.out.println(q.poll()); // null,因为队列为空
//输出的顺序按照了首字母排序
}
}
因此,放入PriorityQueue的元素,必须实现Comparable接口,PriorityQueue会根据元素的排序顺序决定出队的优先级。
如果我们要放入的元素并没有实现Comparable接口怎么办?PriorityQueue允许我们提供一个Comparator对象来判断两个元素的顺序。我们以银行排队业务为例,实现一个PriorityQueue:
import 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;
}
}
实现PriorityQueue的关键在于提供的UserComparator对象,它负责比较两个元素的大小(较小的在前)。UserComparator总是把V开头的号码优先返回,只有在开头相同的时候,才比较号码大小。
上面的UserComparator的比较逻辑其实还是有问题的,它会把A10排在A2的前面,请尝试修复该错误。
Deque 双端队列
Java集合提供了接口Deque来实现一个双端队列,它的功能是:
- 既可以添加到队尾,也可以添加到队首;
- 既可以从队首获取,又可以从队尾获取。
我们来比较一下Queue和Deque出队和入队的方法:
| Queue | Deque | |
|---|---|---|
| 添加元素到队尾 | add(E e) / offer(E e) | addLast(E e) / offerLast(E e) |
| 取队首元素并删除 | E remove() / E poll() | E removeFirst() / E pollFirst() |
| 取队首元素但不删除 | E element() / E peek() | E getFirst() / E peekFirst() |
| 添加元素到队首 | 无 | addFirst(E e) / offerFirst(E e) |
| 取队尾元素并删除 | 无 | E removeLast() / E pollLast() |
| 取队尾元素但不删除 | 无 | E getLast() / E peekLast() |
对于添加元素到队尾的操作,Queue提供了add()/offer()方法,而Deque提供了addLast()/offerLast()方法。添加元素到队首、取队尾元素的操作在Queue中不存在,在Deque中由addFirst()/removeLast()等方法提供。
注意到Deque接口实际上扩展自Queue:
public interface Deque<E> extends Queue<E> {
...
}
因此,Queue提供的add()/offer()方法在Deque中也可以使用,但是,使用Deque,最好不要调用offer(),而是调用offerLast():
import java.util.Deque;
import java.util.LinkedList;
public class Main {
public static void main(String[] args) {
Deque<String> deque = new LinkedList<>();
deque.offerLast("A"); // A
deque.offerLast("B"); // A <- B
deque.offerFirst("C"); // C <- A <- B
System.out.println(deque.pollFirst()); // C, 剩下A <- B
System.out.println(deque.pollLast()); // B, 剩下A
System.out.println(deque.pollFirst()); // A
System.out.println(deque.pollFirst()); // null
}
}
如果直接写deque.offer(),我们就需要思考,offer()实际上是offerLast(),我们明确地写上offerLast(),不需要思考就能一眼看出这是添加到队尾。
因此,使用Deque,推荐总是明确调用offerLast()/offerFirst()或者pollFirst()/pollLast()方法。
Deque是一个接口,它的实现类有ArrayDeque和LinkedList。
我们发现LinkedList真是一个全能选手,它即是List,又是Queue,还是Deque。但是我们在使用的时候,总是用特定的接口来引用它,这是因为持有接口说明代码的抽象层次更高,而且接口本身定义的方法代表了特定的用途。
// 不推荐的写法:
LinkedList<String> d1 = new LinkedList<>();
d1.offerLast("z");
// 推荐的写法:
Deque<String> d2 = new LinkedList<>();
d2.offerLast("z");
可见面向抽象编程的一个原则就是:尽量持有接口,而不是具体的实现类。
Stack
栈(Stack)是一种后进先出(LIFO:Last In First Out)的数据结构。
Stack只有入栈和出栈的操作:
- 把元素压栈:
push(E); - 把栈顶的元素“弹出”:
pop(); - 取栈顶元素但不弹出:
peek()。
在Java中,我们用Deque可以实现Stack的功能:
- 把元素压栈:
push(E)/addFirst(E); - 把栈顶的元素“弹出”:
pop()/removeFirst(); - 取栈顶元素但不弹出:
peek()/peekFirst()。
为什么Java的集合类没有单独的Stack接口呢?因为有个遗留类名字就叫Stack,出于兼容性考虑,所以没办法创建Stack接口,只能用Deque接口来“模拟”一个Stack了。
当我们把Deque作为Stack使用时,注意只调用push()/pop()/peek()方法,不要调用addFirst()/removeFirst()/peekFirst()方法,这样代码更加清晰。
Iterator
java的集合类都可以使用for each循环,实际上,java编译器并不知道如何遍历List,编译器把for each循环通过Iterator改写为了普通的for循环:
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
这种通过Iterator对象遍历集合的模式称为迭代器。
使用迭代器的好处在于,调用方总是以统一的方式遍历各种集合类型,而不必关心它们内部的存储结构。
Iterator是一种抽象的数据访问模型。使用Iterator模式进行迭代的好处有:
- 对任何集合都采用同一种访问模型;
- 调用者对集合内部结构一无所知;
- 集合类返回的
Iterator对象知道如何迭代。
Java提供了标准的迭代器模型,即集合类实现java.util.Iterable接口,返回java.util.Iterator实例。
Collections
Collections是JDK提供的工具类,同样位于java.util包中。它提供了一系列静态方法,能更方便地操作各种集合。
创建空集合
对于旧版的JDK,可以使用Collections提供的一系列方法来创建空集合:
- 创建空List:
List<T> emptyList() - 创建空Map:
Map<K, V> emptyMap() - 创建空Set:
Set<T> emptySet()
要注意到返回的空集合是不可变集合,无法向其中添加或删除元素。
线程安全集合
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开始,引入了更高效的并发集合类,所以上述这几个同步方法已经没有什么用了。

浙公网安备 33010602011771号