lambda表达式、函数式接口、方法引用、高阶函数:Core Java 6.3

晏志明
2023-12-01

方法参数:当要传递的参数为一段代码块时,该如何传递

java 8 之前,参数为代码块的应用场景及实现实例

当用Arrays::sort( T[] a, Comparator<? supter T> c)方法对数组中的对象进行排序时,需要传递一个比较器对象(Comparator接口的实例c)为参数。

最终,是将比较器中实现的接口方法Comparator::compare(T first, T second)中的代码块传递给sort方法,在sort方法中,会调用c.compare(a,b)来对数组中的所有元素两两比较,以此确定元素的顺序。

在jdk1.8版本之前,因为Java的面向对象特性,要传递代码块作为参数,只能传递一个其中定义了要传递的代码块的类的对象。具体步骤是:先创建一个类,类中定义一个包括了要传递的代码块的方法,然后创建一个此类的对象,将此对象作为参数传递。

示例:如果我们想对字符串排序,但是不想按照字符串的字母顺序对字符串进行排序,而是想依据是字符串的长度对字符串排序,长度越小越靠前、长度越长越靠后,如下代码实现了这种排序。

import java.util.*;
public class SortTest{
	public static void main(String [] args){
		String [] strings = {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus"};
		
		Arrays.sort(strings, new LengthCompare());
		
		System.out.println(strings);  // 按字符串长度顺序排序的序列,串越短越靠前,串越长越靠后
	}
}

class LengthCompare implements Comparator<String>{

	public int compare(String first,String second){
		return first.length()-second.length();
	}
}

为了传递一个代码块,需要创建一个类,再创建一个对象实例,这种方式让人感觉有点冗余,不够方便、简洁(不过Java好像就是如此??)。
java se 8提供了lambda表达式,可以使得这种场景下的代码更简洁。

lamada表达式

lambda即希腊字符λ的英文形式。

λ表达式的语法规范

个人理解:由于本身lamda表达式是用来替代方法参数的,因此其语法像是匿名方法(自创的名词,不是JAVA术语),就像是将方法的定义省去了方法访问权限限定符、返回值类型、方法名,只保留了方法参数和方法体。

λ表达式的常规语法格式

(参数类型1 参数名, 参数类型2 参数名…) -> { 代码块;}

对于引子中的代码,可以使用Lambda表达式使之更简洁:

import java.util.*;
public class  SortTest{
	String []  strings = {...};
	
	Arrays.sort(strings, 
					(String first,String second)-> {
						return first.length()-second.length();
					}
				);
	System.out.println(strings);  // 按字符串长度顺序排序的序列,串越短越靠前,串越长越靠后
				
}

省略参数类型

当编译器可以根据上下文确定参数类型时,λ表示式中的参数可以省略参数类型的声明。

如上例中,Comparator接口只有一个抽象方法,方法中自然对参数类型做了确定地声明。而根据Arrays.sort(T[] a,Comparator<? super T> c)方法的声明,就可以确定接λ表示的参数类型为? super T。T虽然是泛型,但是在实际运行时,却可以根据实际传入的第一个参数的类型来确定。

Arrays.sort(strings, 
				(first, second) -> { return first.length()- second.length(); } 
			);

省略参数外的()

当只有一个参数,且可以省略参数类型时,可以省略参数外的(),但必须同时省略参数的类型

省略{}

当lambda体只有一句代码时,可以省略{},但必须同时省略return和语句结尾的分号;。因为不在{}中时,不是代码块,只能算作一个表达式。

而当lamada体用花括号{}括起来时,{}中必须是语句,每句要有;,返回语句要加return 。

Arrays.sort(ss, 
	(first , second) -> { return first.length() - second.length(); }
)
// 可简写如下
Arrays.sort(ss, 
	(first , second) -> first.length() - second.length()
)

当没有参数时,必须有()

即使λ表达式没有参数,仍然要提供(),就像无参方法一样。

