Java 8 新特性深度解析:探索 Lambda 表达式、Stream API 和函数式编程的革新之路

Java8 新特性

Java 8 的革新之路

自 1995 年首次发布以来,Java 已经成为世界上最广泛使用的编程语言之一。随着时间的推移,Java 经历了多次版本更新,其中最具里程碑意义的便是 Java 8 的发布。这个版本引入了许多重大变革,包括 Lambda 表达式、Stream API 和函数式编程等新特性。这些变化不仅提升了 Java 开发者的生产力,还使得 Java 在性能、简洁性和可读性方面达到了新的高度。

本文将深入探讨 Java 8 的主要新特性,并通过示例代码和实际应用案例帮助你理解这些特性的优势和使用场景。无论你是 Java 初学者还是经验丰富的开发者,这篇文章都将为你提供一个全面了解 Java 8 新特性的平台,帮助你在日常开发中更高效地利用这些功能。

让我们一起踏上 Java 8 的革新之旅,探索如何通过这些新特性提高我们的代码质量和开发效率。

主要内容

  1. Lambda 表达式(核心)
  2. 函数式接口
  3. 方法引用与构造器引用
  4. Stream API
  5. 新时间日期 API
  6. 接口中默认方法与静态方法
  7. 其他新特性

新特性简介

  • 速度更快
  • 代码更少(简洁,增加了新的语法 Lambda 表达式
  • 强大的 Stream API
  • 便于并行
  • 可以最大化减少空指针异常 Optional – 因为这个类的方法

为什么便于运行?

三点原因:

  1. Stream API 可以让我们轻松地对集合进行并行处理。
  2. Lambda表达式 可以让我们以一种简洁的方式定义匿名函数,这种方式可以使代码更加简洁。
  3. 方式引用已经存在的方法或构造函数,可以避免我们重复编写相似的代码。

综上所述,Java8 新特性便于并行的原因是,它们可以让我们以一种简洁的方式处理集合数据,同时充分利用多核 CPU 的优势,从而提高程序的执行效率。

1、Lambda 表达式

何为 Lambda 表达式?

Lambda 是一个匿名函数,作为一种更紧凑的代码风格,使 Java 的语言表达能力得到了提升,可以写出更简洁、更灵活的代码。

代码示例

  1. Runnable 接口,从匿名内部类到 Lambda 的转换**(无参)**
// 匿名内部类
Runnable r1 = new Runnable() {
  @Override
  public void run() {
    System.out.println("Hello World!");
  }
};

// Lambda 表达式
Runnable r1 = () -> System.out.println("Hello Lambda!");
  1. Comparator 接口,参数传递 – 从匿名内部类到 Lambda 的转换**(有参)**
// 使用匿名内部类作为参数传递
TreeSet<String> ts = new TreeSet<>(new Comparator<String>() {
  @Override
  public int compare(String o1, String o2) {
    return Interger.compare(o1.length(), o2.length());
  }
});

// 使用 Lambda 表达式作为参数传递
TreeSet<String> ts2 = new TreeSet<>(
	(o1, o2) -> Integer.compare(o1.length(), o2.length()) // 升序排序
);
  1. Listener 接口
JButton button = new JButton();
button.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
   e.getItem();
}
});

// Lambda
button.addItemListener(e -> e.getItem());

语法

-> 箭头操作符简介

Lambda 表达式在 Java 语言中引入了一个新的语法元素和操作符。这个操作符为 “->”,该操作符被称为 Lambda 操作符或箭头操作符

它将 Lambda 分为两个部分:

  • 左侧:指定了 Lambda 表达式需要的所有参数
  • 右侧:指定了 Lambda 体,即 Lambda 表达式要执行的功能
六种格式
格式一:

无参,无返回值,Lambda 体只需要一条语句即可

Runnable r1 = () -> System.out.println("Hello Lambda!");
格式二:

Lambda 需要一个参数

Consumer<String> fun = (args) -> System.out.println(args);
格式三:

Lambda 只需要一个参数时,参数的小括号可以省略

Consumer<String> fun = args -> System.out.println(args);
格式四:

Lambda 需要两个参数,并且有返回值时

BinaryOperator<Long> bo = (x, y) -> {
	System.out.println("实现函数接口方法!");
	return x + y;
};
格式五:

当 Lambda 体只有一条语句时,return 与大括号可以省略

BinaryOperator<Long> bo = (x, y) -> x + y;
格式六:

带参数的数据类型 – 数据类型可以省略(如上所示),因为可由编译器推断得出,称为“类型推断”

BinaryOperator<Long> bo = (Long x, Long y) -> {
	System.out.println("实现函数接口方法!");
	return x + y;
};
类型推断

所谓的“类型推断”是指:Lambda 表达式中的参数类型依赖于上下文环境,都是由编译器推断得出的。

在 Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了参数的类型

总结

大致可分为三种类型:

  1. 格式为一种 – 无需参数,无返回值,Lambda 体一条语句即可
  2. 格式二、三为一种 – 需要一个参数,无返回值,可省略参数小括号,也是一条语句即可
  3. 格式四、五、六为一种 – 需要两种参数,有返回值,可以省略大括号与 return

