Java8 Lambdas 与匿名类

125

由于Java8最近发布,它的全新lambda表达式看起来非常酷,我想知道这是否意味着我们如此习惯的匿名类将会消失。

我进行了一些研究,发现Lambda表达式将系统地替换掉那些类,例如Collection的sort方法,该方法以前使用匿名Comparator实例执行排序:

Collections.sort(personList, new Comparator<Person>(){
  public int compare(Person p1, Person p2){
    return p1.firstName.compareTo(p2.firstName);
  }
});

现在可以使用Lambda来完成:

Collections.sort(personList, (Person p1, Person p2) -> p1.firstName.compareTo(p2.firstName));

看起来非常简洁。所以我的问题是,在Java8中是否有任何理由继续使用那些类而不是使用Lambda表达式?

编辑

同样的问题,但是相反的方向,使用Lambda表达式的好处是什么,因为Lambda表达式只能与单个方法接口一起使用,这个新特性只是在少数情况下使用的快捷方式还是真正有用的东西?


5
好的,针对所有提供具有副作用方法的匿名类。 - tobias_k
12
请注意,你也可以使用以下方式构建比较器:Comparator.comparing(Person::getFirstName),如果getFirstName()是一个返回firstName的方法。 - skiwi
1
或者匿名类具有多个方法,或者... - Mark Rotteveel
1
我倾向于投票关闭问题,因为在编辑之后还有其他的问题,这使得问题过于宽泛。 - Mark Rotteveel
1
这个主题的一篇深入文章:https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood - Ram Patra
显示剩余2条评论
9个回答

119

匿名内部类(AIC)可用于创建抽象类或具体类的子类。 AIC还可以提供接口的具体实现,包括状态(字段)的添加。可以使用this在其方法体中引用AIC的实例,因此可以调用进一步的方法,随时间变异其状态等。这些都不适用于lambda表达式。

我猜大多数使用AIC的用途是提供无状态实现单个函数,因此可以用lambda表达式替换它们,但是还有其他用途的AIC,不能用lambda表达式实现。 AIC将继续存在。

更新

AIC和lambda表达式之间的另一个区别是AIC引入了新的作用域。也就是说,名称是从AIC的超类和接口中解析的,并且可以遮蔽在词法上封闭的环境中出现的名称。对于lambda表达式,所有名称都是按词法解析的。


3
Lambda表达式可以拥有状态。在这方面,我认为Lambda表达式和AIC没有区别。 - nosid
2
@nosid AICs,就像任何类的实例一样,可以在字段中保存状态,并且该状态可由类的任何方法访问(并且可能是可变的)。此状态存在直到对象被GC'd,即它具有无限的范围,因此它可以跨越方法调用持久存在。Lambda具有无限范围的唯一状态是在遇到lambda时捕获的;此状态是不可变的。 Lambda内部的局部变量是可变的,但它们仅在lambda调用正在进行时存在。 - Stuart Marks
@StuartMarks:Lambda表达式中的变量是(有效的)_final_。但是,所引用的对象是可变的。关于_functional interfaces_和_state_的实现,您可以使用_lambda表达式_做与_anonymous inner classes_相同的事情。例如:IntSupplier makeCounter(int start) { int[] value = { start }; return () -> value[0]++; }。如果您熟悉JavaScript,则使用captures创建statefull函数对象是一种常见模式。 - nosid
1
@nosid 啊,单元素数组的技巧。只是不要尝试从多个线程中使用你的计数器。如果你要在堆上分配一些东西并在lambda中捕获它,那么最好使用一个AIC并添加一个可以直接修改的字段。以这种方式使用lambda可能有效,但为什么麻烦呢?你可以使用一个真正的对象。 - Stuart Marks
2
AIC会创建一个类似于A$1.class的文件,但Lambda不会。我可以在Difference中添加这个吗? - Asif Mushtaq
4
这主要是一个实现问题,它不会影响如何使用 AICs 和 lambdas 进行编程,这也是本问题的主要内容。请注意,lambda 表达式确实会生成一个类名为 LambdaClass$$Lambda$1/1078694789 的类。但是,这个类是由 lambda metafactory 在运行时动态生成的,而不是由 javac 生成的,因此没有相应的 .class 文件。同样,这也只是一个实现方面的问题。 - Stuart Marks

66

