问题背景
之前看代码的时候不懂为什么要使用var that = this;来保留this关键字,本文深入了解一下。
部分代码如下:
@Override
public Pair<Long, Long> updateInventoryAndCostPriceForPurchase(List<InboundOrderDto.SaveRequest.StockTransactionRequest> items) {
// 保存this引用
var that = this;
// 创建用于更新库存的基本对象
Iterable<UpdateInventoryItem> updateInventoryItems = items.stream()
.map(item -> new UpdateInventoryItem() {
@Override
public UUID skuUuid() {
return item.getSku().getUuid();
}
@Override
public Long stockUnitRatio() {
Long sellingUnitId = uuidCacheService.uuidToId(item.getSellingUnitUuid(), sellingUnitRepository).orElseThrow(EntityNotFoundException::new);
SellingUnit sellingUnit = that.sellingUnitRepository.findById(sellingUnitId).orElseThrow(EntityNotFoundException::new);
if (!sellingUnit.getProductSkuId().equals(uuidCacheService.uuidToId(item.getSku().getUuid(), productSkuRepository).orElseThrow(EntityNotFoundException::new))) {
throw new IllegalStateException();
}
return sellingUnit.getRatio();
}
}).collect(Collectors.toList());
}之前认为this关键字永远指向当前类。比如:
public class UserController {
private UserService userService;
public void page() {
this.userService.page();
}
}这里的this指向的是UserController。但是在内部类中并不是这样。
我们首先了解一下什么是内部类。
内部类
菜鸟教程中关于内部类是这样介绍的:
在 Java 中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。
这里简单介绍一下成员内部类和匿名内部类
成员内部类
成员内部类是最普通的内部类,定义为位于另一个类的内部。
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
class Inner {
void hello() {
System.out.println("Hello, " + Outer.this.name);
}
}
}Outer是一个普通类,Inner是成员内部类。成员内部类是依附外部类而存在的,如果想要创建成员内部类Inner的对象,必须存在一个外部类的对象。
public class Main {
public static void main(String[] args) {
Outer outer = new Outer("张三");
// 必须通过Outer对象来创建
Outer.Inner inner = outer.new Inner();
inner.hello();
}
}为什么Inner能访问Outer的private成员
为什么实例化Inner需要依赖于Outer对象
成员内部类并不是完全独立的对象,每一个Inner对象,都默认绑定了一个Outer对象
Outer outer = new Outer("张三");
Outer.Inner inner = outer.new Inner();实际上更像
Inner inner = new Inner(outer);Inner类中除了有一个this指向自己,还隐含地持有一个Outer实例,可以用Outer.this访问外部类对象。
查看Inner的反编译代码:
class Outer$Inner {
Outer$Inner(final Outer this0ドル) {
this.this0ドル = this0ドル;
}
void hello() {
System.out.println("Hello, " + this.this0ドル.name);
}
}可以看到,编译器为Inner类生成了一个this0ドル字段,用来保存对应的Ouer实例。因此,
Outer.this.name本质上会被编译成
this0ドル.name所以在内部类中直接访问
System.out.println(name);等价于
System.out.println(this0ドル.name)也就是说,内部类之所以能够直接访问外部类成员,本质上是因为它持有一个外部类对象的引用。
匿名内部类
匿名内部类不需要在Outer calss中明确定义,而是在方法内部定义。
class Outer {
Outer(String name) {
this.name = name;
}
void asyncHello() {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello, " + Outer.this.name);
}
};
new Thread(r).start();
}
}观察asyncHello()方法,我们在方法内部实例化了一个Runnable。
Runnable本身是接口,接口是不可以被实例化的。所以这里是定义了一个实现Runnable接口的匿名类,new Runnable() {}实例化了实现Runnable接口的匿名内部类对象,然后将该对象转型为Runnable。
示例代码等价于
class Xxx implements Runnable {
@Override
public void run() {
System.out.println("Hello, " + Outer.this.name);
}
}
Runnable r = new Xxx();匿名内部类没有类名,所以这里用Xxx。
发现实际上被实例化的对象类型,是某个匿名类,并不是Runnable。
编译器在编译过程中,Outer类被编译为Outer.class,而匿名类被编译为Outer1ドル.class。如果有多个匿名类,Java编译器会将每个匿名类依次命名为Outer1ドル、Outer2ドル、Outer3ドル......
本质上类似于
Outer1ドル obj = new Outer1ドル();
Runnable r = obj;因为
Outer1ドル implements Runnable所以
Outer1ドル -> Runnable是一个向上转型,子类转为父类。
回归问题,为什么要var that = this
简化问题代码,以此代码为例:
class OuterService {
private OuterRepository outerRepository;
void test() {
var that = this;
Runnable r = new Runnable() {
@Override
public void run() {
that.outerRepository.findAll();
System.out.println(this);
System.out.println(that);
}
};
}
}我们已经知道,普通方法中this指向的是OuterService对象,但是进入匿名内部类后:
new Runnable() {
@Override
public void run() {
}
}已经产生了一个新的类:
OuterService1ドル因此,此时的this不再指向OuterService,而是指向OuterService1ドル,即匿名内部类对象本身。
所以在匿名内部类中使用this无法访问外部类成员,才需要
var that = this;提前把外部类的this保存下来,后面再去访问that.outerRepository(),实际上是在访问外部的OuterService对象
除了保留this关键字,还可以这种方式访问外部对象:
OuterService.this.outerRepository()使用var that = this并不是"必须"的,而是一种
- 历史写法
- 避免长类名
- 避免泛型嵌套时可读性差
的习惯。
Lambda 表达式为什么不需要保留this
Lambda 表达式是java8的新特性,提供了一种更为简洁的语法,尤其适用于函数式接口。相比于传统的匿名内部类,Lambda 表达式使代码更紧凑。
Lambda 表达式,可以简单理解为就是 Java 用来"简化匿名内部类"的语法。
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
};使用 Lambda表达式
Runnable r = () -> {
System.out.println("hello");
};效果完全一样。
因为Runnable只有一个抽象方法 void run(); 所以java推断 () -> {} 就是run()的实现。
因此
() -> {
System.out.println("hello");
}本事上是在
快速实现一个接口
所以 Lambda 可以看作是对匿名内部类的简化写法
Lambda表达式虽然与匿名内部类的"功能相近",但是底层机制并不完全一样。
匿名内部类编译后会创建一个新的类,所以this关键字指向的是匿名内部类。
Lambda表达式 () -> {} 不会创建新的this作用域。Lambda内部的this直接继承外层。
Lambda表达式更接近于把一段函数当作参数传递,它只是"捕获到外层上下文"。
内部类的作用
再来看一下内部类的几个典型场景的特点
内部类可以直接访问外部类状态。
比如上文提到的
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
class Inner {
void hello() {
System.out.println("Hello, " + Outer.this.name);
}
}
}这里
System.out.println("Hello, " + Outer.this.name);实际上是访问外部类的Outer的成员。
如果没有外部类:
class UserPrinter {
private UserService userService;
UserPrinter(UserService userService) {
this.userService = userService;
}
}需要通过手动传递对象引用。
内部类适合"只服务于当前类"的逻辑
class UserService {
private class UserValidator {
boolean valid(String name) {
return name != null && !name.isBlank();
}
}
}UserValidator 只给 UserService 使用。
外部不需要知道它的存在。因此内部类可以增强封装性。
匿名内部类适合"一次性实现"
比如:
void asyncHello() {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello, " + Outer.this.name);
}
};
new Thread(r).start();
}并不需要单独写一个类实现Runnable接口。代码会更紧凑。
内部列可以"模拟多继承"
Java 类只能单继承:
class A extends B不能
class A extends B, C但是内部类可以继承不同的类:
class Outer extends A {
class Inner extends B {
}
}虽然并不是真正的多继承,但是在某些设计中可以实现类似的效果。这种写法在现代开发中已经比较少见了。
总结
在普通类中 this 指向当前对象,匿名内部类中会生成新的类 Outer1,ドル因此匿名内部类中的this实际指向的是Outer1,ドル而不是外部类对象。如果需要访问外部类实例,可以通过OuterClass.this或var that = this;提前保留外部类引用。
参考文章:
https://liaoxuefeng.com/books/java/oop/basic/inner-class/inde...
https://www.runoob.com/w3cnote/java-inner-class-intro.html
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。