3

问题背景

之前看代码的时候不懂为什么要使用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.thisvar that = this;提前保留外部类引用。

参考文章:
https://liaoxuefeng.com/books/java/oop/basic/inner-class/inde...
https://www.runoob.com/w3cnote/java-inner-class-intro.html


wyhhh
21 声望12 粉丝

下一篇 »

引用和评论

0 条评论
评论支持部分 Markdown 语法:**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用 @ 来通知其他用户。

AltStyle によって変換されたページ (->オリジナル) /