	()->{
			for(int i = 100; i >= 0; i--)
				System.out.println(i);
		}

λ表达式的作用原理及意义:函数式接口

函数式接口

有且只有一个抽象方法的接口,称为函数式接口。

当需要函数式接口的对象时,就可以提供一个λ表达式。例如:

Arrays.sort(strings, 
			(first, second)-> { return first.length()-second.length();

在底层,Arrays.sort 方法会接收实现了 Comparator 的某个类的对象(底层是如何实现的??-是否运行时JDK动态地生成了接口的一个实现类A,直接将lamada表达式的体作为类A对接口中抽象方法的实现?-待研),在这个对象上调用compare方法时,会执行lambda表达式内的体。

最好把lambda表达式看作一个函数,而不是一个对象。即运行时,是把lambda表达式传递到函数式接口。(但是在语法上,却可以用一个变量引用它,此变量是对方法的引用,而不是对象的引用)
语法示例:

Comparator  c = 
	(first, second)->{ return  first.length() - second.length();}

在java中,lambda表达式的意义也只在于转换为函数式接口。

可以对函数式接口加注解:@FunctionalInterface,但这不是必须的。即便不加,只要接口有且只有一个抽象方法,JDK就认为它是一个函数式接口。

方法引用

语法规范

类名::方法名

应用场景:

当函数式接口的实现类中的方法,仅仅是调用了其它类的一个方法时,就可以直接用方法引用来取代lambda表达式。例如:

Timer timer = new Timer(1000, event -> System.out.println(event));

可以看到,lambda体内部其实是直接调用了其它方法,并没有任何自己的算法。这时可以写成如下形式:

Timer timer = new Timer(1000,System.out::println);

表达式:System.out::println就是一个方法引用,它等价于lambda表达式 event -> System.out.println(event)

又如:要实现不分大小写的字符串排序,String本身提供了一个不分大小写的比较方法compareToIgnoreCase,我们可以如下实现:

String [] strings = {""};
Arrays.sort(strings, 
	(first,second)->{return first.compareToIgnoreCase(second);}
);

此时lambda表达式的体中,只是调用了JDK中String类的一个成员方法compareToIgnoreCase方法,我们可以作如下简写:

Arrays.sort(strings, String::compareToIgnoreCase)

compareToIgnoreCase是String类的一个实例方法,jvm会将第一个参数作为方法的调用者,其余的参数作为被调用的方法的参数

同lambda表达式一样,方法引用也只能转化为函数式接口的实例。

可以在方法引用中用this参数

如:this::equals等同于x -> this.equals(x)

构造器引用

和方法引用类似,不过方法名为new,如:
Person::new相当于lambda表达式(String name) -> { return new Person(name); }
int[]::new相当于lambda表达式x -> new int[x]

当lambda方法需要访问外围方法或者类中的变量

例如:

public static void repeatMessage(String text ,int delay){ 
	ActionListener listener = event -> {
		System.out.println(text);
		Toolkit.getDefaultTookit.beep();
	}; //函数式接口
	new Timer(delay, listener).start();
}

如果我们这样调用上面的方法:

String text = "hell";
repeatMessage(text,1000); // 每1000毫秒打印hello,并响铃

那么会如下问题:lambda表达式的代码可能会在repeatMessage方法执行完毕很久之后才会运行,而那时repeatMessage方法早已执行完,线程栈中并不会有这个方法调用的栈帧,text等方法的变量的作用域也早已随着repeatMessage方法的执行完毕而消失。事实上,lambda表达式的代码是在另一个线程中执行的。

对于此问题,这里引入一个概念:lambda表达式中的自由变量。

自由变量

lambda表达式有3个部分:

  1. 一个代码块
  2. 参数
  3. 自由变量的值

关于lambda表达式中自由变量的语法规范

  1. lambda表达式必须存储自由变量的值,在上例中就是"hello",我们说它被lambda变量捕获(captured)。

关于代码块及自由变量值有一个术语叫做闭包(closure),lambda表达式就是一个闭包。

  1. lambda表达式捕获的自由变量必须是最终变量(effectively final),也就是一旦初始化后就不能改变的。

在代码实现上,要么是类似String,LocalDate这种类,它们的值本身就是final的,要么就直接把变量声明为final。

原因之一是如果lambda表达式中的代码块是并发的,那么对自由变量的更新就是不安全的;另外一个原因是如果不是final的,那么lambda表达式外部也能够对此变量进行更新。

下例就是一个不合法示范:

public static repeat(String text,int count){
	for(int i = 0; i < count; i++){
		ActionListener listner = event ->
			{
				System.out.println(i + ":" + text);
			};
		new Timer(1000, listener).start();
	}
}

高阶函数

如果一个函数,接受一个函数作为参数,或者返回一个函数作为返回值,那么这个函数就叫高阶函数。

这里的函数,也可以扩展为函数式接口。如果一个方法接收一个函数式接口的实例作为参数,或者返回一个函数式接口的实例,也是高阶函数。

Arrays::sort(Comparator c)就是一个高阶函数。

通用的函数式接口 ( java.util.function.* )

java.util.function包中定义了一系列的类似 Comparator / Runnable 的函数式接口。

函数式接口的通用性

函数式接口本身,无非是定义了一个接口方法,包括方法的 参数个数、参数类型、返回值类型。

而在泛型机制的支持下,参数类型、返回值类型,也不是函数式接口的限定因素了。

至此,函数式接口的接口方法的定义的限定,只在于参数个数、有无返回值上。

因此,函数式接口的通用性大大提高。

例如:Runnable接口本身在JDK中承载的意义是多线程的线程操作。但是一旦我们按照函数式接口的通用性进行拆解,就会发现,Runnable接口的接口方法就是定义了一个无参数、无返回值的行为。
如果我们有个场景,比如每10分钟就响一次闹铃,那么就可以将响铃这个行为作为Runnable接口的实现类的行为。

再如:Comparator接口本身在JDK中承载的意义是比较两个对象。但是按照函数式接口的通用性进行拆解,这个接口就是定义了一个有 两个相同类型的参数、返回int类型值 的接口方法。
如果我们有个应用场景是对两个int型的数做减法,后再进行一系列其它操作,最后再返回运算结果,也可以通过实现Comparator接口来定义这个行为。但是要确保正常情况下的运算结果一定是int类型。

也就是说,对于以下所有方法,由于他们的参数个数相同、且都是有返回值的,因此一个函数式接口就可以代替以下所有方法的定义:

public class Test{
	int add(int a,int b){return a+b;}
	int sub(int b,int b){return a-b;}
	long multi(long a,long b ){return a*b;}
	
	public static void main(String[] args){
		int a=2,b=3;
		int c = new Test().add(a,b);
		int d = new Test().sub(a,b);
		long e = new Test().multi(a,b);
	}
}	

用一个高阶函数,替代以上三个方法的定义:

public class Test{
	T compute(T a,T b,BiOperator<T> operator){
		return operator.apply(a,b);	
	}
	public static void main(String[] args){
		int a=2,b=3;
		int c = new Test().compute(a,b,(a,b)->{return a+b});
		int d = new Test().compute(a,b,(a,b)->{return a-b});
		long e = new Test().compute(a,b,(a,b)->{return a*b});
	}
}	

在上边的示例中,将三个方法的方法体作为高阶函数compute的一个参数,并将三个方法的参数,作为高阶函数compute的其它参数传递给它,而在compute中会将方法体应用到其它参数上去,变相实现了三个方法的执行(实际就是原来的方法的方法体作用于原来的方法的参数)。
只不过方法不用显式地声明和定义了,而是直接作为高阶函数的参数,在调用高阶函数时再定义出来。
又如:

public  class  Example{
	boolean  startsWith(String s,String start){return s.startsWith(start);}
	boolean  lessThan(int a,int b){return a-b<0;}
	boolean  greaterThan(double a,double b){return a-b>0;}
	
	// 以上方法的定义可以由这一个高阶函数替代
	boolean compute(T a,T b,BiPredicate<T,T> predicate){
		return predicate.test(a,b);
	}
	//实际的使用示例:
	public static void main(String [] args){
		boolean r1 = new Example.compute("fangfang","f",(a,b)->{return a.startsWith(b);});	
		boolean r1 = new Example.compute(3,4,(a,b)->{return a-b<0;});
		boolean r1 = new Example.compute(12.3,24.6,(a,b)->{return a-b>0;});	
	}
}

通用函数式接口

以下为不超过一个参数的通用函数式接口:

接口名方法定义描述释义
Runnablevoid run()无参数、无返回值
Supplier<T>T get()无参数、有返回值。Supplier,供给方,不需要参数,只供应出返回值
Consumer<T>void accept(T t)一个参数、无返回值。Consumer,消费者,只进不出,只接收参数,不返回
Function<T,R>R apply(T t)一个参数、返回值的类型可以与参数类型不同 。Function,典型的方法,参数+返回值
Predicate<T>boolean test(T t)一个参数, 返回boolean值。Predicate,断言,是或者不是,返回boolean值
UnaryOperator<T>T apply(T t)Function接口的子接口,限定参数和返回值同类型UnaryOperator,一元运算符,类似于运算符++或者–的函数,即只需要一个参数,且返回值类型与参数相同,因此取名为Unary(一元)Operator(运算符)

以下为两个泛型参数的通用函数式接口,均以Bi开头,即Binary(二元)函数:

接口名方法定义描述释义
BiConsumer<T,U>void accept(T t, U u)二个参数、无返回值。BiConsumer,消费者,二元函数,只进不出,只接收参数,不返回
BiFunction<T,U,R>R apply(T t, U u)二个参数、返回值的类型可以与参数类型不同 。BiFunction,二元方法,二个参数+返回值
BiPredicate<T,U>boolean test(T t, U u)二个参数,返回boolean值。BiPredicate,断言二元函数,是或者不是,返回boolean值
BinaryOperator<T>T apply(T t, T u)BiFunciton的子接口,限定了二个参数的类型和返回值的类型全都相同BianryOperator,二元运算符,类似于运算符+或者-的函数,需要二个操作数,且返回值与参数的类型相同,因此取名为Binary(二元)Operator(运算符)

其它指定了确定类型的参数或者返回值的函数式接口:

接口名方法定义描述释义
BooleanSupplierboolean getAsBoolean()指定返回值是boolean类型的供给者BooleanSupplier(boolean供给者),即供出一个boolean类型的返回值,不需要原材料即不需要参数
IntSupplier
LongSupplier
DoubleSupplier
指定返回值类型为 Int / Long / Double的供给者
IntConsumer
LongConsumer
DoubleConsumer
指定参数类型为 Int / Long / Double的消费者
ObjIntConsumer<T>
ObjLongConsumer<T>
ObjDoubleConsumer<T>
两个参数的消费者,第一个参数泛型,指定第二个参数类型为 Int / Long / Double的消费者
IntPredicate
LongPredicate
DoublePredicate
指定参数类型为 Int / Long / Double的断言
ToIntFunction<T>
ToLongFunction<T>
ToDoubleFunction<T>
指定返回值类型为 Int / Long / Double的一元方法
IntToLongFunction
IntToDoubleFunction
LongToIntFunction
LongToDoubleFunction
DoubleToIntFunciton
DoubleToLongFunction
指定参数类型为 Int,返回值类型为 Long / Double的一元方法
ToIntBiFunction<T, U>
ToLongBiFunction<T, U>
ToDoubleBiFunction<T, U>
指定返回值类型为 Int / Long / Double的二元方法
IntUnaryOperator
LongUnaryOperator
DoubleUnaryOperator
指定了参数类型的一元函数,参数类和返回值类型同为 Int / Long / Double的一元函数
IntBinaryOperator
LongBinaryOperator
DoubleBinaryOperator
指定了参数类型的二元函数,参数类和返回值类型同为 Int / Long / Double的一元函数
通用函数式接口中的默认方法和static 方法
接口名默认方法说明
Consumer<T>Consumer<T> andThen(Consumer<? superT> c)可以充分利用andThen,将一个Consumer中的语句,写成多个andThen的方法调用
BiConsumer<T, U>BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> c)
BiFunction<T, U>BiFunction<T, U> andThen(BiFunction<T, U> f)
BiPredicate<T, U>BiPredicate<T, U> and(BiFunction<T, U> other)
BiPredicate<T, U>BiPredicate<T, U> or(BiFunction<T, U> other)

default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }

Predicate

此接口的抽象方法返回一个boolean值。声明如下:

@FunctionalInterface
public interface Predicate<T>{
	boolean test(T t);
	...
}

有个方法ArrayList<E> :: removeIf( Predicate<? super E> filter ),实现的业务是if( filter.test(element)) 则最终移会除掉element,应用的示例如下:

import java.util.*;
public class Test
{
	public static void main(String [] args){
	
		String [] ss = {"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus"};

		ArrayList<String> list= new ArrayList<String>(Arrays.asList(ss));

		list.removeIf(
			(String e)->{ return e.length()>5; }  // 移除长度大于5的字符串
		);	
		// 可简写为:list.removeIf(e -> e.length()>5)
	}
}

处理lambda表达式

使用lambda表达式的重点是延迟执行。毕竟,如果想要立即执行代码,完全可以直接执行,而无需把它包装到一个lambda表达式中。之所以希望以后再执行,这有很多原因(有很多需求场景下必须延迟执行),如:

