kotlin入门潜修之类和对象篇—嵌套类及其原理

本文收录于 kotlin入门潜修专题系列,欢迎学习交流。

创作不易,如有转载,还请备注。

嵌套类

所谓嵌套类就是类中有类。如下所示:

class OuterClass {//定义一个外部类OuterClass
    class NestedClass {//这里NestedClass嵌套在了OuterClass的内部,所以称为嵌套类。而OuterClass相对
NestedClass可以称作外部类。
        fun m1(): String {
            println(outerProperty)//!!!错误,嵌套类无法访问外部类成员,无论是否为公有!
            return "hello nested class "
        }
    }
}

class Main {
    companion object {
        @JvmStatic fun main(args: Array<String>) {
           //可以通过OuterClass来引用嵌套类NestedClass
            val sayHello = OuterClass.NestedClass().m1()
            println(sayHello)//打印'hello nested class'
        }
    }
}

有朋友看到上面的写法自然而然就会想到这不就是内部类嘛!是的,这种写法在java中确实就是内部类的写法,但是在kotlin中并不能称为内部类,只能称为嵌套类,因为kotlin为内部类的定义提供了一个特定的关键字:inner,只有使用inner修饰的嵌套类才叫做内部类!

上面的措辞可能有点过,从形式上来看嵌套类确实也是内部类。但是为了区分真正的内部类,我们在此约定,只有inner修饰的类才叫内部类,其他在类内部写的统一称为嵌套类。

内部类的定义如下所示:

class OuterClass {
    inner class InnerClass {//使用inner关键字修饰的嵌套类才叫内部类!
        fun m1(): String {
            println(outerProperty)//!!!正确,可以访问外部类成员,无论该成员是公有的还是私有的,都可以!
            return "hello innner class "
        }
    }
}

那么普通的嵌套类和内部类有什么区别?上面两段代码已经展示出了二者的一个区别:内部类可以访问外部类成员,嵌套类则不能!

还是以上面两个类为例,让我们再来看一个二者的区别:

Main {
    companion object {
        @JvmStatic fun main(args: Array<String>) {
            OuterClass.NestedClass().m1()//正确,可以调用
            OuterClass.InnerClass().m1()//错误,无法通过外部类的名称来访问内部类
            OuterClass().InnerClass().m1()//正确,可以通过外部类实例来访问内部类
        }
    }
}

上面代码演示了嵌套类和内部类的又一区别:嵌套类可以通过外部类名直接访问,而内部类则不可以,只能通过外部类的实例进行访问。

需要强调的是所谓嵌套类或者匿名类一定是写在一个类中的,否则即使位于同一文件中也不叫嵌套类。

匿名内部类

注意这里说的是匿名内部类,而不是匿名嵌套类。

所谓匿名内部类是指,我们不再通过显示定义一个父实现的子类型来创建对象,而是直接通过父实现来创建一个没有名字的对象,这个对象对应的类就是匿名内部类。这里所说的父实现一般是指接口或者抽象类。

java中的匿名内部类实现如下所示:

//这里定义了一个接口,ITest,包含一个test()方法
interface ITest {
    void test();
}
//定义了一个类MyTest实现了ITest接口
class MyTest implements ITest {
    @Override
    public void test() {
    }
}
//测试类
public class Main {
    public void main(String[] args) {
        MyTest myTest = new MyTest();
        myTest.test();//通过显示定义的ITest的子实现MyTest实例来调用tes方法t,这个是我们平时常用的方式。
        new ITest() {//注意这里,我们直接通过new ITest接口的方式来调用test()方法,这就是匿名内部类的使用
            @Override
            public void test() {   
            }
        }.test();
    }
}

主要看上面代码中通过new ITest这种使用方式:我们不必再像MyTest那样先显示的去实现ITest,然后通过生成其实例的方式去调用test方法,而是直接通过new一个匿名对象并实现其test方法的方式来完成我们的目的,这个匿名对象所对应的类就是匿名内部类。

看完java的实现方式,那么kotlin中的匿名内部类是不是也是这样实现的呢?

实际上,在kotlin中实现匿名类的方式和java不再一样了,kotlin对于匿名内部类的实现引入了object关键字,此object并不是java中的Object类,这个object是首字母小写的kotlin关键字。关于object的接下来会有一篇文章来探讨它的作用,这里先暂时给出kotlin中匿名内部类的写法,示例如下:

interface ITest {//ITest接口
    fun test()
}
class Main {
    companion object {
        @JvmStatic fun main(args: Array<String>) {
            object : ITest {//!!!注意这里,这就是kotlin中匿名内部类的写法,前面使用object关键修饰,后面跟冒号(:)
                override fun test() {
                }
            }
        }
    }
}

嵌套类实现原理

前面基本演示了嵌套类及内部类的使用,本章节来看下相关的原理。

