「码上开学——hencoder」Kotlin笔记(Kotlin 里那些「不是那么写的」)

341 阅读12分钟

原文地址:【码上开学】# Kotlin 里那些「不是那么写的」

我是二期课程的学员,最近正好补一下Kotlin的基础,发现rengwuxian的码上开学上的文章写的通俗易懂,我就摘抄来作为笔记。

原文写的太好了,我就整篇博客大部分摘抄下来作为记录,如果想看详细建议看原本,经过和扔物线老师的同意才发出来的。

一、Constructor

下面具体看看Kotlin的构造器和Java有什么不一样的地方:

  • Java
public class User {
    int id;
    String name;
    
    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
}
  • Kotlin
class User {
    val id: Int
    val name: String
    
    constructor(id: Int, name: String) {
        // 👆🏻没有public
        this.id = id
        this.name = name
    }
}

可以发现有两点不同:

  • Java中的构造器和类名相同,Kotlin中使用constructor表示。
  • Kotlin中构造器没有public修饰符,因为默认可见性是公开的

init

除了构造器,Java里常常配合一起使用的init代码块,在Kotlin里的写法也有一点点改变:你需要给它加一个init前缀。

  • Java
public class User {
    {
        //初始化代码块,先于下面的构造器执行
    }
    public User() {
    }
}
  • Kotlin
class User {
    init {
        // 初始化代码块,先于下面的构造器执行
    }
    constructor() {
    }
}

正如上面标注的那样,Kotlin的init代码块和Java一样,都在实例化时执行,并且执行顺序都在构造器之前。 上一篇提到,Java的类如果不加final关键字,默认是可以被继承的,而Kotlin的类默认就是final的。在Java里final还可以用来修饰变量,接下来让我们看看Kotlin是如何实现类似功能的。

final

Kotlin中的val和Java中的final类似,表只读变量,不能修改。这里分别从成员变量、参数和局部变量来和Java做对比:

  • Java
final int final1 = 1;

void method(final String final2) {
    final String final3 = "The paramter is " + final2;
}
  • Kotlin
val final1 = 1
// 参数是没有val的
funal method(final2: String) {
    val final3 = "The paramter is " + final2
}

可以看到不同点主要有:

  • final 变成了val。
  • Kotlin函数参数默认是val类型,所以参数嵌不需要写val关键字,Kotlin里这样设计的原因是保证了参数不会被修改,而Java的参数可修改(默认没final修饰)会增加出错的概率。

上一期说过,var是variable的缩写,val是value的缩写。

启始我们写Java代码的时候,很少会有人用final,但final用来修饰变量启始是很有用的,但大家都不用:可妮如果去看看国内国外的人写Kotlin代码,你会发现很多人的代码里都会有一堆的val。为什么?因为final写起来比val麻烦一点:我需要多写一个单词。虽然只麻烦这一点点,但就是导致很多人不写。

这就是一件很有意思的事:从finalval,只方便了一点点,但却让它的使用频率有了巨大的改变。这种改变是会影响到代码质量的:在该加限制的地方加上限制,就可以减少代码出错的概率。

val自定义getter

不过valfinal还是有一点点区别的,虽然val修饰的变量不能二次赋值,但可以通过自定义变量的getter函数,让变量每次被访问时,返回动态获取的值:

val size: Int
    get() {// 每次获取size值都会执行items.size
        return items.size
    } 

不过这个属于val的另外一种用法,大部分情况下val还是对应于Java中的final使用的。

static property / function

刚才说到大家都不喜欢写fianl对吧?但是一种场景,大家是最喜欢用fianl的:常量:

public static final String CONST_STRING = "A String";

在Java里面写常量,我们用的是static+final。而在Kotlin里面,除了final的写法不一样,static的写法也不一样,而且是更不一样。确切的说:在Kotlin里,静态变量和静态方法这两个概念被去除了。

那如果想在Kotlin中像Java一样通过类直接引用该怎么办呢?Kotlin的答案是 companion object:

class Sample {
    ...
    companion object {
        vaal anotherString = "Another String"
    }
}的地方vb

为啥Kotlin越改越复杂了?不着急,我们先看看object是个什么东西。

object

