Java 内部类

问:Java 常见的内部类有哪几种,简单说说其特征?

答:静态内部类、成员内部类、方法内部类(局部内部类)、匿名内部类。

  • 静态内部类是定义在另一个类里面用 static 修饰 class 的类,静态内部类不需要依赖于外部类(与类的静态成员属性类似)且无法使用其外部类的非 static 属性或方法(因为在没有外部类对象的情况下可以直接创建静态内部类的对象,如果允许访问外部类的非 static 属性或者方法就会产生矛盾)。

  • 成员内部类是没有用 static 修饰且定义在在外部类类体中的类,是最普通的内部类,可以看做是外部类的成员,可以无条件访问外部类的所有成员属性和成员方法(包括 private 成员和静态成员),而外部类无法直接访问成员内部类的成员和属性,要想访问必须得先创建一个成员内部类的对象然后通过指向这个对象的引用来访问;当成员内部类拥有和外部类同名的成员变量或者方法时会发生隐藏现象(即默认情况下访问的是成员内部类的成员,如果要访问外部类的同名成员需要通过 OutClass.this.XXX 形式访问);成员内部类的 class 前面可以有 private 等修饰符存在。

  • 方法内部类(局部内部类)是定义在一个方法里面的类,和成员内部类的区别在于方法内部类的访问仅限于方法内;方法内部类就像是方法里面的一个局部变量一样,所以其类 class 前面是不能有 public、protected、private、static 修饰符的,也不可以在此方法外对其实例化使用。

  • 匿名内部类是一种没有构造器的类(实质是继承类或实现接口的子类匿名对象),由于没有构造器所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调,匿名内部类在编译的时候由系统自动起名为 OutClass$1.class,一般匿名内部类用于继承其他类或实现接口且不需要增加额外方法的场景(只是对继承方法的实现或是重写);匿名内部类的 class 前面不能有 pravite 等修饰符和 static 修饰符;匿名内部类访问外部类的成员属性时外部类的成员属性需要添加 final 修饰(1.8 开始可以不用)。

问:下面说法哪些不正确,为什么?
内部类不能有自己的成员方法和成员变量。

内部类可用 abstract 修饰符定义为抽象类,也可以用 private 或 protected 定义。

内部类可作为其他类的成员,而且可以访问它所在类的成员。

除 static 内部类外,不能直接在内部类中声明 static 成员(static 常量除外)。

答:A 的描述是错误的,其他都正确。
因为内部类是指在一个类的内部嵌套定义的类,与普通类一样具有自己的成员方法和成员变量,成员和方法是类存在且有意义的基础。

问:下面的内部类哪些是正确的,哪些是错误的?
        public class OutClass {
            class InnerClass1 {
                public static int func() {
                    return 1;
                }
            }

            public class InnerClass2 {
                static int func() {
                    return 1;
                }
            }

            private class InnerClass3 {
                int func() {
                    return 1;
                }
            }

            static class InnerClass4 {
                protected int func() {
                    return 1;
                }
            }

            abstract class InnerClass5 {
                public abstract int func();
            }
        }

答:由于静态内部类可以有静态成员或者静态方法,而非静态内部类不能有静态成员或者静态方法,所以 InnerClass1、InnerClass2 是错的。其他 InnerClass3、InnerClass4、InnerClass5 都是正确的,解释如第一题答案。

问:Java 中为什么成员内部类可以直接访问外部类的成员?

答:成员内部类可以无条件访问外部类的成员或者方法的原因解释我们可以通过下面例子来说明。

        public class OutClass {
            public class InnerClass {
            }
        }

我们执行命令 javac OutClass.java 编译会发现得到两个 class 文件,分别为 OutClass.class 和 OutClass$InnerClass.class,所以编译器在进行编译的时候会把成员内部类单独编译成一个字节码文件,我们接着通过 javap [-v] OutClass$InnerClass 看下编译后的成员内部类的字节码,如下:

        Compiled from "OutClass.java" public class OutClass$InnerClass {
            final OutClass this $0;

            public OutClass$InnerClass(OutClass);
        }

