Koltin-类与类的映射

270 阅读7分钟

第六讲 Koltin 中的类与类的映射

前介

前面讲了 kotlin 是如何定义一个类,我们发现与 Java 还是存在很多不同。今天我们来讲解下 Kotlin 中都有什么类型的类?以及如何定义呢?

讲解前大家想想 Java 中有什么样的类呢?普通类丶抽象类丶非静态内部类丶静态内部类丶匿名内部类丶枚举类大家应该对这些类都在熟悉不过了吧? Kotlin 中也存在这些类只是定义方式不同罢了,并且 Kotlin 中新增了部分类(密封类 丶 Data类丶单例类)。

静态内部类

静态内部类,定义在一个类里面,主要特点就是不持有外部类的引用。Kotlin 中如何定义呢?

class Parent{
    /**
     * 定义静态内部类
     */
    class Child{}
}

反编译的代码:

public final class Parent {
    ...
   public static final class Child {
   }
}

我们看看反编译的 Java 代码,可以发现 Kotlin 中默认定义的内部类就是静态内部类(刚好与 Java 相反)。还发现定义的静态内部类是被 final 修饰的,也就是说不可以被继承的,那如何让其可以继承呢?其实前面讲过通过 open 修饰即可。

非静态内部类

非静态内部类,持有外部类的引用(小心内存泄漏哦)。Kotlin 中只需要通过 inner 修饰符,修饰内部类即可。

class Parent{
    val value = "我是外部的变量"
    /**
     * 定义非静态内部类
     */
    inner class Child{
        fun call(){
            println(value)
        }
    }
}

fun main() {
    // 定义非静态内部类,必须先创建外层类的一个对象,在创建内部类
    val child = Parent().Child()
    child.call()
}

输出结果:
我是外部的变量

反编译的代码:

public final class Parent {
   @NotNull
   private final String value = "我是父类的变量";

   @NotNull
   public final String getValue() {
      return this.value;
   }

   public final class Child {
      public final void call() {
         String var1 = Parent.this.getValue();
         boolean var2 = false;
         System.out.println(var1);
      }
   }
}

通过上面的 Java 的代码可以清晰的看出内部类,没有被 static 修饰。

匿名内部类

匿名内部类,在 Android 中经常被使用,例如我们经常一个方法的参数接收一个接口的实现类或者抽象类对象,我们以前都会直接通过 new 关键词,直接创建一个无名类的对象,并实现其对应方法。

其实匿名内部类,并不是没有名字的。在编译后也会生成 class 文件,类名会定义成 外部类昵称$N (这里的N是一个数字,数字依次递增表示多个匿名内部类)。

/**
 * 定义抽象类
 */
abstract class AbstractClass(val name:String){
   abstract fun call2()
}
class Parent{
    /**
     * 定义一个匿名内部类,赋值给变量
     */
    val callImpl = object : ICall {
        override fun call() {
            println("实现")
        }
    }

    /**
     * 定义抽象类对应的匿名内部类
     */
    val callE=object :AbstractClass("阿文"){
        override fun call2() {
            println("继承")
        }
    }
}

我们看到 Kotlin 中定义匿名内部类不再是通过 new 关键词来定义了?而是使用 object 来定义,并且若抽象类具有构造函数,一定要传入对应构造参数。(大家想想 object 关键词还在哪里用呢?)

我们来看看生成的 class 文件。

参考图片

的确是生成了2个 class文件,奇怪怎么和我说数的 外部类昵称$N结果不一样呢?我特意在写了一个 Java 类。

public class Test {
    ICall callImpl = new ICall() {

        @Override
        public void call() {

        }
    };

    AbstractClass call2 = new AbstractClass("阿文") {

        @Override
        public void call2() {
        }
    };
}

接下来,我通过 ApkTool 反编译了下,直接看 smali 文件更为的直接。看了下图 Java 的确是这样命名规则,但是 Kotlin 规则变了 外部类昵称$接收对象昵称$N(应该是 Kotlin 编译器搞的鬼吧)。为何纠结这个问题呢?因为有时候我想反射创建匿名内部类对象的时候,会用到这个知识点。

枚举类

枚举类和 Java 是一样的,并且支持使用 when 语句。

enum class TestEnum {
    A, B, C
}

fun main() {
    val value = TestEnum.A
    /**
     * 发现有啥特点吗? when 若包含枚举的全部类型,不用写 else 就能使用返回值功能了
     */
    val type = when (value) {
        TestEnum.A -> {
            0
        }
        TestEnum.B -> {
            1
        }
        TestEnum.C -> {
            2
        }
    }
}

单例

Java 中单例设计模式是再常见的一种设计模式了,常见的单例有 懒汉丶饿汉丶枚举单例丶静态内部类单例,以前写单例这种搬砖代码,我都是添加一个代码模块。Kotlin 的开发者似乎也头疼这种问题,为我们专门定义了一种单例类。Kotlin 中只需要通过 object 修饰即可。

/**
 * 定义一个单例
 */
object Single{
    fun call(){}
}

是不是超级简单?哪它属于什么样的单例呢?我们看下反编译后的 Java 代码,原来是饿汉式单例。大家试想下 Kotlin 如何定义一个懒汉式的单例呢?(提示:伴生对象)

public final class Single {
   public static final Single INSTANCE;

   public final void call() {
   }

   private Single() {
   }

