Kotlin 学习笔记(五)类型、密封类、数据协变、类型投射和泛型

Kotlin 学习笔记(五)类型、密封类、数据协变、类型投射和泛型_第1张图片
PipingPlover_ZH-CN0992806167_1920x1080.jpg

前言
本文章只是用于记录学习,所以部分地方如果有错误或者理解不对的地方,麻烦请指正。本篇为 csdn 原文章 转移修改版 原文章

Kotlin 学习笔记(四)

简述:

  1. kotlin 中数据类的声明及条件
  2. kotlin 中 密封类/ 枚举
  3. kotlin 中类型判断
  4. kotlin 中泛型 和 数据协变
  5. kotlin 中的类型投射

1. 数据类

  在java 中我们通常会创建很多 Bean 类来存储 数据,在kotlin 中有专门的数据类,“data”

data class User(val name: String, val age: Int)

数据类必须满足几个条件

  1. 主构造函数需要至少有一个参数;
  2. 主构造函数的所有参数需要标记为 val 或 var;
  3. 数据类不能是抽象、开放、密封或者内部的;

数据类 也为我们自动生成了部分代码:

  1. equals()/hashCode() 对;
  2. toString() 格式是 "User(name=John, age=42)";
  3. componentN() 函数 按声明顺序对应于所有属性;
  4. copy() 函数

  在 JVM 中,如果生成的类需要含有一个无参的构造函数,则所有的属性必须指定默认值。

data class User(val name: String = "", val age: Int = 0)

  下面我们使用代码简单联系一下

data class User(val name: String, val age: Int)


fun main(args: Array) {
    var json = User(name = "ymc",age = 1)
    val json1 = json.copy(age = 2)
    println(json1)  // 默认调用 User的 tostring()
}

   在java 中我们通常想要赋值一个值,但是只需要改变某一项的值的数据信息,kotlin 中的 copy函数,只需要传入不一样的数据,就会自动化改变,并返回给你修改后的所有数据信息。

2.密封类

  密封类,可以理解为枚举,规定了有限个类型,不可以存在其他类型,但枚举每个枚举常量只存在一个示例,但是密封类的子类可以有多个示例,所以可以将密封类看做是枚举的拓展,基于枚举,高于枚举,青出于蓝而胜于蓝。

  声明一个密封类,需要在类名前面添加 sealed 修饰符。虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。eg:

// 密封类
sealed class Expr

data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

  相对于密封类的子类 必须要在一个文件中,扩展密封类子类的类(间接继承者)可以放在任何位置,而无需在同一个文件中。

密封类注意点:

1.一个密封类是自身抽象的,它不能直接实例化 ,但是可以有抽象(abstract)成员。
2.密封类不允许有非-private 构造函数(其构造函数默认为 private)。

  使用密封类的关键好处在于使用 when 表达式 的时候,如果能够验证语句覆盖了所有情况,就不需要为该语句再添加一个 else 子句了。

fun eval(expr: Expr): Double{
    return when(expr) {
        is Const -> expr.number
        is Sum -> eval(expr.e1) + eval(expr.e2)
        NotANumber -> Double.NaN
    }
}

2. 类型检测以及自动类型转换

我们可以使用 is 运算符检测一个表达式是否某类型的一个实例(类似于Java中的instanceof关键字)。

fun getStringLength(obj: Any): Int? {
    if (obj is String) {
        // 做过类型判断以后,obj会被系统自动转换为String类型
        return obj.length
    }

    //在这里还有一种方法,与Java中instanceof不同,使用!is
    // if (obj !is String){
    //   // XXX
    // }

    // 这里的obj仍然是Any类型的引用
    return null
}

或者 可以再运算式中 使用

fun getStringLength(obj: Any): Int? {
  // 在 `&&` 运算符的右侧, `obj` 的类型会被自动转换为 `String`
  if (obj is String && obj.length > 0)
    return obj.length
  return null
}

3. 泛型

  kotlin中的泛型和 java 中的差不多,但是很多方面更加简洁。

// kotlin中的 泛型
class Box(t: T) {
    var value = t
}

如果我们要创建