参数类型均可省略,推荐使用一、二、五

2、函数式接口

何为函数式接口?

  • 函数式接口是指:只包含一个抽象方法的接口,该接口的对象可以通过 Lambda 表达式来创建。

    (若 Lambda 表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明)

  • 一般建议函数式接口上使用 @FunctionalInterface 注解修饰, 这样做可以检查它是否是一个函数式接口,同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口

@FunctionalInterface 注解:用于标识该接口是函数式接口,防止在接口中添加多余的抽象方法。

不是所有的函数式接口都有 @FunctionalInterface 注解修饰,但是建议所有函数式接口都应该加上该注解,因为这有助于编译器检查该接口是否符合函数式接口的要求,即只有一个抽象方法。如果该注解标注在有多个抽象方法的接口上,则编译器会报错,提示该接口不符合函数式接口的定义。同时,加上该注解也可以让其他开发者更容易地理解该接口的用途和特点。

什么是抽象方法?

抽象方法指的是:没有实现代码的方法,只有方法声明(包括方法名、返回值类型、参数列表和异常列表)。在 Java 中,如果一个类中包含了抽象方法,那么这个类必须被声明为抽象类,因为抽象方法无法被直接调用。只有当一个类继承了抽象类并实现了抽象方法,才能创建该类的对象并调用该方法。

抽象方法的语法格式为:在方法声明中使用 abstract 关键字修饰方法,方法体中不包含实现代码。例如:

public abstract void draw();

抽象方法的作用是:为了让子类实现自己的方法逻辑,从而实现多态性和代码复用。在设计模式中也经常使用抽象方法来定义基础框架和算法流程,具体实现由子类来完成。

在函数式接口中,由于该接口只包含一个抽象方法,因此该方法默认为抽象方法,不需要显式使用 abstract 关键字进行修饰。

代码示例:

@FunctionalInterface
public interface MyInterface {
    void myMethod();
}
MyInterface myInterface = () -> System.out.println("Hello World!");
myInterface.myMethod(); // 输出:Hello World!

自定义函数式接口

只要方法的参数是函数式接口都可以用 Lambda 表达式

@FunctionalInterface
public interface MyNumber {
  double getValue();
}

// 在函数式接口中使用泛型
public interface MyFunc<T> {
   T getValue(T t);
}

// 作为参数传递 Lambda 表达式
public String toUpperString(MyFunc<String) mf, String str) {
	return mf.getValue(str);
}

String newStr = toUpperString(
	(str) -> str.toUpperCase(), "abcdef"); // 该 Lambda 表达式的作用是将字符串转换成大写字母并返回
System.out.println(newStr); // "ABCDEF"

作为参数传递 Lambda 表达式:

为了将 Lambda 表达式作为参数传递,接收 Lambda 表达式的参数类型必须是与该 Lambda 表达式兼容的函数式接口的类型。

例如一个计算器接口,可以定义如下的函数式接口:
@FunctionalInterface
public interface Calculator {
    int calculate(int x, int y);
}

在该接口中,定义了一个抽象方法 calculate(int x, int y),接受两个整型参数 xy ,返回一个整型结果。该接口标记了 @FunctionalInterface 注解,表示该接口是一个函数式接口。 然后,可以使用 Lambda 表达式来创建该接口的实例,例如:

Calculator add = (x, y) -> x + y;
Calculator subtract = (x, y) -> x - y;
Calculator multiply = (x, y) -> x * y;
Calculator divide = (x, y) -> x / y;

在这个例子中,分别使用 Lambda 表达式创建了加法、减法、乘法、除法四种计算器操作的实例,这些实例都是 Calculator 函数式接口的实例。 然后,可以使用这些实例进行计算,例如:

int result = add.calculate(2, 3); // 加法运算,结果为 5

在这个例子中,使用加法操作的实例 add 对参数 2 和 3 进行了加法运算,并将结果赋值给 result 变量。

总之,自定义函数式接口可以根据具体的业务需求定义,然后使用 Lambda 表达式来创建该接口的实例,从而实现对业务逻辑的封装和复用。

Java 内置四大核心函数式接口

四种:消费型接口、供给型接口、函数型接口、断定型接口

1. Consumer 接口 – 消费型接口

Consumer 接口定义了一个接受一个泛型参数并返回 void 的操作。该接口有一个抽象方法 accept(T t),接收一个泛型参数 T,表示该操作的输入参数,无返回值。

源码解析:

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Consumer<T> {
    /**
     * 对给定的参数执行操作
     *
     * @param t 要执行操作的参数
     */
    void accept(T t);
    /**
     * 返回一个组合了多个Consumer的新Consumer,表示执行顺序为该Consumer接口先执行,然后执行after中的Consumer接口
     *
     * @param after 要执行的另一个Consumer接口
     * @return 组合后的新Consumer接口
     */
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> {
            accept(t);
            after.accept(t);
        };
    }
}