首先来看普通的嵌套类,kotlin为什么允许在一个class中再写一个class?从类加载的角度来讲,怎么找到这个嵌套类呢?而外部类是怎么能访问到嵌套类,而嵌套类为啥又不能访问到外部类的成员?这些是怎么做到的呢?

首先我们尝试分析下,从上面第一个例子来看,嵌套类的访问是通过外部类的名称加点(.)访问符进行的,这看起来显然有在java中使用静态变量的味道,所以推断是不是在外部类中为嵌套类生成了一个静态成员?想确认推断,只有一个办法,那就是看字节码!

先贴出分析的源码:

class OuterClass {
    private val outerProperty = "i am outer class property"
    class NestedClass {
        fun m1(): String {
            return "hello nested class "
        }
    }
}

其生成的字节码如下所示:

public final class OuterClass {//对应于外部类OuterClass
  // access flags 0x12
  private final Ljava/lang/String; outerProperty = "i am outer class property"//outerclass的成员属性
  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 2 L1
    ALOAD 0
    LDC "i am outer class property"
    PUTFIELD OuterClass.outerProperty : Ljava/lang/String;
    RETURN
   L2
    LOCALVARIABLE this LOuterClass; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
  @Lkotlin/Metadata;(mv={1, 1, 1}, bv={1, 0, 0}, k=1, d1={"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\u0008\u0002\n\u0002\u0010\u000e\n\u0002\u0008\u0002\u0018\u00002\u00020\u0001:\u0001\u0005B\u0005\u00a2\u0006\u0002\u0010\u0002R\u000e\u0010\u0003\u001a\u00020\u0004X\u0082D\u00a2\u0006\u0002\n\u0000\u00a8\u0006\u0006"}, d2={"LOuterClass;", "", "()V", "outerProperty", "", "NestedClass", "production sources for module Kotlin-demo"})
  // access flags 0x19
  public final static INNERCLASS OuterClass$NestedClass OuterClass NestedClass//重点看这里!!!
  // compiled from: Main.kt
}

//注意,下面竟然多出来了一个叫做OuterClass$NestedClass的新类!!!
// ================OuterClass$NestedClass.class =================
// class version 50.0 (50)
// access flags 0x31
public final class OuterClass$NestedClass {
  // access flags 0x11
  public final m1()Ljava/lang/String;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
   L0
    LINENUMBER 6 L0
    LDC "hello nested class "
    ARETURN
   L1
    LOCALVARIABLE this LOuterClass$NestedClass; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 4 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LOuterClass$NestedClass; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
  // access flags 0x19
  public final static INNERCLASS OuterClass$NestedClass OuterClass NestedClass

上面字节码不多不少,正是我们要分析的源码生成的,主要关注以下几点:

