Java基础——集合

209 阅读12分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

一、概念

什么是集合(Collection)?集合就是“由若干个确定的元素所构成的整体”。

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

  • List:一种有序列表的集合,数组和链表:ArrayList,LinkedList
  • Set:一种保证没有重复元素的集合,就是Map的key,HashSet,TreeSet
  • Map:一种通过键值(key-value)查找的映射表集合,HashMap,TreeMap

特点

  • 实现了接口和实现类相分离,例如,有序表的接口是List,具体的实现类有ArrayListLinkedList
  • 支持泛型,我们可以限制在一个集合中只能放入同一种数据类型的元素

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

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

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

  • Enumeration<E>:已被Iterator<E>取代

遍历

通过Iterator遍历集合永远是最高效的方式。Java的for each循环本身就使用Iterator遍历。

只要实现了Iterable接口的集合类都可以直接用for each循环来遍历,Java编译器本身并不知道如何遍历集合对象,但它会自动把for each循环变成Iterator的调用,原因就在于Iterable接口定义了一个Iterator<E> iterator()方法,强迫集合类必须返回一个Iterator实例。

 package com.study.collection;
 ​
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 ​
 public class CollectionDemo {
     public static void main(String[] args) {
         // 数组也是一种集合
         String[] ss = new String[10]; // 可以持有10个String对象
         ss[0] = "Hello"; // 可以放入String对象
         String first = ss[0]; // 可以获取String对象
         System.out.println(first);
         System.out.println(Arrays.toString(ss));
         System.out.println("==========================");
 ​
         // List:ArrayList, LinkedList
         List<String> list1 = new ArrayList<>();
         list1.add("apple"); // size=1
         list1.add(null); // List 允许添加 null
         list1.add("pear"); // size=3
         list1.add("pear"); // 允许重复添加元素
         String second = list1.get(1); // null
         System.out.println(second);
         System.out.println(list1); // AbstractCollection 实现了 toString()
         System.out.println("==========================");
 ​
         // JDK11 可以使用 List.of 创建List
         // List<String> list3 = List.of("apple", "pear", "banana");
         List<String> list2 = Arrays.asList("apple", "pear", "banana");
         // for (int i = 0; i < list2.size(); i++) {
         //     String s = list2.get(i);
         //     System.out.println(s);
         // }
         // 遍历 List,不推荐使用遍历size的方法,性能最低
         // foreach 实际上使用的就是 Iterator 遍历
         for (Iterator<String> it = list2.iterator(); it.hasNext(); ) {
             String s = it.next();
             System.out.println(s);
         }
         for (String s : list2) {
             System.out.println(s);
         }
         System.out.println("==========================");
 ​
         // List 转换为 Array
         Object[] array2 = list2.toArray();
         for (Object s : array2) {
             System.out.println(s);
         }
         System.out.println("==========================");
 ​
         // Array 转换为 List
         // List<Integer> list3 = Arrays.asList(12, 34, 56);
         Integer[] array = {12, 34, 56};
         List<Integer> list3 = Arrays.asList(array);
         // Integer[] array3 = list3.toArray(new Integer[3]);
         Integer[] array3 = list3.toArray(new Integer[list3.size()]);
         for (Integer n : array3) {
             System.out.println(n);
         }
     }
 }
 ​

如果我们自己编写了一个集合类,想要使用for each循环,只需满足以下条件:

  • 集合类实现Iterable接口,该接口要求返回一个Iterator对象;
  • Iterator对象迭代集合内部数据。

这里的关键在于,集合类通过调用iterator()方法,返回一个Iterator对象,这个对象必须自己知道如何遍历该集合。

