Java 集合(一)

一. Java 集合概述

  • Java 集合类的作用
    • 保存数量不确定的数据
    • 保存具有映射关系的数据
  • 因为集合类主要负责保存、盛装其他数据,所以也被称为容器类
  • 所有集合类都位于 java.util 包下,Java 5 还在 java.util.concurrent 包下提供了一些多线程支持的集合类,用以处理多线程环境下的并发安全问题。
  • 集合类和数组的区别
    • 数组元素既可以是基本类型的值,也可以是对象。集合里只能保存对象。这里说的对象实际上是保存对象的引用变量
    • 数组长度不可变化,集合长度可以变化。
  • Java 的集合类主要由两个接口派生而出:CollectionMap,它们是 Java 集合框架的根接口
    • 下图是 Collection 接口、子接口及其实现类的继承树。
      Java 集合(一)_第1张图片
      • Set 和 List 接口是 Collection 接口派生的两个子接口,它们分别代表无序集合有序集合。Queue 是 Java 提供的队列实现,有点类似于 List。
    • 下图是 Map 体系的继承树。
      Java 集合(一)_第2张图片
    • Map 保存的每项数据都是 key-value 对,其中 key 是不可重复的。因为 key 用于标识集合里的每项数据,查阅 Map 中的数据时,总是根据 Map 中的 key 来获取。
  • 我们可以将 Java 集合分为三类
    • Set 类类似于一个罐子
      • Set 集合无法记住添加元素的顺序
      • Set 集合里的元素不能重复
      • Set 中的元素只能根据元素本身来访问
    • List 集合非常像数组
      • List 可以记住每次添加元素的顺序
      • List 的长度可变。
      • List 集合中的元素可以根据元素的索引来访问
    • Map 集合也像一个罐子,只是它里面的每项数据都由两个值构成。
      • Map 中的的元素可以根据 key 访问 value。
    • 三种集合示意图
      Java 集合(一)_第3张图片

二. Collection 和 Iterator 接口

1. Collection 接口里定义的方法

  • boolean add(Object o):向集合中添加一个元素,如果集合对象被添加操作改变了,则返回 true。
  • boolean addAll(Collection c):将集合 c 中的所有元素添加到指定集合中。如果集合对象被改变,则返回 true。
  • void clear():清除集合中的所有元素,将集合长度变为 0。
  • boolean contains(Object o):返回集合里是否包含指定元素。
  • boolean containsAll(Collection c):返回集合里是否包含集合 c 里的所有元素。
  • boolean isEmpty():返回集合是否为空。集合长度为 0 时返回 true,否则返回 false。
  • Iterator iterator():返回一个 Iterator 对象,用于遍历集合中的所有元素。
  • boolean remove(Object o):删除集合中的指定元素 o,集合中包含多个元素 o 时,该方法只删除第一个符合条件的元素,该方法将返回 true。
  • boolean removeAll(Collection c):从集合中删除集合 c 中包含的所有元素。如果删除了一个或一个以上的元素,则该方法返回 true。
  • boolean retainAll(Collection c):从集合中删除集合 c 里不包含的元素,如果该操作改变了调用该方法的集合,则该方法返回 true。
  • int size():该方法返回集合里元素的个数。
  • Object[] toArray():该方法将集合转换为一个数组,所有的集合元素变成对应的数组元素。

2. 使用 Lambda 表达式遍历集合

  • Java 8 为 Iterable 接口新增了一个 forEach(Consumer action) 默认方法,该方法所需参数类型是一个函数式接口。
  • Iterable 接口是 Collection 接口的父接口,所以 Collection 集合也可以直接调用该方法。
  • 程序调用 Iterable 的 forEach(Consumer action) 遍历集合元素时,程序会依次将集合元素传给 Consumer 的 accept(T t) 方法。正因为 Consumer 是函数式接口,因此可以使用 Lambda 表达式。
    示例程序如下:
public class CollectionEach {
    public static void main(String[] args) {

        Collection books = new HashSet();

        books.add("傲慢与偏见");
        books.add("呼啸山庄");
        books.add("禅与摩托车维修艺术");

        boos.forEach(obj -> System.out.println("迭代集合元素:" + obj));
    }
}