  1. 对于嵌套类,实质上我们会发现,kotlin会为我们自动编译生成一个新类,该类的命名规则是外部类+ $ +嵌套类名称(比如上面代码生成的新类名为OuterClass$NestedClass),这个类实际上和普通的类没有区别。这就解答了类加载器如何找到嵌套类的问题:kotlin为嵌套类生成了新类。
  2. 我们之所以能够通过外部类+点(.)访问符访问嵌套类,正如我们推测的那样,原来是kotlin编译器为我们生成了final static的成员属性,即下面字节码完成的功能
public final static INNERCLASS OuterClass$NestedClass OuterClass NestedClass
  1. 从字节码来看,嵌套类没有任何能够访问到外部类成员(无论是private还是public)的入口,比如outerProperty这个属性,在外部类中是私有的成员,外部类也没有为其暴露出公有的get方法之类的,所以嵌套类就无法访问。

看完嵌套类之后,我们来看下内部类的实现原理,同样先贴出我们要分析的源码:

class OuterClass {
    private val outerProperty = "i am outer class property"
    private fun test(){}
    inner class InnerClass {
        fun m1(): String {
            println(outerProperty)
            return "hello innner class"
        }
    }
}

接着,来看下这段代码生成的字节码:

public final class OuterClass {//外部类OuterClass
  // access flags 0x12
  private final Ljava/lang/String; outerProperty = "i am outer class property"
// access flags 0x12
  private final test()V//外部类中test方法的字节码
   L0
    LINENUMBER 4 L0
    RETURN
   L1
    LOCALVARIABLE this LOuterClass; L0 L1 0
    MAXSTACK = 0
    MAXLOCALS = 1
  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 2 L1
    ALOAD 0
    LDC "i am outer class property"
    PUTFIELD OuterClass.outerProperty : Ljava/lang/String;
    RETURN
   L2
    LOCALVARIABLE this LOuterClass; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
// access flags 0x1019
  public final static synthetic access$test(LOuterClass;)V//注意这里!在外部类中为其方法生成了public final staitc方法
//实现就是直接调用本类中的test方法
   L0
    LINENUMBER 1 L0
    ALOAD 0
    INVOKESPECIAL OuterClass.test ()V
    RETURN
   L1
    LOCALVARIABLE $this LOuterClass; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
  // access flags 0x1019
  public final static synthetic access$getOuterProperty$p(LOuterClass;)Ljava/lang/String;//看这里,竟然有个合成的关于outerProperty属性的静态的方法
  @Lorg/jetbrains/annotations/NotNull;() // invisible
   L0
    LINENUMBER 1 L0
    ALOAD 0
    GETFIELD OuterClass.outerProperty : Ljava/lang/String;
    ARETURN
   L1
    LOCALVARIABLE $this LOuterClass; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
  // access flags 0x11
  public final INNERCLASS OuterClass$InnerClass OuterClass InnerClass//看这里,依然有个public的内部类成员,和嵌套类的区别就是不在有static修饰符
  // compiled from: Main.kt
}
//kotlin依然会为我们生成一个新类
// ================OuterClass$InnerClass.class =================
// class version 50.0 (50)
// access flags 0x31
public final class OuterClass$InnerClass {
  // access flags 0x11
  public final m1()Ljava/lang/String;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
   L0
    LINENUMBER 13 L0
    ALOAD 0
    GETFIELD OuterClass$InnerClass.this$0 : LOuterClass;
    INVOKESTATIC OuterClass.access$getOuterProperty$p (LOuterClass;)Ljava/lang/String;//这里语句上对应的是println(outerProperty),实际上
//访问的就是上面外部类生成的静态方法
    ASTORE 1
    NOP
   L1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   L2
   L3
   L4
    LINENUMBER 14 L4
    LDC "hello innner class"
    ARETURN
   L5
    LOCALVARIABLE this LOuterClass$InnerClass; L0 L5 0
    MAXSTACK = 2
    MAXLOCALS = 2
  // access flags 0x1
  // signature ()V
  // declaration: void <init>()
  public <init>(LOuterClass;)V//注意内部类的init方法,竟然接收了外部类类型入参
    @Ljava/lang/Synthetic;() // parameter 0
   L0
    LINENUMBER 11 L0
    ALOAD 0
    ALOAD 1
    PUTFIELD OuterClass$InnerClass.this$0 : LOuterClass;
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LOuterClass$InnerClass; L0 L1 0
    LOCALVARIABLE $outer LOuterClass; L0 L1 1
    MAXSTACK = 2
    MAXLOCALS = 2
  // access flags 0x1010
  final synthetic LOuterClass; this$0//看这里,生成的内部类持有了外部类的引用
  public final INNERCLASS OuterClass$InnerClass OuterClass InnerClass

字节码看着显然有点混乱,这里照例来分析下主要实现部分:

  1. 对于内部类,kotlin依然会为我们生成一个新类,类的命名规则就是外部类名 +$ + 内部类名。
  2. 外部类实际上会为其成员(包括方法和属性)生成一个public final static修饰的新成员,新成员的命名规则是access+$+get+成员名(对于方法来讲,这个成员名就是方法原来的签名,对于属性来讲,则是首字母大写的属性名),这正是我们可以通过内部类访问外部类成员的原因(无论该成员是私有的还是公有的),具体字节码实现摘出来如下所示:
//这里是外部类为属性生成的静态方法
  public final static synthetic access$getOuterProperty$p(LOuterClass;)Ljava/lang/String;
//下面是内部类调用上述方法的字节码
INVOKESTATIC OuterClass.access$getOuterProperty$p (LOuterClass;)Ljava/lang/String;
//这是外部类为方法生成的对外暴露的方法
  public final static synthetic access$test(LOuterClass;)V
//下面是内部类调用上述方法的字节码
INVOKESTATIC OuterClass.access$test (LOuterClass;)V
  1. 内部类在执行构造方法的时候,接收了外部类类型的入参,这表明内部类持有了外部类的引用。字节码示例如下:
//下面是系统生成的内部类的构造方法,接收了外部类作为入参
public <init>(LOuterClass;)V
//这句字节码表明,内部类持有了外部类的引用
final synthetic LOuterClass; this$0
  1. 无论是嵌套类还是内部类,外部类都可以调用他们的非private、非protected方法。这个是显而易见的,因为嵌套类或者内部类实际上是个独立的类,只不过是由kotlin为我们生成的而已。

匿名内部类原理

前面阐述了嵌套类以及内部类的实现原理,本章节最后阐述下匿名内部类的实现原理。

照例,先上要分析的源代码:

interface ITest {//ITest接口
    fun test()//有个test方法
}
//这里是测试类,Demo
class Demo {
    fun m1(){//在方法m1中使用了匿名内部类
        object : ITest {//生成了一个内部类
            override fun test() {
            }
        }.test()//这里我们通过匿名内部类的实例调用其test方法
    }
}

来看下上述代码生成的字节码,如下所示:

public abstract interface ITest {//接口ITest
  // access flags 0x401
  public abstract test()V
    LOCALVARIABLE this LITest; L0 L1 0
}
// ================Demo.class =================
// class version 50.0 (50)
// access flags 0x31
public final class Demo {//Demo 类,这个容易理解
  // access flags 0x11
  public final m1()V//Demo类中的m1方法,内部类的调用正是在这里
   L0
    LINENUMBER 7 L0
   L1
    LINENUMBER 11 L1
   L2
    LINENUMBER 7 L2
    NEW Demo$m1$1//注意这里,竟然new了一个Demo$m1$1类型的实例?这是什么鬼!
    DUP
    INVOKESPECIAL Demo$m1$1.<init> ()V//调用Demo$m1$1的构造方法
   L3
    LINENUMBER 11 L3
    INVOKEVIRTUAL Demo$m1$1.test ()V//注意这里,完成了对匿名内部对象方法的调用
   L4
    LINENUMBER 12 L4
    RETURN
   L5
    LOCALVARIABLE this LDemo; L0 L5 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LDemo; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
  // access flags 0x19
  public final static INNERCLASS Demo$m1$1 null null//这里多一个对Demo$m1$1的引用
  // compiled from: Main.kt
}
//注意,下面这个类是kotlin为我们生成的!!!
// ================Demo$m1$1.class =================
// class version 50.0 (50)
// access flags 0x31
public final class Demo$m1$1 implements ITest  {//而且实现了ITest接口
  OUTERCLASS Demo m1 ()V
  // access flags 0x1
  public test()V
   L0
    LINENUMBER 9 L0
    RETURN
   L1
    LOCALVARIABLE this LDemo$m1$1; L0 L1 0
    MAXSTACK = 0
    MAXLOCALS = 1
  // access flags 0x0
  <init>()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LDemo$m1$1; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
  // access flags 0x19
  public final static INNERCLASS Demo$m1$1 null null
  // compiled from: Main.kt
}

万万没有想到,一段小小的匿名内部类代码生成了这么多的字节码,比非匿名内部类生成的还多!确实是这样,基于上面字节码我们来分析总结下:

  1. kotlin实际上同样会为匿名内部类生成一个新类,只不过这个类有点特殊,它同时实现了父接口,其对应的字节码如下所示:
//编译器生成的新类,类名的命名规则是:
匿名对象所处的类的类名+$+所在的方法名+$+数字,
//这个数字是从1开始,每增加一个匿名内部类对象该值就相应加1。
//因为一个匿名内部类对象就会对应一个匿名内部类
public final class Demo$m1$1 implements ITest  {
  1. 在我们调用匿名内部类方法的时候,实际上就是通过上述kotlin为我们生成的匿名内部类实例进行调用的,其对应的字节码如下所示(位于类Demo的字节码中):
 L2
    LINENUMBER 7 L2
    NEW Demo$m1$1//首先new一个匿名类实例
    DUP
    INVOKESPECIAL Demo$m1$1.<init> ()V//完成构造方法初始化
   L3
    LINENUMBER 11 L3
    INVOKEVIRTUAL Demo$m1$1.test ()V//通过该实例调用test方法。

上面调用的test方法正是Demo$m1$1需要实现的方法(因为Demo$m1$1实现了ITest接口,所以必须要实现该方法),对应用源码中就是下面这段:

        object : ITest {
            override fun test() { }//就是这段代码
        }.test()
  1. 匿名内部类在没有访问外部类任何成员的情况下,不再持有外部类的引用。这点可以从字节码得到验证。如下所示:
//这个是匿名内部类的构造方法,并没有外部类类型的入参
<init>()V

这和java不太一样。java中匿名内部类即使没有访问外部成员也会默认持有外部类的引用。

  1. 当匿名内部类访问外部类成员的时候,就会持有外部类的引用,这个和java一致。比如我们在上述代码中作如下变更:
class Demo {
    val demoProperty = 0;//增加了一个属性
    fun m1(){
        object : ITest {
            override fun test() {
                val test = demoProperty//匿名类的方法中使用了外部类的属性
            }
        }
    }
}

上面代码生成的匿名类的构造方法如下所示:

//这里只截取了构造方法部分,由该构造方法可以看出,
//此时却是有外部类类型的入参,表示内部类持有了外部类。
<init>(LDemo;)V

上面的第2、3点可以认为是kotlin做的一些小优化,同时也提醒我们编码的时候多加注意,毕竟匿名内部类是引起内存泄露的常见原因之一。

至此,本篇文章讲解结束。

推荐阅读更多精彩内容