java8特性lambda基本原理及性能分析

Java8发布,Lambda表达式作为一项重要的特性随之而来。或许现在你已经在使用Lambda表达式来书写简洁灵活的代码。
      Lambda 表达式是一种匿名函数(对 Java 而言这并不完全正确,但现在姑且这么认为),简单地说,它是没有声明的方法,也即没有访问修饰符、返回值声明和名字。
      你可以将其想做一种速记,在你需要使用某个方法的地方写上它。当某个方法只使用一次,而且定义很简短,使用这种速记替代之尤其有效,这样,你就不必在类中费力写声明与方法了。


函数式接口

      函数式接口(functional interface)。简单来说,函数式接口是只包含一个方法的接口。比如Java标准库中的java.lang.Runnable和java.util.Comparator都是典型的函数式接口。
      java 8提供 @FunctionalInterface作为注解,这个注解是非必须的,只要接口符合函数式接口的标准(即只包含一个方法的接口),
      虚拟机会自动判断,但 最好在接口上使用注解@FunctionalInterface进行声明,以免团队的其他人员错误地往接口中添加新的方法。
Java中的lambda无法单独出现,它需要一个函数式接口来盛放,lambda表达式方法体其实就是函数接口的实现,

 

包含三个部分
      一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数
      一个箭头符号:->
      方法体,可以是表达式和代码块,方法体函数式接口里面方法的实现,如果是代码块,则必须用{}来包裹起来,且需要一个return 返回值,但有个例外,若函数式接口里面方法返回值是void,则无需{}
      (parameters) -> expression 或者 (parameters) -> { statements; }
      我们看个线程的demo


Lambda语法

[Java] 纯文本查看 复制代码
1
2
3
4
5
6
包含三个部分
一个括号内用逗号分隔的形式参数,参数是函数式接口里面方法的参数
一个箭头符号:->
方法体,可以是表达式和代码块,方法体函数式接口里面方法的实现,如果是代码块,则必须用{}来包裹起来,且需要一个return 返回值,但有个例外,若函数式接口里面方法返回值是void,则无需{}
(parameters) -> expression 或者 (parameters) -> { statements; }
我们看个线程的demo



Lambda原理

      以上是通过不同的方法来实现线程的,那么我们来看下JVM编译执行过程的原理是不是一样的?
      通过 javap -c TestLambda查看字节码
java8特性lambda基本原理及性能分析
      通过上面的对比发现红色部分不太一样Lambda是InvokeDynamic 内部类是 invokespecial,那么是什么意思呢?
      我们先温习下jvm指令
           invokeinterface:调用接口方法;
           invokespecial:专门用来调用父类方法、私有方法和初始化方法;
           invokestatic:调用静态方法;
           invokevirtual:调用对象的一般方法。
       这四个指令所对应的类、调用的方法在编译时几乎是固定的:invokestatic所对应的类为静态方法所在的类,方法为静态方法本身;invokespecial所对应的类为当前对象,方法是固定的;invokeinterface和invokevirtual所对应的类也为当前对象,方法可以因为继承和实现进行选择,但也仅限于整个继承体系中选择。
      在java7 JVM中增加了一个新的指令invokedynamic,用于支持动态语言,即允许方法调用可以在运行时指定类和方法,不必在编译的时候确定。
      字节码中每条invokedynamic指令出现的位置称为一个动态调用点,invokedynamic指令后面会跟一个指向常量池的调用点限定符,这个限定符会被解析为一个动态调用点。
       我们发现Lambda采用的是invokedynamic指令,所以Lambda还是有别于普通方式的调用的。
       我们在来看下List中Lambda的使用。