Kotlin里的object——首字母消息的,不是大小,Java里的Object在Kotlin里不用了。

Java中的`Object`在Kotlin中变成`Any`,和`Object`作用一样:作为所有类的基类。

object不是类,像class一样在Kotlin中属于关键字:

object Sample {
    val name = "A name"
}

它的意思很直接:创建一个类,并且创建一个这个类的对象。这个就是object的意思:对象。 在代码中如果要使用这个对象,直接通过它的类型就可以访问:

Sample.name

这不就是单例么,所以在Kotlin中创建单例不用像Java中那么复杂,只需要把class变成object就可以了。

  • 单例类 我们看一个简单的例子,分别用Java和Kotlin实现:

    • Java中实现单例类(非线程安全)
    public class A {
        private static A ssInstance;
        
        public static A getInstance() {
            if (sInstance == null) {
                sInstance = new A();
            }
            return sInstance;
        }
        
        // 👇🏻还有很多模版代码
    }
    

    可以看到Java中为了实现单例类写了大量的末班代码,稍显繁琐。

    • Kotlin中实现单例类:
    // 👇🏻class替换成了object
    object A {
        val number: Int = 1
        fun method() {
            println("A.method()")
        }
    }
    

    和Java相比的不同点有:

    • 和类的定义类似,但是把class换成了object
    • 不需要额外维护一个实例变量sInstance
    • 不需要「保证实例只创建一次」的getInstance()方法。

    相比Java的实现简单多了。 这种通过object实现的单例是一个饿汉式的单例,并且实现了线程安全。

  • 继承类和实现接口 Kotlin中不仅类可以继承别的类,可以实现接口,object也可以:

    open class A {
        override fun method() {
            ...
        }
    }
    
    interface B {
        fun interfaceMethod()
    }
    
    object C : A(), B {
        
        override fun method() {
            ...
        }
        
        override fun interfaceMethod() {
            ...
        }
    }
    

为什么object可以实现接口呢?简单来讲object其实是把两部合并成了一部,既有class关键字的功能,又实现了单例,这样就很容易理解了。

  • 匿名类 另外,Kotlin还可以创建Java中的匿名类,只是写法上有点不同:
    • Java
    ViewPager.SimpleOnPageChangeListener listener = new ViewPager.SimpleOnPageChangeListener() {
        @Override
        publiic void onPageSelected(int position) {
            // override
        }
    }
    
    • Kotlin
    val listener = object: ViewPager.SimpleOnPageChangeListener() {
        override fun onPageSelected(position: Int) {
            // override
        }
    }
    
    和Java 创建匿名类的方式很相似,只不过把new换成了object::
    • Java中new用来创建一个匿名类的对象
    • Kotlin中的object:也可以用来创建匿名类的对象 这里的newobject:修饰的都是接口或者抽象类。

companion object

object修饰的对象中的变量和函数都是静态的额,但有时候,我们只想让类中的一部分函数和变量是静态的该怎么做呢:

class A {
    object B {
        var c: Int = 0
    }
}

如上,可以在类中创建一个对象,把需要静态的变量或函数放在内部对象B中,外部可以通过如下的方法调用该静态变量:

A.B.c

类中嵌套的对象可以用companion修饰:

class A {
    companion object B {
        var c: Int = 0
    }
}

companion可以理解为伴随、伴生,表示修饰的对象和外部类绑定。

但这里有一个小限制:一个类中最多只可以有一个伴生对象,但可以有多个嵌套对象。就像皇帝后宫佳丽三千,但皇后只有一个。

这样的好处是调用的时候可以省掉对象名:

A.c // B 没了

所以,当有companion修饰时,对象的名字也可以省略掉:

class A {
                    // B 没了
   companion object {
       var c: Int = 0
   } 
}

这就是这节最开始讲到的,Java静态变量和方法的等价写法:companion object变量和函数。

  • 静态初始化 Java中的静态变量和方法,在Kotlin中都放在了companion object中。因此Java中的静态初始化在Kotlin中自然也放在companion object中的,像类的初始化代码一样,由init和一堆大括号表示:
class Sample {
    companion object {
        init {
            ...
        }
    }
}

top-level property/function声明

