写在前面,本文有部分内容引用自***java核心技术***
成员内部类
内部类就是构建于一个类里面的类。
内部类的特点:
-
不仅可以访问自身的数据域,还可以引用其外部类的变量(包括私有变量)
public class outerClass { private int privateNum=0; public int publicNum=1; public class innerClass{ int number; void getOuter(){ System.out.println(privateNum); System.out.println(publicNum); } } }
也就是说,在内部类中存在一个指向外部类的指针。
在内部类中,可以用外部类.this来表示这个指针,以免造成混淆。
上面的打印语句可以改成这样:System.out.println(outerClass.this.privateNum); System.out.println(outerClass.this.publicNum);
除了访问之外,还可以对成员变量进行修改。
在内部类调用外部类的私有变量时,编译器会在外部添加一个access方法,用于取得外部的private变量,通过字节码指令查看:
0 aload_0 1 getfield #2 <Test/BasicTest/outerClass.privateNum : I> 4 ireturn
如果调用了多个静态变量,则会产生多个access方法
-
反过来说,外部类也可以通过一定方法来声明一个内部类对象
我们可以简单地在一个非静态方法中声明:innerClass i = new innerClass();
如果要在这个类的外部进行声明:
outerClass o = new outerClass(); outerClass.innerClass i = o.new innerClass();
声明引用变量时,由于要指明是哪个类,所以要加上外部类来进行限定;
那为什么在使用new调用构造函数时,还需要指明具体对象呢?
当不指明具体对象时,报错信息一直提示将其转换成一个静态类。我们可以把一个内部类当成外部类的一个属性,如果为非静态的,那么它就是一个‘实例变量,如果为静态的,那么它就是一个类变量。
事实上,由于非静态内部类可以访问外部类的实例变量,所以在声明时必须明确它到底要访问哪一个具体的实例。(我们之后再谈静态类) -
不能声明静态变量、静态方法等等
内部类的其它表现形式:局部内部类、匿名内部类、静态内部类。
局部内部类
局部内部类是定义在一个方法内部的类。
特点:
- 局部类无法用public或private访问说明符进行修饰,作用域被限定在这个局部类的块中。这意味着,即使是外部类的其它方法也不能使用这个类。
- 局部类同样可以访问/修改外部类的变量,这点与成员内部类一样。不同的是,局部类对于局部变量有特别的访问方式。
这段代码可以成功运行,看似没什么大惊小怪的。但我们设想一个场景,当调用void newInner(){ int num=0; class innerClass implements Inner{ int number; void getLocal(){ System.out.println(num); } } innerClass i = new innerClass(); i.getLocal(); }
newInner()
这个方法时,步骤是:- 声明一个局部变量
num=0
- 调用局部类
innerClass
的构造器,以便初始化对象i - 调用
getLocal()
方法
我们都知道,局部变量表仅会存储当前方法声明的局部变量,如果我们改用一个定时器调用getLocal()方法,使其延迟执行,这时方法已经结束了,局部变量已经死了,它会从哪里取得num
的值?
回过味来了吧?局部类的神奇之处就在于此。
对于一个普通类的实例来说,其实例变量是存在堆里的,可以很轻松地取得。
对于一个成员内部类来说,它可以取得外部类的实例变量,是因为它本身算作是实例变量的一员。
那么,对于一个局部类来说,若要使用局部变量,就必定有一个特殊的地方来存放这个局部变量。
口说无凭,我们上代码:
通过反编译字节码文件,我们果然在局部类中发现了一个新的变量final int val$num; descriptor: I flags: ACC_FINAL, ACC_SYNTHETIC
num
, 并且被修饰为final。
为什么要声明为final?
我们现在都知道局部类对局部变量的访问方式是在内部拷贝一个变量,这种方式似乎有些类似方法参数的传递。但方法参数的值是可以改变的(如果未声明final),这种改变并不影响要传进来的原变量的值,为什么在这里不能这么做呢?
如果我在多处改变局部变量的值,那么局部类到底该拷贝哪一个值?这会导致不确定性。void newInner(){ int num=0; class innerClass implements Inner{ int number; void getLocal(){ System.out.println(num); } } num=1; innerClass i1 = new innerClass(); num=2; innerClass i2 = new innerClass(); }
其次,我们的本意是借用局部变量,而非声明一个新的对象,或使用其他的对象,这样就失去了借用的意义了。使用final可以保证引用的一定是同一个对象。
最后,内部类在形式上看起来像是直接使用局部变量,因为内部类内部是隐式地持有这个复制,如果在内部修改这个复制的值,看上去就像直接修改这个局部变量,这显然又会造成一定程度的误解,
(以上三个解释都是我个人的理解,仁者见仁智者见智) - 声明一个局部变量
匿名内部类
如果我只需要创建这个类的一个对象,就不需要命名了。
语法格式为:
new SuperType(parameters){
inner class methods and data
}
其中SuperType
可以是接口,内部类就需要实现这个接口;也可以是一个类,内部类则要去扩展它,内部类就相当于这个类的子类。
由于其没有类名, 所以匿名类不能有构造器,而是将构造器参数传递给超类构造器,如果实现的是接口,那么就不能提供任何参数。
双括号初始化
在构造函数之后加一对大括号是要扩展这个类,再加一对括号就是使用对象构造块,可以在里面进行初始化。
于是就有这样的用法:
new ArrayList<String>() {{add("Harry"; add("Tony"))}};
是不是显得比较简洁?
而其它规则与前面一致。
静态内部类
有时候内部类仅是为了把一个类隐藏在一个类的内部。
只有内部类可以声明为static
静态内部类我们可以把它当做是类的静态变量,但其却并非和静态变量一样在外部类加载时加载/初始化,而是在用到了这个类时才会被加载(比如调用静态变量、静态方法等等,和普通类的加载情况是相似的)
对于静态内部类,我们不需要通过外部类的实例来声明一个静态类的实例。而是通过外部类来找到静态类,这也符合我们对静态变量的印象。
void getStaticInner(){
outerClass.staticInner s = new outerClass.staticInner();
}
除此之外,内部类中,仅有静态类才能使用静态修饰符。