接触Kotlin前前后后也有大半年了,和Java相比最直观的感受就是两点:1、API功能更全面、更简洁。Kotlin除了能提供Java API里所有的功能,还在此基础上添加了新的一些常用的方法,而这些方法之前使用Java时很多需要自己实现。 2、协程,它提供了比线程更小一级的执行单位,使CPU利用率更高了。
下面这些记录主要是我复习和巩固用的,也趁机把它写成博客,对于已经很熟悉Kotlin的同学来说没多大价值。
1、属性Property和字段field的区别
这里一直是我很迷惑的地方,直到开始研究Kotlin编译后的Java代码,在做它们的对比并理解Kotlin的思想之后才逐渐明白。字段是数据存储位置,是一小块内存,属性是一种对外暴露的访问规则,本质就是怎么访问;属性不一定有字段。在Java中 int age = 18,在内存中就真的会有一个4字节的空间来存储这个字段,当访问user.age的时候其实就是真的去读了这个内存值,这就是字段;在Kotlin中,定义一个 var age = 18,也有字段,但是如果是定义一个
var aget:Int
get() = (1..100).random()
set(value) {
println("设置的值是:$value")
}
可就没有字段了,这种情况就非常体现出字段和属性的区别了,属性------一种访问规则。在这个情况下,你读取age值,它就通过get方法返回一个临时的值,你通过set设置值,其实真实逻辑可以随便实现,并不一定要真的设置一个值。先来看下Kotlin代码编译后的情况吧
//Kotlin源码
class User{
var age:Int = 8
val name:String = "Tom"
var address: String
get() = "深圳市"
set(value) {
println("设置了一个新的地址:${value}")
}
}
//编译后的Java代码
public final class User {
private int age = 8;
@NotNull
private final String name = "Tom";
public final int getAge() {
return this.age;
}
public final void setAge(int i) {
this.age = i;
}
@NotNull
public final String getName() {
return this.name;
}
@NotNull
public final String getAddress() {
return "深圳市";
}
public final void setAddress(@NotNull String value) {
Intrinsics.checkNotNullParameter(value, "value");
System.out.println((Object) ("设置了一个新的地址:" + value));
}
}
可知,age和name都有对应的字段,且age由于是var,还自动生成了get和set方法,name是val,所以自动生成了get方法,当读取age、name时其实就是在调用它们的get方法,当修改age时,其实就是在调用setAge方法;而address方法,由于我们手动提供了get/set,所以,也就没有字段了。那能不能这样定义呢
var address: String ="龙华区"
get() = "深圳市"
set(value) {
println("设置了一个新的地址:${value}")
}
不能!Kotlin的规则,一旦 自己同时重写了 getter 和 setter,这个属性就不再是"默认访问器 + 默认字段"的模式了,没有字段可以存储这个"龙华区"。不过可以这样写
var address: String = "龙华区"
get() = field
set(value) {
field = value
println("设置了一个新的地址:$value")
}
field就是字段,你在get/set里使用到了字段,也就会自动生成一个字段了。
2、数据类data class
专门用来保存数据的类。与普通类相比,使用 data 关键字声明的类,编译器会自动为它生成几个标准且常用的方法,这省去了我们手动编写大量样板代码的麻烦。
一句简单的 data class KotlinUser(var name:String,var age:Int){ var id:Int = 0 },编译后就是这样的
public final class KotlinUser {
@NotNull
private String name;
private int age;
private int id;
@NotNull
public final String component1() {
return this.name;
}
public final int component2() {
return this.age;
}
@NotNull
public final KotlinUser copy(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
return new KotlinUser(name, age);
}
public static /* synthetic */ KotlinUser copy$default(KotlinUser kotlinUser, String str, int i, int i2, Object obj) {
if ((i2 & 1) != 0) {
str = kotlinUser.name;
}
if ((i2 & 2) != 0) {
i = kotlinUser.age;
}
return kotlinUser.copy(str, i);
}
@NotNull
public String toString() {
return "KotlinUser(name=" + this.name + ", age=" + this.age + ')';
}
public int hashCode() {
int result = this.name.hashCode();
return (result * 31) + Integer.hashCode(this.age);
}
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (!(other instanceof KotlinUser)) {
return false;
}
KotlinUser kotlinUser = (KotlinUser) other;
return Intrinsics.areEqual(this.name, kotlinUser.name) && this.age == kotlinUser.age;
}
public KotlinUser(@NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
this.name = name;
this.age = age;
}
@NotNull
public final String getName() {
return this.name;
}
public final void setName(@NotNull String str) {
Intrinsics.checkNotNullParameter(str, "<set-?>");
this.name = str;
}
public final int getAge() {
return this.age;
}
public final void setAge(int i) {
this.age = i;
}
public final int getId() {
return this.id;
}
public final void setId(int i) {
this.id = i;
}
}
编译器自动为其改写了equals()、hashcode()、toString()、copy()。Kotlin中的 == 不同于Java,在Java中 == 对于基本数值,比较的是它们的值,对于引用对象,则比较的是引用地址,但Kotlin中,对于data class来说 == 比较的是主构造参数是否一一相等,对于非data class来说, == 比较的是 equals()。Kotlin中对于引用的比较则是用 === 。
val user1 = KotlinUser("H",2)
user1.id = 1
val user2 = KotlinUser("H",2)
user2.id = 3
println(user1.equals(user2)) //结果为true
println(user1 == user2) //结果为true
val a =A("H") //A是非data class
val b =A("H")
println(a==b) //false
println(a.equals(b)) //false
3、密封类 sealed class
是 Kotlin 中一个用于限制类层次结构的特殊抽象类。它的核心作用是:一个类只能有有限的、已知的子类型,并且这些子类型都定义在同一个文件内(Kotlin 1.5 之后放宽到同一个编译单元,但通常仍建议同文件)。
sealed class NetworkResult {
data class Success(val data: String) : NetworkResult()
data class Error(val throwable: Throwable) : NetworkResult()
object Loading : NetworkResult()
}
看起来跟枚举类很像,而Kotlin也确实存在枚举类,那么,有了枚举类,为什么又要设计一个密封类呢?看上面的代码就可以很明显地看出来,以往枚举类只能定义成统一的格式,如果想表达上述的同样的效果,则每个枚举值都得带有data、throwable,密封类则可以只带自己需要的。
4、伴生对象(companion object)
伴生对象(Companion Object) 是 Kotlin 中用来替代 Java static 关键字的机制。它允许你在类内部定义一个属于类本身而不是实例 的对象。Kotlin 语言设计者 Andrey Breslav 明确指出:Kotlin 中不存在类的 "静态成员" 概念 。在纯面向对象范式中,所有行为和状态都应属于对象,而static成员本质上是不属于任何对象的全局实体,破坏了面向对象的封装性和一致性。
Java 号称"一切皆对象",但 static 打破了这个承诺:
(1)static 方法/字段不属于任何实例,而是属于"类本身"------但"类本身"在 JVM 中不是一个对象(虽然 Class 对象存在,那是另一回事)。
(2)static 方法不能被继承、不能被重写、不能作为参数传递,也不能作为实现接口的方法。它更像是过程式语言中的全局函数,只是借用了类的命名空间。
Kotlin 的设计者希望消除这种割裂:每个函数、每个属性都必须属于一个对象。当你需要在类级别(不依赖实例)提供功能时,你其实是在定义一个属于类本身的一个对象------这就是伴生对象。也就是,kotlin的设计者认为类不该有属性和方法,每个属性和方法都必须属于某个对象,所以对于Java中static的属性和方法就要通过一个对象来实现,不过既然是一个对象,那么就得有这个对象的类,这个类是什么呢?看如下代码
//Kotlin源码
class HelloKotlin{
companion object{
const val NAME = "test"
val ADDRESS = "深圳"
var age = 18
fun show(info:String){
println(info)
}
}
}
//编译后生成的Java代码
public final class HelloKotlin {
@NotNull
public static final String NAME = "test";
@NotNull
public static final Companion Companion = new Companion((DefaultConstructorMarker) null);
@NotNull
private static final String ADDRESS = "深圳";
private static int age = 18;
}
//编译后还自动生成了一个HelloKotlin$Companion.class文件,里面的代码如下
public final class HelloKotlin$Companion {
public /* synthetic */ HelloKotlin$Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
private HelloKotlin$Companion() {
}
@NotNull
public final String getADDRESS() {
return HelloKotlin.access$getADDRESS$cp();
}
public final int getAge() {
return HelloKotlin.access$getAge$cp();
}
public final void setAge(int i) {
HelloKotlin.access$setAge$cp(i);
}
public final void show(@NotNull String info) {
Intrinsics.checkNotNullParameter(info, "info");
System.out.println((Object) info);
}
}
所以这个伴生对象的类就是由编译器自动生成的 XXX$Companion。
观察可发现伴生对象里,const val变成了类的public static final,这一点好理解;var 和 val 却都变成了private static ,而get/set方法却又放到了Companion类里作为了实例方法,看起来不伦不类的,这是什么设计?如果是按照Java的思路,那应该把var、val也都放到HelloKotlin作为public static,但是这样就违背了Kotlin任何方法、属性都应该属于对象的设计原则;如果按照Kotlin的思路,那就应该全都放到 HelloKotlin$Companion里,作为对象的实例字段和实例get/set方法才对,但是这样的话,Java调用Kotlin代码时就会变成HelloKotlin.Companion.age,而不是HelloKotlin.age或HelloKotlin.getAge(),这样的调用就会很奇怪(会疑惑Companion是什么,莫名其妙)。而之所以const val最终转换为public static final,是因为const val与Java的static final等效,这叫编译时常量,JVM对这类数据有一些特定的规则,为了能和JVM兼容,必须转换为public static final。总结一下就是,Kotlin为了和Java兼容,同时又为了尽量践行一切属性和方法都属于对象的设计原则,最终做出了如上的折中措施。
不过Kotlin还是提供了 @JVMStatic 来提高与Java之间的互操作性,也更方便习惯Java思维的人使用。如下
//Kotlin源码
class HelloKotlin{
companion object{
@JvmStatic
var age = 18
}
}
//编译后得到的Java源码
public final class HelloKotlin {
@NotNull
public static final Companion Companion = new Companion((DefaultConstructorMarker) null);
private static int age = 18;
public static final int getAge() {
return Companion.getAge();
}
public static final void setAge(int i) {
Companion.setAge(i);
}
}
既然伴生对象是一个类的实例,那这个类是否也可以实现接口、继承其他类呢?可以。比如如下这样
class HelloKotlin{
companion object: InterfaceHou{
const val NAME = "test"
val ADDRESS = "深圳"
var age = 18
fun show(info:String){
println(info)
}
override fun haha() {
}
}
}
5、扩展属性/扩展方法
扩展方法和扩展属性让你能够"扩展"任何类(包括标准库的类、第三方库的类)而无需继承它们或直接修改它们的源代码。下面示例修改原生API里的ArrayList
fun ArrayList<Int>.showSum(){
var sum=0;
for(i in this){
sum += i
}
println(sum)
}
fun main() {
val arrayList = ArrayList<Int>()
arrayList.add(1)
arrayList.add(2)
arrayList.add(3)
arrayList.showSum()
}
//编译后得到的Java代码如下
public static final void showSum(@NotNull ArrayList<Integer> arrayList) {
Intrinsics.checkNotNullParameter(arrayList, "<this>");
int sum = 0;
Iterator<Integer> it = arrayList.iterator();
Intrinsics.checkNotNullExpressionValue(it, "iterator(...)");
while (it.hasNext()) {
Integer next = it.next();
Intrinsics.checkNotNullExpressionValue(next, "next(...)");
int i = next.intValue();
sum += i;
}
System.out.println(sum);
}
public static final void main() {
ArrayList arrayList = new ArrayList();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
showSum(arrayList);
}
其实原理就是生成了一个static方法,参数就是被扩展的类的一个对象,并且对象是作为第一个参数(如果还有其他参数,其他参数就排在后面)。
扩展属性类似
var ArrayList<Int>.info:String
get() = "元素个数:${this.size}"
set(value) {}
fun main() {
val arrayList = ArrayList<Int>()
arrayList.add(1)
arrayList.add(2)
arrayList.add(3)
arrayList.showSum(2)
println(arrayList.info)
}
//编译后
public static final String getInfo(@NotNull ArrayList<Integer> arrayList) {
Intrinsics.checkNotNullParameter(arrayList, "<this>");
return "元素个数:" + arrayList.size();
}
public static final void setInfo(@NotNull ArrayList<Integer> arrayList, @NotNull String value) {
Intrinsics.checkNotNullParameter(arrayList, "<this>");
Intrinsics.checkNotNullParameter(value, "value");
}
public static final void main() {
ArrayList arrayList = new ArrayList();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
System.out.println((Object) getInfo(arrayList));
}