在这个源码中,首先使用 @FunctionalInterface 注解标记了该接口是一个函数式接口。然后定义了一个抽象方法accept(T t),表示对给定的参数执行操作,并无返回值。接着定义了一个默认方法 andThen(Consumer after),用于组合多个 Consumer 实例,表示执行顺序为该 Consumer 接口先执行,然后执行 after 中的Consumer 接口。

总之,Consumer接口可以用于对给定的参数执行操作,例如打印、修改状态等。该接口提供了一个默认方法 andThen 可以用于组合多个 Consumer 实例,从而实现更加复杂的操作。

应用场景:

Consumer 接口可以用于对集合进行遍历、对异步任务结果进行处理、以及对对象状态进行修改等场景中。使用Consumer 接口可以使代码更加简洁、易于维护,增强代码的可读性和可重用性。

以下是一些常见的场景:

  1. 集合遍历:可以使用 Consumer 接口对集合中的每个元素进行操作。

    例如对一个字符串列表中的每个字符串进行大写转换并输出:

List<String> list = Arrays.asList("apple", "banana", "orange");
Consumer<String> toUpperCase = s -> System.out.println(s.toUpperCase());
list.forEach(toUpperCase);

在这个例子中,定义了一个字符串列表 list,其中包含三个字符串元素。然后定义了一个 Consumer 实例 toUpperCase,用于将字符串转换为大写并输出。接着使用 forEach 方法遍历 list 中的每个元素,并将 toUpperCase 实例作为参数传入,实现对每个字符串的大写转换和输出操作。

  1. 异步任务处理:可以使用 Consumer 接口对异步任务的结果进行处理。

    例如对一个异步任务的结果进行打印:

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello, World!");
Consumer<String> printResult = s -> System.out.println(s);
future.thenAccept(printResult); // "Hello, World!"

在这个例子中,使用 CompletableFuture 创建一个异步任务,该任务返回一个字符串 "Hello, World!"。然后定义了一个 Consumer 实例 printResult,用于打印异步任务的结果。使用 thenAccept 方法注册了一个 Consumer 类型的回调函数,该回调函数的作用是在 future 对象执行完毕后,将结果传递给 printResult Lambda 表达式并输出。表示当异步任务完成时对结果进行处理,体现了异步任务执行完成后的回调机制。

  1. 状态修改:可以使用 Consumer 接口对对象的状态进行修改。

    例如修改一个订单对象的状态:

public class Order {
    private String status;
    // getter和setter方法
    // ...
}
Consumer<Order> updateStatus = o -> o.setStatus("已发货");
Order order = new Order();
updateStatus.accept(order);

在这个例子中,定义了一个 Order 类,其中包含一个状态属性 status。然后定义了一个 Consumer 实例 updateStatus,用于将订单状态修改为 "已发货"。接着创建了一个订单对象 order,并将 updateStatus 实例作为参数传入,实现对订单状态的修改。

2. Supplier 接口 – 供给型接口

Supplier 接口产生给定泛型类型的结果。与 Function 接口不同,Supplier 接口不接受参数。

该接口有一个抽象方法 get(),用于返回一个泛型参数 T,表示该操作的结果。

源码解析:

package java.util.function;

@FunctionalInterface
public interface Supplier<T> {
    /**
     * 获取一个结果
     *
     * @return 表示操作结果的泛型参数T
     */
    T get();
}

在这个源码中,首先使用 @FunctionalInterface 注解标记了该接口是一个函数式接口。然后定义了一个抽象方法 get(),用于获取一个结果并返回一个泛型参数 T。该方法没有参数,仅返回一个表示操作结果的泛型参数。

总之,Supplier 接口主要用于提供一个泛型参数类型的结果,例如获取当前时间、生成随机数、从数据库中获取数据等。Supplier 接口的 get() 方法没有参数,返回一个表示操作结果的泛型参数,可以与其它函数式接口进行组合使用,实现更加复杂的操作。

应用场景:

用于提供一个泛型参数类型的结果,例如生成随机数、从数据库中获取数据、获取当前时间等场景中。

  1. 生成随机数
Supplier<Integer> randomSupplier = () -> new Random().nextInt(100);
int randomNum = randomSupplier.get();
  1. 从数据库中获取数据
public class SupplierExample {
    public static void main(String[] args) {
        Supplier<List<String>> supplier = () -> {
            List<String> list = new ArrayList<>();
            try {
                ResultSet resultSet = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "password")
                        .createStatement().executeQuery("select * from user");
                while (resultSet.next()) {
                    list.add(resultSet.getString("name"));
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return list;
        };
        List<String> result = supplier.get();
        System.out.println(result.stream().collect(Collectors.joining(", ")));
    }
}
  1. 获取当前时间
Supplier<LocalDateTime> timeSupplier = LocalDateTime::now;
LocalDateTime currentTime = timeSupplier.get();
3. Function 接口 – 函数型接口

Function 接口接收一个参数并生成结果。默认方法可用于将多个函数链接在一起(compose, andThen):

package java.util.function;

import java.util.Objects;

@FunctionalInterface
public interface Function<T, R> {

