本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:JDK 1.8是Java开发中一个重要的里程碑,提供了Lambda表达式、函数式接口、Stream API、默认方法等创新特性。这份手册旨在为Java开发者提供全面的JDK 1.8特性和API指南,覆盖了从Lambda表达式的使用到Date和Time API增强等关键知识点。通过中文资料,开发者可以更易于理解和应用JDK 1.8的新特性,提升开发效率和代码质量。 jdk1.8 中文开发手册

1. JDK 1.8特性概览

Java Development Kit (JDK) 1.8,也被称作Java 8,是Java语言的一次重要更新,引入了函数式编程范式的关键特性,包括Lambda表达式、函数式接口、Stream API等。这些新特性不仅提高了Java语言的表达能力,还提升了开发效率,特别是在集合数据处理和并发编程方面。

在本章中,我们将简要介绍JDK 1.8的一些主要特性,为后续章节中对Lambda表达式、函数式接口、Stream API等的深入探讨打下基础。我们会从语言层面的变化讲起,逐步过渡到对新API的介绍,帮助读者建立起对JDK 1.8改进的全局视角。

接下来,让我们从JDK 1.8新增的Lambda表达式开始,进一步探索其在简化代码、提高开发效率方面带来的变革。

2. Lambda表达式及其应用

2.1 Lambda表达式基础

2.1.1 Lambda表达式的定义与语法

Lambda表达式是Java 8中引入的一个重要特性,它允许我们以函数式的方式传递行为,大大简化了代码,尤其是在处理集合和多线程等场景时。Lambda表达式的语法非常简洁,它由参数列表、箭头符号 -> 和函数体三部分组成。例如:

// Lambda表达式写法
Comparator<String> comparator = (String s1, String s2) -> s1.compareTo(s2);

在这个例子中, (String s1, String s2) 是参数列表, -> 是Lambda箭头符号, s1.compareTo(s2) 是函数体。如果Lambda表达式体只包含一条语句,可以省略大括号,但如果函数体包含多条语句,则需要使用大括号。

2.1.2 Lambda与匿名类的区别

与Lambda表达式最直接的对比是匿名类,它们都可以用来创建匿名的实例。但Lambda表达式与匿名类在以下几个方面有显著的区别:

  • 语法简洁 :Lambda表达式提供了一种更简洁的方式来表达单一抽象方法的接口(SAM)实现。而匿名类则需要更多的代码来实现相同的功能。

  • 类型推断 :Lambda表达式允许使用类型推断,不需要显式声明参数类型。而匿名类通常需要显式声明类型。

  • 实例化方式 :Lambda表达式不能直接创建类的实例,但可以实现接口。匿名类可以创建任何类的实例,包括接口、抽象类或具体类。

  • this关键字 :在Lambda表达式中, this 关键字指向创建Lambda表达式的外部类。在匿名类中, this 指向匿名类自己。

2.2 Lambda表达式在集合中的应用

2.2.1 集合的遍历和操作简化

Lambda表达式极大地简化了集合的操作,特别是在遍历和简单操作上。我们可以使用Lambda表达式来替代传统的for循环和匿名类的迭代器。例如,使用Lambda表达式遍历List集合:

List<String> list = Arrays.asList("Apple", "Banana", "Cherry");
list.forEach(item -> System.out.println(item));

在上面的例子中,我们使用了 forEach 方法配合Lambda表达式来遍历集合中的每个元素并打印出来。这种方式比传统的for循环更加简洁明了。

2.2.2 集合排序和自定义比较器

Lambda表达式还可以用于自定义排序逻辑。例如,使用Lambda表达式对字符串列表进行排序:

List<String> list = Arrays.asList("Banana", "Apple", "Cherry");
list.sort((s1, s2) -> s1.length() - s2.length());

在这个例子中,我们使用了 sort 方法配合Lambda表达式来根据字符串长度进行排序。

2.3 Lambda表达式在多线程中的应用

2.3.1 线程创建与执行的新方式

在Java 8之前,创建和执行线程的传统方式是通过扩展 Thread 类或实现 Runnable 接口。Lambda表达式的引入提供了一种更简洁的方式来创建和启动线程。例如:

Thread thread = new Thread(() -> System.out.println("Thread example"));
thread.start();

这里,我们通过Lambda表达式创建了一个线程,并在其中执行打印语句。