在编写Iterator的时候,我们通常可以用一个内部类来实现Iterator接口,这个内部类可以直接访问对应的外部类的所有字段和方法。例如,上述代码中,内部类ReverseIterator可以用ReverseList.this获得当前外部类的this引用,然后,通过这个this引用就可以访问ReverseList的所有字段和方法。

 package com.study.collection;
 ​
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 ​
 public class MyCollectionDemo {
     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() {
         return new ReverseIterator(list.size());
     }
 ​
     class ReverseIterator implements Iterator<T> {
         private int index;
 ​
         public ReverseIterator(int index) {
             this.index = index;
         }
 ​
         @Override
         public boolean hasNext() {
             return index > 0;
         }
 ​
         @Override
         public T next() {
             index--;
             return ReverseList.this.list.get(index);
         }
     }
 }
 ​

二、List

List内部并不是通过==判断两个元素是否相等,而是使用equals()方法判断两个元素是否相等。

因此,要正确使用Listcontains()indexOf()这些方法,放入的实例必须正确覆写equals()方法,否则,放进去的实例,查找不到。我们之所以能正常放入StringInteger这些对象,是因为Java标准库定义的这些类已经正确实现了equals()方法。

编写equals()方法可借助Objects.equals()判断。

如果不在List中查找元素,就不必覆写equals()方法。

 package com.study.collection;
 ​
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 ​
 public class ListDemo {
     public static void main(String[] args) {
         List<String> list = Arrays.asList("A", "B", "C"); // ArrayList
         System.out.println(list.contains("C")); // true
         System.out.println(list.contains("X")); // false
         System.out.println(list.indexOf("C")); // 2
         System.out.println(list.indexOf("X")); // -1
         System.out.println("==========================");
 ​
         // List 使用 equals() 方法判断两个元素是否相等
         System.out.println(list.contains(new String("C"))); // true or false?
         System.out.println(list.indexOf(new String("C"))); // 2 or -1?
         System.out.println("==========================");
 ​
         List<Person> list2 = Arrays.asList(
                 new Person("Xiao Ming", 2),
                 new Person("Xiao Hong", 3),
                 new Person("Bob", 4)
         );
         System.out.println(list2.contains(new Person("Bob", 4))); // true
     }
 }
 ​
 class Person {
     public String name;
     public int age;
 ​
     public Person(String name, int age) {
         this.name = name;
         this.age = age;
     }
 ​
     @Override
     public boolean equals(Object obj) {
         // if (obj instanceof Person) {
         //     Person p = (Person) obj;
         //     boolean nameEquals = false;
         //     if (this.name == null && p.name == null) {
         //         nameEquals = true;
         //     }
         //     if (this.name != null) {
         //         nameEquals = this.name.equals(p.name);
         //     }
         //     return nameEquals && this.age == p.age;
         // }
         // return false;
 ​
         if (obj instanceof Person) {
             Person p = (Person) obj;
             // 如果 Person 有多个引用类型字段,可用 Objects.equals 简化引用类型的比较,减少 null 的判断
             // 对于引用字段比较,我们使用equals(),对于基本类型字段的比较,我们使用==
             return Objects.equals(this.name, p.name) && this.age == p.age;
         }
         return false;
     }
 }
 ​

三、Map

重复放入key-value并不会有任何问题,但是一个key只能关联一个value。在上面的代码中,一开始我们把key对象"apple"映射到Integer对象123,然后再次调用put()方法把"apple"映射到789,这时,原来关联的value对象123就被“冲掉”了。实际上,put()方法的签名是V put(K key, V value),如果放入的key已经存在,put()方法会返回被删除的旧的value,否则,返回null

始终牢记:Map中不存在重复的key,因为放入相同的key,只会把原有的key-value对应的value给替换掉。

遍历

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

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

MapList不同的是,Map存储的是key-value的映射关系,并且,它不保证顺序

查找

因为在Map的内部,对key做比较是通过equals()实现的,这一点和List查找元素需要正确覆写equals()是一样的,即正确使用Map必须保证:作为key的对象必须正确覆写equals()方法。

通过key计算索引的方式就是调用key对象的hashCode()方法,它返回一个int整数。HashMap正是通过这个方法直接定位key对应的value的索引,继而直接返回value