虽然Lambda是一个很好的特性,但只能与SAM类型一起使用。也就是说,只有一个抽象方法的接口。如果您的接口包含多个抽象方法,它将会失败。这就是匿名类非常有用的地方。

因此,我们不能忽略匿名类。还有需要提醒的是,您的sort()方法可以更加简化,跳过p1p2的类型声明:

Collections.sort(personList, (p1, p2) -> p1.firstName.compareTo(p2.firstName));

您还可以在此处使用方法引用。您可以在Person类中添加一个compareByFirstName()方法,然后使用:

您也可以使用方法引用。您可以在 Person 类中添加一个 compareByFirstName() 方法,然后使用:

Collections.sort(personList, Person::compareByFirstName);

或者,添加一个 firstName 的 getter 方法,然后直接从 Comparator.comparing() 方法中获取 Comparator

Collections.sort(personList, Comparator.comparing(Person::getFirstName));

5
我知道,但就可读性而言我更喜欢长的那个,否则很容易让人困惑不知道这些变量来自何处。 - Amin Abu-Taleb
@AminAbu-Taleb 为什么会让人感到困惑呢?这是有效的Lambda语法。类型会被自动推断。总之,这是个人选择。您可以明确给出类型,没有问题。 - Rohit Jain
1
Lambda表达式和匿名类之间还有一个微妙的区别:匿名类可以直接使用新的Java 8类型注释进行注释,例如new @MyTypeAnnotation SomeInterface(){};。但是Lambda表达式不支持此操作。有关详细信息,请参见我的这个问题:Annotating the functional interface of a Lambda Expression - Balder

41

匿名类的Lambda表达式性能

当应用程序启动时,每个类文件必须加载和验证。

编译器将匿名类处理为给定类或接口的新子类型,因此将为每个类生成一个新的类文件。

Lambda表达式在字节码生成时与众不同,它们更加高效,使用了随JDK7一起提供的invokedynamic指令。

对于Lambda表达式,该指令被用于延迟将lambda表达式翻译为字节码直到运行时。(该指令仅在第一次调用时被调用)

结果Lambda表达式将成为静态方法(在运行时创建)。(在无状态和有状态情况下有微小的差别,这些差别通过生成的方法参数进行解决)


1
每个lambda也需要一个新类,但它是在运行时生成的,因此从这个意义上说,lambda并不比匿名类更有效率。Lambda是通过invokedynamic创建的,而invokespecial用于创建匿名类的新实例通常比较慢。因此,在这个意义上,lambda也比较慢(然而,JVM大多数情况下可以优化invokedynamic调用)。 - ZhekaKozlov
3
@AndreiTomashpolskiy 1. 请保持礼貌。2. 阅读这位编译器工程师的评论:https://habrahabr.ru/post/313350/comments/#comment_9885460 - ZhekaKozlov
@ZhekaKozlov,你不需要成为编译器工程师才能阅读JRE源代码并使用javap/debugger。你所忽略的是,包装lambda方法的包装类生成完全在内存中完成,成本几乎为零,而实例化AIC涉及解析和加载相应的类资源(这意味着I/O系统调用)。因此,与已编译的匿名类相比,“invokedynamic”通过特殊的类生成方法非常快。 - Andrei Tomashpolskiy
@AndreiTomashpolskiy I/O 不一定慢。 - ZhekaKozlov

18
以下是不同之处:
1)语法
与匿名内部类(AIC)相比,Lambda表达式看起来更简洁。
public static void main(String[] args) {
    Runnable r = new Runnable() {
        @Override
        public void run() {
            System.out.println("in run");
        }
    };

    Thread t = new Thread(r);
    t.start(); 
}

//syntax of lambda expression 
public static void main(String[] args) {
    Runnable r = ()->{System.out.println("in run");};
    Thread t = new Thread(r);
    t.start();
}

2)范围

匿名内部类是一个类,这意味着它有用于在内部类中定义的变量的作用域。

然而,lambda表达式不是自己的作用域,而是封闭作用域的一部分。

当在匿名内部类和lambda表达式中使用super和this关键字时,类似的规则适用。在匿名内部类中,this关键字指的是局部作用域,而super关键字指的是匿名类的父类。而在lambda表达式中,this关键字则指向封闭类型的对象,而super将引用封闭类的父类。

