kotlin基础

字符串比较

在kotlin中,用==检查两个字符串中的字符是否匹配,用===检查两个变量是否指向内存堆上同一对象。

fun main() {
    val str1 = "Jason"
    val str2 = "Jason"
    println(str1 === str2) //输出true,因为字符串常量池
    val str2 = "jason".capitalize()
    println(str1 === str2) //输出false,因为capitalize方法创建了一个新的"Jason"
    val str2 = "jason".capitalize().intern()
    println(str1 === str2) //输出true,因为intern方法将capitalize方法创建的字符串放进了字符串常量池,并返回了字符串常量池中该字符串的引用
}

解构

解构声明的后台实现就是声明component1、component2等若干个组件函数,让每个函数负责管理你想返回的一个属性数据,如果你定义一个数据类,它会自动为所有定义在主构造函数的属性添加对应的组件函数。

const val str = "aa,bb,cc"
fun main() {
    val (a,b,c) = names.split(",") //输出aa bb cc
    val (a) = names.split(",") //输出aa
    val data = names.split(",") //输出[aa,bb,cc]
}
可变参数
fun test(vararg str: String) {
    println(str.size) //输出2
}
fun main() {
    test("1", "2")
}

标准库函数

apply

apply函数可看作一个配置函数,你可以传入一个接收者,然后调用一系列函数来配置它以便使用,如果提供lambda给apply函数执行,它会返回配置好的接收者。

//T.() -> Unit是一个泛型类型的匿名函数,这样使用是因为匿名函数内部this指向一个File对象,用于隐士调用。
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
let

let函数能使某个变量作用于其lambda表达式里,让it关键字能引用它。let与apply比较,let会把接收者传给lambda,而apply什么都不传,匿名函数执行完,apply会返回当前接收者,而let会返回lambda的最后一行。

run

与apply不同的是,run函数不返回接收者,返回的是lambda结果;另外run函数也能用来执行函数引用

fun main() {
    "The people's Republic of China."
            .run(::isLong)
            .run(::showMessage)
            .run(::println)
}
fun isLong(name: String) = name.length >= 10
fun showMessage(isLong: Boolean): String {
    return if (isLong) "Name is too long." else "Please rename."
}
with

with函数是run的变体,他们的功能行为是一样的,但with的调用方式不同,调用with时需要值参作为其第一个参数传入

also

also函数和let函数功能相似,和let一样,also也是把接收者作为值参传给lambda,但有一点不同:also返回接收者对象,而let返回lambda结果。因为这个差异,also尤其适合针对同一原始对象,利用副作用做事,既然also返回的是接收者对象,你就可以基于原始接收者对象执行额外的链式调用。

taskIf

和其他标准函数有点不一样,takeIf函数需要判断lambda中提供的条件表达式,给出true或false结果,如果判断结果是true,从takeIf函数返回接收者对象,如果是false,则返回null。如果需要判断某个条件是否满足,再决定是否可以赋值变量或执行某项任务,takeIf就非常有用,概念上讲,takeIf函数类似于if语句,但它的优势是可以直接在对象实例上调用,避免了临时变量赋值的麻烦。

taskUnless

takeIf辅助函数takeUnless,只有判断你给定的条件结果是false时,takeUnless才会返回原始接收者对象。

集合

去重
fun main() {
    val result1 = listOf("aa","bb","aa").distinct()
    println(result1)
    val result2 = listOf(Student("aa"),Student("bb"),Student("aa")).distinctBy{ it.name }
    println(result2)
}
data class Student (
    val name: String = ""
)

临时变量

我们在Player类的定义头中定义一个主构造函数,使用临时变量为Player的各个属性提供初始值,在Kotlin中,为便于识别,临时变量(包括仅引用一次的参数),通常都会以下划线开头的名字命名。

class Player(
    _name: String
) {
    var name = _name
        get() = field.capitalize()
        private set(value) {
            field = value.trim()
        }
}
构造函数

优先级:

  1. 主构造函数里声明的属性
  2. 类级别的属性赋值
  3. init初始化块里的属性赋值和函数调用
  4. 次构造函数里的属性赋值和函数调用
延迟初始化
//方案1
class Player {
    lateinit var equipment: String
    fun battle() {
        if (::equipment.isInitialized) println(equipment)
    }
}
//方案2
fun main() {
    val str: String by lazy { "aa" }
    println(str.trim())
}
//通过阅读编译后代码可知,lazy方法实际上是通过LazyKt的lazy方法,重写value属性实现。另外它还可以设置LazyThreadSafetyMode参数,默认实现为SynchronizedLazyImpl。