3. 使用 Java 8 增强的 Iterator 遍历集合元素

  • Iterator 接口也是 Java 集合框架的成员,但它与 Collection 系列、Map 系列的集合不同
    • Collection 系列集合、Map 系列集合主要用于盛装其他对象,Iterator 主要用于遍历 Collection 集合中的元素。
  • Iterator 对象也被称为迭代器
  • Iterator 接口方法:
    • boolean hasNext():若被迭代的集合元素还没有遍历完,则返回 true。
    • Object next():返回集合里的下一个元素。
    • void remove():删除集合里上一次 next 方法返回的元素。
    • void forEachRemaining(Consumer action):此方法可使用 Lambda 表达式来遍历集合元素。
  • 示例程序:
import java.util.*;

public class IteratorTest {
    public static void main(String[] args) {
        Collection books = new HashSet();

        books.add("傲慢与偏见");
        books.add("呼啸山庄");
        books.add("禅与摩托车维修艺术");

        // 获取 books 集合对应的迭代器。注意:迭代器是通过集合获取的。
        Iterator it = books.iterator();

        while(it.hasNext()) {
            // it.next() 方法返回的是 Object 类型,因此需要强制类型转换
            String book = (String) it.next();
            System.out.println(book);

            if(book.equals("呼啸山庄")) {
                // 从集合中删除上一次 next() 方法返回的元素
                it.remove();
            }
            // 对 book 变量赋值,不会改变集合元素本身
            book = "测试字符串";
        }
        System.out.println(books);
    }
}

输出:
    禅与摩托车维修艺术
    傲慢与偏见
    呼啸山庄
    [禅与摩托车维修艺术, 傲慢与偏见]
  • 从上面代码可以看出:
    • Iterator 仅用于遍历集合,Iterator 本身并不提供盛装对象的能力。
    • 如果需要创建 Iterator 对象,则必须有一个被迭代的集合。
    • 使用 Iterator 进行迭代时,只是值传递。并不是把集合元素本身传给迭代变量,只是将集合元素的值传给迭代变量,所以修改迭代变量对集合元素本身没有影响。
  • 使用 Iterator 迭代访问 Collection 集合元素时,除使用 Iterator 的 remove() 方法删除元素外,Collection 集合里的元素不能被改变,否则会引发 java.util.ConcurrentModificationException 异常。
    • Iterator 迭代器采用快速失败(fail-fast)机制,一旦迭代过程中检测到该集合已被修改,立刻引发 ConcurrentModificationException 异常。

4. 使用 Lambda 表达式遍历 Iterator

  • Java 8 为 Iterator 新增了一个 forEachRemaining(Consumer action) 方法,该方法所需的 Consumer 参数是函数式接口,程序调用 Iterator 的 forEachRemaining(Consumer action) 方法遍历集合元素时,程序会依次将集合元素传给 Consumer 的 accept(T t) 方法。
  • 示例程序:
import java.util.*;

import javax.swing.plaf.synth.SynthSeparatorUI;

public class IteratorTest {
    public static void main(String[] args) {
        Collection books = new HashSet();

        books.add("傲慢与偏见");
        books.add("呼啸山庄");
        books.add("禅与摩托车维修艺术");

        // 获取 books 集合对应的迭代器。注意:迭代器是通过集合获取的。
        Iterator it = books.iterator();

        it.forEachRemaining(obj -> System.out.println("迭代集合元素:" + obj));
    }
}

输出结果:
    迭代集合元素:禅与摩托车维修艺术
    迭代集合元素:傲慢与偏见
    迭代集合元素:呼啸山庄

5. 使用 foreach 循环遍历集合元素

  • 用 foreach 循环来遍历集合元素会更方便
  • 示例程序:
import java.util.*;

public class ForeachTest {
    public static void main(String[] args) {
        Collection books = new HashSet();

        books.add("傲慢与偏见");
        books.add("呼啸山庄");
        books.add("禅与摩托车维修艺术");

        for (Object obj : books) {
            String book = (String) obj;
            System.out.println(book);
            if(book.equals("呼啸山庄")) {
                books.remove(book);  //①
            }
        }

        System.out.println(books);
    }
}

输出结果:
    禅与摩托车维修艺术
    傲慢与偏见
    呼啸山庄
    [禅与摩托车维修艺术, 傲慢与偏见]
  • foreach 循环中的迭代变量也不是集合元素本身,只是值传递。改变迭代变量的值没有意义
  • 使用 foreach 循环迭代访问集合元素的过程中,该集合也不能被改变,否则引发 ConcurrentModificationException 异常。上面程序中 ① 处代码将引发该异常。

