Java 8实战-第三章

80 阅读5分钟

Lambda表达式

Lambda表达式,可以简洁地表示一个行为或传递代码。

3.1 Lambda管中窥豹

Lambda表达式有三个部分,参数列表,箭头,Lambda主体。

Lambda没有return语句,因为已经隐含了return。

基本语法是 : (parameters) -> expression (parameters) -> {statements;}

3.2 在哪里以及如何使用Lambda

可以在函数式接口上使用Lambda表达式。

3.2.1 函数式接口

函数式接口就是只定义一个抽象方法的接口。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。

Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例。

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"));

3.2.2 函数描述符

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名,将这种抽象方法叫做函数描述符。 @FunctionalInterface标注该接口为一个函数式接口

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

资源处理时一个常见的模式就是打开一个资源,做一些处理,然后关闭资源。这个设置和清理阶段总是很类似,并且会围绕着执行处理的那些重要代码。

3.3.1 第1步:记得行为参数化

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

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

创建一个能匹配BufferedReader -> String,还可以抛出IOException异常的接口。

3.3.3 第3步:执行一个行为

需要一种方法在processFile主体内执行Lambda所代表的代码。

public static String processFile(BufferedReaderProcessor p) throws IOException {
      try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
      return p.process(br);
}
}

3.3.4 第4步: 传递Lambda

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

3.4 使用函数式接口

介绍Predicate、Consumer和Function

3.4.1 Predicate

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

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

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

3.4.2 Consumer

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

public static <T> void forEach(List<T> list, Consumer<T> c) {
    for (T t : list) {
       c.accept(t);
    }
}

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

3.4.3 Function

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

public 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;
}

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

任何函数式接口都不允许抛出受检异常,如果需要Lambda表达式来抛出异常,一个是定义一个自己的函数式接口,并声明受检异常,或者把Lambda包在一个try/catch块中。

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

3.5.1 类型检查

Lambda的类型是从使用Lambda的上下文推断出来的。具体是根据入参的函数式接口,从接口的入参和出参进行匹配。

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

有了目标类型的概念,同一个Lambda表达式就可以与不同的函数式接口联系起来,只要抽象方法的签名能够兼容。

3.5.3 类型推断

编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。

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

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

3.5.4 使用局部变量

Lambda表达式只能捕获指派给它们的局部变量一次。第一Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。第二不鼓励使用改变外部变量的典型命令式编程模式。

方法引用

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

3.6.1 管中窥豹

方法引用主要用静态方法的方法引用、指向任意类型实例方法的方法引用、指向现有对象的实例方法的方法引用。编译器会进行一种与Lambda表达式类似的类型检查过程,来确定对于给定的函数式接口,方法引用的签名必须和上下文类型匹配。

3.6.2 构造函数引用

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

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

3.7 Lambda和方法引用实战

用不同的排序策略给一个Apple列表排序

3.7.1 第1步:传递代码

void sort(Comparator<? super E> c)

public class AppleComparator implements Comparator<Apple> {
   public int compare(Apple a1, Apple a2) {
     return a1.getWeight().compareTo(a2.getWeight());
   }
}

inventory.sort(new AppleComparator());

3.7.2 第2步:使用匿名类

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

3.7.3 第3步:使用Lambda表达式

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

进一步
inventory.sort(Comparator.comparing((a) -> a.getWeight()));

3.7.4 第4步:使用方法引用

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

3.8 复合Lambda表达式的有用方法

可以把多个简单的Lambda复合成复杂的表达式。

3.8.1 比较器复合

逆序

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

比较器链

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

3.8.2 谓词复合

谓词接口包括negate、and和or

Predicate<Apple> notRedApple = redApple.negate();

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

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

3.8.3 函数复合

Function配了andThen和compose两个默认方法来复合表达式。

Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 2;
Function<Integer, Integer> h = f.andThen(g);
Function<Integer, Integer> i = f.compose(g);

3.9 小结

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

Lambda表达式让你可以简洁地传递代码。

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

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

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

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

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

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

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

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

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