全面掌握 Java 内部类

  1. 创建内部类与连接外部类
  2. 内部类与向上转型
  3. 局部内部类
  4. 匿名内部类
  5. 局部内部类和匿名内部类
  6. 嵌套类
  7. 内部类的继承
  8. 内部类可以被覆盖吗
  9. 为什么需要内部类
  10. 内部类标识符

内部类是一种非常有用的特性,它可以把一些逻辑相关的类组织在一起,并控制位于内部的类的可视性,下文中内部类均指非静态内部类。

1、创建内部类与连接外部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Car {
private int speed = 100;

class Tyre {
int getSpeed() { //访问外部类成员
return speed;
}
Car getCar() { //通过.this获取外部类对象
return Car.this;
}
}

public static void main(String[] args) {
Car car = new Car();
Car.Tyre tyre = car.new Tyre();
tyre.getSpeed();
System.out.println(car == tyre.getCar()); // true
}
}

要想创建内部类的对象,必须使用外部类的对象,如上,且必须地具体指明这个对象的类型:OuterClassName.InnerClassName,然后通过 OuterClassInstance.new InnerClassName() 来创建内部类对象。

内部类对象会暗暗连接到创建它的外部类对象上,捕获外部类的对象引用,然后在内部类中访问此外部类的成员时,就是通过这个引用来访问的,并且拥有其外部类的所有元素的访问权。正是由于此原因,Android 中非静态内部类创建静态实例才会造成内存泄漏。

在内部类中通过 OuterClassName.this 可获取外部类对象,如上代码中通过 Car.this 获取 Car 类对象。

2、内部类与向上转型

当将内部类向上转型为基类,尤其是转型为一个接口的时候,此内部类能够隐藏这个接口的实现,如下例:

1
2
3
interface cost{
double getPrice();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Car {
private int speed = 100;

class Tyre implements cost{
double originalPrice = 100;

@Override
public double getPrice() {
return originalPrice * 0.75;
}
}

public static void main(String[] args) {
Car car = new Car();
cost tyre = car.new Tyre(); //向上转型
tyre.getPrice(); // 75.0
}
}

3、局部内部类

前面提到的都是处于外部类中的内部类,而内部类也可以定义在一个方法里面或者在任意的作用域中,这么做有以下两个理由:

1.如实现某接口的内部类,可以在方法中创建并返回对其的引用

2.要解决一个复杂的问题,需要创建一个类来辅助,但不希望这个类是公共可用的

例1:在方法中定义内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Car {
private int speed = 100;

cost getTyre() { //方法内部
class Tyre implements cost {
double originalPrice = 100;
@Override
public double getPrice() { //打折操作
return originalPrice * 0.75;
}
}
return new Tyre();
}

public static void main(String[] args) {
Car car = new Car();
cost tyre = car.getTyre();
tyre.getPrice(); // 75.0
}
}

例2:在任意作用域中定义内部类:

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
public class Car {
private boolean hadTyre = true; //是否有轮胎

cost getTyre() {
if (hadTyre) { //任意作用域中
class Tyre implements cost {
double originalPrice = 100;

@Override
public double getPrice() { //打折操作
return originalPrice * 0.75;
}
}
return new Tyre();
}else{
return null;
}
}

public static void main(String[] args) {
Car car = new Car();
cost tyre = car.getTyre();
tyre.getPrice(); // 75.0
}
}

定义在一个方法里面或者在任意的作用域中的内部类也叫作局部内部类,局部内部类不能有 private 等访问说明符,因为它不是外部类的一部分,但是它可以访问当前代码块内的常量以及外部类中的所有成员:

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
public class Car {
private boolean hadTyre = true;
private double coupon1 = 10; // 外部类成员(10元优惠券)

cost getTyre() {
if (hadTyre) {
double coupon2 = 5; // 代码块内常量(5元优惠券)
class Tyre implements cost {
double originalPrice = 100;
@Override
public double getPrice() {
return originalPrice * 0.75 - coupon1 - coupon2;
}
}
return new Tyre();
} else {
return null;
}
}

public static void main(String[] args) {
Car car = new Car();
cost tyre = car.getTyre();
System.out.print(tyre.getPrice()); // 60.0
}
}

书中说可以访问当前代码块内的常量,这里有点不解,coupon2 是常量?

4、匿名内部类

上例中 getTyre() 方法要创建一个 cost 对象,从 cost tyre = car.getTyre(); 看出我们并不关心内部类的名字 Tyre,只要返回的是 cost 类对象就足够了,所以这里可以用匿名内部类来实现,顾名思义,它没有名字,如下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Car {
cost getTyre(double p) {
return new cost() {
double coupon = 10; // 10元优惠券
private double price = p; //原价
@Override
public double getPrice() {
return price - coupon;
}
};
}
public static void main(String[] args) {
Car car = new Car();
cost tyre = car.getTyre(100);
System.out.print(tyre.getPrice()); // 90.0
}
}

你可能会有疑问,这段代码可以编译通过吗?应该是 getTyre(final double p) 吧?确实,在匿名内部类中使用一个在其外部定义的对象,那么编译器必须要求其参数引用是 final 类型,以上代码在低于 Java 8 的版本编译不会通过,但是在 Java 8 版本不用 final 修饰局部变量也可以编译通过,只不过不能修改值,只能打印输出或赋值给其他变量。

5、局部内部类和匿名内部类

首先来看局部内部类和匿名内部类的对比实现:

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
public class Car {
private double coupon2 = 5;
cost getTyre1(double p) { //局部内部类
class Tyre implements cost {
double coupon = 10;
private double price = p;

@Override
public double getPrice() {
return price - coupon - coupon2;
}
}
return new Tyre();
}
cost getTyre2(double p) { //匿名内部类
return new cost() {
double coupon = 10;
private double price = p;

@Override
public double getPrice() {
return price - coupon - coupon2;
}
};
}
}

getTyre() 方法用来创建一个 cost 类对象,我们分别使用局部内部类和匿名内部类实现了这个功能,它们具有相同的行为和能力,既然局部内部类的名字在方法外是不可见的,那为什么我们仍然使用局部内部类而不是匿名内部类呢?唯一的理由是:我们需要一个已命名的构造器,或者需要重载狗仔器,而匿名内部类只能用于实例初始化,所以使用局部内部类而不使用匿名内部类的另一个理由就是:需要不止一个该内部类的对象。

6、嵌套类

如果不需要内部类对象与其外部类对象之间有联系,那么可以将内部类声明为 static,即静态内部类,也称嵌套类,静态内部类和非静态内部类的最大区别就是非静态内部类对象隐士的保存了一个外部类对象的引用,这意味着:

1.不需要外部类的对象就可以创建静态内部类的对象

2.不能从静态内部类的对象中访问非静态的外部类对象

3.静态内部类中可以定义静态或者非静态的成员,而非静态内部类则不能有静态成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Car {
private static double price = 100;
static class Tyre implements cost {
private static double coupon = 5; // 3.若为非静态内部类则无法定义为 static 类型
@Override
public double getPrice() {
return price; // 2.若 price 不为 static 则无法访问
}
}
public static void main(String[] args) {
Car.Tyre tyre = new Car.Tyre(); // 1.静态内部类的创建不依赖外部类对象
}
}

这也是静态内部类和内部类的关键区别。此外静态内部类也可定义在接口内部:

1
2
3
interface cost {
class Price { } // 默认为 public 、static
}

如果你想要创建某些公共代码,使得它们可以被某个接口的所有不同实现所公用,那么使用接口内部嵌套类会显得很方便。

7、内部类的继承

怎么继承自一个内部类?内部类的构造必须依赖其外部类对象,所以在继承内部类的时候,事情会变得复杂,比如我们要继承自 Size 类:

1
2
3
class Tyre{
class Size{}
}

可以这样写:

1
2
3
public class TyreSize extends Tyre.Size{

}

编译器会报错: No enclosing instance of type ‘com.example.Tyre’ is in scope ,即缺少 Tyre 类的实例,若要创建位于 Tyre 内部的 Size 类,则必须要有 Tyre 的实例对象,要解决这个问题,需要引入 Tyre 实例且说清它们之间的关联:

1
2
3
4
5
public class TyreSize extends Tyre.Size{
TyreSize(Tyre tyre) {
tyre.super();
}
}

编译通过。

8、内部类可以被覆盖吗

如果创建了一个内部类,然后继承其外部类并重新定义此内部类时,内部类可以被覆盖吗?例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Car {
Car() {
System.out.println("new Car()");
new Tyre();
}
class Tyre { // 我会被覆盖吗
Tyre() { System.out.println("new Tyre()"); }
}
}

public class BigCar extends Car {
class Tyre {
Tyre() {System.out.println("BigCar new Tyre()"); }
}

public static void main(String[] args) {
new BigCar();
}
}

在 Car 的构造器中新建的 Tyre 是 Car 中的 Tyre 还是 BigCar 中的 Tyre 呢?运行程序输出:

new Car()
new Tyre()

BigCar 中定义的 Tyre 内部类并没有覆盖 Car 中的 Tyre 内部类,实际上这两个内部类是完全独立的两个实体,各自在自己的命名空间内,没有谁覆盖谁之说。

9、为什么需要内部类

也就是说,内部类存在的意义是什么呢?为什么 Sun 公司如此费心地增加这项语言特性呢?这里将内部类的意义总结为以下四点:

A. 逻辑上被包含且对外隐藏

如果一个类在逻辑上被包含于另一个类,那么将此类设置为另一个类的内部类,比如轮胎类可以写作汽车类的内部类:

1
2
3
public class Car {
public class Tyre{}
}

上面代码中只存在被包含关系,也可通过组合方式实现,写作内部类是没有必要的,当想对外保密汽车使用何种轮胎时,写作内部类才是有必要的:

1
2
3
public class Car {
private class Tyre{}
}

B. 实现多重继承

每个内部类都能独立的继承一个接口,无论外部类是否已经继承了某个接口的实现,对于内部类都没有影响。内部类提供可以继承多个抽象类或具体的类的能力,有效的实现了多重继承,网上一个实例简单直观的展示了通过内部类实现多重继承(儿子利用多重继承来继承父亲和母亲的优良基因):

1
2
3
4
5
6
7
8
9
10
11
public class Father { //父亲
public int strong(){
return 9;
}
}

public class Mother { //母亲
public int kind(){
return 8;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Son { // 儿子通过内部类实现多重继承

class Father_1 extends Father{
public int strong(){
return super.strong() + 1;
}
}

class Mother_1 extends Mother{
public int kind(){
return super.kind() - 2;
}
}

public int getStrong(){
return new Father_1().strong();
}

public int getKind(){
return new Mother_1().kind();
}
}

C. 闭包与回调

闭包是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域。通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外部类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外部类对象的引用,在此作用域内,内部类有权操作所有的成员,包括private成员。

通过内部类提供闭包功能比指针更灵活、更安全

例如:一个接口程序员和一个基类作家都有一个相同的方法work,相同的方法名,但是其含义完全不同,这时候就需要闭包。

1
2
3
4
5
6
class Writer { //作家基类
void work(){}
}
interface programmer{ //程序员接口
void work();
}

闭包实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class WriterProgrammer extends Writer {
@Override
public void work(){
//写作
}
public void code(){
//写代码
}
class ProgrammerInner implements programmer{
@Override
public void work(){
code();
}
}
}

WriterProgrammer 继承自 Writer , 直接实现父类作家的work()方法,然后使用内部类实现程序员的work()方法回调code()方法。如果WriterProgrammer 同时继承自 Writer 且实现 programmer 接口,那么就不能同时实现作家和程序员的意义不同的 work()方法:

1
2
3
4
5
6
class WriterProgrammer extends Writer implements programmer{
@Override
public void work() { //programmer的 work

}
}

D. 控制框架

应用程序框架就是被设计用以解决某类特定问题的一个类或一组类,而控制框架就是一类特殊的应用程序框架,它用来解决响应事件的需求,主要用来响应事件的系统被称作事件驱动系统。 Java Swing 库就是一个控制框架,它优雅的解决了 GUI 的问题,并使用了大量的内部类

控制框架的完整实现是由单个类创建的,内部类用来表示解决问题所必须的各种不同的 action,另外内部类能够很容易的访问外部类的任意成员,可以让这种实现更轻松。

例如控制温室的运作:控制灯光、水、温度调节器的开关等,每个行为都是完全不同的,使用内部类可以在单一的类中产生对同一个基类 Event 的多种导出版本,对于温室系统的每一种行为,都继承一个新的 Event 内部类,并在要实现的 action() 中编写控制代码:

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 GreenhouseControls{

public class LightOn extends Event {
public void action() {
//开灯...
}
}

public class LightOff extends Event {
public void action() {
//关灯...
}
}

public class WaterOn extends Event {
public void action() {
//开水闸...
}
}

// 其他操作...
}

10、内部类标识符

每个类都会产生一个.class文件,其中包含了如何创建该类型的对象的全部信息,内部类也必须生成一个 .class 文件,从而可以包含它自己的 Class 对象信息,这些类文件的命名有严格的规则:外部类名字+“$”+ 内部类名字,例如:

1
2
3
class Car {
class Tyre {}
}

生成的 .class 文件包括:

Car.class
Car$Tyre.class

如果内部类是匿名的,编译器会简单的产生一个数字作为其标识符,如果内部类是嵌套在别的内部类之中,只需直接将它们的名字加在其外部类标识符与”$”后面,这是 Java 的标准命名方式,产生的文件自动都是平台无关的。

总结

本文主要参考 Java 编程思想书中对内部类部分的讲解思路,将各部分知识点转换为自以为更易理解的例子~嗯~总算较为全面的理解内部类了~以后应该不会怕啦hhh~

参考文章:

BruceEckel. Java编程思想:第4版[M]. 机械工业出版社, 2007.