浅谈Java内部类

由于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<>() {{
            add("Jay");
            add("acemurder");
            add("Mike");
        }});
    }

    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核心技术 卷一
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 如果文章对你有所帮助,请点喜欢并关注,这将是我最大的动力,谢谢 为什么要有内部类 1.内部类是为了更好的封装,把内...
    光哥很霸气阅读 9,694评论 -1 31
  • 局部内部类 局部内部类是内部类的第二种形式,它让内部类的“隐藏”得更深一层——写在外部类的方法内部,而不是处于和外...
    java部落阅读 397评论 0 2
  • 正文 前言说到java内部类,想必大家首先会想到比较常用的“匿名内部类”,但实际上,这只是内部类的其中一种使用方式...
    java部落阅读 473评论 0 6
  • 时间总是走的很快,春去秋来,又要到一年寒冬时节。树叶绿了又黄,转眼即将化成来年的肥料,光秃秃的树干上,偶尔有小鸟停...
    苏穆凉阅读 301评论 0 1
  • 一晃五六年,匆匆又夏天! 有的人,尽管很多年不联系,但却可以一直住在你心里很久很久很久,因为是在你最特别的时候陪伴...
    voler22阅读 401评论 4 4