2.3.2 并行流的使用与注意事项

Lambda表达式还与Stream API紧密集成,可以轻松创建并行流以并行处理集合中的元素。例如:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = numbers.parallelStream().reduce(0, Integer::sum);

在这个例子中,我们使用 parallelStream() 方法创建了一个并行流,并用 reduce 方法计算了列表中所有数字的总和。并行流对于处理大数据集合非常有用,但应该注意数据分割和合并的开销,并非所有的操作都适合并行化。

3. 函数式接口介绍

在这一章中,我们将深入探讨函数式接口的核心概念,包括它们是什么以及如何在实际应用中创建和使用它们。函数式接口作为Lambda表达式的基石,为Java 8引入的函数式编程特性提供了基础。我们将按照以下结构展开讨论:

3.1 函数式接口概念解析

3.1.1 什么是函数式接口

函数式接口是Java语言中的一个术语,指的是只包含一个抽象方法声明的接口。它们类似于其他语言中的单抽象方法接口(SAM)类型。函数式接口的重要性在于,它们为Lambda表达式提供了目标类型。换句话说,函数式接口定义了Lambda表达式的类型签名。由于Java 8引入的Lambda表达式,需要一个具体的目标类型来进行实例化,因此函数式接口成为了实现此目的的理想选择。

从技术角度看,任何符合上述条件的接口都可以被视为函数式接口,即使它包含默认方法和静态方法。但是,通常函数式接口还包含一个名为 @FunctionalInterface 的注解,这是Java提供的一个标识,用于明确指示接口设计为函数式接口,确保接口中只有一个抽象方法。

3.1.2 函数式接口的典型实例

Java中的函数式接口被广泛应用于各种场景,包括集合操作、多线程任务以及数据处理等。 java.util.function 包中定义了许多标准的函数式接口,如 Consumer<T> , Supplier<T> , Function<T,R> , Predicate<T> UnaryOperator<T> 。这些接口是构建Lambda表达式的强大工具。

例如, Function<T,R> 接口表示接受一个参数并返回一个结果的函数,其签名为 R apply(T t) 。这在需要将数据从一种类型转换为另一种类型时非常有用。而 Predicate<T> 用于定义一个布尔表达式,接受参数并返回一个布尔值,常用于筛选或测试集合中的元素。

3.2 常用函数式接口详解

3.2.1 Predicate、Consumer、Function接口

Predicate 接口

Predicate<T> 是一个可以应用于任何类型的布尔函数,定义了方法 boolean test(T t) 。它通常用于对对象进行条件测试,比如在集合的 stream 操作中过滤元素。

// 使用Predicate过滤集合中长度大于5的字符串
List<String> list = Arrays.asList("Hello", "Java", "Lambda", "World");
List<String> filteredList = list.stream()
                                .filter(str -> str.length() > 5)
                                .collect(Collectors.toList());

Consumer 接口

Consumer<T> 接口代表了对数据的消费操作,定义了 void accept(T t) 方法。它经常用于对集合中的元素执行某种操作,而不返回任何值。

// 使用Consumer消费集合中的每个元素
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println("Processing " + name));

Function 接口

Function<T,R> 接口接受一个参数并返回一个结果,定义了 R apply(T t) 方法。它在需要将一个类型转换为另一个类型时非常有用。

// 使用Function将字符串转换为大写
List<String> strings = Arrays.asList("apple", "banana", "cherry");
List<String> upperCaseStrings = strings.stream()
                                        .map(String::toUpperCase)
                                        .collect(Collectors.toList());

3.2.2 Supplier、UnaryOperator、BinaryOperator接口

Supplier 接口

Supplier<T> 接口不接受参数,返回一个结果,通常用于生成数据。其方法是 T get()

// 使用Supplier生成当前日期
Supplier<LocalDate> todaySupplier = LocalDate::now;
LocalDate today = todaySupplier.get();

UnaryOperator 接口

UnaryOperator<T> 是一个特殊的 Function<T,T> ,表示对单一操作数的应用。适用于对数据进行单一操作并返回相同类型的值。

// 使用UnaryOperator对数字进行平方操作
UnaryOperator<Integer> square = x -> x * x;
int squared = square.apply(10);

BinaryOperator 接口