除了静态函数这种简单地调用方式,Kotlin还有更方便的东西:「top-level declaration顶层声明」。其实就是把属性和函数的声明不写在class里面,这个Kotlin里是允许的:

package com.hencoder.plus
//属于package,不在class/object内
 fun topLevelFunction() {
 
 }

这样写的属性和函数,不属于任何class,而是直接属于package,它和静态变量、静态函数一样是全局的,但用起来更方便:你再其他地方用的时候,就连类名都不用写:

import com.hencoder.pluss.topLevelFunction // 直接import函数

topLevelFunction()

写在顶级的函数或者变量有个好处:在Android Studio中写代码时,IDE很容易根据你写的函数前几个字母自动联想出相应的函数。这样提高了写代码的效率,而且可以减少项目中的重复代码。

  • 命名相同的顶级函数 顶级函数不写在类中可能有一个问题:如果在不同文件中声明命名相同的函数,使用的时候会不会混淆?来看一个例子:
    • org.kotlinmaster.library1包下面有一个函数method:
    package org.kotlinmaster.library1
    
    fun method() {
        println("library1 method()")
    }
    
    -在org.kotlinmaster.library2包下有一个同名函数:
    package org.kotllinmaster.library2
    
    fun method() {
        println("library2 method()")
    }
    
    在使用的时候如果同时调用这两个同名函数还会怎么样:
    import org.kotlinmaster.library1.method
    
    fun test() {
        method()
        
        org.kotlinmaster.library2.method()
    }
    
    可以看到当前两个同名顶级函数时候,IDE会自动加上包前缀来区分,这也印证了「顶级函数属于包」的特性。

对比

那在实际使用中,在objectcompanion object和top-level中该选择哪一个呢?简单来说按照下面这两个原则判断

  • 如果想写工具类的功能,直接创建文件,写top-level「顶层」函数。
  • 如果需要继承别的类或者实现接口,就用objectcompanion object

常亮

Java中,除了上面讲到的静态变量和方法会用到static,声明常量时也会用到,那Kotlin中声明常量会有什么变化呢?

  • Java中声明常量
public class Sample {
    public static final int CONST_NUMBER = 1;
}
  • Kotlin中声明常量
class Sample {
    componion object {
        const val CONST_NUMBER = 1
    }
}

const val CONST_SECOND_NUMBER = 2

发现不同点有:

  • Kotlin的常量必须声明在对象(包括伴生对象)或者「top-levell顶层」,因为常量是静态的
  • Kotlin新增了修饰常量的const关键字。 除此之外还有一个区别:
  • Kotlin中只有基本类型和String类型可以声明成常量。

原因是Kotlin中的常量指的是「compile-time constant编译时常量」,它的意思是「编译器在编译的啥时候就知道这个东西在每个调用处的实际值」,因此可以在编辑时直接把这个值硬编码到代码里使用的地方。

而非基本和String类型的变量,可以通过调用对象的方法或变量改变对象内部的值,这样这个变量就不是常量了,来看一个Java的例子,比如一个User类:

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的常量因为限制类型必须是基本类型,所以不存在这种问题,更符合常量的定义。

前面讲的val「只读变量」和静态变量都是针对单个变量来说的,接下来我们看看编程中另外一个常见的主题:宿主和集合。

数组和集合

数组

生命一个String数组:

  • Java中的写法:
String[] strs = {"a", "b", "c"};
  • Kotlin中的写法
val strs: Array<String> = arrayOf("a", "b", "c")

可以看到Kotlin中的数组是一个拥有泛型的类,创建函数也是泛型函数,和结合数据一样。

针对泛型的知识点, 我们在后面的文章会讲,这里就先按照Java泛型来理解.

将数组泛型化有什么好处呢?对数组的操作可以像集合一样功能强大, 由于泛型化,Kotlin可以给数组增加很多有用的工具函数:

  • get()/set()
  • contains()
  • first()
  • find()

