1. 概念
在 Java 语言中,可以将一个类定义在另一个类里面或者一个方法里面,我们把这样的类称为内部类。
与之对应的,包含内部类的类被称为外部类。请阅读下面的代码:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
代码中,Engine
就是内部类,而 Car
就是外部类。
2. 分类
Java 中的内部类可以分为 4 种:成员内部类、静态内部类、方法内部类和匿名内部类。接下来我们按照分类一一介绍。
2.1 成员内部类
2.1.1 定义
成员内部类也称为普通内部类,它是最常见的内部类。可以将其看作外部类的一个成员。在成员内部类中无法声明静态成员。
如下代码中声明了一个成员内部类:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
我们在外部类 Car
的内部定义了一个成员内部类 Engine
,在 Engine
下面有一个 run()
方法,其功能是打印输出一行字符串:“发动机启动了!”。
另外,需要注意的是,与普通的 Java 类不同,含有内部类的类被编译器编译后,会生成两个独立的字节码文件:
- 1
- 2
内部类 Engine
会另外生成一个字节码文件,其文件名为:外部类类名 $ 内部类类名.class。
2.1.2 实例化
内部类在外部使用时,无法直接实例化,需要借助外部类才能完成实例化操作。关于成员内部类的实例化,有 3 种方法:
- 我们可以通过
new 外部类().new 内部类()
的方式获取内部类的实例对象:
运行结果:
- 1
- 我们可通过先实例化外部类、再实例化内部类的方法获取内部类的对象实例:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
编译执行,成功调用了内部类的 run () 方法:
- 1
- 2
- 3
- 我们也可以在外部类中定义一个获取内部类的方法
getEngine()
,然后通过外部类的实例对象调用这个方法来获取内部类的实例:
运行结果:
- 1
这种设计在是非常常见的,同样可以成功调用执行 run()
方法。
2.1.2 成员的访问
成员内部类可以直接访问外部类的成员,例如,可以在内部类的中访问外部类的成员属性:
观察 Engine
的 run()
方法,调用了外部类的成员属性 name
,我们在主方法实例化 Car
后,已经为属性 name
赋值。
运行结果:
- 1
相同的,除了成员属性,成员方法也可以自由访问。这里不再赘述。
还存在一个同名成员的问题:如果内部类中也存在一个同名成员,那么优先访问内部类的成员。可理解为就近原则。
这种情况下如果依然希望访问外部类的属性,可以使用外部类名.this.成员
的方式,例如:
运行结果:
- 1
- 2
- 3
请观察内部类 run()
方法中的语句:第一行语句调用了内部类自己的属性 name
,而第二行调用了外部类 Car
的属性 name
,第三行调用了外部类的方法 run()
,并将外部类的属性 name
作为方法的参数。
2.2 静态内部类
2.2.1 定义
静态内部类也称为嵌套类,是使用 static
关键字修饰的内部类。如下代码中定义了一个静态内部类:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
2.2.2 实例化
静态内部类的实例化,可以不依赖外部类的对象直接创建。我们在主方法中可以这样写:
- 1
- 2
- 3
- 4
运行结果:
- 1
- 2
2.2.2 成员的访问
在静态内部类中,只能直接访问外部类的静态成员。例如:
在 run()
方法中,打印的 name
属性就是外部类中所定义的静态属性 name
。编译执行,将会输出:
- 1
对于内外部类存在同名属性的问题,同样遵循就近原则。这种情况下依然希望调用外部类的静态成员,可以使用外部类名.静态成员
的方式来进行调用。这里不再一一举例。
如果想要访问外部类的非静态属性,可以通过对象的方式调用,例如在 run()
方法中调用 Car1
的实例属性 brand
:
- 1
- 2
- 3
- 4
- 5
2.3 方法内部类
2.3.1 定义
方法内部类,是定义在方法中的内部类,也称局部内部类。
如下是方法内部类的代码:
运行结果:
- 1
- 2
如果我们想调用方法内部类的 run()
方法,必须在方法内对 Engine
类进行实例化,再去调用其 run()
方法,然后通过外部类调用自身方法的方式让内部类方法执行。
2.3.2 特点
与局部变量相同,局部内部类也有以下特点:
- 方法内定义的局部内部类只能在方法内部使用;
- 方法内不能定义静态成员;
- 不能使用访问修饰符。
也就是说,Car2.getEngine()
方法中的 Engine
内部类只能在其方法内部使用;并且不能出现 static
关键字;也不能出现任何的访问修饰符,例如把方法内部类 Engine
声明为 public
是不合法的。
2.4 匿名内部类
2.4.1 定义
匿名内部类就是没有名字的内部类。使用匿名内部类,通常令其实现一个抽象类或接口。请阅读如下代码:
运行结果:
- 1
- 2
上述代码中的抽象父类中有一个方法 run()
,其子类必须实现,我们使用匿名内部类的方式将子类的定义和对象的实例化放到了一起,通过观察我们可以看出,代码中定义了两个匿名内部类,并且分别进行了对象的实例化,分别为 car
和 airPlain
,然后成功调用了其实现的成员方法 run()
。
2.4.2 特点
- 含有匿名内部类的类被编译之后,匿名内部类会单独生成一个字节码文件,文件名的命名方式为:
外部类名称$数字.class
。例如,我们将上面含有两个匿名内部类的Transport.Java
编译,目录下将会生成三个字节码文件:
- 1
- 2
- 3
- 匿名内部类没有类型名称和实例对象名称;
- 匿名内部类可以继承父类也可以实现接口,但二者不可兼得;
- 匿名内部类无法使用访问修饰符、
static
、abstract
关键字修饰; - 匿名内部类无法编写构造方法,因为它没有类名;
- 匿名内部类中不能出现静态成员。
2.4.2 使用场景
由于匿名内部类没有名称,类的定义可实例化都放到了一起,这样可以简化代码的编写,但同时也让代码变得不易阅读。当我们在代码中只用到类的一个实例、方法只调用一次,可以使用匿名内部类。
3. 作用
3.1 封装性
内部类的成员通过外部类才能访问,对成员信息有更好的隐藏,因此内部类实现了更好的封装。
3.2 实现多继承
我们知道 Java 不支持多继承,而接口可以实现多继承的效果,但实现接口就必须实现里面所有的方法,有时候我们的需求只是实现其中某个方法,内部类就可以解决这些问题。
下面示例中的 SubClass
,通过两个成员内部类分别继承 SuperClass1
和 SuperClass2
,并重写了方法,实现了多继承:
- 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
编译执行 SubClass.Java
,屏幕将会打印:
- 1
- 2
- 3
- 4
3.3 解决继承或实现接口时的方法同名问题
请阅读如下代码:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
此时,我们无法确定 Demo1
类中的 test()
方法是父类 One
中的 test
还是接口 Two
中的 test
。这时我们可以使用内部类解决这个问题:
- 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
运行结果:
- 1
- 2
4. 小结
本小节,我们知道了什么是内部类,也知道了在 Java 中有四种内部类:成员内部类、静态内部类、方法内部类和匿名内部类。对于它们的定义和调用也做了详细讲解,理解内部类的作用是使用好内部类的关键。