BinaryOperator<T> UnaryOperator 类似,只不过它接受两个操作数。它有三个方法: T apply(T t1, T t2) T max(T t1, T t2) T min(T t1, T t2) 。它是 BiFunction<T,T,T> 的一个特化版本,适用于对两个值进行操作并返回相同类型的结果。

// 使用BinaryOperator计算两个数的和
BinaryOperator<Integer> sum = (x, y) -> x + y;
int result = sum.apply(1, 2); // result is 3

3.3 自定义函数式接口

3.3.1 设计自定义函数式接口的准则

自定义函数式接口时,应当遵循几个基本准则:

  • 单一抽象方法原则 :接口必须只定义一个抽象方法,这保证了它是函数式接口。
  • 使用 @FunctionalInterface 注解 :通过此注解声明,可以确保接口符合函数式接口的设计要求,同时也会在设计上有误时提供编译时警告。
  • 包含合理的默认实现 :如果接口需要具有可重用的逻辑,那么使用默认方法和静态方法作为工具方法是合适的。

3.3.2 实际场景中的自定义函数式接口应用

自定义函数式接口可以应用于那些现有的函数式接口不能满足的特定场景。例如,你可能想要定义一个自定义的 Comparator 接口,它具有额外的比较逻辑。

@FunctionalInterface
public interface CustomComparator<T> {
    int compare(T a, T b);

    default CustomComparator<T> reversed() {
        return (a, b) -> compare(b, a);
    }
}

// 使用自定义Comparator
CustomComparator<String> comparator = (a, b) -> a.length() - b.length();
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Collections.sort(names, comparator);

以上示例中, CustomComparator 接口扩展了Java中现有的 Comparator 接口,提供了额外的 reversed 方法作为默认实现。这允许用户在创建比较器时进行扩展。

在自定义函数式接口时,应仔细考虑它们的设计,以确保它们可以灵活地被用于各种上下文中,并且遵循函数式接口的设计最佳实践。

4. Stream API的使用与实践

4.1 Stream API基础

4.1.1 Stream的创建与数据源

在Java 8中,引入了Stream API作为Java集合框架的一部分,用于支持函数式编程的特性。Stream是一种支持连续、并行处理的元素序列。我们可以从各种数据源创建Stream,常见的数据源包括集合、数组以及I/O通道。

创建Stream最直接的方法是从集合开始,使用集合提供的 .stream() 方法。例如,创建一个简单的Stream可以这样写:

List<String> list = Arrays.asList("Java", "8", "Stream", "API");
Stream<String> stream = list.stream();

除了集合,数组也可以转换成Stream,Java提供了 Arrays.stream() 方法来实现这一操作:

String[] array = new String[]{"Java", "8", "Stream", "API"};
Stream<String> streamFromArray = Arrays.stream(array);

对于基本类型数据,Java同样提供了对应的Stream实现,如 IntStream , LongStream , DoubleStream 等,以便更高效地处理基本类型数据。

4.1.2 中间操作与终止操作

Stream API的强大之处在于它提供的中间操作和终止操作的组合。中间操作如 filter , map , sorted 等,返回的是另一个Stream,允许链式调用。终止操作如 collect , forEach , reduce 等,会触发实际的计算并返回结果。

中间操作一般具有延迟执行的特性,也就是说,中间操作不会立即执行,而是在终止操作被调用时,整个Stream的处理流程才真正开始执行。

List<String> filteredList = list.stream()
                               .filter(s -> s.contains("Stream"))
                               .map(String::toUpperCase)
                               .sorted()
                               .collect(Collectors.toList());

上述代码中, filter , map , 和 sorted 方法都是中间操作,它们共同构建了一个Stream处理流程。只有当 collect 这个终止操作被调用时,所有的中间操作才会按照定义的流程被依次执行。

4.2 Stream API高级特性

4.2.1 并行流的原理与应用

并行流是Stream API中一个重要的特性,它可以利用多核处理器的优势,将任务分拆成并行执行的多个子任务,从而达到提高处理效率的目的。

为了创建并行流,我们只需要在流上调用 .parallelStream() 方法即可。一旦转换为并行流,所有的中间操作都会尽可能以并行的方式执行,而终止操作会将结果汇总到单个结果中。

Stream<String> parallelStream = list.parallelStream();

并行流的原理是将Stream分割为若干子流,然后在不同的线程上并行处理这些子流,最后将处理结果合并。在JDK内部,它使用了 ForkJoinPool 来执行并行任务。

