03-lambda-basic
- Java8新引入的语法糖《Lambda表达式》(关于lambda是否属于语法糖存在很多争议,不要纠结于字面表述)
- Lambda表达式是一种用于取代匿名类,把函数行为表述为函数式编程风格的一种匿名函数
- 再重申一下:Lambda表达式是实现函数式接口的一个匿名类的对象
1. basic
1. 示例代码
- 需求:遍历List集合
1. Lambda表达式
List<String> strList = Arrays.asList("a", "b", "c");
strList.forEach(s -> {
System.out.println(s);
});
2. 匿名内部类
- 使用匿名内部类来实现上述lambda的功能
List<String> strList = Arrays.asList("a", "b", "c");
// 通过匿名内部类来代替lambda
strList.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
2. 示例代码分析
forEach()
是Iterable
接口的一个默认方法,需要一个Consumer
类型参数- 方法体中是一个for循环,对迭代器的每个Obj进行遍历,调用参数对象的
accept()
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
}
Consumer
的accept(T)
方法。Consumer
是一个函数式接口(只有一个抽象方法的接口)
@FunctionalInterface
public interface Consumer<T> {
/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);
// ...
}
s -> {System.out.println(s);}
相当于是实现了Consumer
接口的一个匿名(内部类)对象System.out.println(s);
相当于重写了accept()
的方法体
3. 反编译lambda代码
- jad系列的反编译工具不支持jdk1.8,使用CFR进行反编译
- cfr下载地址
- 语法:
java -jar cfr-0.145.jar LambdaTest.class --decodelambdas false
public class LambdaTest {
public static void main(String ... args) {
List<String> strList = Arrays.asList("a", "b", "c");
strList.forEach(
(Consumer<String>)LambdaMetafactory.metafactory(
null,
null,
null,
(Ljava/lang/Object;)V,
lambda$main$0(java.lang.String ),
(Ljava/lang/String;)V)()
);
}
private static /* synthetic */ void lambda$main$0(String s) {
System.out.println(s);
}
}
- 调用了
java.lang.invoke.LambdaMetafactory#metafactory()
,该方法的第5个参数implMethod
指定了方法实现 - 调用
lambda$main$0()
方法进行输出。跟踪metafactory()
(参数较多,可以跳过)
public static CallSite metafactory(
// 调用者(LambdaTest)可访问权限的上下文对象,JVM自动填充
MethodHandles.Lookup caller,
// 要执行的方法名,即Consumer.accept(),JVM自动填充
String invokedName,
// 调用点预期的签名(包含目标方法参数类型String和Lambda返回类型Consumer),JVM自动填充
MethodType invokedType,
// 函数式接口抽象方法的签名, (Object)void,泛型String被擦出,所以是Object
MethodType samMethodType,
// 直接方法句柄,真正被调用的方法,即lambda$main$0,签名为MethodHandle(String)void
MethodHandle implMethod,
// 实例化的方法签名,即调用时动态执行的方法签名,
// 可能与samMethodType相同,也可能包含了泛型的具体类型,比如(String)void
MethodType instantiatedMethodType) throws LambdaConversionException {
AbstractValidatingLambdaMetafactory mf;
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
return mf.buildCallSite();
}
- 其中
new InnerClassLambdaMetafactory
创建了一个Lambda相关的内部类
public InnerClassLambdaMetafactory(...)
throws LambdaConversionException {
// ...
lambdaClassName = targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();
cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// ...
}
lambdaClassName
——Lambda表达式对应的类名,而ClassWriter
对象cw
,暴露了Lambda表达式的底层实现机制:ASM技术(Assembly,Java字节码操作和分析框架,程序运行时动态生成和操作字节码文件)。在这个构造方法里,初始化了大量的ASM技术需要的成员变量,为后续生成字节码的相关操作完成了一系列的初始化- Lambda表达式底层是通过一个匿名内部类来实现的,这个类由ASM技术在程序运行时动态生成,它实现了函数式接口,并重写了对应的抽象方法
4. 验证猜想
metafactory()
方法中,跟踪方法结尾的返回语句mf.buildCallSite();
—— 创建调用点
/**
* 创建调用点。定义一个实现了函数式接口的类并生成它的类文件
* Build the CallSite. Generate a class file which implements the functional
* interface, define the class, if there are no parameters create an instance
* of the class which the CallSite will return, otherwise, generate handles
* which will call the class' constructor.
*
* 返回一个调用点,执行的时候,将会返回一个函数式接口(Consumer)的实例
* @return a CallSite, which, when invoked, will return an instance of the
* functional interface
*/
@Override
CallSite buildCallSite() throws LambdaConversionException {
final Class<?> innerClass = spinInnerClass();
// 省略部分代码...
try {
Object inst = ctrs[0].newInstance();
return new ConstantCallSite(MethodHandles.constant(samBase, inst));
}
// ...
}
- 方法的注释非常清晰的告诉我们,这个方法在运行期会返回一个函数式接口的实例,也就是
Consumer
接口的匿名对象 - 第一行
spinInnerClass()
,使用ASM技术生成了一个Class文件,然后使用sun.misc.Unsafe
将该类加载到JVM(创建并返回该类的Class对象)
private final ClassWriter cw; // ASM class writer
/**
* 生成一个实现函数式接口的类文件,定义并返回该类的Class实例
* Generate a class file which implements the functional
* interface, define and return the class.
*
* 返回一个实现函数式接口的Class实例
* @return a Class which implements the functional interface
*/
private Class<?> spinInnerClass() throws LambdaConversionException {
// ...
// ClassWriter通过visit方法动态构造类的字节码
cw.visit(, , lambdaClassName, null, , interfaces); // 生成接口字节码
// ...
for ( ; ; ) {
cw.visitField( , , , null, null); // 生成域的字节码
}
generateConstructor(); // 生成构造器字节码
// ...
cw.visitMethod( , , , null, null); // 生成普通方法字节码
// ...
cw.visitEnd(); // end
// Define the generated class in this VM.
final byte[] classBytes = cw.toByteArray();
// If requested, dump out to a file for debugging purposes
if (dumper != null) { // 转储对象
AccessController.doPrivileged(new PrivilegedAction<Void>() {
@Override
public Void run() {
dumper.dumpClass(lambdaClassName, classBytes);
return null;
}
}, null,
new FilePermission("<<ALL FILES>>", "read, write"),
// createDirectories may need it
new PropertyPermission("user.dir", "read"));
}
// 使用Unsafe对象定义并返回,该内部类字节码文件对象(Class)
return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);
}
if (dumper != null)
代码块提供了将该内部类转储到本地磁盘,以下代码,将Lambda表达式对应的内部类转储到指定目录:
System.setProperty("jdk.internal.lambda.dumpProxyClasses", "out/production/");
- 程序运行之后,Lambda表达式对应的内部类文件生成出来
com.boxuegu.intermediate.language.sugar.lambda.LambdaTest$$Lambda$1
:
import java.lang.invoke.LambdaForm;
import java.util.function.Consumer;
final class LambdaTest$$Lambda$1 implements Consumer { // 实现函数式接口
private LambdaTest$$Lambda$1() {
}
@LambdaForm.Hidden
public void accept(Object object) { // 重写抽象方法
LambdaTest.lambda$main$0((String)object);
}
}
- Lambda表达式底层是用内部类来实现的
- 该内部类实现了 (根据Lambda所属的代码指定) 函数式接口,并重写了该接口的抽象方法
- 该内部类是在程序运行时使用ASM技术动态生成的,所以编译期没有对应的
.class文件
,但是可以通过设置系统属性将该内部类文件转储出来
5. Lambda编译、运行过程
- Java7在 JSR(Java Specification Requests,Java 规范提案)292 中增加了对动态类型语言的支持,使得Java也可以像C语言那样将方法作为参数传递,其实现在
java.lang.invoke
包中。核心就是invokedynamic
指令,为后续函数式编程、响应式编程提供了前置支持 invokedynamic
指令对应的执行方法会关联到一个动态调用点对象(java.lang.invoke.CallSite
),一个调用点(call site)是一个方法句柄(method handle,调用点的目标)的持有者,这个调用点对象会指向一个具体的引导方法(bootstrap method,eg:metafactory()
),引导方法成功调用之后,调用点的目标将会与它持有的方法句柄的引用永久绑定,最终得到一个实现了函数式接口(eg:Consumer)的对象- Lambda在编译期进行脱糖(desugar),它的主体部分会被转换成一个脱糖方法(desugared method,即
lambda$main$0
),这是一个合成方法,如果Lambda没有用到外部变量,则是一个私有的静态方法,否则将是个私有的实例方法——synthetic,表示不在源码中显示,并在Lambda所属的方法(eg:main方法)中生成invokedynamic
指令 - 进入运行期,
invokedynamic
指令会调用引导方法metafactory()
初始化ASM生成内部类所需的各项属性,然后由spinInnerClass()
方法组装内部类并用Unsafe加载到JVM,通过构造方法实例化内部类的实例(Lambda的实现内部类的构造是私有的,需要手动设置可访问属性为true),最后绑定到方法句柄,完成调用点的创建 - 可以把调用点看成是函数式接口(eg:Consumer等)的匿名对象,当然,内部类是确实存在的——eg:
final class LambdaTest$$Lambda$1 implements Consumer
。值得注意的是,内部类的实现方法里并没有Lambda的任何操作,它不过是调用了脱糖后定义在调用点目标类(targetClass
,即LambdaTest
类)中的合成方法(即lambda$main$0
)而已,这样做使得内部类的代码量尽可能的减少,降低内存占用,对效率的提升更加稳定和可控
6. Lambda语法糖结论
- Lambda在编译期脱去糖衣语法,生成了一个“合成方法“,在运行期,
invokedynamic
指令通过引导方法创建调用点,过程中生成一个实现了函数式接口的内部类并返回它的对象,最终通过调用点所持有的方法句柄完成对合成方法的调用,实现具体的功能 - Lambda是一个语法糖,但远远不止是一个语法糖
- 语法糖(Syntactic Sugar),也称糖衣语法,对语言本身的功能并没有影响,但是更方便程序员使用
- 特点
- 是一个语法结构,对原生语法的封装
- 不被JVM支持
- 解语法糖发生在编译期
- 不一定能提高性能
- 目的
- 简化代码,提高编码效率
- 增加可读性
- 减少错误率
- 特点
2. 方法引用
- 使用Lambda,实际上传递进去的代码是一种解决方案:拿什么参数做什么操作
- 在Lambda中所指定的操作方案,已经有地方存在相同方案,没必要再写重复逻辑
1. 冗余的Lambda场景
@FunctionalInterface
public interface Printable {
void print(String str);
}
// 只管调用`Printable`接口的`print()`,而并不管`print()`的具体实现逻辑,将字符串打印到什么地方
private static void printString(Printable printable) {
printable.print("Hello, World!");
}
public static void main(String[] args) {
// 指定`Printable`的具体操作方案:拿到`String`(类型可推导,所以可省略)数据后,在控制台中输出它
printString(s ‐> System.out.println(s));
}
2. 问题分析
- 对字符串进行控制台打印输出的操作方案,已经有了现成的实现,
System.out
对象中的println(String)
。既然Lambda希望做的事情就是调用println(String)
,那何必手动调用呢?
3. 方法引用改进
双冒号 ::
printString(System.out::println);
4. 方法引用符
- 双冒号
::
为引用运算符,它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者
1. 语义分析
- Lambda写法:
s -> System.out.println(s);
- 方法引用写法:
System.out::println
- 第一种语义:拿到参数之后经Lambda之手,继而传递给
System.out.println
方法去处理 - 第二种等效写法的语义指:直接让
System.out
中的println
方法来取代Lambda。复用了已有方案,更加简洁
2. 推导、省略
- 无需指定参数类型,也无需指定的重载形式,都将被自动推导
@FunctionalInterface
public interface PrintableInteger {
void print(int str);
}
- 方法引用将会自动匹配到
println(int)
的重载形式
private static void printInteger(PrintableInteger data) {
data.print(1024);
}
public static void main(String[] args) {
printInteger(System.out::println);
}
5. 对象名
- 最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法:
public class MethodRefObject {
// String大写转化逻辑
public void printUpperCase(String str) {
System.out.println(str.toUpperCase());
}
}
@FunctionalInterface
public interface Printable {
void print(String str);
}
- 使用这个
printUpperCase()
成员方法来替代Printable
接口的Lambda时,通过对象名引用成员方法
private static void printString(Printable lambda) {
lambda.print("Hello");
}
public static void main(String[] args) {
MethodRefObject obj = new MethodRefObject();
printString(obj::printUpperCase);
}
6. 类名
- 在
java.lang.Math
类中已经存在了静态方法abs
@FunctionalInterface
public interface Calcable {
int calc(int num);
}
- 使用Lambda表达式:
private static void method(int num, Calcable lambda) {
System.out.println(lambda.calc(num));
}
public static void main(String[] args) {
method(‐10, n ‐> Math.abs(n));
}
- 使用方法引用
private static void method(int num, Calcable lambda) {
System.out.println(lambda.calc(num));
}
public static void main(String[] args) {
method(‐10, Math::abs);
}
7. super
- 如果存在继承关系,当Lambda中需要出现
super
调用时,也可以使用方法引用进行替代。首先是函数式接口:
@FunctionalInterface
public interface Greetable {
void greet();
}
public class Human {
public void sayHello() {
System.out.println("Hello!");
}
}
public class Man extends Human {
@Override
public void sayHello() {
System.out.println("hello, 我是Man!");
}
// 定义方法method,参数传递Greetable
public void method(Greetable g){
g.greet();
}
public void show(){
// 1. Lambda表达式
method(() ‐> {
new Human().sayHello();
});
// 2. 简化Lambda
method(() ‐> new Human().sayHello());
// 3. super关键字代替父类对象
method(() ‐> super.sayHello());
// 4. 方法引用
method(super::sayHello);
}
}
8. this
- this代表当前对象,如果需要引用的方法是当前类中的成员方法,
this::成员方法
@FunctionalInterface
public interface Richable {
void buy();
}
public class Husband {
private void buyHouse() {
System.out.println("买套房子");
}
private void marry(Richable lambda) {
lambda.buy();
}
public void beHappy() {
// lambda
marry(() ‐> this.buyHouse());
// 方法引用
marry(this::buyHouse);
}
}
9. 构造器
- 构造器引用使用:
类名称::new
格式
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
public interface PersonBuilder {
Person buildPerson(String name);
}
public static void printName(String name, PersonBuilder builder) {
System.out.println(builder.buildPerson(name).getName());
}
public static void main(String[] args) {
// lambda
printName("hello", name ‐> new Person(name));
// 方法引用
printName("hello", Person::new);
}
10. 数组构造器
- 数组也是
Object
的子类对象,所以同样具有构造器,只是语法不同
@FunctionalInterface
public interface ArrayBuilder {
int[] buildArray(int length);
}
private static int[] initArray(int length, ArrayBuilder builder) {
return builder.buildArray(length);
}
public static void main(String[] args) {
// lambda
int[] array = initArray(10, length ‐> new int[length]);
// 方法引用
int[] array = initArray(10, int[]::new);
}
3. Stream流
1. 引言
- I/O Stream。Java8中,得益于Lambda带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端
- 几乎所有的集合(eg:
Collection
接口、Map
接口等)都支持直接或间接的遍历操作
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
for (String name : list) {
System.out.println(name);
}
1. 循环遍历弊端
Java8的Lambda更加专注于做什么(What),而不是怎么做(How)
- for循环的语法就是“怎么做”
- for循环的循环体才是“做什么”
循环是遍历的唯一方式吗?
- 遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环
- 遍历是目的,循环只是一种方式
对集合中的元素进行筛选过滤:
- 将集合A根据条件一过滤为子集B
- 然后再根据条件二过滤为子集C
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
List<String> zhangList = new ArrayList<>();
for (String name : list) {
// 1. 首先筛选所有姓张的人
if (name.startsWith("张")) {
zhangList.add(name);
}
}
List<String> shortList = new ArrayList<>();
for (String name : zhangList) {
// 2. 然后筛选名字有三个字的人
if (name.length() == 3) {
shortList.add(name);
}
}
for (String name : shortList) {
// 3. 最后进行对结果进行打印输出
System.out.println(name);
}
2. Stream优雅写法
- 完美展示无关逻辑方式的语义:获取流 -> 过滤姓张 -> 过滤长度为3 -> 逐一打印
- 代码中并没有体现使用线性循环或是其他任何算法进行遍历
@Test
public void lambda() {
List<String> list = Arrays.asList("张无忌", "周芷若", "赵敏", "张强", "张三丰");
list.stream()
.filter(s -> s.startsWith("张"))
.filter(s -> s.length() == 3)
.forEach(System.out::println);
}
2. 流式思想概述
- 整体来看,流式思想类似于工厂车间的“生产流水线”
- 需要对多个元素进行操作(特别是多步操作)时,考虑到性能及便利性。首先拼好一个“模型”步骤方案,这种方案就是一种“函数模型”
filter(), map(), skip()
都是对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法count
执行时,整个模型才会按照指定策略执行。而这得益于Lambda的延迟执行特性
备注:“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)
Stream
(流)是一个来自数据源的元素队列- 元素是特定类型的对象,形成一个队列。Java中的
Stream
并不会存储元素,而是按需计算 - 数据源流的来源。可以是集合、数组等
- 元素是特定类型的对象,形成一个队列。Java中的
- 和以前的
Collection
操作不同,Stream
操作还有两个基础的特征- Pipelining:中间操作都会返回流对象本身。多个操作可以串联成一个管道,流式风格(fluent style)。可以对操作进行优化
- eg:延迟执行(laziness)、短路(short-circuiting)
- 内部迭代:
Iterator
、forEach
,显式的在集合外部进行迭代,这叫做外部迭代。Stream提供了内部迭代的方式,流可以直接调用遍历方法
- Pipelining:中间操作都会返回流对象本身。多个操作可以串联成一个管道,流式风格(fluent style)。可以对操作进行优化
- 当使用一个流时,通常包括三个基本步骤
- 获取一个数据源(source)
- 数据转换
- 执行操作获取想要的结果
- 每次转换原有
Stream
对象不改变,返回一个新的Stream
对象(可以多次转换),这就允许对其操作可以像链条一样排列,变成一个管道
3. 获取流
java.util.stream.Stream<T>
Java8新加入的最常用的流接口(并不是一个函数式接口)
1. Collection
java.util.Collection
接口中加入了default方法stream()
用来获取流
List<String> list = new ArrayList<>();
Stream<String> stream1 = list.stream();
Set<String> set = new HashSet<>();
Stream<String> stream2 = set.stream();
Vector<String> vector = new Vector<>();
Stream<String> stream3 = vector.stream();
2. Map
java.util.Map
接口不是Collection
的子接口,且其K-V
数据结构不符合流元素的单一特征,获取对应的流需要分key、value、entry
Map<String, String> map = new HashMap<>();
Stream<String> keyStream = map.keySet().stream();
Stream<String> valueStream = map.values().stream();
Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();
3. Array
- 如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以
Stream.of()
String[] array = { "张无忌", "张翠山", "张三丰", "张一元" };
// `of()`参数其实是一个可变参数,支持数组
Stream<String> stream = Stream.of(array);
4. 常用方法
- 延迟方法:返回值类型仍然是Stream,因此支持链式调用(除了终结方法外,其余方法均为延迟方法)
- 终结方法:返回值类型不再是Stream,因此不再支持链式调用。
count()
、forEach()
等
1. 逐一处理(forEach)
- 与for循环中的“for-each”昵称不同
Stream<String> stream = Stream.of("张无忌", "张三丰", "周芷若");
stream.forEach(name -> System.out.println(name));
- 接收一个Consumer函数接口,会将每一个流元素交给该函数进行处理
void forEach(Consumer<? super T> action);
1. Consumer接口
@FunctionalInterface
public interface Consumer<T> {
// 消费一个指定泛型的数据
void accept(T var1);
}
2. 过滤(filter)
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
// Lambda表达式指定筛选条件
Stream<String> result = original.filter(s -> s.startsWith("张"));
- 接收一个Predicate函数接口(Lambda、方法引用)作为筛选条件
Stream<T> filter(Predicate<? super T> predicate);
1. Predicate接口
@FunctionalInterface
public interface Predicate<T> {
// 指定的条件是否满足。true,留用元素;false,舍弃元素
boolean test(T var1);
}
3. 映射(map)
- 将流中的元素映射到另一个流中
Stream<String> original = Stream.of("10", "12", "18");
// String转换为Integer
Stream<Integer> result = original.map(str -> Integer.parseInt(str));
- Function函数式接口,可以将当前流中的T类型数据转换为另一种R类型的流
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
1. Function接口
@FunctionalInterface
public interface Function<T, R> {
// 一种T类型转换成为R类型,这种转换的动作,称为“映射”
R apply(T var1);
}
4. 统计个数(count)
- Collection当中的
size()
一样,Stream提供count()
数一数其中的元素个数 - 返回一个long值代表元素个数(集合是int值)
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.filter(s -> s.startsWith("张"));
System.out.println(result.count());
5. 取用前几个(limit)
// 如果集合当前长度大于参数则进行截取;否则不进行操作
Stream<T> limit(long maxSize);
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.limit(2);
System.out.println(result.count()); // 2
6. 跳过前几个(skip)
// 流的当前长度大于n,则跳过前n个;否则得到一个长度为0的空流
Stream<T> skip(long n);
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.skip(2);
System.out.println(result.count()); // 1
7. 组合(concat)
- 两个流合并成为一个流
- 是一个静态方法,与
java.lang.String
当中的concat()
不同
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
Stream<String> streamA = Stream.of("张无忌");
Stream<String> streamB = Stream.of("张翠山");
Stream<String> result = Stream.concat(streamA, streamB);