Kotlin-通过Java反编译撕开Kotlin的高端语法糖

前言

书接上回,上回跟大家聊了下kotlin的一些常用的语法和如何通过将kotlin转换为java代码来深入了解koltin语法的本质,今天我们继续了解一下kotlin的其他一些特性。

其实我在写上一篇文章的时候,介绍的一些方法主要是为了跟大家分享一下,如何去理解kotlin语法糖背后的本质,不过后来想想前面的内容又太单一简单了,因此就继续水一篇文章。我们继续了解一下一下kotlin其他的语法糖。

密封类和枚举类

枚举类我们就不详细展开了,相信大家都很了解。它常常用于定义有限的集合。因此常被我们当做单例和多例模式。

1
2
3
4
5
6
7
kotlin复制代码enum class Gender{
MALE,// 定义男性
FEMALE// 定义女性
}

// 使用方式,判断是否是男性
fun isMale(people : Gender) = people == MALE

那么什么是密封类呢?刚接触密封类的时候,我还是比较奇怪的,首先我们来看下密封类的使用方式

1
2
3
4
5
6
7
8
kotlin复制代码// 首先定义一个密封类,人类
sealed class People {
class Male : People() // 定义男性
class Female : People() // 定义女性
}

// 使用方式,判断是否是男性
fun isMale(people : People) = people is Male

在形式上看似比较像,但是这里有一个很大的区别,枚举类里面我们使用的是 == 进行判断,密封类我们是使用is(类似Java的instanceOf),看到这里大家可能已经有一个大致的区分。枚举类定义出来的,已经是类的实例对象,而密封类定义的是类的子类。他们都用于表示限定的类层次结构,但是方式不同。因此,在使用上,我们常常拿枚举当做单例或者多例模式。而密封类常常当做一个被限定子类的集合,但是子类的实例可以有多个,就好比如我们去请求一个接口,返回的结果成功、失败、异常,但是可以有很多次成功、失败、异常。
再回到上面的例子看,枚举里的男性、女性我们通常可以设定为一个属性,而密封类的男性、女性我们设定为一个子类特征集合,他们互不冲突,两者是可以互补的,我们讲上述的例子结合一下。

1
2
3
4
5
6
7
8
9
10
kotlin复制代码// 首先定义一个密封类,
enum class Gender{
MALE,// 定义男性
FEMALE// 定义女性
}

sealed class People(gender : Gender) {
class Male : People(Gender.MALE) // 定义男性
class Female : People(Gender.FEMALE) // 定义女性
}

我们将密封类和枚举类结合,枚举类的Gender是密封类People的一个性别属性。看到这里,大家基本上就可以分清楚枚举类和密封类的区别了。

我们将上述代码转换成Java代码看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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语句)的判定的。我们来看下以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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的编译器帮助我们能够使用重载之后的运算符进行开发,提高我们的开发效率。

下面,我们来看一个运算符重载的例子

1
2
3
4
5
6
7
8
9
10
11
12
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代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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的解构。我们看下下面这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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对应的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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的时候,我们发现会报如下错误。

image.png

我们按照提示,将componentN的方法加上去如下:

image.png

我们发现,代码顺利通过。这里我们惊奇的发现,componentN前面有个关键字,是operator,没错,解构的本质就是kotlin的操作符重载,之所以把解构放在操作符重载后面讲就是这个原因。

解构的方式其实我们还可以用在lamda表达式当中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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代码可以看出来的,有些很多也是依赖编译器一起实现的,例如协程的原因。那么本次介绍的语法糖就到这里吧。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%