黑马程序员技术交流社区

标题: 【济南中心】解读JDK1.8新特性之Lambda表达式与Functional接口 [打印本页]

作者: 大山哥哥    时间: 2016-7-29 20:43
标题: 【济南中心】解读JDK1.8新特性之Lambda表达式与Functional接口
本帖最后由 大山哥哥 于 2016-7-29 20:42 编辑

    大家的安卓基础班课程是基于jdk7的,但是在每个学生开班入学的时候电脑上几乎都是安装的jdk8,因为大家总觉得新版的就是最好的,但其实这种认识是错误的。一门技术从出现到成熟再到普遍使用是要经历很长时间的,比如jdk6是在2006年12月12日发布的,而它真正的被在开发当中广泛被采用也是近几年的事情了,因为毕竟刚刚出现的技术可能会存在很多的bug以及不兼容因素,需要经历一段时间去发现和解决,最终达到一个成熟的版本而被广大技术人员所应用。同样的道理,jdk8也是这样的,但是jdk8早晚会有被广泛应用到实际项目中去的一天,所以接下来我就为大家来讲解一下jdk8中最大的卖点 - Lambda表达式。
    Lamda是一种基于函数的编程语言(也就是咱们课上讲面想对象时候说的面向过程语言),对于这种类型的语言典型的代表就是Haskell,但是Java是面向对象编程语言,一切的操作都是必须有类,所有的功能都必须定义在类之中,所以这就造成了一点弊端,也就是很多习惯于函数编程的开发者就觉得Java不好用,但也并不是说Java中没有基于函数编程的语法体现,只不过稍微麻烦些,那么他的实现模式就是匿名内部类(就是咱们说的接口的匿名的子类对象,可以不用创建类,就能创建出一个接口的子类对象),于是在Java诞生20年之后,Java终于推出了Lamda表达式,其最要的目的是解决匿名内部类的书写麻烦问题,而且由于能够在纯Java语言环境中提供一种优雅的方式来支持函数式编程,也使它具有吸引越来越多程序员到Java平台上的潜力。
    首先我们在讲解Lamda表达式之前,先来回顾一下我们的匿名内部类如何定义?请看下面的代码。   
[Java] 纯文本查看 复制代码
package com.heima.lamda;
interface Inter{                                 //如果使用匿名内部类必须先存在一个接口
        public void print(String str);    //使用匿名内部类实现的接口,一般里面只有一个方法
}
public class Demo {
        public static void main(String[] args) {
                Inter it = new Inter() {           //使用匿名内部类,让父类的引用指向子类对象
                        public void print(String str) {    //重写接口中的方法
                                System.out.println(str);
                        }
                };  
                it.print("helloWorld");            //父类的引用调用子类重写后的方法
        }
}
以上代码就是在jdk1.8之前我们写匿名内部类的代码,其实上面的代码我们真正需要的就是一个输出语句,却写了这么一大堆的代码。 而且这也是我们写的最简单的代码了,因为要想利用一个接口里面的方法去打印一个HelloWorld,只能先创建一个类实现接口并重写其中的方法,然后创建该类对象,从而调用方法进行打印,这时候匿名内部类就是我们能写出的最精简的代码了,这是由于java之中类结构的强制限制,所以很多人就觉得代码过于麻烦了。但是Lambda表达式的出现很好的解决了这一问题,那么下面我们就用Lambda表达式来演示上面的案例:
[Java] 纯文本查看 复制代码
package com.heima.lamda;
interface Inter{                                      //Lambda表达式只试用与实现接口的匿名内部类
        public void print(String str);        //Lambda表达式要求接口里面必须必须只能有一个抽象方法
}
public class Demo {
        public static void main(String[] args) {
                Inter it = (s) -> System.out.println(s);//Lambda表达式 ,s就是接口里的方法参数              
                it.print("helloWorld");               //父类的引用调用子类重写后的方法
        }
}
       首先,先不看语法,至少通过上面的程序我们发现,使用了Lambda表达式,这个语句少了,更简单了,没有了类结构的控制。      那么上面整个实现的Lambda表达式语句是:  (s) -> System.out.println(s);    Lambda表达式的结构是:   (参数)  ->  函数体;
           (String s) -> System.out.println(s);          
  1.  在某些情况下lambda的函数体会更加复杂,这时可以把函数体放到在一对花括号中,就像在Java中定义普通函数一样。例如:
                                     (s) -> {
                                           System.out.println(s);
                                           System.out.println(s+"HelloWorld");
                                     };                               
  2.  Lambda表达式中的函数体也就是匿名内部类中的方法体, 所以在Lambda表达式的方法体中使用局部变量,局部变量前面也必须加final修饰,而再jdk8中如果这些变量不是final的话,它们会被隐含的转为final,这样效率更高
  3.  有时候接口中的抽象方法会有返回值,Lambda也就会返回一个值。返回值的类型也是由编译器推测出来的。如果lambda的函数体只有一行的话,那么没有必要显式使用return语句。下面两个代码片段是等价的:
[Java] 纯文本查看 复制代码
package com.heima.lamda;
interface Inter{                                                               
        public String getString(String str);                                
}
public class Demo {
        public static void main(String[] args) {
                Inter it = (s) -> s;               
                String str = it.getString("hello");
                System.out.println(str);    //打印出 hello
        }
}
上面的代码和下面的代码是等价的
[Java] 纯文本查看 复制代码
package com.heima.lamda;
interface Inter{                                                               
        public String getString(String str);                                
}
public class Demo {
        public static void main(String[] args) {
                Inter it = new Inter() {               
                        public String getString(String str) {        
                                return str;
                        }
                };               
                String str = it.getString("hello");
                System.out.println(str);    //打印出 hello
        }
}
   4.   Lambda表达式只适用于 接口的匿名内部类,而且接口中必须有且只有一个抽象方法

       我们了解到Lambda表达式只适用于接口中有且只有一个抽象方法的情况,那如果再开发过程中有开发者不小心给接口添加了多个抽象方法,那么就不再支持Lambda了,语言设计者投入了大量精力来思考如何使现有的函数友好地支持lambda。最终采取的方法是:增加函数式接口的概念。函数式接口就是一个具有一个方法的普通接口。像这样的接口,可以被隐式转换为lambda表达式。java.lang.Runnable与java.util.concurrent.Callable是函数式接口最典型的两个例子。在实际使用过程中,函数式接口是容易出错的:如有某个人在接口定义中增加了另一个方法,这时,这个接口就不再是函数式的了,并且编译过程也会失败。为了克服函数式接口的这种脆弱性并且能够明确声明接口作为函数式接口的意图,Java 8增加了一种特殊的注解@FunctionalInterface(Java 8中所有类库的已有接口都添加了@FunctionalInterface注解)。让我们看一下这种函数式接口的定义
[Java] 纯文本查看 复制代码
package com.heima.lamda;
@FunctionalInterface
interface Inter{                                                               
        public String getString(String str);        
}
把这种注解写在接口上面,当开发人员想继续写抽象方法的时候,Eclipse就会报红色波浪线提示编译失败,这和@Override注解有些类似,下面我们也举例说明@Override注解:
[Java] 纯文本查看 复制代码
interface Inter{                                                                
        public String getString(String str);        
}
public class Demo implements Inter{
        @Override
        public String getString(String str) {
                return str;
        }        
}
将@Override注解写在重写的方法的上面,如果你在书写方法声明的时候有一点和被重写的方法不一样的地方,Eclipse也会立刻报出红色波浪线提示编译失败。
毫无疑问,jdk8发行版是自jdk5(发行于2004,已经过了相当一段时间了)以来最具革命性的版本。jdk8为Java语言、编译器、类库、开发工具与JVM(Java虚拟机)带来了大量新特性。在这篇教程中,我只介绍了jdk8最大的亮点Lambda表达式,以及为Lambda表达式专门设计的函数式接口。希望对大家有所帮助,如果大家对这篇教程感兴趣的话,敬请阅读大山哥哥的下一篇教程吧~











作者: cat73    时间: 2016-7-29 21:29
本帖最后由 cat73 于 2016-7-29 21:53 编辑

抢沙发0.0
已报名济南 JavaEE 基础班0.0

其实例子里的 print 还可以写的更简单一些,局部变量可以省略,参数类型的括号也可以省略。
[Java] 纯文本查看 复制代码
((Inter) s -> System.out.println(s)).print("helloWorld");


附上 Oracle 的 Java 规范中对 Lambda 表达式的说明供大家参考:
https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.27



作者: xushengbin    时间: 2016-7-29 21:36
给力
作者: zhaosenyang    时间: 2016-7-29 21:40
赞一个^_^
作者: 职业规划—甄苗苗老师    时间: 2016-7-29 21:57
大贤哥就是如此给力!已收藏,期待续集。。。
作者: 大山哥哥    时间: 2016-7-29 22:01
cat73 发表于 2016-7-29 21:29
抢沙发0.0
已报名济南 JavaEE 基础班0.0

谢谢哈~。这样写的话 ((Inter) s -> System.out.println(s)).print("helloWorld");,只要是怕学生们看不太懂,毕竟这只是针对没有学完或者刚刚学完java基础的同学写的,我想尽可能的易懂一些,谢谢您附上的链接~也替学员们谢谢您~
作者: 大山哥哥    时间: 2016-7-29 22:01
cat73 发表于 2016-7-29 21:29
抢沙发0.0
已报名济南 JavaEE 基础班0.0

谢谢哈~。这样写的话 ((Inter) s -> System.out.println(s)).print("helloWorld");,只要是怕学生们看不太懂,毕竟这只是针对没有学完或者刚刚学完java基础的同学写的,我想尽可能的易懂一些,谢谢您附上的链接~也替学员们谢谢您~
作者: cat73    时间: 2016-7-29 22:04
大山哥哥 发表于 2016-7-29 22:01
谢谢哈~。这样写的话 ((Inter) s -> System.out.println(s)).print("helloWorld");,只要是怕学生们看不 ...