可以看到编译后的成员内部类中有一个指向外部类对象的引用,且成员内部类编译后构造方法也多了一个指向外部类对象的引用参数,所以说编译器会默认为成员内部类添加了一个指向外部类对象的引用并且在成员内部类构造方法中对其进行赋值操作,因此我们可以在成员内部类中随意访问外部类的成员,同时也说明成员内部类是依赖于外部类的,如果没有创建外部类的对象则也无法创建成员内部类的对象。

问:Java 1.8 之前为什么方法内部类和匿名内部类访问局部变量和形参时必须加 final?

答:在 Java 1.8 以下因为对于普通局部变量或者形参的作用域是方法内,当方法结束时局部变量或者形参就要随之消失,而其匿名内部类或者方法内部类的生命周期又没结束,匿名内部类或者方法内部类如果想继续使用方法的局部变量就需要一些手段,所以 Java 在编译匿名内部类或者方法内部类时就有一个规定来解决生命周期问题,即如果访问的外部类方法的局部变量值在编译期能确定则直接在匿名内部类或者方法内部类里面创建一个常量拷贝,如果访问的外部类方法的局部变量值无法在编译期确定则通过构造器传参的方式来对拷贝进行初始化赋值。由此说明在匿名内部类或者方法内部类中访问的外部类方法的局部变量或者形参是内部类自己的一份拷贝,和外部类方法的局部变量或者形参不是一份,所以如果在匿名内部类或者方法内部类对变量做修改操作就一定会导致数据不一致性(外部类方法的参数不会跟着被修改,引用类型仅是引用,值修改不存在问题),为了杜绝数据不一致性导致的问题 Java 就要求使用 final 来保证,所以必须是 final 的。在 Java 1.8 开始我们可以不加 final 修饰符了,系统会默认添加,Java 将这个功能称为 Effectively final。

上面这段话可以通过下面的例子说明(对于非 final 无法编译通过,所以不再举例),如下:

        public class OutClass {
            private int out = 1;

            public void func(final int param) {
                final int in = 2;
                new Thread() {
                    @Override
                    public void run() {
                        out = param;
                        out = in;
                    }
                }.start();
            }
        }

上面类文件在 java 1.8 以下通过 javac 编译后执行 javap -l -v OutClass$1.class 查看匿名内部类的字节码可以发现如下情况:

......
class OutClass$1 extends java.lang.Thread
......
{
  
//匿名内部类有了自己的 param 属性成员。
  
final int val$param;
  
......
  
//匿名内部类持有了外部类的引用作为一个属性成员。
  
final OutClass this $0;
  
......
  
//匿名内部类编译后构造方法自动多了两个参数,一个为外部类引用,一个为 param 参数。
  
OutClass$1(OutClass, int);
    
......
  
public void run();
    
......
    
Code:
      stack=2, locals=, args_size=1
           
//out = param;语句,将匿名内部类自己的 param 属性赋值给外部类的成员 out。
         
0: aload_0
         
1: getfield      #1    // Field this$0:LOutClass;
4: aload_0
         
5: getfield      #2    // Field val$param:I
         
8: invokestatic  #4    // Method OutClass.access$002:(LOutClass;I)I
        
11: pop        //out = in;语句,将匿名内部类常量 2 (in在编译时确定值)赋值给外部类的成员 out。
        
12: aload_0
        
13: getfield      #1    // Field this$0:LOutClass;
        
//将操作数2压栈,因为如果这个变量的值在编译期间可以确定则编译器默认会在
        
//匿名内部类或方法内部类的常量池中添加一个内容相等的字面量或直接将相应的
        
//字节码嵌入到执行字节码中。
        
16: iconst_2
        
17: invokestatic  #4    // Method OutClass.access$002:(LOutClass;I)I
        
20: pop
       
21: return
      
......
}
......

