前言
本文章只是用于记录学习,所以部分地方如果有错误或者理解不对的地方,麻烦请指正。本篇为 csdn 原文章 转移修改版 原文章
Kotlin 学习笔记(四)
简述:
- kotlin 中数据类的声明及条件
- kotlin 中 密封类/ 枚举
- kotlin 中类型判断
- kotlin 中泛型 和 数据协变
- kotlin 中的类型投射
1. 数据类
在java 中我们通常会创建很多 Bean 类来存储 数据,在kotlin 中有专门的数据类,“data”
data class User(val name: String, val age: Int)
数据类必须满足几个条件
- 主构造函数需要至少有一个参数;
- 主构造函数的所有参数需要标记为 val 或 var;
- 数据类不能是抽象、开放、密封或者内部的;
数据类 也为我们自动生成了部分代码:
- equals()/hashCode() 对;
- toString() 格式是 "User(name=John, age=42)";
- componentN() 函数 按声明顺序对应于所有属性;
- 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 中的 泛型 使用
- 通配符上界,只能从中读取元素,不能添加元素,称为生产者(Producers),用< ? extends T>表示。
- 通配符下界,只能添加元素,不能直接读取下界类型的元素,称为消费者(Consumers),用< ? super T>表示。
3.1 通配符上界
< ? extends T>(T表示通配符的上界),表示可以接收T以及T的子类参数,也就是说可以安全的读取到T的实例,事实上所有的集合元素都是T的子类的实例,但不能向其添加元素,因为没法确定添加的实例类型跟定义的类型是否匹配
List strs = new ArrayList();
strs.add("0");
strs.add("1");
List extends Object> 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 super String> objs = strs;
objs.add("1");
objs.set(0, "2");
// 得到Object类型,如果想要String 还需要强转
Object s = objs.get(0);
Kotlin 中的泛型
不管是Java还是Kotlin,泛型都是使用擦除来实现的,这意味着当你在使用泛型时,任务具体的类型信息都被擦除的,你唯一知道的就是你再使用一个对象。比如,Box
- 类型协变
- 类型投射
- 泛型约束
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
这种声明在Kotlin中称为类型投射(type projection),类型投射的主要用于对参数做了相对因的限定,避免了对该参数类的不安全操作。
3.5 泛型函数
类可以有类型参数。函数也可以有。类型参数要放在函数名称之前:
fun singletonList(item: T): List {
// ……
}
fun T.basicToString() : String { // 扩展函数
// ……
}
// 调用方式 指定 类型为 int
val l = singletonList(1)