Java8 教程

Java8 集合类增强

Java8 中集合库的最大改变就是支持了前面介绍的流。同时,还有其他一些略小的改动。

下表列举了除 stream、parellelstream 和 spliterator 方法以外,Java8 中添加到集合类和接口中的其他方法:

类/接口

新方法

Iterable

forEach

Collection

removeIf

List

replaceAll、sort

Map

forEach、replace、replaceAll、remove(k,v) 仅当 k 和 v 映射存在时才删除 、putIfAbsent、compute、computeIfAbsent、computeIfPresent、merge

Iterator

forEachRemaining

BitSet

stream

你可能会奇怪,为什么 Stream 接口有那么多接受 lambda 表达式的方法,但是 Collection 接口只添加了一个 removelf。如果你仔细看过 Stream 中的方法,你就会发现它们中的大多数方法返回的是一个单独的值,或者是一个将原始流中的值经过转换的流(filter和 distinct 方法除外)。removelf 方法与 filter 方法相反,会删除所有匹配的值,并且是立即原地删除。distinct(去重)方法对任何一个集合来说消耗都是比较大的。

List 接口像 map 接口一样新增了一个 replaceAll 方法,此外,还增加了一个显然很有用的 sort 方法。

Map 接口有许多对于并发访问十分重要的方法。

Iterator 接口中有一个 forEachRemaining 方法,能够将剩余的迭代元素都传递给一个函数。

最后,BitSet 类有一个方法可以生成集合中的所有元素,返回一个由 int 值组成的流。

比较器

利用 Java8 新特性,接口也能拥有自己的具体实现方法(默认方法、静态方法)的特点,Comparator 接口新增了许多有用的方法。

静态方法 comparing  可以接受一个“键提取器”函数,将某类型 T 映射为一个可比较的类型 (例如 String)。该函数会被应用于进行比较的对象,随后会对返回的键进行比较。方法定义如下:

  • static <T,U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T,? extends U> keyExtractor)  接受一个从类型 T 中提取 Comparable 排序键的函数,并返回一个通过该排序键进行比较的 Comparator<T>。

  • static <T,U> Comparator<T> comparing(Function<? super T,? extends U> keyExtractor, Comparator<? super U> keyComparator)  接受一个从类型 T 中提取排序键的函数,并返回一个使用指定比较器按排序键进行比较的 Comparator<T>。

  • static <T> Comparator<T> comparingDouble(ToDoubleFunction<? super T> keyExtractor)  接受一个从类型 T 提取 double 类型的排序键的函数,并返回一个用该排序键进行比较的 Comparator<T>。

  • static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor)  接受一个从类型 T 提取 int 类型的排序键的函数,并返回一个通过该排序键进行比较的 Comparator<T>。

  • static <T> Comparator<T> comparingLong(ToLongFunction<? super T> keyExtractor)  接受一个从类型 T 提取 long 类型的排序键的函数,并返回一个通过该排序键进行比较的 Comparator<T>。

例如:假设你现在拥有一个 Person 对象的数组,下面是你按照姓名对其进行排序的代码:

package com.hxstrive.jdk8.collection;

import java.util.Arrays;
import java.util.Comparator;

/**
 * jdk8 collection 增强
 * @author hxstrive.com
 */
public class CollectionDemo1 {

    public static void main(String[] args) {
        Person[] persons = {
                new Person(1, "Tom", 20),
                new Person(2, "Bill", 20),
                new Person(3, "Harry", 20),
                new Person(4, "William", 27)
        };
        // 对 person 数组按照 name 排序
        Arrays.sort(persons, Comparator.comparing(Person::getName));
        // 输出排序结果
        Arrays.stream(persons).forEach(System.out::println);
        // 结果:
        //Person{id=2, name='Bill', age=20}
        //Person{id=3, name='Harry', age=20}
        //Person{id=1, name='Tom', age=20}
        //Person{id=4, name='William', age=27}
    }

    static class Person {
        private int id;
        private String name;
        private int age;

        public Person(int id, String name, int age) {
            this.id = id;
            this.name = name;
            this.age = age;
        }

        // 省略 getter 和 setter