通过字节码指令我想不用再多解释了吧,上面字节码包含了访问局部变量编译时可确定值和不可确定值的两种情况,自己可以再琢磨下。

问:下面关于成员内部类 InnerClass 的子类实现 ChildInnerClassX 中哪些是可以编译运行的?
        class OutClass {
            class InnerClass {
            }
        }
        class ChildInnerClass1 extends OutClass.InnerClass {
        }
        class ChildInnerClass2 extends OutClass.InnerClass {
            public ChildInnerClass2() {
                super();
            }
        }
        class ChildInnerClass3 extends OutClass.InnerClass {
            public ChildInnerClass3(OutClass outClass) {
                super();
            }
        }
        class ChildInnerClass4 extends OutClass.InnerClass {
            public ChildInnerClass3(OutClass outClass) {
                outClass.super();
            }
        }

答:只有 ChildInnerClass4 子类是可以编译运行的,其他都无法编译通过。(虽然开发中很少会遇到这种)因为成员内部类的继承语法格式要求继承引用方式为 Outter.Inner 形式且继承类的构造器中必须有指向外部类对象的引用,并通过这个引用调用 super(),其实这个要求就是因为成员内部类默认持有外部类的引用,外部类不先实例化则无法实例化自己。

问:下面程序的运行结果是什么?为什么?
        List list1 = new ArrayList();
        List list2 = new ArrayList() {
        };
        List list3 = new ArrayList() {{
        }};
        List list4 = new ArrayList() {
            {
            }

            {
            }

            {
            }
        };
        //1 
        System.out.println(list1.getClass() == list2.getClass());
        // 2 
        System.out.println(list1.getClass() == list3.getClass());
        // 3
        System.out.println(list1.getClass() == list4.getClass());
        // 4 
        System.out.println(list2.getClass() == list3.getClass());
        // 5 
        System.out.println(list2.getClass() == list4.getClass());
        // 6 
        System.out.println(list3.getClass() == list4.getClass());

答:程序运行返回 6 个 false。
首先 list1 指向一个 ArrayList 对象实例;list2 指向一个继承自 ArrayList 的匿名类内部类对象;list3 也指向一个继承自 ArrayList 的匿名内部类(里面一对括弧为初始化代码块)对象;list4 也指向一个继承自 ArrayList 的匿名内部类(里面多对括弧为多个初始化代码块)对象;由于这些匿名内部类都出现在同一个类中,所以编译后其实得到的是 Demo$1、Demo$2、Demo$3 的形式,所以自然都互补相等了,不信你可以通过 listX.getClass().getName() 进行验证。

问:开发中使用 Java 匿名内部类有哪些注意事项(经验)?

答:常见的注意事项如下。
使用匿名内部类时必须是继承一个类或实现一个接口(二者不可兼得且只能继承一个类或者实现一个接口)。
匿名内部类中是不能定义构造函数的,如需初始化可以通过构造代码块处理。
匿名内部类中不能存在任何的静态成员变量和静态方法。
匿名内部类为局部内部类,所以局部内部类的所有限制同样对匿名内部类生效。
匿名内部类不能是抽象类,必须要实现继承的类或者实现接口的所有抽象方法。

问:Java 匿名内部类在使用时如何初始化吗?

答:匿名内部类无法通过构造方法初始化,所以我们只能通过构造代码块进行初始化。

问:非静态内部类里面为什么不能有静态属性和静态方法?

答:static 类型的属性和方法在类加载的时候就会存在于内存中,要使用某个类的 static 属性或者方法的前提是这个类已经加载到 JVM 中,非 static 内部类默认是持有外部类的引用且依赖外部类存在,所以如果一个非 static 的内部类一旦具有 static 的属性或者方法就会出现内部类未加载时却试图在内存中创建内部类的 static 属性和方法,这自然是错误的,类都不存在(没被加载)却希望操作它的属性和方法。从另一个角度讲非 static 的内部类在实例化的时候才会加载(不自动跟随主类加载),而 static 的语义是类能直接通过类名来访问类的 static 属性或者方法,所以如果没有实例化非 static 的内部类就等于非 static 的内部类没有被加载,所以无从谈起通过类名访问 static 属性或者方法。

