java类打比方_【Java 8实战笔记】Lambda表达式

Lambda表达式

利用行为参数化这个概念,就可以编写更为灵活且可重复使用的代码。但同时,使用匿名类来表示不同的行为并不令人满意。Java 8引入了Lambda表达式来解决这个问题。它使你以一种很简洁的表示一个行为或传递代码。

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

匿名 - 因为它不像普通的方法一样有一个明确的名称。

函数 - 说它是函数是因为Lambda函数不像方法那样属于某个特定的类,但和方法要一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。

传递 - Lambda表达式可以作为参数传递给方法或存储在变量中。

简洁 - 无需像匿名类那样写很多模板代码

使用Lambda的最终结果就是你的代码变得更清晰、灵活。打比方,利用Lambda表达式,可以更为简洁地自定义一个Comparator对象。

942c55adda49

Lambda表达式由参数、箭头和主体组成

对比以下两段代码:

Comparator byWeight = new Comparator(){

public int compare(Apple a1,Apple a2){

return a1.getWeight().compareTo(a2.getWeight());

}

};

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

代码看起来更清晰了。基本上只传递了真正需要的代码(compare方法的主体)。

Lambda表达式由三个部分,如图 Lambda表达式由参数、箭头和主体组成 所示

参数列表 - 这里它采用了Comparator中compare方法的参数,两个Apple。

箭头 - 箭头 -> 把参数列表与Lambda主体分隔开。

Lambda主体 - 比较两个Apple的重量。表达式就是Lambda的返回值。

Lambda 的基本语法是:

(parameters) -> expression 或 (parameters) -> { statements; }

函数式接口

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

public interface Predicate{

boolean test(T t);

}

简而言之,函数式接口就是只定义一个抽象方法的接口。Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。

函数式接口的抽象方法的签名

把Lambda付诸实践:环绕执行模式

要从一个文件中读取一行所需的模板代码:

public static String processFile() throws IOException{

try (BufferedReader br =

new BufferedReader(new FileReader("data.txt"))){

return br.readLine();

}

}

现在这段代码的局限性在于只能读文件的第一行,如果想要返回头两行,甚至是返回使用最频繁的词。这时需要把processFile的行为参数化。

需要一个接收BufferedReader并返回String的Lambda。下面是从BufferedReader中打印两行的写法:

String result = processFile(BufferedReader br) -> br.readLine() + br.readLine());

现在需要创建一个能匹配BufferedReader -> String,还可以抛出IOException异常的接口:

@FunctionalInterface

public interface BufferedReaderProcessor{

String process(BufferedReader b) throws IOException;

}

@FunctionalInterface 是什么?

这个标注用于表示该接口会设计成一个函数式接口。如果用 @FunctionalInterface 定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。

使用它不是必须的,但是使用它是比较好的做法。

现在就可以把这个接口作为新的processFile方法的参数,在方法主体内,对得到BufferedReaderProcessor对象调用process方法执行处理:

public static String processFile(BufferedReaderProcessor p) throws IOException {

try (BufferedReader br =

new BufferedReader(new FileReader("data.txt"))){

return p.process(br);

}

}

现在可以通过传递不同的Lambda重用processFile方法,并以不用的方式处理文件了。

处理一行:

String oneLine =

processFile((BufferedReader br) -> br.readLine());

处理两行:

String twoLines =

processFile((BufferedReader br) -> br.readLine() + br.readLine());

完整代码如下:

import java.io.BufferedReader;

import java.io.FileReader;

import java.io.IOException;

public class test{

@FunctionalInterface

public interface BufferedReaderProcessor{

String process(BufferedReader b) throws IOException;

}

public static String processFile(BufferedReaderProcessor p) throws IOException {

try (BufferedReader br =

new BufferedReader(new FileReader("c:/tmp/data.txt"))){

return p.process(br);

}

}

public static void main(String[] args) throws IOException {

String oneLine = processFile((BufferedReader br) -> br.readLine());

String twoLines = processFile((BufferedReader br) -> br.readLine() + br.readLine());

System.out.println(oneLine);

System.out.println(twoLines);

}

}

