前言
书接上回,上回跟大家聊了下kotlin的一些常用的语法和如何通过将kotlin转换为java代码来深入了解koltin语法的本质,今天我们继续了解一下kotlin的其他一些特性。
其实我在写上一篇文章的时候,介绍的一些方法主要是为了跟大家分享一下,如何去理解kotlin语法糖背后的本质,不过后来想想前面的内容又太单一简单了,因此就继续水一篇文章。我们继续了解一下一下kotlin其他的语法糖。
密封类和枚举类
枚举类我们就不详细展开了,相信大家都很了解。它常常用于定义有限的集合。因此常被我们当做单例和多例模式。
kotlin
enum class Gender{
MALE,// 定义男性
FEMALE// 定义女性
}
// 使用方式,判断是否是男性
fun isMale(people : Gender) = people == MALE
那么什么是密封类呢?刚接触密封类的时候,我还是比较奇怪的,首先我们来看下密封类的使用方式
kotlin
// 首先定义一个密封类,人类
sealed class People {
class Male : People() // 定义男性
class Female : People() // 定义女性
}
// 使用方式,判断是否是男性
fun isMale(people : People) = people is Male
在形式上看似比较像,但是这里有一个很大的区别,枚举类里面我们使用的是 == 进行判断,密封类我们是使用is(类似Java的instanceOf),看到这里大家可能已经有一个大致的区分。枚举类定义出来的,已经是类的实例对象,而密封类定义的是类的子类。他们都用于表示限定的类层次结构,但是方式不同。因此,在使用上,我们常常拿枚举当做单例或者多例模式。而密封类常常当做一个被限定子类的集合,但是子类的实例可以有多个,就好比如我们去请求一个接口,返回的结果成功、失败、异常,但是可以有很多次成功、失败、异常。 再回到上面的例子看,枚举里的男性、女性我们通常可以设定为一个属性,而密封类的男性、女性我们设定为一个子类特征集合,他们互不冲突,两者是可以互补的,我们讲上述的例子结合一下。
kotlin
// 首先定义一个密封类,
enum class Gender{
MALE,// 定义男性
FEMALE// 定义女性
}
sealed class People(gender : Gender) {
class Male : People(Gender.MALE) // 定义男性
class Female : People(Gender.FEMALE) // 定义女性
}
我们将密封类和枚举类结合,枚举类的Gender是密封类People的一个性别属性。看到这里,大家基本上就可以分清楚枚举类和密封类的区别了。
我们将上述代码转换成Java代码看一下:
scala
// Gender.java
package com.yuanyi.myapplication.kt;
import kotlin.Metadata;
// 定义的枚举类型
public enum Gender {
MALE,
FEMALE;
}
// People.java
package com.yuanyi.myapplication.kt;
import kotlin.Metadata;
import kotlin.jvm.internal.DefaultConstructorMarker;
// 定义的密封类,我们发现
// 其实我们定义的密封类在Java当中是一个抽象类
public abstract class People {
private People(Gender gender) {
}
// $FF: synthetic method
public People(Gender gender, DefaultConstructorMarker $constructor_marker) {
this(gender);
}
// 我们定义的Male子类是密封类的静态内部子类
public static final class Male extends People {
public Male() {
super(Gender.MALE, (DefaultConstructorMarker)null);
}
}
// 我们定义的Female子类是密封类的静态内部子类
public static final class Female extends People {
public Female() {
super(Gender.FEMALE, (DefaultConstructorMarker)null);
}
}
}
看到这里,大家应该就能理解枚举类和密封类的本质区别在哪里了。 我们知道,在kotlin中,枚举类和密封类都是支持when(类似Java的switch-case语句)的判定的。我们来看下以下代码:
kotlin
// 通过枚举类型的判定
fun isMaleByGender(gender: Gender) : Boolean =
when(gender){
Gender.MALE-> true
Gender.FEMALE -> false
}
// 通过密封类的判定
fun isMaleByPeople(people: People) : Boolean=
when(people){
is People.Male -> true
is People.Female -> false
}
枚举类型的判定是直接通过when(param) -> value 的方式判定的,而密封类的是通过when(param) -> is value的方式进行的。
kotlin的运算符重载
kotlin和C++一样,是支持运算符重载的,可是Java是没有运算符重载的。那么为什么Java没有运算符重载呢?因为Java的设计者认为,运算符本质上也是函数的调用。没错,其实kotlin的运算符本质上也是方法调用,只不过kotlin的编译器帮助我们能够使用重载之后的运算符进行开发,提高我们的开发效率。
下面,我们来看一个运算符重载的例子
kotlin
// 一个简单的数据类
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Foo) : Foo = Foo(x + other.x, y + other.y)
}
fun main(args: Array<String>) {
// 使用的时候
val f1 = Foo(10, 20)
val f2 = Foo(30, 40)
// 直接用+运算符代替plus函数,事实上会调用plus函数
println(f1 + f2) // 打印内容为Foo(x=40, y=60)
}
在上述例子当中,我们实现了加法运算符号的重载来计算两个坐标相加的功能。其实例子很简单,我们来简单了解下运算符重载的本质是什么。老规矩,我们看下翻译成Java代码
java
// Point.java
package com.yuanyi.myapplication.kt;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class Point {
private final int x;
private final int y;
@NotNull
public final Point plus(@NotNull Point other) {
Intrinsics.checkNotNullParameter(other, "other");
return new Point(this.x + other.x, this.y + other.y);
}
public final int getX() {
return this.x;
}
public final int getY() {
return this.y;
}
public Point(int x, int y) {
this.x = x;
this.y = y;
}
...
}
// KtOperatorKt.java
package com.yuanyi.myapplication.kt;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
public final class KtOperatorKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkNotNullParameter(args, "args");
Point f1 = new Point(10, 20);
Point f2 = new Point(30, 40);
Point var3 = f1.plus(f2);
System.out.println(var3);
}
}
我们发现,在Java当中,我们的"+"的操作符,被转换成了plus方法,这是kotlin帮我们做的。所以,本质上,kotlin的运算符重载就是方法调用,其实我们使用的block() 亦是如此,他重载了invoke。更多支持重载的运算符,这里就不一一展开了,可以查看kotlin的中文网的说明。
kotlin的解构
在kotlin当中,我们可以将对象的属性赋值给多个新定义的属性,这种被称为kotlin的解构。我们看下下面这段代码
kotlin
data class Human(
val name : String,
val age : Int,
val gender : Int
)
fun deconstruct(){
val human = Human(
name = "Jack",
age = 18,
gender = 1
)
// 直接定义解构接收的多个参数
// 接收的参数类型需要对齐
val (name, age , gender ) = human
println(
"name:$name,age:$age,gender:$gender"
)
}
这里是不是觉得好理解,其实本质上应该就是参数的赋值吧。那么究竟是不是呢,我们来看一下Java对应的代码
kotlin
// Deconstruct.java
package com.yuanyi.myapplication.kt;
import kotlin.Metadata;
@Metadata(
mv = {1, 8, 0},
k = 1,
d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002¨\u0006\u0003"},
d2 = {"Lcom/yuanyi/myapplication/kt/Deconstruct;", "", "()V", "app_debug"}
)
public final class Deconstruct {
}
// Human.java
package com.yuanyi.myapplication.kt;
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@Metadata(
mv = {1, 8, 0},
k = 1,
d1 = {"\u0000 \n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\b\n\u0002\b\f\n\u0002\u0010\u000b\n\u0002\b\u0004\b\u0086\b\u0018\u00002\u00020\u0001B\u001d\u0012\u0006\u0010\u0002\u001a\u00020\u0003\u0012\u0006\u0010\u0004\u001a\u00020\u0005\u0012\u0006\u0010\u0006\u001a\u00020\u0005¢\u0006\u0002\u0010\u0007J\t\u0010\r\u001a\u00020\u0003HÆ\u0003J\t\u0010\u000e\u001a\u00020\u0005HÆ\u0003J\t\u0010\u000f\u001a\u00020\u0005HÆ\u0003J'\u0010\u0010\u001a\u00020\u00002\b\b\u0002\u0010\u0002\u001a\u00020\u00032\b\b\u0002\u0010\u0004\u001a\u00020\u00052\b\b\u0002\u0010\u0006\u001a\u00020\u0005HÆ\u0001J\u0013\u0010\u0011\u001a\u00020\u00122\b\u0010\u0013\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\u0014\u001a\u00020\u0005HÖ\u0001J\t\u0010\u0015\u001a\u00020\u0003HÖ\u0001R\u0011\u0010\u0004\u001a\u00020\u0005¢\u0006\b\n\u0000\u001a\u0004\b\b\u0010\tR\u0011\u0010\u0006\u001a\u00020\u0005¢\u0006\b\n\u0000\u001a\u0004\b\n\u0010\tR\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\u000b\u0010\f¨\u0006\u0016"},
d2 = {"Lcom/yuanyi/myapplication/kt/Human;", "", "name", "", "age", "", "gender", "(Ljava/lang/String;II)V", "getAge", "()I", "getGender", "getName", "()Ljava/lang/String;", "component1", "component2", "component3", "copy", "equals", "", "other", "hashCode", "toString", "app_debug"}
)
public final class Human {
@NotNull
private final String name;
private final int age;
private final int gender;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public final int getGender() {
return this.gender;
}
public Human(@NotNull String name, int age, int gender) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.age = age;
this.gender = gender;
}
// 我们发现,多了component1~3的方法
@NotNull
public final String component1() {
return this.name;
}
public final int component2() {
return this.age;
}
public final int component3() {
return this.gender;
}
// 这里还多了一个copy的方法
@NotNull
public final Human copy(@NotNull String name, int age, int gender) {
Intrinsics.checkNotNullParameter(name, "name");
return new Human(name, age, gender);
}
// $FF: synthetic method
public static Human copy$default(Human var0, String var1, int var2, int var3, int var4, Object var5) {
if ((var4 & 1) != 0) {
var1 = var0.name;
}
if ((var4 & 2) != 0) {
var2 = var0.age;
}
if ((var4 & 4) != 0) {
var3 = var0.gender;
}
return var0.copy(var1, var2, var3);
}
@NotNull
public String toString() {
return "Human(name=" + this.name + ", age=" + this.age + ", gender=" + this.gender + ")";
}
public int hashCode() {
String var10000 = this.name;
return ((var10000 != null ? var10000.hashCode() : 0) * 31 + Integer.hashCode(this.age)) * 31 + Integer.hashCode(this.gender);
}
public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof Human) {
Human var2 = (Human)var1;
if (Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age && this.gender == var2.gender) {
return true;
}
}
return false;
} else {
return true;
}
}
}
// DeconstructKt.java
package com.yuanyi.myapplication.kt;
import kotlin.Metadata;
@Metadata(
mv = {1, 8, 0},
k = 2,
d1 = {"\u0000\b\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001¨\u0006\u0002"},
d2 = {"deconstruct", "", "app_debug"}
)
public final class DeconstructKt {
public static final void deconstruct() {
Human human = new Human("Jack", 18, 1);
// 关键在这里,我们发现,参数的或并不是通过class#getParams()的方式,而是通过class#componentN()的方式赋值的
String name = human.component1();
int age = human.component2();
int gender = human.component3();
String var4 = "name:" + name + ",age:" + age + ",gender:" + gender;
System.out.println(var4);
}
}
那么为什么不是使用getParams的方式,而是要多几个方法呢?这不是多此一举吗?其实不然,不知道大家发现没有,上述的class我使用的是data class,在kotlin当中,data class都会默认生成componentN和copy的方法,原因就是这个,当我们去掉data的,让其变成一个普通的class的时候,我们发现会报如下错误。
我们按照提示,将componentN的方法加上去如下:
我们发现,代码顺利通过。这里我们惊奇的发现,componentN前面有个关键字,是operator,没错,解构的本质就是kotlin的操作符重载,之所以把解构放在操作符重载后面讲就是这个原因。
解构的方式其实我们还可以用在lamda表达式当中,如下所示:
kotlin
fun deconstruct(){
val human = Human(
name = "Jack",
age = 18,
gender = 1
)
blockDeconstruct(human){
(name,age,gender)-> // 前提是支持解构
run {
println(
"name:$name,age:$age,gender:$gender"
)
}
}
}
fun blockDeconstruct(human: Human,block : (Human) -> Unit) = block(human)
其实kotlin帮我们封装了很多的方法,来提升我们的开发效率,比如Collections当中的filter、map都是依靠支持迭代器(Iterator)的扩展函数实现的,因此我们在学习kotlin的语法糖的时候,要找对方法。不过也不是所有的语法糖都能通过java代码可以看出来的,有些很多也是依赖编译器一起实现的,例如协程的原因。那么本次介绍的语法糖就到这里吧。