Files
www/blogs/java/java8/java8-1.md
zhuhjay 22e48d9558 build(www): 添加 Drone CI 流水线配置
- 新增 .drone.yml 文件用于定义 CI/CD 流程
- 配置了基于 Docker 的部署步骤
- 设置了工作区和卷映射以支持持久化数据
- 添加了构建准备阶段和 Docker 部署阶段
- 定义了环境变量和代理设置
- 配置了 artifacts 目录的处理逻辑
- 添加了 timezone 映射以确保时间同步
- 设置了 docker.sock 映射以支持 Docker in Docker
2025-11-01 13:36:00 +08:00

562 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: JDK8 特性(一)
date: 2022-05-23
sidebar: 'auto'
tags:
- JDK8
categories:
- Java
---
## 1 Lambda表达式
### 1.1 Lambda标准格式
- Lambda省去面向对象的条条框框, Lambda的标准格式格式由3个部分组成
- 格式说明:
- `(...args)`: 参数列表
- `->`: 分隔或连接参数与方法体的标识符
- `{...;}`: 方法体, 主要的代码逻辑
- eg:
```java
class Demo{
public static void main(String[] args) {
new Thread(() -> {
System.out.println("线程启动");
}).start();
}
}
```
### 1.2 Lambda初体验
#### 无参无返回值
```java
public class Demo1 {
public static void main(String[] args) {
fishDo(() -> System.out.println("小鱼在欢乐的游着..."));
}
private static void fishDo(Fish fish){
fish.swim();
}
}
/**
* 定义一个接口, 并且该接口只有一个需要实现的方法
**/
interface Fish{
void swim();
}
```
#### 有参有返回值
```java
public class Demo1 {
public static void main(String[] args) {
catEat(foodName -> {
System.out.println("小猫在吃" + foodName);
return 3;
});
}
private static void catEat(Cat cat){
System.out.println("小猫吃了" + cat.eat("🐟") + "分钟");
}
}
/**
* 定义一个接口, 并且该接口只有一个需要实现的方法
**/
interface Cat{
int eat(String foodName);
}
```
#### 小结
以后我们调用方法时,看到参数是接口就可以考虑使用Lambda表达式,Lambda表达式相当于是对接口中抽象方法的重写
### 1.3 Lambda实现原理
现有以下类
- 接口类
```java
public interface Swimable {
void swimming();
}
```
- main入口
```java
public class SwimImplDemo {
public static void main(String[] args) {
goSwimming(new Swimable() {
@Override
public void swimming() {
System.out.println("去匿名内部类游泳了");
}
});
}
private static void goSwimming(Swimable swim){
swim.swimming();
}
}
```
- 将以上的main方法进行执行后, 会编译生成以下字节码文件
- ![](/java8/匿名内部类生成的字节码.png)
- 将内部类的字节码文件通过 [XJad] 进行反编译
- 匿名内部类在编译后会形成一个新的类.$
```java
// Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://kpdus.tripod.com/jad.html
// Decompiler options: packimports(3) fieldsfirst ansi space
// Source File Name: SwimImplDemo.java
package com.zhuhjay.lambda;
import java.io.PrintStream;
// Referenced classes of package com.zhuhjay.lambda:
// Swimable, SwimImplDemo
static class SwimImplDemo$1 implements Swimable{
public void swimming(){
System.out.println("去匿名内部类游泳了");
}
SwimImplDemo$1(){}
}
```
- main入口改为使用Lambda表达式
```java
public class SwimImplDemo {
public static void main(String[] args) {
goSwimming(() -> {
System.out.println("去Lambda游泳了");
});
}
private static void goSwimming(Swimable swim){
swim.swimming();
}
}
```
- 将以上的main方法进行执行后, 不会生成多余的字节码文件
- 使用 [XJad] 反编译工具会失败
- 使用JDK工具来对Lambda表达式的字节码进行反汇编
```text
javap -c -p 文件名.class
-c表示对代码进行反汇编 -p显示所有类和成员
```
- 对Lambda表达式的字节码文件进行反汇编
```S
public class com.zhuhjay.lambda.SwimImplDemo {
public com.zhuhjay.lambda.SwimImplDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: invokedynamic #2, 0 // InvokeDynamic #0:swimming:()Lcom/zhuhjay/lambda/Swimable;
5: invokestatic #3 // Method goSwimming:(Lcom/zhuhjay/lambda/Swimable;)V
8: return
private static void goSwimming(com.zhuhjay.lambda.Swimable);
Code:
0: aload_0
1: invokeinterface #4, 1 // InterfaceMethod com/zhuhjay/lambda/Swimable.swimming:()V
6: return
private static void lambda$main$0();
Code:
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String 去Lambda游泳了
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
```
- 通过与源代码的比较后, 发现多了静态方法`lambda$main$0`
- 得出: Lambda表达式会在类中新生成一个私有的静态方法, 命名为`lambda$方法名$序列`
- 通过断点调试Lambda也可以从调用栈中发现该静态方法的生成(当然不会显式的在源码中出现)
- 验证在Lambda表达式运行中会生成一个内部类
- 命令格式: `java -Djdk.internal.lambda.dumpProxyClasses 要运行的包名.类名`
- 执行(需要退出包运行该命令): `java -Djdk.internal.lambda.dumpProxyClasses com.zhuhjay.lambda.SwimImplDemo`
- ![](/java8/获得运行时Lambda内部类.png)
- 查看反编译结果
```java
final class SwimImplDemo$$Lambda$1
implements Swimable {
public void swimming() {
SwimImplDemo.lambda$main$0();
}
private SwimImplDemo$$Lambda$1() {}
}
```
- 对以上结论来推断Lambda生成的字节码文件
```java
public class SwimImplDemo {
public static void main(String[] args) {
// 也是相当于Lambda生成了一个匿名内部类, 来调用Lambda生成的静态方法
goSwimming(new Swimable() {
public void swimming(){
SwimImplDemo.lambda$main$0();
}
});
}
/** Lambda生成的私有静态方法 **/
private static void lambda$main$0(){
System.out.println("去Lambda游泳了");
}
private static void goSwimming(Swimable swim){
swim.swimming();
}
}
```
- 小结
- 匿名内部类在编译的时候会一个class文件
- Lambda在程序运行的时候形成一个类
1. 在类中新增一个方法,这个方法的方法体就是Lambda表达式中的代码
2. 还会形成一个匿名内部类, 实现接口, 重写抽象方法
3. 在接口的重写方法中会调用新生成的方法.
### 1.4 Lambda省略格式
在Lambda标准格式的基础上使用省略写法的规则为
1. 小括号内参数的类型可以省略
2. 如果小括号内有且仅有一个参数,则小括号可以省略
3. 如果大括号内有且仅有一个语句可以同时省略大括号、return关键字及语句分号
### 1.5 Lambda前提条件
Lambda的语法非常简洁但是Lambda表达式不是随便使用的使用时有几个条件要特别注意
1. 方法的参数或局部变量类型必须为接口才能使用Lambda
2. 接口中有且仅有一个抽象方法(@FunctionalInterface用来检测接口是否为函数式接口)
### 1.6 Lambda和匿名内部类对比
1. 所需的类型不一样
- 匿名内部类,需要的类型可以是类,抽象类,接口
- Lambda表达式,需要的类型必须是接口
2. 抽象方法的数量不一样
- 匿名内部类所需的接口中抽象方法的数量随意
- Lambda表达式所需的接口只能有一个抽象方法
3. 实现原理不同
- 匿名内部类是在编译后会形成class
- Lambda表达式是在程序运行的时候动态生成class
## 2 JDK8接口增强
### 2.1 增强介绍
- JDK8以前的接口:
```text
interface 接口名 {
静态常量;
抽象方法;
}
```
- JDK8的接口:
```text
interface 接口名 {
静态常量;
抽象方法;
默认方法;
静态方法;
}
```
### 2.2 接口默认方法和静态方法的区别
1. 默认方法通过实例调用,静态方法通过接口名调用。
2. 默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。
3. 静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用。
## 3 常用内置函数式接口
### 3.1 内置函数式接口的由来
我们知道使用Lambda表达式的前提是需要有函数式接口。而Lambda使用时不关心接口名抽象方法名只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda方便JDK提供了大量常用的函数式接口。
### 3.2 常用函数式接口的介绍
它们主要在`java.util.function`包中。下面是最常用的几个接口。
#### Supplier:生产者
`java.util.function.Supplier<T>`接口,它意味着"供给", 对应的Lambda表达式需要"对外提供"一个符合泛型类型的对象数据。
```java
@FunctionalInterface
public interface Supplier<T> {
public abstract T get();
}
```
#### Consumer:消费者
`java.util.function.Consumer<T>`接口则正好相反,它不是生产一个数据,而是消费一个数据,其数据类型由泛型参数决定。
```java
@FunctionalInterface
public interface Consumer<T> {
public abstract void accept(T t);
/** 该方法使得两个Consumer先后调用 c1.andThen(c2).accept(t) **/
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
```
- 默认方法: andThen
- 如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法 andThen
- 要想实现组合需要两个或多个Lambda表达式即可而 andThen 的语义正是"一步接一步"操作
#### Function:类型转换
`java.util.function.Function<T,R>`接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值。
```java
@FunctionalInterface
public interface Function<T, R> {
public abstract R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
}
```
- 默认方法: andThen
- Function 接口中有一个默认的 andThen 方法,用来进行组合操作
- 该方法同样用于"先做什么,再做什么"的场景,和 Consumer 中的 andThen 差不多
#### Predicate:判断
有时候我们需要对某种类型的数据进行判断从而得到一个boolean值结果。这时可以使用`java.util.function.Predicate<T>`接口。
```java
@FunctionalInterface
public interface Predicate<T> {
public abstract boolean test(T t);
/** && **/
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);
}
/** || **/
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
```
- 默认方法: and
- 既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate 条件使用"与"逻辑连接起来实现"并且"的效果时可以使用default方法 and
- 默认方法: or
- 与 and 的"与"类似,默认方法 or 实现逻辑关系中的"或"
- 默认方法: negate
- "与"、"或"已经了解了,剩下的"非"取反也会简单。它是执行了test方法之后对结果boolean值进行"!"取反而已。一定要在 test 方法调用之前调用 negate 方法
## 4 方法引用
方法引用的注意事项
1. 被引用的方法,参数要和接口中抽象方法的参数一样
2. 当接口抽象方法有返回值时,被引用的方法也必须有返回值
### 4.1 方法引用简化Lambda
使用Lambda表达式求一个数组的和
```java
public class Demo2 {
public static void main(String[] args) {
// 将数组进行求和
printSum((arr) -> {
int sum = 0;
for (int i : arr) {
sum += i;
}
System.out.println("sum = " + sum);
});
// 使用已有的方法进行方法引用(让已实现的方法复用)
// 类名::静态方法
printSum(Demo2::getSum);
}
/** 已有的求和方法 **/
private static void getSum(int[] arr){
int sum = 0;
for (int i : arr) {
sum += i;
}
System.out.println("sum = " + sum);
}
/** 使用消费者函数式接口 **/
private static void printSum(Consumer<int[]> consumer){
int[] arr = new int[]{11, 22, 33, 44};
consumer.accept(arr);
}
}
```
### 4.2 方法引用格式
- 符号表示: `::`
- 符号说明: 双冒号为方法引用运算符,而它所在的表达式被称为方法引用。
- 应用场景: 如果Lambda所要实现的方案 , 已经有其他方法存在相同方案,那么则可以使用方法引用
### 4.3 常见引用方式
方法引用在JDK 8中使用方式相当灵活有以下几种形式
1. `instanceName::methodName` 对象::方法名
2. `ClassName::staticMethodName` 类名::静态方法
3. `ClassName::methodName` 类名::普通方法
4. `ClassName::new` 类名::new 调用的构造器
5. `TypeName[]::new` String[]::new 调用数组的构造器
#### 对象::成员方法
这是最常见的一种用法。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为:
```java
public class MethodRefDemo {
public static void main(String[] args) {
Date now = new Date();
// 使用Lambda表达式获取当前时间
Supplier<Long> su1 = () -> now.getTime();
System.out.println(su1.get());
// 使用方法引用获取当前时间
Supplier<Long> su2 = now::getTime;
System.out.println(su2.get());
}
}
```
#### 类名::静态方法
由于在`java.lang.System`类中已经存在了静态方法 currentTimeMillis所以当我们需要通过Lambda来调用该方法时,可以使用方法引用, 写法是:
```java
public class MethodRefDemo {
public static void main(String[] args) {
Supplier<Long> su3 = () -> System.currentTimeMillis();
// 等同于, 调用该类的静态方法
Supplier<Long> su4 = System::currentTimeMillis;
}
}
```
#### 类名::引用实例方法
Java面向对象中类名只能调用静态方法类名引用实例方法是有前提的实际上是拿第一个参数作为方法的调用者。
```java
public class MethodRefDemo {
public static void main(String[] args) {
Function<String, Integer> f1 = (str) -> str.length();
// 等同于, 将参数作为调用者去调用方法, 然后接收对应数据类型的返回值
Function<String, Integer> f2 = String::length;
BiFunction<String, Integer, String> f3 = String::substring;
// 等同于, 将第一个参数作为调用者, 第二个参数作为参数, 然后接收对应数据类型的返回值
BiFunction<String, Integer, String> f4 = (str, index) -> str.substring(index);
}
}
```
#### 类名::new
由于构造器的名称与类名完全一样。所以构造器引用使用`类名称::new`的格式表示
```java
public class MethodRefDemo {
public static void main(String[] args) {
// 使用无参构造器实例一个String类
Supplier<String> s1 = String::new;
// 等同于
Supplier<String> s2 = () -> new String();
s1.get();
// 把具体地调用体现在了接口上
// 使用一个参数的构造器实例一个String类
Function<String, String> s3 = String::new;
// 等同于
Function<String, String> s4 = (str) -> new String(str);
s3.apply("张三");
}
}
```
#### 数组::new
数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同
```java
public class MethodRefDemo {
public static void main(String[] args) {
Function<Integer, int[]> f = length -> new int[length];
// 等同于
Function<Integer, int[]> ff = int[]::new;
}
}
```
#### 小结
方法引用是对Lambda表达式符合特定情况下的一种缩写它使得我们的Lambda表达式更加的精简也可以理解为Lambda表达式的缩写形式, 不过要注意的是方法引用只能"引用"已经存在的方法!