val box: Box = Box(1)
// 1 具有类型 Int,所以编译器知道我们说的是 Box。
val box = Box(1) 

在学习下边Kotlin 的 泛型特性的时候,我们先回顾一下 java 中的 泛型 使用

  1. 通配符上界,只能从中读取元素,不能添加元素,称为生产者(Producers),用< ? extends T>表示。
  2. 通配符下界,只能添加元素,不能直接读取下界类型的元素,称为消费者(Consumers),用< ? super T>表示。

3.1 通配符上界

  < ? extends T>(T表示通配符的上界),表示可以接收T以及T的子类参数,也就是说可以安全的读取到T的实例,事实上所有的集合元素都是T的子类的实例,但不能向其添加元素,因为没法确定添加的实例类型跟定义的类型是否匹配

List strs = new ArrayList();
strs.add("0");
strs.add("1");
List objs = strs;

objs.get(0); // 可以获取

objs.add(1); // 但是添加的时候报错

  经过本人测试,不管添加 Int ,String 类型都提示无法添加,上面的例子说明了objs可以读取值,但是再往objs里面添加值的时候,就会出错,没法确定添加的实例类型跟定义的类型是否匹配。

3.2 通配符下界

< ? super T>,其中T就表示通配符的下界。
举个栗子:Collection< ? super String>是Collection< String>的父类型,所以可以直接add和set,但是get的时候获取到的类型是Object而不是String类型。

List strs = new ArrayList();
strs.add("0");
List objs = strs;
objs.add("1");
objs.set(0, "2");
// 得到Object类型,如果想要String 还需要强转
Object s = objs.get(0);

Kotlin 中的泛型

  不管是Java还是Kotlin,泛型都是使用擦除来实现的,这意味着当你在使用泛型时,任务具体的类型信息都被擦除的,你唯一知道的就是你再使用一个对象。比如,Box和Box在运行时是想的类型,都是Box的实例。在使用泛型时,具体类型信息的擦除是我们不不懂得不面对的,在Kotlin中也为我们提供了一些可供参考的解决方案:、

  1. 类型协变
  2. 类型投射
  3. 泛型约束

3.3 类型协变

  假设我们有一个泛型接口Source< in T, out R >, 其中T由协变注解in修饰,R由协变注解Out修饰.

internal interface Source {

    // in 函数,可以当做参数使用,消费,但是不能作为返回值
    fun mapT(t: T): Unit
    
    // out 函数,不能用来当参数,不能消费,但是可以作为返回值
    fun nextR(): R
}

in T:     来确保Source的成员函数只能消费T类型,而不能返回T类型
out R:  来确保Source的成员函数只能返回R类型,而不能消费R类型

  从上面的解释中,我们可以清楚的知道了协变注解in和out的用意,其实际上是定义了类型参数在该类或者接口的用途,是用来消费的还是用来返回的,对其做了相应的限定。

3.4 类型投射

  从上述代码中我们了解到 泛型的 in 和 out 的使用,下面我们通过一段 代码了解 到底什么是 类型投射

fun copy(from: Array, to: Array) {
    // ...
}

fun fill(dest: Array, value: String) {
    // ...
}

  from的泛型参数使用了协变注解out修饰,意味着该参数不能在该函数中消费,在该方法中 禁止 对该参数进行任何操作。

  对于fill函数中,dest的泛型参数使用了协变注解in修饰,Array与Java的 Array < ? super String> 相同, 也就是说, 你可以使用CharSequence数组,或者 Object 数组作为 fill() 函数的参数、

  这种声明在Kotlin中称为类型投射(type projection),类型投射的主要用于对参数做了相对因的限定,避免了对该参数类的不安全操作。

3.5 泛型函数

  类可以有类型参数。函数也可以有。类型参数要放在函数名称之前:

fun  singletonList(item: T): List {
    // ……
}

fun  T.basicToString() : String {  // 扩展函数
    // ……
}

// 调用方式 指定 类型为 int
val l = singletonList(1)

你可能感兴趣的:(Kotlin 学习笔记(五)类型、密封类、数据协变、类型投射和泛型)