要注意的是,并行流并不一定总能带来性能提升,它适合于数据量大且可以高效并行处理的场景。对于数据量小或计算复杂度高的情况,并行流可能因为线程调度的开销而不带来预期的性能提升。

4.2.2 无序流的使用与控制

Stream API也允许我们创建无序流( unordered ),这在某些情况下可以提升处理效率。比如,在处理不需要保持元素顺序的任务时,无序流可以减少排序操作的计算开销。

Stream<String> unorderedStream = list.parallelStream().unordered();

无序流不会保证流中的元素顺序,特别是对于并行流,无序流可以更好地利用并行处理的优势。在某些情况下,无序流还可以带来更好的性能提升。

然而,无序流并不意味着所有的操作都是无序的,例如,操作的结果仍然可能受到流中元素初始顺序的影响。如果需要在无序流中执行有序操作,可以显式调用 sorted() 方法来确保结果的顺序。

4.3 Stream API的实践应用

4.3.1 复杂数据处理的案例分析

在实际应用中,Stream API非常适合于处理复杂的数据操作需求。例如,当我们需要从一组复杂对象中提取特定信息并进行统计时,可以链式调用多个中间操作,然后用终止操作完成整个处理流程。

假设我们有一个商品列表,每个商品有名称、价格和类型等属性。我们希望统计每种类型商品的平均价格。

class Product {
    private String name;
    private double price;
    private String type;
    // Getters and setters...
}

List<Product> products = // ... 初始化商品列表 ...

Map<String, Double> averagePricePerType = products.stream()
                                                  .collect(Collectors.groupingBy(
                                                      Product::getType,
                                                      Collectors.averagingDouble(Product::getPrice)
                                                  ));

在这个案例中,我们使用了 groupingBy 收集器将商品按类型分组,并计算每组商品的平均价格。这一过程通过中间操作构建了数据处理流程,而 collect 方法触发了终止操作并返回最终结果。

4.3.2 与Lambda表达式的结合实例

Stream API的许多操作都是以Lambda表达式的形式存在的。Lambda表达式的简洁性和表达能力极大地增强了Stream API的易用性和表达力。

举个例子,假设我们要过滤出价格高于100元的商品,并输出它们的名称:

products.stream()
        .filter(p -> p.getPrice() > 100)
        .map(Product::getName)
        .forEach(System.out::println);

在这个例子中,我们使用了 filter 操作来筛选出价格高于100元的商品,使用 map 操作将商品对象映射到它们的名称上,最后使用 forEach 操作输出每个商品的名称。通过Lambda表达式,我们可以直接在方法参数中定义简短的逻辑代码块,避免了编写复杂的匿名内部类。

总的来说,Stream API与Lambda表达式的结合,使得Java在处理集合数据时更加简洁和高效。通过将数据处理流程分解为一系列操作的组合,代码的可读性和可维护性得到了极大的提升。

5. 接口中的默认方法与静态方法

5.1 默认方法的引入与作用

5.1.1 默认方法的概念及其语法

Java 8中引入的默认方法是接口中的一个新特性,它允许我们在接口中添加新的方法,同时不需要改变已经实现这些接口的类。默认方法通过使用 default 关键字进行声明,并且必须提供一个默认的实现。这使得Java接口变得更加灵活,并允许接口随着时间的推移而演进,而不会破坏现有的实现。

public interface MyInterface {
    // ... 其他方法 ...

    default void newMethod() {
        System.out.println("This is a default method.");
    }
}

5.1.2 解决单继承问题的方案

在Java中,类只能继承一个类,但可以实现多个接口。默认方法的一个主要用途就是解决Java单继承的限制问题。通过在接口中定义默认方法,我们可以为实现接口的类提供默认行为,这样即使它们继承自其他类,也可以使用这些默认方法而无需重新实现。

例如,Java 8为 Collection 接口添加了多个默认方法,如 forEach , removeIf , replaceAll 等,这些方法为所有实现了 Collection 接口的类提供了额外的功能。

5.2 静态方法在接口中的应用

5.2.1 接口静态方法的定义和特点

静态方法在接口中的引入是Java 8的另一个重要特性。通过在接口中使用 static 关键字,我们可以定义静态方法,这些方法可以直接通过接口名调用,而不需要通过类的实例。这种特性进一步增加了接口的灵活性和实用性。

