摘自廖雪峰的java教程,有部分删改


Java集合简介

Java的集合类定义在java.util包中,支持泛型,主要提供了3种集合类,包括ListSetMap。Java集合使用统一的Iterator遍历,尽量不要使用遗留接口

使用List

  • 分为ArrayListLinkedList
    • 通常情况下,我们总是优先使用ArrayList

List的特点

  • List接口允许我们添加重复的元素,即List内部的元素可以重复:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import 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
    13
    import 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

  1. 不推荐,用for循环根据索引配合get(int)方法遍历
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import 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);
    }
    }
    }
  2. 使用Iterator遍历List
    1
    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转换

  1. 调用toArray()方法直接返回一个Object[]数组
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import 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);
    }
    }
    }

    这种方法会丢失类型信息,所以实际应用很少。

  2. toArray(T[])传入一个类型相同的ArrayList内部自动把元素复制到传入的Array
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import 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()方法判断两个元素是否相等
  • 因此,要正确使用Listcontains()indexOf()这些方法,放入的实例必须正确覆写equals()方法,否则,放进去的实例,查找不到。我们之所以能正常放入StringInteger这些对象,是因为Java标准库定义的这些类已经正确实现了equals()方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import 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):对于非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

eg:
Person类为例

1
2
3
4
public class Person {
public String name;
public int age;
}

简化引用类型的比较,我们使用Objects.equals()静态方法

1
2
3
4
5
6
public boolean equals(Object o) {
if (o instanceof Person p) {
return Objects.equals(this.name, p.name) && this.age == p.age;
}
return false;
}

总结

  1. 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
  2. instanceof判断传入的待比较的Object是不是当前类型,如果是,继续比较,否则,返回false
  3. 对引用类型用Objects.equals()比较,对基本类型直接用==比较。
  • 使用Objects.equals()比较两个引用类型是否相等的目的是省去了判断null的麻烦。两个引用类型都是null时它们也是相等的。
  • 如果不调用Listcontains()indexOf()这些方法,那么放入的元素就不需要实现equals()方法。

使用Map

Map构建

Map有关于哈希表,这里不多赘述,感兴趣可以搜索了解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.HashMap;
import java.util.Map;

public class Main {
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
}
}

class Student {
public String name;
public int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
}

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

遍历Map

  1. Map来说,要遍历key可以使用for each循环遍历Map实例的keySet()方法返回的Set集合,它包含不重复的key的集合
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import 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);
    }
    }
    }
  2. 同时遍历keyvalue可以使用for each循环遍历Map对象的entrySet()集合,它包含每一个key-value映射
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import 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()作用

  1. 快速定位与高效查找:通过取模运算将HashCode映射到哈希表的特定存储桶(Bucket),时间复杂度为O(1)
  2. 减少equals调用次数:当多个对象HashCode相同时(哈希冲突),仅需在冲突的存储桶内使用equals()比较元素,而非遍历整个集合。

equals方法和hashcode的关系

  1. 如果两个对象equals相等,那么这两个对象的HashCode一定也相同
  2. 如果两个对象的HashCode相同,不代表两个对象就相同,只能说明这两个对象在散列存储结构中,存放于同一个位置

重写哈希函数

1
2
3
4
5
6
7
8
9
10
11
@Override
public int hashCode() {
return Objects.hash(id, name); // 基于id和name计算
}
// 手动实现(类似Guava风格)
public int hashCode() {
int result = 17;
result = 31 * result + id;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}

hashCode()方法编写得越好,HashMap工作的效率就越高。

使用EnumMap

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


举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//我们以DayOfWeek这个枚举类型为例,为它做一个“翻译”功能:
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));
}
}

使用EnumMap的时候,我们总是用Map接口来引用它,因此,实际上把HashMap和EnumMap互换,在客户端看来没有任何区别。

使用TreeMap

  • SortedMap接口的实现类,在内部对Key进行排序
  1. SortedMap保证遍历时以Key的顺序来进行排序。例如,放入的Key是"apple""pear""orange",遍历的顺序一定是"apple""orange""pear",因为String默认按字母排序:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import 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接口。StringInteger这些类已经实现了Comparable接口,因此可以直接作为Key使用。作为Value的对象则没有任何要求。
  2. 如果作为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
    29
    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 + "}";
    }
    }
    • 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
      30
      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) {
      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.scorep2.score不相等的时候,它的返回值是正确的,但是,在p1.scorep2.score相等的时候,它并没有返回0!这就是为什么TreeMap工作不正常的原因:TreeMap在比较两个Key是否相等时,依赖Key的compareTo()方法或者Comparator.compare()方法。在两个Key相等时,必须返回0。因此,修改代码如下:
        1
        2
        3
        4
        5
        6
        public int compare(Student p1, Student p2) {
        if (p1.score == p2.score) {
        return 0;
        }
        return p1.score > p2.score ? -1 : 1;
        }

使用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
    6
    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");

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

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

    调用getProperty()获取配置时,如果key不存在,将返回null。我们还可以提供一个默认值,这样,当key不存在的时候,就返回默认值。


  • 也可以从classpath读取.properties文件:
    1
    2
    Properties 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
2
3
4
Properties props = new Properties();
props.setProperty("url", "http://www.liaoxuefeng.com");
props.setProperty("language", "Java");
props.store(new FileOutputStream("C:\\conf\\setting.properties"), "这是写入的properties注释");

编码

  • 从JDK9开始,Java的.properties文件可以使用UTF-8编码了。
  • 不过,需要注意的是,由于load(InputStream)默认总是以ASCII编码读取字节流,所以会导致读到乱码。我们需要用另一个重载方法load(Reader)读取:
    1
    2
    Properties 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
    14
    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

    • 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
    46
    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;
    }
    }

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);
    }

    @Override
    public Iterator<T> iterator() {
    return new ReverseIterator(list.size());
    }

    class ReverseIterator implements Iterator<T> {
    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);
    }
    }
    }

Collections

注意:Collections结尾多了一个s,不是Collection!
以下涉及版本JDK≥9

创建空集合

使用List.of()Map.of()Set.of()来创建空集合。

创建单元素集合

使用List.of(T...)Map.of(T...)Set.of(T...)来创建任意个元素的集合。

排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.*;

public class Main {
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);
}
}

洗牌

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.*;

public class Main {
public static void main(String[] args) {
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);
}
}

不可变集合

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开始,引入了更高效的并发集合类,所以上述这几个同步方法已经没有什么用了