现在已经能成功的利用函数式接口来传递Lambda。

使用函数式接口

函数接口定义且只定义了一个抽象方法。函数式接口的抽象方法的签名称为函数描述符。因此,为了应用不同的Lambda表达式,需要一套能够描述常见函数描述符的函数式接口。

Java 8在java.util.function包中加入了许多新的函数式接口,你可以重用它来传递多个不同的Lambda。

Predicate

java.util.function.Predicate接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。在需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。

public interface Predicate {

boolean test(T t);

}

例如:

@FunctionalInterface

public interface Predicate {

boolean test(T t);

}

public static List filter(List list, Predicate p){

List results = new ArrayList<>();

for (T s: list){

if(p.test(s)){

results.add(s);

}

}

return results;

}

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

List nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

Consumer

java.util.function.Consumer定义了一个名叫accept的抽象方法,它接受泛型T对象,没有返回。

例如:

@FunctionalInterface

public interface Comsumer {

void accept(T t);

}

public static void forEach(List list, Predicate c){

for (T i: list){

c.accept(i);

}

}

forEach(

Arrays.asList(1,2,3,4,5),

(Interger i) -> System.ou.println(i)

);

Function

java.util.function.Function定义了一个名叫apply的抽象方法,它接受一个泛型T对象,并返回一个泛型R的对象。

下面将创建一个map方法,以将一个String列表映射到包含每个String长度的Interger列表:

@FunctionalInterface

public interface Function {

R accept(T t);

}

public static List map(List list, Function f){

List result = new ArrayList<>();

for (T s: list){

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

}

return result;

}

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

原始类型特化

Java的类型有两种: 引用类型 和 原始类型 。但是泛型只能绑定到引用类型。这是由于泛型内部的实现方式造成的。因此Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫装箱(boxing)。相反的操作便叫拆箱(unboxing)。装箱和拆箱操作是可以由自动装箱机制来自动完成的。但是这在性能方面要付出代价,装箱后的值需要更多的内存并且需要额外的内存。

Java 8为原始类型带来了一个专门的版本,用于在输入和输出都是原始类型时避免自动装箱的操作:

public interface IntPredicate{

boolean test(int t);

}

无装箱:

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

evenNumbers.test(1000);

装箱:

Predicate oddNumbers = (Integer i) -> i%2 == 1;

oddNumbers.test(1000);

类型检查、类型推断以及限制

Lambda本身并不包含它在实现哪个函数式接口的信息。为了全面了解Lambda表达式应该知道Lambda的实际类型是什么。

类型检查

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

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

有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来。

类型推断

可以进一步简化你的代码。编译器会从上下文(目标类型)推断出痛什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,这样就可以再Lambda语法中省去标注参数类型。

没有自动类型推断:

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

有自动类型推断:

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

使用局部变量

之前介绍的所有Lambda表达式都只用到了其主体里面的参数。但Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。它们被称作捕获Lambda。

例如:

int portNumber = 1337;

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

Lambda捕获了portNumber变量。但关于对这些变量可以做什么有一些限制。Lambda可以没有限制的捕获实例变量和静态变量(也就是在其主体中引用)。但是局部变量必须显示声明为final(或事实上是final)。

下面的代码无法编译,因为portNumber变量被赋值了两次:

int portNumber = 1337;

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

portNumber = 31337;

实例变量和局部变量背后的实现有一个关键的不同。实例变量都存储在堆中,而局部变量则保存在栈上。Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后去访问该变量,这回引发造成线程不安全的新的可能性。

方法引用

方法引用可以重复使用现有的方法定义,并像Lambda一样传递它们。

下面是用方法引用写的一个排序的例子:

先前:

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

之后(使用方法引用和 java.util.Comparator.comparing ):

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

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

方法引用就是让你根据已有的方法实现来创建Lambda表达式。

当你需要使用方法引用时,目标引用放在分隔符 :: 前,方法的名称放在后面。例如 Apple::getWeight 就是引用了Apple类中定义的方法getWeight。

