Java——集合

文章目录

  • 五、集合
    • 1、集合简介
    • 2、List
      • 2.1 ArrayList
      • 2.2 LinkedList
      • 2.3 区别
      • 2.4 遍历List
      • 2.5 List和Array转换
        • List -> Array
        • Array -> List
    • 3、编写equals方法
    • 4、Map
      • 4.1 遍历Map
    • 5、编写equals和hashCode
    • 6、EnumMap
    • 7、TreeMap
    • 8、Properties
    • 9、Set
      • 9.1 TreeSet
    • 10、Queue
      • 10.1 LinkedList
    • 11、PriorityQueue
    • 12、Deque
    • 13、Stack
    • 14、Collections
      • 14.1 创建空集合
      • 14.2 创建单元素集合
      • 14.3 排序
      • 14.4 洗牌
      • 14.5 不可变集合

五、集合

1、集合简介

在Java中,如果一个Java对象可以在内部持有若干其他Java对象,并对外提供访问接口,我们把这种Java对象称为集合。很显然,Java的数组可以看作是一种集合。

String[] ss = new String[10]; // 可以持有10个String对象
ss[0] = "Hello"; // 可以放入String对象
String first = ss[0]; // 可以获取String对象

为什么在又了数组这种数据类型后,还需要其他集合类?

因为数组有如下的限制:

  • 数组初始化后的大小不能改变;
  • 数组只能按索引顺序存取。

因此,我们需要各种不同的集合来满足不同的需求。例如:

  • 可变大小的顺序链表;
  • 保证无重复元素的集合;

Collection

Java标准库自带的java.util包提供了集合类:Collection,它是除Map外所有其他集合类的根接口。

Java的java.util包主要提供了以下三种类型的集合:

  • List :一种有序列的集合;
  • Set :一种保证没有重复元素的集合;
  • Map :一种通过键值查找的映射表集合。

Java集合的设计有几个特点:

  1. 实现了接口和实现类相分离,例如,有序表的接口是List,具体的实现类有ArrayListLinkedList等;
  2. 支持泛型,可以限制在一个集合中只放入同一种数据类型的元素。
  3. 访问集合时总是通过统一的方式——迭代器(Iterator)来实现,它最明显的好处在于无需知道集合内部元素是按什么方式存储的。

由于Java的集合设计非常久远,中间经历过大规模改进,我们要注意到有一小部分集合类是遗留类,不应该继续使用:

  • Hashtable:一种线程安全的Map实现;
  • Vector:一种线程安全的List实现;
  • Stack:基于Vector实现的LIFO的栈。

还有一小部分接口是遗留接口,也不应该继续使用:

  • Enumeration:已被Iterator取代。

2、List

List是最基础的一种集合:它是一种有序链表。

List 的行为和数组几乎完全相同:List 内部按照放入元素的先后顺序存放,每个元素都可以通过索引值确定自己的位置。

2.1 ArrayList

在实际应用中,需要增删元素的有序列表,因此我们使用最多的是ArrayList

实际上,ArrayList 在内部使用了数组来存储所有元素。它的处理和数组的使用类似,例如,一个ArrayList拥有5个元素,实际数组大小为6(即有一个空位):

size=5
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ E │   │
└───┴───┴───┴───┴───┴───┘

当添加一个元素并指定索引到ArrayList时,ArrayList自动移动需要移动的元素:

size=5
┌───┬───┬───┬───┬───┬───┐
│ A │ B │   │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘

然后,往内部指定索引的数组位置添加一个元素,然后把size1

size=6
┌───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │
└───┴───┴───┴───┴───┴───┘

继续添加元素,但是数组已满,没有空闲位置的时候,ArrayList先创建一个更大的新数组,然后把旧数组的所有元素复制到新数组,紧接着用新数组取代旧数组:

size=6
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │   │   │   │   │   │   │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘

现在,新数组就有了空位,可以继续添加一个元素到数组末尾,同时size1

