二、内部类
概念:内部类即是内部中的类,它是一种编译器现象。编译器将会把内部类翻译成用$分隔外部类名与内部类名的常规类文件,而虚拟机则对此一无所知。
特性:内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。
语法规则——调用外部类数据成员:
内部类调用外部类数据成员:
格式:OuterClass.this.数据成员。
例如:TalkingClock.this.beep。
优点:
①命名控制:内部命名在外部看来格式为:外部类名::内部类名,所以不会与其他相同类名冲突。(Java包已经提供了相同的命名控制)
②访问控制:通过private定义内部类,对其他类均不可见,其只能通过其外部类中的方法进行访问,不会暴露给其他代码。因此可以将内部类的数据成员设置为公有的也是安全的。
不安全性:
①任何人都可以通过调用access$0方法很容易读取到私有域deep。当然,access$0不是Java合法方法名。但熟悉类文件结构的黑客可以使用十六进制编译器轻松地创建一个用虚拟机指令调用该方法的类文件,由于需要拥有包可见性,所以攻击代码需要与被攻击类放在同一个包中。
②内部类访问私有数据域,就有可能通过附加在外围类所在包中的其他类访问它们,但做这些事情需要高超的技巧和极大的决心。程序员不可能无意之中就获得对类的访问权限,而且必须刻意地构建或修改类文件才有可能达到这个目的。
注意:声明在接口中的内部类自动成为static和public(静态内部类)。
问题一:为什么内部类能够访问外围类私有数据成员?
原理:内部类对象总有一个隐式引用,它指向了创建它的外部类对象。
编译器修改了所有内部类的构造器以及添加了一个用final标识的外部类实例,并且在构造器添加了一个外围类引用的参数。
该参数引用是编译器合成的,在自己编写的代码中不能够引用它。
问题二:我们自己可以实现内部类吗?
原理:不能。这种特性我们自己写代码实现不了,因为不能直接访问别类的私有数据成员。
问题三:编译器如何使虚拟机支持私有类?(虚拟机不支持私有类)
原理:①编译器利用private B(A)私有构造器,生成一个包可见的的构造器 B(A,A$1),此构造器将调用原有的私有构造器。
②并且会在A实例化B中的方法中修改new方法。如B b = new B(A);修改为B b = new B(A,A$1) ;
实例:
①
public class A {
private int deep;
public A(int a){
deep = a;
}
/*
* 编译器在外围类添加静态方法access$0,返回作为参数传递给它的对象域deep。
*/
static int access$0(A);
public void startA() {
B b = new B();
b.startB();
}
}
②
public class A$B{
final A this$0 ; //编译器添加了一个用final标识的外部类实例
public A$B(A) ; //编译器修改了所有内部类的构造器,并且在构造器添加了一个外围类引用的参数。
public int startB(){
return A.this.deep ; //正确:内部类具有特殊权限,可以访问外围类的私有数据
//return access$0(a) 这样调用会提高调用的效率
}
}
分类:
1)局部内部类:当只在某方法中使用了一次这个类型对象时,采用局部内部类。
语法规则:①不能使用public或private访问说明符进行声明,因为其作用域被限定在声明这个局部类的块中。
②不能调用外围类数据成员;
③局部变量必须声明为final。
优点:①对外界完全地隐藏起来。除了该方法之外,没有任何方法知道该内部类的存放,连外围类也不知道该类的存在。
②不仅能够访问包含它们的外围类,还可以访问局部变量(这些局部变量必须声明为final)。
问题四:为什么Java规定局部内部类调用外围方法的参数要声明为final?(定义final可以不初始化,final参数能在定义后被初始化一次)NULL就是没有引用。
原理:简单:因为这样保持了局部变量与在局部内部类内的拷贝引用保持一致。
详细:在内部内访问局部变量,编译器实际上会为该内部类创建一个成员变量,以及带有参数的构造方法,然后将该变量传入构造方法,也就是说外围方法变量和内部类里面的变量只是名字相同而已,此时
你无论修改哪一个变量都对另一个不产生影响。如果我想在内部类里面修改这个值的话,就影响不到外围方法的参数,修改的值传不出来,那么修改就没意义了。为了防止这种矛盾出现,故规定只准
问题五:编译器为什么会为内部类调用的外围参数备份?使用final变量。(final能限制复制品与原始引用一致)外围类数据成员的话,实际上内部类访问就是用类似于Test.this.xxx的形式访问跟类信息或者对象信息有关的,所以修改哪一个都会造成变量值的修改,就不存在上面的那种矛盾了!
原理:是参数的生命周期问题。因为当外围方法调用结束的时候,该方法的参数变量也不复存在。
实例:
class A$1B{
//创建带有deep参数的构造器
A$1B(A,int deep) ;
public int startB() ;
//创建final说明标识符的deep变量,就是为了该引用与外围方法参数的引用保持一致。
final int val$deep ;
final A this$0 ;
}
2)匿名内部类: 只创建这个类的一个对象时,采用匿名内部类。
原理:创建了一个实现了 接口或超类 的类 的新对象。
格式:new (superType or interfaceType)(){
method and data
}
优点:可以节省写代码的时间,更加实际,可以说便于理解。
缺点:让人感觉混乱。
实例:
public void startA(){
/*
* 其实质是创建了一个实现了 ActionListener接口的类 的一个新对象。
*/
ActionListener listener = new ActionListener(){
public void actionPerformed(ActionEvent event){
System.out.println("调用成功!") ;
}
}
}
3)静态内部类:只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象,静态内部类目的就是为了取消产生引用。
原理:通过static取消内部类里面的外围类的引用。
优点:可以返回内部类的实例对象。
实例:
class A{
public static class B{
private int first ;
public B(int first){
his.first = first ;
}
public int getFirst(){ return first ;}
}
/*
* 在return new B(first)中,如果B不是静态内部类时,编译器将会给出错误报告:没有可用的隐式A类型对象初始化内部类对象
*/
public static B minmax(int first){
return new B(first) ; //注意:如果B不是静态内部类时,编译器将会给出错误报告:没有可用的隐式A类型对象初始化内部类对象。
}
}
问题六:为什么静态方法中只能返回静态内部类实例对象,而其他内部类不行?
原理:因为静态方法实在程序编译最初时期产生的,而非静态类则晚于静态方法产生。因为内部类里面调用了外围类的引用,在静态方法产生的时候还没有实例化外围类,固然没有外围类的引用,所以如果使用
静态方法使用非静态内部类的时候,将会报出“没有可用的隐式A类型对象初始化内部类对象。”的错误!
而静态内部类则隔绝了外围类的引用,也就是说不再引用外围类引用,所以外围静态方法只能调用静态内部类。