美文网首页
《Java 8 实战》Ch3: Lambda表达式(下):类型与

《Java 8 实战》Ch3: Lambda表达式(下):类型与

作者: WenxuanLi | 来源:发表于2019-04-24 11:26 被阅读0次

    李文轩 2019-04-23


    3.5 类型的检查和判断;变量捕获限制

    类型检查

    • Lambda的类型是从使用Lambda的上下文推断出来的。

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

    • Lambda表达式的类型检查的过程

        //Main
        List<Apple> heavierThan150g = filter(inventory, (Apple a) -> a.getWeight() > 150);
        
        //Lambda表达式的类型检查的过程
        //1. 找到filter方法的声明 -> filter(List<Apple> inventory, Predicate<Apple> p)
        //2. Lambda所在的第二个参数为 Predicate<Apple>,T绑定了Apple
        //3. Predicate<Apple> 是一个函数式接口,包含了一个test的抽象方法
        //4. test的函数描述符为 接受一个Apple对象,返回一个boolean值
        //5. 查看Lambda的签名和(4)的描述符是否一致,
    

    类型推断

    • 在Java 8的Lambda表达式中,如果没有加入参数的显式类型,Java编译器可以从上下文推断出用什么函数式接口来配合Lambda表达式,即推断出Lambda表达式的签名。
        //Lambda表达式的参数没有写出显式类型
        List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor());
        
        //写出了显式类型,编译器没有进行类型推断
        Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
        
        //没有写出显式类型,编译器将进行类型推断
        Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
    
    • 有时候写出显式类型会更便于阅读,有时候隐去显式类型代码看上去更加直接和简洁。

    Lambda使用局部变量

    • Lambda表达式与匿名类一样,是允许使用自由变量的,即在外层作用域中定义的变量
        //以下代码是有效的
        int portNumber = 1337;
        Runnable r = () -> System.out.println(portNumber);
    
    • 对于实例变量和静态变量,Lambda表达式可以没有限制的使用
    • 但是对域局部变量,局部变量必须显式声明为 final或者事实上是 final,即 Lambda表达式只能捕获指派给它们的局部变量一次。
        //以下代码是无效的
        int portNumber = 1337;
        Runnable r = () -> System.out.println(portNumber);
        portNumber = 31337;
    
    • 对局部变量的限制的原因
      • 实例变量和局部变量背后的实现的不同
      • 实例变量都存储在堆中,局部变量都存储在栈上
      • 如果Lambda是在一个线程中使用,使用此线程时,在分配该变量的线程将这个变量收回之后,去访问该变量。(这样可能会发生内存读取的错误)
      • Java在访问自由局部变量时,实际上是访问变量的副本,而不是访问原始变量。
      • 因为是访问副本,所以需要用 final来限制变化,或者隐形的 final即只赋值一次。

    3.6 方法引用

    • 方法引用是根据已有的方法实现来创建Lambda表达式
    • 用方法引用(显式地指明方法的名称)会让代码更具有可读性。
    • 具体操作:目标引用放在 ::前,方法名称放在后面
    • 一些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
    

    方法引用的三种类型

    1. 指向静态方法的方法引用
      • 比如 Integer 的 parseInt 方法,可以写作 Integer::parseInt
      //Lambda
      (args) -> ClassName.staticMethod(args)
      //方法引用
      ClassName::staticMethod
    
    1. 指向任意类型实例方法的方法引用
      • 比如 String 的 length 方法,可以写作 String::length
      //Lambda
      (arg0, rest) -> arg0.instanceMethod(rest)
      //方法引用
      ClassName::instanceMethod
    
    1. 指向现有对象的实例方法的方法引用
      • 比如你有一个局部变量 greenApple 用于存放 Apple 类型的对象,它支持实例方法 getColor,那么可以写作 greenApple::getColor
      //Lambda
      (args) -> expr.instanceMethod(args)
      //方法引用
      expr::instanceMethod
    

    ** 第二和第三的区别在于,第二个类型的方法引用是传递一个实例,这个实例会作为Lambda的参数;第三个方法则是在Lambda表达式外的已存在的对象。

    ** 一开始会有一些懵,可能因为是在方法引用里看不到函数所用的参数。实际上,因为是用已定义的方法来代替Lambda表达式传递到其他方法中,不需要再在这里显式参数;编译器会在运行时,自动根据签名来检查。函数的参数传递则在调用函数式接口的方法时,比如说 Predicate 的 test,参数通过test传递。

    构造函数引用

    • 实际上和指向静态方法的引用类似,用类型的名称和 new关键字; Classname::new
        //若构造函数没有参数,Supplier的签名就适用;() -> T
        
        //这个引用指向默认的Apple构造函数
        Supplier<Apple> c1 = Apple::new
        Apple a1 = c1.get();
        
        //等价的Lambda表达式
        Supplier<Apple> c1 = () -> new Apple();
        Apple a1 = c1.get();
    
        //若构造函数有参数,Function的签名就适用;T -> R
        
        //这个引用指向Apple(Integer weight)的构造函数
        Function<Integer, Apple> c2 = Apple::new;
        Apple a2 = c2.apply(110);
        
        //等价的Lambda表达式
        Function<Integer, Apple> c2 = (weight) -> new Apple(weight);
        Apple a2 = c2.apply(110);
    

    3.8 复合Lambda表达式

    谓词复合

    • Predicate 接口包括三个方法: negateandor,都返回一个Predicate的实例;这些方法能让已有的Predicate创建更加复杂的谓词。
    • 需要注意的是andor方法是按照表达式链中的位置,从左向右确定优先级
      • a.or(b).and(c)等价于 (a || b) && c
    //用negate返回一个Predicate的非,比如苹果不是红的
    Predicate<Apple> notRedApple = redApple.negate();
            
    //用and串联两个方法,用来得到又红又大的苹果
    Predicate<Apple> redAndHeavyApple = redApple.and( a -> a.getWeight() > 150);
            
    //用or串联多个方法,等到结果要么是又红又大的苹果要么是绿苹果
    Predicate<Apple> redAndHeavyApple = 
    redApple.and( a -> a.getWeight() > 150)
            .or(a -> "green".equals(a.getColor()));
    

    函数复合

    • Function 接口包括两个方法:andThencompose,都返回一个Function的实例。

    • andThen方法会先把输入应用到一个给定函数,再将这个输出应用到另一个函数。

    Function<Integer, Integer> f = x -> x + 1;
    Function<Integer, Integer> g = x -> x * 2;
    Function<Integer, Integer> h = f.andThen(g); //数学上写作 g(f(x))
    
    int result = h.apply(1);
    //结果为4
    
    • compose方法会先把给定函数用作compose的参数里给的那个函数,然后在把函数本身用于结果。
    Function<Integer, Integer> f = x -> x + 1;
    Function<Integer, Integer> g = x -> x * 2;
    Function<Integer, Integer> h = f.compose(g); //数学上写作 f(g(x))
            
    int result = h.apply(1);
    //结果为3
    
    • compose方法可以用于一些流水线中
    public class Letter{
        public static String addHeader(String text){
            return "From xxx : " + text;
        }
            
        public static String add addFooter(String text){
            return text + " Kind regards";
        }
            
        public static String checkSpelling(String text){
            return text.replaceAll("labda", "lambda");
        }
    }
            
    //Main
    //创建一个流水线,给信加上抬头,检查拼写,再加上落款
    Function<String, String> addHeader = Letter::addHeader; 
    Function<String, String> transformationPipeline
        = addHeader.andThen(Letter::checkSpelling).andThen(Letter::addFooter);
    

    Reference:

    [1] Fusco, Mario, and Alan Mycroft. Java 8 in Action: Lambdas, Streams, and Functional-Style Programming. Manning, 2015.

    相关文章

      网友评论

          本文标题:《Java 8 实战》Ch3: Lambda表达式(下):类型与

          本文链接:https://www.haomeiwen.com/subject/tebwgqtx.html