Java内部类

Java内部类总结

Posted by Anriku on March 26, 2018

由于Java核心技术上的例子很不错,本篇内部类博客部分代码以Java核心技术上的例子进行讲解。

内部类总览

内部类分为四种,分别是:

  • 一般的内部类(与方法在一级上的)
  • 局部内部类(在方法中的类)
  • 匿名内部类(没有类名的类)
  • 静态内部类(也称为嵌套类)

一般的内部类

下面是一个一般内部类的代码展示:

public class InnerClassTest {
    public static void main(String[] args) {
        TalkingClock clock = new TalkingClock(1000, true);
        clock.start();
        JOptionPane.showMessageDialog(null, "Quit Program?");
        System.exit(0);
    }
}

class TalkingClock {

    private int interval;

    private boolean beep;

    public TalkingClock(int interval, boolean beep) {
        this.interval = interval;
        this.beep = beep;
    }

    public void start() {

        ActionListener listener = new TimePrinter();
        Timer timer = new Timer(interval, listener);
        timer.start();
    }

	//一般的内部类
    public class TimePrinter implements ActionListener{

        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("At the tone, the time is " + new Date());
            if (beep) Toolkit.getDefaultToolkit().beep();
        }
    }
}

从上面我们可以看到在TimePrinter这个内部类中的actionPerformed方法中引用了外部类TalkingClock的interval和beep域。所以说我们从这里可以看出一般的内部类的一个特点就是可以访问自身的数据域,也可以访问创建它的外部类对象的数据域。

那么为了让内部类有这样的特性,它是怎么做到的呢?其实,在内部类中总有一个隐式的引用指向着创建它的外部类的对象。就像下面的一样:

一般内部类引用关系

光这么说可能你理解并不会太深刻,那么现在我们走向代码里面去。先来一张图,再来一一解释吧!

内部类分析及外部类

当然我是在生成字节码的目录下,然后了解Linux的童鞋都知道ls是列出当然目录下的可见的文件或者文件夹的命令。然后这些字节码有点多,但别去管其它的其它是一些无关紧要的类的字节码。我们直接看到TalkingClock.class和TalkingClock$TimePrinter.class,他们分别就是上面外部类和内部类的字节码啦!看到这是是不是感觉被骗了,emmm…内部类其实就是编译器的一个语法,在编译后的话没有啥字节码了,就是和泛型foreach等一样的是一层编译器语法糖。内部类会被单独的编译出来,然后通过$来连接外部类和内部类成为内部类的类名。

然后在后面我们通过javap -private <字节码>(这里我们看第一个javap得到的东西)用来显示所有类和成员。我看的final TalkingClock this$0;this$0这个变量是TalkingClock类型的,没错,其实这就是我们上面给出的图中的outer那个变量。这样一切都明了了。之所以在内部类中能够去引用外部类的数据域就是因为我们的内部类只用隐藏着一个外部类的引用。

然后,还想说的就是我们这样一般的内部类,可以是私有类,但是常规类(也就是我们的外部类一样的类)只能是包可见性或公有可见性。然后,也就是如果我们的内部类对于其它类可见的时候,我也可以来进行内部类的创建。但是得注意一点的是我们如果要在其它类进行一般内部类的创建的时候,我们要借助于一个外部类对象进行创建。也就是像下面一样进行创建。

TalkingClock clock = new TalkingClock(1000,true);

TalkingClock.TimePrinter printer = clock.new TimePrinter();

虽然,基于Java 8的Java核心技术书上在说内部类中声明的所有静态域都必须是final以及可以声明静态方法。但是笔者在Java 9上发现这样的一般内部类是不能进行静态域或者是静态方法的声明的,要声明只能让内部类变成静态内部类才行。

通过前面的代码我们可以发现一般的内部类是能够去访问对应外部类的私有域的。它是怎么做到的呢!我们看到第二个javap得到的内容。我们发现其中有一个static boolean access$000(TalkingClock)的方法。