size=7
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ F │ C │ D │ E │ G │   │   │   │   │   │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘

2.2 LinkedList

LinkedList通过“链表”也实现了List接口。在LinkedList中,它的内部每个元素都指向下一个元素:

        ┌───┬───┐   ┌───┬───┐   ┌───┬───┐   ┌───┬───┐
HEAD ──>│ A │ ●─┼──>│ B │ ●─┼──>│ C │ ●─┼──>│ D │   │
        └───┴───┘   └───┴───┘   └───┴───┘   └───┴───┘

2.3 区别

ArrayList LinkedList
获取指定元素 速度很快 需要从头开始查找元素
添加元素到末尾 速度很快 速度很快
在指定位置添加/删除 需要移动元素 不需要移动元素
内存占用 较大

通常情况下,我们总是优先使用ArrayList

2.4 遍历List

   @Test
    public void m0() {
        List<String> list = new ArrayList<>();
        list.add("apple");
        list.add("pear");
        list.add("orange");

        //方法一:通过索引遍历,不推荐
        for (int i = 0; i < list.size(); ++i) {
            String str = list.get(i);
            System.out.println(str);
        }

        //方法二:通过Iterator遍历
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        //方法三:for earch方法遍历
        for (String str : list) {
            System.out.println(str);
        }

    }

上面通过三种方法遍历了List:

  • 第一种方法并不推荐,一是因为代码复杂,二是因为get(i) 方法只有ArrayList的实现是高效的,换成LinkedList后,索引越大,访问速度越慢。
  • 第二种方法:使用迭代器Iterator来访问ListIterator本身也是一个对象,但它是由List的实例调用iterator()方法的时候创建的。Iterator对象知道如何遍历一个List,并且不同的List类型,返回的Iterator对象实现也是不同的,但总是具有最高的访问效率。
  • 第三种方法:Java编译器会自动把for each循环变成Iterator的调用。

需要记住,通过Iterator遍历List永远是最高效的方式。

2.5 List和Array转换

List -> Array

Array -> List

对于JDK 11之前的版本,可以使用Arrays.asList(T...)方法把数组转换成List。但返回的是一个只读 List

		@Test
    public void m1() {
        Integer[] array = { 1, 2, 3 };
        List<Integer> list = Arrays.asList(array);
        for (int i : list) {
            System.out.println(i);
        }

        list.add(5);    //java.lang.UnsupportedOperationException
    }

对于高版本的,可以通过List.of(T...)方法转换,返回的也是只读 List

Integer[] array = { 1, 2, 3 };
List<Integer> list = List.of(array);

3、编写equals方法

List 中提供了有两个方法:

  • boolean contains(Object o) 方法判断 List 是否包含某个指定元素;
  • int indexOf(Object o) 方法返回某个元素的索引,如果元素不存在,返回 -1

下面是一个例子:

		@Test
    public void m3() {

        List<String> list = new ArrayList<>();
        list.add("apple");
        list.add("pear");
        list.add("orange");
        System.out.println(list.contains(new String("apple")));	//true
        System.out.println(list.contains(new String("banana")));	//false
        System.out.println(list.indexOf(new String("apple")));	//0
        System.out.println(list.indexOf(new String("banana")));	//-1
    }

虽然传入的 new String("apple")List 中的 apple 是不同的实例。但是仍然得到了true。这是因为在 List 内部并不是通过 == 来判断两个元素是否相等,而是通过 equals() 方法判断两个元素是否相等。源码如下:

/*ArrayList.java*/
public boolean contains(Object o) {
  return indexOf(o) >= 0;
}

public int indexOf(Object o) {
  if (o == null) {
    for (int i = 0; i < size; i++)
      if (elementData[i]==null)
        return i;
  } else {
    for (int i = 0; i < size; i++)
      if (o.equals(elementData[i]))
        return i;
  }
  return -1;
}