问:Java 匿名内部类为什么不能直接使用构造方法,匿名内部类有没有构造方法?

答:因为类是匿名的(相当于没有名字),而且每次创建的匿名内部类同时被实例化后只能使用一次,所以就无从创建一个同名的构造方法了,但是可以直接调用父类的构造方法(譬如 new InnerClass(xxx, xxx) {})。

实质上匿名内部类是有构造方法的,是通过编译器在编译时帮忙生成的,如下代码:

        class InnerClass {
        }
        public class OutClass {
            InnerClass clazz = new InnerClass() {
            };
        }

通过编译后生成了 InnerClass.class、OutClass$1.class、OutClass.class,可以看见 OutClass$1.class 就是我们匿名内部类的字节码名字,我们通过 javap -v OutClass$1.class 可以看到如下:

......
{
  
final OutClass this$0;
  
......
  
OutClass$1(OutClass);
    descriptor: (LOutClass;)
    flags:
    Code:
      stack=2, locals=2, args_size=2 
0: aload_0
         
1: aload_1
         
2: putfield      #1      // Field this$0:LOutClass;
         
5: aload_0
         
6: invokespecial #2      // Method InnerClass."<init>":()V
         
9: return    
LineNumberTable:        
        line 8: 0
}
......

可以很明显看到内部类的字节码中编译器为我们生成了参数为外部类引用的构造方法,其构造方法和普通类的构造方法没有区别,都是执行 <init> 方式。

问:Java 中非静态内部类和静态内部类有什么区别?

答:常见的区别如下。
非静态内部类默认持有外部类的引用,静态内部类不存在该特性。
非静态内部类中不能定义静态成员或者方法,静态内部类中可以随便定义。
非静态内部类可以直接访问外部类的成员变量或者方法,静态内部类只能直接访问外部类的静态成员或者方法(实质是持有外部类名)。
非静态内部类可以定义在外部类的任何位置(方法里外均可,在方法外面定义的内部类的 class 访问类型可以是 public、protected 等,方法里的只能是默认 class,类似局部变量),静态内部类只能定义在外部类中最外层,class 修饰符可以是 public、protected 等。
非静态内部类创建实例时必须先创建外部类实例,静态内部类不依赖外部类实例。
静态方法中定义的内部类是静态内部类(这时不能在类前面加 static 关键字),静态方法中的静态内部类与普通方法中的内部类使用类似,除了可以直接访问外部类的 static 成员变量或者方法外还可以访问静态方法中的局部变量(java 1.8 以前局部变量前必须加 final 修饰符)。

问:Java 中内部类有什么好处(即 Java 的内部类有什么作用)?

答:具体好处如下。

  1. 内部类可以很好的实现隐蔽,一般的非内部类,是不允许有 private 与 protected 等权限的,但内部类(除过方法内部类)可以通过这些修饰符来实现隐藏。

  2. 内部类拥有外部类的的访问权限(分静态非静态情况),通过这一特性可以比较好的处理类之间的关联性,将一类事物的流程放在一起内部处理。

  3. 通过内部类可以实现多重继承,java 默认是单继承,我们可以通过多个内部类继承实现多个父类,接着由于外部类完全可访问内部类,所以就实现了类似多继承的效果。

  4. 通过内部类可以避免修改接口而实现同一个类中两种同名方法的调用(譬如你的类 A 中有一个参数为 int 的 func 方法,现在类 A 需要继承实现一个接口 B,而接口 B 中也有一个参数为 int 的 func 方法,此时如果直接继承实现就会出现同名方法矛盾问题,这时候如果不允许修改 A、B 类的 func 方法名则可以通过内部类来实现 B 接口,因为内部类对外部类来说是完全可访问的)。

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