1
class Person(val name: String, var age: Int)

Kotlin 定义的类,在默认情况下是 public 的

自定义属性 getter

1
2
3
4
5
6
class Person(val name: String, var age: Int) {
val isAdult
get() = age >= 18
// ↑
// 这就是isAdult属性的getter方法
}

所谓 getter,就是获取属性值的方法

如果 get() 方法内部的逻辑比较复杂,我们仍然可以像正常函数那样,带上花括号:

1
2
3
4
5
6
7
class Person(val name: String, var age: Int) {
val isAdult: Boolean
get() {
// do something else
return age >= 18
}
}

在这种情况下,编译器的自动类型推导就会失效了,所以我们要为 isAdult 属性增加明确的类型:Boolean。

判断一个人是否为成年人,我们只需要判断 age 这个属性即可,为什么还要引入一个新的属性 isAdult 呢?

  • 实际上,这里涉及到 Java 到 Kotlin 的一种思想转变。让我们来详细分解上面的问题:首先,从语法的角度上来说,是否为成年人,本来就是属于人身上的一种属性。我们在代码当中将其定义为属性,更符合直觉。而如果我们要给 Person 增加一个行为,比如 walk,那么这种情况下定义一个新的方法就是非常合适的。
  • 其次,从实现层面来看,我们确实定义了一个新的属性 isAdult,但是 Kotlin 编译器能够分析出,我们这个属性实际是根据 age 来做逻辑判断的。在这种情况下,Kotlin 编译器可以在 JVM 层面,将其优化为一个方法。
  • 通过以上两点,我们就成功在语法层面有了一个 isAdult 属性;但是在实现层面,isAdult 仍然还是个方法。这也就意味着,isAdult 本身不会占用内存,它的性能和我们用 Java 写的方法是一样的。而这在 Java 当中是无法实现的。

自定义属性 setter

所谓 setter,就是可以对属性赋值的方法

1
2
3
4
5
6
7
8
9
10
class Person(val name: String) {
var age: Int = 0
// 这就是age属性的setter
// ↓
set(value: Int) {
log(value)
field = value
}
// 省略
}

有的时候,我们不希望属性的 set 方法在外部访问,那么我们可以给 set 方法加上可见性修饰符

1
2
3
4
5
6
7
8
class Person(val name: String) {
var age: Int = 0
private set(value: Int) {
log(value)
field = value
}
// 省略
}

抽象类与继承

1
2
3
4
abstract class Person(val name: String) {
abstract fun walk()
// 省略
}
1
2
3
4
5
6
7
8
9
10
11
12
//                      Java 的继承
// ↓
public class MainActivity extends Activity {
@Override
void onCreate(){ ... }
}

// Kotlin 的继承
// ↓
class MainActivity : AppCompatActivity() {
override fun onCreate() { ... }
}

没有用 open 修饰的话,它是无法被继承的。

1
2
3
4
5
6
7
class Person() {
fun walk()
}

// 报错
class Boy: Person() {
}

Kotlin 的类,默认是不允许继承的,除非这个类明确被 open 关键字修饰了。另外,对于被 open 修饰的普通类,它内部的方法和属性,默认也是不允许重写的,除非它们也被 open 修饰了:

1
2
3
4
5
6
7
8
9
10
11
12
open class Person() {
val canWalk: Boolean = false
fun walk()
}

class Boy: Person() {
// 报错
override val canWalk: Boolean = true
// 报错
override fun walk() {
}
}

Java 的继承是默认开放的,Kotlin 的继承是默认封闭的。Kotlin 的这个设计非常好,这样就不会出现 Java 中“继承被滥用”的情况。

嵌套

1
2
3
4
5
6
7
8
9
10
class A {
val name: String = ""
fun foo() = 1


class B {
val a = name // 报错
val b = foo() // 报错
}
}

这种写法就对应了 Java 当中的静态内部类

1
2
3
4
5
6
7
8
9
10
11
// 等价的Java代码如下:
public class A() {
public String name = "";
public int foo() { return 1; }


public static class B {
String a = name) // 报错
int b = foo() // 报错
}
}

Kotlin 当中的普通嵌套类,它的本质是静态的。相应地,如果想在 Kotlin 当中定义一个普通的内部类,我们需要在嵌套类的前面加上 inner 关键字