//AIC
    public static void main(String[] args) {
        final int cnt = 0; 
        Runnable r = new Runnable() {
            @Override
            public void run() {
                int cnt = 5;    
                System.out.println("in run" + cnt);
            }
        };

        Thread t = new Thread(r);
        t.start();
    }

//Lambda
    public static void main(String[] args) {
        final int cnt = 0; 
        Runnable r = ()->{
            int cnt = 5; //compilation error
            System.out.println("in run"+cnt);};
        Thread t = new Thread(r);
        t.start();
    }

3) 性能

运行时,匿名内部类需要进行类加载、内存分配和对象初始化,并调用非静态方法,而 Lambda 表达式是纯编译时活动,在运行时不会产生额外的成本。因此,与匿名内部类相比,Lambda 表达式的性能更好。

**我意识到这一点并不完全正确。请参考以下问题以了解详情:Lambda 与匿名内部类的性能:减轻 ClassLoader 的负担?


3
Java 8 中引入了 Lambda 表达式,用于函数式编程。通过使用 Lambda,您可以避免样板代码。我在一篇有趣的关于 Lambda 的文章中看到了这个内容。

http://radar.oreilly.com/2014/04/whats-new-in-java-8-lambdas.html

对于简单逻辑,建议使用lambda函数。如果使用lambda实现复杂逻辑,在出现问题需要调试代码时会增加过多负担。


1
+----------------------------------+----------------------------------------------------------+---------------------------------------------+
|                                  |                                       Lambdas            |              Anonymous classes              |
+----------------------------------+----------------------------------------------------------+---------------------------------------------+
| Definition                       | An anonymous method that can be created without belonging| An inner class without a name.              |
|                                  | to any class                                             |                                             |
+----------------------------------+----------------------------------------------------------+---------------------------------------------+
| Scope of variables of main class | Available                                                | Not available                               |
| (this and super keywords also)   |                                                          |                                             |
+----------------------------------+----------------------------------------------------------+---------------------------------------------+
| Lines of codes                   | Reduced the lines of code. It’s a short form of          | Have more lines of code compared to lambdas |
|                                  | anonymous class.                                         |                                             |
+----------------------------------+----------------------------------------------------------+---------------------------------------------+
| Criteria for creating            | Needs to be Functional Interface, ie interface with      | Can use interfaces(including Functional     |
|                                  | only one abstract method. Example : Runnable Interface   | interfaces) and abstract classes to create. |
+----------------------------------+----------------------------------------------------------+---------------------------------------------+
| Example:                         | Runnable r = ()->{System.out.println("Hello World");};   | Runnable r = new Runnable() {               |
|                                  |                                                          |         @Override                           |
|                                  |                                                          |         public void run() {                 |
|                                  |                                                          |          System.out.println("Hello World"); |
|                                  |                                                          |                                        }    |
|                                  |                                                          |     };                                      |
+----------------------------------+----------------------------------------------------------+---------------------------------------------+

0
  • lambda语法不需要编写Java可以推断的明显代码。
  • 通过使用invoke dynamiclambda在编译时不会转换回匿名类(Java不必经过创建对象的过程,只需关心方法的签名,即可绑定到方法而无需创建对象)
  • lambda更加强调我们想要做什么,而不是我们必须在执行之前做什么。

0

匿名类是必须的,因为Lambda适用于具有单个抽象方法的函数,但对于所有其他情况,匿名内部类是你的救星。


0

Lambda表达式仅涵盖匿名内部类的一种特定情况。您可以将其视为Lambda表达式的超集的匿名内部类。

Lambdas Expression ⊂ Anonymous Inner classes

有一些情况下,可以用Lambda表达式替换注解内部类:

  1. 如果要实现的内部类是一个函数接口(只有一个抽象方法)

例如:

Interface A{
 public void m1();
 public void m2();
}

Interface B{
 public void m();
}

Class Temp{
 public static void main(String[] args){
  // Anonymous inner class implementation
  A a = new A()
  {
   public void m1(){
    //
   }
   public void m2(){
    //
   }
  };
  a.m1(); or a.m2();
 
  // Here B is a functional Interface so we can replace the anonymous class to Lambda Expression
  B b = () => { ... }
  b.m();
  
 }
}

除此之外,它们的编译风格也有所不同。对于 Lambda 表达式编译器而言,它不会生成任何其他类,但对于匿名内部类则会生成。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接