更新:
2015/11/28更新:增加多态中向上转型,方法重载,动态绑定有关编译/类加载/分派相关知识整理;
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
主要是从面向对象的几个特性:封装、继承(复用)、多态的顺序总结Java有关的基础知识,它们的核心目的就是解耦和复用。
从接口和实现分离的角度:
封装:定义了“什么可以做和什么不可做”;
继承和组合:提供了复用的两种手段;
多态:定义了“做什么和怎么做”;
接口和内部类:使得接口和实现分离更加结构化,都是是Java实现多重继承的变种手段;
1. 访问控制权限:
1.1 访问控制权限的种类:
私有:private,default-private(包访问权限);
保护:protected,protected也给予了包访问权限;
公有:public;
1.2 为什么要访问控制权限:
信息隐藏:私有权限可以增强内聚性,便于之后的版本更新;公有权限说明对外界的承诺,必须兼容。
1.3 重要概念:
编译单元:一个Java源代码文件,最多顶层可以有一个public类,该类的名称必须与文件名相同;
封装:具体实现的隐藏。把数据和方法包装进类中,以及具体的实现隐藏,共同称为封装;
接口和实现:将访问控制的界限放在类的内部是为了(1)控制客户端程序员的访问;(2)接口和实现的分离;
2. 最小化访问权限的要点:
(1)只用一个类使用的顶层类,可以变成嵌套类;
(2)Serializable可以导致意外的“信息泄漏”;
(3)公有类的protected声明也是一种对外的承诺,必须保证兼容;
(4)子类不能降低父类成员和方法的权限,父类的方法/域如果是protected的子类可以提升为public,如果是父类中是private的,子类中同签名方法是一个新方法而不是重写;
(5)接口的方法是public的,域是public static final的;
3. 不应该公开的域:公开会导致多线程环境下的不安全和漏洞:
(1)实例域;
(2)静态域(除了一些全局final变量);
(3)final域中不应该包含非final的引用;
(4)引用数组域不应该公开,即使数组是final的,数组元素也不是(使用保护性拷贝,Collections.unmodifiableList等,返回clone数组等);
PS:在公有类中使用访问方法而不是公有域;
2. 每个类都有的方法:
共有9个方法:equals,hashCode,clone,getclass,toString,wait(3个重载版本),notify,notifyAll,finalize;
2.1 覆盖equals的通用约定:
2.1.1 不需要覆盖得到有意义的equals的情况:
(1)类的每个实例本质上是唯一的(包括全局唯一单例,枚举等);
(2)不关心类是否提供了“逻辑相等”的测试功能;
(3)超类已经覆盖了equals,从超类继承equals方法也是合适的;
(4)类是私有或包级私有的,可以 确定它的equals永远不会被调用,可以覆盖它抛出异常;
2.1.2 等价关系:
(1)自反性;
(2)对称性:在子类覆盖equals时,如果增加的了新的属性比较,父类调用equals为true时,子类调用可能就会为false;
(3)传递性:如果在上面的情况保证对称性,传递性可能就无法保证;
(4)一致性:只要不修改,多次调用结果一致;
(5)非空性:对于任何非null引用x,x.equals(null)必须返回false;
2.1.3 覆盖equals的注意:
(1)无法在扩展 可实例化的类的同时,既增加新的值组件,同时又保留equals约定;
(2)使用getClass来代替instanceof的方法,也限制了在扩展新子类的情况下,保证一些父类的功能;
(3)使用复合代替继承,或者使用抽象父类;
(4)java.sql.TimeStamp违反了对称性,不要在Set中混合使用TimeStamp和Date(jdk1.7及以上没有这个问题因为在TimeStamp中getTime计算了nanos的值);
(5)java.net.URL的equals依赖对URL中主机IP的比较,而主机名和IP需要DNS,可能造成不确定的结果;
2.1.4 覆盖equals的好做法:
(1)覆盖equals时总要覆盖hasCode,以免equals相等而hashCode不相等,导致HashMap/Set等不能正常工作;
(2)对float和double进行特殊处理,因为存在Float.NaN,-0.0f等情况;
2.2 覆盖hashCode方法:
2.2.1 Object规范中hashCode的约定:
(1)如果两个对象equals为true,hashCode返回的结果必须相等;
(2)如果两个对象equals为false,hashCode返回的结果不一定要不同,但是不同可以提高散列性能;
(3)一致性:如果涉及的值没有修改,返回值必须一致;
2.2.2 好的散列函数:为不相等的对象产生不相等的散列值
(1)引用域递归调用hashCode方法;
(2) 使用31作为系数,一是奇素数防止偶数乘法溢出信息丢失,二是编译器会优化成(i<<5) - i;
(3)不可变类的hashCode计算一次可以缓存;
2.3覆盖clone方法:
Object虽然提供了clone方法,但是没有实现Cloneable接口,它让需要的子类去实现,如果一个类没有实现Cloneable接口就调用clone方法,会抛出CloneNotSupportedException异常;
覆盖clone方法注意:
(1)super.clone()是原始对象的复制,复制的是基本类型和引用值,也就是浅拷贝;
(2)通过JDK1.5提供的协变返回类型,可以将clone的返回修改为具体的类型;
(3)数组需要调用它的clone方法进行递归的复制,并且编译器会将array.clone()方法类型转换成我们需要的;
(4)对于链表结构的对象,需要自己实现深拷贝方法,使用迭代优于递归;
(5)不要在clone方法中调用任何可被覆盖的方法;
(6)专门为了继承而设计的类,不要实现Cloneable接口,由子类决定;
PS:拷贝工厂和拷贝构造器提供另一种选择;
2.4 实现Comparable接口:
comparable的行为和特定和equals等价关系很像。
注意:
(1)comparable结果为0,equals结果为true是约定,但是不一定实现,BigDecimal类,就违反了这个约定,new BigDecimal("1.0")和new BigDecimal("1.00")的equals结果为false,但是使用TreeSet而不是HashSet就会发现,它们是相等;
(2)CompareTo方法依赖与参数化,而不是类型检查;
(3)对于整型可以用>,<,浮点型应该用Double.compare和Float.compare方法;
(4)注意在使用差值计算比较大小时可能出现的溢出问题(正数减负数);
3. 复用类(组合,has-a和继承,is-a):
3.1 复用与初始化:
(1)组合:主要有4种初始化形式:
a.在定义处进行初始化,在构造器调用前就会进行初始化;
b.实例初始化;
c.惰性初始化;
d.构造器中进行初始化;
(2)继承与初始化:
构造器调用链(栈):先调用基类的构造器;
因此在子类的构造器中需要在其他所有语句前显调用父类的构造器,如果是默认构造器,编译器会自动补上;
3.2 复用与清理:
继承中的清理:
finalize方法会显调用子类的再调用父类的(如果覆盖了的话),但它不能保证资源被及时释放;
使用自定义dispose()方法时,要注意调用父类的super.close/dispose方法, 调用顺序保证和finalize相同;
3.3 继承中的方法覆盖和重载:
(1)子类中可以覆盖和重载父类的非私有方法(C++并不是这样);
(2)继承的类和接口之间,接口和接口之间有相同的方法,签名一致,返回类型一致,可以“共用”一个方法;
public class ReuseClass {
private interface I {
void i();
}
private interface R {
void i();
}
private abstract static class A {
public abstract void i();
}
private static class B extends A implements I, R {
//可以用内部类来模拟多重继承解决意外的“重名”
@Override
public void i() {
}
}
}
(3)
继承的类和接口之间,接口和接口之间有相同的方法,签名一致,返回类型不一致,会产生冲突,可以通过组合和内部类解决;
public static class RI {
private interface I {
void i();
}
private interface R {
int i();
}
private abstract static class A {
public abstract void i();
}
private static class B extends RI.A implements RI.I {
//可以用内部类来模拟多重继承解决意外的“重名”
@Override
public void i() {
}
public RI.R makeR() {
return new RI.R() {
@Override
public int i() {
return 0;
}
};
}
}
}
3.4 向上转型:
向上转型一般是安全,导出类是基类的超集;
3.5 final关键字:使可变性最小化(典型的:String类)
3.5.1 final使用情况:
(1)final域:注意必须要在使用前进行初始化( 实例初始化器或 构造器);
(2)final参数;
(3)final方法:
方法锁定,防止继承修改;
不要用它来优化效率;
所有的private方法隐式地是final的(子类的同名方法不是Override是一个新方法);
(4)final类:防止继承,所有的方法都是隐式final的;
3.5.2 不可变创建的准则:
(1)不要提供任何会修改状态的方法;
(2)保证类不被扩展(private和final声明);
(3)所有域是final的;
(4)所有域是私有的(private和包级私有);
(5)保证对任何可变组件的互斥访问,如果存在非final引用,确保客户端程序不能获取它(保护性拷贝);
3.5.3 不可变对象的创建:
(1)通过构造器注入可变对象来初始化,一定要进行保护性拷贝;
(2)如果域之间存在约束条件,在创建前进行检查,创建之后状态不变,保证约束条件不变;
(3)通过静态工厂valueOf+享元模式,可以减少新对象的创建(比如,Integer等的valueOf,Integer的享元通过数组保存对象引用,字符串常量池等);
public final class FinalTest {
private final String a;
private Inner inner;
//通过实例初始化赋值final域
{
a = "final";
}
//这是不安全的做法,客户端可以在外部修改inner
// public FinalTest(Inner inner) {
// this.inner = inner;
// }
public FinalTest(Inner inner) {
//Inner是非final类,调用它的clone方法并不安全,因为可被子类覆盖
// this.inner = inner.clone();
//这是一个安全的做法,因为我们使用了明确的构造器
this.inner = new Inner(inner.a);
}
//对于没有实现cloneable接口的类,调用clone方法会抛出异常(Object中的clone是protected和native的)
public static class Inner implements Cloneable {
private int a;
public Inner(int a) {
this.a = a;
}
@Override
public Inner clone() {
Inner inner = null;
try {
inner = (Inner)super.clone();
inner.a = a;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return inner;
}
}
}
3.5.4 不可变对象的优点:
(1)线程安全的,可以安全的共享;
(2)不用实现clone方法,可以直接使用;
(3)内部信息可以共享,比如String的substring方法返回的是指向同一final数组的新String对象引用;
(4)hashCode值可以计算一次缓存起来;
3.5.5 不可变对象的缺点:
问题1:[真正意义的缺点]由于不能修改,在一些变值的操作中可能产生大量的对象;
解决:提供配套类进行操作,使用享元模式缓存(字符串常量池等),比如StringBuilder于String;
问题2:不可变类如果包含一个或多个可变对象的域在序列化时并不安全(伪字节流攻击,内部域盗用);
解决:
(1)提供保护性拷贝的readObject方法:
必须提供一个readObject方法;
在readObject中进行保护性拷贝,并检查约束条件,不满足则抛出InvalidObjectException;
注意使用readObject中的保护性拷贝,域的final的关键字要去掉;
(2)readResolve方法和枚举类型:
因为单例模式中我们也经常使用private static final;
无论使用默认的序列化形式,还是自定义序列化形式,是否提供readObject无关,都会创建新的实例,违反了“单例”的要求;
a.使用readResolve方法,直接返回单例引用,新建的引用丢弃:
private static class ReadResolveWrapper implements Serializable {
public static final ReadResolveWrapper SINGLETON = new ReadResolveWrapper();
//所有的域都应该是transient的
private transient A a = new A();
private ReadResolveWrapper() {}
private Object readResolve() {
return SINGLETON;
}
}
b.使用枚举类型来实现单例;
(3)序列化代理代替序列化实例(静态内部类,性能比保护性readObject要高,但是可以使用final关键字,更加稳定):
序列化代理的默认序列化形式是外围类最好的序列化形式:
局限:一是不能与可以被客户端扩展的类兼容;二是不能与对象图中包含循环的某些类相兼容;
3.6 继承与类加载和初始化:
过程概述(下面说的子类是代码中的直接使用类,相对于它的父类):
(1) [类加载]:先检查是否有基类,直到检查到一个尚未被加载的根基类。先加载根基类基类,再加载其子类,依次类推,类加载的过程是先父后子的;
(2) [类初始化]:如果访问一个子类,如果父类没有初始化,会先初始化父类;
(3) [实例初始化和构造器]:先进行父类的实例初始化并调用构造器,再进行子类的实例初始化并调用子类的构造器器;
4. 多态(向上转型,方法重载和动态绑定):
多态是什么:
多态表示不同的对象可以执行相同的动作,但要通过它们自己的实现代码来执行;
多态的作用:
接口和实现分离的另一角度的设计,之前已经提到通过访问控制权限来封装进行接口和实现的分离。
消除类型之间的耦合关系,使得程序代码获得了可扩展性。
多态建立在继承或者接口实现,重写等机制的基础上,接口和实现的分离,主要的方式之一就是 向上转型和 动态绑定;
PS:下面会概括的解释向上转型,方法重载和动态绑定在编译,类加载或运行时的实现原理,其中编译时验证和类加载时的验证都是必要,类加载时的验证是为了避免伪造的字节码,因为Java是基于开放来源的二进制字节流的Class文件可能来自于网络等。
4.1 向上转型:
把某个对象的引用作为对其基类的引用;
注意:
(1)子类以父类的身份出现;
(2)子类在工作时,有自己的代码实现;
(3)子类以父类的身份出现时,子类特有的方法和属性不能使用;
实现和保证:
(1)编译时,在语义分析阶段,在Attr标注检查阶段,委托com.sun.tools.javac.comp.Check来检查方法返回类型与接受的引用类型之间是否匹配;
(2)类加载时,在字节码验证阶段,检查方法体中的类型转换是否有效,子类对象赋值给父类数据类型是安全的,父类对象赋值给子类数据类型是不合法的;
(3)数据流验证具有复杂性,JDK1.6后通过Code属性表的“stackMapTable”属性,它描述了方法体中基本块本地变量表和操作栈的应用状态,通过类型检查而不是类型推导效率更高。
4.2 方法重载(静态分派/解析):
方法重载是编译/解析时确定的,因此有些人不认为它属于“多态”,不过这里还是一起联系讨论一下。
形如SuperClass subClass = new SubClass();
SuperClass是subClass的 静态类型,SubClass是其 动态类型;
实现和保证:
编译时,编译器根据调用方法的 简单名称和 参数数量以及参数的 静态类型,根据 优先级决定使用哪一个版本,将方法的符号引用写到invoke~指令的参数中;
重载的优先级:以静态类型为char的变量为例,优先级与转型相关
(1)char类型本身,如f(char);
(2)宽化转型类型,顺序char,int,long,float,double,f(int)等;
(3)自动装箱类型:Character;
(4)父类/实现接口:如Serializable,同理在没有f(SubClass)是subClass(静态类型为SubClass)可以匹配f(SuperClass);
类/接口实现体系中越往上层,优先级越低,存在同一优先级具有模糊性,编译会报错。
(5)可变参数列表:如f(char...c)优先级最低;
PS:实际中不应该设计如此复杂的重载版本方法;
4.3 动态绑定(动态分派):
实现多态的原理就是动态绑定(后期绑定,运行时绑定),区别与编译时进行的静态绑定,动态绑定通过对象中的类型信息在运行的时候决定具体的调用方法。
Java中哪些是动态绑定的:
非final(包括private)方法的实例方法都属于动态绑定的,也就是通过invokevirtual和invokeinterface两个指令处理的调用;
Java中哪些不是动态绑定的:
域(成员变量),static方法,private方法(private是隐式final的)都是前期绑定的,也就是编译的时候进行绑定,它们不存在多态行为;
因此前面提过的private方法在子类中定义同名的方法,并非是“覆盖”而是一个新的方法,JVM不会用invokevirtual指令来处理它,而是invokespeical指令;
对于final方法,final虽然通过invokevirtual指令调用,但是它是非虚方法,在类解析阶段就可以确定调用方法版本。
实现和保证(invokevirtual指令的多态查找,基于Hotspot实现):
(1)取得操作数栈栈顶元素(直接引用)所指向的对象的实际类型,记作C;Hotspot过程是通过直接引用找到堆中的实例数据,根据对象头中的类型指针找到方法区中Class对象;
(2)如果在类型C中找到与常量池中的描述符和简单名称都相符的方法,进行符号引用验证,检查访问权限,成功,返回方法的直接引用查找结束,失败,抛出java.lang.IllegalAccessError;
(3)如果没有在类型C中找到,按照继承关系 从下往上对C的各个父类进行第2步的查找过程;
(4)如果最后没有找到合适的方法,抛出java.lang.AbstractMethodError;
JVM动态分派实现:
优化的必要性:虚方法调用是最为频繁的,因此如果每次调用同一个方法都按照继承链查找效率太低;
解决方法: 虚方法表/接口方法表
(1)子类如果没有实现某个方法,同样会包含父类方法的地址入口等信息;
(2)具有相同签名的方法,在父类、子类的虚方法表中具有一样的索引序号,在类型转换时按索引转换出所需的入口地址;
(3)初始化,在类加载的 连接阶段(包括验证,准备,解析)进行初始化方法表, 准备(准备阶段)类的变量初始值后,虚拟机将该类的方法表也初始化完毕;
PS:另外还有内联缓存,守护内联两种非稳定的优化手段。
4.4 实例初始化和多态:
在构造器,clone方法,readObject方法中不要使用可覆盖的方法;原因在于可覆盖方法在构造时通过invokevirtual会使用对象在继承树中最低类型的方法实现,如果依赖于子类的成员变量,可能不能得到正确的初始化而产生错误。
4.5 协变返回类型:
导出类的被覆盖方法可以返回基类方法返回类型的某个派生类型;
4.6 用继承进行设计:
4.6.1 什么时候使用继承:
存在明确“is-a”关系时才使用继承;
明确需要使用“向上转型”的情况需要使用继承;
用继承表达行为间的差异,用字段(组合)表达状态上的变化;
(1)为继承而设计的类要编写文档说明(包括方法间的依赖关系);
(2)不要在构造器,clone,readObject中调用可被覆盖的方法,也就是说在这些方法中使用动态绑定并不安全;
(3)回调框架不适合使用组合;
PS:禁止类实例化,(1)private构造器;(2)abstract声明类;
“is-like-a”关系:
继承中除了向上转型,即使存在“is-a”关系软件开发的需求的变化使得需要依赖派生类型的特性成为可能。
Java中对象是带有类型信息的,通过运行时类型识别(反射),判断或者转换为具体的派生类。
Java所有的转型都是经过判断的,通过checkcast指令,错误抛出ClassCastException;
4.6.2 组合(复合)优于继承:
如果同一个包中使用继承没有什么问题;
跨包继承可能出现的问题:
(1)破坏了封装性,子类随着超类的改变可能不安全;
比如,HashSet中addAll通过add方法实现的,这种关系可能带来子类的不正确的行为,而这种可覆盖方法间的关系并不是“承诺”而是具体实现;
(2)超类中添加了新的方法,可能会破坏子类的一些约束条件;
(3)子类中添加了新的方法,可能因为父类之后添加新的同名(返回类型不同)方法而出错;
(4)单继承限制了子类的扩展能力;
5. 接口:
5.1 抽象类和抽象方法:
防止类实例化,abstract方法必须包含abstract类;
抽象类和重构:
抽象类是很有用的重构工具,当发现存在公共方法时,可以将它上移放到抽象类中,这也是 模板方法模式的体现;
5.2 接口:
接口本身可以选择public,默认包权限,private,protected;
接口中的域是public static final的;
接口中的方法是public的,实现接口时只能将接口方法实现为public的,因为Java编译器 不允许降低访问权限;
5.3 完全解耦,接口优于抽象类:
(1)Java中类之间只允许单继承,如果在方法参数等地方使用类,那只能限制于这个类及其子类对象传入,使用接口可以避免耦合于类型;
(2)单继承的另一个问题是导致“类的组合爆炸”,如果方法要接受两种类体系的类,必须提供一个要增加一个公共的祖先,而通过接口,让需要传入的对象,实现这个接口就可以了;
在客户端代码中使用依赖于接口,而不是类,进一步增强可扩展性和复用性;
抽象类和接口的结合使用:
(1)结合接口和抽象类,在提供接口体系的同时,通过AbstractInterface提供骨架类,提供一些基本的实现;
(2)抽象类往往位于接口体系的“底层”,因为这样功能体系基本稳定,而且抽象类专为继承实现,而且只提供最核心和基础的共同的实现,典型的例子Java Collections Framework,Spring BeanFactory Framework;
5.4 接口与多重继承:
5.4.1 通过继承扩展接口:
用接口层次代替类层次;
5.4.2 接口和类“多重继承中”名称冲突:
(1)如果要实现的接口或者类中,用方法签名相同(名称和参数列表),但返回类型不同的,无法直接同时实现/继承;
(2)即使返回类型相同,也不能保证两个接口/类的同名方法应该用同一个实现而没有问题;
(3)对于包含同名方法的接口,使用组合而不是实现/继承,用内部类实现接口/类,通过返回一个内部类的实例(闭包)来避免名称冲突;
5.5 接口域与初始化:
(1)接口中的域是public static final的;
(2)接口只用来只用来定义类型,用接口存放常量是一种反模式;
(3)接口中域的初始化发生在该接口中某个域被一次访问时;
(4)接口的初始化与类不同,子接口的初始化不会导致先初始化父接口;
5.6 嵌套接口:
(1)接口中不能嵌套包含private接口;
(2)接口中也可以嵌套包含class,是public static的;
(3)接口中的方法是public的,域是public static final的,接口和类是public static的;
(4)class中可以包含private接口,这样可以防止向外部返回它的任何实现;
6. 内部类:
6.1 内部类的种类:
(1)静态成员类;
(2)非静态成员类;
(3)局部内部类:任何可以定义局部变量的地方都可以定义局部内部类,主要用于复用;
(4)匿名内部类;
6.2内部类与闭包的实现:
通过接口+私有内部类,可以创建“闭包”,客户端代码无法访问到具体的类型,同时私有内部类可以获取到外围类对象的状态;
Java中使用这种闭包的例子:
List等集合类中的Iterator的实现;
6.3 4种内部类的要点:
6.3.1 非静态成员类:
(1)创建:.new,在外部创建一个外围类C的内部类用:c.new关键字; ;
(2)构造器:经过编译器编译获取了外围类的this指针作为内部类的成员变量:this$0;
编译器为非静态内部类生成一个带有外围类类型的参数,用来注入外围类的引用;
6.3.2 匿名内部类:
(1)匿名内部类的创建:可以用类,抽象类,接口来定义匿名内部类,匿名内部类其实是它们的派生类;
(2)匿名内部类的构造器:不能手动定义构造器,Java编译器会根据使用的基类的构造器,为匿名内部类生成一个带有基类类型的参数的构造器,同时为匿名类添加一个基类类型的成员变量,通过构造器设置这个变量包含外围类的引用;
虽然不能创建构造器,但是可以使用实例化器;
(3)匿名内部类的名称:默认是基类名称$n,n是调用类中第n个匿名内部类,这是一种编译器的内部机制,尽管可以通过这个名称使用反射获取指定构造器来创建匿名内部类对象,但这不是匿名内部类应该有的用法;
(4)匿名内部类不能再进行扩展;
6.3.3 静态成员类:
(1) 类初始化:与外围类没有直接关系,类初始化的触发条件仍然和普通类一样,5点;
(2) 实例化:与外围类没有直接关系;
(3)不能从静态成员类内部访问外围类的非静态成员和方法;
6.3.4 局部内部类:
主要是为了复用和局部使用需要一个重载的构造器,否则一般不会用;
只要能定义局部变量的地方就可一定义;
它的“作用域”和局部变量一样;
不能有访问修饰符;
可以访问外围的成员和代码块的 常量;
6.4 内部类的作用:
(1)多重继承:
内部类和接口是解决Java多重继承的方案;
接口可以在设计的时候,将一些通用的功能设计成接口,但是对于系统中已经有的类,想要多重继承多个类,只能使用内部类的方式实现;
使用内部类可以解决接口/类继承时的名称冲突;
(2)闭包和回调:
由于内部类可以访问外围的所用状态(包括private的),因此实际上它是一个“闭包”;
Java中没有函数指针,因此回调,通过“函数对象”实现,闭包可以作为回调对象传递给调用者;
类似的用法:Thread+runnable(命令模式),事件监听(观察者模式)等等;
6.5 内部类的继承:
构造器的实现:
继承一个非静态内部类时,外围类.super()并不是在调用外围类的基类构造器,而是要继承的内部类的构造器,这是一种特殊的语法,相同的还有“外围类.new”想到这.new应该就不会对".super"过于纠结了吧。
外围类实现:
class A {
A() {
System.out.println("A");
}
}
public class InnerClassTest extends A {
class Inner {
public Inner(int x) {
}
}
}
继承内部类:
public class InnerExtend extends InnerClassTest.Inner {
public InnerExtend(InnerClassTest test, int x) {
test.super(x);
}
public static void main(String[] args) {
new InnerExtend(new InnerClassTest(), 1);
}
}
PS 内部类不可以被“覆盖”
内部类不可以被覆盖,继承一个外围类时,即使提供一个同名的内部类也不是覆盖,查看编译之后的class文件可以发现实际上,这个内部类类名是“外围类$内部类”,外围类只是内部类的“命名空间”;
参考资料:
[1] Java编程思想 第4版;
[2] Effective Java;
[3] 深入Java Web技术内幕;
[4] 深入理解Java虚拟机;