6. 使用 Java 8 新增的 Predicate 操作集合

  • Java 8 为 Collection 集合新增了一个 removeIf(Predicate filter) 方法。该方法会批量删除符合 filter 条件的所有元素。

    • 此方法需要一个 Predicate(谓词)对象作为参数,Predicate 是函数式接口,其中只有一个 test 抽象方法,可以使用 Lambda 表达式。
    • 示例程序:
      import java.util.*;
      
      public class PredicateTest {
          public static void main(String[] args) {
              Collection books = new HashSet();
      
              books.add("傲慢与偏见");
              books.add("呼啸山庄");
              books.add("禅与摩托车维修艺术");
      
              books.removeIf(ele -> ((String)ele).length() > 5);
      
              System.out.println(books);
          }
      }
      
      输出结果:
          [傲慢与偏见, 呼啸山庄]
  • 使用 Predicate 可以充分简化集合的运算,假设有下面三个需求:

    • 统计书名中出现“八年级”字符串的图书数量
    • 统计书名中出现“英语”字符串的图书数量
    • 统计书名长度大于 5 的图书数量
    • 示例程序:
      import java.util.*;
      import java.util.function.Predicate;
      
      public class PredicateTest2 {
          public static void main(String[] args) {
              Collection books = new HashSet();
      
              books.add("傲慢与偏见");
              books.add("呼啸山庄");
              books.add("禅与摩托车维修艺术");
              books.add("八年级语文");
              books.add("八年级数学");
              books.add("八年级英语");
      
              // 统计书名中出现“八年级”字符串的图书数量
              System.out.println(calAll(books, ele -> ((String)ele).contains("八年级")));
              // 统计书名中出现“英语”字符串的图书数量
              System.out.println(calAll(books, ele -> ((String)ele).contains("英语")));
              // 统计书名长度大于 5 的图书数量
              System.out.println(calAll(books, ele -> ((String)ele).length() > 5));
          }
      
          public static int calAll(Collection books, Predicate p) {
              int total = 0;
      
              for(Object obj : books) {
                  // 使用 Predicate 的 test 方法判断该对象是否满足 Predicate 指定的条件
                  if(p.test(obj)) {
                      total++;
                  }
              }
      
              return total;
          }
      }
      
      输出结果:
          3
          1
          1

    可以看到,如果用传统的编程方式来完成这些需求,需要执行三次循环。但采用 Predicate 只需要一个方法。

