我们知道 Java 8 是 Java 发布历史上一个里程碑式的版本,哪怕现在 Java 的最新版本已发展到 22,但仍有相当一部分企业在使用 Java 8,可以说 Java 8 是后续 Java 新版本得以快速迭代的基石。本文即重点回顾 Java 8 引入的那些主要特性。
Lambda 表达式是 Java 8 引入的一个重要特性,其允许将代码块看作普通方法参数一样来进行传递。同时,Lambda 表达式使得匿名类的写法变得更加简洁。
Lambda 表达式的语法如下:
(parameters) -> expression
或者:
(parameters) -> { statements; }
其中:
parameters 指定了 Lambda 表达式的参数列表。其可以为空,也可以为一个或多个参数。-> 是 Lambda 操作符,其将参数列表与 Lambda 表达式的主体分隔开来。expression 可以是一个表达式或 Lambda 表达式的返回值。{ statements; } 包含了 Lambda 表达式的执行体,可以是单条语句或多条语句。Lambda 表达式的主要用途是简化函数式接口(只包含一个抽象方法,可使用 @FunctionalInterface 注解来检验)实例的创建。
下面使用一个小例子来演示 Lambda 表达式的使用。
如下代码声明了一个函数式接口 MyInterface,其只包含一个抽象方法,且使用 @FunctionalInterface 注解来标记:
// src/main/java/MyInterface.java
@FunctionalInterface
public interface MyInterface {
// 抽象方法
void print(String str);
// 默认方法
default int version() {
return 1;
}
// 静态方法
static String info() {
return "functional interface test";
}
}
需要注意的是,要满足函数式接口的定义,其内部只能包含一个抽象方法,但默认方法或静态方法的数量不受限制。再者,@FunctionalInterface 注解只用来校验接口是否满足定义,并不要求强制使用。
如下即是分别使用匿名内部类和 Lambda 表达式创建函数式接口实例的代码:
// src/main/java/LambdaFeatureTest.java
public class LambdaFeatureTest {
public static void main(String[] args) {
// 使用匿名类创建 Runnable 接口的实例
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("a new thread started!");
}
}).start();
// 使用 Lambda 表达式创建 Runnable 接口的实例
new Thread(() -> System.out.println("a new thread started!")).start();
// 使用匿名类创建 MyInterface 接口的实例
MyInterface myInterface1 = new MyInterface() {
@Override
public void print(String str) {
System.out.println(str);
}
};
myInterface1.print("my functional interface called");
// 使用 Lambda 表达式创建 MyInterface 接口的实例
MyInterface myInterface2 = System.out::println;
myInterface2.print("my functional interface called");
}
}
分析一下上述代码,因 Runnable 接口是一个函数式接口(其代码如下)。
package java.lang;
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
因此,在 main() 方法的前两步,我们针对线程的创建,分别使用匿名内部类和 Lambda 表达式的方式创建了 Runnable 接口的实例。
接着,在 main() 方法的后两步,我们针对自定义的函数式接口 MyInterface,也分别使用匿名内部类和 Lambda 表达式的方式创建了其实例。可以看到,使用了 Lambda 表达式后的代码变得更加简洁和紧凑。
除了 Runnable 接口外,自 Java 8 起,诸多接口(如:java.util.function.Predicate、java.util.Comparator、java.io.FileFilter 等)均已标记为 @FunctionalInterface 接口。因此,针对这些接口的使用均可以换为对应 Lambda 表达式的写法。
因旧的日期相关的 API(诸如:java.util.Date、java.util.Calendar、java.text.SimpleDateFormat 等)存在非线程安全、类可变以及时区转换不够灵活等问题,Java 8 重新设计了日期时间 API(统一放在 java.time 包下),以更好地支持日期和时间的计算、格式化、解析和比较等操作。此外,java.time 包还提供了对日历系统的支持,包括对 ISO-8601 日历系统的全面支持。
下面列出 java.time 包中一些主要的类和接口:
Instant:表示时间线上的一个点,即一个瞬间,是一个不可变类,可以精确到纳秒级别。可以在忽略时区的情况下进行时间的表示、计算和比较。LocalDate:表示不包含时间信息的日期(如:年、月、日),不包含时区信息,也是一个不可变类。LocalTime:表示不包含日期信息的时间(如:时、分、秒),不包含时区信息,同为不可变类。LocalDateTime:表示日期和时间,不包含时区信息,同为不可变类。ZonedDateTime:表示包含时区信息的日期和时间,同为不可变类。Duration:表示时间间隔(如:几小时、几分钟、几秒),不可变类。Period:表示日期间隔(如:几年、几月、几日),不可变类。DateTimeFormatter:用于日期和时间的格式化和解析,不可变类。ZoneId:表示时区。ZoneOffset:表示时区偏移量,不可变类。下面使用一个简单的示例来演示新的日期时间 API 的使用:
// src/main/java/DateTimeAPITest.java
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
public class DateTimeAPITest {
public static void main(String[] args) throws InterruptedException {
// 使用 Instant 和 Duration 计算时间差
Instant start = Instant.now();
TimeUnit.SECONDS.sleep(1);
Instant end = Instant.now();
System.out.println(Duration.between(start, end).toSeconds()); // 1
// 使用 LocalDate 计算下个月的今天,并使用 Period 计算两个日期的间隔月数
LocalDate localDate = LocalDate.now();
LocalDate localDateNextMonth = localDate.plusMonths(1);
System.out.println(localDateNextMonth); // 2024-08-23
Period period = Period.between(localDate, localDateNextMonth);
System.out.println(period.getMonths()); // 1
// 打印当前时区,获取当前 ZonedDateTime 并使用 DateTimeFormatter 格式化后进行打印;然后转换为洛杉矶 ZonedDateTime 并进行格式化和打印
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
ZoneId currentTimeZone = ZoneId.systemDefault();
System.out.println(currentTimeZone); // "Asia/Shanghai"
ZonedDateTime shanghaiZonedDateTime = ZonedDateTime.now();
System.out.println(shanghaiZonedDateTime.format(formatter)); // 2024-07-23 13:08:15
ZonedDateTime losangelesZonedDateTime = shanghaiZonedDateTime.withZoneSameInstant(ZoneId.of("America/Los_Angeles"));
System.out.println(losangelesZonedDateTime.format(formatter)); // 2024-07-22 22:08:15
}
}
可以看到,Java 8 新的日期时间 API 对于日期时间的获取、计算、格式化、时区转换等都有很好的支持。
Java 8 引入一个新的 Optional 类,主要用于解决饱受诟病的 NullPointerException 问题。java.util.Optional<T> 类是一个容器类,其可以保存一个泛型的值 T,T 可以是一个非空 Java 对象,也可以是 null。
下面使用一个简单的例子看一下 Optional 是如何使用的:
// src/main/java/OptionalTest.java#main
Optional<String> optional = Optional.of("hello"); // Optional.ofNullable(null);
if (optional.isPresent()) {
String message = optional.get();
System.out.println(message);
} else {
System.out.println("message is null");
}
可以看到,Optional 类可以对真实的对象进行包装。Optional 中的值可以是一个非 null 值,也可以是一个 null 值。因 Optional 构造方法是私有的,创建 Optional 对象时可以使用 Optional.of() 或 Optional.ofNullable() 工厂方法来实现。使用 Optional.of() 方法创建对象时,传入的值不可以为 null(否则会抛出 NullPointerException),而使用 Optional.ofNullable() 方法创建对象时,传入的值可以为 null。
在使用 Optional 类时,可以先通过其 isPresent() 方法判断值是否存在,如果存在则可以通过 get() 方法获取该值,这样即避免了 NullPointerException 的发生。
Optional 类除了可以避免 NullPointerException 的发生外,还支持一系列链式写法,从而使代码更加简洁紧凑。
下面的示例代码包含两个类:Order 与 Customer,两者是一种嵌套关系,即 Order 中有一个 Customer,Customer 中有一个 address 字段。
// src/main/java/OptionalTest.java
class Order {
private final Customer customer;
public Order(Customer customer) {
this.customer = customer;
}
public Customer getCustomer() {
return this.customer;
}
}
class Customer {
private final String address;
public Customer(String address) {
this.address = address;
}
public String getAddress() {
return this.address;
}
}
如果我们想编写一个方法来获取 Order 的 address 信息,常规的包含 null 检查的写法可以是下面这个样子:
// src/main/java/OptionalTest.java
public static String getOrderAddress(Order order) {
if (null == order
|| null == order.getCustomer()
|| null == order.getCustomer().getAddress()) {
throw new RuntimeException("Invalid Order");
}
return order.getCustomer().getAddress();
}
如果换作使用 Optional 类来包装并进行链式操作呢?写法会变成下面的样子:
// src/main/java/OptionalTest.java
public static String getOrderAddressUsingOptional(Order order) {
return Optional.ofNullable(order)
.map(Order::getCustomer)
.map(Customer::getAddress)
.orElseThrow(() -> new RuntimeException("Invalid Order"));
}
可以看到,使用 Optional 类后,代码变为了一行,且更加直观明了。
所以,在 Java 8 引入 Optional 类后,我们可以对可空对象进行包装,从而避免空指针的发生,也可以借助Optional 类提供的链式方法编写出更加紧凑的代码。
在 Java 8 之前,接口中定义的变量必须是 public static final 的,定义的方法必须是 public abstract 的。我们知道,接口的设计需要「深思熟虑」,因为在接口中新增一个方法,需要对它的所有实现类都进行修改,对于实现类比较多的情况,涉及的工作量非常巨大。
为了解决这个问题,Java 8 支持在接口添加默认方法(使用 default 关键字定义),其使得接口可以包含方法的实现,而不仅仅是抽象方法的定义。
默认方法允许接口在不破坏实现类的情况下进行演进。这对于标准化库的维护和扩展非常有用,因为可以为接口添加新的方法来满足新的需求,而不会影响已有的实现。同时,默认方法使得接口可以通过通用方法实现,这可以减少代码的重复性,提供了代码的可维护性,这时的接口就有点像抽象类了。
此外,Java 8 还支持在接口中定义静态方法,非常适用于被用作工具方法的场景。
下面即是一个在接口中定义默认方法和静态方法的例子:
// src/main/java/InterfaceWithDefaultMethodsTest.java
public class InterfaceWithDefaultMethodsTest {
public interface Animal {
String greeting();
default void firstMeet(String someone) {
System.out.println(greeting() + "," + someone);
}
static void sleep() {
System.out.println("呼呼呼");
}
}
public static class Cat implements Animal {
@Override
public String greeting() {
return "喵喵喵";
}
}
public static class Dog implements Animal {
@Override
public String greeting() {
return "汪汪汪";
}
}
public static void main(String[] args) {
Animal cat = new Cat();
System.out.println(cat.greeting()); // 喵喵喵
cat.firstMeet("主人"); // 喵喵喵,主人
Animal dog = new Dog();
System.out.println(dog.greeting()); // 汪汪汪
dog.firstMeet("主人"); // 汪汪汪,主人
Animal.sleep(); // 呼呼呼
}
}
上面的代码中,Animal 接口拥有一个抽象方法 greeting()、一个默认方法 firstMeet() 和一个静态方法 sleep(),除抽象方法外,其它两个方法均拥有自己的实现。Animal 接口的实现类 Cat 和 Dog 必须实现其抽象方法 greeting(),而无须实现其默认方法 firstMeet()。对于其静态方法 sleep(),与类的静态方法无异,直接使用类名方式调用即可。
Java 8 新添加的 Stream API 提供了一种更简洁和强大的处理集合数据的方式。使用 Stream API,我们可以对集合数据进行一系列的流水线操作(如:筛选、映射、过滤和排序等)来高效地处理数据。
Stream API 性能很好,因为其求值是惰性的,即其操作分中间操作(filter、map、distinct、sorted、limit、skip 等)和终端操作(forEach、collect、reduce、count 等),而只有在调用终端操作时才会真正执行所有的中间操作链。
下面看一下使用 Stream API 的例子:
// src/main/java/StreamAPITest.java
import java.util.List;
import java.util.stream.IntStream;
public class StreamAPITest {
public static void main(String[] args) {
// 生成一个 [1, 2, ..., 100] 的数组,然后对每个元素求平方后进行求和
long sum = IntStream.rangeClosed(0, 100)
.mapToLong(num -> (long) num * num)
.sum();
System.out.println(sum);
// 对 List 进行过滤、映射、排序后进行打印
List<String> languages = List.of("java", "golang", "python", "php", "javascript");
languages.stream()
.filter(lang -> lang.length() < 8)
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
}
}
可以看到,第一个例子生成了一个 [1, 100] 的 IntStream,然后对该 IntStream 使用 mapToLong() 对每个元素进行了求平方并转型为了 long 类型,最后计算了所有元素的总和;第二个例子针对一个 List<String> 的 stream(),作了过滤、映射、排序后调用 forEach() 作了打印。
Base64 算法是一种常用的编码方法,它可以将任意的二进制数据转换成纯文本字符串的形式,使得二进制数据可以在文本协议中传输或存储。
在 Java 8 之前,我们需要依赖第三方库来实现 Base64 编码解码。为了能够提供一个标准的、更加安全的方法来进行 Base64 的编码和解码操作,使得开发者们不再需要依赖外部库,Java 8 添加了标准的 Base64 工具类。
下面即看一下该工具类如何使用:
// src/main/java/Base64Test.java
import java.util.Base64;
public class Base64Test {
public static void main(String[] args) {
String text = "java8";
String encoded = Base64.getEncoder().encodeToString(text.getBytes());
System.out.println(encoded); // amF2YTg=
byte[] decoded = Base64.getDecoder().decode(encoded);
System.out.println(new String(decoded)); // java8
String url = "https://leileiluoluo.com/posts/java-8-new-features.html";
String urlEncoded = Base64.getUrlEncoder().encodeToString(url.getBytes());
System.out.println(urlEncoded); // aHR0cHM6Ly9sZWlsZWlsdW9sdW8uY29tL3Bvc3RzL2phdmEtOC1uZXctZmVhdHVyZXMuaHRtbA==
byte[] urlDecoded = Base64.getUrlDecoder().decode(urlEncoded);
System.out.println(new String(urlDecoded)); // https://leileiluoluo.com/posts/java-8-new-features.html
}
}
可以看到,需要对文本或 URL 进行 Base64 编码、解码时,需要先拿到对应的 Encoder 或 Decoder,然后调用其 encode() 或 decode() 方法即可实现编码、解码工作。
Java 8 引入的方法引用可以进一步简化 Lambda 表达式的编写。方法引用的本质是可以提供一种简洁的方式引用类或者实例的方法(包括构造器方法),引用格式为:类名::方法名、实例名::方法名。
下面看一下我们在前面介绍 Stream API 时用到的一个例子:
// src/main/java/MethodReferenceTest.java#main
List<String> languages = List.of("java", "golang", "python", "php", "javascript");
languages.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
可以看到,上述 main() 方法中对 languages 列表中的元素进行了流式操作,即对每一个元素进行大写字母转换后进行了打印。这里的 String::toUpperCase 与 System.out::println 均为方法引用。
而不使用方法引用的话,这段代码该怎么写呢?可以是下面这个样子:
List<String> languages = List.of("java", "golang", "python", "php", "javascript");
languages.stream()
.map((lang) -> lang.toUpperCase())
.forEach((lang) -> System.out.println(lang));
但并不是所有的 Lambda 表达式都有对应的方法引用简写方式,可否将 Lambda 表达式写法转换为方法引用写法是有限制的。限制是:Lambda 表达式中仅有一个方法调用,且方法引用的目标方法的参数数量、参数类型以及返回类型必须与函数接口的要求完全匹配。
方法引用支持的方法不仅可以是静态方法、实例方法,还可以是构造方法(引用格式为:类名::new),甚至还支持数组引用(引用格式为:Type[]::new)。
下面看一段示例代码:
// src/main/java/MethodReferenceTest.java
import java.util.List;
import java.util.stream.Collectors;
public class MethodReferenceTest {
static class Language {
private final String name;
public Language(String name) {
this.name = name;
}
}
public static void main(String[] args) {
List<String> languages = List.of("java", "golang", "python", "php", "javascript");
Language[] languageArray = languages.stream()
.map(Language::new)
.toArray(Language[]::new);
}
}
上面代码中的 map(Language::new).toArray(Language[]::new) 即使用了构造方法引用和数组引用。
Java 8 之前,注解仅可以标记在类、方法、字段上。为了便于注解用于增强代码分析、编译期检查等场景的能力,Java 8 引入了类型注解,这样注解将不仅能应用于声明,还能应用于任何使用类型的地方。
看一段示例代码:
private static void printLength(@NonNull String str) {
System.out.println(str.length());
}
public static void main(String[] args) {
List<@Nonnull String> languages = new ArrayList<>();
languages.add(null); // Adding 'null' element to parameter annotated as @NotNull
printLength(null); // Passing 'null' argument to parameter annotated as @NotNull
}
上述代码中,languages List 的参数被标记为 @Nonnull,试图在该 List 加入一个 null 值时会有编译器错误。同理,printLength() 方法的参数也被标记为 @NonNull,试图传入一个 null 值时也会报编译器错误。
Java 8 引入了针对 Lambda 表达式的参数类型推断,使得在大多数情况下可以省略参数类型的显式声明。
看一段示例代码:
// src/main/java/TypeInferenceTest.java#main
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.sort((o1, o2) -> o1.compareTo(o2)); // names.sort((String o1, String o2) -> o1.compareTo(o2));
上述代码对 names List 进行排序时,传入的 Lambda 表达式为 (o1, o2) -> o1.compareTo(o2) 而非 (String o1, String o2) -> o1.compareTo(o2),这是因为编译器会自动推断参数的类型,从而可以省略参数类型的显式声明。
在 Java 8 中,引入了 @Repeatable 注解用于支持注解的多次标记。这个特性允许我们在同一个目标上多次使用同一个注解,而无需使用容器注解来包装多个注解实例。
如在 Java 8 之前,在一个类上对一个注解进行多次标记是不允许的:
@PropertySource("classpath:config.properties")
@PropertySource("classpath:application.properties")
public class PropertyConfig {
}
为解决该问题,我们必须定义一个注解的容器:
public @interface PropertySources {
PropertySource[] value();
}
然后使用容器注解来进行标记:
@PropertySources({
@PropertySource("classpath:config.properties"),
@PropertySource("classpath:application.properties")
})
public class PropertyConfig {
}
而借助 Java 8 引入的 @Repeatable 注解,我们可以轻而易举的解决这个问题:
@Repeatable(PropertySource.class) // 声明可重复注解
public @interface PropertySource {
}
这样,加了 @Repeatable 注解的 @PropertySource 即可供我们重复使用了:
@PropertySource("classpath:config.properties")
@PropertySource("classpath:application.properties")
public class PropertyConfig {
}
Java 8 对 java.util.concurrent 包下的并发工具类做了一些扩展,以提供更强大、更灵活的并发编程能力。
CompletableFuture 是一种用于异步编程的强大工具,其提供了一种方便的方式(回调、组合、转换等)来管理多个异步操作的结果。
下面简单看一下 ComplatableFuture 类的使用:
// src/main/java/CompletableFutureTest.java
import java.util.concurrent.*;
public class CompletableFutureTest {
public static void main(String[] args) {
// 使用 CompletableFuture 进行异步计算
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
try {
// 模拟耗时操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 42;
});
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 100;
});
// 使用 CompletableFuture 处理异步计算结果,这里需要两个任务都完成
CompletableFuture<Integer> allFutures = CompletableFuture.allOf(future1, future2)
.thenApply(result -> future1.join() + future2.join());
// 等待所有任务完成
System.out.println(allFutures.join());
}
}
上述示例中,首先使用 CompletableFuture 创建了两个异步任务 future1 和 future2,然后使用 CompletableFuture.allOf(future1, future2); 等待两个任务并发执行完成后调用 thenApply() 获取两个任务的合并结果,最后在主线程使用 join() 方法等待所有任务执行完成。
LongAdder 与 DoubleAdder 类是对 AtomicLong 和 AtomicDouble 类的改进,提供了更高的并发性能。LongAdder 与 DoubleAdder 类通过分解内部计数器,将更新操作分散到多个变量上,减少了竞争和锁争用。
看一个示例:
// src/main/java/AdderTest.java
import java.util.concurrent.atomic.DoubleAdder;
import java.util.concurrent.atomic.LongAdder;
public class AdderTest {
public static void main(String[] args) throws InterruptedException {
LongAdder longAdder = new LongAdder();
DoubleAdder doubleAdder = new DoubleAdder();
// 启动一个线程循环自增 100 次
Thread thread = new Thread(() -> {
for (int i = 0; i < 100; i++) {
longAdder.increment();
doubleAdder.add(0.5);
}
});
thread.start();
// 在主线程循环自增 100 次
for (int i = 0; i < 100; i++) {
longAdder.increment();
doubleAdder.add(0.5);
}
// 等待 thread 执行完成
thread.join();
// 打印自增结果
System.out.println(longAdder.sum()); // 200
System.out.println(doubleAdder.sum()); // 100.0
}
}
上述示例中,首先创建了 LongAdder 与 DoubleAdder 实例,然后分别在一个新线程、主线程并发「循环自增 100 次」,打印结果,发现最后的 sum 值准确无误。
综上,我们速览了 Java 8 引入的一些主要特性。本文涉及的所有示例代码已提交至 GitHub,欢迎关注或 Fork。
参考资料
[1] Oracle: What’s New in JDK 8? - https://www.oracle.com/java/technologies/javase/8-whats-new.html
[2] 掘金:一口气读完 Java 8 ~ Java 21 所有新特性 - https://juejin.cn/post/7315730050577006592
[3] 掘金:JDK 8 - JDK 17 新特性总结 - https://juejin.cn/post/7250734439709048869
正在加载评论......