当用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表达式,可以使得这种场景下的代码更简洁。
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]
例如:
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个部分:
关于lambda表达式中自由变量的语法规范
关于代码块及自由变量值有一个术语叫做闭包(closure),lambda表达式就是一个闭包。
在代码实现上,要么是类似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包中定义了一系列的类似 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;});
}
}
以下为不超过一个参数的通用函数式接口:
接口名 | 方法定义 | 描述 | 释义 |
---|---|---|---|
Runnable | void 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(运算符) |
其它指定了确定类型的参数或者返回值的函数式接口:
接口名 | 方法定义 | 描述 | 释义 |
---|---|---|---|
BooleanSupplier | boolean 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的一元函数 |
接口名 | 默认方法 | 说明 |
---|---|---|
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表达式中。之所以希望以后再执行,这有很多原因(有很多需求场景下必须延迟执行),如:
(以上这段文字出自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的类型系统,很难说这是好是坏。
一般编写业务代码没必要用 lambda,做科学计算方面的用函数式编程就很多了。