1
2
3
4
5
6
7
8
9
10
class A {
val name: String = ""
fun foo() = 1
// 增加了一个关键字
// ↓
inner class B {
val a = name // 通过
val b = foo() // 通过
}
}

Kotlin 的这种设计非常巧妙。如果你熟悉 Java 开发,你会知道,Java 当中的嵌套类,如果没有 static 关键字的话,它就是一个内部类,这样的内部类是会持有外部类的引用的。可是,这样的设计在 Java 当中会非常容易出现内存泄漏!而大部分 Java 开发者之所以会犯这样的错误,往往只是因为忘记加“static”关键字了。这是一个 Java 开发者默认情况下就容易犯的错。

Kotlin 则反其道而行之,在默认情况下,嵌套类变成了静态内部类,而这种情况下的嵌套类是不会持有外部类引用的。只有当我们真正需要访问外部类成员的时候,我们才会加上 inner 关键字。这样一来,默认情况下,开发者是不会犯错的,只有手动加上 inner 关键字之后,才可能会出现内存泄漏,而当我们加上 inner 之后,其实往往也就能够意识到内存泄漏的风险了。

也就是说,Kotlin 这样的设计,就将默认犯错的风险完全抹掉了

接口和实现

Kotlin 的接口,跟 Java 最大的差异就在于,接口的方法可以有默认实现,同时,它也可以有属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Behavior {
// 接口内的可以有属性
val canWalk: Boolean

// 接口方法的默认实现
fun walk() {
if (canWalk) {
// do something
}
}
}

class Person(val name: String): Behavior {
// 重写接口的属性
override val canWalk: Boolean
get() = true
}

虽然在 Java 1.8 版本当中,接口也引入了类似的特性,但由于 Kotlin 是完全兼容 Java 1.6 版本的。因此为了实现这个特性,Kotlin 编译器在背后做了一些转换。这也就意味着,它是有一定局限性的(TODO 后面会讲)。

Kotlin 中的特殊类

数据类

用于存放数据的类

1
2
3
    // 数据类当中,最少要有一个属性

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

编译器会为数据类自动生成一些有用的方法。它们分别是:

  • equals();
  • hashCode();
  • toString();
  • componentN() 函数;
  • copy()。
1
2
3
4
5
6
7
8
9
10
11
12
val tom = Person("Tom", 18)
val jack = Person("Jack", 19)

println(tom.equals(jack)) // 输出:false
println(tom.hashCode()) // 输出:对应的hash code
println(tom.toString()) // 输出:Person(name=Tom, age=18)

val (name, age) = tom // name=Tom, age=18
println("name is $name, age is $age .")

val mike = tom.copy(name = "Mike")
println(mike) // 输出:Person(name=Mike, age=18)

“val (name, age) = tom”这行代码,其实是使用了数据类的解构声明。这种方式,可以让我们快速通过数据类来创建一连串的变量

密封类

密封类,是更强大的枚举类,需要使用 sealed 关键字

Android 开发当中,我们会经常使用密封类对数据进行封装。比如我们可以来看一个代码例子:

1
2
3
4
5
6
7
sealed class Result<out R> {
data class Success<out T>(val data: T, val message: String = "") : Result<T>()

data class Error(val exception: Exception) : Result<Nothing>()

data class Loading(val time: Long = System.currentTimeMillis()) : Result<Nothing>()
}

使用:

1
2
3
4
5
fun display(data: Result) = when(data) {
is Result.Success -> displaySuccessUI(data)
is Result.Error -> showErrorMsg(data)
is Result.Loading -> showLoading()
}

小结

Kotlin 语法在一些细节的良苦用心

  • Kotlin 的类,默认是 public 的。
  • Kotlin 的类继承语法、接口实现语法,是完全一样的。
  • Kotlin 当中的类默认是对继承封闭的,类当中的成员和方法,默认也是无法被重写的。这样的设计就很好地避免了继承被滥用。
  • Kotlin 接口可以有成员属性,还可以有默认实现。
  • Kotlin 的嵌套类默认是静态的,这种设计可以防止我们无意中出现内存泄漏问题。
  • Kotlin 独特的数据类,在语法简洁的同时,还给我们提供了丰富的功能。
  • 密封类,作为枚举和对象的结合体,帮助我们很好地设计数据模型,支持 when 表达式完备性。

关注我