Java的java.util包主要提供了三种类型的集合:List、Set、Map
Java集合的设计特点:
- 接口和实现类相分离,例如有序表的接口是List,具体的实现类有ArrayList、LinkedList等
- 支持泛型
- 访问集合总是通过迭代器
注意:避免使用历史遗留类和接口
有一小部分集合类是遗留类,不应该继续使用:
- Hashtable:一种线程安全的Map实现;
- Vector:一种线程安全的List实现;
- Stack:基于Vector实现的LIFO的栈。
还有一小部分接口是遗留接口,也不应该继续使用:
- Enumeration:已被Iterator取代。
List 有序列表
List<E>
接口主要有以下方法:
在末尾添加一个元素:boolean add(E e)
在指定索引添加一个元素:boolean add(int index, E e)
删除指定索引的元素:E remove(int index)
删除某个元素:boolean remove(Object e)
获取指定索引的元素:E get(int index)
获取链表大小(包含元素的个数):int size()
常用的实现有:ArrayList和LinkedList
ArrayList
ArrayList内部由数组实现,增删元素时会自动将其余元素挪位置,加满时会自动创建一个更大的数组,将原数组的所有元素复制过去,取代旧数组。
LinkedList
LinkedList内部由链表实现。
创建List
方法一:创建空List,用add添加元素
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(3);
或用双括号:
List<Integer> list=new ArrayList<Integer>(){{
add(1);
add(2);
add(3);
}};
方法二:使用Arrays.asList()
//创建List
List<Integer> list=Arrays.asList(1, 2, 3); //注意要用Integer
//创建元素为List的list
List<List<Integer>> list = Arrays.asList(Arrays.asList(1,2), Arrays.asList(3,4));
注意Arrays.asList()
创建的List是一个只读对象,不能再用add操作了:
List<Integer> list = List.of(12, 34, 56);
list.add(999); // UnsupportedOperationException
asList 是 java.util.Arrays 类的一个方法
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
容易犯的错误:
int[] array = {1,2,3};
List myList = Arrays.asList(array); //将array对象作为list元素了
因为asList的接口是泛型:List<T> asList(T...)
,int作为原始类型不属于Object,所以不能被作为泛类型参数。整个数组倒是一个Object,所以整个数组就用来取代T
,所以得到了一个List<int[]>
。正确的做法是传入一个Integer[]
,就能得到一个List<Integer>
了。
方法三:从Stream中创建
@Test
public void givenStream_thenInitializeList(){
List<String> list = Stream.of("foo", "bar")
.collect(Collectors.toList());
assertTrue(list.contains("foo"));
}
方法四:工厂方法 .of (Java 9)
Java 9+ 版本,可以用List.of()
取代Arrays.asList()
。
遍历List
方法一:for循环 + get(i)
for (int i=0; i<list.size(); i++) {
String s = list.get(i);
System.out.println(s);
}
不推荐这种方法,因为只对ArrayList高效,对LinkedList很低效。
方法二:迭代器Iterator (推荐!)
迭代器:
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
简化版为:
//自动使用迭代器
for (String s : list) {
System.out.println(s);
}
不同的List类型,返回的Iterator对象实现也是不同的,但总是具有最高的访问效率。
Java的for each
循环会自动使用Iterator遍历。实际上,只要实现了Iterable接口的集合类都可以直接用for each循环来遍历。
List转为Array
Integer[] array = list.toArray(new Integer[3]);
Integer[] array = list.toArray(new Integer[list.size()]);
Integer[] array = list.toArray(Integer[]::new); //函数式写法
编写equals方法
List中contains()、indexOf()等方法使用equals()方法判断两个元素是否相等。
因此,对于自定义的类型,要想正确使用List的contains()、indexOf()这些方法,放入的实例必须正确覆写equals()方法,否则,放进去的实例查找不到。
public class Person {
public String name;
public int age;
}
public boolean equals(Object o) {
if (o instanceof Person) {
Person p = (Person) o;
//引用字段用equals,基本类型字段用==
return this.name.equals(p.name) && this.age == p.age;
}
return false;
}
但上面方法中若this.name
为null
,equals()
方法就会报错。一种解决方式是手动判断是否为null
,但这显然太麻烦。一般使用Objects.equals()
静态方法即可。
标准覆写equals()
方法样例:
public boolean equals(Object o) {
if (o instanceof Person) {
Person p = (Person) o; //安全向下转型
return Objects.equals(this.name, p.name) && this.age == p.age;
}
return false;
}
Map 映射表
put(),get()分别用于添加、获取映射。
Student s = new Student("Xiao Ming", 99);
Map<String, Student> map = new HashMap<>(); //创建HashMap
map.put("Xiao Ming", s); // 添加元素
Student target = map.get("Xiao Ming"); // 查找元素
遍历Map,顺序是不确定的:
//方法一:通过key遍历,keySet()相当于python中的keys()
for (String key : map.keySet()) {
Integer value = map.get(key);
System.out.println(key + " = " + value);
}
//方法二:直接遍历key、value,entrySet()相当于python中的items()
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + " = " + value);
}
用List存储全量数据,用Map做缓存的例子:
class Students {
List<Student> list;//定义了一个Student的List,可能十分巨大
Map<String, Integer> cache;//定义了一个Map,缓存被查询频率很高的学生
Students(List<Student> list) {
this.list = list;
cache = new HashMap<>();
}
int getScore(String name) {
Integer score = this.cache.get(name);//试图从cache中查找这娃对应的成绩
if (score == null) {
score = findInList(name);
if (score!= null) {
cache.put(name, score);//加入缓存
}
}
return score == null ? -1 : score.intValue();
}
Integer findInList(String name) {
for (var ss : this.list) {
if (ss.name.equals(name)) {
return ss.score;
}
}
return null;//表示查询的人不在名单内
}
}
编写equals()和hashcode()方法
在Map的内部,对key做比较是通过equals()实现的,这一点和List查找元素需要正确覆写equals()是一样的。
所以若是自定义的对象作为key,一定要覆写equals()方法(常用的String等作为key时,已经自带覆写了equals方法)。
另外,还应覆写hashCode()
方法,它返回一个int整数。
package oop_package.src.com.itranswarp.sample;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class Main {
public static void main(String[] args) {
Map<Person, Integer> mp = new HashMap<>();
Person p1 = new Person("Bob", "Dylan", 23);
Person p2 = new Person("Bob", "Dylan", 23);
System.out.println(p1.hashCode()); //2126225543
System.out.println(p2.hashCode()); //2126225543
mp.put(p1, 100);
System.out.println(mp.get(p1)); //100
System.out.println(mp.get(p2)); //100
}
}
class Person {
String firstName;
String lastName;
int age;
public Person(String firstName, String lastName, int age){
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
@Override
public int hashCode() {
return Objects.hash(firstName, lastName, age);
}
@Override
public boolean equals(Object p) { //equals传入的参数是Object类型,若写成Person类型则无法正确覆写
if (p instanceof Person){
Person person = (Person) p;
return Objects.equals(this.lastName, person.lastName)
&& Objects.equals(this.firstName, person.firstName)
&& this.age == person.age;
}
return false;
}
}
注意覆写equals()时,传入的参数必须是Object类型。若写成Person类型则无法正确覆写!
哈希冲突解决方案
哈希散列内部是用数组存储的,数组长度当然不能和int范围一样大。
初始化内部数组默认长度16
HashMap初始化时默认的数组大小只有16,任何key,无论它的hashCode()有多大,都可以简单地通过:
int index = key.hashCode() & 0xf; // 0xf = 15
把索引确定在0~15。
按需扩容
添加超过一定数量的key-value时,HashMap会在内部自动扩容,每次扩容一倍,即长度为16的数组扩展为长度32,相应地,需要重新确定hashCode()计算的索引位置。例如,对长度为32的数组计算hashCode()对应的索引,计算方式要改为:
int index = key.hashCode() & 0x1f; // 0x1f = 31
每次扩容都要重新计算hashcode,效率很低。所以最好能提前指定HashMap的容量:
//初始容量总是2的指数倍,所以实际大小为比10000大的 16384 = 2^14
Map<String, Integer> map = new HashMap<>(10000);
冲突解决 类似矩阵邻接表
万一hash的结果相同,比如a和b都映射到了5的位置,就在那里建立一个List,包含两个Entry,一个是"a"的映射,一个是"b"的映射:
┌───┐
0 │ │
├───┤
1 │ │
├───┤
2 │ │
├───┤
3 │ │
├───┤
4 │ │
├───┤
5 │ ●─┼───> List<Entry<String, Person>>
├───┤
6 │ │
├───┤
7 │ │
└───┘
冲突的概率越大,这个List就越长,Map的get()方法效率就越低。所以应尽量保证hashcode()返回值不同。
EnumMap
若key是enum类型,就能使用EnumMap。
它在内部以一个非常紧凑的数组存储value,并且根据enum类型的key直接定位到内部数组的索引,并不需要计算hashCode(),不但效率最高,而且没有额外的空间浪费。
import java.time.DayOfWeek;
import java.util.*;
public class Main {
public static void main(String[] args) {
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
HashMap的Key无序,SortedMap的Key有序。SortedMap是个接口,其实现类是TreeMap。
┌───┐
│Map│
└───┘
▲
┌────┴─────┐
│ │
┌───────┐ ┌─────────┐
│HashMap│ │SortedMap│
└───────┘ └─────────┘
▲
│
┌─────────┐
│ TreeMap │
└─────────┘
TreeMap中key是按顺序排列的,相当被sort()过。例如String类型的key就按照字典序排列:
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
如果是自定义的class作为key,则需要实现Comparable
接口,要不没法排序啊。例如:
Map<Person, Integer> map = new TreeMap<>(new Comparator<Person>() {
public int compare(Person p1, Person p2) {
return p1.name.compareTo(p2.name);
}
});
注意:Person类并未覆写equals()和hashCode(),因为TreeMap不使用equals()和hashCode()。
有趣的例子
Comparator接口要求实现一个比较方法,它负责比较传入的两个元素a和b。
如果a<b,则返回负数,通常是-1;
如果a==b,则返回0;
如果a>b,则返回正数,通常是1;
TreeMap内部根据比较结果对Key进行排序。
TreeMap在比较两个Key是否相等时,依赖Key的compareTo()方法或者Comparator.compare()方法。若返回0就相当于判定二者equals。
package oop_package.src.com.itranswarp.sample;
import 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) {
if (p1.score == p2.score) {
return 0;
}
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))); // 2
}
}
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);
}
}
使用Properties HashTable的一种应用
Java专门为配置文件准备的Properties文件,默认扩展名是.properties
,每行以key=value
表示,以#
为注释:
Properties文件
# setting.properties
last_open_file=/data/hello.txt
auto_save_interval=60
读取Properties
String f = "setting.properties";
Properties props = new Properties(); //1. 创建Properties实例
props.load(new java.io.FileInputStream(f)); //2. 用load读取文件
String filepath = props.getProperty("last_open_file"); //3.用getProperty获取配置
String interval = props.getProperty("auto_save_interval", "120");
也可从classpath或jar包中读取:
props.load(getClass().getResourceAsStream("/common/setting.properties")); //2. 用load从classpath或jar包中读取
也可从内存读取:
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"));
打印:
course: Java
last_open_date: 2019-08-07T12:35:01
last_open_file: null
auto_save: 60
从java9开始,读取中文时,能指定编码:
Properties props = new Properties();
props.load(new FileReader("settings.properties", StandardCharsets.UTF_8));
写入Properties
Properties props = new Properties();
props.setProperty("url", "http://www.liaoxuefeng.com");
props.setProperty("language", "Java");
props.store(new FileOutputStream("C:\\conf\\setting.properties"), "这是写入的properties注释");
Set 集合 只有key的Map
支持add、contains、remove操作:
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,一共两个元素
实现原理是,只存储key、不存储value的Map。
因为Map的key是不能重复的,所以能保证Set里元素不重复。
和Map一样,也需要正确实现equals()
和hashCode()
方法,否则元素无法正确添加:
package oop_package.src.com.itranswarp.sample;
import java.util.*;
public class Main {
public static void main(String[] args) {
Set<Person> set = new HashSet<>();
Person p1 = new Person("Bob", "Dylan", 23);
Person p2 = new Person("Bob", "Dylan", 23);
System.out.println(set.add(p1)); //true
System.out.println(set.add(p2)); //false
}
}
class Person {
String firstName;
String lastName;
int age;
public Person(String firstName, String lastName, int age){
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
@Override
public int hashCode() {
return Objects.hash(firstName, lastName, age);
}
@Override
public boolean equals(Object p) {
if (p instanceof Person){
Person person = (Person) p;
return Objects.equals(this.lastName, person.lastName)
&& Objects.equals(this.firstName, person.firstName)
&& this.age == person.age;
}
return false;
}
}
TreeSet
与TreeMap类似,是SortedSet接口的实现,是有序set。
定义自定义类型的TreeSet时必须传入一个Comparator
对象用于指定排序规则。
package oop_package.src.com.itranswarp.sample;
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Message> received = Arrays.asList(
new Message(1, "Hello!"),
new Message(2, "发工资了吗?"),
new Message(2, "发工资了吗?"),
new Message(3, "去哪吃饭?"),
new Message(3, "去哪吃饭?"),
new Message(4, "Bye")
);
List<Message> displayMessages = process(received);
for (Message message : displayMessages) {
System.out.println(message.text);
}
}
static List<Message> process(List<Message> received) {
Set<Message> st = new TreeSet<>(new Comparator<Message>() {
//<0,o1在前;
//>0,o1在后;
//=0,二者相等;
@Override
public int compare(Message o1, Message o2) {
return o1.sequence - o2.sequence; //排序函数编写技巧
}});
for (Message m : received){
st.add(m);
}
return new ArrayList<>(st);
}
}
class Message {
public final int sequence;
public final String text;
public Message(int sequence, String text) {
this.sequence = sequence;
this.text = text;
}
}
打印:
Hello!
发工资了吗?
去哪吃饭?
Bye
Queue 队列
Queue接口定义了两组方法:
int size():获取队列长度;
boolean add(E) / boolean offer(E):添加元素到队尾;
E remove() / E poll():获取队首元素并从队列中删除;
E element() / E peek():获取队首元素但并不从队列中删除。
前面一组会抛异常,后面一组不抛异常而是返回false或null。
使用LinkedList
类即可,它既实现了List接口,又实现了Queue接口:
// 这是一个List:
List<String> list = new LinkedList<>();
// 这是一个Queue:
Queue<String> queue = new LinkedList<>();
面向抽象编程的妙处。
PriorityQueue 优先队列
PriorityQueue和Queue的区别在于,它的出队顺序与元素的优先级有关,对PriorityQueue调用remove()或poll()方法,返回的总是优先级最高的元素。
String类型的PriorityQueue,默认按字典序排序,字典序越靠前优先级越高:
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,因为队列为空
若是存放自定义类型,则需要实现Comparable接口:
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 Integer.valueOf(u1.number.substring(1)) - Integer.valueOf(u2.number.substring(1));
}
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 双端队列
Deque提供了分别向队首队尾增删元素的方法:
addFirst(E e) / offerFirst(E e)
addLast(E e) / offerLast(E e)
E removeFirst() / E pollFirst()
E removeLast() / E pollLast()
E getFirst() / E peekFirst()
E getLast() / E peekLast()
它的实现类有ArrayDeque
和LinkedList
。
LinkedList
yyds!
用deque时应调用xxxFirst()/xxxLast()以便与Queue的方法区分开。(Queue里的方法如poll它也都能调)
Stack 栈
用Deque实现Stack,把往双向队列的队头作为栈顶:
把元素压栈:push(E)/addFirst(E);
把栈顶的元素“弹出”:pop()/removeFirst();
取栈顶元素但不弹出:peek()/peekFirst()。
把Deque作为Stack使用时,注意只调用push()/pop()/peek()方法,不要调用addFirst()/removeFirst()/peekFirst()方法,这样代码更加清晰。
栈的应用:函数调用
函数调用就是靠栈,JVM的做法是:
- 调用一个方法时,先将参数压栈,然后执行对应的方法;
- 当方法返回时,返回值压栈,调用方法通过出栈操作获得方法返回值。
栈的应用:中缀表达式
前缀表达式(例:+ 3 4 )
后缀表达式(例:3 4 + )
中缀表达式(例:3 + 4)
运算符在中间的表达式叫中缀表达式。中缀表达式不容易被电脑解析,但仍被许多程序语言使用,因为它符合人们的普遍用法。 与前缀或后缀记法不同的是,中缀记法中括号是必需的。 计算过程中必须用括号将操作符和对应的操作数括起来,用于指示运算的次序。
编译器会把中缀表达式转为后缀表达式如:1 + 2 * (9 - 5) => 1 2 9 5 - * +
计算后缀表达式用到了栈:
//首先准备一个空的栈:
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└───┘
//依次扫描后缀表达式1 2 9 5 - * +,遇到数字1,就直接扔到栈里:
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ 1 │
└───┘
//紧接着,遇到数字2,9,5,也扔到栈里:
│ │
│ 5 │
│ │
│ 9 │
│ │
│ 2 │
│ │
│ 1 │
└───┘
//接下来遇到减号时,弹出栈顶的两个元素,并计算9-5=4,把结果4压栈:
│ │
│ │
│ │
│ 4 │
│ │
│ 2 │
│ │
│ 1 │
└───┘
//接下来遇到*号时,弹出栈顶的两个元素,并计算2*4=8,把结果8压栈:
│ │
│ │
│ │
│ │
│ │
│ 8 │
│ │
│ 1 │
└───┘
接下来遇到+号时,弹出栈顶的两个元素,并计算1+8=9,把结果9压栈:
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ 9 │
└───┘
//扫描结束后,没有更多的计算了,弹出栈的唯一一个元素,得到计算结果9。
例子:10进制转16进制
package oop_package.src.com.itranswarp.sample;
import java.util.*;
public class Main {
public static void main(String[] args) {
String hex = toHex(12500);
if (hex.equalsIgnoreCase("30D4")) {
System.out.println("测试通过");
} else {
System.out.println("测试失败");
}
}
static String toHex(int n) {
Deque<Character> st = new LinkedList<Character>();
while (n != 0){
st.push(int2hex(n % 16));
n /= 16;
}
StringBuilder sb = new StringBuilder();
while (!st.isEmpty()){
sb.append(st.pop());
}
return sb.toString();
}
static char int2hex(int n){
if (n < 10){
return (char)(n + '0');
}
else{
return (char)(n - 10 + 'A');
}
}
}
使用Iterator
Iterator是一种抽象的数据访问模型。使用Iterator模式进行迭代的好处有:
- 对任何集合都采用同一种访问模型;
- 调用者对集合内部结构一无所知;
- 集合类返回的Iterator对象知道如何迭代。
Java提供了标准的迭代器模型,即集合类实现java.util.Iterable接口,返回java.util.Iterator实例。
自己实现倒序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);
}
@Override
public Iterator<T> iterator() { //为了能用for each,集合类需实现Iterable接口,该接口要求返回一个Iterator对象;
return new ReverseIterator(list.size());
}
class ReverseIterator implements Iterator<T> { //具体Iterator对象
int index;
ReverseIterator(int index) {
this.index = index;
}
@Override
public boolean hasNext() {
return index > 0;
}
@Override
public T next() {
index--;
return ReverseList.this.list.get(index); //ReverseList.this获得当前外部类的this引用,通过它可以访问ReverseList的所有字段和方法。直接用this,只能指向内部ReverseIterator。
}
}
}
Collections 工具类
位于java.util包中的工具类,提供了一系列静态方法,能更方便地操作各种集合。
创建集合
创建空List:List<T> emptyList()
创建空Map:Map<K, V> emptyMap()
创建空Set:Set<T> emptySet()
返回的空集合是不可变集合,无法向其中添加或删除元素。
创建一个单元素集合:
创建一个元素的List:List<T> singletonList(T o)
创建一个元素的Map:Map<K, V> singletonMap(K key, V value)
创建一个元素的Set:Set<T> singleton(T o)
把可变集合转为不可变集合,创建代理对象拦截修改方法实现的:
封装成不可变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)
sort和shuffle
Collections.sort(list); //对list内部排序
Collections.shuffle(list); //对list内部随机打乱顺序