Java进阶之深入理解内部类的本质

1 内部类

1.1 什么是内部类?

答:类都对应于一个独立的Java源文件,但一个类还可以放在另一个类的内部,称之为内部类,相对而言,包含它的类称之为外部类。不过,内部类只是Java编译器的概念,对于Java虚拟机,它是不知道内部类这回事的,每个内部类最后都会被编译为一个独立的类,生成一个独立的字节码文件

1.2 为什么放到别的类内部呢?

答:内部类与包含它的外部类有比较密切的关系,而与其他类关系不大,定义在类内部,可以实现对外部完全隐藏,可以有更好的封装性,代码实现上也往往更为简洁。

1.3 内部类有什么作用?

答:(1)内部类声明为private从而实现对外完全隐藏,相关代码写在一起,写法也更为简洁;(2)内部类拥有外部类所有变量/方法的所有访问权限,可以方便的访问外部类的private变量

1.3.1 内部类案例

答:

public class Outer {
    private final String TAG = "Outer";
    
    private int count = 0;
    private void add() {
        count++;
    }

    /**
     * 内部类
     */
    public class InnerClass {
        private int number = 1;
        private int getSomething() {
            // 直接可以调用外部类的方法。等价于:Outer.this.add()
            add();
            // 等价于:this.number + Outer.this.count
            return number + count;
        }
    }

    /**
     * 静态内部类
     */
    public static class StaticInnerClass {
        private int number = 1;
        public int getValue() {
            // add(); 编译器直接报错
            // 不持有外部类的引用,所以必须通过外部类的对象来访问
            Outer Outer = new Outer();
            Outer.add();
            // 等价于:this.number + Outer.count
            return number + Outer.count;
        }
    }

    public void main() {
        // 1、测试内部类
        testInnerNested();
        // 2、测试外部类可以访问内部类的private
        testInnerPrivate();
    }

    /**
     * 1、匿名内部类的名字
     */
    private void testInnerNested() {
        // 内部类:必须现new出外部类的对象,才能new出内部类的对象
        Outer.InnerClass innerClass = new Outer().new InnerClass();  // new InnerClass()
        int res = innerClass.getSomething();
        LogUtils.e(TAG, "testInnerNested + Java + 内部类1:" + res);

        // 静态内部类:直接可以new出内部类的对象
        Outer.StaticInnerClass staticInnerClass = new StaticInnerClass();
        int value = staticInnerClass.getValue();
        LogUtils.e(TAG, "testInnerNested + Java + 静态内部类1:" + value);
    }

    /**
     * 2、测试外部类可以访问内部类的private
     */
    private void testInnerPrivate() {
        // 在 Java 中,外部类 可以访问 内部类 的 private 变量:
        int result1 = new InnerClass().number * 2;
        int result2 = new StaticInnerClass().number * 2;
    }
}
1.3.1 为什么Java内部类可以访问外部类的私有变量?

答:内部类可以访问外部类的私有变量。因为编译器在外部类添加了非私有静态变量access$100和静态方法access$000,它们分别返回外部类的私有静态变量count和方法add(),这样内部类就可以通过这些新增的静态方法访问外部类的私有变量或方法
(1)代码如1.4.1的(1、测试内部类),Outer的反编译后多了access$100和access$000:

 // add()
@groovyx.ast.bytecode.Bytecode
  static void access$000(com.read.kotlinlib.inner.Outer a) {
    aload 0
    INVOKESPECIAL com/read/kotlinlib/inner/Outer.add ()V
    return
  }
  // count
  @groovyx.ast.bytecode.Bytecode
  static int access$100(com.read.kotlinlib.inner.Outer a) {
    aload 0
    getfield com.read.kotlinlib.inner.Outer.count >> int
    ireturn
  }
static int access$100(){
	return count;
}
static void access$000(){
	return add();
}

(2)内部类中通过字节码分析是如何调用静态方法access$100的