因此,正确使用Map必须保证:

  1. 作为key的对象必须正确覆写equals()方法,相等的两个key实例调用equals()必须返回true
  2. 作为key的对象还必须正确覆写hashCode()方法,且hashCode()方法要严格遵循以下规范:
  • 如果两个对象相等,则两个对象的hashCode()必须相等;
  • 如果两个对象不相等,则两个对象的hashCode()尽量不要相等。

equals()用到的用于比较的每一个字段,都必须在hashCode()中用于计算;equals()中没有使用到的字段,绝不可放在hashCode()中计算。一个类如果覆写了equals(),就必须覆写hashCode()

 package com.study.collection;
 ​
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 ​
 public class MapDemo {
     public static void main(String[] args) {
         Student s = new Student("Xiao Ming", 99);
         Map<String, Student> map = new HashMap<>();
         map.put("Xiao Ming", s); // 将"Xiao Ming"和Student实例映射并关联
         Student target = map.get("Xiao Ming"); // 通过key查找并返回映射的Student实例
         System.out.println(target == s); // true,同一个实例
         System.out.println(target.score); // 99
         Student another = map.get("Bob"); // 通过另一个key查找
         System.out.println(another); // 未找到返回null
         System.out.println("==========================");
 ​
         Map<String, Integer> map2 = new HashMap<>();
         map2.put("apple", 123);
         map2.put("pear", 456);
         map2.put("banana", 789);
         // 先遍历出 key,再通过 key 查 value
         for (String key : map2.keySet()) {
             Integer value = map2.get(key);
             System.out.println(key + " = " + value);
         }
         // 先遍历出 entry,再获取 key 和 value
         for (Map.Entry<String, Integer> entry : map2.entrySet()) {
             String key = entry.getKey();
             Integer value = entry.getValue();
             System.out.println(key + " = " + value);
         }
     }
 }
 ​
 class Student {
     public String name;
     public int score;
 ​
     public Student(String name, int score) {
         this.name = name;
         this.score = score;
     }
 }
 ​
 class MapKey {
     public String firstName;
     public String lastName;
     public int age;
 ​
     @Override
     public boolean equals(Object obj) {
         if (obj instanceof MapKey) {
             MapKey k = (MapKey) obj;
             return Objects.equals(this.firstName, k.firstName) &&
                     Objects.equals(this.lastName, k.lastName) &&
                     this.age == k.age;
         }
         return false;
     }
 ​
     @Override
     public int hashCode() {
         // int h = 0;
         // h = 31 * h + firstName.hashCode();
         // h = 31 * h + lastName.hashCode();
         // h = 31 * h + age;
         // return h;
 ​
         // firstName,lastName 可能为 null
         return Objects.hash(firstName, lastName, age);
     }
 }
 ​

EnumMap

如果Map的key是enum类型,推荐使用EnumMap,既保证速度,也不浪费空间。使用EnumMap的时候,根据面向抽象编程的原则,应持有Map接口。

 package com.study.collection;
 ​
 import java.util.EnumMap;
 import java.util.Map;
 ​
 public class EnumMapDemo {
     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));
     }
 }
 ​
 enum DayOfWeek {
     MONDAY,
     TUESDAY,
     WEDNESDAY,
     THURSDAY,
     FRIDAY,
     SATURDAY,
     SUNDAY,
 }
 ​

TreeMap

还有一种Map,它在内部会对Key进行排序,这种Map就是SortedMap。注意到SortedMap是接口,它的实现类是TreeMap

使用TreeMap时,放入的Key必须实现Comparable接口。StringInteger这些类已经实现了Comparable接口,因此可以直接作为Key使用。如果作为Key的class没有实现Comparable接口,那么,必须在创建TreeMap时同时指定一个自定义排序算法。

另外,Key可不覆写equals()hashCode(),因为TreeMap不使用equals()hashCode()