        @Override
        public String toString() {
            return "Person{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

}

你还可以将比较器通过 thenComparing 方法连接起来,来进行多级比较。例如:

Person[] persons = {
        new Person(1, "Tom", 20),
        new Person(2, "Bill", 20),
        new Person(3, "Harry", 20),
        new Person(4, "William", 27)
};
// 对 person 数组按照 age,name 排序
Arrays.sort(persons, Comparator.comparing(Person::getAge)
        .thenComparing(Person::getName));
// 输出排序结果
Arrays.stream(persons).forEach(System.out::println);
// 结果:
//Person{id=2, name='Bill', age=20}
//Person{id=3, name='Harry', age=20}
//Person{id=1, name='Tom', age=20}
//Person{id=4, name='William', age=27}

如果两个人 age(年龄)一样,则会按照 name 继续排序。

当然,你也可以为 comparing 和 thenComparing 方法提取的键指定一个比较器。例如,下面我们可以按照人员姓名的长度来排序:

Person[] persons = {
        new Person(1, "Tom", 20),
        new Person(2, "Bill", 20),
        new Person(3, "Harry", 20),
        new Person(4, "William", 27)
};
// 对 person 数组按照 age,name 排序
Arrays.sort(persons, Comparator.comparing(Person::getName,
        (a, b) -> Integer.compare(a.length(), b.length())));
// 输出排序结果
Arrays.stream(persons).forEach(System.out::println);
// 结果:
//Person{id=1, name='Tom', age=20}
//Person{id=2, name='Bill', age=20}
//Person{id=3, name='Harry', age=20}
//Person{id=4, name='William', age=27}

此外,comparing 和 thenComparing 方法都有可以避免 int、long 或者 double 值装箱/拆箱的重载形式。如下代码可以更简单地实现上述操作:

Person[] persons = {
        new Person(1, "Tom", 20),
        new Person(2, "Bill", 20),
        new Person(3, "Harry", 20),
        new Person(4, "William", 27)
};
// 对 person 数组按照 age,name 排序
Arrays.sort(persons, Comparator.comparingInt(p -> p.getName().length()));
// 输出排序结果
Arrays.stream(persons).forEach(System.out::println);
// 结果:
//Person{id=1, name='Tom', age=20}
//Person{id=2, name='Bill', age=20}
//Person{id=3, name='Harry', age=20}
//Person{id=4, name='William', age=27}

如果你的键函数可以返回 null, 那么你一定会喜欢 nullsFirst 和 nullsLast 方法。这两个静态方法可以对已有的比较器进行修改,因此当遇到 null 值时,它们不会抛出异常,而是将 null 值看作是小于或大于正常值的值。例如,如果某人没有名字,getName 方法会返回 null。那么你可以使用:

Person[] persons = {
        new Person(1, "Tom", 20),
        new Person(2, "Bill", 20),
        new Person(3, null, 20),
        new Person(4, "William", 27)
};
// 对 person 数组按照 age,name 排序
Arrays.sort(persons, Comparator.comparing(Person::getName,
        Comparator.nullsFirst((a, b) -> Integer.compare(a.length(), b.length()))));
// 输出排序结果
Arrays.stream(persons).forEach(System.out::println);
// 结果:
//Person{id=3, name='null', age=20}
//Person{id=1, name='Tom', age=20}
//Person{id=2, name='Bill', age=20}
//Person{id=4, name='William', age=27}

上面例子中,nullFirst 方法需要一个比较器来比较两个字符串。方法定义如下:

  • static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator)  返回 null 友好的比较器,该比较器认为 null 小于非空。

  • static <T> Comparator<T> nullsLast(Comparator<? super T> comparator)  返回 null 友好的比较器,该比较器认为 nul 大于非空。

Collections 类

Java6 引入了 NavigableSet 和 NavigableMap 接口,通过利用元素或键的排序,对于任意指定 v, 能够高效定位 ≥ v 或 > v 的最小值,或者 ≤ v 或 < v 的最大值。现在 Collections 类也通过如下方法支持了这些接口:

  • static <K,V> NavigableMap<K,V> checkedNavigableMap(NavigableMap<K,V> m, Class<K> keyType, Class<V> valueType)  返回指定的可导航 map 的动态类型安全视图。

  • static <E> NavigableSet<E> checkedNavigableSet(NavigableSet<E> s, Class<E> type)  返回指定的可导航 set 的动态类型安全视图。

  • static <K,V> NavigableMap<K,V> emptyNavigableMap()  返回一个空的可导航 map (不可变)。

  • static <E> NavigableSet<E> emptyNavigableSet()  返回一个空的可导航 set (不可变)。

