Java8实战-Lambda表达式

简介: Java8实战-Lambda表达式

在《Java8实战》中第三章主要讲的是Lambda表达式,在上一章节的笔记中我们利用了行为参数化来因对不断变化的需求,最后我们也使用到了Lambda,通过表达式为我们简化了很多代码从而极大地提高了我们的效率。那我们就来更深入的了解一下如何使用Lambda表达式,让我们的代码更加具有简洁性和易读性。

Lambda管中窥豹

什么是Lambda表达式?简单的来说,Lambda表达式是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应其中的Lambda抽象(lambda abstraction),是一个匿名函数,既没有函数名的函数。Lambda表达式可以表示闭包(注意和数学传统意义的不同)。你也可以理解为,简洁的表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出异常的列表。

有时候,我们为了简化代码而去使用匿名类,虽然匿名类能简化一部分代码,但是看起来很啰嗦。为了更好的的提高开发的效率以及代码的简洁性和可读性,Java8推出了一个核心的新特性之一:Lambda表达式。

Java8之前,使用匿名类给苹果排序的代码:

apples.sort(new Comparator<Apple>() {
    @Override
    public int compare(Apple o1, Apple o2) {
        return o1.getWeight().compareTo(o2.getWeight());
    }
});
复制代码

是的,这段代码看上去并不是那么的清晰明了,使用Lambda表达式改进后:

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

或者是:

Comparator<Apple> byWeight = Comparator.comparing(Apple::getWeight);

不得不承认,代码看起来跟清晰了。要是你觉得Lambda表达式看起来一头雾水的话也没关系,我们慢慢的来了解它。

现在,我们来看看几个Java8中有效的Lambda表达式加深对Lambda表达式的理解:

// 这个表达式具有一个String类型的参数并返回一个int,Lambda并没有return语句,因为已经隐含了return。
(String s) -> s.length() 
// 这个表达式有一个Apple类型的参数并返回一个boolean(苹果重来是否大于150克)
(Apple a) -> a.getWeight() > 150
// 这个表达式具有两个int类型二的参数并且没有返回值。Lambda表达式可以包含多行代码,不只是这两行。
(int x, int y) -> {
    System.out.println("Result:");
    System.out.println(x + y);
}
// 这个表达式没有参数类型,返回一个int。
() -> 250
// 显式的指定为Apple类型,并对重量进行比较返回int
(Apple a2, Apple a2) -> a1.getWeight.compareTo(a2.getWeight())

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

(parameters) -> expression

或者(请注意花括号):

(parameters) -> {statements;}

是的,Lambda表达式的语法看起来就是那么简单。那我们继续看几个例子,看看以下哪几个是有效的:

(1) () -> {}
(2) () -> "Jack"
(3) () -> {return "Jack"}
(4) (Interge i) -> return "Alan" + i;
(5) (String s) -> {"IronMan";}
复制代码

正确答案是:(1)、(2)、(3)

原因:

(1) 是一个无参并且无返回的,类似与private void run() {}.

(2) 是一个无参并且返回的是一个字符串。

(3) 是一个无参,并且返回的是一个字符串,不过里面还可以继续写一些其他的代码(利用显式返回语句)。

(4) 它没有使用使用显式返回语句,所以它不能算是一个表达式。想要有效就必须加一对花括号, (Interge i) -> {return "Alan" + i}

(5) "IronMan"很显然是一个表达式,不是一个语句,去掉这一对花括号或者使用显式返回语句即可有效。

在哪里以及如何使用Lambda

我们刚刚已经看了很多关于Lambda表达式的语法例子,可能你还不太清楚这个Lambda表达式到底如何使用。

还记得在上一章的读书笔记中,实现的filter方法中,我们使用的就是Lambda:

List<Apple> heavyApples = filter(apples, (Apple apple) -> apple.getWeight() > 150);
复制代码

我们可以在函数式接口上使用Lambda表达式,函数式接口听起来很抽象,但是不用太担心接下来就会解释函数式接口是什么。

函数式接口

还记得第二章中的读书笔记,为了参数化filter方法的行为使用的Predicate接口吗?它就是一个函数式接口。什么是函数式接口?一言蔽之,函数式接口就是只定义了一个抽象方法的接口。例如JavaAPI中的:Comparator、Runnable、Callable:

public interface Comparable<T> {
    public int compareTo(T o);
}
public interface Runnable {
    public abstract void run();
}
public interface Callable<V> {
    V call() throws Exception;
}

当然,不只是它们,还有很多一些其他的函数式接口。

