构造器Constructor
| 不同点 | Java | Kotlin |
|---|---|---|
| 构造器名 | 与类名相同 | 用 constructor 表示 |
| 修饰符 | 需指明公有性 | 没有 public 修饰,因为默认可见性就是公开的 |
class User {
val id: Int
val name: String
👇
constructor(id: Int, name: String) {
//👆 没有 public
this.id = id
this.name = name
}
}
constructor的2种写法
Kotlin中类的构造器有2种:
- 主构造器
主构造器是类的唯一或者首要的构造器,它用于初始化类的属性和执行初始化代码块。主构造器不能包含任何代码,它只能在类名后面或者类的花括号里面声明参数和修饰符。主构造器的参数可以在初始化代码块和属性初始化器中使用。例如:
class Person(val name: String, var age: Int) {
// 在类名后面声明主构造器
init {
// 使用初始化代码块
println("Person created: $name, $age")
}
}
- 次构造器
次构造器是类的其他或者辅助的构造器,它用于提供不同的方式来创建类的实例。次构造器可以包含任何代码,它必须在类的花括号里面使用constructor关键字声明。次构造器必须直接或间接地调用主构造器,这样才能保证类的属性和初始化代码块被正确执行。例如:
class Person(val name: String, var age: Int) {
// 在类名后面声明主构造器
constructor(name: String) : this(name, 0) {
// 在类的花括号里面声明次构造器,并调用主构造器
println("Secondary constructor")
}
}
所以constructor有两种写法:
- 在类名后面写,这种写法适用于主构造器,也就是类的唯一或者首要的构造器。
class Person constructor(name: String, age: Int) {
// 在类名后面写主构造器
}
- 在类的花括号里面写,这种写法适用于次构造器,也就是类的其他或者辅助的构造器。次构造器必须直接或间接地调用主构造器。例如:
class Person(name: String, age: Int) {
// 在类名后面写主构造器
constructor(name: String) : this(name, 0) {
// 在类的花括号里面写次构造器,并调用主构造器
}
}
注意:如果一个类没有任何注解或可见性修饰符,那么constructor关键字可以省略。例如:
class Person(name: String, age: Int) {
// 等价于 class Person constructor(name: String, age: Int)
}
init初始化代码块的写法
Kotlin 的 init 代码块和 Java 一样,都在实例化时执行,并且执行顺序都在构造器之前。
-
Java
☕️ public class User { 👇 { // 初始化代码块,先于下面的构造器执行 } public User() { } } -
Kotlin
🏝️ class User { 👇 init { // 初始化代码块,先于下面的构造器执行 } constructor() { } }
final 修饰符
Kotlin 函数参数默认是 val 类型,所以参数前不需要写 val 关键字。
-
目的:保证了函数参数不会被修改。因为Java 的参数可修改(默认没 final 修饰)会增加出错的概率。
-
Java的写法
☕️ 👇 final int final1 = 1; 👇 void method(final String final2) { 👇 final String final3 = "The parameter is " + final2; } -
Kotlin的写法
🏝️ 👇 val fina1 = 1 // 👇 参数是没有 val 的 fun method(final2: String) { 👇 val final3 = "The parameter is " + final2 }
去看国内国外的人写的 Kotlin 代码,你会发现很多人的代码里都会有一堆的
val。为什么?因为val比final写起来更简单,简化了给变量加限制的麻烦程度。在该加限制的地方加上限制,就可以减少代码出错的概率,所以从总体来看是件好事。
val自定义 getter
val 和 final 还是有一点区别的,虽然 val 修饰的变量不能二次赋值,但可以通过自定义变量的 getter 函数,让变量每次被访问时,返回动态获取的值:
val size: Int
get() { // 👈 每次获取 size 值时都会执行 items.size
return items.size
}
Kotlin的object关键字
object不是类,像class一样在 Kotlin 中属于关键字。object的意思很直接:创建一个类,并且创建一个这个类的对象。这个就是object的意思:对象。- 用
object修饰的对象中的变量和函数都是静态的。
Java 中的
Object在 Kotlin 中变成了Any,和Object作用一样:作为所有类的基类。
object Sample {
val name = "A name"
}
...
//使用这个对象,直接通过类名即可访问
Sample.name
创建单例类
- Java 中实现单例类(非线程安全):
☕️
public class A {
private static A sInstance;
public static A getInstance() {
if (sInstance == null) {
sInstance = new A();
}
return sInstance;
}
// 👇还有很多模板代码
...
}
- Kotlin 中实现单例类:
🏝️
// 👇 class 替换成了 object
object A {
val number: Int = 1
fun method() {
println("A.method()")
}
}
Kotlin 和 Java 相比的不同点有:
- 和类的定义类似,但是把
class换成了object。 - 不需要额外维护一个实例变量
sInstance。 - 不需要「保证实例只创建一次」的
getInstance()方法。
这种通过
object实现的单例是一个饿汉式的单例,并且实现了线程安全。
继承类和实现接口
object其实是把两步合并成了一步,既有Java的class关键字的功能,又实现了单例。
实例:
open class A {
open fun method() {
...
}
}
interface B {
fun interfaceMethod()
}
👇 👇 👇
object C : A(), B {
override fun method() {
...
}
override fun interfaceMethod() {
...
}
}
匿名类
Kotlin 和 Java 创建匿名类的方式很相似,只不过把 new 换成了 object:
- Java 中
new用来创建一个匿名类的对象 - Kotlin 中
object:也可以用来创建匿名类的对象
这里的 new 和 object: 修饰的都是接口或者抽象类。
-
Java:
ViewPager.SimpleOnPageChangeListener listener = new ViewPager.SimpleOnPageChangeListener() { @Override // 👈 public void onPageSelected(int position) { // override } }; -
Kotlin:
val listener = object: ViewPager.SimpleOnPageChangeListener() { override fun onPageSelected(position: Int) { // override } }
类中变量的直接引用companion object(Java中的static)
companion可以理解为伴随、伴生,表示修饰的对象和外部类绑定。- 如果想像Java一样让类中的一部分函数和变量是静态的,可以通过类直接引用:
- 可以将此变量放在类的伴生对象
companion object中:
class Sample {
...
👇
companion object {
val anotherString = "Another String"
}
}
...
//实际使用时
val str = Sample.anotherString
- 可以在类中创建一个有名的对象
object,把需要静态的变量或函数放在这个类的嵌套对象中,外部可以通过如下的方式调用该静态变量。
class A {
👇
object B {
var c: Int = 0
}
}
...
//实际使用时
A.B.c
类中嵌套对象可以用 companion 修饰。
class A {
👇
companion object B {
var c: Int = 0
}
}
- 注意事项:
伴生对象是一种特殊的嵌套对象。当使用companion修饰一个嵌套对象时,它就变成了伴生对象。如果省略了对象的名字,那么它的默认名字就是Companion。
限制:一个类中最多只可以有一个伴生对象,但可以有多个嵌套对象。就像皇帝后宫佳丽三千,但皇后只有一个。
所以只能使用companion修饰一个嵌套对象,否则会报错。
- 静态初始化
Java 中的静态变量和方法,在 Kotlin 中都放在了 companion object 中。因此 Java 中的静态初始化在 Kotlin 中自然也是放在 companion object 中的,像类的初始化代码一样,由 init 和一对大括号表示:
class Sample {
👇
companion object {
👇
init {
...
}
}
}
top-level顶层声明
- Kotlin的top-level顶层声明是指在任何类、对象、接口或其他结构之外定义的函数或属性。其实就是把属性和函数的声明不写在
class里面。
package com.hencoder.plus
// 👇 属于 package,不在 class/object 内
fun topLevelFuncion() {
}
- 这样写的属性和函数,不属于任何
class,而是直接属于package,它和静态变量、静态函数一样是全局的,但用起来更方便。在其它地方用的时候,就连类名都不用写。
import com.hencoder.plus.topLevelFunction // 👈 直接 import 函数
topLevelFunction()
- 命名相同的顶级函数
- 如果在不同文件中声明命名相同的函数,使用的时候IDE会自动加上包前缀来区分,这也印证了「顶级函数属于包」的特性。
import org.kotlinmaster.library1.method
👆
fun test() {
method()
👇
org.kotlinmaster.library2.method()
}
静态函数和属性的书写选择
在实际使用中,在 object、companion object 和 top-level 中该选择哪一个呢?简单来说按照下面这两个原则判断:
- 如果想写工具类的功能,直接创建文件,写 top-level「顶层」函数。
- 如果需要继承别的类或者实现接口,就用
object或companion object。
常量
- Kotlin 的常量必须声明在对象(包括伴生对象)或者「top-level 顶层」中,因为常量是静态的。
- Kotlin 新增了修饰常量的
const关键字。 - Kotlin 中只有基本类型和 String 类型可以声明成常量。
Kotlin 中声明常量:
class Sample {
companion object {
👇 // 👇
const val CONST_NUMBER = 1
}
}
const val CONST_SECOND_NUMBER = 2
Java 中声明常量:
public class Sample {
👇 👇
public static final int CONST_NUMBER = 1;
}
Kotlin的常量与Java中的常量的不同
Kotlin 中的常量指的是 「compile-time constant 编译时常量」,它的意思是「编译器在编译的时候就知道这个东西在每个调用处的实际值」,因此可以在编译时直接把这个值硬编码到代码里使用的地方。
而非基本和 String 类型的变量,可以通过调用对象的方法或变量改变对象内部的值,这样这个变量就不是常量了。
举例:
public class User {
int id; // 👈 可修改
String name; // 👈 可修改
public User(int id, String name) {
this.id = id;
this.name = name;
}
}
在使用的地方声明一个 static final 的 User 实例 user,它是不能二次赋值的:
static final User user = new User(123, "Zhangsan");
👆 👆
但是可以通过访问这个 user 实例的成员变量改变它的值:
user.name = "Lisi";
👆
所以 Java 中的常量可以认为是「伪常量」,因为可以通过上面这种方式改变它内部的值。而 Kotlin 的常量因为限制类型必须是基本类型,所以不存在这种问题,更符合常量的定义。
数组和集合
数组
Kotlin 中的数组是一个拥有泛型的类,创建函数也是泛型函数,和集合数据类型一样。
//创建方法1
val strs: Array<String> = arrayOf("a", "b", "c")
👆 👆
//创建方法2
val array1 = Array(5) { i -> i * i } // 使用构造器来创建Array
//这个构造器接受两个参数:数组的大小和一个函数,这个函数根据给定的索引来返回对应的元素的初始值。
Java 中的写法:
String[] strs = {"a", "b", "c"};
👆 👆
将数组泛型化有什么好处呢?对数组的操作可以像集合一样功能更强大,由于泛型化,Kotlin 可以给数组增加很多有用的工具函数:
get() / set()contains()first()find()
数组的取值和修改
Kotlin 中获取或者设置数组元素和 Java 一样,可以使用方括号加下标的方式索引:
println(strs[0])
👇 👆
strs[1] = "B"
数组不支持协变
Kotlin中,子类数组对象不能赋值给父类的数组变量。
-
Kotlin
val strs: Array<String> = arrayOf("a", "b", "c") 👆 val anys: Array<Any> = strs // compile-error: Type mismatch 👆 -
而这在 Java 中是可以的:
String[] strs = {"a", "b", "c"}; 👆 Object[] objs = strs; // success 👆
集合
Kotlin中的集合类型
Kotlin 和 Java 一样有三种集合类型:List、Set 和 Map,它们的含义分别如下:
List以固定顺序存储一组元素,元素可以重复。Set存储一组互不相等的元素,通常没有固定顺序。Map存储 键-值 对的数据集合,键互不相等,但不同的键可以对应相同的值。
List
Kotlin 中创建一个 List 特别的简单,有点像创建数组的代码。
- Kotlin 中创建一个列表:
//创建方法1
val strList = listOf("a", "b", "c")
//创建方法2
val list = List(5) { i -> i * i } //List接口有一个构造器,接受列表的大小和一个函数
//这个函数根据给定的索引来返回对应的元素的初始值。
//这个构造器返回的是一个只读的列表,而不是一个数组。
- Java中创建List:
List<String> strList = new ArrayList<>();
strList.add("a");
strList.add("b");
strList.add("c"); // 👈 添加元素繁琐
而且 Kotlin 中的 List 多了一个特性:支持 covariant(协变)。也就是说,可以把子类的 List 赋值给父类的 List 变量。
-
Kotlin:
val strs: List<String> = listOf("a", "b", "c") 👆 val anys: List<Any> = strs // success 👆 -
而这在 Java 中是会报错的:
List<String> strList = new ArrayList<>(); 👆 List<Object> objList = strList; // 👈 compile error: incompatible types 👆
对于协变的支持与否,List 和数组刚好反过来了。
数组&List使用场合
- 在一些性能需求比较苛刻的场景,并且元素类型是基本类型时,用数组
Array好一点。 - 元素不是基本类型时,相比
Array,用List更方便些。
Set
- Kotlin中创建
Set:
val strSet = setOf("a", "b", "c")
- Java 中创建
Set:
Set<String> strSet = new HashSet<>();
strSet.add("a");
strSet.add("b");
strSet.add("c");
和 List 类似,Set 同样具有 covariant(协变)特性。
Map
- Kotlin 中创建一个
Map:
val map = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 3)
和上面两种集合类型相似创建代码很简洁。mapOf 的每个参数表示一个键值对,to 表示将「键」和「值」关联,这个叫做「中缀表达式」.
- Java 中创建一个
Map:
Map<String, Integer> map = new HashMap<>();
map.put("key1", 1);
map.put("key2", 2);
map.put("key3", 3);
map.put("key4", 3);
Map的取值和修改
-
Kotlin 中的 Map 除了和 Java 一样可以使用
get()根据键获取对应的值,还可以使用方括号的方式获取:👇 val value1 = map.get("key1") 👇 val value2 = map["key2"] -
类似的,Kotlin 中也可以用方括号的方式改变
Map中键对应的值:👇 val map = mutableMapOf("key1" to 1, "key2" to 2) 👇 map.put("key1", 2) 👇 map["key1"] = 2
这里创建Map用的是
mutableMapOf()而不是mapOf(),因为只有mutableMapOf()创建的Map才可以修改。
这里用到了「操作符重载」的知识,实现了和数组一样的「Positional Access Operations」.
可变集合/不可变集合
Kotlin 中集合分为两种类型:只读的和可变的。这里的只读有两层意思:
- 集合的 size 不可变
- 集合中的元素值不可变
实例
listOf()创建不可变的List,mutableListOf()创建可变的List。setOf()创建不可变的Set,mutableSetOf()创建可变的Set。mapOf()创建不可变的Map,mutableMapOf()创建可变的Map。
可以看到,有
mutable前缀的函数创建的是可变的集合,没有mutbale前缀的创建的是不可变的集合。
不可变转换成可变
不可变的集合可以通过 toMutable*() 系函数转换成可变的集合。
val strList = listOf("a", "b", "c")
👇
strList.toMutableList()
val strSet = setOf("a", "b", "c")
👇
strSet.toMutableSet()
val map = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 3)
👇
map.toMutableMap()
这里有一点需要注意下:
toMutable*()返回的是一个新建的集合,原有的集合还是不可变的,所以只能对函数返回的集合修改。
序列
除了集合 Kotlin 还引入了一个新的容器类型 Sequence,它和 Iterable 一样用来遍历一组数据并可以对每个元素进行特定的处理。
创建序列
有如下3种方式可以创建序列。
-
- 类似
listOf(),使用一组元素创建:
sequenceOf("a", "b", "c") - 类似
-
- 使用
Iterable创建:
val list = listOf("a", "b", "c") list.asSequence()这里的
List实现了Iterable接口。asSequence函数的返回值是一个Sequence对象,而Sequence对象是Iterable的子类。 - 使用
-
使用 lambda 表达式创建:
// 👇 第一个元素
val sequence = generateSequence(0) { it + 1 }
// 👆 lambda 表达式,负责生成第二个及以后的元素,it 表示前一个元素
println(sequence.take(5).toList()) // [0,1,2,3,4]
println(sequence.take(7).toList()) // [0,1,2,3,4,5,6]
Sequence对象使用generateSequence函数创建,它接受一个初始值和一个函数(此处使用lambda表达式)作为参数。初始值是序列的第一个元素,函数是用来计算下一个元素的规则。函数的参数是上一个元素的值,返回值是下一个元素的值。当函数返回null时,序列结束。所以,这句话创建了一个从0开始,每次加1的无限序列。
可见性修饰符
Kotlin 中有四种可见性修饰符:
public:公开,可见性最大,哪里都可以引用。private:私有,可见性最小,根据声明位置不同可分为类中可见和文件中可见。protected:保护,相当于private+ 子类可见。internal:内部,仅对 module 内可见。
相比 Java 少了一个 default 「包内可见」修饰符,多了一个 internal「module 内可见」修饰符。
public
- Java 中没写可见性修饰符时,表示包内可见,只有在同一个
package内可以引用。如果要在package外引用,需要在class前加上可见性修饰符public表示公开。 - Kotlin 中如果不写可见性修饰符,就表示公开,和 Java 中
public修饰符具有相同效果。在 Kotlin 中public修饰符「可以加,但没必要」。
internal
internal 表示修饰的类、函数仅对 module 内可见,这里的 module 具体指的是一组共同编译的 kotlin 文件,常见的形式有:
- Android Studio 里的 module
- Maven project
internal使用场合
internal 在写一个 library module 时非常有用,当需要创建一个函数仅开放给 module 内部使用,不想对 library 的使用者可见,这时就应该用 internal 可见性修饰符。
protected
- Java 中
protected表示包内可见 + 子类可见。 - Kotlin 中
protected表示private+ 子类可见。
Kotlin 相比 Java protected 的可见范围收窄了,原因是 Kotlin 中不再有「包内可见」的概念了,相比 Java 的可见性着眼于 package,Kotlin 更关心的是 module。
private
- Java 中的
private表示类中可见,作为内部类时对外部类「可见」。 - Kotlin 中的
private表示类中或所在文件内可见,作为内部类时对外部类「不可见」。
private 修饰的变量「类中可见」和 「文件中可见」:
class Sample {
private val propertyInClass = 1 // 👈 仅 Sample 类中可见
}
private val propertyInFile = "A string." // 👈 范围更大,整个文件可见
private 修饰内部类的变量时,在 Java 和 Kotlin 中的区别
-
在 Java 中,外部类可以访问内部类的
private变量:public class Outter { public void method() { Inner inner = new Inner(); 👇 int result = inner.number * 2; // success } private class Inner { private int number = 0; } }
在 Kotlin 中,外部类不可以访问内部类的 private 变量:
```Kotlin
class Outter {
fun method() {
val inner = Inner()
👇
val result = inner.number * 2 // compile-error: Cannot access 'number': it is private in 'Inner'
}
class Inner {
private val number = 1
}
}
```
- Kotlin中
private可以修饰类和接口 可以修饰类和接口
- Java 中一个文件只允许一个外部类,所以
class和interface不允许设置为private,因为声明private后无法被外部使用,这样就没有意义了。 - Kotlin 允许同一个文件声明多个
class和 top-level 的函数和属性,所以 Kotlin 中允许类和接口声明为private,因为同个文件中的其它成员可以访问:
private class Sample {
val number = 1
fun method() {
println("Sample method()")
}
}
// 👇 在同一个文件中,所以可以访问
val sample = Sample()