本文收录于 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
上面字节码不多不少,正是我们要分析的源码生成的,主要关注以下几点:
- 对于嵌套类,实质上我们会发现,kotlin会为我们自动编译生成一个新类,该类的命名规则是外部类+ $ +嵌套类名称(比如上面代码生成的新类名为OuterClass$NestedClass),这个类实际上和普通的类没有区别。这就解答了类加载器如何找到嵌套类的问题:kotlin为嵌套类生成了新类。
- 我们之所以能够通过外部类+点(.)访问符访问嵌套类,正如我们推测的那样,原来是kotlin编译器为我们生成了final static的成员属性,即下面字节码完成的功能
public final static INNERCLASS OuterClass$NestedClass OuterClass NestedClass
- 从字节码来看,嵌套类没有任何能够访问到外部类成员(无论是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
字节码看着显然有点混乱,这里照例来分析下主要实现部分:
- 对于内部类,kotlin依然会为我们生成一个新类,类的命名规则就是外部类名 +$ + 内部类名。
- 外部类实际上会为其成员(包括方法和属性)生成一个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
- 内部类在执行构造方法的时候,接收了外部类类型的入参,这表明内部类持有了外部类的引用。字节码示例如下:
//下面是系统生成的内部类的构造方法,接收了外部类作为入参
public <init>(LOuterClass;)V
//这句字节码表明,内部类持有了外部类的引用
final synthetic LOuterClass; this$0
- 无论是嵌套类还是内部类,外部类都可以调用他们的非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
}
万万没有想到,一段小小的匿名内部类代码生成了这么多的字节码,比非匿名内部类生成的还多!确实是这样,基于上面字节码我们来分析总结下:
- kotlin实际上同样会为匿名内部类生成一个新类,只不过这个类有点特殊,它同时实现了父接口,其对应的字节码如下所示:
//编译器生成的新类,类名的命名规则是:
匿名对象所处的类的类名+$+所在的方法名+$+数字,
//这个数字是从1开始,每增加一个匿名内部类对象该值就相应加1。
//因为一个匿名内部类对象就会对应一个匿名内部类
public final class Demo$m1$1 implements ITest {
- 在我们调用匿名内部类方法的时候,实际上就是通过上述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()
- 匿名内部类在没有访问外部类任何成员的情况下,不再持有外部类的引用。这点可以从字节码得到验证。如下所示:
//这个是匿名内部类的构造方法,并没有外部类类型的入参
<init>()V
这和java不太一样。java中匿名内部类即使没有访问外部成员也会默认持有外部类的引用。
- 当匿名内部类访问外部类成员的时候,就会持有外部类的引用,这个和java一致。比如我们在上述代码中作如下变更:
class Demo {
val demoProperty = 0;//增加了一个属性
fun m1(){
object : ITest {
override fun test() {
val test = demoProperty//匿名类的方法中使用了外部类的属性
}
}
}
}
上面代码生成的匿名类的构造方法如下所示:
//这里只截取了构造方法部分,由该构造方法可以看出,
//此时却是有外部类类型的入参,表示内部类持有了外部类。
<init>(LDemo;)V
上面的第2、3点可以认为是kotlin做的一些小优化,同时也提醒我们编码的时候多加注意,毕竟匿名内部类是引起内存泄露的常见原因之一。
至此,本篇文章讲解结束。