7. 使用 Java 8 新增的 Stream 操作集合

  • Java 8 新增了 Stream、IntStream、LongStream、DoubleStream 等流式 API,这些 API 代表多个支持串行和并行聚集操作的元素。
    • Stream 是一个通用的流接口;
    • IntStream 是元素类型为 int 的流;
    • LongStream 是元素类型为 long 的流;
    • DoubleStream 是元素类型为 double 的流。
  • Java 8 还为上面的每个流式 API 提供了对应的 Builder,开发者可以通过这些 Builder 来创建对应的流
    • Stream.Builder
    • IntStream.Builder
    • LongStream.Builder
    • DoubleStream.Builder
  • 独立使用 Stream 的步骤:
    1. 使用 Stream 或 XxxStream 的 builder() 类方法创建该 Stream 对应的 Builder;
    2. 重复调用 Builder 的 add() 方法向该流中添加多个元素;
    3. 调用 Builder 的 build() 方法获取对应的 Stream;
    4. 调用 Stream 的聚集方法(对大多数聚集方法而言,每个 Stream 只能执行一次)。
  • 示例程序:

    import java.util.stream.IntStream;
    
    public class IntStreamTest {
        public static void main(String[] args) {
            IntStream is = IntStream.builder()
                    .add(20)
                    .add(13)
                    .add(-2)
                    .add(18)
                    .build();
    
            // ①
            System.out.println("is 所有元素的最大值:" + is.max().getAsInt());
            System.out.println("is 所有元素的最小值:" + is.min().getAsInt());
            System.out.println("is 所有元素的总和:" + is.sum());
            System.out.println("is 所有元素的总数:" + is.count());
            System.out.println("is 所有元素的平均值:" + is.average());
            System.out.println("is 所有元素的平方是否都大于 20:"
                    + is.allMatch(ele -> ele * ele > 20));
            System.out.println("is 是否包含任何元素的平方大于 20:"
                    + is.anyMatch(ele -> ele * ele > 20));
            // 将 is 映射成一个新的 Stream,新 Stream 每个元素是原 Stream 元素的 2 倍 + 1
            IntStream newIs = is.map(ele -> ele * 2 + 1);
            // 使用方法引用的方式来遍历集合元素
            newIs.forEach(System.out::println);
            // ②
        }
    }

    注意:① 和 ② 之间的代码每次只能执行一句。

  • Stream 提供了大量方法进行聚集操作。这些方法既可以是“中间的”(intermediate),也可以是“末端的”(terminal)
    • 中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法中间方法的返回值是另一个流
    • 末端方法:末端方法是对流的最终操作。当对某个 Stream 执行末端方法后,该流将会被“消耗”,不可再用。上面程序中的 sum()、count()、average() 等方法都是末端方法。
  • 关于流的方法还有以下两个特征:
    • 有状态的方法:这种方法会给流增加一些新的属性,譬如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销
    • 短路方法:短路方法可以尽早结束对流的操作,不必检查所有的元素
  • Stream 中常用的中间方法:
    • filter(Predicate predicate):过滤 Stream 中所有不符合 predicate 的元素。
    • mapToXxx(ToXxxFunction mapper):使用 ToXxxFunction 对流中的元素执行一对一的转换,该方法返回的新流中包含了 ToXxxFunction 转换生成的所有元素。
    • peek(Consumer action):依次对每个元素执行一些操作,该方法返回的流与原有流包含相同的元素。此方法主要用于调试
    • distinct():该方法用于排序流中所有重复的元素(判断元素重复的标准是使用 equals() 比较返回 true)。这是一个有状态的方法
    • sorted():该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法
    • limit(long maxSize):该方法用于保证对该流的后续访问中最大允许访问的元素。这是一个有状态的,短路的方法
  • Stream 常用的末端方法
    • forEach(Consumer action):遍历流中所有元素,对每个元素执行 action;
    • toArray():将流中所有元素转换为一个数组;
    • reduce():该方法有三个重载的版本,都用于通过某种操作来合并流中的元素;
    • min():返回流中所有元素的最小值;
    • max():返回流中所有元素的最大值;
    • count():返回流中所有元素的数量;
    • anyMatch(Predicate predicate):判断流中是否至少包含一个元素符合 Predicate 条件;
    • allMatch(Predicate predicate):判断流中是否每个元素都符合 Predicate 条件;
    • noneMatch(Predicate predicate):判断流中是否每个元素都不符合 Predicate 条件;
    • findFirst():返回流中第一个元素;
    • findAny():返回流中任意一个元素。
  • Java 8 允许用流式 API 来操作集合,Collection 接口提供了一个 stream() 默认方法,该方法可返回该集合对应的流,接下来就可以通过流式 API 来操作集合元素。由于 Stream 可以对集合元素进行整体的聚集操作,因此 Stream 极大地丰富了集合功能。

    • 上面的程序需要额外定义一个 calAll() 方法来遍历集合元素,然后依次对每个集合元素进行判断。如果使用 Stream,即可直接对集合中所有元素进行批量操作,更为便捷。
      import java.util.*;
      
      public class CollectionStream {
          public static void main(String[] args) {
              Collection books = new HashSet();
      
              books.add("傲慢与偏见");
              books.add("呼啸山庄");
              books.add("禅与摩托车维修艺术");
              books.add("八年级语文");
              books.add("八年级数学");
              books.add("八年级英语");
      
              // 统计书名中出现“八年级”字符串的图书数量
              System.out.println(books.stream()
                      .filter(ele -> ((String)ele).contains("八年级"))
                      .count());
              // 统计书名中出现“英语”字符串的图书数量
              System.out.println(books.stream()
                      .filter(ele -> ((String)ele).contains("英语"))
                      .count());
              // 统计书名长度大于 5 的图书数量
              System.out.println(books.stream()
                      .filter(ele -> ((String)ele).length() > 5)
                      .count());
              // 先调用 Collection 对象的 stream() 方法将集合转换为 Stream
              // 再调用 Stream 的 mapToInt() 方法获取原有的 Stream 对应的 IntStream
              books.stream().mapToInt(ele -> ((String)ele).length())
                  // 调用 forEach() 方法遍历 IntStream 中每个元素
                  .forEach(System.out::println);
          }
      }
      
      输出结果:
          3
          1
          1
          9
          5
          5
          5
          4
          5

你可能感兴趣的:(Java学习,java)