Java 8新特性实战:从示例到练习
简介:Java 8引入了多项新特性,如lambda表达式、函数式接口、Stream API、方法引用、默认方法、Optional类、新的日期时间API、并行流以及Map接口和构造函数引用的改进,显著提升了开发效率和代码可读性。本项目”java8-examples”将通过具体片段、示例和练习,帮助开发者深入学习这些特性,并通过实践活动掌握它们的应用,从而提高编程技能和开发能力。 
1. Java 8新特性概述
Java 8是Java语言历史上的一个重大更新,引入了许多革命性的特性,如lambda表达式、Stream API、接口默认方法以及新的日期时间API等。这些新特性的加入,不仅使得Java语言更加现代化,而且还提高了开发者的编程效率。通过本章内容,我们将概览Java 8带来的新特性,为你在后续章节中更深入地探索每一个特性奠定基础。
2. lambda表达式应用示例
2.1 lambda表达式的定义与优势
2.1.1 lambda表达式的语法结构
lambda表达式是Java 8中引入的一种简洁的表示可传递的匿名方法的方式。它提供了一种更灵活的方式去处理函数式接口。Lambda表达式的基本语法非常简洁,由参数列表、箭头符号( -> )以及函数体三部分组成。
// 语法结构示例
(parameters) -> expression
(parameters) -> { statements; }
在参数列表中,可以声明参数的类型,也可以不声明,后者称为“类型推断”。当函数体只有一条语句时,大括号可以省略,而返回值在没有大括号的情况下,会自动返回该语句的结果。当有大括号时,返回值需要明确使用 return 语句。
2.1.2 lambda与匿名类的对比
在Java 8之前,为了实现简单的功能传递,我们通常使用匿名类。但匿名类的语法较为复杂,不如lambda表达式直观。比较下面两种方式实现同一个功能:
// 使用匿名类
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello world!");
}
};
// 使用lambda表达式
Runnable r = () -> System.out.println("Hello world!");
从上面的例子可以看出,lambda表达式将代码块变得更加简洁易读。此外,lambda表达式可以轻松地作为参数传递给方法,或者作为结果返回,而匿名类则需要多行代码。
2.2 lambda表达式在集合中的应用
2.2.1 集合的遍历
Java集合框架的迭代器模式曾经是遍历集合的唯一方法,lambda表达式的引入为我们提供了一种更简洁的方式:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name));
上面的代码使用了 forEach 方法配合lambda表达式,它提供了一种简洁的方式来迭代集合。对于Java 8之前的版本,相同的逻辑可能需要使用 for-each 循环或者迭代器来实现。
2.2.2 集合的排序与筛选
lambda表达式也可以与集合的 sort 方法配合使用,完成复杂的排序操作。例如,使用lambda表达式对列表进行排序:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.sort((a, b) -> a.compareToIgnoreCase(b));
在这个例子中, sort 方法接受一个lambda表达式作为参数,这个lambda表达式定义了比较两个字符串的方式,这里使用了 String 类的 compareToIgnoreCase 方法,表示不区分大小写的比较。
筛选操作可以通过 Collection 接口中的 removeIf 方法结合lambda表达式来实现:
names.removeIf(name -> !name.startsWith("A"));
这里的 removeIf 方法接受一个lambda表达式来决定哪些元素将被移除,上述代码示例将移除所有不以”A”开头的名字。
2.3 lambda表达式的高级用法
2.3.1 表达式中的变量捕获
lambda表达式不仅能访问定义在其外部的局部变量,还能修改这些变量(即闭包的概念)。局部变量必须是 final 的,或者实际上的 final (即在使用前不能改变)。
int a = 10;
List<Integer> list = Arrays.asList(1, 2, 3);
list.forEach(i -> System.out.println(a + i));
在上述代码中,lambda表达式访问了外部的局部变量 a ,并且将这个变量与集合中的元素相加后输出。
2.3.2 闭包与延迟执行
lambda表达式可以被看作是一种闭包,它能够延迟执行其中的代码。这意味着lambda表达式可以被存储在一个变量中,并且在之后的某个时刻被执行。
Runnable delayedAction = () -> System.out.println("Action will be executed later.");
// 延迟执行
new Thread(delayedAction).start();
在这个例子中,我们创建了一个 Runnable 的lambda表达式,并且将其赋值给 delayedAction 变量。之后,我们创建了一个新的线程,并在该线程中执行了 delayedAction ,从而实现了延迟执行。
在lambda表达式中捕获变量和延迟执行的特性,使得它在需要延迟调用或者回调的场景中非常有用。
3. 内置函数式接口
3.1 函数式接口的定义与分类
函数式接口是Java 8中的一个重要特性,它为Java语言提供了函数式编程的能力。函数式接口是只包含一个抽象方法声明的接口,可以被隐式转换为lambda表达式。Java 8内置了一些常用的函数式接口,它们被分类到不同的接口系列中,以便于满足不同的编程需求。
3.1.1 Function接口系列
Function 接口是函数式接口的典型代表,它接受一个参数并生成一个结果。 Function 接口系列还包括了如 UnaryOperator 、 BiFunction 、 DoubleFunction 等,它们分别用于处理不同类型的输入和输出。
// 示例代码:使用Function接口进行字符串转换
Function<String, Integer> lengthFunction = String::length;
int length = lengthFunction.apply("Java 8"); // 返回值为5
在上述代码中, Function 接口的 apply 方法用于接收一个字符串参数并返回其长度。 String::length 是lambda表达式的简写形式,它将 length 方法引用为 Function 接口的实现。
3.1.2 Predicate接口系列
Predicate 接口用于表示一个断言(布尔值函数),它接受一个参数并返回一个 boolean 值。 Predicate 接口系列同样有扩展,如 BiPredicate 、 DoublePredicate 等。
// 示例代码:使用Predicate接口进行字符串判断
Predicate<String> isLongerThanFive = s -> s.length() > 5;
boolean result = isLongerThanFive.test("Java 8"); // 返回值为true
在这段代码中, Predicate 接口的 test 方法用于判断字符串长度是否大于5。
3.2 常用函数式接口详解
3.2.1 Consumer接口的使用场景
Consumer 接口代表一个操作,它接受一个输入参数并且不返回任何结果。这种接口通常用于消费某个对象,例如,对一个对象执行操作,如打印到控制台。
// 示例代码:使用Consumer接口进行打印操作
Consumer<String> printConsumer = System.out::println;
printConsumer.accept("Hello, Java 8"); // 输出: Hello, Java 8
Consumer 接口的 accept 方法允许对传入的对象执行操作,在这个示例中是打印字符串到控制台。
3.2.2 Supplier接口的应用示例
Supplier 接口产生一个结果,但不接受任何参数。它通常用于生成需要延迟计算或特定条件下的值。
// 示例代码:使用Supplier接口生成一个随机数
Supplier<Integer> randomSupplier = () -> (int)(Math.random() * 100);
Integer randomNumber = randomSupplier.get(); // 返回0到99之间的随机数
这里使用 Supplier 接口的 get 方法来提供一个基于当前时间随机数的生成方式。这种接口的用法非常符合延迟计算的需求。
3.3 自定义函数式接口
虽然Java 8提供了丰富的函数式接口,但有时候开发者仍需自定义函数式接口来适应特定的业务场景。
3.3.1 创建自己的函数式接口
创建一个函数式接口非常简单,只需要定义一个只包含一个抽象方法的接口。Java 8提供的 @FunctionalInterface 注解可以帮助开发者确保接口符合函数式接口的要求。
@FunctionalInterface
public interface MyFunctionalInterface {
// 必须只有一个抽象方法
String myMethod(String input);
}
3.3.2 与现有的Java库集成
自定义函数式接口的一个常见用法是与Java标准库中的类集成。例如,可以创建一个自定义接口,用于处理 List 和 Stream 中的元素。
@FunctionalInterface
public interface CustomMapper<T, U> {
U map(T input);
}
// 使用自定义的函数式接口
List<String> words = Arrays.asList("hello", "java", "8");
List<Integer> lengths = words.stream()
.map((CustomMapper<String, Integer>) s -> s.length())
.collect(Collectors.toList());
在这个例子中,我们定义了一个 CustomMapper 接口,并在流式处理 List 时使用它来获取字符串长度。
在本章节中,我们深入学习了Java 8中内置函数式接口的基础知识、分类以及如何使用它们来简化代码。通过上述示例和代码块,我们展示了如何有效地利用这些接口来实现函数式编程的特性。
4. Stream API数据处理
4.1 Stream API的构成与特点
4.1.1 流的创建与终止操作
在Java 8中,Stream API提供了一种高效、声明式的处理集合数据的方式。Stream API允许开发者以声明式的方式对集合进行操作,而不需要编写具体的迭代代码。首先,我们将了解如何创建流以及流的终止操作。
创建流主要有两种方式:通过集合或者数组。对于集合,可以直接使用 stream() 方法,而对于数组,则使用 Arrays.stream() 方法。例如,对于一个简单的 List ,创建流可以这样做:
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
创建流后,我们常常需要对其进行一系列中间操作,例如映射、过滤等,最终以终止操作结束流的处理,如求和、收集结果等。
终止操作是流处理的终点,它们会强制流的执行,并产生一个结果。常见的终止操作有 forEach , collect , reduce 等。使用 forEach 可以遍历流中的每个元素:
stream.forEach(System.out::println);
collect 操作可以收集流中的元素到一个新的集合中。它通常与 Collectors 类中的方法一起使用,比如 Collectors.toList() 将流中的元素收集到一个新的 List 中:
List<String> collectedList = stream.collect(Collectors.toList());
reduce 操作可以用来累积流中的元素,比如计算流中所有元素的总和:
int sum = list.stream().mapToInt(String::length).sum();
4.1.2 流中的中间操作详解
流的中间操作是连接流的创建与终止操作的桥梁,它们是延迟执行的,意味着只有在进行终止操作时,这些中间操作才会实际执行。中间操作包括 filter , map , sorted , limit , skip 等。
例如,我们可以使用 filter 来筛选出满足特定条件的元素:
List<String> filteredList = list.stream()
.filter(s -> s.contains("a"))
.collect(Collectors.toList());
map 操作用于将流中的元素转换为另一种形式,它接受一个函数作为参数,这个函数应用于流中每个元素,然后产生一个新的流。例如,我们可以将字符串转换为它们的长度:
List<Integer> lengths = list.stream()
.map(String::length)
.collect(Collectors.toList());
sorted 操作可以对流中的元素进行排序。在Java 8中, sorted 方法默认返回自然排序的结果:
List<String> sortedList = list.stream()
.sorted()
.collect(Collectors.toList());
此外, limit 和 skip 操作分别用于限制流的大小或跳过前面的若干元素,这在处理大量数据时非常有用。
理解中间操作和终止操作对于有效使用Stream API至关重要。它们是构建数据处理管道的基石,并允许我们以声明式的方式进行高效的数据处理。
5. 方法引用实践
5.1 方法引用的基本概念
5.1.1 方法引用的种类与用法
方法引用是Java 8引入的一项新特性,它允许你通过特定的语法直接引用已存在的方法,作为Lambda表达式的简洁替代。通过方法引用可以提高代码的可读性和重用性。方法引用主要有以下几种类型:
- 静态方法引用 :使用类名和双冒号操作符来引用静态方法。例如:
ClassName::staticMethodName。 - 实例方法引用 :使用对象和双冒号操作符来引用实例方法。例如:
instance::methodName。 - 特定类型的方法引用 :使用类名和双冒号操作符来引用某个实例对象的非静态方法。例如:
ClassName::methodName。 - 构造函数引用 :使用类名和双冒号操作符来引用构造函数。例如:
ClassName::new。
下面是这些方法引用的示例代码:
// 静态方法引用
Consumer<String> print = System.out::println;
// 实例方法引用
String str = "Hello";
Function<String, Integer> toLength = str::length;
// 特定类型的方法引用
BiPredicate<List<String>, String> contains = List::contains;
// 构造函数引用
Supplier<List> supplier = ArrayList::new;
5.1.2 方法引用与lambda表达式的转换
方法引用本质上是Lambda表达式的一种特殊形式,编译器会将其转换为相应的Lambda表达式。例如,静态方法引用 Math::pow 实际上可以转换为Lambda表达式 (a, b) -> Math.pow(a, b) 。
让我们看一个实例方法引用转换为Lambda表达式的例子:
BiFunction<Integer, Integer, Integer> sum = (a, b) -> a + b;
// 转换为方法引用
BiFunction<Integer, Integer, Integer> sumMethodRef = Integer::sum;
通过这种方式,可以使得代码更加简洁和易于理解。
5.2 方法引用与Stream API结合
5.2.1 使用方法引用来优化Stream API操作
在Java中,Stream API是处理集合的一种高效方式,结合方法引用可以使代码更加优雅。例如,如果你想要将一系列字符串转换为大写,可以使用方法引用:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> upperCaseNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
这段代码通过 .map(String::toUpperCase) 方法引用,直接使用了String类的 toUpperCase 方法,替代了可能存在的Lambda表达式形式。
5.2.2 方法引用在实际开发中的应用案例
实际开发中,方法引用常用于集成已有的功能性代码。假设你有一个工具类,提供了一些静态方法,你可以通过方法引用直接使用这些方法:
class Util {
static boolean isLongerThan(String str, int length) {
return str.length() > length;
}
}
List<String> longNames = names.stream()
.filter(Util::isLongerThan.bind(5))
.collect(Collectors.toList());
在这个例子中,我们使用了 Util::isLongerThan 方法引用,并通过 bind 方法(假设实现)将长度参数绑定为5。这使得 filter 方法可以接收一个符合预期参数类型的谓词。
5.3 构造函数引用
5.3.1 构造函数引用的基本使用
构造函数引用允许你直接引用一个类的构造器,作为工厂方法来创建对象。与引用普通方法类似,构造函数引用也使用双冒号语法:
Supplier<String> supplier = String::new;
5.3.2 构造函数引用与集合的初始化
构造函数引用通常与集合的初始化结合使用。使用构造函数引用,可以简化集合的创建和初始化过程:
List<String> stringList = Stream.of("one", "two", "three")
.collect(Collectors.toList());
Set<String> stringSet = Stream.of("one", "two", "three")
.collect(Collectors.toSet());
Map<String, Integer> stringMap = Stream.of("one", "two", "three")
.collect(Collectors.toMap(Function.identity(), String::length));
在这个例子中,我们创建了三种类型的集合:列表(List)、集合(Set)和映射(Map),并且都使用了构造函数引用来进行初始化。这样,我们可以非常灵活地创建和初始化各种数据结构。
6. 接口默认方法和Optional类
在Java 8之前,接口只能包含常量定义和抽象方法。Java 8为了增强接口的灵活性,引入了默认方法(Default Methods)和静态方法(Static Methods)。此外,为了更好地处理值可能为null的情况,Java 8还引入了Optional类。这些改进对于开发者来说是极为重要的,它们增强了Java语言的表达能力和安全性。现在让我们深入探讨它们的用法和在实际开发中的优势。
6.1 接口默认方法的引入与用途
6.1.1 默认方法的定义与实现
在Java 8之前,如果需要扩展一个接口,则所有的类都需要实现这个接口中新增加的方法,这会导致向后兼容性的问题。默认方法的引入就是为了解决这个问题。默认方法允许在接口中直接包含实现代码。当接口声明了一个默认方法,实现该接口的类可以选择提供该方法的具体实现,或者继承默认方法。
一个默认方法的声明非常直接,通过在方法声明前添加 default 关键字来实现。下面是一个简单的例子:
public interface Vehicle {
void start();
void stop();
default void refuel() {
System.out.println("Refueling...");
}
}
在这个例子中, refuel 方法是一个带有默认实现的默认方法,任何实现了 Vehicle 接口的类都可以直接使用 refuel 方法而无需额外实现它。
6.1.2 解决接口的演进问题
默认方法是解决接口演进问题的关键,因为它们允许向现有接口添加新的方法而不需要修改已有的实现类。这在维护大型应用和库时尤其重要。比如,假设我们有一个排序算法接口 Sorter :
public interface Sorter {
void sort();
}
我们可以在不破坏已有的 Sorter 实现的情况下,向该接口添加一个新的排序算法:
public interface Sorter {
void sort();
default void sortWithNewAlgorithm() {
// new algorithm implementation here
}
}
任何之前实现了 Sorter 接口的类仍然可以正常工作,但也可以选择实现新的 sortWithNewAlgorithm 方法。
6.2 Optional类的使用与优势
6.2.1 Optional类的创建与使用
Optional 类是一个容器类,代表一个值存在或不存在。它旨在为函数式编程提供更好的支持,使得代码更加清晰且易于维护。它的出现,主要是为了解决Java中常见的空指针异常(NullPointerException)。
创建一个 Optional 实例相对简单:
Optional<String> optionalString = Optional.of("I'm Optional");
Optional<String> emptyOptional = Optional.empty();
这里, of 方法要求传入的参数不能是 null,否则会抛出空指针异常。 empty 方法用来创建一个空的 Optional 实例。
我们可以通过 isPresent 方法检查 Optional 实例中是否包含值:
if (optionalString.isPresent()) {
System.out.println(optionalString.get());
} else {
System.out.println("No value");
}
get 方法用来获取 Optional 中的值,但是如果没有值,它会抛出 NoSuchElementException 。
6.2.2 防止空指针异常的实践
一个更优雅的处理 Optional 的方式是使用 ifPresent 方法,它接受一个 Consumer 函数式接口,如果 Optional 中存在值,则对这个值执行给定的代码块:
optionalString.ifPresent(s -> System.out.println(s));
此外, orElse 和 orElseGet 方法可以用来提供一个默认值,如果 Optional 中没有值的话:
String value = optionalString.orElse("Default value");
String value = optionalString.orElseGet(() -> computeDefaultValue());
orElse 方法直接传入一个默认值,而 orElseGet 方法可以传入一个 Supplier 函数式接口,由其提供默认值,这为延迟计算提供了一种可能。
6.3 接口默认方法与Optional结合
6.3.1 在流式API中使用默认方法
接口默认方法和流式API结合使用时,可以极大简化代码的复杂度。考虑下面的例子,我们使用默认方法扩展 Stream API,创建一个返回 Optional 结果的中间操作:
default <T> Stream<T> filterWithDefault(Predicate<? super T> predicate, T defaultValue) {
return this.filter(predicate).findFirst().orElse(Optional.of(defaultValue)).stream();
}
6.3.2 Optional与集合的交互操作
使用 Optional 类与集合进行交互时,可以避免在查找集合中元素时产生空指针异常。我们可以结合 Optional 来优雅地处理可能不存在的元素:
Optional<String> optionalElement = list.stream()
.filter(element -> element.startsWith("example"))
.findFirst();
optionalElement.ifPresent(element -> process(element));
在这个例子中,我们使用了 Stream 的 filter 和 findFirst 方法来查找可能存在的元素,并且优雅地处理了可能为 null 的情况。
接口默认方法和 Optional 类为Java 8带来了更强大的功能和更好的编程实践,使得开发者可以编写出更加优雅、易于维护的代码。随着时间的推移,这些特性已经被广泛接受,并成为了Java开发者日常工作的一部分。
7. Java 8的日期和时间API
Java 8 引入了一套全新的日期和时间API,旨在解决旧版 java.util.Date 和 java.util.Calendar API中的一些设计问题。新API通过引入不可变对象和清晰的领域模型解决了易用性和可读性的问题,同时为多线程环境下的日期和时间操作提供了更好的支持。
7.1 新日期时间API概述
7.1.1 旧日期时间API的问题
在Java 8之前,开发者们经常使用 java.util.Date 和 SimpleDateFormat 类,但这两个类都有明显的缺点。 Date 类同时代表了日期和时间,这使得它在功能上既不够强大也不够清晰。此外, Date 类的方法不是线程安全的,并且 SimpleDateFormat 在多线程环境下可能会引发错误。这些类也不提供国际化支持,对于时区的处理也很复杂。
7.1.2 新API的设计理念与优势
新的日期时间API定义在 java.time 包中,它由几个主要类组成,包括 LocalDate 、 LocalTime 、 LocalDateTime 和 ZonedDateTime 等。这些类是不可变的,并且设计上是线程安全的,解决了旧版API存在的多线程问题。新API的另一个重要特性是其领域模型,它清晰地区分了日期、时间和时区的概念。此外,它还提供了丰富的API来处理时间和日期的操作,如时间间隔计算、日期格式化和解析等。
7.2 新API的核心组件
7.2.1 LocalDate、LocalTime与LocalDateTime
LocalDate 类用于表示没有时间没有时区的日期,例如生日。 LocalTime 表示一天中的时间,不包括日期或时区。而 LocalDateTime 则结合了日期和时间信息,但不包括时区。
这些类都遵循相同的API设计模式。例如,创建和操作这些对象的常用方法包括:
// 创建LocalDate实例
LocalDate date = LocalDate.of(2023, Month.MARCH, 15);
// 获取当前日期
LocalDate today = LocalDate.now();
// 创建LocalTime实例
LocalTime time = LocalTime.of(15, 30, 45);
// 获取当前时间
LocalTime now = LocalTime.now();
// 创建LocalDateTime实例
LocalDateTime dateTime = LocalDateTime.of(2023, Month.MARCH, 15, 15, 30, 45);
// 获取当前日期时间
LocalDateTime currentDateTime = LocalDateTime.now();
7.2.2 Period和Duration的使用
Period 用于表示两个日期之间的差异,如几年、几个月和几天。 Duration 则用于表示两个时间点之间的时间长度,以秒和纳秒为单位。
以下是如何使用这些类的示例:
// 计算两个日期之间的Period
LocalDate start = LocalDate.of(2023, Month.MARCH, 15);
LocalDate end = LocalDate.of(2024, Month.MARCH, 15);
Period period = Period.between(start, end);
System.out.println("Period: " + period.getDays() + " days, " + period.getMonths() + " months, " + period.getYears() + " years");
// 计算两个时间点之间的Duration
LocalTime startTime = LocalTime.of(15, 30, 45);
LocalTime endTime = LocalTime.of(17, 30, 45);
Duration duration = Duration.between(startTime, endTime);
System.out.println("Duration: " + duration.toSeconds() + " seconds");
7.3 日期时间API的高级特性
7.3.1 时间区间的操作
新的日期时间API提供了强大的时间区间操作功能,可以轻松地创建和操作时间区间的实例。例如,可以创建一个时间区间并执行如下操作:
// 创建并操作时间区间
LocalDate start = LocalDate.of(2023, Month.MARCH, 15);
LocalDate end = LocalDate.of(2023, Month.MARCH, 20);
LocalDatePeriod period = LocalDatePeriod.of(start, end);
// 判断区间是否包含某个日期
LocalDate testDate = LocalDate.of(2023, Month.MARCH, 18);
boolean contains = period.contains(testDate);
System.out.println("Period contains testDate: " + contains);
// 获取区间的持续时间
Duration duration = period.duration();
System.out.println("Duration of period: " + duration.toDays() + " days");
7.3.2 日期时间的格式化与解析
新API使用 DateTimeFormatter 类处理日期时间的格式化和解析,这比旧API提供了更好的国际化支持和更灵活的格式化选项。
以下是如何格式化和解析日期时间的示例:
// 定义日期时间格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 格式化日期时间对象为字符串
LocalDateTime dateTime = LocalDateTime.of(2023, Month.MARCH, 15, 15, 30, 45);
String formatted = dateTime.format(formatter);
System.out.println("Formatted date time: " + formatted);
// 从字符串解析日期时间对象
LocalDateTime parsed = LocalDateTime.parse(formatted, formatter);
System.out.println("Parsed date time: " + parsed);
Java 8的日期和时间API彻底改变了Java开发中处理日期和时间的方式。通过使用新API,开发者可以享受到更强大的功能和更好的国际化支持,同时还可以提高代码的清晰度和安全性。
简介:Java 8引入了多项新特性,如lambda表达式、函数式接口、Stream API、方法引用、默认方法、Optional类、新的日期时间API、并行流以及Map接口和构造函数引用的改进,显著提升了开发效率和代码可读性。本项目”java8-examples”将通过具体片段、示例和练习,帮助开发者深入学习这些特性,并通过实践活动掌握它们的应用,从而提高编程技能和开发能力。
更多推荐




所有评论(0)