java 8 lambda如何使用_Java 8 (2) 使用Lambda表达式

什么是Lambda?

可以把Lambda表达式理解为 简洁的表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

使用Lambda可以让你更积极的使用行为参数化,而不用像匿名类那样写很多模板代码。

Lambda表达式由三部分组成:

(Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

参数列表:这里采用了Comparator中Compare方法的两个参数,两个 Apple。

箭头:-> 把参数和函数主体分开。

函数主体:比较两个Apple的重量,表达式就是Lambda的返回值了。

Java 8中有效的Lambda表达式

//具有一个String类型的参数 返回一个int Lambda没有return 语句,因为已经隐含了

(String s) ->s.length()//Apple类型的参数 返回一个boolean

(Apple a) -> a.getWeight() > 150

//两个int参数 没有返回值

(int x, int y) ->{

System.out.println(x+y);

}//空参数 返回int 42

() -> 42

Java语言设计者选择这样的语法,是因为在C#和Sala等语言中类似的功能广受欢迎。Lambda的基本语法是:

(parameters) ->expression//或者使用花括号

(parameters) -> { statements; }

注意: 在没有花括号时,是不需要return的。在有花括号写多行时,是需要有return的。

在哪里以及如何使用Lambda

你可以在函数式接口上使用Lambda表达式,昨天的例子中filter方法中的参数 Predicate 就是一个函数式接口。

List greenApple = filter(apples,(Apple apple) -> "green".equals(apple.getColor()));

函数式接口

函数式接口就是之定义一个抽象方法的接口。如昨天定义的Predicate,它就是一个函数式接口,因为它仅仅定义了一个抽象方法:

public interface Predicate{booleantest(T t);

}

昨天还使用了两个Java API中的函数式接口,就是Comparator和Runnable

@FunctionalInterfacepublic interface Comparator{intcompare(T o1, T o2);

}

@FunctionalInterfacepublic interfaceRunnable {public abstract voidrun();

}

@FunctionalInterface注解:标记该接口会涉及成一个函数式接口,如果你用这个注解标记了一个接口,它只能定义一个抽象方法,如果多个编译器会报错。这个注解不是必须的,只是为了更好的阅读,和@Override注解一样(表示该方法是一个重写方法)。

Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体来说,是函数式接口一个具体实现的实例)。使用匿名内部类也可以完成同样的事情,只不过比较笨拙。

函数描述符

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫做函数描述符。例如:Runnable接口可以看做一个什么也不接受什么也不返回的函数签名,因为它只有一个void run的抽象方法。 Lambda中 () -> void 代表了参数列表为空,并且什么也不返回,这正是Runnable接口所代表的。

由此可知:Lambda表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法。当然这个Lambda表达式的签名要和函数式接口的抽象方法一样。

public static voidprocess(Runnable r){

r.run();

}

直接把一个Lambda表达式传递给process方法

process(() -> System.out.println("hehe"));

为什么只有在需要函数式接口的时候才可以传递Lambda呢?

语言设计者也考虑过其他方法,例如给Java添加函数类型,但是他们选择了现在这种方式,因为这种方式自然且能避免语言变得更复杂。

环绕执行模式