  • static <K,V> NavigableMap<K,V> synchronizedNavigableMap(NavigableMap<K,V> m)  返回由指定的可导航 map 支持的同步 (线程安全) 可导航 map。

  • static <T> NavigableSet<T> synchronizedNavigableSet(NavigableSet<T> s)  返回由指定的可导航 set 支持的同步 (线程安全) 可导航 set。

  • static <K,V> NavigableMap<K,V> unmodifiableNavigableMap(NavigableMap<K,? extends V> m)  返回指定的可导航 map  的不可修改视图。

  • static <T> NavigableSet<T> unmodifiableNavigableSet(NavigableSet<T> s)  返回指定的可导航 set 的不可修改视图。

例如:

// 创建一个原始的 NavigableSet 集合
NavigableSet<Integer> set = new TreeSet<>();
set.add(12);

// 使用 checkedNavigableSet 创建类型安全的集合
NavigableSet<Integer> checkedSet = Collections.checkedNavigableSet(set, Integer.class);

// 可以对 checkedSet 进行操作,它会在运行时进行类型检查
checkedSet.add(23);

// 以下操作会在编译时报错,因为类型不匹配
// checkedSet.add("abc");

Collections.checkedNavigableSet 常见的使用场景包括以下几种:

  • 多线程环境:在多线程程序中,当多个线程可能同时访问和操作一个NavigableSet时,使用checkedNavigableSet可以确保在编译时就进行类型检查,避免不同线程意外地添加错误类型的元素,增强了集合操作在多线程环境下的安全性和稳定性。

  • 代码维护和协作:在大型项目中,有多个开发者共同操作集合时,明确指定集合元素的类型并通过checkedNavigableSet进行类型安全检查,有助于防止因开发者对集合元素类型的理解不一致而导致的错误添加操作,使得代码维护和协作更加可靠。

  • 复杂数据结构处理:当处理具有特定类型要求且结构相对复杂的导航性集合(如按照特定顺序或规则存储和访问元素的场景)时,利用checkedNavigableSet来保证元素类型的正确性,避免运行时因类型不匹配导致的异常,提高程序的健壮性。

  • 防止意外错误:即使在开发过程中有基本的类型检查机制,checkedNavigableSet提供了额外的一层保障,能更严格地在编译阶段就捕捉到不符合预期类型的操作,提前发现潜在问题,而不是等到运行时才暴露错误。

同时,Collections 类还添加了一个被大家忽视很久的方法 —— 包装方法 checkedQueue。这里提醒一下,这个包装方法有一个 class 参数,当你插入一个错误类型的元素时会抛出一个 ClassCastException 异常。方法定义如下:

  • static <E> Queue<E> checkedQueue(Queue<E> queue, Class<E> type)  返回指定队列的动态类型安全视图。

这些类用来作为调试工具。假设你声明了一个 Queue<Path>, 并且在你的某段代码中,因为尝试将 String 类型转换为 Path 类型而抛出了一个 ClassCastException 异常。你将队列传递给了一个没有类型参数的方法 void getMoreWork(Queue q)。然后,某人在某处将一个 String 插入到了队列 q 中 (由于泛型会被忽略,所以编译器无法检测到这一点)。再然后,你将这个 String 取出来,还以为它是一个 Path, 于是错误产生了。如果你临时将队列换成一个CheckedQueue(new LinkedList<Path>, Path.class), 那么在运行时会检查每次插入,于是你可以定位到错误的插入代码。

最后,Collections 还提供了 emptySorted(Set|Map) 方法来返回有序集合的轻量级实例,方法定义如下:

  • static <K,V> SortedMap<K,V> emptySortedMap()  返回空的排序 map(不可变)。

  • static <E> SortedSet<E> emptySortedSet()  返回空的排序 set(不可变)。

例如:

public SortedSet<String> getUserSortedSet(String condition) {
    if (condition.isEmpty()) {
        return Collections.emptySortedSet();
    }
    // 其他处理逻辑
}

它的主要用途在于:

  • 作为函数返回值:当某个方法需要返回一个空的有序集合,但又不希望调用者能够对其进行修改时,可以使用这个方法。

  • 作为初始化值:在某些场景下,可能需要一个初始的空有序集合,并且后续不希望它被意外修改。

  • 用于强调不可修改性:当代码的逻辑要求一个集合始终保持为空且不可修改的状态时,使用 Collections.emptySortedSet() 能清晰地表达这种意图。

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
公众号