函数式接口到底可以用来干什么?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实例,并把整个表达式作为函数式接口的实例(具体来说,是函数式接口一个具体实现的实例)。你也可以使用匿名类实现,只不过看来并不是那么的一目了然。使用匿名类你需要提供一个实例,然后在直接内联将它实例化。

通过下面的代码,你可以来比较一下使用函数式接口和使用匿名类的区别:

// 使用Lambda表达式
Runnable r1 = () -> System.out.println("HelloWorld 1");
// 使用匿名类
Runnable r2 = new Runnable() {
    @Override
    public void run() {
        System.out.println("HelloWorld 2");
    }
};
// 运行结果
System.out.println("Runnable运行结果:");
// HelloWorld 1
process(r1);
// HelloWorld 2
process(r2);
// HelloWorld 3
process(() -> System.out.println("HelloWorld 3"));
        
private static void process(Runnable r) {
    r.run();
}

酷,从上面的代码可以看出使用Lambda表达式你可以减少很多代码同时也提高了代码的可读性而使用匿名类却要四五行左右的代码。

函数描述符

函数接口的抽象方法的前面基本上就是Lambda表达式的签名。我们将这种抽象方法叫做函数描述符。例如,Runnable接口可以看作一个什么也不接受什么也不返回的函数签名,因为它只有一个叫做run的抽象方法,这个方法没有参数并且是无返回的。

使用函数式接口

函数式接口很有用,因为抽象方法的签名可以描述Lambda表达式的签名。函数式接口的抽象方法的签名称为函数描述符。

Predicate

在第一章的读书笔记中,有提到过Predicate这个接口,现在我们来详细的了解一下它。

java.util.function.Predicate接口定义了一个名字叫test的抽象方法,它接受泛型T对象,并返回一个boolean值。之前我们是创建了一个Predicate这样的一个接口,现在我们所说到的这个接口和之前创建的一样,现在我们不需要再去创建一个这样的接口就直接可以使用了。在你需要表示一个涉及类型T的布尔表达式时,就可以使用这个接口。比如,你可以定义一个接受String对象的Lambda表达式:

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
private static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
    List<T> result = new ArrayList<>();
    for (T t : list) {
        if (predicate.test(t)) {
            result.add(t);
        }
    }
    return result;
}
List<String> strings = Arrays.asList("Hello", "", "Java8", "", "In", "Action");
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> stringList = filter(strings, nonEmptyStringPredicate);
// [Hello, Java8, In, Action]
System.out.println(stringList);

如果,你去查看Predicate这个接口的源码你会发现有一些and或者or等等一些其他的方法,并且这个方法还有方法体,不过你目前无需关注这样的方法,以后的文章将会介绍到为什么在接口中能定义有方法体的方法。

Consumer

java.util.function.Consumer定义了一个叫做accept的抽象方法,它接受泛型T的对象,并且是一个无返回的方法。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口。比如,你可以用它来创建一个foreach方法,并配合Lambda来打印列表中的所有元素.

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}
private static <T> void forEach(List<T> list, Consumer<T> consumer) {
    for (T i : list) {
        consumer.accept(i);
    }
}
// 使用Consumer
forEach(Arrays.asList("Object", "Not", "Found"), (String str) -> System.out.println(str));
forEach(Arrays.asList(1, 2, 3, 4, 5, 6), System.out::println);

Function

java.util.function.Function<T, R>接口定义了一个叫做apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象。如果你需要定义一个Lambda,将输入对象的信息映射到输出,就可以使用这个接口(比如提取苹果的重量,把字符串映射为它的长度)。在下面的代码中,我们来看看如何利用它来创建一个map方法,将以一个String列表映射到包含每个String长度的Integer列表。

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
private static <T, R> List<R> map(List<T> list, Function<T, R> function) {
    List<R> result = new ArrayList<>();
    for (T s : list) {
        result.add(function.apply(s));
    }
    return result;
}
 List<Integer> map = map(Arrays.asList("lambdas", "in", "action"), (String s) -> s.length());
// [7, 2, 6]
System.out.println(map);
原始类型特化

我们刚刚了解了三个泛型函数式接口:Predicate、Consumer和Function<T, R>。还有些函数式接口专为某些类而设计。

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

List<Integer> list = new ArrayList<>;
for (int i = 0; i < 100; i++) {
    list.add(i);
}

但是像这种自动装箱和拆箱的操作,性能方面是要付出一些代价的。装箱的本质就是将原来的原始类型包起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。

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

@FunctionalInterface
public interface IntPredicate {
    boolean test(int value);
}
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
// 无装箱
evenNumbers.test(1000);
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
// 装箱
oddNumbers.test(1000);

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

Java8中还有很多常用的函数式接口,如果你有兴趣可以去查找一些相关的资料,了解了这些常用的函数接口之后,会对你以后了解Stream的知识有很大的帮助。

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

当我们第一次提到Lambda表达式时,说它可以为函数式接口生成一个实例。然而,Lambda表达式本身并不包含它在实现哪个函数式接口的信息。为了全面了解Lambda表达式,你应该知道Lambda的实际类型是什么。

类型检查

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

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

有了目标类型的概念,同一个Lambda表达式就可以与不同的函数接口关联起来,只要它们的抽象方法能够兼容。比如,前面提到的Callable,这个接口代表着什么也不接受且返回一个泛型T的函数。

同一个Lambda可用于多个不同的函数式接口:

Comparator<Apple> c1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());;

是的,ToIntFunction和BiFunction都是属于函数式接口。还有很多类似的函数式接口,有兴趣的可以去看相关的源码。

到目前为止,你应该能够很好的理解在什么时候以及在哪里使用Lambda表达式了。它们可以从赋值的上下文、方法调用(参数和返回值),以及类型转换的上下文中获得目标类型。为了更好的了解Lambda表达的时候方式,我们来看看下面的例子,为什么不能编译:

Object o = () -> {System.out.println("Tricky example");};

答案:很简单,我们都知道Object这个类并不是一个函数式接口,所以它不支持这样写。为了解决这个问题,我们可以把Object改为Runnable,Runnable是一个函数式接口,因为它只有一个抽象方法,在上一节的读书笔记中我们有提到过它。

Runnable r = () -> {System.out.println("Tricky example");};

你已经见过如何利用目标类型来检查一个Lambda是否可以用于某个特定的上下文。其实,它也可以用来做一些略有不同的事情:tuiduanLambda参数的类型。

类型推断

我们还可以进一步的简化代码。Java编译器会从上下文(目标类型)推断出用什么函数式接口来匹配Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda与法中省去标注参数类型。换句话说,Java编译器会向下面这样推断Lambda的参数类型:

// 参数a没有显示的指定类型
List<Apple> greenApples = filter(apples, a -> "green".equals(a.getColor()));

Lambda表达式有多个参数,代码可独行的好处就更为明显。例如,你可以在这用来创建一个Comparator对象:

// 没有类型推断,显示的指定了类型
Comparator<Apple> cApple1 = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 有类型推断,没有现实的指定类型
Comparator<Apple> cApple2 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

有时候,指定类型的情况下代码更易读,有时候去掉它们也更易读。并没有说哪个就一定比哪个好,需要根据自身情况来选择。

使用局部变量

我们迄今为止所介绍的所有Lambda表达式都只用到了其主体里的参数。但Lambda表达式也允许用外部变量,就像匿名类一样。他们被称作捕获Lambda。例如:下面的Lambda捕获了portNumber变量:

int portNumber = 6666;
Runnable r3 = () -> System.out.println(portNumber);

尽管如此,还有一点点小麻烦:关于能对这些变量做什么有一些限制。Lambda可以没有限制地捕获(也就是在主体中引用)实例变量和静态变量。但局部变量必须显示的声明final,或实际上就算final。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this)。例如,下面的代码无法编译。

int portNumber = 6666;
Runnable r3 = () -> System.out.println(portNumber);
portNumber = 7777;

portNumber是一个final变量,尽管我们没有显示的去指定它。但是,在代码编译的时候,编译器会自动给这个变量加了一个final,起码我看反编译后的代码是有一个final的。

对于局部变量的限制

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

现在,我们来了解你会在Java8代码中看到的另一个功能:方法引用。可以把它们视为某些Lambda的快捷方式。

方法引用

方法引用让你可以重复使用现有的方法,并像Lambda一样传递它们。在一些情况下,比起用Lambda表达式还要易读,感觉也更自然。下面就是我们借助Java8 API,用法引用写的一个排序例子:

// 之前
apples.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
// 之后,方法引用
apples.sort(Comparator.comparing(Apple::getWeight));

酷,使用::的代码看起来更加简洁。在此之前,我们也有使用到过,它的确看起来很简洁。

管中窥豹

方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是:“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。但是,显示地指明方法的名称,你的代码可读性会更好。它是如何工作的?当你需要使用方法引用是,目标引用放在分隔符::前,方法的名称放在后面。 例如,Apple::getWeight就是引用了Apple类中定义的getWeight方法。请记住,不需要括号,因为你没有实际调用这个方法。方法引用就是用Lambda表达式(Apple a) -> a.getWeight()的快捷写法。

我们接着来看看关于Lambda与方法引用等效的一些例子:

Lambda:(Apple a) -> a.getWeight() 
方法引用:Apple::getWeight
Lambda:() -> Thread.currentThread().dumpStack() 
方法引用:Thread.currentThread()::dumpStack
Lambda:(str, i) -> str.substring(i)
方法引用:String::substring
Lambda:(String s) -> System.out.println(s)
方法引用:System.out::println

你可以把方法引用看作是Java8中个一个语法糖,因为它简化了一部分代码。

构造函数引用

对于一个现有的构造函数,你可以利用它的名称和关键字new来创建它的一个引用:ClassName::new。如果,一个构造函数没有参数,那么可以使用Supplier来创建一个对象。你可以这样做:

Supplier<Apple> c1 = Apple::new;
Apple apple = c1.get();

这样做等价于

Supplier<Apple> c1 = () -> new Apple();
Apple apple = c1.get();
复制代码

如果,你的构造函数的签名是Apple(Integer weight),那么可以使用Function接口的签名,可以这样写:

Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(120);

这样做等价于

Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
Apple a2 = c2.apply(120);

如果有两个参数Apple(weight, color),那么我们可以使用BiFunction:

BiFunction<Integer, String, Apple> c3 = Apple::new;
Apple a3 = c3.apply(120, "red");

这样做等价于

BiFunction<Integer, String, Apple> c3 =(weight, color) -> new Apple(weight, color);
Apple a3 = c3.apply(120, "red");

到目前为止,我们了解到了很多新内容:Lambda、函数式接口和方法引用,接下来我们将把这一切付诸实践。

Lambda和方法引用实战

为了更好的熟悉Lambda和方法引用的使用,我们继续研究开始的那个问题,用不同的排序策略给一个Apple列表排序,并需要展示如何把一个圆使出报的解决方案变得更为简明。这会用到我们目前了解到的所有概念和功能:行为参数化、匿名类、Lambda表达式和方法引用。我们想要实现的最终解决方案是这样的:

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

第1步:代码传递

很幸运,Java8的Api已经提供了一个List可用的sort方法,我们可以不用自己再去实现它。那么最困难部分已经搞定了!但是,如果把排序策略传递给sort方法呢?你看,sort方法签名是这样的:

void sort(Comparator<? super E> c)

它需要一个Comparator对象来比较两个Apple!这就是在Java中传递策略的方式:它们必须包裹在一个对象利。我们说sort的行为被参数化了了:传递给他的排序策略不同,其行为也会不同。

可能,你的第一个解决方案是这样的:

public class AppleComparator implements Comparator<Apple> {
    @Override
    public int compare(Apple o1, Apple o2) {
        return o1.getWeight().compareTo(o2.getWeight());
    }
}
apples.sort(new AppleComparator());

它确实能实现排序,但是还需要去实现一个接口,并且排序的规则也不复杂,或许它还可以简化一下。

第2步:使用匿名类

或许你已经想到了一个简化代码的办法,就是使用匿名类而且每次使用只需要实例化一次就可以了:

apples.sort(new Comparator<Apple>() {
    @Override
    public int compare(Apple o1, Apple o2) {
        return o1.getWeight().compareTo(o2.getWeight());
    }
});

看上去确实简化一些,但感觉还是有些啰嗦,我们接着继续简化:

第3步:使用Lambda表达式

我们可以使用Lambda表达式来替代匿名类,这样可以提高代码的简洁性和开发效率:

apples.sort((o1, o2) -> o1.getWeight().compareTo(o2.getWeight()));

太棒了!这样的代码看起来很简洁,原来四五行的代码只需要一行就可以搞定了!但是,我们还可以使这行代码更加的简洁!

第4步:使用方法引用

使用Lambda表达式的代码确实简洁了不少,那你还记得我们前面说的方法引用吗?它是Lambda表达式的一种快捷写法,相当于是一种语法糖,那么我们来试试糖的滋味如何:

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

恭喜你,这就是你的最终解决方案!这样的代码比真的很简洁,这比Java8之前的代码好了很多。这样的代码比较简短,它的意思也很明显,并且代码读起来和问题描述的差不多:“对库存进行排序,比较苹果的重量”。

复合(组合)Lambda表达式的有用方法

Java8的好几个函数式接口都有为方便而设计的的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的Comparator、Function和Predicate都提供了允许你进行复合的方法。这是什么意思呢?在实践中,这意味着你可以把多个简单的Lambda复合成复杂的表达式。比如,你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结果成为另一个函数的输入。你可能会想,函数式接口中怎么可能有更多的方法?(毕竟,这违背了函数式接口的定义,只能有一个抽象方法)还记得我们上一节笔记中提到默认方法吗?它们不是抽象方法。关于默认方法,我们以后在进行详细的了解吧。

比较复合器

还记刚刚我们对苹果的排序吗?它只是一个从小到大的一个排序,现在我们需要让它进行逆序。看看刚刚方法引用的代码,你会发现它貌似无法进行逆序啊!不过不用担心,我们可以让它进行逆序,而且很简单。

1.逆序

想要实现逆序其实很简单,需要使用一个reversed()方法就可以完成我们想要的逆序排序:

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

按重量递减排序,就这样完成了。这个方法很有用,而且用起来很简单。

2.比较器链

上面的代码很简单,但是你仔细想想,如果存在两个一样重的苹果谁前谁后呢?你可能需要再提供一个Comparator来进一步定义这个比较。比如,再按重量比较了两个苹果之后,你可能还想要按原产国进行排序。thenComparing方法就是做这个用的。它接受一个函数作为参数(就像comparing方法一样),如果两个对象用第一个Comparator比较之后还是一样,就提供第二个Comparator。我们又可以优雅的解决这个问题了:

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

复合谓词

谓词接口包括了三个方法: negate、and和or,让你可以重用已有的Predicate来创建更复杂的谓词。比如,negate方法返回一个Predicate的非,比如苹果不是红的:

private static <T> List<T> filter(List<T> list, Predicate<T> predicate) {
    List<T> result = new ArrayList<>();
    for (T t : list) {
        if (predicate.test(t)) {
            result.add(t);
        }
    }
    return result;
}
List<Apple> apples = Arrays.asList(new Apple(150, "red"), new Apple(110, "green"), new Apple(100, "green"));
// 只要红苹果
Predicate<Apple> apple = a -> "red".equals(a.getColor());
// 只要红苹果的非
Predicate<Apple> notRedApple = apple.negate();
// 筛选
List<Apple> appleList = filter(apples, notRedApple);
// 遍历打印
appleList.forEach(System.out::println);

你可能还想要把Lambda用and方法组合起来,比如一个苹果即是红色的又比较重:

Predicate<Apple> redAndHeavyApple = apple.and(a -> a.getWeight() >= 150);

你还可以进一步组合谓词,表达要么是重的红苹果,要么是绿苹果:

Predicate<Apple> redAndHeavyAppleOrGreen =
                apple.and(a -> a.getWeight() >= 150)
                        .or(a -> "green".equals(a.getColor()));

这一点为什么很好呢?从简单的Lambda表达式出发,你可以构建更复杂的表达式,但读起来仍然和问题陈述的差不多!请注意,and和or方法是按照表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and(c)可以看作(a || b) && c。

函数复合

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

andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。假设,有一个函数f给数字加1(x -> x + 1),另外一个函数g给数字乘2,你可以将它们组合成一个函数h:

Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
// result = 4
int result = h.apply(1);

你也可以类似地使用compose方法,先把给定的函数左右compose的参数里面给的那个函数,然后再把函数本身用于结果。比如在上一个例子用compose的化,它将意味着f(g(x)),而andThen则意味着g(f(x)):

Function<Integer, Integer> f1 = x -> x + 1;
Function<Integer, Integer> g1 = x -> x * 2;
Function<Integer, Integer> h1 = f1.compose(g1);
// result1 = 3
int result1 = h1.apply(1);

它们的关系如下图所示:

compose和andThen的不同之处就是函数执行的顺序不同。compose函数限制先参数,然后执行调用者,而andThen限制先调用者,然后再执行参数。

总结

在《Java8实战》第三章中,我们了解到了很多概念关键的念。

  1. Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可抛出的异常列表。
  2. Lambda表达式让我们可以简洁的传递代码。
  3. 函数式接口就是仅仅只有一个抽象方法的接口。
  4. 只有在接受函数式接口的地方才可以使用Lambda表达式。
  5. Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
  6. Java8自带一些常用的函数式接口,在java.util.function包里,包括了Predicate、Function<T, R>、Supplier、Consumer和BinaryOperatory。
  7. 为了避免装箱操作,等于Predicate和Function<T, R>等通用的函数式接口的原始类型特化:IntPredicate、IntToLongFunction等。
  8. Lambda表达式所需要代表的类型称为目标类型。
  9. 方法引用可以让我们重复使用现有的方法实现并且直接传递它们。
  10. Comparator、Predicate和Function等函数式接口都有几个可以用来结合Lambda表达式的默认方法。
目录
相关文章
|
5天前
|
Java 程序员 API
Java中的Lambda表达式:简化你的代码
【7月更文挑战第10天】Lambda表达式,这一Java 8的闪亮特性,为开发者提供了一种更为简洁、灵活的编程方式。本文将探讨Lambda表达式如何优化代码结构,提升开发效率,以及在实际项目中应用的一些实例。我们将从基础语法入手,逐步深入到高级用法,最后讨论其性能影响,旨在帮助读者全面理解并有效利用Lambda表达式。
31 20
|
6天前
|
前端开发 Java 关系型数据库
Java中的电子商务网站开发实战
Java中的电子商务网站开发实战
|
4天前
|
Java 编译器 API
Java中的Lambda表达式:简化代码,提升性能
在Java 8中,Lambda表达式的引入为开发者提供了一种更加简洁、灵活的编程方式。本文将深入探讨Lambda表达式的概念、语法、使用场景及其在Java中的应用示例,帮助读者更好地理解和掌握这一强大工具,从而优化代码结构,提高开发效率。
|
5天前
|
Java API
Java面试题:说明Lambda表达式在Java中的应用,以及函数式接口的概念和作用。
Java面试题:说明Lambda表达式在Java中的应用,以及函数式接口的概念和作用。
11 0
|
5天前
|
算法 Java 开发者
Java面试题:Java内存探秘与多线程并发实战,Java内存模型及分区:理解Java堆、栈、方法区等内存区域的作用,垃圾收集机制:掌握常见的垃圾收集算法及其优缺点
Java面试题:Java内存探秘与多线程并发实战,Java内存模型及分区:理解Java堆、栈、方法区等内存区域的作用,垃圾收集机制:掌握常见的垃圾收集算法及其优缺点
8 0
|
5天前
|
安全 Java 调度
Java面试题:Java内存优化、多线程安全与并发框架实战,如何在Java应用中实现内存优化?在多线程环境下,如何保证数据的线程安全?使用Java并发工具包中的哪些工具可以帮助解决并发问题?
Java面试题:Java内存优化、多线程安全与并发框架实战,如何在Java应用中实现内存优化?在多线程环境下,如何保证数据的线程安全?使用Java并发工具包中的哪些工具可以帮助解决并发问题?
7 0
|
2月前
|
Java API
Java 8新特性之Lambda表达式与Stream API
【5月更文挑战第17天】本文将介绍Java 8中的两个重要特性:Lambda表达式和Stream API。Lambda表达式是一种新的编程语法,它允许我们将函数作为参数传递给其他方法,从而使代码更加简洁。Stream API是一种用于处理集合的新工具,它提供了一种高效且易于使用的方式来处理数据。通过结合使用这两个特性,我们可以编写出更加简洁、高效的Java代码。
37 0
|
26天前
|
Java 大数据 API
Java中的Lambda表达式和Stream API的高效使用
【6月更文挑战第18天】在Java 8中引入的Lambda表达式和Stream API为集合操作带来了革命性的改进,提供了一种更加简洁、声明式的编程方式。本文将深入探讨如何利用这些特性来提升代码的可读性和开发效率,同时避免常见的性能陷阱。
|
2月前
|
Java API 数据处理
Java 8新特性之Lambda表达式与Stream API
【5月更文挑战第28天】本文将介绍Java 8中的两个重要特性:Lambda表达式和Stream API。Lambda表达式是一种新的编程语法,可以使代码更加简洁、易读。而Stream API则是一种处理数据的新方法,可以让我们更高效地处理集合数据。通过学习这两个特性,我们可以编写出更加简洁、高效的Java代码。
|
2月前
|
Java API
Java 8新特性之Lambda表达式与Stream API
【5月更文挑战第28天】本文将介绍Java 8中的两个重要特性:Lambda表达式和Stream API。Lambda表达式是一种新的语法特性,允许我们将函数作为参数传递给方法或者作为返回值。而Stream API是一种处理集合的新方式,它提供了一种高效且简洁的方式来处理数据。通过学习这两个特性,我们可以编写出更加简洁、高效的Java代码。