• 初识Java 10-2 集合


    目录

    LinkedList

    Stack

    Queue

    Set

    Map

    新特性:记录(record)类型


    本笔记参考自: 《On Java 中文版》


    LinkedList

            LinkedList同样实现了基本的List接口。相比于ArrayList,LinkedList拥有更快的插入和删除效率,但随机访问的能力会差一些。

            由于LinkedList添加了一些方法,它也可以被用作栈、队列或双端队列进行使用。这些被添加的方法大多和其他方法类似,名字的不同只是来自于使用场景的变化。如:

    • getFirst():和element()方法完全相同,返回一个列表的头部(第一个元素),且不进行移除。
    • peek():和getFirst()类似,但若列表为空,会返回null。
    • removeFirst():和remove()完全相同,移除并返回列表的头。
    • poll():和removeFirst()类似,但列表为空时,返回null。
    1. import java.util.LinkedList;
    2. public class LinkedListFeatures {
    3. public static void main(String[] args) {
    4. LinkedList pets = new LinkedList<>(new PetCreator().list(5));
    5. System.out.println(pets);
    6. // getFirst()和element()方法完全相同:获取第一个元素
    7. System.out.println("pets.getFirst(): " + pets.getFirst());
    8. System.out.println("pets.element(): " + pets.element());
    9. System.out.println();
    10. // 仅当列表为空时,peek()会和上面两个方法产生区别
    11. System.out.println("pets.peek(): " + pets.peek());
    12. // remove()和removeFirst()方法完全相同:移除并返回第一个元素
    13. System.out.println("pets.remove(): " + pets.remove());
    14. System.out.println("pets.removeFirst(): " + pets.removeFirst());
    15. // 仅当列表为空时,peek()会和上面两个方法产生区别
    16. System.out.println("pets.poll(): " + pets.poll());
    17. System.out.println(pets);
    18. // 在列表的开头插入一个元素
    19. System.out.println();
    20. pets.addFirst(new Rat());
    21. System.out.println("执行addFirst()后: " + pets);
    22. // 在列表的尾部插入一个元素
    23. System.out.println();
    24. pets.offer(new PetCreator().get());
    25. System.out.println("执行offer()后: " + pets);
    26. pets.add(new PetCreator().get());
    27. System.out.println("执行add()后: " + pets);
    28. pets.addLast(new PetCreator().get());
    29. System.out.println("执行addLast()后: " + pets);
    30. // 移除最后一个元素
    31. System.out.println();
    32. System.out.println("pets.removeLast(): " + pets.removeLast());
    33. }
    34. }

            程序执行的结果是:

            若观察Queue接口,就会发现element()offer()peek()poll()remove()方法都被添加到了LinkedList中,所以LinkedList也可以算作是Queue的一个实现。

    Stack

            栈是一种“后进先出”(LIFO)的集合,也被称为下堆栈。Java 1.0就提供了Stack类,但其设计非常糟糕。而且因为向后兼容的缘故,这种设计错误难以摆脱。Java 6加入了ArrayDeque,提供了直接实现栈的方法

    1. import java.util.ArrayDeque;
    2. import java.util.Deque;
    3. public class StackTest {
    4. public static void main(String[] args) {
    5. Deque stack = new ArrayDeque<>();
    6. for (String s : "The weather is fine today".split(" "))
    7. stack.push(s);
    8. while (!stack.isEmpty())
    9. System.out.print(stack.pop() + " ");
    10. System.out.println();
    11. }
    12. }

            程序执行的结果是:

            尽管Deque在各方面表现都像栈,但我们必须把它声称为Deque。当然,我们可以自己定义一个Stack

    1. package onjava;
    2. import java.util.ArrayDeque;
    3. import java.util.Deque;
    4. public class Stack {
    5. private Deque storage = new ArrayDeque<>();
    6. public void push(T v) {
    7. storage.push(v);
    8. }
    9. public T peek() {
    10. return storage.peek();
    11. }
    12. public T pop() {
    13. return storage.pop();
    14. }
    15. public boolean isEmpty() {
    16. return storage.isEmpty();
    17. }
    18. @Override
    19. public String toString() {
    20. return storage.toString();
    21. }
    22. }

            上述代码通过泛型给出了一个简单的Stack类的定义。类名后的告诉编译器这是一个参数化的类型,类型参数是T,当这个类被使用时,它会被替换为实际类型

            若只需要栈的行为,在这里使用继承就不合适了,因为这样会得到一个具有ArrayDeque所有方法的类,这很明显是冗余的。若使用组合,我们就可以选择暴露哪些方法,以及如何为它们命名。

            上面创建的Stack的使用例如下:

            若想要在自己的代码中使用自己的Stack,在创建对象时必须指定完整的包名,或者在创建时更改类名,防止和java.util中的Stack冲突(或者在使用时通过全限定名进行特定Stack的指定)。

    Queue

            队列是一种“先进后出”(FIFO)的集合。LinkedList实现了Queue接口,并提供了支持队列行为的方法,这使得我们可以将LinkedList视为Queue的一种实现进行使用。以下例子会展示Queue接口的特有用法:

    1. import java.util.LinkedList;
    2. import java.util.Queue;
    3. import java.util.Random;
    4. public class QueueDemo {
    5. public static void printQ(Queue queue) {
    6. while (queue.peek() != null)
    7. System.out.print(queue.remove() + " ");
    8. System.out.println();
    9. }
    10. public static void main(String[] args) {
    11. Queue queue = new LinkedList<>();
    12. Random rand = new Random(47);
    13. for (int i = 0; i < 10; i++)
    14. queue.offer(rand.nextInt(i + 10));
    15. printQ(queue);
    16. Queue qc = new LinkedList<>();
    17. for (char c : "今天天气真好".toCharArray())
    18. qc.offer(c);
    19. printQ(qc);
    20. }

            程序执行的结果如下:

    • 用于插入 —— offer():当无法插入时返回false
    • 用于返回队列的头部元素(不删除元素)—— peek()element()
      • peek():若队列为空,返回null
      • element():若队列为空,抛出NoSuchElementException
    • 用于删除头部元素(并返回该元素)—— poll()remove()
      • poll():若队列为空,返回null
      • remove():若队列为空,抛出NoSuchElementException

            Queue只允许我们访问这个接口中定义的方法,所以LinkedList中的其他方法就无法被访问了。另外,Queue特有的方法都提供了完整且独立的功能。换言之,尽管Queue继承了Collection,但即使不使用Collection中的方法,我们也可以使用一个可用的Queue

            优先级队列说明,下一个要拿出的元素是需求最强烈的元素(优先级最高)。Java 5添加了PriorityQueue,为这一概念提供了一个实现。

            若使用offer()将元素放入PriorityQueue中,这个对象会在排序后放入队列。而默认的排序方法是使用对象在队列中的自然顺序,但使用者可以提供了一个Comparator来修改这一顺序。

        实际上,优先级队列可能会在插入时排序,也可能在删除时选择最重要的元素。

            下面是一个PriorityQueue的使用例:

    1. import java.util.PriorityQueue;
    2. import java.util.Random;
    3. import java.util.Arrays;
    4. import java.util.List;
    5. import java.util.Collections;
    6. import java.util.Set;
    7. import java.util.HashSet;
    8. public class PriorityQueueDemo {
    9. public static void main(String[] args) {
    10. PriorityQueue priorityQueue = new PriorityQueue<>();
    11. Random rand = new Random(47);
    12. for (int i = 0; i < 10; i++)
    13. priorityQueue.offer(rand.nextInt(i + 10));
    14. QueueDemo.printQ(priorityQueue);
    15. List ints = Arrays.asList(25, 22, 20, 18, 14, 9, 3, 1, 1, 2, 3, 9, 14, 18, 21, 23, 25);
    16. priorityQueue = new PriorityQueue<>(ints);
    17. QueueDemo.printQ(priorityQueue);
    18. priorityQueue = new PriorityQueue<>(ints.size(), Collections.reverseOrder());
    19. priorityQueue.addAll(ints);
    20. QueueDemo.printQ(priorityQueue);
    21. String fact = "TODAY IS A GOOD DAY";
    22. List strings = Arrays.asList(fact.split(""));
    23. PriorityQueue stringPQ = new PriorityQueue<>(strings);
    24. QueueDemo.printQ(stringPQ);
    25. stringPQ = new PriorityQueue<>(strings.size(), Collections.reverseOrder());
    26. stringPQ.addAll(strings);
    27. QueueDemo.printQ(stringPQ);
    28. Set charSet = new HashSet<>();
    29. for (char c : fact.toCharArray())
    30. charSet.add(c);// 自动装箱
    31. PriorityQueue characterPQ = new PriorityQueue<>(charSet);
    32. QueueDemo.printQ(characterPQ);
    33. }
    34. }

            程序执行的结果是:

            值可以重复,最小的值优先级最高(在String中,空格的也是值,且优先级高于字母)。

            在上述程序中,使用了两次Collections.reverseOrder(),通过使用这个方法,生成了一个可以反向排序的Comparator()

        IntegerStringCharacter之所以配合PriorityQueue进行使用,是因为这些类已经有了自然顺序。若想让自己的类也可以在PriorityQueue中进行使用,就必须包含额外用于生成自然顺序的功能,或提供一个Comparator

    Set

            Set中不允许出现重复的对象值Set最常见的用法是测试成员身份,我们可以轻松检测某个对象是否存在于Set当中。因此,查找通常也是Set最重要的操作(所以HashSet通常是我们的首选)。

            SetCollection有相同的接口,但不同于List添加了额外的功能,Set就是一个行为不同的Collection

        Set是根据对象的“值”来确定成员身份。

    1. import java.util.HashSet;
    2. import java.util.Random;
    3. import java.util.Set;
    4. public class SetOfInteger {
    5. public static void main(String[] args) {
    6. Random rand = new Random(System.currentTimeMillis()); // 使用的参数是一个时间戳
    7. Set intset = new HashSet<>();
    8. for (int i = 0; i < 10000; i++)
    9. intset.add(rand.nextInt(30));
    10. System.out.println(intset);
    11. }
    12. }

            程序执行的结果是:

            在早期的Java版本中,HashSet输出的顺序没有明显的规律。因为HashSet会通过哈希来提高效率,这使得HashSet的维护和储存与其他Set都不相同(虽然LinkedHashSet也使用了哈希,但它会通过链表按照顺序维护元素)。

        另外,TreeSet是通过红黑树数据结构进行储存的。

            不过现在哈希算法变了,因此我们的输出变得有规律了(我们不应该依赖这种行为)。

    1. import java.util.HashSet;
    2. import java.util.Set;
    3. public class SetOfString {
    4. public static void main(String[] args) {
    5. Set colors = new HashSet<>();
    6. for (int i = 0; i < 100; i++) {
    7. colors.add("黄色");
    8. colors.add("蓝色");
    9. colors.add("红色");
    10. colors.add("蓝色");
    11. colors.add("黄色");
    12. colors.add("红色");
    13. colors.add("橙色");
    14. colors.add("紫色");
    15. }
    16. System.out.println(colors);
    17. }
    18. }

            程序执行的结果是:

            使用TreeSet可以获得有顺序的数列:

            对Set而言,最常见的操作之一是使用contain()来测试Set成员身份:

    1. import java.util.Collection;
    2. import java.util.Collections;
    3. import java.util.HashSet;
    4. import java.util.Set;
    5. public class SetOperations {
    6. public static void main(String[] args) {
    7. Set set1 = new HashSet<>();
    8. Collections.addAll(set1, "A B C D E F G H I J K L".split(" "));
    9. set1.add("M");
    10. System.out.println("H: " + set1.contains("H"));
    11. System.out.println("N: " + set1.contains("N"));
    12. System.out.println();
    13. Set set2 = new HashSet<>();
    14. Collections.addAll(set2, "H I J K L".split(" "));
    15. System.out.println("set2是否在set1中: " + set1.containsAll(set2));
    16. System.out.println();
    17. set1.remove("H");
    18. System.out.println("set1: " + set1);
    19. System.out.println("set1是否在set2中: " + set2.containsAll(set1));
    20. System.out.println();
    21. set1.removeAll(set2);
    22. System.out.println("删去set1中所有存在于set2中的元素: " + set1);
    23. System.out.println();
    24. Collections.addAll(set1, "X Y Z".split(" "));
    25. System.out.println("将'X Y Z'添加到set1中: " + set1);
    26. }
    27. }

            程序执行的结果是:

            在读取文件时,一个没有重复元素的列表会非常有用:

    1. import java.nio.file.Files;
    2. import java.nio.file.Paths;
    3. import java.util.List;
    4. import java.util.Set;
    5. import java.util.TreeSet;
    6. public class UniqueWords {
    7. public static void main(String[] args) throws Exception {
    8. List lines = Files.readAllLines(Paths.get("SetOperations.java"));
    9. Set words = new TreeSet<>();
    10. for (String line : lines)
    11. for (String word : line.split("\\W+"))
    12. if (word.trim().length() > 0)
    13. words.add(word);
    14. System.out.println(words);
    15. }
    16. }

            程序执行的结果如下:

            上述程序会依次处理文件中的每一行,以正则表达式\\W+为参数,使用String.split()将其分解为单词。

            由于使用的是TreeSet,所以获得的字符串会以字典顺序进行划分,因此大写字母和小写字母没有连续在一起。若需要的是按字母顺序排序,需要将String.CASW_INSENSITIVE_ORDER这个Comparator(比较器,用来建立顺序关系的对象)传递给TreeSet

    Map

            Map实现了这样一个概念:将对象映射到其他对象上。例如,现在需要测试Java的Random类的随机性,需要生成大量的随机数,并计算不同区间的数的数量:

    1. import java.util.HashMap;
    2. import java.util.Map;
    3. import java.util.Random;
    4. public class Statistics {
    5. public static void main(String[] args) {
    6. Random rand = new Random(10);
    7. Map m = new HashMap<>();
    8. for (int i = 0; i < 10000; i++) {
    9. // 随机数的范围是0~20
    10. int r = rand.nextInt(20);
    11. Integer freq = m.get(r);
    12. m.put(r, freq == null ? 1 : freq + 1); // 自动装箱机制可以把int转换为Integer
    13. }
    14. System.out.println(m);
    15. }
    16. }

            程序执行的结果是:

            上述程序中,若集合中还不存在键r,则get()会返回null。否则get()会返回与键相关联的Integer值。

            Map的键不仅可以是这种基本类型,也可以是String等类。另外,Map同样具有各种用于检测的contains方法:

    1. import java.util.HashMap;
    2. import java.util.Map;
    3. public class PetMap {
    4. public static void main(String[] args) {
    5. Map petMap = new HashMap<>();
    6. petMap.put("我的猫", new Cat("汤姆"));
    7. petMap.put("我的狗", new Dog("斯派克"));
    8. petMap.put("我的仓鼠", new Dog("野牛"));
    9. System.out.println(petMap);
    10. Pet dog = petMap.get("我的狗");
    11. System.out.println("petMap.get(\"我的狗\"): " + dog);
    12. System.out.println("通过键进行检测: " + petMap.containsKey("我的狗"));
    13. System.out.println("通过值进行检测: " + petMap.containsValue(dog));
    14. }
    15. }

            程序执行的结果是:

            与数组和Collection类似,Map也可以扩展为多维:我们可以创建一个值为MapMap(内部的Map的值可以是其他集合,包括其他Map)。例如:

    1. import java.util.Arrays;
    2. import java.util.HashMap;
    3. import java.util.List;
    4. import java.util.Map;
    5. import reflection.Person;
    6. public class MapOfList {
    7. public static final Mapextends Pet>> petPeople = new HashMap<>();
    8. static {
    9. petPeople.put(new Person("小红"),
    10. Arrays.asList(new Cymric("茉莉"), new Mutt("斑点")));
    11. petPeople.put(new Person("小绿"),
    12. Arrays.asList(new Cat("斯凯特"), new Dog("玛格特")));
    13. petPeople.put(new Person("小黄"),
    14. Arrays.asList(new Pug("路易斯·杜普里"),
    15. new Cat("斯丹福"),
    16. new Cat("粉色可乐")));
    17. }
    18. public static void main(String[] args) {
    19. System.out.println("宠物的主人:" + petPeople.keySet());
    20. System.out.println("宠物:" + petPeople.values());
    21. for (Person person : petPeople.keySet()) {
    22. System.out.println();
    23. System.out.println(person + "的宠物有:");
    24. for (Pet pet : petPeople.get(person))
    25. System.out.println(" " + pet);
    26. }
    27. }
    28. }

            程序执行的结果是:

    新特性:记录(record)类型

            使用Map,可以做到很多具有想象力的事。但如果要让一个类成为Map中的键,首先需要为这个类定义两个函数:equal()hashCode(),这无疑增加了对这个类的创建和维护成本。

            为此,JDK 16最终引入了record关键字。这一关键字定义的是希望成为数据传输对象(也叫数据载体)的类。当使用record关键字时,编译器会自动生成:

    • 不可变的字段
    • 一个规范的构造器
    • 每个元素都有的访问器方法
    • equals()
    • hashCode()
    • toString()

            例如:

    1. import java.util.Map;
    2. record Employee(String name, int id) {
    3. }
    4. public class BasicRecord {
    5. public static void main(String[] args) {
    6. var hong = new Employee("小红", 11);
    7. var lan = new Employee("小蓝", 9);
    8. // hong.id = 12; // 错误的使用:id在Employee中的访问权限是private
    9. System.out.println(hong.name()); // 需要使用访问器进行访问
    10. System.out.println(hong.id());
    11. System.out.println(hong);
    12. // Employee可以作为Map中的键
    13. var map = Map.of(hong, "A", lan, "B");
    14. System.out.println(map);
    15. }
    16. }

            程序执行的结果如下:

            record关键字会自动创建规范的构造器,并且会自动添加内部的private final字段nameid。构造器会根据提供的参数列表初始化字段。除此之外,使用record时还需注意:

    1. record中不能添加字段,但允许静态的成员(方法、字段和初始化器)。
    2. record的参数列表定义的每个属性都会自动获得自己的访问器

            就如之前提到的,record会自动创建合理定义的hashCode()equals()。这么做的方便之处在于,即使之后对record中的字段进行增删,这个类也可以正常进行工作。

            record中可以定义方法,但这些方法只能用于读取字段:

    1. record FinalFields(int i) {
    2. int timesTen() {
    3. return i * 10;
    4. }
    5. /*
    6. // 不能对final变量i进行赋值
    7. void tryToChange() {
    8. i++; // 编译器报错
    9. }
    10. */
    11. }

            除此之外,record的参数也可以是其他对象,例如:

    1. record Company(Employee[] e) { // 参数可以是对象
    2. }
    3. // class Conglomerate extends Company { // record不允许继承
    4. // }

            record不允许被继承,因为它是隐式的final。除此之外,record也不允许继承其他类。但record可以实现接口:

    1. interface Star {
    2. double brightness();
    3. double density();
    4. }
    5. record ImplementingRecord(double brightness) implements Star {
    6. @Override
    7. public double density() {
    8. return 100.0;
    9. }
    10. }

            在上述例子中,并没有实现接口的brightness()方法,但编译器没有报错。这是因为在record的参数中存在一个brightness,编译器会自动为这个参数生成对应的访问器,这个访问器刚好可以匹配Star接口中的brightness()

            record也可以被嵌套在类或某个方法中。嵌套和局部的record都是隐式静态的

    1. public class NestedLocalRecords {
    2. record Nested(String s) {
    3. }
    4. void method() {
    5. record Local(String s) {
    6. }
    7. }
    8. }

            虽然record会自动构建构造器,但我们依旧可以使用一个紧凑构造器来添加构造器行为,这种构造器常被用于验证参数。这种紧凑构造器是没有参数列表的。:

    1. record PlusTen(int x) {
    2. PlusTen { //无参的
    3. x += 10; // 对字段的调整只能在构造器中进行
    4. }
    5. // 无法在构造器外调整字段
    6. /* void mutate() {
    7. x += 10;
    8. }*/
    9. public static void main(String[] args) {
    10. System.out.println(new PlusTen(10));
    11. }
    12. }

            程序执行的结果如下:

            编译器会为x创建一个中间的占位符,然后在构造器的最后执行一次赋值,将结果赋值给this.x。若有必要,也可以使用普通构造器语法替换规范构造器

    1. record Value(int x) {
    2. Value(int x) { // 带有参数的普通构造器
    3. this.x = x;
    4. }
    5. }

            record会要求这个非紧凑构造器精确复制record的签名,包括标识符的名字。这意味着像Value(int init)之类的语句是不被允许的。除此之外,若使用的是非紧凑构造器,final字段x不会被初始化,所以若不在上述这个构造器中添加语句this.x = x,编译器将会报错。

            若需要复制一个record,必须将它的所有字段显式地传递给其构造器:

    1. record R(int a, double b, char c) {
    2. }
    3. public class CopyRecord {
    4. public static void main(String[] args) {
    5. var r1 = new R(11, 2.2, 'z');
    6. var r2 = new R(r1.a(), r1.b(), r1.c());
    7. System.out.println(r2);
    8. }
    9. }

            程序执行,返回true

        record在提高代码的可读性上也有显著作用。

  • 相关阅读:
    由投影仪到智能会议平板,经历怎样的发展过程?
    Springboot项目RestController中函数参数注解使用
    c++概述-语言特征
    mysql用事务实现更新数据
    说说React的事件机制?
    中考理化生实验操作考试平台
    JVM基础01
    04 C++ 表达式
    对@Bean注解的学习理解(大白话解释)
    为什么Python常年排行第一第二?
  • 原文地址:https://blog.csdn.net/w_pab/article/details/133135846