public interface MyInterface {
    // ... 其他方法 ...

    static void staticMethod() {
        System.out.println("This is a static method in an interface.");
    }
}

静态方法只能在接口内部被调用,并且不能被类覆盖。它们通常用于提供工具方法或辅助方法,类似于类中的静态方法。

5.2.2 接口静态方法的使用场景

接口静态方法的一个典型使用场景是为接口提供辅助函数。例如, Comparator 接口中就提供了一些静态方法,如 Comparator.comparing ,它允许我们创建基于特定属性比较的比较器。

Comparator<Item> byPrice = Comparator.comparing(Item::getPrice);
Comparator<Item> byName = Comparator.comparing(Item::getName);

这些静态方法提供了快速创建实现的便捷方式,尤其在需要快速定义比较逻辑时非常有用。

5.3 默认方法和静态方法的实践案例

5.3.1 框架升级与向下兼容的处理

默认方法特别适合在需要升级现有框架或库而又希望保持向后兼容性时使用。假设我们有一个名为 MyList 的接口,它扩展了一个已经被广泛使用的库中的 List 接口。如果我们想要添加一个新的方法,可以这样做:

public interface MyList<E> extends List<E> {
    // ... 其他MyList特有的方法 ...

    default void sortListByCustomRule() {
        // 实现自定义排序规则
    }
}

这样,所有实现了 MyList 接口的类会自动获得 sortListByCustomRule 方法,而不需要修改现有的代码。

5.3.2 接口工具方法的最佳实践

静态方法的引入也允许我们将工具方法放在更合适的地方。例如,如果有一个 CollectionUtils 类,我们可以将其方法作为接口静态方法。

public interface CollectionUtils {
    static boolean isNotEmpty(Collection<?> coll) {
        return !coll.isEmpty();
    }
}

然后,我们可以直接通过接口名调用这个方法,而不是创建 CollectionUtils 的实例:

if (CollectionUtils.isNotEmpty(myCollection)) {
    // 执行操作
}

这使得代码更加简洁,并且静态方法的查找效率也更高。

以上所述展示了Java 8接口中默认方法和静态方法的引入和应用,以及它们如何在实际开发中提供解决方案和便利性。通过这些新特性,Java程序员能够更好地管理代码库的演进和维护,同时利用函数式编程的优势。

6. 方法引用与日期时间API的更新

随着Java 8的发布,Java语言引入了一些新的特性和API,极大地增强了开发人员处理数据的能力。本章将重点介绍方法引用这一特性以及日期时间API的更新,这两个新特性对于编写简洁、高效且易于理解的代码大有裨益。

6.1 方法引用的原理与分类

方法引用是Java 8中引入的一种新特性,它允许我们直接引用现有的方法,并将其作为Lambda表达式的替代。这种特性特别适用于那些只调用一个方法的Lambda表达式。

6.1.1 方法引用的定义和语法

方法引用提供了引用方法而不调用该方法的一种简洁方式。它通过使用双冒号操作符 :: 来实现,其基本语法如下:

  • 引用静态方法: 类名 :: 方法名
  • 引用实例方法: 对象 :: 方法名
  • 引用类构造方法: 类名 :: new
  • 引用数组构造方法: 类型[] :: new

6.1.2 方法引用的四种类型

方法引用可以根据引用方法的类型进行分类,包括:

  • 静态方法引用
  • 实例方法引用
  • 构造方法引用
  • 数组构造方法引用

代码块展示与分析

// 静态方法引用
Function<String, Integer> lengthFunc = String::length;

// 实例方法引用
BiPredicate<String, String> equalsPred = String::equals;

// 构造方法引用
Supplier<StringBuilder> supplier = StringBuilder::new;

// 数组构造方法引用
Function<Integer, String[]> arrayGenerator = String[]::new;

在上述代码中,我们展示了四种不同类型的引用。使用方法引用,代码不仅更加简洁,而且更易于理解和维护。

6.2 方法引用的实践应用

6.2.1 与Lambda表达式的对比分析

方法引用与Lambda表达式在功能上有些重叠,但它们适用于不同的场景。当Lambda表达式只调用一个方法时,使用方法引用可以进一步简化代码。

// Lambda表达式实现
Function<String, Integer> lengthFuncLambda = s -> s.length();