  • 在一个单独的线程中运行代码
  • 多次运行代码
  • 在算法的适当位置运行代码
  • 发生某种情况时执行代码
  • 只在必要时才运行代码

以上这段文字出自Core Java 6.3。个人只能理解线程的延迟执行的,对于其它4种都不明白为什么叫延迟执行?照其它几个来说,对任何方法的调用都是延迟执行。o(︶︿︶)o,如果有实践经验的大神看到,还请评论里解惑一下:->

个人尝试从其它角度分析什么场景下要使用lambda表达式:既然lambda表达式或者方法引用仅能转化为函数式接口发挥作用,那么使用函数式接口的原因就是使用lambda表达式的原因。试着举例分析一下:在Arrays.sort( T [], Comparator<? super T> c)方法中,调用c.compare(T x,T y))时调用的是每种类自己的compare方法,因为每种类的比较都有不同的逻辑,因此无法在sort方法中直接写比较算法,只能调用每种类自己的比较方法,那么就只能用统一的接口来规范方法参数、名称、返回值。这跟“延迟执行”的概念好像并没有什么关系。)

在Core Java 6.7中,所谓处理lambda表达式,看起来就是当你根据需求写出来了一段用到了lambda表达式的代码时,你应该选择什么样的函数式接口去接收它。

