面试题三:Java的匿名内部类有哪些限制?

Java的匿名内部类有哪些限制?

一、面试官视角:这道题想考察什么?

  • 考察匿名内部类的概念和用法
  • 考察语言规范以及语言的横向对比等
  • 作为考察内存泄露的切入点

二、题目剖析:

1、匿名内部类的名字

匿名内部类没有名字,但在字节码中会定义为 包路径.OuterClass$1..N(外部类加$N,N是匿名内部类的顺序,如:$1表示类里面定义的第一个匿名内部类)

2、匿名内部类的继承结构

匿名内部类被new出来的时候,先天就会弄出来一个父类,同时new的时候也可以是一个接口,但不能new一个又继承了一个类又实现接口的匿名内部类,因为在Java中不支持或类型,Jdk10支持类型推导,但是也不支持又继承了一个类又实现接口的匿名内部类。

Java不支持,但Kotlin支持:

1
2
3
val runnableFoo = object: Foo(), Runnable {
override fun run() {}
}

3、匿名内部类的构造方法

匿名内部类的构造方法是编译器定义的,非静态内部类会引用外部类的实例,静态内部类则不会引用外部类的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// OuterClass指的是父类的外部类
public class OuterClass {
// InnerClass指的是父类
public abstract class InnerClass {
abstract void test();
}
}

public class Clinet {
public void run() {
InnerClass innerClass = new OuterClass().new InnerClass() {
...
}
}
}

// 编译器定义的构造方法
public class Client$1 {
// Clinet client:自己的外部类实例(匿名内部类所在的方法对应的外部作用域)
// OuterClass outerClass:非静态父类的外部类实例
public Clent$1(Clinet client, OuterClass outerClass) {
...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class OuterClass {
// interface定义的类,与静态类效果一致,不会引用外部类实例
public interface InnerClass {
void test();
}
}

public class Client {
// 非静态方法
public void run() {
InnerClass innerClass = new OuterClass.InnerClass() {
...
}
}
}

// 编译器定义的构造方法
public class Client$1 {
// Clinet client:自己的外部类实例
public Clinet$1(Clinet client) {
...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class OuterClass {
// interface定义的类,与静态类效果一致,不会引用外部类实例
public interface InnerClass {
void test();
}
}

public class Client {
// 静态方法
public static void run() {
InnerClass innerClass = new OuterClass.InnerClass() {
...
}
}
}

// 编译器定义的构造方法
public class Client$1 {
// 因为定义在静态方法中,所以一个外部类的实例都不需要
public Clinet$1() {
...
}
}
1
2
3
4
5
6
7
8
9
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
public class OuterCalss {
// interface定义的类,与静态类效果一致,不会引用外部类实例
public interface InnerClass {
void test();
}
}

public class Client {
// 静态方法
public static void run() {
// 为什么需要final修饰?因为在匿名内部类中使用时,是捕获的一份快照,也就是复制了一份引用供匿名内部类使用,如果在外部没有对object进行final修饰,就说明object随时可以再次赋值,那外部和内部的object就不一致了,所以必须final;但在Jdk8里,只要object没有重新被赋值,就不需要使用final修饰,其实编译器会自动帮你final,只是不需要我们手动写了。
final Object object = new Object();
InnerClass innerClass = new OuterClass.InnerClass() {
@Override
void test() {
System.out.println(object.toString());
}
}
}
}

// 编译器定义的构造方法
public class Client$1 {
// Object object:捕获外部变量(自由变量)
public Client$1(Object object) {
...
}
}

// 结论:
// 1、匿名内部类构造方法是通过编译器生成的
// 2、参数列表包括:
// 1. 外部对象(定义在非静态方法体内)
// 2. 父类的外部对象(父类非静态)
// 3. 父类的构造方法参数(父类有构造方法且参数列表不为空)
// 4. 外部捕获的变量(匿名内部类的方法体内有引用外部final变量)

4、Lambda转换(SAM类型)

接口类且只有单一方法,可以使用Lambda转换;而抽象类就算有且只有单一方法,也不能使用Lambda转换。

1
2
3
4
5
6
7
8
interface CallBack {
void onSuccess()
}

// 可以使用Lambda转换
CallBack callBack = () -> {

}

三、题目结论:

  • 没有人类认知意义上的名字
  • 只能继承一个父类或实现一个接口
  • 父类是非静态的类型,则需父类外部实例来初始化
  • 如果定义在非静态作用域内,会引用外部类实例
  • 只能捕获外部作用域内的final变量
  • 创建时只有单一方法的接口可以用Lambda转换