Java内部类

296 阅读5分钟

一.引言

内部类可以简单分为静态内部类和非静态内部类,匿名内部类,局部内部类,匿名内部类和局部类,不过静态内部类和非静态内部类才是本文的重点,会花更多的笔墨去介绍,匿名内部类和局部内部类更多是一笔带过。

二.静态内部类

静态内部类可以当作普通的类来使用。

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()方法的截图如下所示。

image.png

可以看到打印的是外部类的字段。

3.3 原理

使用javac将OuterClass.java文件编译成class文件,会发现当前目录下多出个OuterClass$InnerClass.class文件,具体内容如下所示。

image.png

该文件就是内部类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文件。

image.png

该文件就是匿名内部类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;//报错的代码
            }
        };
    }
}    

上述代码会进行报错。

image.png

说人话就是,要么就用final修饰局部变量,要么就不要在匿名内部类修改局部变量,这就是effectively final的含义。

删除报错的代码后重新编译获取class文件。

image.png

可以看到,构造器方法传入了两个变量,第二个变量就是局部变量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内部类(一篇就够)