示例:
假设你想重复一个动作n次,将这个动作和重复的次数传递到一个repeat方法:

repeat(10, () -> System.out.println("hello world"));

要接受这个lambda表达式,需要选择(偶尔可能需要你自己编写提供)一个函数式接口。这个例子中,我们可以选择Runnable接口,因为Runnable接口的抽象方法没有参数、没有返回值,恰好符合我们的lambda表达式。因此,我们可以根据这个需求的代码而定义以下方法:

public static viod repeat(int n, Runable action){
	for (int i = 0; i < n; i++){
		action.run();
	}
}

另一个例子:在上例的需求之上,我们希望告诉这个动作它现在是在第几次的迭代中执行的,因此需要传递一个参数x。据此,我们需要选择这样一个函数式接口,它的抽象方法有一个int类型的参数,没有返回值。我们可以选择IntConsumer接口:

// JDK 的IntConsumer如下
@FunctionalInterface
public interface IntConsumer{
	void accept(int value);
	...
}

因此我们的lambda表达式应该这样写的:

repeat(int n ,i -> System.out.println("iterations "+i))

我们的方法应该这样定义:

public static void repeat(int n ,IntConsumer c){
	for (int i = 0; i < n; i++){
		c.accept(i);
	}
}

当需要传递一段代码块时(比如以上5中情况下),最好使用java.util.function包下的函数式接口,比如当需要对满足特殊条件的文件进行处理时,可以用FileFiler来对特殊条件进行定义,但是使用函数式接口Predicate也完全可以处理这种情况,这时我们应该优先选择后者。

