Java8新特性
一、 lambda表达式
- Lambda 表达式是JDK8 的一个新特性,也被称为闭包,Lambda表达式允许把函数作为一个方法的参数(即行为参数化)传递进方法中。
- Lambda表达式可以取代大部分的匿名内部类,写出更优雅的 Java 代码,尤其在集合的遍历和集合操作中,可以极大地优化代码结构。
- Lambda表达式的参数列表的数据类型可以省略不写,因为JVM编译器能够通过上下文推断出数据类型,这就是“类型推断“。
- lambda表达式可以返回值,就像从方法中返回值一样,添加一个return;
说明:
- lambda 表达式引用的外层局部变量必须是具有final语义的变量:
- 在域外已被final修饰的局部变量
- 没有被final修饰,但是必须不可被后面的代码修改(包括lambda内部和外部均不可修改)(即隐性的具有 final 的语义)
- 在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。
示例
- 无参数
()-> System.outprintln("hello word!");
- 一个参数
Consumer<string> con =(x)->System.out.println(x);
当只有一个参数时可以省略括号
Consumer<string> con =x->System.out.println(x);
- 多个参数
BinaryOperator<Integer> bo = (a,b) -> {
system.outprintln("函数式接口);
return a + b;
};
- 指定参数类型
如果编译器无法从lambda匹配的函数式接口抽象方法推断参数类型,则有时可能需要为lambda表达式指定参数类型
(Car car)-> System.out.println("The car is"+ car.getName ());
- 只有一条语句时
当Lambda体只有一条语句时,return和大括号可以省略,示例:
BinaryOperator<Integer>bo= (a,b) -> a + b;
- 完整示例
//JDK 8之前
new Thread(new Runnable () {
@Override
public void run() {
system.out.printIn("使用匿名内部类,开线程");
}
}).start();
//JDK 8 使用Lambda表达式
new Thread(() -> System.out.println("使用lambda表达式,开线程")).start();
二、 函数式接口
- lambda表达式需要函数式接口的支持
- 对于函数式接口,我们可以理解为只有一个抽象方法的接口,除此之外它和别的接口相比并没有什么特殊的地方。
- 为了确保函数式接口的正确性,我们可以给这个接口添加@FunctionalInterface注解,这样当其中有超过一个抽象方法时就会报错。
- 只要一个对象是函数式接口的实例,那么该对象就可以用Lambda表达式来表示
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target (ElementType.TYPE)
public @interface FunctionalInterface {}
常见函数式接口
-
Consumer 表示接受一个输入参数并且不返回结果的操作。其中 T 表示输入参数类型。它有一个名为 accept 的抽象方法,可以使用 Lambda 表达式来实现该方法。例如:
Consumer<String> printString = s -> System.out.println(s); printString.accept("Hello World");
-
Function<T, R> 表示接受一个输入参数并返回结果的操作。其中 T 表示输入参数类型,R 表示返回结果类型。它有一个名为 apply 的抽象方法,可以使用 Lambda 表达式来实现该方法。例如:
Function<Integer, String> convertToString = i -> i.toString(); String str = convertToString.apply(123); System.out.println(str);
-
BiFunction<T, U, R> 表示接受两个输入参数并返回结果的操作。其中 T 和 U 分别表示两个输入参数的类型,R 表示返回结果类型。它有一个名为 apply 的抽象方法,可以使用 Lambda 表达式来实现该方法。例如:
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b; int result = add.apply(1, 2); System.out.println(result);
-
Supplier 表示提供一个结果的操作。其中 T 表示返回结果的类型。它有一个名为 get 的抽象方法,可以使用 Lambda 表达式来实现该方法。例如:
Supplier<String> getMessage = () -> "Hello World"; String msg = getMessage.get(); System.out.println(msg);
三、 方法引用
java中的方法引用(Method Reference)是一种简化Lambda表达式的语法,它可以直接引用已有方法或构造方法,并将其作为Lambda表达式的参数传递。方法引用可以使代码更加简洁易读,提高代码的可维护性。需要注意的是,方法引用只是Lambda表达式的一种语法糖,它并不是新的语言特性。
在Java 8中,方法引用的语法格式为:
方法引用类型::方法名
其中,方法引用类型可以是以下四种:
- 静态方法引用:Class::staticMethod
//使用 Integer.parseInt 静态方法引用来将字符串转换为整型
List<String> strList = Arrays.asList("1", "2", "3");
List<Integer> intList = strList.stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
- 实例方法引用:instance::instanceMethod
class MyClass {
public static void main(String[] args) {
// 使用方法引用
//字符串拼接
MyClass obj = new MyClass();
Consumer<String> instanceMethod = obj::instanceMethod;
instanceMethod.accept("sssss");
//生成随机数
Random random = new Random();
Function<Integer, Integer> integerFunction = random::nextInt;
Integer randomInt = integerFunction.apply(10);
}
// 实例方法
public void instanceMethod(String name) {
System.out.println("Hello " + name);
}
}
- 构造方法引用:Class::new
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
class MyPerson{
public static void main(String[] args) {
BiFunction<String, Integer, Person> stringPersonFunction = Person::new;
Person xiaolin = stringPersonFunction.apply("xiaolin", 18);
}
}
- 数组构造方法引用(创建数组):Type[]::new
使用场景:在需要大量创建数组来存储数据的情况下,示例:
String[] array = Stream.generate(() -> "Hello")
.limit(5)
.toArray(String[]::new);
//---输出---
[Hello, Hello, Hello, Hello, Hello]
四、 Stream流
Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据,Stream流是对集合(Collection)对象功能的增强,与Lambda表达式结合,可以提高编程效率、间接性和程序可读性
两种运行方式:
- 顺序流(Sequential Stream)是普通的单线程流,所有的元素按顺序依次处理。这种方式适合于处理小型数据集,或者需要保持元素顺序的场景。
- 并行流(Parallel Stream)是多线程流,将数据集分割成多个小块,并且在不同线程中并行处理,然后将结果合并返回。这种方式适合于处理大型数据集或需要加速处理的场景。并行流的处理方式是 Java 自动管理的,我们只需要将顺序流转换为并行流即可。
两种操作方式:
- 中间操作(Intermediate Operations):在流上执行的操作,它们返回一个新的流,并且支持链式调用。中间操作不会立即执行,只有在终止操作被调用时才会触发执行。
- 终止操作(Terminal Operations):是流的最后一步操作,它们会执行中间操作链上的所有操作,并产生最终的结果
生成流
- 集合创建:可以通过 Collection 接口的 stream() 或 parallelStream() 方法创建 Stream 流。比如:
List<String> list = Arrays.asList("apple", "banana", "orange");
Stream<String> stream = list.stream(); // 串行流
Stream<String> parallelStream = list.parallelStream(); // 并行流
- Array数组创建:可以通过 Arrays 类的 stream() 方法创建 Stream 流。比如:
int[] arr = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(arr);
注:通过Arrays.stream方法生成流,该方法生成的流是数值流【即IntStream】而不是 Stream,使用数值流可以避免计算过程中拆箱装箱,提高性能。
- Stream创建:可以通过 Stream 接口的静态方法 of() 创建 Stream 流。比如:
Stream<String> stream = Stream.of("apple", "banana", "orange");
- 函数创建 :可以通过 Stream 接口的静态方法 generate() 或 iterate() 创建 Stream 流。比如:
Stream<Integer> stream = Stream.generate(() -> new Random().nextInt(10)).limit(5);;
Stream<Integer> iterateStream = Stream.iterate(0, n -> n + 2).limit(5);;
注: generate() 或 iterate() 创建的流都是无限流,所以需要用limit(n)进行截断
- 通过 BufferedReader 的 lines() 方法创建 Stream 流:可以通过 BufferedReader 的 lines() 方法创建 Stream 流,用于读取文件中的文本内容。比如:
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
Stream<String> lines = br.lines();
} catch (IOException e) {
e.printStackTrace();
}
注:返回的 Stream
对象,每个字符串表示文件中的一行文本。
中间操作符
- filter:过滤,用于通过设置的条件过滤出元素
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
- map:接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)
List<String> words = Arrays.asList("apple", "banana", "orange");
List<String> upperCaseWords = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
- distinct:去重,返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流
List<Integer> numbers = Arrays.asList(1, 2, 3, 2, 1, 4, 5, 3);
List<Integer> distinctNumbers = numbers.stream()
.distinct()
.collect(Collectors.toList());
- sorted:sorted()返回一个自然排序后的 Stream,sorted(Comparator comparator):返回一个按指定比较器排序后的 Stream
List<Integer> numbers = Arrays.asList(3, 1, 4, 2, 5);
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.collect(Collectors.toList());
- limit:截取流中前n个元素
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> limitedNumbers = numbers.stream()
.limit(3)
.collect(Collectors.toList());
- skip:返回一个扔掉了前n个元素的流
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> skippedNumbers = numbers.stream()
.skip(2)
.collect(Collectors.toList());
- flatMap:将每个元素映射为一个 Stream 流,然后将所有的 Stream 流合并成一个 Stream 流,它返回的是一个 Stream 流的流(Stream)
List<String> lines = Arrays.asList("hello world", "welcome to java", "stream example");
List<String> words = lines.stream()
.flatMap(line -> Arrays.stream(line.split(" ")))
.collect(Collectors.toList());
System.out.println(words);
//----------输出内容----
[hello, world, welcome, to, java, stream, example]
- peek:对元素进行遍历处理,在处理 Stream 中的每个元素时查看它们的值,但不会修改元素本身
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squares = numbers.stream()
.peek(System.out::println)
.map(x -> x * x)
.collect(Collectors.toList());
注:map和peek的区别:
- map是将流中每个元素进行处理并转换,最终形成一个新的流
- peek是操作流中的所有元素,完成一定的动作(例如诊断性质的操作),但是不会修改流本身,不会生成新的流
终端操作符
一个流有且只能有一个终端操作,当这个操作执行后,流就被关闭了,无法再被操作,因此一个流只能被遍历一次,若想在遍历需要通过源数据在生成流
-
forEach:对流中的每个元素进行操作,不返回值。例如:
List<String> list = Arrays.asList("a", "b", "c"); list.stream().forEach(System.out::println);
-
count:返回流中元素的个数。例如:
List<String> list = Arrays.asList("a", "b", "c"); long count = list.stream().count(); System.out.println(count);
-
collect:将流中的元素收集到一个集合中。例如:
List<String> list = Arrays.asList("a", "b", "c"); List<String> newList = list.stream().collect(Collectors.toList()); System.out.println(newList);
-
reduce:对流中的元素进行归约操作。例如:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream().reduce(0, (a, b) -> a + b); System.out.println(sum); //上述代码中,reduce()方法将流中的元素归约成一个值,初始值为0,BinaryOperator函数的实现是将两个元素相加 //---输出--- 15 List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); Optional<Integer> max = numbers.stream().reduce(Integer::max); System.out.println(max.get()); //上述代码中,使用Integer::max方法引用作为BinaryOperator函数,将流中的元素归约成一个最大值,最终得到Optional对象,需要使用get()方法获取结果。 //---输出--- 5
-
anyMatch:判断流中是否有任意一个元素满足指定的条件。例如:
List<String> list = Arrays.asList("apple", "banana", "cherry"); boolean result = list.stream().anyMatch(s -> s.startsWith("a")); System.out.println(result); //---输出--- true
-
allMatch:判断流中是否所有元素都满足指定的条件。例如:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); boolean result = list.stream().allMatch(i -> i > 0); System.out.println(result); //---输出--- true
-
noneMatch:判断流中是否没有任何一个元素满足指定的条件。例如:
List<String> list = Arrays.asList("apple", "banana", "cherry"); boolean result = list.stream().noneMatch(s -> s.startsWith("d")); System.out.println(result); //---输出--- true
-
findFirst:返回流中的第一个元素。例如:
List<String> list = Arrays.asList("apple", "banana", "cherry"); Optional<String> first = list.stream().findFirst(); System.out.println(first.get()); //---输出--- apple
-
findAny:返回流中的任意一个元素。例如:
List<String> list = Arrays.asList("apple", "banana", "cherry"); Optional<String> any = list.stream().findAny(); System.out.println(any.get()); //---输出--- apple
-
min()、max():返回流中的最小值和最大值,可以接受一个Comparator作为参数进行比较。例如:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); Optional<Integer> min = list.stream().min(Comparator.naturalOrder()); Optional<Integer> max = list.stream().max(Comparator.naturalOrder()); System.out.println("Min value: " + min.get()); // 输出:Min value: 1 System.out.println("Max value: " + max.get()); // 输出:Max value: 5
-
sum()方法返回流中所有元素的和。例如:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); int sum = list.stream().mapToInt(Integer::intValue).sum(); System.out.println("Sum: " + sum); // 输出:Sum: 15
-
Collect收集
Collector:结果收集策略的核心接口,具备将指定元素累加存放到结果容器中的能力;并在Collectors工具中提供了Collector接口的实现
-
toList(): 将Stream中的元素收集到List中,例如:
List<String> list = Stream.of("apple", "banana", "orange") .filter(s -> s.contains("a")) .collect(Collectors.toList());
-
toSet(): 将Stream中的元素收集到Set中,例如:
Set<Integer> set = Stream.of(1, 2, 3, 2, 1) .collect(Collectors.toSet());
-
joining(): 将Stream中的元素连接成一个字符串,例如:
String str = Stream.of("Hello", "World", "Java") .collect(Collectors.joining(", ", "[", "]")); //---输出--- [Hello, World, Java]
-
groupingBy(): 将Stream中的元素按照某个属性分组,例如:
Map<String, List<Person>> personGroups = Stream.of( new Person("John", "Doe", 28), new Person("Jane", "Doe", 26), new Person("Mary", "Smith", 30), new Person("Tom", "Smith", 32)) .collect(Collectors.groupingBy(Person::getLastName));
-
partitioningBy(): 将Stream中的元素按照某个条件分成两组,例如:
Map<Boolean, List<Integer>> partitioned = Stream.of(1, 2, 3, 4, 5, 6) .collect(Collectors.partitioningBy(i -> i % 2 == 0)); //---输出--- {false=[1, 3, 5], true=[2, 4, 6]}
-
maxBy()/minBy(): 返回Stream中最大/最小的元素,例如:
Optional<Integer> max = Stream.of(1, 2, 3, 4, 5) .collect(Collectors.maxBy(Integer::compareTo)); Optional<Integer> min = Stream.of(1, 2, 3, 4, 5) .collect(Collectors.minBy(Integer::compareTo));
-
counting(): 统计Stream中的元素个数,例如:
long count = Stream.of(1, 2, 3, 4, 5) .collect(Collectors.counting());
-
toMap():将流中的元素转化为一个 Map,其中流中的每个元素都将作为 key-value 对的一部分插入到 Map 中,例如:
List<String> words = Arrays.asList("hello", "world", "hello", "java"); Map<String, Integer> wordCountMap = words.stream() .collect(Collectors.toMap(Function.identity(), s -> 1, Integer::sum)); System.out.println(wordCountMap); //---输出--- {world=1, java=1, hello=2}
解析:上面的示例中,我们使用了 toMap() 方法,将 words 中的元素转化为一个 Map,其中 key 是元素本身,value 是该元素在流中出现的次数。
- Function.identity() 表示使用元素本身作为 key
- s -> 1 表示每个元素最初的 value 为 1
- Integer::sum 表示当出现重复的 key 时,将对应的 value 相加
-
summingInt()、summingLong()、summingDouble():将元素转换为整数(Long、Double)后进行求和操作。具体来说,该方法返回一个 Collector 对象,可用于将 Stream 中的元素转换为整数(Long、Double)并求和,示例:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = numbers.stream() .collect(Collectors.summingInt(Integer::intValue)); System.out.println(sum); // 输出 15
-
五、 CompletableFuture
CompletableFuture是Java 8中新增的一个类,用于支持异步编程。它可以在一个任务执行完成后自动触发下一个任务的执行,从而实现
链式调用。同时,CompletableFuture还提供了丰富的方法来处理任务的结果、异常和取消操作,以及等待任务执行完成的方法。
CompletableFuture和Future都是Java中用于异步执行任务的类,但是它们之间有一些重要的区别:
- 链式调用:CompletableFuture支持链式调用,可以在一个任务执行完成后自动触发下一个任务的执行,从而实现异步编程。而Future不支持链式调用,需要手动处理多个任务之间的依赖关系。
- 异常处理:CompletableFuture提供了异常处理方法,可以在任务执行过程中捕获异常并进行处理。而Future只能通过try-catch块来捕获异常。
- 取消任务:CompletableFuture支持取消任务,可以在任务执行过程中取消任务,或者在任务还没有开始执行时取消任务。而Future只能等待任务执行完成或者超时后取消任务。
- 等待任务完成:CompletableFuture提供了多种等待任务完成的方法,可以等待所有任务完成、等待任意一个任务完成或者等待指定时间后超时。而Future只能等待任务执行完成或者超时。
- 结果处理:CompletableFuture提供了多种方法来处理任务的结果,可以在任务执行完成后对结果进行转换、合并或者过滤。而Future只能通过get()方法来获取任务的结果。
常用方法(部分):
- runAsync():创建一个没有返回值的异步任务。
- supplyAsync():创建一个有返回值的异步任务
- thenRun()/thenRunAsync():上一个任务执行完成后,执行回调方法,但是前后两个任务没有参数传递,第二个任务也没有返回值。
- thenAccept()/thenAcceptAsync():上一个任务执行完成后,将上一个任务的返回值作为参数传到thenAccept方法中,thenAccept和thenAcceptAsync没有返回值
- thenApply()和thenApplyAsync():上一个任务执行完成后,将上一个任务的返回值作为参数传到thenApply方法中,thenApply和thenApplyAsync回调方法是有返回值的
- exceptionally():某个任务执行异常时,执行的回调方法;并且有抛出异常作为参数,传递到回调方法。参数为异常,有返回值。
- whenComplete():某个任务执行完成后,执行的回调方法,无返回值;并且whenComplete方法的参数是上个任务的结果。whenComplete接收的参数为两个,第一个是上个任务的结果,另一个是异常。因此即使主任务有异常,依然会执行。
- handle:和whenComplete差不多,都是表示某个任务执行完成后,执行的回调方法,handle方法的参数是上个任务的结果,但是handle是有返回值的,whenComplete没有返回值。
注意事项
-
CompletableFuture需要获取返回值,才能获取到异常信息。如果不加 get()/join()方法,看不到异常信息。
-
CompletableFuture的get()方法是阻塞的。如果使用它来获取异步调用的返回值,需要添加超时时间:
//反例 CompletableFuture.get(); //正例 CompletableFuture.get(5, TimeUnit.SECONDS);
-
默认线程池的注意点:CompletableFuture代码中使用了默认的线程池,处理的线程个数是电脑CPU核数-1。在大量请求过来的时候,处理逻辑复杂的话,响应会很慢。一般建议使用自定义线程池,优化线程池配置参数。
简单示例
public static void main(String[] args) throws Exception {
long startTime = System.currentTimeMillis();
System.out.println("主线程:程序开始执行");
// 创建一个CompletableFuture对象,表示一个异步任务,任务创建后就会开始执行
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
// 模拟一个长时间运行的任务
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 返回任务结果
System.out.println("第一个任务执行完毕");
return "Hello, world!";
});
System.out.println("主线程:我在两个异步任务中间");
//用上一个异步任务的入参作为返回值执行另一个任务,这个任务会等待上一个任务执行完毕后才会执行
CompletableFuture<String> async = future.thenApplyAsync(result -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第二个异步任务完成");
return result.toUpperCase();
});
System.out.println("主线程:进入睡眠模式,已睡着");
Thread.sleep(3000);
//在回调之前
System.out.println("主线程:我睡了3秒,我醒了");
// 等待异步任务完成
System.out.println("主线程:第一个任务返回"+future.get());
//异步回调
System.out.println("主线程:第二个任务返回"+async.get());
long endTime = System.currentTimeMillis();
System.out.println("主线程:程序执行耗时:"+(endTime-startTime));
}
//---输出---
主线程:程序开始执行
主线程:我在两个异步任务中间
主线程:进入睡眠模式,已睡着
第一个任务执行完毕
第二个异步任务完成
主线程:我睡了3秒,我醒了
主线程:第一个任务返回Hello, world!
主线程:第二个任务返回HELLO, WORLD!
主线程:程序执行耗时:3069
六、 接口默认方法
Java 8允许在接口中定义默认方法,这些方法可以有实现,可以在接口的实现类中直接调用,而无需重新实现。例如:
public interface MyInterface {
default void myMethod() {
System.out.println("Default method");
}
}
public class MyClass implements MyInterface {
// 不需要实现myMethod()方法
}
MyClass obj = new MyClass();
obj.myMethod(); // 输出:Default method
接口默认方法的要求:
- 接口默认方法必须用default关键字修饰。
- 接口默认方法必须有方法体,即必须有方法的实现。
- 接口默认方法可以被实现类覆盖(实现类重写默认方法可以加@Override也可以不加)
七、 Date/Time API
Java 8引入了新的Date/Time API,用于处理日期和时间。它提供了一组新的日期和时间类,如LocalDate、LocalTime、LocalDateTime等,可以更好地处理日期和时间,并提供了许多方便的方法,如计算日期间隔、格式化日期等。
例如:
LocalDate date = LocalDate.of(2022, Month.MARCH, 14);
LocalTime time = LocalTime.of(12, 30);
LocalDateTime dateTime = LocalDateTime.of(date, time);
System.out.println(dateTime); // 输出:2022-03-14T12:30