这样数组的实用性就大大增加了。

  • 取值和修改 Kotlin中获取或者设置数去元素和Java一样,可以使用方括号加下标的方式索引:

    println(str[0])
    str[1] = "B"
    
  • 不支持协变 Kotlin的师叔祖编译成字节码使用的仍然是Java的数组,但在语言层面是泛型实现,这样会失去协变(convariance)特性,就是子类数组对象不能赋值给父类的数组变量:

    • Kotlin
    val str: Array<String> = arrayOf("a", "b", "c")
    
    val anys: Array<Any> = strs // compile-error: Type mismatch
    
    • 而这在Java中是可以的:
    String[] str = {"a", "b", "c"};
    Object[] oobjs = strs; // success
    

集合

Kotlin和Java一样有三种集合类型:List、Set和Map,它们的含义分别如下:

  • List以固定顺序存储一组元素,元素可以重复。
  • Set存储一组互不相等的元素,通常没有固定顺序。
  • Map存储 键-值 对的数据集合,键不互相等,但不同的键可以对应相同的值。

从Java到Kotlin,这三种集合类型的使用有哪些变化呢?我们依次看看。

  • List
    • Java中创建一个列表:
    List<String> strList = new ArrayList<>();
    strList.add("a");
    strList.add("b");
    strList.add("c"); // 添加元素繁琐
    
    • Kotlin中创建一个列表:
    val strList = listOf("a", "b", "c")
    
    首先能看到的是Kotlin中创建一个List特别简单,有点像创建数组的代码。而且Kotlin中的List多了一个特性:支持convariant(协变)。也就是说,可以把子类的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和数组刚好反过来了。
    • 和数组的区别 Kotlin中的数组和MutableList的API是非常像的,主要的区别是数组的元素个数不能变。那在什么时候用数组呢?
      • 这个问题在Java中就存在了,数组和List的功能类似,List的功能更多一些,直觉上应该是List。但数组也不是没有优势,基本类型(int[]、float[])的数组不用自动装箱,性能好一点。
      • 在Kotlin中也是同样的道理,在一些性能需求比较苛刻的场景,并且元素类型是基本类型时,用数去好一点。不过这里要注意一点,Kotlin中要用专门的基本类型数组(IntArray FloatArray LongArray)才可以免于装箱。也就是说元素不是基本类型时,相比Array,用List更方便些。
  • Set
    • Java中创建一个Set:
    Set<String> strSet = new HashSet<>();
    strSet.add("a");
    strSet.add("b");
    strSet.add("c");
    
    • Kotlin中创建相同的Set:
    val strSet = setOf("a", "b", "c")
    
    List类似,Set相同具有convariant(协变)特性。
  • Map
    • Java中创建一个Map:
    Map<String, Integet> map = new HashMap<>();
    map.put("key1", 1);
    map.put("key2", 2);
    map.put("key3", 3);
    map.put("key4", 3);
    
    • Kotlin中创建一个Map:
    val map = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 3)
    
    和上面两种结合类型相似创建代码很简洁。mapOf的每个参数表示一个键值对, to表示将「键」和「值」关联,这个叫做「中缀表达式」,这里先不展开,后面的文章会介绍。
    • 取值和修改
      • 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["key2"] = 2
      
      这里用到了「操作符重载」的知识,实现了和数组一样的「Positional Access Operations」,关于这个概念这里先不展开,后面会讲到。
    • 可变集合?不可变集合 上面修改Map值的例子中,创建函数用的是mutableMapOf()而不是mapOf(),因为只有mutableMapOf()创建的Map才可以修改。Kotlin中集合分为两种类型:只读的和可变的。这里的只读有两层意思:
      • 集合的size不可变
      • 集合中的元素值不可变 以下是三种集合类型创建不可变和可变实例的例子:
    • listOf()创建不可变的ListmutableListOf()创建可变的List.
    • setOf()创建不可变的Set,mutableSetOf()创建可变的Set.
    • mapOf()创建不可变的Map,mutableMapOf()创建可变的Map. 可以看到,有mutable前缀的函数创建的是可变的集合,没有mutable前缀的创建的是不可变的集合,不过不可变的可以通过toMutable*()系函数转换成可变的集合。
    val strList = lisstOf("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*()返回的是一个新建的集合,原有的集合还是不可变的,所以只能对函数返回的集合修改。

Sequence

除了集合Kotlin还引入了一个新的容器类型Sequence、它和Iterable一样用来遍历一组数据并可以对每个元素进行特定的处理,先来看看如何创建一个Sequence

  • 创建
    • 类似listOf(),使用一组元素创建:
    sequenceOf("a", "b", "c")
    
    • 使用Iterable创建
    val list = ListOf("a", "b", "c")
    list.asSequence()
    
    这里的List实现了Iterable接口。
    • 使用lambda表达式创建:
                                    //👇🏻第一个元素
    val sequence = generateSequence(0) { it + 1}
                                        //👆🏻lambda表达式,负责生成第二个及以后的元素,it表示前一个元素
    

这看起来和Iterable一样呀,为啥要多此一举使用Sequence呢?在下一篇文章会结合例子展开讨论。

可见性修饰符

讲完了数据集合,再看看Kotlin中的可见性修饰符,Kotlin中有四种可见性修饰符:

  • public: 公开,可见性最大,哪里都可以引用
  • private: 私有,可见性最小,根据声明位置不同可分为类中可见和文件可见。
  • protected: 保护,相当于private + 子类可见。
  • internal: 内部,仅对module内可见。

相比Java少了一个default「包内可见」修饰符,多了一个internal「module内可见」修饰符。这一节结合讲讲Kotlin这四种可见性修饰符,以及在Kotlin和Java中的不同。 先来看看public:

public

Java中没写可见性修饰符时,表示包内可见,只有在同一个packagee内可以引用:

package org.kotlinmaster.library;
// 没有可见性修饰符
class User {
}
                        // 和上面同一个package
package org.kotlinmaster.library;

public class Example {
    void method() {
        new User(); // successs
    }
}
package org.kotlinmaster;
            // 和上面不是一个package
import org.kotlinmaster.library.User;

public class OtherPackageExample {
    void methood() {
        new User(); // compile-error: 'org.kotlinmaster.library.User' is not public in 'org.kotlinmaster.library'. Cannot be accessed from outside package
    }
}

package外如果要引用,需要在class前加上可见性修饰符public表示公开。

Kotlin中如果不写可见性修饰符,就表示公开,和Java中public修饰符具有相同效果。在Kotllin中public修饰符『可以加,但没必要』。

@hide

在Android的官方sdk中,有一些方法只想对sdk内可见,不想开放给用户使用(因为这些方法不太稳定,在后续版本中很多有可能会修改或删掉)。为了实现这个特性,会在方法的注释中添加一个Javadoc方法@hide,用来限制客户端访问:

/**
* @hide
*/
public void hideMethod() {
    ...
}

但这种限制不太严格,可以通过反射访问到限制的方法。针对这个情况,Kotlin引进了一个更为严格的可见性修饰符:internal

internal

internal表示修饰的类、函数仅对module内可见,这里的mudule具体指的是一组共同编译的Kotlin文件,常见的形式有:

  • Android Studio里的module

  • Maven project

    我们常见的是Android Studio中的moodule这种情况,Maven project仅作了解就好,不用细究。 internal在写一个library module时非常有用,当需要创建一个函数仅开放给module内部使用,不想对library的使用者可见,这时就应该internal可见性修饰符。

Java的「包内可见」怎么没了?

Java的default「包内可见」在Kotlin中被弃用掉了,Kotlin中与它最接近的可见性修饰符是internal「module内可见」。为什么会弃用掉保内可见呢?我觉得有这几个原因:

  • Kotlin鼓励创建top-level函数和属性,一个源码文件可以包含多个类,使得Kotlin的源码结构更加扁平化,包结构不再像Java中那么重要。
  • 为了代码的解耦和可维护性,module越来越多、越来越小,使得internal「module内课件」已经可以满足对于代码封装过的需求。

protected

  • Java中protected表示包内可见+子类可见。
  • Kotlin中protected表示private+子类可见。

Kotlin相比Java protected的可见范围收窄了,原因是Kotlin中不再有「包内可见」的概念了,相比Java的可见性着眼于package,Kotlin更关心的是module。

peivate

  • Java中private表示类中可见,作为内部类时对外部类「可见」。
  • Kotlin中的private表示类中或所在文件内可见,作为内部类时对外部类「不可见」。

private修饰的变量「类中可见」和「文件中可见」:

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

版权声明

本文首发于:rengwuxian.com/kotlin-basi…

微信公众号:扔物线