   static {
      Single var0 = new Single();
      INSTANCE = var0;
   }
}

data 类

Java 中是不是经常定义实体对象呀,然后从写其 getsettoStringhashCodeequals 等方法。Kotlin 再也不用这么复杂了,只需要通过 data 修饰类即可。

/**
 * 这样定义的类 toString,equals,hashcode 等方法都被自动写了.其中包含了 componentN 方法(这个有啥作用呢?)
 */
data class Play(val name: String, val time: Int)
fun main() {
    val play = Play("阿文", 10000)
    //  componentN
    val (name, time) = play
    println("$name  $time")
}

看下反编译结果:

public final class Play {
   @NotNull
   private final String name;
   private final int time;

   @NotNull
   public final String getName() {
      return this.name;
   }

   public final int getTime() {
      return this.time;
   }

   public Play(@NotNull String name, int time) {
      ...
   }

   @NotNull
   public final String component1() {
      return this.name;
   }

   public final int component2() {
      return this.time;
   }

   @NotNull
   public final Play copy(@NotNull String name, int time) {
      ...
   }

   // $FF: synthetic method
   @NotNull
   public static Play copy$default(Play var0, String var1, int var2, int var3, Object var4) {
     ...
   }

   @NotNull
   public String toString() {
      return "Play(name=" + this.name + ", time=" + this.time + ")";
   }

   public int hashCode() {
      String var10000 = this.name;
      return (var10000 != null ? var10000.hashCode() : 0) * 31 + this.time;
   }

   public boolean equals(@Nullable Object var1) {
     ....
   }
}

通过上面的反编译代码,发现还是重写了很多方法的包括 copy,是深拷贝呢?还是浅拷贝呢?

这里补充下 componentN 方法,这个类似一个操作符重载,后续会将当。其实在测试用例中,我通过 val (name, time) = play 快速创建了2个 nametime 变量,并且将 play 对象中的 component1component2 方法返回值赋值给对应。这个是不是很眼熟呀?没错集合遍历的时候用到这个点了。

fun main() {
    val mutableMapOf = mutableMapOf<String, Int>("阿文" to 18, "小丽" to 18)
    // 是不是很眼熟呀
    for ((value, key) in mutableMapOf) {

    }
}

密封类

密封类定义 sealed 修饰,密封类定义有限个数的子类,并且限制定义范围(不包含间接继承,和 Java 中的枚举有异曲同工之处。

密封类与枚举类存在本质区别,密封类是注重子类个数,枚举类注重的是有限对象个数。那密封类有什么好处呢?举个例子,例如用户场景,分为3种用户 普通会员丶 会员丶 超级会员,我们每种会员都有对应的特殊功能,所有的会员都要是 User 的子类。以前我们可以通过枚举定义这种会员类型,但是不能对其定制特定的方法与属性,在 Kotlin 中可以用密封类来代替。

/**
 * 定义密封类使用 sealed 关键词,来尝试完成Java的枚举类
 */
sealed class User(val nameCore: String) {
    object VIP : User("会员")
    object SUPER_VIP : User("超级会员") {
        fun getUserPhone(): String {
            return "我是超级会员的特有方法"
        }
    }

    object COMMON_VIP : User("普通会员")
}

/**
 * 在 `Kotin 1.1` 修改了密封类的继承范围,在同一个kt文件都可以继承。
 */
open class CommonUser : User("普通用户")

fun main() {
    // 密封类可以配合 when 语法

    val user: User = User.SUPER_VIP
    when (user) {
        User.VIP -> {

        }
        User.COMMON_VIP -> {

        }
        User.SUPER_VIP -> {

        }
    }

}

补充下:在 Kotin 1.1 修改了密封类的继承范围,在同一个kt文件都可以继承。
知识点:sealed 修饰的类为何不能 new 呢?其实 Kotlin 编译器搞的鬼,将密封类编译成一个抽象类,所以无法直接创建对象。

类的映射 typealias

这是个补充点,为和放在类篇章呢?因为它和类有一定关系。还记得集合篇章我说过 Kotlin 并没有重复造轮子,完全使用的 Java 中的集合。怎么实现的呢?其实就是通过 typealias 关键词。

/**
 * 定义类的映射
 */
typealias KotlinFile = File

fun main() {
    /**
     * 使用和File的功能一样
     * Kotlin在编译器会自动替换
     */
    val file:File = KotlinFile("ahah")
    file.exists()
}

其实 typealias 关键词主要在告诉编译器,编译到我定义的 KotlinFile 请给我替换成 File

接下来看看,Kotlin 中定义的集合映射吧!大伙想没想过为啥要这样做呢?个人理解为了解耦,假如有一天 Kotlin 想造轮子了,为了保证以前的代码不用更改,只需要修改映射关系即可。

@SinceKotlin("1.1") public actual typealias RandomAccess = Java.util.RandomAccess
@SinceKotlin("1.1") public actual typealias ArrayList<E> = Java.util.ArrayList<E>
@SinceKotlin("1.1") public actual typealias LinkedHashMap<K, V> = Java.util.LinkedHashMap<K, V>
@SinceKotlin("1.1") public actual typealias HashMap<K, V> = Java.util.HashMap<K, V>
@SinceKotlin("1.1") public actual typealias LinkedHashSet<E> = Java.util.LinkedHashSet<E>
@SinceKotlin("1.1") public actual typealias HashSet<E> = Java.util.HashSet<E>

博客地址