另外,大多数函数式接口都提供了非抽象方法来生成或者合并函数,例如Predicate.isEquals(a)等同于a::equals,不过前者中a为null也能正常工作。已经提供了默认方法and、or和negate来合并谓谓词,例如:Predicate.isEquals(a).or(Prediate.isEquals(b))
就等同于
x -> a.equals(x)|| b.equals(x)

有个疑问,Lambda是不是违背了java语言的初衷?

附:在实际的项目工作中,lambda到底受不受欢迎?

lambda表达式本身是个语法,如果讨论性能,指的是用lambda充当匿名内部类、方法引用等场合。说这种场合效率低,我认为没有根据。可能别人想说的是在处理集合数据的时候,stream操作比结构化代码效率低。这些性能差异多数情况下可以忽略。
lambda的特点还在于开发成本高,并且异常难以排查。它的异常堆栈比匿名内部类还要难懂。如果你把stream的操作都写在同一行,则问题更甚。
另外,lambda目前还不是Java程序员必备技能,你留在项目里的代码可能会造成后续维护上的困难。

若鱼1919
Bbs7 版主
性能不用担心,大不了新版本的java继续优化
可读性和项目组成员的接受程度这个更重要

作者:hitsmaxft
链接:https://www.zhihu.com/question/37872003/answer/1062822405
来源:知乎