    //将Function对象应用到输入的参数上,然后返回计算结果。
    R apply(T t);
    //将两个Function整合,并返回一个能够执行两个Function对象功能的Function对象。
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
  
    //返回一个由原始Function执行完后再执行after Function的新Function
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    //接收任意类型参数,并返回相同类型参数的Function
    //作用:快速创建一个无需处理参数的Function实例
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

应用场景:

Function 接口主要用于将一个类型的值转化为另一个类型的值。它接受一个参数并返回一个结果,通常用于在数据处理、转换或映射过程中使用。

  1. 数据转换和映射
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123");     // "123"
  1. 数据过滤和筛选
List<String> list = Arrays.asList("apple", "banana", "orange", "grape");
Function<String, Boolean> filterFunction = s -> s.startsWith("a");
List<String> filteredList = list.stream().filter(filterFunction::apply).collect(Collectors.toList());
System.out.println(filteredList); // [apple]
  1. 数据处理和转换
List<String> list = Arrays.asList("apple", "banana", "orange", "grape");
Function<String, Integer> mapFunction = String::length;
List<Integer> lengthList = list.stream().map(mapFunction::apply).collect(Collectors.toList());
System.out.println(lengthList); // [5, 6, 6, 5]
4. Predicate 接口 – 断定型接口

Predicate 接口是只有一个参数的返回布尔类型值的 断言型 接口。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非):

源码解析:

package java.util.function;
import java.util.Objects;

@FunctionalInterface
public interface Predicate<T> {

    // 该方法是接收一个传入类型,返回一个布尔值.此方法应用于判断.
    boolean test(T t);

    //and方法与关系型运算符"&&"相似,两边都成立才返回true
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }
    // 与关系运算符"!"相似,对判断进行取反
    default Predicate<T> negate() {
        return (t) -> !test(t);
    }
    //or方法与关系型运算符"||"相似,两边只要有一个成立就返回true
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }
   // 该方法接收一个Object对象,返回一个Predicate类型.用于判断两个对象是否相等
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }

应用场景:

Predicate 接口可以用于对集合进行过滤、对方法参数进行校验、以及实现复杂的逻辑判断等场景中。使用Predicate 接口可以使代码更加简洁、易于维护,增强代码的可读性和可重用性。

以下是一些常见的场景:

  1. 集合过滤:可以使用 Predicate 接口对集合进行过滤,例如筛选出所有大于等于 10 的整数:
List<Integer> list = Arrays.asList(1, 5, 10, 15, 20);
Predicate<Integer> predicate = i -> i >= 10;
List<Integer> filteredList = list.stream().filter(predicate).collect(Collectors.toList());

在这个例子中,首先创建了一个整型列表 list,然后定义了一个 Predicate 实例 predicate,用于判断一个整数是否大于等于 10。接着使用 stream 对列表进行流操作,在流操作中使用 filter 方法传入 predicate 实例对整数进行过滤,最后将过滤结果收集到一个新的列表中。

  1. 参数校验:可以使用 Predicate 接口对方法参数进行校验,例如校验一个字符串是否为空:
public void doSomething(String str) {
    Predicate<String> isEmpty = s -> s == null || s.length() == 0;
    if (isEmpty.test(str)) {
        throw new IllegalArgumentException("字符串不能为空");
    }
    // ...
}

在这个例子中,定义了一个 Predicate 实例 isEmpty,用于判断一个字符串是否为空。在方法中,对传入参数str进行校验,如果为空则抛出异常。

  1. 复杂逻辑判断:可以使用 Predicate 接口对多个条件进行组合,实现更加复杂的逻辑判断。例如判断一个字符串是否包含数字和大写字母:
String str = "Hello, World! 123";
Predicate<String> containsDigit = s -> s.matches(".*\\d.*");
Predicate<String> containsUpperCase = s -> s.matches(".*[A-Z].*");
if (containsDigit.and(containsUpperCase).test(str)) {
    System.out.println("字符串符合要求");
}

在这个例子中,定义了两个 Predicate 实例 containsDigitcontainsUpperCase,分别用于判断一个字符串是否包含数字和大写字母。在判断中使用 and 方法将两个实例组合起来,表示字符串必须同时包含数字和大写字母才符合要求。最后使用 test 方法对字符串进行判断,如果符合要求则输出相应信息。

小结
  1. Consumer 接口

    • 抽象方法 void accept(T t); 接收一个泛型参数 T,表示该操作的输入参数,无返回值;

    • 一个默认方法 andThen(Consumer after),用于组合多个 Consumer 实例,表示执行顺序为- 该 Consumer 接口先执行,然后执行 after 中的Consumer 接口。

  2. Supplier 接口

    • 抽象方法 T get; 用于返回一个泛型参数 T,表示该操作的结果。
  3. Function 接口

    • 抽象方法 R apply(T t); 接收参数 T 然后返回计算结果;

    • 两个默认方法(compose, andThen),用于将多个函数链接在一起;

    • 一个静态方法 identity() ,用于快速创建一个无需处理参数的 Function 实例。

  4. Predicate 接口

    • 抽象方法 boolean test(T t); 返回一个布尔值,应用于判断。

    • 包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与(and),或(or),取反(negate))

    • 一个静态方法 isEqual,用于判断两个对象是否相等