我们在内部类的用到了私有的beep,因此在外部类中这个自动生成的静态方法来实现私有beep的访问的。当然由于编译器的不同方法名可能会不同,如:access$0。

局部内部类

为了后面好分析,先来看一波代码吧:

public class InnerClassTest {
    public static void main(String[] args) {
        TalkingClock clock = new TalkingClock();
        clock.start(1000, true);
        JOptionPane.showMessageDialog(null, "Quit Program?");
        System.exit(0);
    }
}

class TalkingClock {

    public void start(int interval,boolean beep) {

		//局部内部类
        class TimePrinter implements ActionListener{

            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("At the tone, the time is " + new Date());
                if (beep) Toolkit.getDefaultToolkit().beep();
            }
        }

        ActionListener listener = new TimePrinter();
        Timer timer = new Timer(interval, listener);
        timer.start();
    }

}

从代码我们可以看到,局部内部类就是将内部类放在了外部类的一个方法内。然后得注意一点的就是局部内部类不能够用public或者是private访问说明符进行声明。它的作用于限定在声明这个局部内部类的块中。因此这也是它的一个优势,那就是这个内部类对外面的世界是完全隐藏的

在局部内部类中实现外部类私有变量的访问的方案是和上面一般的内部类访问私有变量是一样的。但是如果一个局部内部类对一个局部变量的访问的话,那么这个局部内部类中就会存储一个局部变量的副本。就像下面一样:

局部内部类

那么上面代码的整个流程我们来解释一下:

  • 调用start方法
  • 调用内部类TimePrinter的构造器,初始化listener变量
  • 将listener传给Timer构造器,定时器开始计时,start方法结束。然后此时,start方法结束,beep参数不复存在
  • 然后,actionPerformed执行if(beep)…

我们可以看到在start方法结束后,变量beep就不存在了。

Java 8之前的话,必须把局部内部类定义为final的局部变量才行的。比如上面的start方法就会变成下面的样子:

public void start(int interval,final boolean beep){
    ...
}

匿名内部类

咱们依然先来看看代码:

public class InnerClassTest {
    public static void main(String[] args) {
        TalkingClock clock = new TalkingClock();
        clock.start(1000, true);
        JOptionPane.showMessageDialog(null, "Quit Program?");
        System.exit(0);
    }
}


class TalkingClock {

    public void start(int interval, boolean beep) {

        //匿名内部类
        ActionListener listener = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("At the tone, the time is " + new Date());
                if (beep) Toolkit.getDefaultToolkit().beep();
            }
        };
        Timer timer = new Timer(interval, listener);
        timer.start();
    }
}

有上面的代码我们可以看到,匿名内部类就是只用创建一个对象,而不需要对其命名(就是没有类名)的类。从形式上来说就是一个构造参数的闭小括号后面跟一个大括号,就是一个匿名内部类。其中大括号就是普通的类后面的大括号,里面可以写自己的方法也可以重写父类或者是接口中的方法。

由于构造器的名字要和类名相同,但是匿名内部类没有类名。所以,匿名类不能有构造器。

像上面ActionListener这样只有一个方法必须在实现它的类中进行重写的接口,我们叫做函数式接口实现这样的接口的匿名内部类,我们可以用lambada表达式来进行代替。就像下面这样:

ActionListener listener = e -> {
	System.out.println("At the tone, the time is " + new Date());
	if (beep) Toolkit.getDefaultToolkit().beep();
};

对于匿名内部类,我们有一个双括号初始化的技巧。比如像一个方法需要传一个数组列表,但是这个数组列表无需再被用到,那么我们就可以用这个技巧。现在,我们来看看代码:

// 好久没玩LOL,邀请一波好友来玩下!
public class Test {
    public static void main(String[] args) {
        //没有使用匿名内部类
        ArrayList<String> friends = new ArrayList();
        friends.add("anriku");
        friends.add("zzia");
        friends.add("zxZhu");
        inviteFriendToPlayLOL(friends);


        //使用匿名内部类,并用双重括号初始化
        inviteFriendToPlayLOL(new ArrayList<>() );
    }

    private static void inviteFriendToPlayLOL(List<String> friends) {
        System.out.println(friends);
    }
}

大括号中的大括号,叫做构造块构造块会在每一个对象构造的时候进行调用。与静态代码块不同的就是静态代码块只会在一个类被加载的时候进行调用。调用顺序是这样的:静态块>构造块>构造方法

静态内部类

如果我们的内部类只是想完全的隐藏在一个类之中,并不需要这个内部类与外面的类打交道。那么我们可以将内部类声明为static(也只有内部类能够被static修饰),这样的话内部类会不会持有外部类的引用。这样的话,内部类只能够使用外部类的静态变量或者是静态方法了。不能引用外部类的实例域或者是方法了。

现在我们来举个栗子:

public class InnerClassTest {
    public static void main(String[] args) {
        double[] d = new double[20];
        for (int i = 0;i < d.length;i++){
            d[i] = 100*Math.random();
            ArrayAlg.Pair p = ArrayAlg.minmax(d);
            System.out.println("min = " + p.getFirst());
            System.out.println("max = " + p.getSecond());
        }
    }
}

class ArrayAlg{

    //静态内部类
    public static class Pair{
        private double first;

        private double second;

        Pair(double first, double second) {
            this.first = first;
            this.second = second;
        }

        public double getFirst() {
            return first;
        }

        public double getSecond() {
            return second;
        }
    }

    public static Pair minmax(double[] values){
        double min = Double.POSITIVE_INFINITY;
        double max = Double.NEGATIVE_INFINITY;

        for (double v:values){
            if (min > v) min = v;
            if (max < v) max = v;
        }
        return new Pair(min,max);
    }
}

我们为什么要这样写呢?其中的过程就是:

  • 我们需要一个minmax用来将一个数组中的最大最小值比较出来。由于两个方法的话就要进行两次的遍历比较。于是就想到用一个方法,但是要同时返回最大和最小值,那么我们就用Pair类来将两个值连在一起。
  • 但是如果单独做一个类的话,有两个缺点:
    • 以Pair为类名的类太多了
    • 这个Pair类实际上只在ArrayAlg类中使用到
  • 于是我们就让其作为一个内部类
  • 再由于这个内部类与外部没有什么干系(也就是内部类完全不需要访问外围对象),于是就让其作为一个静态内部类

那么静态内部类是不是真的没有去引用外部类对象呢,我们依然用javap命令去看一看:

静态内部类

从上面我们可以看到在这个内部类中我们没有看到之前外部类对象的引用。所以说这是真没关系的。

然后需要注意的一点就是,当外部类进行加载的时候,并且外部类没有用到静态内部类的时候,静态内部类是不会加载的。我们可以来看下测试代码。

//一个测试类,其中包括了一个静态内部类
public class StaticClassTest {

    public static void test(){
        System.out.println("Test");
    }

    static class InnerStaticClass{
        static {
            System.out.println("This is static block of InnerStaticClass");
        }
    }
}

public class Main{
    public static void main(String[] args) {
        StaticClassTest.test();
    }
}

咱们来看一下代码运行的结果:

静态内部类的加载

没错吧,运行结果中没有执行静态内部类中的静态代码块所需打印的东西。

总结

今天,我们学习了内部类相关的东西。虽然还是有点复杂,但是通过我们一步一步的分析,我们揭开了其中神秘的东西。对其了解也是更上一层楼了吧!

其中,一般的内部类局部内部类匿名内部类都会包含一个创建它的外部类的对象的引用。但是静态内部类不会持有这么一个变量。

参考

  • Java核心技术 卷一

转载请注明链接