lambda 可以非常友好地替换掉冗余大量老的 SAM(single abstract method) 匿名类,但是 lambda 始终需要一个完备的 ide 支持, 否则写的时候爽, 后期维护就想杀人了.

// 写的默认的lambda 代码, 写起来方便
future.then((r)-> { return r.json() } ;

// 经过 intellij 自动展开补充了类型信息的代码,类似 scala 
future.then( ( r: HttpResponse) :CompletionStage<JSON>  -> { return r.json() } : Function<HttpResponse, CompletionStage<JSON> )
//经过自动补全后,很容易明白这是对http信息进行json格式化,而以上的默认代码,看的让人一头雾水。

///明显后者阅读体验会提高很多.类型信息对书写者是噪音, 对于阅读的人, 那是速效救心丸.然而还是要正视, java 的lambda 还是挺残废的, 毕竟只是个 sam 的编译魔法. 简单用用还行, 等到需要深度对lambda本身进行处理的时候, 比如反射/调试(比如用 arthas 之类的动态调试), 到时候就觉得这垃圾玩意真是个祸害.所以回过头来看, java 的lambda是什么 ? 就是一个不用写文档(类型信息)的匿名类.

作者:郑晔
链接:https://www.zhihu.com/question/21563900/answer/18631625
来源:知乎

函数式编程是技术的发展方向,而Lambda是函数式编程最基础的内容,所以,Java 8中加入Lambda表达式本身是符合技术发展方向的。
通过引入Lambda,最直观的一个改进是,不用再写大量的匿名内部类。事实上,还有更多由于函数式编程本身特性带来的提升。比如:代码的可读性会更好、高阶函数引入了函数组合的概念。
此外,因为Lambda的引入,集合操作也得到了极大的改善。比如,引入stream API,把map、reduce、filter这样的基本函数式编程的概念与Java集合结合起来。在大多数情况下,处理集合时,Java程序员可以告别for、while、if这些语句。
随之而来的是,map、reduce、filter等操作都可以并行化,在一些条件下,可以提升性能。
不过,对大多数Java程序员来说,他们最熟悉的内容是面向对象,函数式编程是个陌生的概念,是一种“全新”的思维模式。对于喜欢墨守陈规的大多数而言,这无疑会增加Java的入门成本,以及向新版本迁移的成本。
还有一件事,Lambda本身是借助invokedynamic实现的,这是这个Java 7加入的新指令第一次在Java语言层面上得到应用。因为它的存在,我们在某种程度上可以绕过Java的类型系统,很难说这是好是坏。

https://www.v2ex.com/t/442573

一般编写业务代码没必要用 lambda,做科学计算方面的用函数式编程就很多了。

不少Java程序员都觉得lambda很鸡肋,它到底有何用呢?

 类似资料: