Java 8 实战 -- lambda表达式 (一)

lambda 管中窥豹

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

  • 匿名——我们说匿名,是因为它不像普通的方法那样有一个明确的名称:写得少而想
    得多!
  • 函数——我们说它是函数,是因为Lambda函数不像方法那样属于某个特定的类。但和方
    法一样, Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。

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

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

利用Lambda表达式,你可以更为简洁地自定义一个Comparator对象。
这里写图片描述
先前:

Comparator<Apple> byWeight = new Comparator<Apple>() {
            public int compare(Apple a1, Apple a2) {
                return a1.getWeight().compareTo(a2.getWeight());
            }
        };

使用lambda后:

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

可见使用lambda后,代码的确简洁多了。
lambda的基本格式:

  1. 参数列表——这里它采用了Comparator中compare方法的参数,两个Apple。
  2. 箭头——箭头->把参数列表与Lambda主体分隔开。
  3. Lambda主体——比较两个Apple的重量。表达式就是Lambda的返回值了。
    下图为一些常用的lambda示例:
    这里写图片描述

在哪里以及如何使用 Lambda

那到底在哪里可以使用Lambda呢?你可以在函数式接口上使用Lambda表达式。那么什么是函数式接口呢
1.函数式接口
一言以蔽之, 函数式接口就是只定义一个抽象方法的接口。你已经知道了Java API中的一些其他函数式接口:

public interface Comparator<T> {
    int compare(T o1, T o2);
}

public interface Runnable{
    void run();
}

public interface ActionListener extends EventListener{
    void actionPerformed(ActionEvent e);
}

public interface Callable<V>{
    V call();
}

public interface PrivilegedAction<V>{
    V run();
}

用函数式接口可以干什么呢? Lambda表达式允许你直接以内联的形式为函数式接口的抽象
方法提供实现, 并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现
的实例) 。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后
再直接内联将它实例化。下面的代码是有效的, 因为Runnable是一个只定义了一个抽象方法run
的函数式接口:

Runnable r1 = () -> System.out.println("Hello World 1");

Runnable r2 = new Runnable(){
    public void run(){
        System.out.println("Hello World 2");
    }
};

public static void process(Runnable r){
    r.run();
}
process(r1);
process(r2);
process(() -> System.out.println("Hello World 3"))

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

public void process(Runnable r){
  r.run();
}

process(() -> System.out.println("This is awesome!!"));

此代码执行时将打印“ This is awesome!!”。 Lambda表达式()-> System.out.println(“This is awesome!!”)不接受参数且返回void。 这恰恰是Runnable接口中run方法的签名。

@FunctionalInterface又是怎么回事?
如果你去看看新的Java API,会发现函数式接口带有@FunctionalInterface的标注。这个标注用于表示该接口会设计成
一个函数式接口。如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“ Multiple non-overriding abstract methods found in interface Foo”,表明存在多个抽象方法。请注意,@FunctionalInterface不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。 它就像是@Override标注表示方法被重写了。

使用函数式接口

函数式接口定义且只定义了一个抽象方法。函数式接口很有用,因为抽象方法的签名可以描述Lambda表达式的签名。函数式接口的抽象方法的签名称为函数描述符。所以为了应用不同的Lambda表达式,你需要一套能够描述常见函数描述符的函数式接口。Java API中已经有了几个函数式接口,Java 8的库设计师帮你在java.util.function包中引入了几个新的函数式接口。我们接下来会介绍Predicate、 Consumer和Function。

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

//示例
static <T> List<T> filter(List<T> list, Predicate<T> p) {
    List<T> results = new ArrayList<>();
    for(T s: list){
        if(p.test(s)){
            results.add(s);
        }
    }
    return results;
}

//示例
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty();
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);

2.Consumer

@FunctionalInterface
public interface Consumer<T>{
    void accept(T t);
}

//示例
static <T> void forEach(List<T> list, Consumer<T> c){
    for(T i: list){
        c.accept(i);
    }
}
forEach(Arrays.asList(1,2,3,4,5),
    (Integer i) -> System.out.println(i)
);

3.Function

@FunctionalInterface
public interface Function<T, R>{
    R apply(T t);
}

//示例
static <T, R> List<R> map(List<T> list,Function<T, R> f) {
    List<R> result = new ArrayList<>();
    for(T s: list){
        result.add(f.apply(s));
    }
    return result;
}
// [7, 2, 6]
List<Integer> l = map(
    Arrays.asList("lambdas","in","action"),
    (String s) -> s.length()
);

除了上述的三个函数式接口,jdk8还提供了如下的常用函数式接口这里写图片描述

这里写图片描述

异常、 Lambda,还有函数式接口又是怎么回事呢?
请注意,任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda
表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda
包在一个try/catch块中。
比如,

@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
BufferedReaderProcessor p = (BufferedReader br) -> br.readLine();

但是你可能是在使用一个接受函数式接口的API,比如Function

Function<BufferedReader, String> f = (BufferedReader b) -> {
    try{
        return b.readLine();
    }catch(IOException e) {
        throw new RuntimeException(e);
    }
};

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

1.类型检查
Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。让我们通过一个例子,看看当你使用Lambda表达式时背后发生了什么。这里写图片描述
类型检查过程可以分解为如下所示。
-首先,你要找出filter方法的声明。
-第二,要求它是Predicate(目标类型)对象的第二个正式参数。
-第三, Predicate是一个函数式接口,定义了一个叫作test的抽象方法。
-第四, test方法描述了一个函数描述符,它可以接受一个Apple,并返回一个boolean。
-最后, filter的任何实际参数都必须匹配这个要求。

2.同样的lambda,不同的函数式接口
有了目标类型的概念,同一个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());

特殊的void兼容规则
如果一个Lambda的主体是一个语句表达式, 它就和一个返回void的函数描述符兼容(当
然需要参数列表也兼容)。例如,以下两行都是合法的,尽管List的add方法返回了一个
boolean,而不是Consumer上下文(T -> void)所要求的void:

// Predicate返回了一个boolean
Predicate<String> p = s -> list.add(s);
// Consumer返回了一个void
Consumer<String> b = s -> list.add(s);

3.类型推断
java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。

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

有时候显式写出类型更易读,有时候去掉它们更易读。

4.使用局部变量
Lambda表达式引用的局部变量必须是最终的( final)或事实上最终的


方法引用

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

//先前
appleList.sort((Apple a1, Apple a2) -> {
   return a1.getWeight().compareTo(a2.getWeight());
});

//之后使用方法引用
appleList.sort(Comparator.comparing(Apple::getWeight));

方法引用看作针对仅仅涉及单一方法的Lambda的语法糖,因为你表达同样的事情时要写的代码更少了。
这里写图片描述

如何构建方法引用
方法引用主要有三类。
(1) 指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)。
(2) 指 向 任 意 类 型 实 例 方 法 的 方 法 引 用 ( 例 如 String 的 length 方 法 , 写 作String::length)。
(3) 指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,就可以写expensiveTransaction::getValue)。
这里写图片描述

复合 Lambda 表达式的有用方法

1.比较器复合

  • 逆序-reversed()
comparing(Apple::getWeight).reversed();
  • 比较器链-.thenComparing()
inventory.sort(comparing(Apple::getWeight).reversed()
.thenComparing(Apple::getCountry));

2.谓词复合

  • 取非-negate()
Predicate<Apple> redApple = a ->      "red".equals(a.getColor());//取红Apple
Predicate<Apple> notRedApple = redApple.negate();//非红
  • and
Predicate<Apple> redAndHeavyApple =
redApple.and(a -> a.getWeight() > 150);//红 and >150
  • or
Predicate<Apple> redAndHeavyApple =
redApple.or(a -> a.getWeight() < 100);//红 or < 100

请注意, and和or方法是按照在表达式链中的位置,从左向右确定优先级的。因此, a.or(b).and(c)可以看作(a || b) && c。

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

  • addThen
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;

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

//等价于f(g(x))
Function<Integer, Integer> h = f.compose(g); 
int result = h.apply(1); //result = 3

参考资料

  • java8 in action

猜你喜欢

转载自blog.csdn.net/it_freshman/article/details/73608466