因此,要正确使用 Listcontains()indexOf()这些方法,必须要覆写对象的equals()方法。

之所以放入StringInteger这些对象能够得到预期结果,是因为Java标准库定义的这些类已经正确实现了equals()方法。

例如,在 Student 类没有编写 equals 方法时,不能得到正确结果。

@Test
public void m3() {
  List<Student> students = new ArrayList<>();

  Student stu1 = new Student("zhangsan");
  students.add(stu1);

  Student stu2 = new Student("lisi");
  students.add(stu2);

  Student stu3 = new Student("wangwu");
  students.add(stu3);

  System.out.println(students.contains(new Student("zhangsan")));	//false
}

编写equals() 方法,必须满足的条件:

  • 自反性(Reflexive):对于非nullx来说,x.equals(x)必须返回true
  • 对称性(Symmetric):对于非nullxy来说,如果x.equals(y)true,则y.equals(x)也必须为true
  • 传递性(Transitive):对于非nullxyz来说,如果x.equals(y)truey.equals(z)也为true,那么x.equals(z)也必须为true
  • 一致性(Consistent):对于非nullxy来说,只要xy状态不变,则x.equals(y)总是一致地返回true或者false
  • null的比较:即x.equals(null)永远返回false

根据规则,编写Student类的 equals() 方法:

@Override
public boolean equals(Object obj) {
  if (obj instanceof Student) {
    Student student = (Student) obj;
    return Objects.equals(this.name, student.name) && this.id == student.id;
  }

  return false;
}

再来运行上一个测试代码,便能得到正确结果。

使用Objects.equals()比较两个引用类型是否相等的目的是省去了判断null的麻烦。两个引用类型都是null时也返回 true

4、Map

Map这种键值(key-value)映射表的数据结构,作用是能高效地通过key快速查找value(元素)。比如,通过name 查询某个 Student

MapList 一样,也是一个接口,常用的方法包括:

  • put(K key, V value)方法:把keyvalue做了映射并放入Map
  • V get(K key):通过key获取到对应的value。如果key不存在,则返回null
  • boolean containsKey(K key):查询某个 key 是否存在。

对于 Map 来说,常用的实现类是 HashMap

4.1 遍历Map

Map 来说,要遍历 key 可以使用 for each 循环遍历 Map 实例的 keySet() 方法返回的Set集合,它包含不重复的key的集合。

如果要遍历keyvalue,可以使用for each循环遍历Map对象的entrySet()集合,它包含每一个key-value映射。

具体如下:

@Test
public void m3() {
  Map<String, Integer> map = new HashMap<>();
  map.put("apple", 123);
  map.put("pear", 456);
  map.put("banana", 789);

  //方法一
  for (String key : map.keySet()) {
    System.out.println(key + " " + map.get(key));
  }

  //方法二
  for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + " " + entry.getValue());
  }
}

MapList不同:

  • Map存储的是key-value的映射关系,
  • 不保证顺序Map 的遍历顺序没有逻辑可言,甚至不同的JDK版本,相同的代码输出顺序都不同。

5、编写equals和hashCode

HashMap 之所以能够根据 key 直接拿到 value ,原因是它在内部通过空间换时间的方法,用一个大数组存储所有的 value ,并根据 key 直接计算出来 value 应该存储的位置。

  ┌───┐
0 │   │
  ├───┤
1 │ ●─┼───> Student("Xiao Ming")
  ├───┤
2 │   │
  ├───┤
3 │   │
  ├───┤
4 │   │
  ├───┤
5 │ ●─┼───> Student("Xiao Hong")
  ├───┤
6 │ ●─┼───> Student("Xiao Jun")
  ├───┤
7 │   │
  └───┘

