Java漫游笔记-04-接口和内部类

接口

接口主要是用于描述某个类具体什么样的功能,但是又不给出具体的实现。属于较高层次的抽象设计。
不使用抽象类,而选择接口的原因,主要是由于继承的限制。
而 java 的设计者选择不实现多种继承,主要是考虑到多重继承给语言本身所带来的复杂性和低效性。
复杂性比较好理解,而低效性就有点难理解了,目前能想到的有:在类加载阶段,一个类的初始化,需要先完成其全部父类的初始化,而接口则是真正用到其父接口的时候才会去初始化它。另外,在运行时,如果使用多继承,对象的属性或方法的引用地址计算以及对象的类型转换都会影响性能。

在语法上,一个类最多只能 extents 一个类:

1
2
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}

但是,一个接口却可以 extents 若干个接口,例如:

1
2
public interface ConcurrentNavigableMap<K,V>
extends ConcurrentMap<K,V>, NavigableMap<K,V> {}

内部类

首先我们要知道,内部类最终也会被编译成一个独立的 class 文件。
从编译的结果来看,对于虚机机而言,内部类和其它的类,并没有什么区别。

内部类的本质

假设我们有外部类 Outer ,其内部定义了 Inner

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Outer {

private String name = "outer";

public String getName() {
return name;
}

// 内部类
class Inner {

public String getOuterName() {
return name;
}

}
}

编译后会生成:Outer.classOuter$Inner.class
如果我们通过 javap 工具来反编译它们的话,就可以看到:

1
2
3
4
5
6
7
8
9
10
11
public class Outer {
public Outer();
public java.lang.String getName();
static java.lang.String access$0(Outer); // 新增的方法
}

class Outer$Inner {
final Outer this$0; // 新增的常量
Outer$Inner(Outer);
public java.lang.String getOuterName();
}

外部类新增了一个 static 方法:access$0(Outer) (名称可能会略有不同,取决于编译器)。
而内部类,则增加了一个常量:Outer this$0
并且,以上的方法和变量都是包可见的。

我们可以这么理解,内部类相当于持有一个隐形(在代码中看不到)的外部类的对象引用。
因此,它可以访问外部类的属性和方法。

如何构造内部类对象

以下代码定义了一个外部类和相应的内部类:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class Outer {

// 实际上,编译后,还会生成:
// static String access$0(Outer){...}

private static final long serialVersionUID = -6941272093972570759L;

private String name = "outer";

public String getName() {
return name;
}

public static long getSerialversionuid() {
return serialVersionUID;
}

public void printName() {

// JDK 1.8 以后,可以省略 final ,但是其本质上还是常量
final String prefix = "print in local : ";

// 局部类
class Local {
public void print() {
System.out.println(prefix + name);
}
}
Local local = new Local();
local.print();
}

// 内部类
class Inner {

// 实际上,编译后,会生成:
// final Outer this$0;

public String getOuterName() {
// 相当于:
// return Outer.access$0(this.this$0);
return name;
}
}

// 静态内部类
static class Nested {
public long getOuterSerialVersionUID() {
return serialVersionUID;
}
}

}

普通内部类/成员内部类

构造普通内部类对象:

1
2
3
Outer outer = new Outer();
// 使用普通内部类的时候,需要这样使用 new
Outer.Inner inner = outer.new Inner();

静态内部类/嵌套类

构造静态内部类对象:

1
2
// 静态内部类的实例化不依赖于外部类对象
Outer.Nested nested = new Outer.Nested();

通过反编译,我们还可以发现,静态内部类和普通内部类的有一个非常重要的区别:静态内部类并没有指向外部对象的引用

匿名类

匿名类的构造和定义同时进行。
并且还有一个前提:必须继承自一个父类或实现一个接口
我们先定义一个非常简单的接口:

1
2
3
4
5
public interface Printer {

void print(String content);

}

然后就可以这样构造匿名类对象:

1
2
3
4
5
6
7
8
9
10
// 匿名类可能会使你产生错觉,让你觉得自己 new 了一个接口
Printer printer = new Printer() {

@Override
public void print(String content) {
System.out.println("print in anonymous : " + content);

}

};

局部类

局部类是在方法体内部定义的类。这决定了它的作用域。
因此,它只能在定义的方法内部构造,和普通类的构造没什么区别:

1
Local local = new Local();

注意:在局部类中只允许使用局部常量,JDK 1.8 以前,你需要显式地声明为 final ,但 JDK 1.8 之后 final 可以忽略,但是本质上还是常量,如果你尝试修改它的值,编译是不会通过滴。
(至于这里为什么一定要使用常量,暂时还没想明白。。。)

总结

内部类,增加了语法的复杂度,使用场景相对较少。
以下是选择使用内部类的一些理由:

  • 编写回调函数时,使用匿名类,可以使代码更精简。使用匿名类的潜台词往往是这样的:我只需要创建这个类的一个对象就够了,所以就懒得命名了。事件处理、线程处理是匿名类的典型应用场景。
  • 基于某种原因需要隐藏这个类。因为使用内部类,可以对包中的其它类隐藏这个类;使用局部类,可以对外部完全隐藏这个类。
  • 使用静态内部类,我觉得有时候只是为了避免命名冲突(尤其是静态内部枚举)。