方法引用也可以看做针对仅仅设计单一方法的Lambda的语法糖。

构建方法引用

方法引用主要有三类。

指向静态方法的方法引用(Integer::parseInt)

指向任意类型实例方法的方法引用(String::length)

指向现有对象的实例方法的方法引用(Transaction::getValue)

第二种方法引用的思想就是你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。

例如 (String s) -> s.toUpperCase() 可以写作 String::toUpperCase。

第三种方法引用指的是,你在Lambda中调用一个已经存在的外部对象的方法。例如,Lambda表达式 () -> expenssiveTransaction.getValue() 可以写作 expensiveTransaction::getValue。

方法引用不需要括号,是因为没有实际调用这个方法。

构造函数引用

可以利用现有构造函数的名称和关键字来创建它的一个引用 ClassName:new

例如:

List weight = Arrays.asList(7,3,4,10);

List apples = map(weights, Apple::new);

public static List map(List List, Function f){

List result = new ArrayList<>();

for(Integer e: list){

result.add(f.apply(e))

}

return result;

}

Lambda和方法引用实战(用不同的排序策略给一个Apple列表排序)

第1步:传递代码

Java API已经提供了一个List可用的sort方法,要如何把排序策略传递给sort呢?sort方法的签名样子如下:

void sort(Comparator super E> c)

它需要一个Comparator对象来比较两个Apple。

第一个解决方案看上去是这样的:

public class AppleComparator implements Comparator{

public int compare(Apple a1, Apple a2){

return a1.getWeight().compareTo(a2.getWeight());

}

}

inventory.sort(new AppleComparator());

第2步:使用匿名类改进

inventory.sort(new AppleComparator(){

public int compare(Apple a1, Apple a2){

return a1.getWeight().compareTo(a2.getWeight());

}

});

使用匿名类的意义仅仅在于不用为了只实例化一次而实现一个Comparator。

第3步:使用Lambda表达式

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

inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));

Comparator有一个叫做comparing的静态辅助方法,它可以接受一个Function来提取Comparable键值,并生成一个Comparator对象:

import static java.util.Comparator.comparing;

inventory.sort(comparing((a) -> a.getWeight()));

第4步:使用方法引用

inventory(comparing(Apple::getWeight));

复合Lambda表达式的有用方法

比较器复合

对于:

Comparator c = Comparator.comparing(Apple::getWeight);

1.逆序

想要按重量递减排序怎么办?不需要建立一个新的Comparator的实例。接口有个默认方法 reverse 可以使给定的比较器逆序。因此仍然可以用之前的那个比较器:

inventory.sort(comparing(Apple::getWeight).reversed());

2.比较器链

如果两个苹果一样重怎么办,哪个苹果应该排在前面?这时候可能需要再提供一个Comparator来进一步比较。

thenComparing 就是做这个用的。它接受一个函数作为参数(与comparing方法一样),如果两个对象用第一个Comparator比较之后是一样的,就提供第二个Comparator:

inventory.sort(comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry));

谓词复合

谓词接口包括三个方法:negate、and 和 or。

negate

可以使用negate方法来返回一个Predicate的非,比如苹果不是红色:

Predicate notRedApple = redApple.negate()

and

可以用and方法将两个Lambda组合起来:

Predicate redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);

or

Predicate redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150).or(a -> "green".equals(a.getcolor()));

函数复合

还可以把Function接口所代表的Lambda表达式复合起来。Function接口有两个默认方法:andThen和 compose。它们都会返回Function的一个实例。

andThen 方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。

比如函数f给数字加1,另一个函数给数字乘2:

Function f = x -> x + 1;

Function g = x -> x * 2;

Function h = f.andThen(g);

int result = h.apply(1);

在数学上意味着g(f(x))。

compose 方法先把给定的函数用作compose的参数里面给的那个函数,然后再把函数本身用于结果。

Function f = x -> x + 1;

Function g = x -> x * 2;

Function h = f.compose(g);

int result = h.apply(1);

在数学上意味着f(g(x))。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值