/**Specifies how a [Lazy] instance synchronizes initialization among multiple threads.**/
public enum class LazyThreadSafetyMode {
    //Locks are used to ensure that only a single thread can initialize the [Lazy] instance.
    SYNCHRONIZED,
    //Initializer function can be called several times on concurrent access to uninitialized [Lazy] instance value, but only the first returned value will be used as the value of [Lazy] instance.
    PUBLICATION,
    //No locks are used to synchronize an access to the [Lazy] instance value; if the instance is accessed from multiple threads, its behavior is undefined. This mode should not be used unless the [Lazy] instance is guaranteed never to be initialized from more than one thread.
    NONE,
}
数据类

使用数据类的条件:

正是因为上述这些特性,你才倾向于用数据类来表示存储数据的简单对象,对于那些经常需要比较、复制或打印自身内容的类,数据类尤其适合它们。然而,一个类要成为数据类,也要符合一定条件。总结下来,主要有三个方面:

  • 数据类必须有至少带一个参数的主构造函数
  • 数据类主构造西数的参数必须是val或var
  • 数据类不能使用abstract、open、sealed和inner修饰符
class Test1(val x: Int)
data class Test2(val x: Int)

fun main() {
    println(Test1(10) == Test1(10)) //输出false
    println(Test2(10) == Test2(10)) //输出true,因为data class重写了hashCode和equals函数
}
copy函数
data class Coordinate(val x: Int, val y: Int) {
    var z = 0
    constructor(_x: Int) : this(_x, 20) {
        z = 50
    }
    override fun toString(): String {
        return "Coordinate(x=$x, y=$y, z=$z)"
    }
}
fun main() {
    val c1 = Coordinate(10) //输出Coordinate(x=10, y=20, z=50)
    val c2 = c1.copy(30) //输出Coordinate(x=30, y=20, z=0)
}

为什么c1的z值和c2的值不一样?

答:因为c1实例是通过次构造函数创建的,而c2实例是通过主构造函数创建的,编译后的Java代码如下:

public final class KotlinTestKt {
   public static final void main() {
      Coordinate c1 = new Coordinate(10);
      Coordinate c2 = Coordinate.copy$default(c1, 30, 0, 2, (Object)null);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}
//这里只保留了相关的函数
public final class Coordinate {
   @NotNull
   public final Coordinate copy(int x, int y) {
      return new Coordinate(x, y);
   }

