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引入的一些主要特性。