在这里插入图片描述
(3)学习链接
细话Java:"失效"的private修饰符

Java编程的逻辑 (21) - 内部类的本质

1.3.2 为什么Kotlin外部类可以访问内部类的private变量?

答:代码如1.4.2的(1、测试内部类嵌套类),原理同1.3.1 Java。

在这里插入图片描述

1.3.3 为什么Java外部类 可以访问 内部类 的 private 变量?

答:代码如1.4.1的(2、测试外部类 可以访问 内部类 的 private 变量),原理同1.3.1 Java:同样的内部类InnerClass自动为外部类Outer生成了一个非私有变量access$300和方法access$200,它返回InnerClass的私有静态变量number和方法getSomething(),外部类即可调用

在这里插入图片描述

1.3.4 为什么Kotlin外部类 不可以访问 内部类或嵌套类 的 private 变量?

答:代码如1.4.2的(2、测试外部类 不可以访问 内部类或嵌套类 的 private 变量),因为Kotlin中不会让 内部类或嵌套类 的 private 变量,为外部类生成一个非私有变量access$300,它依然是私有变量

在这里插入图片描述

1.4 内部类分类?

答:静态内部类、成员内部类、方法内部类、匿名内部类

1.4.1 Java中内部类 和 静态内部类的区别?

答:(1)内部类 和 静态内部类 除了修饰符的区别之外,最主要的是内部类会默认持有一个外部类的引用,也正是这个原因内部类可以直接引用外部类的属性和方法,而不受制于外部类中属性和方法的修饰符。而静态内部类不持有外部类的应用,所以基本跟一个外部类没有什么区别
(2)通俗的理解:如果把类比喻成鸡蛋,内部类为蛋黄,外部类是蛋壳。那么静态类相当于熟鸡蛋,就算蛋壳破碎(外部类没有实例化),蛋黄依然完好(内部类可以实例化);而成员内部类相当于生鸡蛋,蛋壳破碎(无实例化),蛋黄也会跟着破碎(不能实例化)

(3)问题:内部类的持有一个外部类的引用是怎么传入的?–》## 2
(4)学习链接 Kotlin系列之内部类和嵌套类

1.4.2 Kotlin中的内部类 和 嵌套类的区别?

答:在Kotlin中的内部类和Java中的内部类相似,都会持有一个外部类的引用,但是在Kotlin中内部类的声明方式变化了,必须要使用inner修饰符。在Kotlin中,没有静态内部类一说,Java中的静态内部类在Kotlin中称为嵌套类。而且默认的就是嵌套类,也就是内部类不写任何修饰符就是嵌套类,同样的,嵌套类不持有外部类的引用

class Outerkt {
    private val TAG = "Outerkt"
    
    private var count = 0
    fun add() {
        count++
    }

    /**
     * 内部类
     */
    inner class InnerClass {
        private val number = 1
        fun getSomething(): Int {
            // 默认持有外部类的引用,直接访问外部类的方法属性。等价于:this@Outerkt.add()
            add()
            // 等价于:this.number + this@Outerkt.count
            return number + count
        }
    }

    /**
     * 嵌套类
     */
    class NestedClass {
        private val number = 1
        fun getValue(): Int {
            // add()  编译器直接报错
            // 嵌套类不持有外部类的引用,必须通过外部类的对象访问
            val outClass = Outerkt()
            outClass.add()
            // 等价于:this.number + outClass.count
            return this.number + outClass.count
        }
    }

    fun main() {
        // 1、测试内部类嵌套类
        testInnerNested()
        // 2、测试外部类访问内部类的private
        testInnerPrivate()
    }

    /**
     * 1、测试内部嵌套类
     */
    private fun testInnerNested() {
        // 内部类
        val innerClass = Outerkt().InnerClass()  // InnerClass()
        val res = innerClass.getSomething()
        LogUtils.e(TAG, "testInnerNested + 内部类1:$res")
        // testInnerNested + 内部类1:2

        // 嵌套类
        val nestedClass = NestedClass()
        val value = nestedClass.getValue()
        LogUtils.e(TAG, "testInnerNested + 嵌套类1:$value")
        // testInnerNested + 嵌套类1:1
    }