// 方法引用实现
Function<String, Integer> lengthFunc = String::length;

在上述比较中,可以看出使用方法引用的版本更加简洁明了。

6.2.2 方法引用在实际开发中的应用

在实际开发中,方法引用可以应用于许多场景,比如在流操作中简化代码。

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream().map(String::toUpperCase).forEach(System.out::println);

在这个例子中,我们将每个名字转换为大写并打印出来。通过使用方法引用,我们减少了代码的冗余并增加了可读性。

6.3 日期和时间API的更新

Java 8对日期和时间API进行了彻底的重构,引入了全新的 java.time 包,提供了清晰且易于使用的日期时间处理工具。

6.3.1 新的日期时间API概述

新的日期时间API在设计上优于旧的 java.util.Date Calendar 类,提供了更加直观和健壮的日期时间模型。核心类包括:

  • LocalDate :表示没有时间的日期
  • LocalTime :表示没有日期的时间
  • LocalDateTime :同时包含日期和时间
  • ZonedDateTime :包含时区信息的日期和时间

6.3.2 DateTimeFormatter的使用

DateTimeFormatter 类用于格式化和解析日期时间对象。它能够以易读的格式输出日期时间,同时也能解析这样的格式。

// 创建一个日期时间格式化器
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

// 使用格式化器格式化日期时间
LocalDateTime now = LocalDateTime.now();
String formattedDate = now.format(formatter);

System.out.println("Formatted Date: " + formattedDate);

6.3.3 日期时间API的实践案例

// 获取当前日期
LocalDate today = LocalDate.now();

// 计算特定日期
LocalDate nextWeek = today.plusDays(7);

// 获取当前时间
LocalTime now = LocalTime.now();

// 使用DateTimeFormatter解析日期
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
LocalDate parsedDate = LocalDate.parse("2023/01/01", formatter);

// 打印日期时间信息
System.out.println("Today: " + today);
System.out.println("Next week: " + nextWeek);
System.out.println("Current time: " + now);
System.out.println("Parsed Date: " + parsedDate);

在该案例中,我们演示了如何使用新的日期时间API获取当前日期和时间、计算特定日期以及解析特定格式的日期字符串。

以上章节内容详细阐述了方法引用的原理、分类以及在实际开发中的应用,同时对Java 8中日期和时间API的更新提供了深入的讲解和实践案例。通过这些介绍,读者可以掌握如何利用这些新特性来编写更高效、更简洁的Java代码。

7. 集合工厂方法、Optional类与类型注解

7.1 集合工厂方法的介绍与应用

Java 9引入的集合工厂方法提供了一种简洁的方式来创建不可变集合实例。这不仅有助于提升代码的可读性,还避免了在创建集合后对它的误修改。

7.1.1 Map、List、Set的静态工厂方法

集合接口现在提供了一些静态工厂方法,比如 List.of() , Set.of() , Map.of() ,这些方法可以快速创建指定元素的集合实例。

import java.util.List;
import java.util.Map;
import java.util.Set;

public class CollectionFactoryMethods {
    public static void main(String[] args) {
        List<String> list = List.of("Apple", "Banana", "Cherry");
        Set<Integer> set = Set.of(1, 2, 3, 4, 5);
        Map<String, Integer> map = Map.of("One", 1, "Two", 2, "Three", 3);
    }
}

上述代码展示了如何使用工厂方法快速创建一个列表、一个集合和一个映射。这些方法返回的集合是不可修改的,尝试添加、删除元素或修改集合本身都会抛出 UnsupportedOperationException

7.1.2 创建不可变集合的简单方式

在Java 8中,已经可以通过 Collections.unmodifiableList() 等方法创建不可变集合,但这种方式需要先创建一个可变的集合,然后通过包装器包装成不可变形式。Java 9的工厂方法则提供了一种更为直接且简洁的方法。

import java.util.List;
import java.util.Map;
import java.util.Set;

public class ImmutableCollections {
    public static void main(String[] args) {
        List<String> list = List.of("Apple", "Banana", "Cherry");
        Set<Integer> set = Set.of(1, 2, 3, 4, 5);
        Map<String, Integer> map = Map.of("One", 1, "Two", 2, "Three", 3);

        // 尝试修改集合
        // set.add(6); // 抛出UnsupportedOperationException
    }
}