上述行为就是本科学的 哈希表 ,根据 key 通过一个算法得到一个索引值,将 value 放到相应的位置。这个算法得到的索引值应该尽可能减少冲突,也就是说,对于两个 keyab ,该算法得到的索引应该尽量不一样,如果发生冲突,也会有冲突解决办法。在 HashMap 中采用的办法是:如果发生冲突,在数组中,实际存储的就不是一个Student 实例,而是一个 List ,如下所示:

  ┌───┐
0 │   │
  ├───┤
1 │   │
  ├───┤
2 │   │
  ├───┤
3 │   │
  ├───┤
4 │   │
  ├───┤
5 │ ●─┼───> List>
  ├───┤
6 │   │
  ├───┤
7 │   │
  └───┘

在查找时,先通过 a 得到索引5,再得到List>,接着它还需要遍历这个 List ,才能返回对应的Student 实例。所以如果冲突的概率越大,这个List 就越长,Mapget() 方法查找效率就越低。

由上分析,得到结论:

  • 作为 key 的类必须正确覆写 equals()hashCode()方法;
  • 一个类如果覆写了equals(),就必须覆写hashCode(),并且覆写规则是:
    • 如果equals()返回true,则hashCode()返回值必须相等;
    • 如果equals()返回false,则hashCode()返回值尽量不要相等。
  • 实现hashCode()方法可以通过Objects.hashCode()辅助方法实现

6、EnumMap

如果Map传入的 key 对象是 enum 类型,那么可以使用 EnumMap ,它在内部以一个非常紧凑的数组存储value,并且根据 enum 类型的key直接定位到内部数组的索引,并不需要计算hashCode(),不但效率最高,而且没有额外的空间浪费。

我们以 DayOfWeek (import java.time.DayOfWeek;)枚举来做一个“翻译”功能:

@Test
public void m4() {
  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));
}

7、TreeMap

Map 接口的一个实现HashMap 是一种以空间换时间的映射表,它的实现原理决定了内部的 key 是无序的,导致了在遍历时的输出顺序是不可预测的。

如果要实现对 key 进行排序,就需要用到 SortedMap 接口,它的实现类是 TreeMap

       ┌───┐
       │Map│
       └───┘
         ▲
    ┌────┴─────┐
    │          │
┌───────┐ ┌─────────┐
│HashMap│ │SortedMap│
└───────┘ └─────────┘
               ▲
               │
          ┌─────────┐
          │ TreeMap │
          └─────────┘

要使用 TreeMap 实现key的排序,放入Map 中的key 必须实现 Comparable 接口。当然对于 StringInteger 这些类来说,已经实现了 Comparable 接口,因此可以直接使用。但对于自定义类,比如Student 类,则需要自己实现。

如果作为Key的class没有实现Comparable接口,那么,必须在创建TreeMap时指定一个自定义排序算法。

实现的例子如下:

//方法一
//修改 Student.java
public class Student implements Comparable<Student> {
  
    //...
  
    @Override
    public int compareTo(Student o) {
        if (Objects.equals(this.name, o.getName())) {
            return 0;
        }

        return this.getName().compareTo(o.getName());
    }
}

//测试方法
@Test
public void m5() {
  Map<Student, Integer> map = new TreeMap<>();

  map.put(new Student("zhangsan"), 111);
  map.put(new Student("lisi"), 222);
  map.put(new Student("asan"), 333);
  map.put(new Student("wangwu"), 444);

  for (Student stu : map.keySet()) {
    System.out.println(stu);
  }
}
//方法二
@Test
public void m5() {
  Map<Student, Integer> map = new TreeMap<>(new Comparator<Student>() {
    @Override
    public int compare(Student o1, Student o2) {
      if(Objects.equals(o1.getName(), o2.getName())) {
        return 0;
      }
      return o1.getName().compareTo(o2.getName());
    }
  });

  map.put(new Student("zhangsan"), 111);
  map.put(new Student("lisi"), 222);
  map.put(new Student("asan"), 333);
  map.put(new Student("wangwu"), 444);

  for (Student stu : map.keySet()) {
    System.out.println(stu);
  }
}