其他接口

Java 8 新特性深度解析:探索 Lambda 表达式、Stream API 和函数式编程的革新之路_第1张图片

3、方法引用与构造器引用

概念

使用操作符 “::” 将方法名和对象或类的名字分隔开来。

使用场景

当要传递给 Lambda 体的操作,已经有实现的方法了,可以使用方法引用!(实现抽象方法的参数列表,必须与方法引用方法的参数列表保持一致!)

(一)方法引用

1. 类 :: 静态方法

类 :: 静态方法(ClassName::staticMethodName

Java 8 新特性深度解析:探索 Lambda 表达式、Stream API 和函数式编程的革新之路_第2张图片

Function<String, Integer> strToInt = Integer::parseInt;
int result = strToInt.apply("123");
2. 对象 :: 实例方法

对象 :: 实例方法(instance::instanceMethodName

Java 8 新特性深度解析:探索 Lambda 表达式、Stream API 和函数式编程的革新之路_第3张图片

List<String> list = Arrays.asList("apple", "banana", "orange", "grape");
list.forEach(System.out::println);
3. 类 :: 实例方法

类 :: 实例方法(ClassName::instanceMethodName

Java 8 新特性深度解析:探索 Lambda 表达式、Stream API 和函数式编程的革新之路_第4张图片

List<String> list = Arrays.asList("apple", "banana", "orange", "grape");
list.stream().map(StringUtils::toUppercase).forEach(System.out::println);

(二)构造器引用

格式:ClassName::new

与函数式接口相结合,自动与函数式接口中方法兼容。可以把构造器引用赋值给定义的方法,与构造器参数列表要与接口中抽象方法的参数列表一致。

Java 8 新特性深度解析:探索 Lambda 表达式、Stream API 和函数式编程的革新之路_第5张图片

Function<String, Person> createPerson = Person::new;
Person person = createPerson.apply("Tom"); 

(三)数组引用

格式:type[] :: new

Java 8 新特性深度解析:探索 Lambda 表达式、Stream API 和函数式编程的革新之路_第6张图片

// 创建一个长度为 10 的数组
Function<Integer, String[]> createArray = String[]::new;
String[] array = createArray.apply(10);
// 将其中所有元素设置为 0
Arrays.stream(arr).forEach(int[]::setAll);

4、强大的 Stream API

概念

Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 使用 Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数 据库查询。也可以使用 Stream API 来并行执行操作。简而言之, Stream API 提供了一种高效且易于使用的处理数据的方式。

流(Stream)到底是什么?

是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。

“集合讲的是数据,流讲的是计算!”

注意:

  1. Stream 自己不会存储元素。
  2. Stream 不会改变源对象。相反,他们会返回一个持有结果的新 Stream。
  3. Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。

操作三步骤

Java 8 新特性深度解析:探索 Lambda 表达式、Stream API 和函数式编程的革新之路_第7张图片

(一)创建 Stream
顺序流与并行流

Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:

  • default Stream stream() : 返回一个顺序流

  • default Stream parallelStream() : 返回一个并行流

并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。

Java8 中将并行进行了优化,我们可以很容易的对数据进行并行操作。Stream API 可以声明性地通过 parallel() 与 sequential() 在并行流与顺序流之间进行切换。

数组流

Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:

static Stream stream(T[] array): 返回一个流

重载形式,能够处理对应基本类型的数组:

  • public static IntStream stream(int[] array)
  • public static LongStream stream(long[] array)
  • public static DoubleStream stream(double[] array)
由值创建流

可以使用静态方法 Stream.of(), 通过显示值创建一个流。它可以接收任意数量的参数。

public static Stream of(T… values) : 返回一个流

无限流

由函数创建流。可以使用静态方法 Stream.iterate() 和 Stream.generate(), 创建无限流。

  • 迭代

    public static Stream iterate(final T seed, final UnaryOperator f)

  • 生成

    public static Stream generate(Supplier s)

(二)Stream 的中间操作
什么是中间操作?

中间操作是对 Stream 对象进行加工处理的操作,用于对 Stream 中的元素进行处理、筛选、过滤、排序、去重等操作。中间操作可以使用链式调用的方式进行连续操作,**每次操作都会返回一个新的 Stream 对象,**这样可以构建出一条操作流水线。

惰性求值

多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值” 。

API
筛选与切片
方法 描述
filter(Predicate) 接收 Lambda ,从流中排除某些元素
distinct() 筛选,通过流所生成元素的 hashCode() 和 equals() 去除重复元素
limit(long maxSize) 截断流,使其元素不超过给定数量。
skip(long n) 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补
映射
方法 描述
map(Function f) 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
mapToDouble(ToDoubleFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream。
mapToInt(ToIntFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream。
mapToLong(ToLongFunction f) 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream。flatMap(Function f) 接收一个函数作为参数,将流中的每个值都换成另 一个流,然后把所有流连接成一个流。
排序
方法 描述
sorted() 产生一个新流,其中按自然顺序排序
sorted(Comparator comp) 产生一个新流,其中按比较器顺序排序
(三)终止操作
何为终止操作?
  • 终止操作是对 Stream 对象执行最终操作的操作,用于触发 Stream 的处理流程,并返回最终的结果。
  • 终止操作可以是有返回值的,例如collect()方法,也可以是没有返回值的,例如forEach()方法。
  • 终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void。
API
查找与匹配
方法 描述
allMatch(Predicate p) 检查是否匹配所有元素
anyMatch(Predicate p) 检查是否至少匹配一个元素
noneMatch(Predicate p) 检查是否没有匹配所有元素
findFirst() 返回第一个元素
count() 返回流中元素总数
max(Comparator c) 返回流中最大值
min(Comparator c) 返回流中最小值
forEach(Consumer) 内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反,Stream API 使用内部迭代一一它帮你把迭代做了)
归约
方法 描述
reduce(T iden,BinaryOperator) 可以将流中元素反复结合起来,得到一个值返口 T
reduce(BinaryOperator b) 可以将流中元素反复结合起来,得到一个值。返 Optional

**备注:**map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名

收集
方法 描述
collect(Collector c) 将流转换为其他形式。接收一个 Collector 接口的实现,用于给 Stream 中元素做汇总的方法

Collector 接口中方法的实现决定了如何对流执行收集操作(如收 集到 List、Set、Map)。
但是 Collectors 实用类提供了很多静态 方法,可以方便地创建常见收集器实例,具体方法与实例如下表:

Java 8 新特性深度解析:探索 Lambda 表达式、Stream API 和函数式编程的革新之路_第8张图片Java 8 新特性深度解析:探索 Lambda 表达式、Stream API 和函数式编程的革新之路_第9张图片

ForK/Join 框架

什么是 Fork/Join 框架?

**Fork/Join 框架:**就是在必要的情况下,将一个大任务,进行拆分 (fork) 成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总。

并行流:

parallelStream 可多线程执行,是基于 ForkJoin 框架实现的

forEach() 用到的就是多线程

Java 8 新特性深度解析:探索 Lambda 表达式、Stream API 和函数式编程的革新之路_第10张图片

Fork/Join 框架与传统线程池的区别

采用“工作窃取”模式(work-stealing):当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中窃取一个并把它放在自己的队列中继续执行。

相对于一般的线程池实现,fork/join 框架的优势体现在对其中包含的任务的处理方式上

  • 在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。

  • 而在 fork/join 框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行,这种方式是动态的,减少了线程的等待时间,并提高了性能。

  • Fork/Join 框架提供了一些特殊的方法,比如 fork()join() 等,用于处理分治任务,简化了代码的编写和管理。

5、新时间日期 API

LocalDate、LocalTime、LocalDateTime

三种类的实例都是不可变的对象。

  • LocalDate – 表示使用 ISO-8601 日历系统的日期
  • LocalTime – 表示使用 ISO-8601 日历系统的时间
  • LocalDateTime – 表示使用 ISO-8601 日历系统的日期和时间
LocalDateTime.class //日期+时间 format: yyyy-MM-ddTHH:mm:ss.SSS
LocalDate.class //日期 format: yyyy-MM-dd
LocalTime.class //时间 format: HH:mm:ss

注:ISO-8601 日历系统是国际标准化组织制定的现代公民的日期和时间的表示。

各种方法如下:

Java 8 新特性深度解析:探索 Lambda 表达式、Stream API 和函数式编程的革新之路_第11张图片

instant 时间戳

用于“时间戳”的运算。它是以 Unix 元年(传统的设定为 UTC 时区 1970 年 1 月 1 日午夜时分)开始所经历的描述进行运算。

Duration 和 Period

  • Duration: 用于计算两个**“时间”间隔**

    import java.time.Duration;
    import java.time.LocalDateTime;
    public class DurationExample {
        public static void main(String[] args) {
            //获取当前时间
            LocalDateTime start = LocalDateTime.now();
            System.out.println("开始时间:" + start);
            //模拟程序执行
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //获取执行结束时间
            LocalDateTime end = LocalDateTime.now();
            System.out.println("结束时间:" + end);
            //计算时间差
            Duration duration = Duration.between(start, end);
            System.out.println("程序执行时间:" + duration.toMillis() + " 毫秒");
        }
    }
    
  • Period: 用于计算两个**“日期”间隔**

    import java.time.LocalDate;
    import java.time.Period;
    public class PeriodExample {
        public static void main(String[] args) {
            //获取当前日期
            LocalDate start = LocalDate.now();
            System.out.println("开始日期:" + start);
            //模拟时间间隔
            LocalDate end = start.plusDays(30);
            System.out.println("结束日期:" + end);
            //计算日期差
            Period period = Period.between(start, end);
            System.out.println("日期间隔:" + period.getDays() + " 天");
        }
    }
    

操作日期的类

  • TemporalAdjuster: 时间校正器。

例如,有时我们可能需要获取:将日期调整到“下个周日”等操作。

  • TemporalAdjusters: 该类通过静态方法提供了大量的常用 TemporalAdjuster 的实现,方便开发者对日期进行各种调整操作。
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
public class TemporalAdjusterExample {
    public static void main(String[] args) {
        //获取当前日期
        LocalDate date = LocalDate.now();
        System.out.println("当前日期:" + date);
        //获取本月的第一天
        LocalDate firstDayOfMonth = date.with(TemporalAdjusters.firstDayOfMonth());
        System.out.println("本月第一天:" + firstDayOfMonth);
        //获取本月的最后一天
        LocalDate lastDayOfMonth = date.with(TemporalAdjusters.lastDayOfMonth());
        System.out.println("本月最后一天:" + lastDayOfMonth);
        //获取下一个周二
        LocalDate nextTuesday = date.with(TemporalAdjusters.next(DayOfWeek.TUESDAY));
        System.out.println("下一个周二:" + nextTuesday);
    }
}

日期时间格式化和解析的类

java.time.format.DateTimeFormatter 类:

该类提供了三种格式化方法:

1. 预定义的标准格式

DateTimeFormatter 类提供了一些预定义的标准格式,例如 ISO_DATEISO_TIMEISO_DATE_TIME 等等,这些标准格式已经定义好了日期时间的格式,开发者可以直接使用。

2. 语言环境相关的格式

DateTimeFormatter 类还提供了一些与语言环境相关的格式,例如 ofLocalizedDate(FormatStyle style)ofLocalizedTime(FormatStyle style)ofLocalizedDateTime(FormatStyle style) 等等,这些格式会根据不同的语言环境自动适配不同的日期时间格式

3. 自定义的格式

例如 ofPattern(String pattern) 方法可以传入一个字符串参数,用于定义自己想要的日期时间格式,"yyyy-MM-dd HH:mm:ss" 就是一个自定义的日期时间格式。

时区的处理

Java8 中加入了对时区的支持,带时区的时间为分别为:ZonedDateZonedTimeZonedDateTime 其中每个时区都对应着 ID,地区 ID 都为 “{区域}/{城市}”的格式 例如 :Asia/Shanghai 等

  • ZoneId:该类中包含了所有的时区信息
  • getAvailableZoneIds(): 可以获取所有时区时区信息
  • of(id): 根据指定的时区信息获取 ZoneId 对象

讲一下与传统日期的转换

Java8 中的日期和时间 API 与传统的日期类(如 java.util.Datejava.util.Calendar)之间存在一些差异,因此在进行日期转换时需要注意以下几点:

  1. java.util.Datejava.util.Calendar 类是可变的,而 Java8 中的日期和时间类是不可变的,因此在进行转换时需要特别注意。

  2. java.util.Date 类的精度是毫秒级别,而 Java8 中的日期和时间类的精度可以达到纳秒级别,因此在进行转换时需要注意精度的损失。

  3. Java8 中的日期和时间类之间可以相互转换,例如可以将 LocalDateTime 对象转换为 Instant 对象,但是需要注意转换后的时区和精度问题。

    以下是一个简单的 Java8 和传统日期类(java.util.Datejava.util.Calendar)之间的转换示例:

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Calendar;
import java.util.Date;
public class DateConversionExample {
    public static void main(String[] args) {
        // Java 8日期类转换为传统日期类
        LocalDateTime dateTime = LocalDateTime.now();
        Date date = Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        System.out.println("Java 8日期类转换为传统日期类:" + calendar.getTime());
        // 传统日期类转换为Java 8日期类
        calendar = Calendar.getInstance();
        calendar.set(2022, Calendar.APRIL, 28);
        date = calendar.getTime();
        dateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
        System.out.println("传统日期类转换为Java 8日期类:" + dateTime);
        // Java 8日期类之间的转换
        dateTime = LocalDateTime.now();
        Instant instant = dateTime.atZone(ZoneId.systemDefault()).toInstant();
        dateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
        System.out.println("Java 8日期类之间的转换:" + dateTime);
    }
}

6、接口中的默认方法与静态方法

概念

Java8 中允许接口中包含具有具体实现的方法,该方法称为 “默认方法”,默认方法使用 default 关键字修饰;并允许添加静态方法。

接口默认方法的“类优先”原则:

如果一个类实现了多个接口,而这些接口中定义了同名的默认方法,那么编译器就会产生歧义,因为它不知道应该使用哪个默认方法。为了解决这个问题,Java 8 引入了“类优先”(Class Priority)的原则,即如果一个类继承了另一个类并且实现了一个接口,而这两者中都定义了同名的默认方法,那么类中的方法优先于接口中的方法。

注意:在使用“类优先”原则时,如果类中的方法不是完全覆盖了接口中的方法,那么编译器会报错,此时需要在类中显式地调用接口中的方法。

interface MyInterface {
    default void myMethod() {
        System.out.println("Default method in interface");
    }
}
class MyClass {
    public void myMethod() {
        System.out.println("Method in class");
    }
}
class MyImplementation extends MyClass implements MyInterface {
}
public class DefaultMethodExample {
    public static void main(String[] args) {
        MyImplementation obj = new MyImplementation();
        obj.myMethod(); // 输出 "Method in class"
    }
}

7、其他新特性

Optional 类?

Optional 类 (java.util.Optional) 是一个容器类,代表一个值存在或不存在,原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且 可以避免空指针异常

常用方法:

  1. Optional.of(T t): 创建一个 Optional 实例
  2. Optional.empty(): 创建一个空的 Optional 实例
  3. Optional.ofNullable(T t): 若 t 不为 null,创建 Optional 实例,否则创建空实例
  4. isPresent(): 判断是否包含值
  5. orElse(T t): 如果调用对象包含值,返回该值,否则返回 t
  6. orElseGet(Supplier s): 如果调用对象包含值,返回该值,否则返回 s 获取的值
  7. map(Function f): 如果有值对其处理,并返回处理后的 Optional,否则返回 Optional.empty()
  8. flatMap(Function mapper): 与 map 类似,要求返回值必须是 Optional

重复注解与类型注解

Java8 对注解处理提供了两点改进:可重复的注解可用于类型的注解

  1. 可重复的注解

    在Java 8之前,同一个注解不能在同一个地方重复使用,这在某些情况下会导致代码的冗余。Java8 引入了可重复的注解,它允许同一个注解在同一个地方重复使用。这种注解需要使用 @Repeatable 元注解来标注

    举个例子,假设我们有一个 @Author 注解,用于表示文章的作者。在 Java8 之前,我们无法在同一个类或方法上使用多次 @Author 注解,而必须定义多个注解,如 @Author1@Author2 等。在 Java8 中,我们可以使用可重复的注解来解决这个问题,示例代码如下:

    @Repeatable(Authors.class)
    @interface Author {
        String name();
    }
    @interface Authors {
        Author[] value();
    }
    @Authors({
        @Author(name = "Alice"),
        @Author(name = "Bob")
    })
    public class Article {
        // ...
    }
    
  2. 可用于类型的注解

    Java8 还引入了一种新的注解类型 —— 类型注解(Type Annotation),它可以用于注解类型(如类、接口、枚举、注解等)上。这种注解需要使用新的元注解 @Target 来标注,同时需要指定 ElementType.TYPE_USE 作为注解的目标类型。

    举个例子,假设我们有一个 @NotNull 注解,用于表示一个类型不为 null。在 Java8 之前,我们只能将它应用到方法的参数上,而不能将其应用到类型上。在 Java8 中,我们可以使用类型注解将其应用到类型上,示例代码如下:

    interface NotNull {}
    class Example<@NotNull T> {
        public void foo(List<@NotNull String> strings) {}
    }
    

8、Java 8 实战

推荐参考此篇文章:

https://javaguide.cn/java/new-features/java8-common-new-features.html

9、peek()

peek() 方法是 Stream API 中的一个中间操作,它允许你在不改变数据的情况下,对流中的元素进行某种操作。peek() 方法接收一个 Consumer 接口的实现,对流中的每个元素执行该操作,并返回一个新的流,其中包含与原始流相同的元素。

import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream.of(1, 2, 3, 4, 5)
                .filter(n -> n % 2 == 0)
                .peek(System.out::println)
                .collect(Collectors.toList());
    }
}

在这个示例中,我们创建了一个包含 1 到 5 的整数流,然后使用 filter() 方法过滤出偶数。接下来,我们使用 peek() 方法打印每个元素,最后使用 collect() 方法将流转换为列表。输出结果如下:

2
4

返回一个新的流有啥必要?

返回一个新的流在某些情况下是有必要的,因为这样可以保留原始流的状态。当你对一个流进行操作时,例如过滤、映射或排序等,原始流不会受到影响,因为这些操作都是惰性的,只有在需要时才会执行。

然而,如果你在 forEach 方法中对元素进行了修改,那么这些修改会影响到原始流。这是因为 forEach 方法返回的是一个 void,它不能直接返回一个新的流。但是,你仍然可以在 forEach 方法中对原始流进行操作,然后使用 .collect() 方法将流转换为列表或其他集合类型。

总之,返回一个新的流并不总是必要的,但它在某些情况下是有用的,特别是当你需要保留原始流的状态时。

应用场景

peek() 方法主要用于调试。它可以用于查看流中的元素,但不会改变流的状态。例如,你可以使用 peek() 方法来查看一个整数流中的偶数,但不会将它们从流中删除。这个方法在多线程的场景下也很有用,因为它可以用于读取队列头部元素。

总之,peek() 方法主要用于调试和多线程场景下读取队列头部元素。
html](https://javaguide.cn/java/new-features/java8-common-new-features.html)

你可能感兴趣的:(Java,java,开发语言,学习)