Java8 -Lambda表达式(5)

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

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

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

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

(1).管中窥豹

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

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

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

  你可以把方法引用看作一个针对仅仅涉及单一方法的Lambda的语法糖,因为你表达同样的事情时要写的代码更加少了。

2.如何构建方法引用

  方法引用主要有三类:

  A.指向静态方法的方法引用(例如,Integer的parseInt方法,写作Integer::parseInt)。

  B.指向任意类型实例方法的方法引用(例如,String的length方法,写作String::length)。

  C.指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction 用来存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)。

  第二种和第三种方法引用可能咋看起来有点儿晕。类似于String::length的第二个方法引用的思想就是你在引用一个对象的方法,而这个对象本身是Lambda的一个参数。例如,Lambda表达式(String s) -> s.toUpperCase()可以写作String::toUpperCase。但是第三种方法引用值得是,你在Lambda表达式调用了一个已经存在的外部对象中的方法。例如,Lambda表达式() -> expersiveTransaction.getValue()可以写作expensiveTransaction::getValue。


Java8 -Lambda表达式(5)_第1张图片

  请注意,还有针对构造函数、数组构造函数和父类调用(super-call)的一些特殊形式的方法引用。让我们举一个方法引用的例子吧。例如,你想要对一个字符串的List排序,忽略大小写。List的sort方法需要一个Comparator作为参数。你在之前看到了,Comparator描述了一个具有(T, T) -> int签名的函数描述符。你可以利用String类的compareToIgnoreCase方法来定义一个Lambda表达式(注意:compareToIgnoreCase是String类中预先定义的)。

 List str = Arrays.asList("a", "b", "c", "d");
str.sort((s1, s2) -> s1.compareToIgnoreCase(s2));

  Lambda表达式的签名与Comparator的函数描述符兼容。利用前面所述的方法,这个例子可以用方法引用改写成下面这个样子

List str = Arrays.asList("b", "a", "d", "c");
str.sort(String::compareToIgnoreCase);

  注意,编译器会进行一种与Lambda表达式类似的类型检查过程,来确定对于给定的函数式接口,这个方法引用是否有效:方法引用的签名必须和上下文类型匹配
例如:

A.

先前的Lambda表达式:

Function stringToInteger  = (String s) -> Integer.parseInt(s);

现在的方法引用:

Function stringToInteger  = Integer::parseInt;
B.

先前的Lambda表达式:

BiPredicate, String> contains = (list, element) -> list.contains(element);

现在的方法引用:

BiPredicate, String> contains = List::contains;

  到现在,我们只展示了如何利用现有的方法实现和如何创建方法引用。但是你也可以对类的构造函数做类似的事情。

3.构造函数引用

  对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用:ClassName::new。它的功能与指向静态方法的引用类似。例如,假设有一个构造函数没有参数,那它比较适合Supplier的签名() -> Apple。你可以这样做:

Supplier  c1 = Apple::new; //构造函数的引用指向Apple类的默认构造函数
Apple a1 = c1.get(); //调用Supplier的get方法将产生一个新的Apple对象

  这就等价于:

 Supplier c1 = () -> new Apple(); // 利用默认构造函数创建Apple的Lambda表达式
Apple a1 = c1.get(); //调用Supplier的get方法将产生一个新的Apple

  也等价于:

Supplier c1 = new Supplier() {
            
            @Override
            public Apple get() {
                return new Apple();
            }
        };
        Apple a1 = c1.get();

  如果你的构造函数的签名是int -> Apple(Apple的构造函数为 public Apple(int weight)), 那么它适合Function接口的签名,于是你可以这样写:

 Function f = Apple::new; // 指向Apple(int weight)的构造函数引用
 Apple a1 = f.apply(100); //调用该Function函数的apply方法,并且给出了要求的质量,将产生一个Apple

  这就等价于:

Function f = (weight) -> new Apple(weight); // 用要求的重量创建一个Apple的Labda表达式
 Apple a1 = f.apply(100); //调用该Function函数的apply方法,并且给出要求的重量,将产生一个新的Apple对象。

  在下面的代码中,一个有Integer构成的List中的每个元素都通过我们前面定义的类似的map方法传递给Apple的构造函数,得到了一格具有不同质量苹果的List:

public class Demo7 {
    public static void main(String[] args) {
        List weights = Arrays.asList(70, 20, 100, 200);
        List apples = map(weights, Apple::new);
    }
    public static List map(List weights, Function  f){
        List result = new ArrayList<>();
        for(Integer i : weights){
            result.add(f.apply(i));
        }
        return result ;
    }
}

  如果你有一个具有两个参数的构造函数Apple(String color, Integer weight) ,那么它就适合BiFunction接口的签名,于是你就可以这样写:

BiFunction b = Apple::new;//指向Apple(String color, Integer weight)的构造函数引用
 Apple a1 = b.apply("green", 100); //调用该BiFunction函数的apply方法,并且给出要求的颜色和重量没奖产生一个新的Apple对象

  这就等价于:

BiFunction b = (color, weight) -> new Apple(color, weight);//用要求的颜色和重量创建一个Lambda表达式
 Apple a1 = b.apply("green", 100);//调用该BiFunction函数的apply方法,并且给出要求的颜色和重量,将产生一个新的Apple对象

  不将构造翻书实例化却能够引用它,这个功能有一些有趣的应用。例如,你可以使用Map来讲构造函数映射到某一个字符串值。你可以创建一个getMeFruit方法,给它key、name、weight,它就可以创建不同名字、不同重量的各种水果:

public class Demo8 {
    private static Map> map = new HashMap<>();
    static{
        map.put("apple", Fruit::new);
        map.put("orange", Fruit::new);
    }
    public static Fruit getMeFruit(String key, String name, Integer weight){
        return map.get(key).apply(name, weight);
    }
    public static void main(String[] args) {
        
    }
    
}

4.扩展

  你已经看到了如何将0个、1个、2个参数的构造参数转变为构造函数引用。那要怎么样才能对三个参数的构造函数,比如Color(int r, int g, int b),使用构造函数引用呢?
  构造函数引用的语法是ClassName::new,那么在这个例子里面就是Color::new。但是你需要与构造函数引用的签名匹配的函数式接口。但是语言本身没有并没有提供这样的函数式接口,你可以自己创建一个:

@FunctionalInterface
 public interface TriFunction {
    R apply(T t, U u, V v);
}

  现在你就可以向下面这样使构造函数引用了:

TriFunction f = Color::new;

5.Lambda和方法引用实践

  为了给我们之前学习的所有关于Lambda的内容,我们需要继续研究开始的那个问题--用不同的排序策略给一个Apple集合排序,并且需要展示如何把一个原始粗暴的解决方法变得更加的简明。这会用到迄今为止讲到的行为参数化、匿名类、Lambda表达式和方法引用。我们需要实现的最终解决方法是这样的:

List inventory = Arrays.asList( new Apple(100), new Apple(20), new Apple(150), new Apple(10));
 inventory.sort(Comparator.comparing(Apple::getWeight));

(1).传递代码

  在Java8的API已经为我们提供了List可用的sort方法,所以我们不用自己去实现它。那么最困难的部分搞定了!但是,如何把排序策略传递给sort方法呢?你看sort方法的原型是这样的:

void sort(Comparator  c);

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

public class Demo10 {
    public static void main(String[] args) {
        List inventory = Arrays.asList( new Apple(100), new Apple(20), new Apple(150), new Apple(10));
//        inventory.sort(Comparator.comparing(Apple::getWeight));
        inventory.sort(new AppleComparator());
    }
}
 class AppleComparator implements Comparator{

    @Override
    public int compare(Apple o1, Apple o2) {
        return o1.compareTo(o2);
    }
    
}

(2).使用匿名类

  在这之前,你可以使用匿名类来改进解决方案,而不是实现一个Comparator却只实例化一次:

public class Demo10 {
    public static void main(String[] args) {
        List inventory = Arrays.asList( new Apple(100), new Apple(20), new Apple(150), new Apple(10));
//        inventory.sort(Comparator.comparing(Apple::getWeight));
        inventory.sort(new Comparator() {

            @Override
            public int compare(Apple o1, Apple o2) {
                return o1.compareTo(o2);
            }
        });
    }
}

(3).使用Lambda表达式

  尽管使用匿名类,但是代码还是非常的啰嗦。Java8引入了Lambda表达式,它提供了一种轻量级语法来实现相同的目标:传递代码。你看到了,在需要函数式接口的地方可以使用Lambda表达式。我们回顾一下:函数式接口就是仅仅定义了一个抽象方法的接口。抽象方法的签名(称为函数描述符)描述了Lambda表达式的签名。在这例子中,Comparator代表了函数描述符(T, T) -> int。因为你用的是苹果,它代表的是(Apple, Apple) -> int. 改进后的新解决方法看上去就是这样的

public class Demo10 {
    public static void main(String[] args) {
        List inventory = Arrays.asList( new Apple(100), new Apple(20), new Apple(150), new Apple(10));
//        inventory.sort(Comparator.comparing(Apple::getWeight));
        inventory.sort(( Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
    }
}

  我们前面解释了,Java编译器可以根据Lambda出现的上下文来推断Lambda表达式参数的类型。那么你的解决方法就可以重写这样:

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

  你的代码还能变得更加易读一点吗?Comparator具有一个叫做comparing的静态辅助方法,它可以接收一个Function来提取Comparable键值,并且生成Comparator对象。它就可以像下面这样用(注意你用的Lambda只有一个参数:Lambda说明了如何从苹果中提取需要比较的键值):

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

  现在你可以把代码再改得紧凑一点:

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

(4).使用方法引用

  方法引用就是替代那些转发参数Lambda表达式的语法糖。你可以使用方法引用让你的代码更加简洁

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

你可能感兴趣的:(Java8 -Lambda表达式(5))