导读大纲
-
- [1.1 封装行为和数据: 类和属性](#1.1 封装行为和数据: 类和属性)
-
- [1.1.1 将数据与类关联并使其可被访问: 属性](#1.1.1 将数据与类关联并使其可被访问: 属性)
- [1.1.2 计算属性,而不是存储其值: 自定义访问器](#1.1.2 计算属性,而不是存储其值: 自定义访问器)
- [1.1.3 Kotlin 源代码目录和包](#1.1.3 Kotlin 源代码目录和包)
1.1 封装行为和数据: 类和属性
- 与其他面向对象编程语言一样,Kotlin 也提供类的抽象
- Kotlin 在这方面的概念您一定不会陌生,但会发现与其他面向对象编程语言相比
- Kotlin 可以用更少的代码完成许多常见任务
- <1> Person类---简单的普通 Java 对象(plain old Java object--POJO)
- 它到目前为止只包含一个属性: name
- Kotlin 在这方面的概念您一定不会陌生,但会发现与其他面向对象编程语言相比
java
public class Person { // <1>
private final String name; // <1>
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
-
在 Java 中,这需要一些相当冗长的代码
- 构造函数的主体完全是重复 的
- 因为它只是将参数赋值给具有相应名称的字段
- 按照惯例,要访问name字段 ,Person 类还应提供一个 getter 函数
- 即getName, 该函数也只是返回字段的内容
- 这种重复在 Java 中经常发生
- 而在 Kotlin 中 ,这种逻辑的表达不需要这么多模板
- 构造函数的主体完全是重复 的
-
在导读部分中,我们介绍 Java-to-Kotlin 转换器:
- 这是一种能自动将 Java 代码 替换为做相同事情的 Kotlin 代码的工具
- 让我们来看看转换器的实际操作,将 Person 类转换为 Kotlin 代码
- 直接将上述Java代码拷贝并粘贴到Kotlin源文件中
- <1> Kotlin 提供一种简洁的语法来声明类
- 尤其是只包含数据而不包含代码的类
kotlin
class Person(val name : String) // <1>
- 请注意,修饰符 public 在从 Java 转换到 Kotlin 的过程中消失
- 在 Kotlin 中,public 是默认的,因此可以省略
1.1.1 将数据与类关联并使其可被访问: 属性
-
类的概念 是将数据和处理这些数据的代码封装成一个实体
- 在 Java 中,数据存储在字段 中,这些字段通常是私有的
- 如果需要让类的客户端访问这些数据
- 则需要提供访问方法(accessor methods):一个getter和一个setter
- setter还可以包含验证传递值 、发送更改通知等附加逻辑
-
在 Java 中,字段及其访问器(accessors)的组合 通常被称为属性
- 在 Kotlin 中,属性是一等语言特性,完全取代fields和accessor方法
- 在类中声明属性的方式与声明变量的方式相同 : 使用 val 和 var 关键字
- 声明为 val 的属性是只读 的,而 var 属性是可变的,可以更改
- <1> 例如, Person 类已包含一个只读的 name 属性
- 可以用一个可变的 isStudent 属性来扩展该类
- <2> 只读属性--生成一个field和一个getter
- <3> 可写属性--field、getter 和 setter
- 基本上,当你声明一个属性时,你就声明相应的访问器(accessors)
- 对于只读属性 是一个 getter
- 对于可写属性 是一个 getter 和一个 setter
kotlin
class Person( // <1>
val name: String, // <2>
var isStudent: Boolean // <3>
)
-
默认情况 下,这些访问器的实现非常简单
- 创建一个字段来存储值,getter 返回该字段值,setter 更新其值
- 但如果需要,可以声明一个自定义访问器
- 使用不同的逻辑 来计算或更新属性值
- 上述简洁的 Person 类 声明隐藏与原始 Java 代码相同的底层实现
- 这是一个带有私有字段的类 ,在构造函数中初始化
- 并可通过相应的 getter 访问
- 这是一个带有私有字段的类 ,在构造函数中初始化
- 这意味着可以在 Java 和 Kotlin 中 以相同的方式使用这个类
- 而无需考虑它是在哪里声明 的, 使用方法看起来完全相同
-
下面是如何在 Java 代码中使用 Kotlin 的 Person 类
- 创建一个名为 Bob 且是学生的的新 Person
- 请注意, Person 在 Java 和 Kotlin 中的定义看起来是一样 的
- Kotlin 的 name 属性作为名为 getName 的 getter 方法暴露给 Java
- getter 和 setter 的命名规则有一个例外 :
- 如果属性名以 is 开头 , getter 将不添加额外的前缀
- 而在 setter 名称中,is 将被 set 代替
- 如果属性名以 is 开头 , getter 将不添加额外的前缀
- <1> 在Java中调用 isStudent() 和 setStudent() 来访问isStudent属性
kotlin
public class Demo {
public static void main(String[] args) {
Person person = new Person("Bob", true);
System.out.println(person.getName());
// Bob
System.out.println(person.isStudent()); // <1>
// true
person.setStudent(false); // <1>
// Graduation!
System.out.println(person.isStudent()); // <1>
// false
}
}
// =========================================
Bob
true
false
- 将上述Java调用转换为Kotlin代码
- <1> 调用构造函数时不使用 new 关键字
- <2> 直接访问属性,但会调用 getter
- 现在,你不再显式调用 getter ,而是直接引用属性
- 逻辑保持不变 ,但代码更加简洁
- <3> 直接赋值,但会调用 setter
- 在 Java 中,可以使用 person.setStudent(false) 来表示毕业
- 而在 Kotlin 中,可以直接对属性进行赋值:person.isStudent=false
kotlin
object Demo {
@JvmStatic
fun main(args: Array<String>) {
val person = Person("Bob", true) // <1>
println(person.name) // <2>
// Bob
println(person.isStudent) // <2>
// true
person.isStudent = false // <3>
// Graduation!
println(person.isStudent)
// false
}
}
- 您也可以对 Java 中定义的类使用 Kotlin 属性语法
- 在 Kotlin 中, Java类中的Getters 可作为 val 属性访问
- 而getter-setter对可作为 var 属性访问
- 例如,如果一个 Java 类定义名为 getName 和 setName 的方法
- 可以将其作为名为 name 的属性进行访问
- 如果该类定义 isStudent 和 setStudent 方法
- 则对应的 Kotlin 属性名称 将是 isStudent
- 在 Kotlin 中, Java类中的Getters 可作为 val 属性访问
kotlin
class Person2 {
private final String name;
private Boolean isStudent;
public Person2(String name, Boolean isStudent){
this.name = name;
this.isStudent = isStudent;
}
public String getName() {
return name;
}
public Boolean isStudent() {
return isStudent;
}
public void setStudent(Boolean isStudent) {
this.isStudent = isStudent;
}
}
==========================================
val person2 = Person2("Bob2", true)
println(person2.name)
// Bob
println(person2.isStudent)
// true
person2.isStudent = false
// Graduation!
println(person2.isStudent)
// false
- 在大多数情况下,属性会有一个相应的备选字段 用来存储属性值
- 但如果属性值需要即时计算 (例如,通过从其他属性派生 )
- 您可以使用自定义 getter 来表达
- 但如果属性值需要即时计算 (例如,通过从其他属性派生 )
1.1.2 计算属性,而不是存储其值: 自定义访问器
- 一种常见的情况 是,属性是对象中其他属性的直接结果
- 如果您有一个存储宽度和高度的Rectangle类
- 可以提供一个当宽度和高度相等时为 true 的isSquare属性
- 由于这是一个属性,因此可以"随用随查" ,在访问时进行计算
- 无需将该信息存储为一个单独的字段
- <1> 相反,您可以提供一个自定义的getter
- 每次访问该属性 时都会计算矩形的"方形度"
- <2> 不必使用带大括号的完整语法 ,即使用表达式体语法 定义getter
- 表达式体语法还允许省略属性类型 而由编译器为你推断类型
- 表达式体函数--传送门
- <3> 无论您选择哪种语法 ,调用 isSquare 保持不变
- 如果您有一个存储宽度和高度的Rectangle类
kotlin
class Rectangle(val height: Int, val width: Int){
val isSquare: Boolean // <1>
get() { // <1>
return height == width
}
}
================================== <2>
class Rectangle(val height: Int, val width: Int){
val isSquare
get() = height == width // <2>
}
==========================================
fun main() {
val rectangle = Rectangle(41, 43)
println(rectangle.isSquare) // <3>
// false
}
- 如果需要从 Java 访问该属性 ,则需要以前一样调用 isSquare 方法
- 您可能会问,是用自定义 getter 声明一个属性好
- 还是在类中定义一个函数 (在Kotlin中称为成员函数或方法)好
- 这两种方法差不多:在实现或性能上没有区别 ,它们的区别只在于可读性
- 一般来说,如果要描述一个类的特性 ,应将其声明为属性
- 如果要描述类的行为 ,则应选择成员函数
- 您可能会问,是用自定义 getter 声明一个属性好
1.1.3 Kotlin 源代码目录和包
-
随着程序越来越复杂 ,由越来越多的函数、类和其他语言结构组成
- 您不可避免 地需要开始考虑如何组织源代码
- 以便保持项目的可维护性和可浏览性
- 让我们来看看 Kotlin 项目通常是如何组织的
- 您不可避免 地需要开始考虑如何组织源代码
-
Kotlin 使用包的概念来组织类(类似于您熟悉的 Java)
- 每个 Kotlin 文件的开头都可以有一个包语句
- 文件中定义的所有声明(类、函数和属性)都将放在该包中
- <1> 下表显示一个源文件示例,其中显示包声明语句的语法
- 如果在同一软件包 中,可以直接使用其他文件中定义的声明
- 如果在不同软件包 中,则需要导入
- 这需要在包声明的下方使用"import"关键字来导入所需的包
- 每个 Kotlin 文件的开头都可以有一个包语句
kotlin
package geometry.shapes // <1>
class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() = height == width
}
fun createUnitSquare(): Rectangle {
return Rectangle(1, 1)
}
- Kotlin 并不区分导入类还是导入函数
- 它允许使用 import 关键字导入任何类型的声明
- 但是不允许直接导入包 , 试图通过包名来访问其下定义的声明
- 但是不允许直接导入包 , 试图通过包名来访问其下定义的声明
- <1> 如果您正在使用geometry.examples包编写项目,那么只需通过名称导入
- 就可以使用geometry.shapes包中的类Rectangle和函数createUnitSquare
- 它允许使用 import 关键字导入任何类型的声明
kotlin
package geometry.examples // <1>
import geometry.shapes.Rectangle // <1>
import geometry.shapes.createUnitSquare // <1>
fun main() {
println(Rectangle(3, 4).isSquare)
// false
println(createUnitSquare().isSquare)
// true
}
-
您还可以导入特定包中定义的所有声明 ,方法是在包名后写".*"
- 请注意,这种星形导入(也称为通配符导入 )使包中定义的所有内容都可见
- 不仅包括类,还包括顶层函数和属性
- 比如
使用import geometry.shapes.*
代替显式导入可以使代码正确编译
- 请注意,这种星形导入(也称为通配符导入 )使包中定义的所有内容都可见
-
在 Java 中,您需要将类放入与包结构相匹配的文件和目录结构中
- 例如,如果您有一个名为 shapes 的包 ,其中包含多个类
- 那么您需要将每个类放入一个名称匹配的单独文件 中
- 并将这些文件存储在一个名为 shapes 的目录中
- 那么您需要将每个类放入一个名称匹配的单独文件 中
- 下面的列表显示geometry包及其子包在 Java 中的组织结构
- 假设 createUnitSquare 函数位于名为 RectangleUtil 的单独文件中
- 假设 createUnitSquare 函数位于名为 RectangleUtil 的单独文件中
- 例如,如果您有一个名为 shapes 的包 ,其中包含多个类
-
在 Kotlin 中,您可以将多个类放在同一个文件中,并为该文件选择任意名称
- Kotlin也没有对源文件在磁盘上的布局 施加任何限制
- 您可以使用任何目录结构来组织文件
- 例如,您可以在 shapes.kt 文件中定义包 geometry.shapes 的所有内容
- 并将该文件放在 geometry 文件夹中
- 而无需创建单独的 shapes 文件夹
- Kotlin也没有对源文件在磁盘上的布局 施加任何限制
-
不过,在大多数情况 下,按照 Java 的目录布局
- 根据包结构将源文件组织到目录 中仍然是一种好的做法
- 在 Kotlin 与 Java 混合使用的项目 中,坚持使用这种结构尤为重要
- 因为这样做可以逐步迁移代码 ,而不会带来任何意外
- 不过,你也不应该犹豫是否要将多个类放到同一个文件 中
- 尤其是当这些类很小 的时候(在 Kotlin 中,它们通常都很小)