一.引言
内部类可以简单分为静态内部类和非静态内部类,匿名内部类,局部内部类,匿名内部类和局部类,不过静态内部类和非静态内部类才是本文的重点,会花更多的笔墨去介绍,匿名内部类和局部内部类更多是一笔带过。
二.静态内部类
静态内部类可以当作普通的类来使用。
2.1 实例化方式
静态内部类实例化方式有两种。
第一种方式与实例化普通类一致,通过“类名 实例名字=new 类名()”的方式进行实例,但这种方法只限于在持有该内部类的外部类的静态方法中使用。
public class OuterClass {
static class StaticInnerClass{
}
public static void main(String[] args) {
StaticInnerClass staticInnerClass=new StaticInnerClass();
}
}
第二种方式是通用的,实例化时不仅需要内部类名字,也需要持有该内部类的外部类名字。
class AnotherClass{
public static void main(String[] args) {
OuterClass.StaticInnerClass staticInnerClass=new OuterClass.StaticInnerClass();
}
}
2.2 权限
由于静态内部类可以不通过外部类的实例创建,静态内部类不允许访问外部类的非静态方法和属性。
2.3 适用情况
当内部类不需要访问外部类时,一般就可以把之设为静态内部类。
常见的静态内部类应用是充当外部类的组件类,该组件类不需要访问外部类,比如说Java容器里面的Entry或者Node类,下面是HashMap中静态内部类Node的部分源码。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
...
}
此外,当静态内部类不允许被其它类访问时,应该将其设置为private。
三.非静态内部类
非静态内部类的实例是与外部类的实例关联的,理解了这句话,内部类的内容就很好理解了
3.1 实例化方式
这里需要注意的是,非静态内部类的实例是与外部类的实例联关联的,换句话说,要实例化内部类,必须得先实例外部类。
与静态内部类相同,非静态内部类也有两种创建方式,核心都是通过外部类的实例进行,但由于进行实例化的位置不同,方式有所区别。
第一种方式适用于外部类的非静态方法。
public class OuterClass {
...
class InnerClass{
}
public void func(){
InnerClass innerClass=new InnerClass();
}
...
}
上述做法之所以能运行的原因是,非静态方法是与外部类实例关联的。
第二种方式是通用的,即先实例化外部类,再通过外部类实例化内部类。
class AnotherClass{
public static void main(String[] args) {
OuterClass outerClass=new OuterClass();
OuterClass.InnerClass innerClass=outerClass.new InnerClass();
}
}
3.2 权限
非内部静态内部类能调用和访问外部类的所有方法和属性,即使该方法或者属性是外部类私有的。
需要注意的是,当非静态内部类有方法或者字段和外部类某个字段或方法重名时,可通过"外部类.this.字段(或方法)名字"来访问外部类。
public class OuterClass {
private int filed1=1;
class InnerClass{
static int filed1=2;
void InnerClassFunc(){
System.out.println("OuterClass.this.filed1 is "+OuterClass.this.filed1);
}
}
}
运行InnerClassFunc()方法的截图如下所示。
可以看到打印的是外部类的字段。
3.3 原理
使用javac将OuterClass.java文件编译成class文件,会发现当前目录下多出个OuterClass$InnerClass.class文件,具体内容如下所示。
该文件就是内部类InnerClass所对应的class文件,细心的读者会发现,构造方法传入了一个外部类的实例,这也就是为什么非静态内部类能访问外部类,是因为非静态内部类隐式持有一个引用指向外部类实例。
3.3 适用情况
非静态内部类适用于需要访问外部类非静态属性的情况,比较常见的应用有迭代器,比如说Java容器类里的迭代器Iterator需要访问容器里的元素,所以各容器的Iterator类一般为非静态内部类。
四.匿名内部类
4.1 原理
public class OuterClass {
private int filed1=1;
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println(filed1);
}
};
}
同样使用javac将OuterClass.java编译成class文件,可以发现当前目录下多了OuterClass$1.class文件。
该文件就是匿名内部类runnable所对应的class文件了,匿名内部类的名字一般是将所在的外部类名字与$数字(从1开始)拼接而成,此外,我们可以看到,其构造器方法形式与非静态内部类相同,都是传入了一个外部类的实例。
4.2 限制
匿名内部类不允许修改所在方法的局部变量。
将上述代码放进新建的run()方法里面。
public class OuterClass {
private int filed1=1;
public void run(){
int filed2=2;
Runnable runnable=new Runnable() {
@Override
public void run() {
System.out.println(filed2);
filed2=3;//报错的代码
}
};
}
}
上述代码会进行报错。
说人话就是,要么就用final修饰局部变量,要么就不要在匿名内部类修改局部变量,这就是effectively final的含义。
删除报错的代码后重新编译获取class文件。
可以看到,构造器方法传入了两个变量,第二个变量就是局部变量filed2了,这里需要注意的是,只有当局部变量是运行时确定(比如说获取运行时的系统时间)的,才会通过构造方法来传值。
五.局部内部类
修改run()方法代码。
public class OuterClass {
private int filed1=1;
public void run(){
class LocalClass {
private void fun(){
System.out.println(filed1);
filed1=3;
}
}
}
}
可以看到局部内部类是允许访问并修改外部类的成员的,但需要注意的是,局部内部类与局部成员一致,作用域只有在方法内部,换句话说就是,在其他地方不能实例化该类,且不能有public,private,static修饰符,只允许用final修饰符进行修饰。
此外,局部内部类是不允许修改方法内的局部变量
六.静态内部类和非静态内部类比较
尽管静态内部类和非静态内部类在语法上区别不大,但一般来说,是更推荐使用静态内部类的,只有当静态内部类不满足需求时,才使用非静态内部。
这是因为非静态内部类会持有外部类实例的引用,这一来会增加空间开销,如果只需进行实例化一次,那开销还可以接受,但若是像作为Java容器里的Node和Entry等需要实例化多次的内部类,这开销就非常可观了。
二来这会导致GC时无法回收外部类,从而可能导致内存泄露的情况,且该情况不易排查,一旦发生,那又是一个通宵加班的夜晚了。
参考资料
1.《Effective Java》
2.Java内部类(一篇就够)