    /**
     * 2、测试外部类 不可以访问 内部类或嵌套类 的 private 变量
     */
    private fun testInnerPrivate() {
        // 在 Kotlin 中,外部类 不可以访问 内部类或嵌套类 的 private 变量:
        // Outerkt().InnerClass().number   编译器直接报错
        // NestedClass().number            编译器直接报错
    }
}

2 Java的匿名内部类有哪些限制?

2.1 这道题想考察什么?

●考察匿名内部类的概念和用法(初级)
●考察语言规范以及语言的横向对比等(中级)
●作为考察内存泄露的切入点(高级)

答:1)编写匿名内部类时,没有人类认知意义上的名字。实际上编译器会指定用于定位的“名字”,一般是 :外部类名称 + $ + N(匿名类顺序)

(2)没有构造函数,构造函数由编译器在编译时创建,开发者没有权定义匿名内部类的构造方法

(3)Java中匿名内部类只能继承一个父类或实现一个接口,不允许同时继承父类和实现接口;但是kotlin可以同时继承父类和实现接口

(4)①如果 定义在非静态方法内(run()) ,匿名内部类会引用外部类对象,可能导致内存泄漏;②如果 父类是非静态时,需父类的外部类对象来初始化自己;③如果匿名内部类持有一份该变量的引用时,为了防止变量变化引起歧义,故要求final保持不变,1.8自动final。实现这些功能的原理是 编译器 会为 匿名内部类的构造方法 引入一些参数,分别是:①匿名内部类自己的外部类对象;②非静态父类的外部类对象;③复制一份局部变量的引用;以及④父类有构造方法且参数列表不为空时,父类的构造方法参数

(5)创建只有单一方法的接口时,可以用Lambda表达式替换实现(多个方法或者抽象类不可以)

在这里插入图片描述

2.2 匿名内部类的名字有什么限制吗?

答:编写匿名内部类时,没有人类认知意义上的名字。实际上编译器会指定用于定位的“名字”,一般是 :外部类名称 + $ + N(匿名类顺序)

new Foo(){…}就是一个匿名内部类了,实际上在字节码中是会定义出来一个用于定位的“名字”,这个**“名字”可见下面代码“com.read.kotlinlib.inner.OuterClass$ 1”,“com.read.kotlinlib.inner.“即代码包名,“OuterClass$ 1“即外部类名 $ 1**。1 代表这个匿名内部类,指的是在前缀的外部类中,定义的第一个匿名内部类,再创建第二个匿名内部类,就是$ 2了。所以匿名内部类跟普通类一样,是可以加载出来的,只不过参数格式不一样,普通类是“class 类名”,匿名内部类是“class 包名.外部类名$num”

public void main() {
    // 1、匿名内部类的名字
    innerClassName();
}

/**
 * 1、匿名内部类的名字
 */