8、Properties

实现对配置文件的读写需要用到Properties 。由于历史遗留原因,Properties内部本质上是一个Hashtable,但我们只需要用到Properties自身关于读写配置的接口。

9、Set

如果我们只需要存储不重复的key,并不需要存储映射的value,那么就可以使用Set。在应该中,我们常用Set 来去除重复元素。

Set用于存储不重复的元素集合,它主要提供以下几个方法:

  • 将元素添加进Setboolean add(E e)
  • 将元素从Set删除:boolean remove(Object e)
  • 判断是否包含元素:boolean contains(Object e)
@Test
public void m5() {
  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实际上相当于只存储key、不存储value的Map。我们经常用Set用于去除重复元素。

因为放入Set的元素和Map的key类似,都要正确实现equals()hashCode()方法,否则该元素无法正确地放入Set

最常用的Set实现类是HashSet,实际上,HashSet仅仅是对HashMap的一个简单封装,它的核心代码如下:

public class HashSet<E>	implements Set<E>
{
 private transient HashMap<E,Object> map;

 // Dummy value to associate with an Object in the backing Map
 private static final Object PRESENT = new Object();

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

	public boolean contains(Object o) {
     return map.containsKey(o);
 }

 public boolean remove(Object o) {
     return map.remove(o) == PRESENT;
 }
}

9.1 TreeSet

MapTreeMap 类似, Set 接口也有一个 TreeSet 。关系如下:

       ┌───┐
       │Set│
       └───┘
         ▲
    ┌────┴─────┐
    │          │
┌───────┐ ┌─────────┐
│HashSet│ │SortedSet│
└───────┘ └─────────┘
               ▲
               │
          ┌─────────┐
          │ TreeSet │
          └─────────┘

用法也是一样,添加的元素必须要正确的实现 Comparable 接口;如果没有实现,那么创建TreeSet时必须传入一个Comparator对象。

10、Queue

队列(Queue)是一种经常使用的集合。Queue实际上是实现了一个先进先出(FIFO:First In First Out)的有序表。它和 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()

注意:不要把null添加到队列中,否则poll()方法返回null时,很难确定是取到了null元素还是队列为空。

10.1 LinkedList

LinkedList类即实现了List接口,又实现了Queue接口,但是,在使用的时候,如果我们把它当作List,就获取List的引用,如果我们把它当作Queue,就获取Queue的引用:

// 这是一个List:
List list = new LinkedList<>();
// 这是一个Queue:
Queue queue = new LinkedList<>();

11、PriorityQueue

PriorityQueue 是一种允许插队的 Queue 。它的出队顺序与元素的优先级有关。对PriorityQueue调用remove()poll()方法,返回的总是优先级最高的元素。

元素的优先级是通过排序得到的,因此和TreeMapTreeSet 一样,放入PriorityQueue的元素,必须实现Comparable接口,如果没有实现,那么创建PriorityQueue 时必须传入一个Comparator对象。

//方法一
@Test
public void m6() {
  Queue<Student> queue = new PriorityQueue<>();
  queue.add(new Student("zhangsan"));
  queue.add(new Student("lisi"));
  queue.add(new Student("asan"));
  queue.add(new Student("wangwu"));
  System.out.println(queue.remove());	//asan
  System.out.println(queue.remove());	//lisi
  System.out.println(queue.remove());	//wangwu
  System.out.println(queue.remove());	//zhangsan
}

12、Deque

Deque 是对 Queue 的一种变体,它允许两端都进,两端都出,叫做双端队列(Double Ended Queue)

QueueDeque出队和入队的方法:

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()

虽然DequeQueue 的扩展,Queue 提供的add()/offer()方法也可以使用,但是使用Deque时,最好不要调用offer(),而是调用offerLast()

Deque是一个接口,它的实现类有ArrayDequeLinkedList

LinkedList是一个全能选手,它即是List,又是Queue,还是Deque。但是我们在使用的时候,总是要用特定的接口来引用它。

13、Stack

栈(Stack)是一种后进先出(LIFO:Last In First Out)的数据结构。

Stack的操作:

  • 把元素压栈:push(E)
  • 把栈顶的元素“弹出”:pop(E)
  • 取栈顶元素但不弹出:peek(E)

在Java中,没有单独的 Stack 接口。

14、Collections

Collections是JDK提供的工具类,同样位于java.util包中。它提供了一系列静态方法,能更方便地操作各种集合。

注意Collections结尾多了一个s,不是Collection!

14.1 创建空集合

Collections提供了一系列方法来创建空集合:

  • 创建空List:List emptyList()
  • 创建空Map:Map emptyMap()
  • 创建空Set:Set emptySet()

但是返回的空集合时不可变集合,无法向其中添加或删除元素。

@Test
public void m8() {
  List<String> list = Collections.emptyList();
  list.add("111");	//java.lang.UnsupportedOperationException
}

14.2 创建单元素集合

Collections提供了一系列方法来创建一个单元素集合:

  • 创建一个元素的List:List singletonList(T o)
  • 创建一个元素的Map:Map singletonMap(K key, V value)
  • 创建一个元素的Set:Set singleton(T o)

同样,返回的单元素集合也是不可变集合,无法向其中添加或删除元素。

14.3 排序

Collections可以对List进行排序。因为排序会直接修改List元素的位置,因此必须传入可变List

@Test
public void m8() {
  List<String> list = new ArrayList<>();
  list.add("apple");
  list.add("pear");
  list.add("orange");
  // 排序前:
  System.out.println(list);
  Collections.sort(list);
  // 排序后:
  System.out.println(list);
}
//输出结果
[apple, pear, orange]
[apple, orange, pear]

14.4 洗牌

Collections提供了洗牌算法,即传入一个有序的List,可以随机打乱List内部元素的顺序,效果相当于让计算机洗牌:

@Test
public void m8() {
  List<Integer> list = new ArrayList<>();
  for (int i=0; i<10; i++) {
    list.add(i);
  }
  // 洗牌前:
  System.out.println(list);
  Collections.shuffle(list);
  // 洗牌后:
  System.out.println(list);
}
//输出结果
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[5, 1, 4, 9, 2, 0, 7, 6, 8, 3]

14.5 不可变集合

Collections还提供了一组方法把可变集合封装成不可变集合:

  • 封装成不可变List:List unmodifiableList(List list)
  • 封装成不可变Set:Set unmodifiableSet(Set set)
  • 封装成不可变Map:Map unmodifiableMap(Map m)

这种封装实际上是通过创建一个代理对象,拦截掉所有修改的方法。

@Test
public void m9() {
  List<String> mutable = new ArrayList<>();
  mutable.add("apple");
  mutable.add("pear");
  // 变为不可变集合:
  List<String> immutable = Collections.unmodifiableList(mutable);
  immutable.add("orange"); // UnsupportedOperationException
}

但是继续对原始的可变List进行增删是可以的,并且,还会影响到封装后的“不可变”List

@Test
public void m8() {
  List<String> mutable = new ArrayList<>();
  mutable.add("apple");
  mutable.add("pear");
  // 变为不可变集合:
  List<String> immutable = Collections.unmodifiableList(mutable);
  mutable.add("orange");
  System.out.println(immutable);	//[apple, pear, orange]
}

所以在返回不可变 List 后,最好立刻将可变List 等于 null ,如下:

@Test
public void m8() {
  List<String> mutable = new ArrayList<>();
  mutable.add("apple");
  mutable.add("pear");
  // 变为不可变集合:
  List<String> immutable = Collections.unmodifiableList(mutable);
  // 立刻扔掉mutable的引用:
  mutable = null;
  System.out.println(immutable);
}

你可能感兴趣的:(Java)