启舰分享android基本功android进阶

Lambda

2018-08-16  本文已影响1人  阿班是小猿

简言

由于之前一直使用的都是Java 7,对Java 8的一些新特性不甚了解。最近Aban学习了一下Java 8的一些新特性,在这里简单总结、分享一下关于Lambda表达式的一些东西。如有错误,还望指正。

行为参数化的思想

在使用Lambda表达式之前,我们应该先理解一个重要概念----行为参数化。

行为参数化,简单来说,就是你准备好一个代码块,却不去执行它,这个代码块以后可以被程序其他部分调用。更通俗的说,就是把方法(你的代码)作为参数传递给另一个方法。 1.png
2.png

比如,将代码块传递给另一个方法,稍后去执行它。这个方法的行为就基于那块代码被参数化了。例如,你要处理一个集合,你一可能会写一个方法:

初试牛刀

我们接下来看一个例子,然后展示一些让代码灵活的最佳做法。(<em><font color="brown">例子较长,请耐心观看~~</font></em>)
有一个农场仓库,里面有很多苹果,我们用一个list集合来表示。

  1. 农民伯伯想要找出所有的绿苹果,听起来很简单:
public static List<Apple> filterGreenApples(List<Apple> apples) {
    List<Apple> greenApples = new ArrayList<Apple>();
    for(Apple apple: apples) {
        if("green".equals(apple.getColor())) {
            greenApples.add(apple);
        }
    }
    return greenApples;
}

我们检出所有的绿苹果之后,农民伯伯改变注意了,想要找出各种颜色苹果:绿色,红色,黄色......

  1. 再展身手,将颜色作为参数
    针对农民伯伯的需求,我们可以把颜色作为参数,这样会更加灵活一点:
public static List<Apple> filterApples(List<Apple> apples,String color) {
    List<Apple> result = new ArrayList<Apple>();
    for(Apple apple: apples) {
        if(apple.getColor().equals(color)) {
            result.add(apple);
        }
    }
    return result;
}

我们再复杂一点,农民伯伯又说,要是能区分轻的苹果和重的苹果就好了,大于100克的算是重的苹果。作为有职业操守的程序员,我们早可以想到农民伯伯可能要改变重量,于是又有了下面的方法:

public static List<Apple> filterApples(List<Apple> apples,int weight) {
    List<Apple> result = new ArrayList<Apple>();
    for(Apple apple: apples) {
        if(apple.getWeight() > weight) {
            result.add(apple);
        }
    }
    return result;
}

虽然有了解决方案,但这有点令人失望,因为中间有很多重复的代码。

  1. 第三次尝试
    我们可以把筛选颜色和重量弄到一个方法里面,定义一个标识来判断是筛选颜色还是重量:
public static List<Apple> filterApples(List<Apple> apples,String color,int weight,boolean flag) {
    List<Apple> result = new ArrayList<Apple>();
    for(Apple apple: apples) {
        if((flag && apple.getColor().equals(color)) ||
            (!flag && apple.getWeight() > weight)) {
            result.add(apple);
        }
    }
    return result;
}

你可以这么用:

List<Apple> greenApples = filterApples(apples,"green",0,true);
List<Apple> heavyApples = filterApples(apples,"",100,false);

但说句实话,这个方案实在糟糕透了,如果需要组合属性做更复杂的查询,或者有更加复杂的需求,可能需要更加冗长复杂的代码(<em><font color="brown">你身为程序员的职业操守呢???</font></em>)

运用行为参数化

从上面可以看到,我们需要一种更好的方法来应对变化的需求,需要更高层次的抽象。
我们可以根据Apple的属性,来返回一个boolean值,我们称之为<b>谓词</b>(即一个返回boolean值的函数)
首先,我们定义一个接口来对选择标准建模:

public interface ApplePredicate {
    boolean test(Apple apple);
}

然后,我们就可以用ApplePredicate的多个实现来代表不同的选择标准:

public class AppleColorPredicate implements ApplePredicate{
    //选出绿苹果
    @Override
    public boolean test(Apple apple) {
       return "green".equals(apple.getColor());
    }
}
public class AppleWeightPredicate implements ApplePredicate{
    //选出重的苹果
    @Override
    public boolean test(Apple apple) {
       return apple.getWeight() > 100;
    }
}

最后,我们的filter方法看起来是这样的:

public static List<Apple> filterApples(List<Apple> apples,ApplePredicate p){
    List<Apple> result = new ArrayList<Apple>();
    for (Apple apple : apples) {
        if(p.test(apple)) {
        result.add(apple);
    }
}
    return result;
}

这里,filterApples方法需要接受ApplePredicate对象,对Apple做条件测试。filterApples方法的行为取决于通过ApplePredicate对象传递的代码,换句话说,我们把filterApples方法的行为参数化了!(<em><font color="brown">身为程序员的你终于有了有一丝丝尊严!!!</font></em>)
由此,我们对行为参数化有了更精确的解释:<b>让方法接受多种行为作为参数,并在内部使用,来完成不同的行为。</b>

进阶

  1. 匿名类
    上面的例子虽然最后算是找到一个<em><font color="brown">还算好一点</font></em>的方案,但还是有点费劲。我们可以使用匿名内部类,它允许你随用随建。
List<Apple> redApples = filterApples(apples,new ApplePredicate() {
        //筛选红苹果
        @Override
        public boolean test(Apple apple) {
            return "red".equals(apple.getColor());
        }
});
  1. 使用Lambda表达式
    但匿名类还是不够好,它往往很笨重,占用很多空间;而且很多程序员觉得它用起来很费解。(<em><font color="brown">唉,处女座就是麻烦...</font></em>)
    在Java 8中,可以用Lambda表达式写成下面的样子:
List<Apple> result = filterApples(apples,(Apple apple) -> "red".equals(apple.getColor()));

Lambda表达式

Lambda表达式的基本语法是:

(parameters) -> expression

或(请注意语句的花括号)

(parameters) -> { statements; }

比如我们利用Lambda表达式比较两个苹果的重量:


3.png

这个Lambda表达式有三个部分:

什么时候可以使用Lambda

Lambda表达式是可以在<b>函数式接口</b>上使用的。<b>函数式接口</b>就是只定义一个抽象方法的接口。比如:

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

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

public interface Runnable{
    void run();
}

其实,Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并<em>把整个表达式作为函数式接口的实例</em>(确切来说,是函数式接口的一个具体实现的实例)。

Lambda表达式的具体使用

  1. 如果我们想要从一个文件中读取一行所需的内容,可以定义这样的方法:
public static String processFile() throws IOException {
    try (BufferedReader br = 
            new BufferedReader(new FileReader("data.txt"))){
        return br.readLine();
    }
}

那如果要读取两行呢,这时候我们就应该记起来行为参数化。

  1. 使用函数式接口来传递行为
    前面已经说过,Lambda仅可用于上下文是函数式接口的情况。我们需要创建一个匹配BufferedReader -> String,还可以抛出异常的接口。
public interface BufferedReaderProcessor {
    String process(BufferedReader br) throws IOException;
}

现在,我们可以把这个接口作为processFile方法的参数:

public static String processFile(BufferedReaderProcessor p) throws Exception{
        try (BufferedReader br = 
              new BufferedReader(new FileReader("data.txt"))){
            return p.process(br);
        }
}
  1. 传递Lambda
    现在我们就可以通过传递不同的Lambda重用processFile方法,以不同方式处理文件。
String oneLine = processFile((BufferedReader br) -> br.readLine());
String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());

几种函数式接口

Java 8中常用的函数式接口有三个:Predicate,Consumer,Function。这里我们简单介绍使用一下,具体使用有兴趣可以自己实践下。

Predicate

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

@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 s: list){ 
        if(p.test(s)){ 
            results.add(s); 
        } 
    } 
    return results; 
} 

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

Consumer

java.util.function.Consumer<T>接口定义了一个名叫accept的抽象方法,它接受泛型T,没有返回值(void)。如果需要访问类型T的对象,并对其执行某些操作,可以使用这个接口。
比如定义一个forEach方法,接受一个Integer类型的列表,并对每个元素执行打印操作。

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

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

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

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

// [6,3,6]
List<Integer> list = map(
        Arrays.asList("lambda","int","action"),
        (String s) -> s.length()
    );

Supplier

java.util.function.Supplier<T>接口定义了一个get的抽象方法,它没有参数,返回一个泛型T的对象,这类似于一个工厂方法。
比如返回一个Apple对象。

public interface Supplier<T> {
    T get();
}

public static <T> T getObject(Supplier<T> s) {
    return s.get();
}

