1、写在开头
书接上回:《探寻内部类的奥秘(上)》,今天我们继续研究:四种内部类的定义与使用,继承机制对内部类的影响,以及内部类的底层编译原理。
2、静态内部类
静态内部类是什么?
在定义内部类的时候,可以在其前面加上一个权限修饰符 static。此时这个内部类就变为了静态内部类。
Nested classes are divided into two categories: static and non-static.
Nested classes that are declared static are called static nested classes. Non-static nested classes are called inner classes.
举例子 1:静态内部类的创建
/**
* <p>
* 静态内部类:声明为static的内部类
* 不需要外围类的对象即可以创建嵌套类
* 嵌套类对象不可访问非静态的外围类对象
* </p>
*/
public class TestStaticsInnerClass {
//静态内部类
public static class Contents{
}
//非静态内部类
class Destination{
}
public static void main(String[] args) {
TestStaticsInnerClass p = new TestStaticsInnerClass();
//非静态内部类,需要外围类的引用来创建
TestStaticsInnerClass.Destination d = p.new Destination();
//静态内部类,通过外围类名来直接创建
TestStaticsInnerClass.Contents c = new TestStaticsInnerClass.Contents();
}
复制代码
代码分析:
在创建静态内部类时不需要将静态内部类的实例绑定在外部类的实例上。静态类和方法只属于类本身,并不属于 该类的对象,更不属于其他外部类的对象。
它解决了一个场景:如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为 static。
与普通的内部类还有一个区别:普通内部类的字段与方法,只能放在类的外部层次上,所以普通的内部类不能有 static 数据和 static 字段, 也不能包含嵌套类。但是在嵌套类里可以包含所有这些东西。也就是说,在非静态内部类中不可以声明静态成员,只有将某个内部类修饰为静态类,然后才能够在这 个类中定义静态的成员变量与成员方法。
3、局部内部类
局部内部类是什么?
我们将局部内部类定位为:在方法和作用域内的内部类。它解决了一个场景问题:我们要想创建一个类来辅助解决方案,但是又不希望这个类是公共可用的。
举例子 2:方法和作用域中的内部类
/**
* <p>
* 方法和作用域中的内部类
* </p>
*/
public class TestMethodInnerClass {
public Destination destination() {
//方法域的内部类(局部内部类:在方法作用域内创建的类),看上去像是“接口实例化”
class PDestination implements Destination {
@Override
public String readLabel() {
return "label";
}
}
return new PDestination();
}
public static void main(String[] args) {
//外围类的方法调用,获取到内部类的引用
TestMethodInnerClass p = new TestMethodInnerClass();
Destination d = p.destination();
}
}
复制代码
代码分析:
方法域的内部类(局部内部类:在方法作用域内创建的类),看上去像是“接口实例化”。
举例子 3:任意代码块中嵌入内部类
/**
* <p>
* 任意代码块中嵌入内部类(内部类与别的类一起被编译。在作用域之外不可访问。此外与普通类一样。)
* </p>
*/
public class TestMethodCodeBlockInnerClass {
private void internalTracking(boolean b) {
if (b) {
//任意代码块中嵌入内部类,那么只能在该代码块中生效,在代码块外不能被访问。
class TrackingSlip {
private String id;
public TrackingSlip(String s) {
id = s;
}
String getSlip() {
return id;
}
}
TrackingSlip ts = new TrackingSlip("slip");
String s = ts.getSlip();
}
// Can’t use it here! Out of scope:
//这里编译会报错,无法引用到 TrackingSlip
// TrackingSlip ts = new TrackingSlip("x");
}
//外围类提供方法1
public void track() {
internalTracking(true);
}
public static void main(String[] args) {
// 创建外围类
TestMethodCodeBlockInnerClass p = new TestMethodCodeBlockInnerClass();
p.track();
}
}
复制代码
代码分析:
任意代码块中嵌入内部类,那么只能在该代码块中生效,在代码块外不能被访问。
4、匿名内部类
匿名内部类是什么?
匿名类本质上是一个表达式,匿名类的语法就类似于调用一个类的构建函数(new HelloWorld()),除些之外,还包含了一个代码块,在代码块中完成类的定义。
举例子 4:匿名内部类的使用
/**
* <p>
* 匿名内部类
* </p>
*/
public class Parcel7 {
//匿名内部类方法-形式1:抽象接口+接口方法实现
public Contents contents() {
return new Contents() {
private int i = 11;
@Override
public int value() {
return i;
}
};
}
//匿名内部类方法-形式2:显式声明接口的实现类并返回该类的实例
class MyContents implements Contents {
private int i = 11;
@Override
public int value() {
return i;
}
}
public Contents contentsV2(){
return new MyContents();
}
public static void main(String[] args) {
Parcel7 p = new Parcel7();
Contents c = p.contents();
Contents c2 = p.contentsV2();
}
}
复制代码
代码分析:
对于匿名类而言,实例初始化的实际效果就是构造器,但它受到了限制:你不能重载实例初始化方法,所以你仅有一个这样的构造器。
匿名内部类与正规的继承相比有些受限,因为匿名内部类既可以扩展类,也可以实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口。
但我们需要明确:应该优先使用类而不是接口,如果设计中需要某个接口,你必须了解它。否则,不到迫不得已,不要将其放到你的设计中。
5、多重嵌套内部类
内部类还可以继续嵌套内部类。
举例子 5:多重嵌套的内部类
/**
* <p>
* 内部类嵌套
* </p>
*/
class MNA{
private void f() {}
class A {
private void g() {}
public class B {
void h() {
g();
f();
}
}
}
}
public class MultiNestingAccess {
public static void main(String[] args) {
MNA mna = new MNA();
//通过外围类引用才能继续创建内部类A类
MNA.A mnaa = mna.new A();
//通过A类引用才能继续创建A类的内部类
MNA.A.B mnaab = mnaa.new B();
mnaab.h();
}
}
复制代码
代码分析:
对于多重嵌套的非静态内部类,我们需要层次分明的构建每一次嵌套的对象,然后通过引用来创建对象。
6、内部类与外围类的继承机制
内部类的继承机制是什么
外围类 A 继承了外围类 B 的内部类,需要注意需要指明继承关系,否则编译器会报错:“No enclosing instance of type 'innerClass.test10.WithInner' is in scope"。
举例子 6:继承内部类的示例
class WithInner{
class Inner{
}
}
// 内部类继承,但是需要指明继承关系
public class InheritInner extends WithInner.Inner {
public InheritInner(WithInner wi) {
//显式调用外围类的引用的构造器
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}
复制代码
代码分析:
继承了内部类的对象,在其构造器的第一行必须显式调用,内部类所在的外围类的对象引用的 super()方法。
因为内部类隐式的持有外部类的引用,所以,我们需要在 InheritInner 的构造方法中显式的调用 WithInner 的构造方法来给 Inner 一个 outer 的引用。
this()和 super()为构造方法,作用是在 JVM 堆中构建出一个对象。因为避免多次创建对象,所以一个方法只能调用一次 this()或 super()。this()和 super()的调用只能写在第一行,避免操作对象时对象还未构建成功。而且 this()和 super()不能同时出现。
7、内部类的编译原理(上)
内部类标识符是什么?
由于每个类都会产生一个.class 文件,其中包含了如何创建该类型的对象的全部信息(此信息产生一个“meta-class”,叫做 Class 对象),因此,内部类也必须生成一个.class 文件以包含它们的 Class 对象信息。
举例子 7:我们对包含一个内部类的外围类进行编译
OuterClass.java
public class OuterClass {
class InnerClass{
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
InnerClass innerClass = outerClass.new InnerClass();
}
}
复制代码
编译指令:
javac -encoding utf-8 OuterClass.java
复制代码
编译结果:
编译结果分析:
这些类文件的命名有严格的规则:外围类的名字,加上“$”,再加上内部类的名字。
实际上还有另外两个规则:
8、内部类的编译原理(下)
承接上面,我们利用 javap 工具来分析 class 文件。
外围类 class 文件:
OuterClass.class:对于外围类编译的类文件,我们主要分析三个方面:
常量池:Class 文件的资源仓库,声明的资源最终被其他项目所调用;
方法表集合:包含了访问标志、名称索引、描述符索引和属性表集合等;
InnerClasses 属性表:用于记录内部类和宿主类的关联,是编译器为外围类和内部类生成的 InnerClass 属性。
内部类 class 文件:
OuterClass$InnerClass.class:对于内部类编译的类文件,我们同样分析常量池、方法表集合、Inner 属性表。
《JVM 虚拟机》对 InnerClasses 属性的定义:
至此,以上就是内部类与外围类的编译文件分析 &相关知识的全部了。
9、总结
这一节里面,我介绍了内部类的四种形态应用(局部内部类/匿名内部类/静态内部类/成员内部类)、继承机制对内部类的影响 以及 内部类编译原理,希望对大家有所帮助。
其实,比起面向对象编程中其他的概念来,接口和内部类更深奥复杂。接口和内部类的使用,理解语法和语义;C++就没有这些困难。但是相比较 C++的多重继承(事实证明非常难以使用),Java 的接口和内部类就相对容易理解了。
10、延伸阅读
《源码系列》
《JDK之Object 类》
《JDK之BigDecimal 类》
《JDK之String 类》
《JDK之Lambda表达式》
《JDK之内部类》
《经典书籍》
《Java并发编程实战:第1章 多线程安全性与风险》
《Java并发编程实战:第2章 影响线程安全性的原子性和加锁机制》
《Java并发编程实战:第3章 助于线程安全的三剑客:final & volatile & 线程封闭》
《服务端技术栈》
《Docker 核心设计理念》
《Kafka史上最强原理总结》
《HTTP的前世今生》
《算法系列》
《读懂排序算法(一):冒泡&直接插入&选择比较》
《读懂排序算法(二):希尔排序算法》
《读懂排序算法(三):堆排序算法》
《读懂排序算法(四):归并算法》
《读懂排序算法(五):快速排序算法》
《读懂排序算法(六):二分查找算法》
《设计模式》
《设计模式之六大设计原则》
《设计模式之创建型(1):单例模式》
《设计模式之创建型(2):工厂方法模式》
《设计模式之创建型(3):原型模式》
《设计模式之创建型(4):建造者模式》
评论