《Java 8 实战》Ch3: Lambda表达式(下):类型与限制、方法引用、复合

李文轩 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.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • 前段时间一直在看lambda表达式,但是总感觉吃不透,在深入了解lambda表达式的时候,需要很多基础的知识栈。这...
    西瓜真好吃丶阅读 2,722评论 0 7
  • 简介 概念 Lambda 表达式可以理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主...
    刘涤生阅读 3,198评论 5 18
  • 声明:本文翻译自The Java™ Tutorials(官方文档) 简述 匿名类有一个问题,如果匿名类的实现非常简...
    猴子小皮球阅读 4,641评论 0 9
  • Lambda表达式 利用行为参数化这个概念,就可以编写更为灵活且可重复使用的代码。但同时,使用匿名类来表示不同的行...
    谢随安阅读 867评论 2 0
  • 注:之前关于Java8的认知一直停留在知道有哪些修改和新的API上,对Lambda的认识也是仅仅限于对匿名内部类的...
    mualex阅读 2,820评论 1 4