这种方法的优势在于代码更加简洁,并且从创建开始,集合就是不可变的,避免了中间状态的可变性。

7.2 Optional类的深入探讨

Optional 类是Java 8中引入的一个容器类,用于包含可能为空的值,从而避免 NullPointerException

7.2.1 Optional类的作用与用法

Optional 类提供了多种方法来处理值的可选性,如 orElse() , orElseGet() , map() 等,允许在值存在时执行操作,不存在时提供替代值或执行替代行为。

import java.util.Optional;

public class OptionalExample {
    public static void main(String[] args) {
        String name = "John";
        Optional<String> optName = Optional.ofNullable(name);

        String upperName = optName.map(String::toUpperCase).orElse("Anonymous");
        System.out.println(upperName); // 输出: JOHN
    }
}

在此代码中,如果 name 变量是 null ,则 map() 不会执行转换,而是直接跳转到 orElse() 返回"Anonymous"。

7.2.2 避免空指针异常的最佳实践

Optional 类的使用可以大大减少空指针异常的发生,其提供的方法有助于编写更为安全的代码。

import java.util.Optional;

public class OptionalAvoidNullPointerException {
    public static void main(String[] args) {
        Optional<String> optName = Optional.ofNullable(null);

        // optName.map(String::length).orElse(0);
        System.out.println(optName.map(String::length).orElse(0)); // 输出: 0
    }
}

上述代码展示了,即便 Optional 的内部值是 null ,我们也可以安全地调用方法,而不会抛出空指针异常。

7.3 类型注解的应用

Java 8引入了注解 @Target ElementType.TYPE_USE ElementType.TYPE_PARAMETER ,允许在任何类型上下文中使用注解。

7.3.1 类型注解的定义和作用

类型注解能够为变量的声明、泛型类型、类型转换、 new 表达式以及方法或构造器的异常类型增加更多的语义信息。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

public class TypeAnnotationsExample {
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE_USE})
    public @interface NonEmpty {}

    public static void main(String[] args) {
        @NonEmpty String str = "Hello";
        if(str == null) {
            // 编译器将会报错,因为str被注解为@NonEmpty,不能为null
        }
    }
}

在此例中, @NonEmpty 注解表示 str 变量的值不应该为 null 。如果尝试将其赋值为 null ,编译器将报错,从而提前发现潜在的空指针异常。

7.3.2 类型注解在实际开发中的应用

类型注解可以与静态分析工具(如Checker Framework、Error Prone等)结合使用,以增强代码的健壮性。

import java.util.List;

public class TypeAnnotationsInPractice {
    private void initializeList(@NonEmpty List<String> list) {
        // ... 初始化list的操作
    }
}

上述方法的参数列表使用了 @NonEmpty 注解,这可以提醒开发者或静态分析工具此列表不应该为 null ,并且在进行列表操作前需要确保它已被正确初始化。

7.3.3 类型注解与传统注解的对比

传统注解主要用于标记方法、字段、类等,而类型注解则提供了更为丰富的类型信息。

| 特征 | 传统注解 | 类型注解 | | --- | --- | --- | | 定义位置 | ElementType.TYPE , ElementType.METHOD , ElementType.FIELD 等 | ElementType.TYPE_USE , ElementType.TYPE_PARAMETER | | 应用场景 | 元数据、框架的契约 | 类型安全性、静态分析 | | 示例 | @Override , @Deprecated , @ SuppressWarnings | @ NonEmpty , @ NonNull |

类型注解提供了一种新的方式来提高代码的安全性和准确性,但它们并不替代传统的注解,而是和传统注解一起,为开发者提供更多的代码信息。

通过本章节的介绍,我们可以看到集合工厂方法、Optional类以及类型注解是如何丰富我们的编程语言,使Java更加健壮且易于维护。这些特性为开发者提供了新的工具来减少编程错误,提高代码质量,并使代码更加清晰和安全。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:JDK 1.8是Java开发中一个重要的里程碑,提供了Lambda表达式、函数式接口、Stream API、默认方法等创新特性。这份手册旨在为Java开发者提供全面的JDK 1.8特性和API指南,覆盖了从Lambda表达式的使用到Date和Time API增强等关键知识点。通过中文资料,开发者可以更易于理解和应用JDK 1.8的新特性,提升开发效率和代码质量。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