02 Kotlin中「不 Java」的写法

218 阅读15分钟

构造器Constructor

不同点JavaKotlin
构造器名与类名相同用 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种:

  1. 主构造器

主构造器是类的唯一或者首要的构造器,它用于初始化类的属性和执行初始化代码块。主构造器不能包含任何代码,它只能在类名后面或者类的花括号里面声明参数和修饰符。主构造器的参数可以在初始化代码块和属性初始化器中使用。例如:

class Person(val name: String, var age: Int) {
  // 在类名后面声明主构造器

  init {
    // 使用初始化代码块
    println("Person created: $name, $age")
  }
}
  1. 次构造器

次构造器是类的其他或者辅助的构造器,它用于提供不同的方式来创建类的实例。次构造器可以包含任何代码,它必须在类的花括号里面使用constructor关键字声明。次构造器必须直接或间接地调用主构造器,这样才能保证类的属性和初始化代码块被正确执行。例如:

class Person(val name: String, var age: Int) {
  // 在类名后面声明主构造器

  constructor(name: String) : this(name, 0) {
    // 在类的花括号里面声明次构造器,并调用主构造器
    println("Secondary constructor")
  }
}

所以constructor有两种写法:

  1. 在类名后面写,这种写法适用于主构造器,也就是类的唯一或者首要的构造器。
class Person constructor(name: String, age: Int) {
  // 在类名后面写主构造器
}
  1. 在类的花括号里面写,这种写法适用于次构造器,也就是类的其他或者辅助的构造器。次构造器必须直接或间接地调用主构造器。例如:
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一样让类中的一部分函数和变量是静态的,可以通过类直接引用:
  1. 可以将此变量放在类的伴生对象companion object中:
class Sample {
    ...
       👇
    companion object {
        val anotherString = "Another String"
    }
}
...

//实际使用时
val str = Sample.anotherString
  1. 可以在类中创建一个有名的对象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()
}

静态函数和属性的书写选择

在实际使用中,在 objectcompanion 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() 创建不可变的 ListmutableListOf() 创建可变的 List
  • setOf() 创建不可变的 SetmutableSetOf() 创建可变的 Set
  • mapOf() 创建不可变的 MapmutableMapOf() 创建可变的 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的子类。

  1. 使用 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 可以修饰类和接口 可以修饰类和接口
  1. Java 中一个文件只允许一个外部类,所以 class 和 interface 不允许设置为 private,因为声明 private 后无法被外部使用,这样就没有意义了。
  2. Kotlin 允许同一个文件声明多个 class 和 top-level 的函数和属性,所以 Kotlin 中允许类和接口声明为 private,因为同个文件中的其它成员可以访问:
private class Sample {
    val number = 1
    fun method() {
        println("Sample method()")
    }
}
            // 👇 在同一个文件中,所以可以访问
val sample = Sample()