Java 中 Lambda 与匿名内部类的区别

1,171 阅读3分钟

事情的起源

为什么会突然想着写这个知识点呢?因为今天在开发的过程中踩坑了。在进行业务开发的过程中,遇到了下面一段代码(为了方便理解,业务逻辑被简化掉了):

public class Service {
    public ExecutorService executorService = Executors.newSingleThreadExecutor();
    public void businessMethodWithLambda() {
        executorService.submit(() -> XXXUtils.execute(this));
    }
​
    public void businessLogic() {
        System.out.println("执行业务逻辑");
    }
}
public class XXXUtils {
    public static void execute(Object object) {
        try {
            Service service = (Service) object;
            service.businessLogic();
        } catch (Exception e) {
            e.printStackTrace();
        }
​
    }
}

这段代码调用 businessMethodWithLambda 方法,提交一个任务到线程池中,使用 Lambda 的形式。看起来没什么毛病,但是采用 Lambda 的形式提交,最终提交的是 Runnable 的一个具体实现。其中只有 run 方法的逻辑,这回导致在执行 run 方法的过程中,会丢失提交任务的线程的上下文,导致 Trace 链路丢失。

所以我手一抖给改成了下面的形式:

public class Service {
    public ExecutorService executorService = Executors.newSingleThreadExecutor();
​
    public void businessMethodWithAnonymousInnerClass() {
        executorService.submit(new AbstractRunnable() {
            @Override
            public void doRun() {
                XXXUtils.execute(this);
            }
        });
    }
​
    public void businessLogic() {
        System.out.println("执行业务逻辑");
    }
}
public abstract class AbstractRunnable implements Runnable {
    private void init() {
        System.out.println("执行一些初始化");
    }
​
    @Override
    public void run() {
        init();
        doRun();
    }
    private void clean(){
        System.out.println("执行一些清理动作");
    }
​
    /**
     * 具体业务逻辑
     */
    public abstract void doRun();
}
​

提交任务的时候,改为匿名类提交的方式,提交了一个 AbstractRunnable 的具体实现。在 AbstractRunnable 的 run 方法中在真正执行业务逻辑前,会执行一些初始化操作,设置线程上下文之类的。在业务逻辑执行完成之后,会执行一些清理操作。

看起来一切都辣么美好,然后反手运行,然而现实予以痛击,一个 ClassCastException 直接抛出:

java.lang.ClassCastException: com.mingming.lamba.Service1cannotbecasttocom.mingming.lamba.Serviceatcom.mingming.lamba.XXXUtils.execute(XXXUtils.java:6)atcom.mingming.lamba.Service1 cannot be cast to com.mingming.lamba.Service at com.mingming.lamba.XXXUtils.execute(XXXUtils.java:6) at com.mingming.lamba.Service1.doRun(Service.java:13) at com.mingming.lamba.AbstractRunnable.run(AbstractRunnable.java:11) at java.util.concurrent.ExecutorsRunnableAdapter.call(Executors.java:511)atjava.util.concurrent.FutureTask.run(FutureTask.java:266)atjava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)atjava.util.concurrent.ThreadPoolExecutorRunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutorWorker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)

可以看到异常提示 com.mingming.lamba.Service1cannotbecasttocom.mingming.lamba.Service。啊这。。。Service1 cannot be cast to com.mingming.lamba.Service。啊这。。。Service1 实例是个什么东西,我在代码中并没有生成这个类呀?

回头想想这一波有什么改动呢?唯一的改动就是把 Lambda 的提交方式改成了匿名类的提交方式,那么一定是 Lambda 和匿名类有一些区别。

Lambda 与匿名类的区别

匿名类Lambda
没有名字的类没有名称的方法(匿名函数)
可以实现拥有任意方法的接口或者抽象类只能使用在仅有一个抽象方法的接口中
可以实例化匿名内部类Lambda 表达式无法实例化
每当我们创建对象时,内存分配都是按需的它驻留在JVM的永久内存中
在匿名内部类内部,“ this”始终是指当前匿名内部类对象,而不是外部对象在Lambda表达式内部,“ this”始终引用当前的外部类对象,即包围类对象
如果我们要处理多种方法,这是最佳选择如果我们要处理接口,这是最佳选择
匿名类可以具有实例变量和方法局部变量Lambda表达式只能具有局部变量
在编译时,将生成一个单独的.class文件在编译时,不会生成单独的.class文件。只是将其转换为外部类的私有方法

可以看到在匿名类中 this 始终是指向的匿名内部类对象,而 Lambda 指向的是包围类对象。并且匿名内部类在编译之后会生成一个单独的 .class 文件,那么这个匿名内部类生成的文件的类名称是什么呢?

测试

可以看到生成的匿名内部类正是前面异常中提到的 Service$1,问题的原因找到了,那么有什么办法能获取到匿名类对应的外部类的实例吗?

测试2

可以看到在匿名类实例中持有一个 this$0 变量,指向 Service 外部类,那我们如何获取这个变量呢?Java 提供了一个语法用来获取部类对象:

public class Service {
    public ExecutorService executorService = Executors.newSingleThreadExecutor();
​
    public void businessMethodWithAnonymousInnerClass() {
        executorService.submit(new AbstractRunnable() {
            @Override
            public void doRun() {
                XXXUtils.execute(Service.this);
            }
        });
    }
​
    public void businessLogic() {
        System.out.println("执行业务逻辑");
    }
}

通过 ClassName.this 的方式就可以简便的获取到外部类对象。

最后

魔鬼在细节,还有许多知识需要学习啊。