代码
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class TestLambdaList {
    public static void main(String[] args) {
        List<User> userList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            User user = new User();
            user.setId(i);
            user.setUserName("un" + i);
            user.setPassword("password" + i);
            userList.add(user);
        }
        // 通过Lambda取出User里面id的值
        List<Integer> idList_LB = userList.stream().map(user -> user.getId()).collect(Collectors.toList());
        System.out.println(idList_LB);
        // 普通方法
        List<Integer> idList_PT = new ArrayList<>();
        for (User user : userList) {
            idList_PT.add(user.getId());
        }
        System.out.println(idList_PT);
    }
}
class User {
    Integer id;
    String  userName;
    String  password;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}
      以上是通过两种不同的方式实现取(出User里面id的值),很明显使用Lambda要简洁很多。


Lambda性能如何?

      有许许多多关于 Java 8 中流效率的讨论,但根据 Alex Zhitnitsky 的测试结果显示:坚持使用传统的 Java 编程风格——iterator 和 for-each 循环——比 Java 8 的实现性能更佳。他们测出的结果是如下图
java8特性lambda基本原理及性能分析
他们的结论:Java 8 中提供的任何一种新方式都会产生约 5 倍的性能差异。有时使用简单迭代器循环比混合 lambda 表达式和流更有效,即便这样需要多写几行代码,且需要跳过甜蜜的语法糖(syntactic suger)。
使用迭代器或 for-each 循环是遍历 ArrayList 最有效的方式,性能比采用索引值的传统 for 循环方式好两倍。
在 Java 8 的方法中,并行流的性能最佳。但是请小心,在某些情况下它也可能会导致程序运行得更慢。

 

我们自己来亲自测试下到底怎么样
将上面的代码添加个时间看下耗时情况
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class TestLambdaList {
    public static void main(String[] args) {
        List<User> userList = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            User user = new User();
            user.setId(i);
            user.setUserName("un" + i);
            user.setPassword("password" + i);
            userList.add(user);
        }
        // 通过Lambda取出User里面id的值
        long t1=System.currentTimeMillis();
        List<Integer> idList_LB = userList.stream().map(user -> user.getId()).collect(Collectors.toList());
        //System.out.println(idList_LB);
        System.out.println(System.currentTimeMillis()-t1);
        // 普通方法
        long t2=System.currentTimeMillis();
        List<Integer> idList_PT = new ArrayList<>();
        for (User user : userList) {
            idList_PT.add(user.getId());
        }
        System.out.println(System.currentTimeMillis()-t2);
       // System.out.println(idList_PT);
    }
}
class User {
    Integer id;
    String  userName;
    String  password;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}
[Java] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
测试1:在一个Main方法里面执行完
i=100     多次测试的结果是:Lambda耗时在120左右。但是foreach每次的结果都是0
i=100000   多次测试的结果是:Lambda耗时在120左右。但是foreach每次的结果都是10秒以内;
i=1000000  多次测试的结果是:Lambda耗时在120左右。但是foreach每次的结果都是10秒以内; 
i=10000000  多次测试的结果是:Lambda耗时在120左右。但是foreach每次的结果都是130秒以内;
 
测试2:分别在一个Main方法里面执行完各自执行,(将其中的一个注释掉)
i=100     多次测试的结果是:Lambda耗时在100左右。但是foreach每次的结果都是0
i=100000   多次测试的结果是:Lambda耗时在100-120左右。但是foreach每次的结果都是12-16秒以内;
i=1000000 多次测试的结果是:Lambda耗时在120180左右。但是foreach每次的结果都是30-50秒以内; 
i=10000000  多次测试的结果是:Lambda耗时在120180左右。但是foreach每次的结果都是40~60秒以内;

 

由此可见:使用简单迭代器循环比混合 lambda 表达式和流更有效.
总结:
      开始使用 Java 8 的第一件事情是在实践中使用 lambda 表达式和流。但是请记住:它确实非常好,好到可能会让你上瘾!但是,我们也看到了,使用传统迭代器和 for-each 循环的 Java 编程风格比 Java 8 中的新方式性能高很多。
      当然,这也不是绝对的。但这确实是一个相当常见的例子,它显示可能会有大约 5 倍的性能差距。如果这影响到系统的核心功能或成为系统一个新的瓶颈,那就相当可怕了。