private void innerClassName() {
    try {
        Class fooClass = Class.forName("com.read.kotlinlib.inner.OuterClass$1");
        LogUtils.e(TAG, "fooClass:" + fooClass);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    // fooClass:class com.read.kotlinlib.inner.OuterClass$1(正常)

    try {
        Class fooClass = Class.forName("com.read.kotlinlib.inner.OuterClass$2");
        LogUtils.e(TAG, "fooClass:" + fooClass);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    // java.lang.ClassNotFoundException: com.read.kotlinlib.inner.OuterClass$1(报错)
}

// new Foo()就是匿名内部类
Foo foo = new Foo() {
    @Override
    int bar() {
        return 0;
    }
};
public abstract class Foo {
    abstract int bar();
}
2.2.1 匿名内部类用法?

答:最常见用法就是设置监听器,如下代码:
在这里插入图片描述

2.2.2 构造一个类的新对象与构造一个扩展了匿名内部类的对象之间有什么差别?

答:如果构造参数列表的结束小括号后面跟着一个开始小括号,就是在定义匿名内部类。

// 构造一个类的新对象
val person = Person("May")
// 构造一个扩展了匿名内部类的对象
val person = Person("Terry") {  

}

2.3 匿名内部类的继承结构有什么限制吗?

答:匿名内部类只能继承一个父类或实现一 个接口

(1)当匿名内部类被创建的时候, 就默认 匿名内部类 是作为一个子类去继承其对应的父类的(接口亦同),如下图:
在这里插入图片描述
(2)既 继承某个父类 又 实现某个接口 的“匿名内部类” 的 这种类型,在Java中是不被接受的,编译期直接报错,写不出来这样的代码。因为这其实是一种 或类型,即Runnable或上Foo的结果,作为一种类,这在Java中是不被接受的:
在这里插入图片描述
(3)即使使用Java 10 的var关键字来定义,也是不行的, 这种类型还是不能被接受:在处理var时,编译器先是查看表达式右边部分,也就是所谓的构造器,并将它作为变量的类型,然后将该类型写入字节码当中:
在这里插入图片描述

2.3.1 可是如果实在是想实现一个,既 继承某个父类 又 实现某个接口 的“匿名内部类”这样的类型,但要不想占用太多资源,要求同匿名内部类一样用完即销毁,怎么办?

答:那别用匿名内部类,定义一个有名的内部类,便可以在方法调用完毕后将其回收,也可以达到需求: (注意:这不是匿名内部类,有名称是RunnableFoo)

/**
     * 方法内部定义一个有名的内部类
     */
    class RunnableFoo extends Foo implements Runnable {

        @Override
        public void run() {

        }
    }
2.3.2 Kotlin可以实现:既 继承某个父类 又 实现某个接口 的“匿名内部类” 的 这种类型吗?

答:Kotlin是可以实现:既 继承某个父类 又 实现某个接口 的“匿名内部类” 的 这种类型的:(object类似于class与定义一个引用, object与后面冒号之间不接名字表示匿名,冒号后面要继承什么,实现什么,直接写出来就是了

private fun structure() {
	val runnableFoo = object: Foo(), Runnable {
        override fun run() {}
	}
}

2.4 匿名内部类的构造方法有什么限制吗?

答:①如果 定义在非静态方法内(run()) ,匿名内部类会引用外部类对象,可能导致内存泄漏;②如果 父类是非静态时,需父类的外部类对象来初始化自己;③如果匿名内部类持有一份该变量的引用时,为了防止变量变化引起歧义,故要求final保持不变,1.8自动final。实现这些功能的原理是 编译器 会为 匿名内部类的构造方法 引入一些参数,分别是:①匿名内部类自己的外部类对象;②非静态父类的外部类对象;③复制一份局部变量的引用;以及④父类有构造方法且参数列表不为空时,父类的构造方法参数。
在这里插入图片描述

2.4.1 匿名内部类定义在 非静态方法内(run()) + 父类是非静态(InnerClass)时,参数列表包括:自己的外部类对象,和 非静态父类的外部类对象

答:如下图中例子,(右上)有一个OuterClass,里边有一个InnerClass;(左上)这时候在Client类中,new出来一个匿名内部类。实际上此匿名内部类的父类就是InnerClass,而InnerClass本身是一个非静态的内部类,外部类是OuterClass内部类持有外部类的引用,所以内部类对象的生成,必须先new出外部类的对象,才能new出内部类的对象,因此OuterClass()的实例也会在这里new出来。

而下方的Client$1就是匿名内部类的类型了,Client$1的命名格式是:外部类名称 + $ + N(匿名类顺序)。图中第二行就是 编译器 为 匿名内部类生成的构造方法:其第一个参数,就是Client,即 匿名内部类自己的外部类对象。因为这里的匿名内部类所在的方法 是非静态方法,所以非静态的内部类本身就会引用最外部类的对象;其第二个参数,即匿名内部类非静态父类的外部类对象,因为需要父类的外部类实例来初始化内部类。

在这里插入图片描述

(1)如何通过实际的测试结果验证以上结论?(字节码)

public class ConstructorClient {
    private final String TAG = "OuterClass";
    /**
     * 非静态的内部类
     */
    public void runInnerClass() {
        Constructor.InnerClass inner = new Constructor().new InnerClass() {
            @Override
            void test() {}
        };
    }
}
class Constructor {
    /**
     * 非静态的内部类
     */
    public abstract class InnerClass {
        abstract void test();
    }
    /**
     * 接口
     */
    public interface InnerInterface {
        void test();
    }
}
17: invokespecial #10  // Method com/read/kotlinlib/inner/ConstructorClient$1."<init>":(Lcom/read/kotlinlib/inner/ConstructorClient;Lcom/read/kotlinlib/inner/Constructor;)V

Constant pool:
#10 = Methodref     #6.#33    // com/read/kotlinlib/inner/ConstructorClient$1."<init>":(Lcom/read/kotlinlib/inner/ConstructorClient;Lcom/read/kotlinlib/inner/Constructor;)V
#6 = Class          #30       // com/read/kotlinlib/inner/ConstructorClient$1
#18 = Utf8          <init>
#30 = Utf8          com/read/kotlinlib/inner/ConstructorClient$1
#33 = NameAndType   #18:#37   // "<init>":(Lcom/read/kotlinlib/inner/ConstructorClient;Lcom/read/kotlinlib/inner/Constructor;)V
#37 = Utf8          (Lcom/read/kotlinlib/inner/ConstructorClient;Lcom/read/kotlinlib/inner/Constructor;)V

分析:< init > 是构造函数,括号里面的是参数类型,Lcom/read/kotlinlib/inner/ConstructorClient;Lcom/read/kotlinlib/inner/Constructor; 是两个参数类型(匿名内部类自己的外部类对象,匿名内部类非静态父类的外部类对象),V表示返回 void。

(2)附录:代码(1)反编译生成的文件

在这里插入图片描述

2.4.2 匿名内部类定义在 非静态方法内(run()) + 父类是静态(InnerClass)(接口跟静态匿名内部类的效果一致,是静态的)时,参数列表包括:自己的外部类对象

答:接口跟静态内部类的效果一致,就是静态的,也就是不会去引用其外部类的对象。这时匿名内部类的构造方法的参数就只有一个,就是Client,即 匿名内部类自己的外部类对象。因为这里的匿名内部类所在的方法 是非静态方法,所以非静态的内部类本身就会引用最外部类的对象;
在这里插入图片描述
(1)如何通过实际的测试结果验证以上结论?(字节码)

5: invokespecial #7       // Method com/read/kotlinlib/inner/ConstructorClient$1."<init>":(Lcom/read/kotlinlib/inner/ConstructorClient;)V

#7 = Methodref    #6.#28  // com/read/kotlinlib/inner/ConstructorClient$1."<init>":(Lcom/read/kotlinlib/inner/ConstructorClient;)V
#6 = Class        #27     // com/read/kotlinlib/inner/ConstructorClient$1
#15 = Utf8        <init>
#27 = Utf8        com/read/kotlinlib/inner/ConstructorClient$1
#28 = NameAndType #15:#30 // "<init>":(Lcom/read/kotlinlib/inner/ConstructorClient;)V
#30 = Utf8        (Lcom/read/kotlinlib/inner/ConstructorClient;)V

分析:< init > 是构造函数,括号里面的是参数类型,Lcom/read/kotlinlib/inner/ConstructorClient;是1个参数类型(匿名内部类自己的外部类对象),V表示返回 void

2.4.3 匿名内部类定义在 静态方法内(run()) + 父类是静态(InnerClass)(接口跟静态匿名内部类的效果一致,是静态的)时,参数列表不包括外部类对象了

答:接口跟静态内部类的效果一致,就是静态的,也就是不会去引用其外部类的对象当匿名内部类所在的方法是静态的,则其构造方法的参数中,不存在所在方法的最外部类对象了
在这里插入图片描述
(1)如何通过实际的测试结果验证以上结论?(字节码)

4: invokespecial   #3       // Method com/read/kotlinlib/inner/ConstructorClient$1."<
nit>":()V  

#2 = Class         #15      // com/read/kotlinlib/inner/ConstructorClient$1            
#3 = Methodref     #2.#14   // com/read/kotlinlib/inner/ConstructorClient$1."<init>":()V 
#7 = Utf8          <init>                                                                      
#8 = Utf8          ()V   
#14 = NameAndType  #7:#8    // "<init>":()V                                              
#15 = Utf8         com/read/kotlinlib/inner/ConstructorClient$1  

分析: < init > 是构造函数,括号里面没有参数类型,V表示返回 void

2.4.4 匿名内部类内 有引用外部方法体的局部变量,复制一份局部变量的引用 作为参数列表

答:匿名内部类内 有引用外部方法体变量时,要求被捕获的局部变量被final修饰的。虽然说如果不final的话,对匿名内部类的构造方法也不是很有影响,因为传给匿名内部类构造方法的这个局部变量,实际上只是复制一份局部变量对象的一个快照,快照即复制一份引用,给匿名内部类的构造方法去使用

但是,如果这个局部变量对象不是final的,那局部变量对象被重新赋值了,外部的局部变量跟传给匿名内部类构造方法的局部变量,就不一样了!这个肯定是不行的。所以,Java要求被传给 匿名构造方法 的这个 局部变量一定是final的,原因就是为了让这个局部变量对象,在外部和在匿名构造方法 中,给保持一致

小结:匿名内部类持有一份外部方法体的局部变量的引用,为了防止变量变化引起歧义,故要求final保持不变,1.8自动final
在这里插入图片描述

public static void runInnerObject() {
        // Java8更加智能,如果局部变量被方法内的匿名内部类访问,该局部变量相当于自动使用了final修饰
        final Object obj = new Object();    // Object obj = new Object();    
        Constructor.InnerInterface inner = new Constructor.InnerInterface() {
            @Override
            public void test() {
                System.out.println("runInnerInterface + obj.toString()" + obj.toString());
            }
        };
    }

(1)如何通过实际的测试结果验证以上结论?(字节码)

13: invokespecial #4   // Method com/read/kotlinlib/inner/ConstructorClient$1."<init>":(Ljava/lang/Object;)V

#3 = Class         #16       // com/read/kotlinlib/inner/ConstructorClient$1
#4 = Methodref     #3.#17    // com/read/kotlinlib/inner/ConstructorClient$1."<init>":(Ljava/lang/Object;)V
#16 = Utf8         com/read/kotlinlib/inner/ConstructorClient$1
#17 = NameAndType  #7:#19    // "<init>":(Ljava/lang/Object;)V
#7 = Utf8          <init>
#19 = Utf8         (Ljava/lang/Object;)V

< init > 是构造函数,括号里面的是参数类型,Ljava/lang/Object;是1个参数类型(复制一份局部变量对象的引用),V表示返回 void。

2.5 匿名内部类的 Lambda转换(SAM转换) 有什么限制吗?

答:创建只有单一方法的接口时,可以用Lambda表达式替换实现(多个方法或者抽象类不可以)。SAM是指single abstract method 单一方法类型。
在这里插入图片描述

2.6 学习链接

Android(Java) | 你真的熟悉Java匿名内部类吗(Java匿名内部类的限制)

内存泄漏-内部类持有外部类引用

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值