Apple apple = getObject(() -> new Apple());

方法引用

我们上面写到的Lambda表达式是很方便的,但确实它们可以再简洁一点,比如根据苹果重量对集合进行排序,Lambda表达式是这样的:

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

使用<em><b> 方法引用 </b></em>和 java.util.Comparator.comparing 可以写成这样子:

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

基本格式

方法引用显式地指明调用的方法的名称,使代码的<b>可读性更好</b>。
当你想要使用方法引用时,目标引用放在分隔符 : : 前,方法名称放在后面:


4.png

下面给出了一些Java 8中方法引用的例子:


5.png

如何构建

方法引用主要有三类。

  1. 指向<em>静态方法</em>的方法引用(例如Integer 的 parseInt 方法)
Integer :: parseInt
  1. 指向<em>任意类型实例方法</em>的方法引用(例如String 的 length 方法)
String :: length  //实例为方法参数
  1. 指向<em>现有对象的实例方法</em>的方法引用(假设你有一个局部变量transaction,为Transaction类型,它支持实例方法getValue,就可以写成下面这样)
Transaction :: getValue  //实例为外部对象

第2钟和第3钟乍一看有点晕,其实第二种方法引用的思想就是你在引用一个对象的方法,这个对象本身是lambda的一个参数;第三种方法引用是你再调用一个已经存在的外部对象的方法。

构造函数引用

上面我展示了如何创建方法引用,其实我们也可以对类的构造函数做类似的事情。
我们可以利用 <b>ClassName :: new</b> 的形式构建一个构造函数的引用。假设一个构造函数没有参数,它试合Supplier的签名() -> Apple:

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

在使用方法引用之前它是这样的:

Supplier<Apple> supplier = () -> new Apple();
Apple apple = supplier.get();

如何你的构造函数是有参数的,比如签名是Apple(Integer weight),那么它就适合Function接口的签名:

Function<Integer,Apple> func = Apple :: new;
Apple apple = func.apply(100);

这就等价于:

Function<Integer,Apple> func = (weight) -> new Apple(weight);
Apple apple = func.apply(100);

Lambda和方法引用实战

接了下来我们继续研究前面的一个例子——按照苹果重量给Apple列表排序,我会展示从原始粗暴的状态到更加简明状态的过程,而且会用到前面提到的概念和功能:行为参数化、匿名类、Lambda表达式和方法引用。

行为参数化——传递代码

Java 8的API已经为我们提供了一个List可用的sort方法,我们可以直接使用。我们可以看下sort方法的签名:

void sort(Comparator<? super E> c)

它需要一个Comparator对象来比较两个Apple,所以第一个方案可以是这样的:

public class AppleComparator implements Comparator<Apple>{

    @Override
    public int compare(Apple a1, Apple a2) {
        return a1.getWeight().compareTo(a2.getWeight());
    }

}

apples.sort(new AppleComparator());

使用匿名类

我们可以使用匿名类来改进,而不是实现一个Comparator却只实例化一次:

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

使用Lambda表达式

使用匿名类的方案还挺啰嗦的,既然我们了解了Lambda表达式,它可以用更轻量级的语法来<em>传递代码</em>。我们需要记住这一点:<b>在需要函数式接口的地方可以使用Lambda表达式,抽象方法的签名描述了Lambda表达式的签名</b>。
Comparator接口抽象方法的签名是符合这种形式的——(T,T) -> int。所以我们改进后的方案是这样的:

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

其实,Java的编译器是可以根据Lambda出现的上下文来推断Lambda表达式参数的类型的:

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

Comparator接口其实有一个comparing的静态方法,可以接受一个Function,并返回一个Comparator对象。像下面这样:

Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight());

这时候我们的代码就可以更简洁一点了:

import static java.util.Comparator.comparing;

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

使用方法引用

最后,我们可以使用方法引用来完成最终解决方案:

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

总结

在了解了Lambda表达式的和方法引用的用法之后,你就可以自己去尝试用Lambda表达式去简化一些代码了(你可以自己去练习一下)。不过用于传递Lambda表达式的Comparator、Function、Predicate等函数式接口提供了允许你进行复合的方法。这意味着你可以把多个简单的Lambda复合成复杂的表达式。有兴趣的童鞋可以自己去了解下,这里不再详细讲解。

上一篇下一篇

猜你喜欢

热点阅读