Java8作为Java发展史上一个划时代的里程碑,即便在Java的最新版本已经跃升至24的今天,它依然在企业级应用中占据着举足轻重的地位。可以说,正是Java8奠定了后续版本快速演进的基础。本文将深入探讨Java8所引入的关键特性,并重温它们如何推动了Java语言的进步。
Lambda表达式
Lambda表达式,作为Java8 的一大创新,彻底改变了我们编写代码的方式。它允许开发者将代码块视作普通的方法参数进行传递,极大地简化了代码结构。此外,Lambda表达式还让匿名类的实现更加精炼,提升了代码的可读性和维护性。
Lambda表达式的语法如下:
(parameters) -> { statements; }
parameters 指定了 Lambda 表达式的参数列表。其可以为空,也可以为一个或多个参数。
-> 是 Lambda 操作符,其将参数列表与 Lambda 表达式的主体分隔开来。
expression 可以是一个表达式或 Lambda 表达式的返回值。
{ statements; } 包含了 Lambda 表达式的执行体,可以是单条语句或多条语句。
Lambda表达式的主要用途是简化函数式接口(只包含一个抽象方法,可使用 @FunctionalInterface 注解来检验)实例的创建。
如下代码声明了一个函数式接口 MyInterface,其只包含一个抽象方法,且使用 @FunctionalInterface 注解来标记:
@FunctionalInterface
public interface MyInterface {
// 抽象方法
void print(String str);
// 默认方法
default int version(){
return 1;
}
// 静态方法
static String info(){
return "info";
}
}
要满足函数式接口的定义,其内部只能包含一个抽象方法,但默认方法或静态方法的数量不受限制。再者,@FunctionalInterface 注解只用来校验接口是否满足定义,并不要求强制使用。
public class LambdaFeatureTest {
public static void main(String[] args) {
// 1. Lambda表达式
new Thread(() -> System.out.println("使用Lambda创建 Runnable接口实例")).start();
// 使用匿名内部类创建
MyInterface myInterface = new MyInterface() {
@Override
public void print(String str) {
System.out.println(str);
}
};
myInterface.print("使用匿名内部类创建");
MyInterface myInterface2 = System.out::println;
myInterface2.print("使用Lambda创建");
}
}
新的日期时间API
因旧的日期相关的 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 的使用:
public static void main(String[] args) throws Exception {
// 使用 Instant 和 Duration 计算时间差
Instant start = Instant.now();
TimeUnit.SECONDS.sleep(2);
Instant end = Instant.now();
System.out.println(Duration.between(start, end).getSeconds()); // 2
// 使用 LocalDate 计算下个月的今天,并使用 Period 计算两个日期的间隔月数
LocalDate now = LocalDate.now();
LocalDate nextMonth = now.plusMonths(1);
System.out.println(nextMonth); // 2024-01-23
Period period = Period.between(now, nextMonth);
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-01-23 13:08:15
ZonedDateTime losangelesZonedDateTime = shanghaiZonedDateTime.withZoneSameInstant(ZoneId.of("America/Los_Angeles"));
System.out.println(losangelesZonedDateTime.format(formatter)); // 2024-01-23 22:08:15
}
Optional类
Java8引入一个新的 Optional 类,Optional类是一个容器类,其可以保存一个泛型的值T,T可以一个非空Java对象,也可以是 null。
Optional类的一些常用方法:
of():创建一个包含非空值的Optional对象。
empty():创建一个空的Optional对象。
ofNullable():根据指定的值创建一个Optional对象,允许值为null。
isPresent():判断Optional对象是否包含值。
get():获取Optional对象中的值,如果没有值,则抛出NoSuchElementException异常。
orElse():获取Optional对象中的值,如果没有值,则返回默认值。
orElseGet():获取Optional对象中的值,如果没有值,则通过提供的Supplier函数生成一个默认值。
orElseThrow():获取Optional对象中的值,如果没有值,则通过提供的Supplier函数抛出指定的异常。
map():对Optional对象中的值进行转换,并返回一个新的Optional对象。
flatMap():对Optional对象中的值进行转换,并返回一个新的Optional对象,该方法允许转换函数返回一个Optional对象。
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 类时,可以先通过其 isPresent() 方法判断值是否存在,如果存在则可以通过 get() 方法获取该值,这样即避免了 NullPointerException 的发生。
下面的示例代码包含两个类:Order 与 Customer,两者是一种嵌套关系,即 Order 中有一个 Customer,Customer 中有一个 address 字段。
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 检查的写法可以是下面这个样子:
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 类来包装并进行链式操作呢?写法会变成下面的样子:
public static String getOrderAddressUsingOptional(Order order) {
return Optional.ofNullable(order)
.map(Order::getCustomer)
.map(Customer::getAddress)
.orElseThrow(() -> new RuntimeException("Invalid Order"));
}
支持在接口添加默认方法
在Java8之前,接口的规范颇为严格:其中的变量必须声明为public static final,而方法则必须声明为public abstract。接口的设计是一项需要慎重考虑的任务,因为一旦在接口中添加新方法,就意味着所有实现该接口的类都必须进行相应的修改。这在实现类众多的情况下,无疑是一项浩大的工程。
为了解决这个问题,Java8支持在接口添加默认方法(使用 default 关键字定义),其使得接口可以包含方法的实现,而不仅仅是抽象方法的定义。
在接口中定义默认方法和静态方法的例子:
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(),与类的静态方法无异,直接使用类名方式调用即可。
Stream API
Java8新添加的 Stream API 提供了一种更简洁和强大的处理集合数据的方式。使用 Stream API,我们可以对集合数据进行一系列的流水线操作(如:筛选、映射、过滤和排序等)来高效地处理数据。
// 生成一个 [1, 2, ..., 100] 的数组,然后对每个元素求平方后进行求和
long sum = IntStream.rangeClosed(0,100)
.mapToLong(num -> num * 10L)
.sum();
System.out.println(sum);
// 对 List 进行过滤、映射、排序后进行打印
List<String> languages = Arrays.asList("java", "golang", "python", "php", "javascript");
languages.stream()
.filter(lang -> lang.length() < 5)
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
方法引用和构造器引用
Java8引入的方法引用可以进一步简化 Lambda 表达式的编写。方法引用的本质是可以提供一种简洁的方式引用类或者实例的方法(包括构造器方法),引用格式为:类名::方法名、实例名::方法名。
List<String> languages = Arrays.asList("java", "golang", "python", "php", "javascript");
languages.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
// 若不使用方法引用,则是下面这个样子
languages.stream()
.map((lang) -> lang.toUpperCase())
.forEach((lang) -> System.out.println(lang));
方法引用支持的方法不仅可以是静态方法、实例方法,还可以是构造方法(引用格式为:类名::new),甚至还支持数组引用(引用格式为:Type[]::new)
static class Language{
private String name;
public Language(String name) {
this.name = name;
}
}
public static void main(String[] args) {
List<String> languages = Arrays.asList("java", "golang");
Language[] languagesArray = languages.stream()
.map(Language::new)
.toArray(Language[]::new);
}
Base64 工具类
在Java8之前,我们需要依赖第三方库来实现 Base64 编码解码。为了能够提供一个标准的、更加安全的方法来进行 Base64的编码和解码操作,使得开发者们不再需要依赖外部库,Java8添加了标准的 Base64工具类。
String str = "小飞技术";
String encoded = Base64.getEncoder().encodeToString(str.getBytes());
System.out.println(encoded);// 5bCP6aOe5oqA5pyv
byte[] decoded = Base64.getDecoder().decode(encoded);
System.out.println(new String(decoded)); // 小飞技术
String url = "https://xffjs.com";
String urlEncoded = Base64.getUrlEncoder().encodeToString(url.getBytes());
System.out.println(urlEncoded); // aHR0cHM6Ly94ZmZqcy5jb20=
byte[] urlDecoded = Base64.getUrlDecoder().decode(urlEncoded);
System.out.println(new String(urlDecoded)); // https://xffjs.com
在文本或 URL 进行 Base64 编码、解码时,需要先拿到对应的 Encoder 或 Decoder,然后调用其 encode() 或 decode() 方法即可实现编码、解码工作。
类型注解
va8之前,注解仅可以标记在类、方法、字段上。为了便于注解用于增强代码分析、编译期检查等场景的能力,Java 8 引入了类型注解,这样注解将不仅能应用于声明,还能应用于任何使用类型的地方。
private static void printLength(@NonNull String str) {
System.out.println(str.length());
}
类型推断
Java8引入了针对 Lambda 表达式的参数类型推断,使得在大多数情况下可以省略参数类型的显式声明。下述代码对 names List 进行排序时,传入的 Lambda 表达式为 (o1, o2) -> o1.compareTo(o2) 而非 (String o1, String o2) -> o1.compareTo(o2),这是因为编译器会自动推断参数的类型,从而可以省略参数类型的显式声明。
List<String> names = Arrays.asList("Charlie", "Bob", "Alice");
names.sort(String::compareTo);
// names.sort((o1, o2) -> o1.compareTo(o2));
System.out.println(names); // [Alice, Bob, Charlie]
可重复注解 @Repeatable
在Java8中,引入了 @Repeatable 注解用于支持注解的多次标记。这个特性允许我们在同一个目标上多次使用同一个注解,而无需使用容器注解来包装多个注解实例。
在Java8之前,在一个类上对一个注解进行多次标记是不允许的:
@PropertySource("classpath:config.properties")
@PropertySource("classpath:application.properties")
public class PropertyConfig {
}
Java8引入的 @Repeatable 注解,我们可以轻而易举的解决这个问题:
综上,我们速览了Java8引入的一些主要特性。
当前共有 0 条评论