TreeMap在比较两个Key是否相等时,依赖Key的compareTo()方法或者Comparator.compare()方法。

 package com.study.collection;
 ​
 import java.util.Comparator;
 import java.util.Map;
 import java.util.TreeMap;
 ​
 public class TreeMapDemo {
     public static void main(String[] args) {
         Map<String, Integer> map = new TreeMap<>();
         map.put("orange", 1);
         map.put("apple", 2);
         map.put("pear", 3);
         // apple, orange, pear 按照key排序,String 字符串顺序
         for (String key : map.keySet()) {
             System.out.println(key);
         }
         System.out.println("==========================");
 ​
         // Teacher 类未实现 Comparable 接口,需要传入 Comparator
         Map<Teacher, Integer> map2 = new TreeMap<>(new Comparator<Teacher>() {
             public int compare(Teacher p1, Teacher p2) {
                 return p1.name.compareTo(p2.name);
             }
         });
         map2.put(new Teacher("Tom"), 1);
         map2.put(new Teacher("Bob"), 2);
         map2.put(new Teacher("Lily"), 3);
         // Bob, Lily, Tom
         for (Teacher key : map2.keySet()) {
             System.out.println(key);
         }
         // Teacher 类未实现 equals()
         System.out.println(map2.get(new Teacher("Bob"))); // 2
     }
 }
 ​
 class Teacher {
     public String name;
 ​
     public Teacher(String name) {
         this.name = name;
     }
 ​
     @Override
     public String toString() {
         return "Teacher{" +
                 "name='" + name + ''' +
                 '}';
     }
 }
 ​

Properties

配置文件的特点是,它的Key-Value一般都是String-String类型的,因此我们完全可以用Map<String, String>来表示它。

因为配置文件非常常用,所以Java集合库提供了一个Properties来表示一组“配置”。由于历史遗留原因,Properties内部本质上是一个Hashtable,但我们只需要用到Properties自身关于读写配置的接口。

Properties读取配置文件非常简单。Java默认配置文件以.properties为扩展名,每行以key=value表示,以#开头的是注释。

 # setting.properties
 ​
 last_open_file=/data/hello.txt
 auto_save_interval=60

Properties读取配置文件,一共有三步:

  1. 创建Properties实例;
  2. 调用load()读取文件;
  3. 调用getProperty()获取配置。

也可以从classpath读取.properties文件,因为load(InputStream)方法接收一个InputStream实例,表示一个字节流,它不一定是文件流,也可以是从jar包中读取的资源流。

如果有多个.properties文件,可以反复调用load()读取,后读取的key-value会覆盖已读取的key-value,可以把默认配置文件放到classpath中,然后,根据机器的环境编写另一个配置文件,覆盖某些默认的配置。

Properties设计的目的是存储String类型的key-value,但Properties实际上是从Hashtable派生的,从Hashtable继承下来的get()put()方法,这些方法的参数签名是Object,我们在使用Properties的时候,不要去调用这些从Hashtable继承下来的方法。

编码

需要注意的是,由于load(InputStream)默认总是以ASCII编码读取字节流,所以会导致读到乱码。我们需要用另一个重载方法load(Reader)读取。

InputStreamReader的区别是一个是字节流,一个是字符流。字符流在内存中已经以char类型表示了,不涉及编码问题。

 package com.study.collection;
 ​
 import java.io.*;
 import java.nio.charset.StandardCharsets;
 import java.util.Properties;
 ​
 public class PropertiesDemo {
     public static void main(String[] args) throws Exception {
         System.out.println(new File(".").getCanonicalPath());
         String f = "7.collection/resources/setting.properties";
         Properties props = new Properties();
         props.load(new FileInputStream(f));
 ​
         String filepath = props.getProperty("last_open_file");
         String interval = props.getProperty("auto_save_interval", "120");
         System.out.println(filepath);
         System.out.println(interval); // 中文会乱码
         System.out.println("==========================");
 ​
         // 也可以从 classpath 通过资源流读取 .properties 文件
         Properties props2 = new Properties();
         props2.load(PropertiesDemo.class.getResourceAsStream("/setting.properties"));
 ​
         String filepath2 = props2.getProperty("last_open_file");
         String interval2 = props2.getProperty("auto_save_interval", "120");
         System.out.println(filepath2);
         System.out.println(interval2);
         System.out.println("==========================");
 ​
         String settings = "# test" + "\n" + "course=Java" + "\n" + "last_open_date=2019-08-07T12:35:01";
         ByteArrayInputStream input = new ByteArrayInputStream(settings.getBytes(StandardCharsets.UTF_8));
         Properties props3 = new Properties();
         props3.load(input);
 ​
         System.out.println("course: " + props3.getProperty("course"));
         System.out.println("last_open_date: " + props3.getProperty("last_open_date"));
         System.out.println("last_open_file: " + props3.getProperty("last_open_file"));
         System.out.println("auto_save: " + props3.getProperty("auto_save", "60"));
         System.out.println("==========================");
 ​
         Properties props4 = new Properties();
         props4.setProperty("url", "http://www.liaoxuefeng.com");
         props4.setProperty("language", "Java编程");
         props4.store(new FileOutputStream("7.collection/resources/setting2.properties"), "这是写入的properties注释");
 ​
         Properties props5 = new Properties();
         props5.load(new FileReader("7.collection/resources/setting3.properties"));
         System.out.println("url: " + props5.getProperty("url"));
         System.out.println("language: " + props5.getProperty("language"));
     }
 }
 ​

四、Set

只需要存储不重复的key,并不需要存储映射的value,那么就可以使用Set

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

  • 将元素添加进Set<E>boolean add(E e)
  • 将元素从Set<E>删除:boolean remove(Object e)
  • 判断是否包含元素:boolean contains(Object e)

Set实际上相当于只存储key、不存储value的Map。我们经常用Set用于去除重复元素。

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

最常用的Set实现类是HashSetSet接口并不保证有序,而SortedSet接口则保证元素是有序的:

  • HashSet是无序的,因为它实现了Set接口,并没有实现SortedSet接口
  • TreeSet是有序的,因为它实现了SortedSet接口
        ┌───┐
        │Set│
        └───┘
          ▲
     ┌────┴─────┐
     │          │
 ┌───────┐ ┌─────────┐
 │HashSet│ │SortedSet│
 └───────┘ └─────────┘
                ▲
                │
           ┌─────────┐
           │ TreeSet │
           └─────────┘

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

 package com.study.collection;
 ​
 import java.util.HashSet;
 ​
 public class SetDemo {
     public static void main(String[] args) {
         HashSet<String> set = new HashSet<>();
 ​
         System.out.println(set.add("hello"));
         System.out.println(set.contains("hello"));
         System.out.println(set.remove("hi"));
     }
 }
 ​

五、Queue

Queue实际上是实现了一个先进先出(FIFO:First In First Out)的有序表。

Queue只有两个操作:

  • 把元素添加到队列末尾;
  • 从队列头部取出元素。

在Java的标准库中,队列接口Queue定义了以下几个方法:

  • int size():获取队列长度;
  • boolean add(E)/boolean offer(E):添加元素到队尾;
  • E remove()/E poll():获取队首元素并从队列中删除;
  • E element()/E peek():获取队首元素但并不从队列中删除。

注意到添加、删除和获取队列元素总是有两个方法,这是因为在添加或获取元素失败时,这两个方法的行为是不同的。我们用一个表格总结如下:

throw Exception返回false或null
添加元素到队尾add(E e)boolean offer(E e)
取队首元素并删除E remove()E poll()
取队首元素但不删除E element()E peek()

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

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

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

PriorityQueue

PriorityQueueQueue的区别在于,它的出队顺序与元素的优先级有关,对PriorityQueue调用remove()poll()方法,返回的总是优先级最高的元素。

放入PriorityQueue的元素,必须实现Comparable接口,PriorityQueue会根据元素的排序顺序决定出队的优先级。

 package com.study.collection;
 ​
 import java.util.Comparator;
 import java.util.LinkedList;
 import java.util.PriorityQueue;
 import java.util.Queue;
 ​
 public class QueueDemo {
     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,因为队列是空的
         System.out.println("==========================");
 ​
         Queue<String> priorityQueue = new PriorityQueue<>();
         // 添加3个元素到队列:
         priorityQueue.offer("apple");
         priorityQueue.offer("pear");
         priorityQueue.offer("banana");
         System.out.println(priorityQueue.poll()); // apple
         System.out.println(priorityQueue.poll()); // banana
         System.out.println(priorityQueue.poll()); // pear
         System.out.println(priorityQueue.poll()); // null,因为队列为空
         System.out.println("==========================");
 ​
         Queue<User> priorityQueue2 = new PriorityQueue<>(new UserComparator());
         // 添加3个元素到队列:
         priorityQueue2.offer(new User("Bob", "A1"));
         priorityQueue2.offer(new User("Alice", "A2"));
         priorityQueue2.offer(new User("Boss", "V1"));
         System.out.println(priorityQueue2.poll()); // Boss/V1
         System.out.println(priorityQueue2.poll()); // Bob/A1
         System.out.println(priorityQueue2.poll()); // Alice/A2
         System.out.println(priorityQueue2.poll()); // null,因为队列为空
     }
 }
 ​
 class UserComparator implements Comparator<User> {
     @Override
     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;
     }
 ​
     @Override
     public String toString() {
         return name + "/" + number;
     }
 }
 ​

六、Deque

允许两头都进,两头都出,这种队列叫双端队列(Double Ended Queue),学名Deque

比较一下QueueDeque出队和入队的方法:

QueueDeque
添加元素到队尾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()

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

 package com.study.collection;
 ​
 import java.util.Deque;
 import java.util.LinkedList;
 ​
 public class DequeDemo {
     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
     }
 }
 ​

七、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()方法,这样代码更加清晰。

八、Collections

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

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

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

  • 创建空List:List<T> emptyList()
  • 创建空Map:Map<K, V> emptyMap()
  • 创建空Set:Set<T> emptySet()

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

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

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

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

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

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)

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

如果我们希望把一个可变List封装成不可变List,那么,返回不可变List后,最好立刻扔掉可变List的引用,这样可以保证后续操作不会意外改变原始对象,从而造成“不可变”List变化了

 package com.study.collection;
 ​
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 ​
 public class CollectionsDemo {
     public static void main(String[] args) {
         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);
         System.out.println("==========================");
 ​
         List<Integer> list1 = new ArrayList<>();
         for (int i = 0; i < 10; i++) {
             list1.add(i);
         }
         // 洗牌前
         System.out.println(list1);
         Collections.shuffle(list1);
         // 洗牌后
         System.out.println(list1);
         System.out.println("==========================");
 ​
         List<String> mutable = new ArrayList<>();
         mutable.add("apple");
         mutable.add("pear");
         // 变为不可变集合
         List<String> immutable = Collections.unmodifiableList(mutable);
         System.out.println(immutable); // [apple, pear]
         mutable.add("orange"); // 原 List 会影响新的不变 List
         // 立刻扔掉mutable的引用:
         mutable = null;
         System.out.println(immutable); // [apple, pear, orange]
     }
 }
 ​

Collections还提供了一组方法,可以把线程不安全的集合变为线程安全的集合:

  • 变为线程安全的List:List<T> synchronizedList(List<T> list)
  • 变为线程安全的Set:Set<T> synchronizedSet(Set<T> s)
  • 变为线程安全的Map:Map<K,V> synchronizedMap(Map<K,V> m)