其实一直想去研究下 Java8 的Lambda 表达式怎么用 0.0
然而一直就没去。。。
借着这个贴做引子跑去看了看 Java 规范 0.0
然后就把这个学惹 0.0

讲的挺容易懂的 0.0
成功把窝忽悠入坑惹 0.0


作者: cat73    时间: 2016-7-29 22:42
本帖最后由 cat73 于 2016-7-29 23:21 编辑
大山哥哥 发表于 2016-7-29 22:01
谢谢哈~。这样写的话 ((Inter) s -> System.out.println(s)).print("helloWorld");,只要是怕学生们看不 ...

可以顺便写写 :: 用法的说明咩 0.0
感觉不太能理解这种用法 0.0

可以看到下面的测试代码里,test 方法的第一个参数要的是 Print,然而我传进去的东西跟 Print 接口一点关系都没有。
然而结果为,编译正常,运行正常,成功输出了 test。
要非得说有什么相同点,那大概就是参数完全一致了。
[Java] 纯文本查看 复制代码
public class LambdaTest {
    public static void main(String[] args) {
        test(PrintObj::renamePrint, "test");
    }
   
    public static void test(Print print, String s) {
        print.print(s);
    }
   
    public static class PrintObj {
        public static void renamePrint(String str) {
            System.out.println(str);
        }
    }

    @FunctionalInterface
    public static interface Print {
        void print(String str);
    }
}

=========== 更新 ===========
看 javap 的结果,似乎是跟 Lambda 表达式的编译结果是一样的:
[Java] 纯文本查看 复制代码
invokedynamic #2,  0

它似乎是用这个静态方法的方法体去构造了一个 Lambda 表达式。

我连续调用了两次 test(PrintObj::renamePrint, "test"),并在 test 里增加了输出 print 的 hashCode 的代码,发现两次调用的输出是不同的,也就是说它构造了两个对象。

相关规范似乎在这里:https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.13
困了。。明个看看吧。。。

作者: 职业规划-王雪老师    时间: 2016-7-30 08:43
大赞,给力!
作者: 小新思密达    时间: 2016-7-30 08:45
太棒了,对于俺这样的小白真的是福音
作者: 小新思密达    时间: 2016-7-30 08:49
cat73 发表于 2016-7-29 22:42
可以顺便写写 :: 用法的说明咩 0.0
感觉不太能理解这种用法 0.0

你发的有点不懂。。。你这么牛为啥还要报基础班,技术分都这么高了
作者: 该用户名已注册    时间: 2016-7-30 08:57
这么好的知识点,我们保存一下吧
作者: 清风幕竹    时间: 2016-7-30 09:02
哎呦。不错哦!
作者: 李情情老师    时间: 2016-7-30 09:34
棒棒哒。。。。大赞
作者: josecheng    时间: 2016-7-30 10:00
楼主好人
作者: 微--尘    时间: 2016-7-30 22:39
好好好好好
作者: 流光    时间: 2016-7-31 18:09
贤哥666666
作者: 斧王就是我    时间: 2016-7-31 18:59
顶一下,大山哥哥,加油吧
作者: Frank_Ms1ZR    时间: 2016-7-31 19:31
马克一下
作者: 橘子哥    时间: 2016-8-1 09:50
山哥威武!9楼老司机的问题不明觉厉
作者: 刘小白    时间: 2016-8-1 13:22
给力
作者: 自渡。    时间: 2016-8-1 19:30
酱油一下
作者: 奥特珞珞猫咪喵    时间: 2016-8-3 16:51
学什么都一样,努力就会有付出的!
作者: 皇亚杰    时间: 2016-8-5 22:55
我是来点赞的
作者: li1991    时间: 2016-8-9 11:39
6的不行
作者: 小东姐姐    时间: 2016-8-10 11:07
赞赞赞,毕竟是“山东”搭  老师们在马不停蹄的研究技术,学员们才能有安全感,我们愿意不断向上
作者: 阿弥陀佛么么哒    时间: 2016-8-12 09:39
1024个赞
作者: 小帆帆    时间: 2016-8-14 08:58
cat73 发表于 2016-7-29 22:42
可以顺便写写 :: 用法的说明咩 0.0
感觉不太能理解这种用法 0.0


public class Demo {
    public static void main(String[] args) {
        test(new Print() {
            @Override
            public void print(String str) {
                PrintObj.renamePrint(str);
            }
        }, "test");
    }

    public static void test(Print print, String s) {
        print.print(s);
    }

    public static class PrintObj {
        public static void renamePrint(String str) {
            System.out.println(str);
        }
    }

    public static interface Print {
        void print(String str);
    }
}
作者: 小帆帆    时间: 2016-8-14 09:00
cat73 发表于 2016-7-29 22:42
可以顺便写写 :: 用法的说明咩 0.0
感觉不太能理解这种用法 0.0

楼上的代码等价于
    public static void main(String[] args) {
        test(str -> PrintObj.renamePrint(str), "test");
    }
然后使用方法引用就成了
    public static void main(String[] args) {
        test(PrintObj::renamePrint, "test");
    }
作者: wain    时间: 2016-11-8 22:47
贤哥威武!{:8_507:}




欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2