通过一个例子,在实践中利用Lambda和行为参数化来让代码更为简洁更为灵活。在资源处理(例如处理文件或数据库)时一个常见的模式就是打开一个资源,做一些处理,然后关闭资源。这个设置和清理阶段总是很类似,并且会围绕着执行处理的那些重要代码。这就是所谓的环绕执行(excute around)模式。例如:从一个文件中读取一行所需的末班代码(使用了Java 7中的带资源的try语句,它已经简化了代码,因此不需要显示的关闭资源了类似c#中的using)。

public static String processFile() throwsIOException {try(BufferedReader br = new BufferedReader(new FileReader("/Users/baidawei/Desktop/test.txt"))) {returnbr.readLine();

}

}

第1步:记得行为参数化

现在这段代码是有局限性的,你只能读取文件的第一行,如果你想返回两行等操作时,理想的情况下是要重用执行设置和清理的代码,并告诉processFile方法对文件执行不同的操作。你需要一种方法把行为传递给processFile,以便它可以利用BufferedReader执行不同的行为。

你需要一个接受BufferedReader并返回String的Lambda,例如打印两行如下写法:

System.out.println(processFile((BufferedReader br) -> br.readLine() + br.readLine()));

第2步:使用函数式接口来传递行为

因为Lambda仅可用于上下文是函数式接口的情况,所以你需要创建一个能匹配BufferedReader->String,还可以抛出IOException异常的接口,首先定义这个接口就命名为BufferedReaderProcessor吧

@FunctionalInterfacepublic interfaceBufferedReaderProcessor {

String process(BufferedReader b)throwsIOException;

}

第3步:执行一个行为

任何BufferedReader->String形式的Lambda都可以作为参数来传递,因为他们符合BufferedReaderProcessor接口的process方法的签名。现在只需要一种方法在processFile主体内执行Lambda所代表的代码。

public static String processFile(BufferedReaderProcessor p) throwsIOException {try(BufferedReader br = new BufferedReader(new FileReader("/Users/baidawei/Desktop/test.txt"))){//处理BufferedReader对象

returnp.process(br);

}

}

第4步:传递Lambda

Lambda表达式允许直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。现在就可以通过传递不同的Lambda重用processFile方法了

处理一行:

System.out.println(processFile((BufferedReader br) -> br.readLine()));

处理两行:

System.out.println(processFile((BufferedReader br) -> br.readLine() + br.readLine()));

我们已经展示了如何利用函数式接口来传递Lambda,但你还是得定义你自己的接口,在下面将展示在Java 8 中加入的新接口,你可以重用它来传递多个不同的Lambda。

使用函数式接口

前面已经说过,函数式接口只定义一个抽象方法。函数式接口很有用,因为抽象方法的签名可以描述Lambda表达式的签名。函数式接口的抽象方法的签名称为函数描述符。所以为了应用不同的Lambda表达式,Java 8库设计师帮你在java.util.function包中引入了几个新的函数式接口。

Predicate

java.util.function.Predicate接口定义了一个test的抽象方法,它接受泛型T对象,并返回一个boolean。这恰恰和你之前创建的一样,现在就可以直接使用了。在你需要表示一个设计类型T的布尔表达式时,就可以使用这个接口。该接口定义如下:

packagejava.util.function;

@FunctionalInterfacepublic interface Predicate{booleantest(T t);

}

你可以定义一个接受String对象的Lambda表达式。

//定义过滤方法 接受一个Predicate

public static List filter(List list, Predicatep) {

List result = new ArrayList<>();for(T e : list){if(p.test(e)){

result.add(e);

}

}returnresult;

}//去掉空字符串

public static voidmain(String[] args){

Predicate nonEmptyStringPredicate = (String s) -> !s.isEmpty();

List nonEmpty = filter(Arrays.asList("sf","","sdf",""),nonEmptyStringPredicate);

System.out.println(nonEmpty);

}

Consumer

java.util.function.Consumer定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回值。你需要访问类型T的对象,执行某型操作,就可以使用这个接口。该接口定义如下:

packagejava.util.function;

@FunctionalInterfacepublic interface Consumer{voidaccept(T t);

}

使用它来创建一个forEach方法,接受一个Integers列表,并对其每个元素执行操作。

//创建forEach方法 没有返回值

public static void forEach(List list, Consumerc){for(T i : list){

c.accept(i);

}

}public static voidmain(String[] args) {

forEach(Arrays.asList(1,2,3,4,5),(Integer i) ->System.out.println(i));

}

Lambda是Consumer中accept方法的实现

Function

java.util.function.Function接口定义了一个叫做apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口。该接口的定义如下:

packagejava.util.function;

@FunctionalInterfacepublic interface Function{

R apply(T t);

}

比如将List字符串映射为一个 字符串长度的Integer List

public static List map(List list, Functionf){

List result= new ArrayList<>();for(T s: list){

result.add(f.apply(s));

}returnresult;

}public static voidmain(String[] args) {

List l = map(Arrays.asList("lambdas","in","action"),(String s) ->s.length());

System.out.println(l);

}

原始类型特化

在Java中要么是引用类型(Byte、Integer、Object、List),要么是原始类型(int、double、byte、char)。但是泛型(比如Consumer中的T)只能绑定到引用类型。这是由泛型内部的实现方式造成的。因此,在Java中有一个将原始类型转换为对应的引用类型的机制。称为装箱(boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,称为拆箱(unboxing)。Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱是自动完成的。比如,这就是为什么下面的代码是有效的(一个int被装箱称为Integer):

List list = new ArrayList<>();for (int i = 1;i<100;i++){

list.add(i);

}

但是这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。

Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。比如,在下面的代码中使用IntPredicate就避免了对值1000进行装箱操作,但要是用Predicate就会把参数1000装箱到一个Integer对象中:

packagejava.util.function;

@FunctionalInterfacepublic interfaceIntPredicate {boolean test(intvalue);

}

//避免自动装箱

IntPredicate evenNumbers = (int i) -> i % 2 == 0;

evenNumbers.test(1000);//会造成装箱

Predicate evenNumbers2 = (Integer i) -> i % 2 == 0;

evenNumbers2.test(1000);

一般来说,针对专门的输入参数类型的函数式接口名称都要加上对应的原始前缀,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function接口还有针对输出类型参数的变种:ToIntFunction、IntToDoubleFunction等。

下表中总结了Java API中提供的最常用的函数式接口及函数描述符。如果有需要可以自己设计一个! (T,U) -> R的表达方式应当如何思考一个函数描述符。表的左侧代表了参数类型。这里它代表一个函数,具有两个参数,分别为泛型T和U,返回类型为R。

Java 8中常用函数式接口

函数式接口

函数描述符

原始类型特化

Predicate

T->boolean

IntPredicate、LongPredicate、DoublePredicate

Consumer

T->void

IntConsumer、LongConsumer、DoubleConsumer

Function

T->R

IntFunction、IntToDoubleFunction、IntToLongFunction

LongFunction、LongToDoubleFunction、LongToIntFunction

DoubleFunction、ToIntFunction、ToDoubleFunction、ToLongFunction

Supplier

()->T

BooleanSupplier、IntSupplier、LongSupplier、DoubleSupplier

UnaryOperator

T->T

IntUnaryOperator、LongUnaryOperator、DoubleUnaryOpertor

BinaryOperator

(T,T)->T

IntBinaryOperator、LongBinaryOperator、DoubleBinaryOperator

BiPredicate

(L,R)->boolean

BiConsumer

(T,U)->void

ObjIntConsumer、ObjLongConsumer、ObjDoubleConsumer

BiFunction

(T,U)->R

ToIntBiFunction、ToLongBiFunction、ToDoubleBiFunction

这些函数式接口,可以用于描述各种Lambda表达式的签名了。

Lambdas及函数式接口的例子

使用案例

Lambda的例子

对应的函数式接口

布尔表达式

(List list) -> list.isEmpty()

Predicate>

创建对象

() -> new Apple(10)

Supplier

消费一个对象

(Apple a) -> System.out.println(a.getWeight())

Consumer

从一个对象中选择/提取

(String s) -> s.length()

Function或ToIntFunction

合并两个值

(int a,int b) - > a * b

INtBinaryOperator

比较两个对象

(Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight())

Comparator或Bifunction

或ToIntBiFunction

关于异常

请注意,任何的函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:一是将lambda表达式包含在try catch块中,另一种方式就是自己定义函数式接口,并声明受检异常,像刚刚定义的BufferedReaderProcessor接口

类型检查

Lambda的类型是从使用Lambda的上下文种推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。

daeb2cf80929c4a5f79bd924c7bbf269.png

同样的Lambda,不同的函数式接口

有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要他们的抽象方法签名能够兼容。比如,前面提到的Callabel和PrivilegedAction这两个接口都代表着什么也不接受且返回一个泛型T的函数,因此,下面两个赋值都是有效的:

Callable c = () -> 42;

PrivilegedAction p = () -> 42;

还有比较苹果重量时,也是可以使用不同的函数式接口的

Comparator c1 = (Apple a1, Apple a2) ->a1.getWeight().compareTo(a2.getWeight());

ToIntBiFunction c2 = (Apple a1, Apple a2) ->a1.getWeight().compareTo(a2.getWeight());

BiFunction c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

菱形运算符<>

在Java 7中引入了菱形运算符<> ,利用泛型推断从上下文推断类型的思想。

List strs = new ArrayList<>();

List apples = new ArrayList<>();

特殊的void兼容规则

如果一个Lambda的主体是一个语句表达式,它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)如:

//Predicate 返回一个boolean

Predicate s1 = s ->strs.add(s);

//Consumer 返回一个void

Consumer s2 = s -> strs.add(s);

类型推断

因为Java编译器可以从上下文(目标类型)推断出用什么函数式接口,所以它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。换句话说,Java编译器会像下面这样推断出Lambda的参数类型:

//此处a 没有显示类型 当只有一个参数时 两边的()可以省略

List greenApples = filter(apples,a-> "green".equals(a.getColor()));//a1 a2 推断出apple

Comparator c = (a1,a2) -> a1.getWeight().compareTo(a2.getWeight());

这种方式有时易读,有时不易读。

使用局部变量

Lambda也支持使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。它们被称作捕获Lambda。例如,下面的Lambda捕获了portNumber变量。

int portNumber = 8080;

Runnable r= () -> System.out.println(portNumber);

Lambda虽然可以没有限制的捕获实例变量和静态变量。但局部变量必须声明为final。换句话说,Lambda表达式只能捕获指派给它们局部变量一次。例如下面这个就无法编译:

int portNumber = 8080;

Runnable r= () ->System.out.println(portNumber);

portNumber= 333;

因为Lambda捕获的局部变量必须是final , 而这里又对portNumber进行了赋值 所以会编译报错!

对局部变量的限制

为什么局部变量有这些限制?第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量保存在栈上。如果Lambda可以直接访问局部变量,而Lambda是在一个线程中使用的,则使用Lambda的线程。可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了-----因此就有了这个限制。

第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(这种模式会阻碍很容易做到的并行处理)。

可以编译成功的,实例变量如下:

Apple a = newApple();

Runnable r= () ->System.out.println(a.getWeight());

a.setWeight(10.1);

r.run();

运行后输出10.1

方法引用

方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递他们。在一些情况下,他们比使用Lambda更易读,如下 一个排序的例子:

//使用Lambda

apples.sort((a1,a2)->a1.getWeight().compareTo(a2.getWeight()));//使用方法引用

apples.sort(comparing(Apple::getWeight));

方法引用可以被看做仅仅调用特定方法的Lambda的一种快捷写法。它的思想是,如果一个Lambda代表的是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。

事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。当你需要方法引用时,目标引用放在::前,方法放在::后,如Apple::getWeight就是引用了Apple类中定义的方法getWeight。不需要括号,因为没有实际调用这个方法,只是引用。相当于lambda: (Apple a) -> a.getWeight()的快捷写法。

如何构建方法引用

1.静态方法 (如:Integer的parseInt方法,写作Integer::parseInt).

2.现有对象的实例方法 (如:假设有一个Apple类型的apple变量,就可以 apple::getWeight).

3.任意类型实例方法 (如:String的length方法,String::length).

当方法引用一个对象的时候,而这个对象本身是Lambda的一个参数,例如 (String s)->s.toUpperCase() 就可以写作String::toUpperCase。

举个栗子:对一个字符串List排序,忽略大小写。

List atr = Arrays.asList("a","c","D","E");

atr.sort((s1,s2)->s1.compareToIgnoreCase(s2));//使用方法引用

atr.sort(String::compareToIgnoreCase);

编译器会进行一种与Lambda类似的类型检查过程,来确定给定的函数式接口,这个方法引用是否有效:方法引用必须和上下文类型匹配。

构造函数引用

对于一个现有的构造函数,可以利用它的名称和关键字new来创建爱你它的一个引用:ClassName::new。它的功能与指向静态方法的引用类似。

1. 没有参数:假设你有一个空餐构造函数,它就适合Supplier的签名() -> Apple。

//无参构造函数

Supplier c1 = Apple::new;//调用Supplier的get方法产生一个新的Apple

Apple a1 =c1.get();//等价于Lambda

Supplier c1 = () -> newApple();

Apple a1= c1.get();

2. 1个参数:假设你有一个Apple(Double weight)的签名,那么它就适合Function接口的签名.

//指向 Apple(Double Weight)

Function c2 = Apple::new;//调用Function的apply传入重量 获取一个Apple对象

Apple a2 =c2.apply(18d);//等价于

Function c2 = (weight) -> newApple(weight);

Apple a2= c2.apply(18d);

再来看看之间创建的map方法,我们这次传递Apple的构造函数,就得到了一个具有不同重量的苹果的List:

public static voidmain(String[] args) {

List apps = map(Arrays.asList(1d,2d,3d), Apple::new);

System.out.println(apps);

}public static List map(List list, Functionf){

List result= new ArrayList<>();for(T s: list){

result.add(f.apply(s));

}returnresult;

}//输出结果

[Apple{Id=null, Color='null', Weight='1.0'}, Apple{Id=null, Color='null', Weight='2.0'}, Apple{Id=null, Color='null', Weight='3.0'}]

3. 2个参数假设 你有一个具有两个参数的构造方法Apple(String Color,Double Weight) 那么它就适合BiFunction接口的签名:

//指向 Apple(String Color,Double Weight)

BiFunction c3 = Apple::new;//调用BiFunction函数的apply方法,给出颜色和重量 产生一个Apple对象

Apple a3 = c3.apply("green",33d);//相当于

BiFunction c3 = (color,weight) -> newApple(color,weight);

Apple a3= c3.apply("green",33d);

4. 3个参数 如果超过2个 那么没有默认定义好的了,需要我们自己创建与构造函数引用的签名匹配的函数式接口。如:

public interface TriFunction{

R apply(T t,U u,V v);

}//现在就可以这样使用了

TriFunction colorFactory = Color::new;

复合Lambda表达式

可以把多个简单的Lambda复合成复杂的表达式。在Predicate等函数式接口中提供了默认方法(Java 8新出的 一种可以在接口中写方法体的方法),这些接口提供了很多有用的复合方法。

1.比较器复合

//对Apple按照重量排序

Comparator c =Comparator.comparing(Apple::getWeight);//逆序 接口提供了一个reversed()方法

Comparator c1 =Comparator.comparing(Apple::getWeight).reversed();//比较器链 thenby

Comparator c2 =Comparator.comparing(Apple::getWeight)

.reversed()

.thenComparing(Apple::getColor);

2.谓词复合

谓词接口包含三个方法:negate、and和or。让你可以重用已有的Predicate来创建更复杂的谓词。

Predicate redApple = (Apple a) -> a.getColor().equals("red");//取反

Predicate notRedApple =redApple.negate();//并且

Predicate redAndBigApple = redApple.and(a->a.getWeight()>32);//或者

Predicate redOrGreenApple = redApple.or(a->a.getColor().equals("green"));

函数复合

最后,你还可以把Function接口所代表的Lambda表达式复合起来。Function接口为此配了andThen和compose两个默认方法,他们都会返回Function的一个实例。

andThen方法会返回一个函数,它先对输入应用一个给定函数,在对输出应用另一个函数。比如函数f=(x->x+1),另一个函数g给数字乘2,你可以将他们组合成一个函数。

Function f = x-> x+1;

Function g = x -> x * 2;

Function h =f.andThen(g);int result = h.apply(1); //4

compose方法,先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果,比如andThen相当于f(g(x)),compose相当于g(f(x))。

Function f = x-> x+1;

Function g = x -> x * 2;

Function h =f.compose(g);int result = h.apply(1); //3

小结:

1.Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,还可以有一个抛出异常的列表。

2.Lambda表达式让你可以写出更简洁的代码。

3.函数式接口就是仅仅声明了一个抽象方法的接口。

4.只有在接受函数式接口的地方才可以使用Lambda表达式。

5.Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。

6.Java 8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate、Function、Supplier、Consumer和BinaryOperator。

7.为了避免装箱操作,对Predicate和Function等通用函数式接口的原始类型特化:IntPredicate、IntToLongFunction等

8.环绕执行模式(即在方法锁必须的代码中间,你需要执行点什么操作,比如资源分配和清理)可以配合Lambda提高灵活性和可重用性。

9.Lambda表达式所需要代表的类型称为目标类型。

10.方法引用让你重复使用现有的方法实现并直接传递它们。

11.Comparator、Predicate和Function等函数式接口都有几个可以用来结合Lambda表达式的默认方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值