Java基础-内部类详解

931 阅读4分钟

我的博客 转载请注明原创出处。

内部类(inner class)是定义在另一个类内部的类。之所以定义在内部是因为内部类有一些普通类没有的“特权”,可以方便实现一些需求。

内部类

先来看一个简单的例子:

public class Apple {

    private int size = 16;

    private class Book {
        public void print() {
            System.out.println(size);
        }
    }

}

Book类就是定义在Apple类中的一个内部类,Book类引用了Apple类的私有域size却没有报错,这就是上文提到的特权了,内部类可以引用外围类的所有域和方法包括私有的。那么为什么内部类可以做到这样神奇的事情呢?原来是编译器在背后偷偷干的好事!

把上文的例子编译后可以看到编译器会额外生成一个Apple$Book.class文件:

class Apple$Book {
    private Apple$Book(Apple var1) {
        this.this$0 = var1;
    }

    public void print() {
        System.out.println(Apple.access$000(this.this$0));
    }
}

可以看到这个类的名称是用外围类名称加内部类名称用$符号分割,而且编译器在内部类的构造函数里自动添加了一个外围类的参数,这样内部类就能引用到外围类的域和参数了。

不过这样还有一个问题,我们完全可以按普通的方式自己写一个构建方式来接收Apple类而不用内部类的方式,不过这样的类却无法引用Apple类的私有域和私有方法。

眼尖的同学可能已经发现奥秘了,Apple.access$000(this.this$0)这一条语句就是关键了。内部类在引用外围类的私有域和方法时编译器会在外围类内部生成一个静态方法access$XXX,这个方法会返回外围类的私有域或调用私有方法,方法的第一个参数是外围类的引用。

不过这样就有了安全风险,任何人都可以通过调用Apple.access$000方法很容易地读取到私有域size。当然,access$000不是Java的合法方法名。但熟悉类文件结构的黑客可以使用十六进制编辑器轻松地创建一个用虚拟机指令调用那个方法的类文件。由于隐秘地访问方法需要拥有包可见性,所以攻击代码需要与被攻击类放在同一个包中。

特殊的语法

内部类有一些特殊的语法,比如获取传入的外围类引用的语法是OuterClass.this,外围类的类名加上this关键字。还有明确的使用内部类的构建函数outerObject.new InnerClass {construction parameters)。在内部类中声明的静态域必须是不可变的,即必须用final修饰符修饰,且不能有静态方法。例子:

public class Apple {

    private int size = 16;

    private class Book {
        public void print() {
            System.out.println(Apple.this.size);
        }
    }

    public static void main(String[] args) {
        Apple apple = new Apple();
        Apple.Book book = apple.new Book();
    }

}

局部内部类

内部类也可以在一个方法内声明,这样定义的内部类就是局部内部类。局部内部类和内部类的区别在于局部内部类的作用域局限于定义它的方法块内,除了这个方法内部局部内部类都是不可见的。

public class Apple {

    private int size = 16;

    private void print() {
        class Book {
            public void print() {
                System.out.println(size);
            }
        }
        Book book = new Book();
        book.print();
    }

}

匿名内部类

顾名思义,匿名内部类是一种没有类名的类。因为有时候我们只需要有一个一次性使用的类的对象,匿名内部类可以方便我们实现。通常的语法格式为:

SuperType superType = new SuperType(construction parameters) {
    inner class methods and data
}

如果SuperType是一个接口,那么就需要在大括号里实现接口定义的抽象方法。如果SuperType是一个类,可以在大括号里扩展这个类。因为匿名内部类没有类名,所以是不能定义构建函数的。在Java8以后,使用lambda表达式会比匿名内部类更加方便。

双括号初始化

利用匿名内部类的特殊语法的特殊初始化技巧,比如初始化一个数组:

List<String> arrayList = new ArrayList<String>() {{
    add("test");
    add("test2");
}};

不过就这个例子来说这样更好:List<String> arrayList = Arrays.asList("test", "test2");

静态内部类

上文说到内部类都会有一个外围类的引用,不过有时我们只是想把类放在另一个类内部并不需要引用它,这时就可以用到静态内部类。例子:

public class Apple {

    private int size;

    private int price;

    public Apple(int size, int price) {
        this.size = size;
        this.price = price;
    }

    public static void main(String[] args) {
        Apple apple = AppleBuilder.builder().setPrice(20).setSize(16).build();
    }

    static class AppleBuilder {

        private int size;

        private int price;

        static AppleBuilder builder() {
            return new AppleBuilder();
        }

        Apple build() {
            return new Apple(size, price);
        }

        AppleBuilder setSize(int size) {
            this.size = size;
            return this;
        }

        AppleBuilder setPrice(int price) {
            this.price = price;
            return this;
        }

    }

}

后记

一周一篇的第八篇了,接下来再复习一下并发相关的内容就准备去看看JVM相关的内容了。知识学是学不完的,只希望自己能坚持学到老,不要待在舒适区变成一个曾经讨厌的老顽固。这次同样是参考《Java核心技术 卷1》,这可真是一本好书,建议Java新手都去看看。