   // $FF: synthetic method
   public static Coordinate copy$default(Coordinate var0, int var1, int var2, int var3, Object var4) {
      if ((var3 & 1) != 0) {
         var1 = var0.x;
      }

      if ((var3 & 2) != 0) {
         var2 = var0.y;
      }

      return var0.copy(var1, var2);
   }
}
运算符重载
操作符函数名作用
+plus把一个对象添加到另一个对象里
+=plusAssign一个对象添加到另一个对象里,然后将结果赋值给第一个对象
==equals如果两个对象相等,则返回true,否则返回false
>compareTo如果左边的对象大于右边的对象,则返回true,否则返回false
[]get返回集合中指定位置的元素
..rangeTo创建一个range对象
incontains如果对象包含在集合里,则返回true

示例:

data class Coordinate(val x: Int, val y: Int) {
    operator fun plus(other: Coordinate) = Coordinate(x + this.x, y + this.y)
}
fun main() {
    println(Coordinate(10, 20) + Coordinate(10, 20)) //输出Coordinate(x=20, y=40)
}
密封类

ADT(代数数据类型):可以用来表示一组子类型的闭集,枚举类就是一种简单的ADT。

对于更复杂的ADT,你可以使用Kotlin的密封类 (sealed class)来实现更复杂的定义,密封类可以用来定义一个类似于枚举类的ADT,但你可以更灵活地控制某个子类型。
密封类可以有若干个子类,要继承密封类,这些子类必须和它定义在同一个文件里。

sealed class LicenseStatus {
    object UnQualified : LicenseStatus()
    object Learning : LicenseStatus()
    class Qualified(val licenseId: String) : LicenseStatus()
}
class Driver(var status: LicenseStatus) {
    fun checkLicense(): String {
        return when (status) {
            is LicenseStatus.UnQualified -> "没资格"
            is LicenseStatus.Learning -> "在学习"
            is LicenseStatus.Qualified -> "有资格,驾驶证编号:${(this.status as LicenseStatus.Qualified).licenseId}"
        }
    }
}
fun main() {
    val status = LicenseStatus.Qualified("363636")
    val driver = Driver(status)
    println(driver.checkLicense())
}

泛型

class MagicBox<T>(private val item: T) {
    var available: Boolean = false
    //元素修改,比如说将一个男孩变成一个男人
    fun <R> fetch(modifyFun: (T) -> R): R? {
        return modifyFun.invoke(item).takeIf { available }
    }
}
data class Boy(val name: String, val age: Int)
data class Man(val name: String, val age: Int)
fun main() {
    val boyBox: MagicBox<Boy> = MagicBox(Boy("Jack", 10))
    boyBox.available = true
    val man = boyBox.fetch {
        Man(it.name, it.age.plus(20))
    }
    println(man)
}
invariant & out & in

invariant(不变):如果泛型类既将泛型类型作为函数参数,又将泛型类型作为函数的输出,那么既不用out也不用in。
out(协变):如果泛型类只将泛型类型作为函数的返回(输出),那么使用out,可以称之为生产类/接口,因为它主要是用来生产 (produce)指定的泛型对象。
in(逆变):如果泛型类只将泛型类型作为函数的入参(输入),那么使用in,可以称之为消费者类/接口,因为它主要是用来消费(consume)指定的泛型对象。

子类泛型对象可以赋值给父类泛型对象,用out。
父类泛型对象可以赋值给子类泛型对象,用in。

//out
interface Production<out T> {
    fun product(): T
}
//in
interface Consumer<in T> {
    fun consume(item: T)
}
//
open class Food
open class Burger : Food()
//生产者
class FoodStore : Production<Food> {
    override fun product() : Food {
        println("Produce Food.")
        return Food()
    }
}
//消费者
class Everybody : Consumer<Food>{
    override fun consume(item: Food) {
        println("Eat burger.")
    }
}
fun main() {
    //子类泛型对象可以赋值给父类泛型对象,用out。
    val production: Production<Food> = FoodStore()
    //父类泛型对象可以赋值给子类泛型对象,用in。
    val consumer: Consumer<Burger> = Everybody()
}
reified

有时候,你可能想知道某个泛型参数具体是什么类型,reified关键字能帮你检查泛型参数类型。Kotlin不允许对泛型参数T做类型检查,因为泛型参数类型会被类型擦除,也就是说,T的类型信息在运行时是不可知的,Java也有这样的规则。

open class Human(val age: Int)
class Boy(val name: String, age: Int) : Human(age) {
    override fun toString(): String {
        return "Boy(name='$name', age='$age')"
    }
}
class Man(val name: String, age: Int) : Human(age) {
    override fun toString(): String {
        return "Boy(name='$name', age='$age')"
    }
}
class MagicBox<T: Human>() {
    //随机产生一个对象,如果不是指定类型的对象,就通过backup函数生成一个指定类型的对象
    inline fun <reified T> randomOrBackup(backup : () -> T) : T {
        val items = listOf(
                Boy("Jack", 20),
                Man("John", 30)
        )
        val random: Human = items.shuffled().first()
        println(random)
        return if (random is T) {
            random
        } else {
            backup()
        }
    }
}
fun main() {
    val box : MagicBox<Human> = MagicBox()
    //由backup函数推断出来T的类型
    val subject = box.randomOrBackup {
        Man("Jimmy", 40)
    }
    println(subject)
}

infix

infix关键字适用于有单个参数的扩展和类函数,可以让你以更简洁的语法调用函数,如果一个函数定义使用了infix关键字,那么调用它时,接收者和函数之间的点操作以及参数的一对括号都可以不要。

重命名扩展

有时候,你想使用一个扩展或一个类,但它的名字不合你的意时,可以通过以下方式重命名:

import com.example.kotlin.extension.randomTake as randomizer
fun main() {
    val list = listOf("aa", "bb")
    list.randomizer()
}

generateSequence

针对某个序列,你可能会定义一个只要序列有新值产生就被调用一下的函数,这样的函数叫迭代器函数,要定义一个序列和它的迭代器,你可以使用Kotlin的序列构造函数generateSequence,generateSequence函数接收一个初始种子值作为序列的起步值,在用generateSequence定义的序列上调用一个函数时,generateSequence函数会调用你指定的迭代器函数,决定下一个要产生的值。

fun Int.isPrime() : Boolean {
    return (2 until this).map { this % it }.none { it == 0 }
}
fun main() {
    //找到从2开始的1000个素数
    val oneThousandPrimes = generateSequence(2) { value -> value + 1}.filter { it.isPrime() }.take(1000)
    println(onThousandPrimes.toList().size)
}

@JvmName

为Kotlin文件重命名,使用方式:@file:JvmName("新的文件名")

添加新评论