前语

随着金三银四的到来,这段时刻连续敞开了面试的热潮,目前Kotlin作为Android日常开发中的首要的言语根底,无疑成为Android面试中常考的一部分,为了查验自身巩固自己的言语根底掌握状况,所以笔者收拾收集了当下网上Kotlin常见的一些问题,但由于篇幅内容过长所以分了三个部分(根底篇,协程篇,Flow篇),以下是根底篇部分,笔者选取了其间比较经典的25个问题,有需求的小伙伴们能够自行拓展,然后进行查缺补漏

Q1:Kotlin 相关于 Java 有什么优势?

其实关于这个问题,依据咱们开发人员运用Kotlin的实践经验,答案天然有所差异,自己稍微总结一下就好了,一千人就有一千个哈姆雷特;

这儿笔者对这个问题最直观的感受便是Kotlin写的代码更加简练,相比Java编写相同代码花费的时刻和思考更少。以下是自己喜爱Kotlin相关于Java的几个长处:

  • 数据类:在 Java中,有必要为每个目标创立 getter 和 setter,并正确编写 hashCode(或让IDE去构建它,每次更新类时都有必要这样做)、toString 和 equals;在Kotlin数据类的帮助下,咱们不再需求处理hashcode、getter 和setter 等。
  • 扩展函数:Java中并不支撑扩展函数,Kotlin供给了扩展函数的支撑,使得代码更加明晰和简练。
  • 支撑一个通用代码库:能够提取一个通用代码库,运用 Kotlin 多渠道框架一起针对一切这些代码库。
  • 支撑 Null Safety:Kotlin内置了null safety支撑,这是一个救命稻草,尤其是在充满旧Java风格API的 Android上。
  • 容错率变高:犯错的空间更小,由于它比Java更简练、更具表现力。

Q2: 差异Kotlin和Java

这儿运用一个表格来大致归纳Kotlin和Java之间的不同,以便于咱们更好的差异

basic Kotlin Java
空安全 默许状况下,Kotlin 中的各种变量都是不行空的(也便是说,咱们不能将null分配给任何变量或目标)。假如咱们测验分配或回来空值,Kotlin 代码将无法构建。假如咱们绝对想要一个变量的空值,咱们能够这样声明它:value num: Int? = null NullPointerExceptions 是 Java 开发人员的一大烦恼。用户能够将 null 分配给任何变量,可是,当拜访具有 null 值的目标引证时,将抛出空指针反常,用户有必要管理该反常。
协程支撑 咱们能够在 Kotlin 的多个线程中履行长时刻运转的昂贵使命,但咱们也有协程支撑,它会在给定时刻停止履行,而不会在履行长时刻运转的高要求操作时堵塞线程。 每逢咱们启动长时刻运转的网络 I/0 或 CPU 密集型使命时,Java 中的相应线程就会被堵塞。Android 默许是单线程操作系统。Java 答应您在后台创立和履行很多线程,但管理它们是一项困难的操作。
数据类 假如咱们需求在 Kotlin 中有数据保存类,咱们能够在类声明中界说一个带有要害字“data”的类,编译器会处理一切事情,包括为各个字段结构结构函数、getter 和 setter 办法。 假定咱们需求一个 Java 类,它只保存数据而没有其他内容。结构函数、存储数据的变量、getter 和 setter 办法、hashcode()、函数 toString() 和 equals() 函数都需求由开发人员显式编写。
函数式编程 Kotlin 是过程式和函数式编程(咱们旨在将一切内容绑定到函数单元中的一种编程范式)言语,它具有许多有用的功用,例如 lambda 表达式、运算符重载、高阶函数和慵懒求值等。 Java 直到 Java 8 才答应函数式编程,可是在开发 Android 运用程序的时分,它现已支撑 Java 8 功用的子集。
扩展功用 Kotlin 使开发人员能够向现有类增加新功用。经过将类名作为新函数名的前缀,咱们能够构建扩展函数。 在Java中,假如咱们想增强现有类的功用,就有必要创立一个新类并承继父类。因而,Java 没有任何扩展功用。
数据类型推断 咱们不必依据它将在 Kotlin 中处理的赋值来声明每个变量的类型。假如需求,咱们能够明确指定。 在Java中声明变量时,咱们有必要显式地声明每个变量的类型。
智能转化 Kotlin 中的智能转化将运用要害字“is-checks”处理这些转化查看,它查看不行变值并进行隐式转化。 咱们有必要查看 Java 中变量的类型,并为咱们的操作适当地转化它们。
查看反常 咱们在 Kotlin 中没有查看反常。因而,开发人员无需声明或捕获反常,这既有利也有弊。 咱们现已在 Java 中查看了反常支撑,这使开发人员能够声明和捕获反常,然后发生更强健的代码和更好的过错处理。

Q3: Kotlin 有哪些可用的数据类型?

原始数据类型是Kotlin中最根本的数据类型,其他都是数组、字符串等引证类型。Kotlin包括一切数据类型作为目标。以下是Kotlin中可用的不同数据类型:

谱写Kotlin面试指南三部曲-基础篇
  • 整数数据类型

    数据类型 需求空间
    byte 8 bits
    short 16 bits
    int 32 bits
    long 64 bits
  • 浮点数据类型

    数据类型 需求空间
    float 32 bits
    double 64 bits
  • 布尔数据类型

    真或假是布尔数据类型所代表的仅有信息。在 Kotlin 中,布尔类型与Java中相同。

    数据类型 需求空间
    boolean 1 bit
  • 字符数据类型

    字符数据类型标明小写字母 (az)、大写字母 (AZ)、数字 (0-9) 和其他符号。

    数据类型 需求空间
    char 8 bits
  • 字符串数据类型

    字符串在 Kotlin 中由 String 类型标明,字符串值一般是用双引号 (“) 括起来的字符序列,这种状况下所需的空间取决于字符串中的字符数。

  • 数组数据类型

    Kotlin 中的Array 类用于标明数组。它具有 get 和 set 函数,由于运算符重载约好,这些函数也能够用作“[]”。数组所需的空间还取决于它具有的元素数量。

Q4: Kotlin 中变量是怎样声明的?Kotlin 中有哪些不同类型的变量?

Kotlin中的每个变量都有必要在运用前声明,假如测验在不声明变量的状况下运用它会导致语法过错,有权放入内存地址的数据类型由变量类型声明决定,在局部变量的状况下,能够依据初始化值确定变量的类型

Kotlin中有两种类型的变量,它们如下:

谱写Kotlin面试指南三部曲-基础篇
  • 不行变变量——不行变变量也称为只读变量。它们是运用 val 要害字声明的。一旦声明晰这些变量,咱们就无法更改它们的值。

    语法如下:

    val variableName = value
    

    由于它能够用变量的值初始化,所以不行变变量不是常量。这意味着不行变变量的值不需求在编译时知道,并且假如它在屡次调用的结构中界说,它能够在每次函数调用时选用不同的值。

  • 可变变量– 在可变变量中,能够更改动量的值,咱们运用要害字“var”来声明此类变量。

    语法如下:

    var variableName = value
    

Q5: Kotlin 中的数据类是什么?

Data类是一个简略的类,它保存数据并供给典型的功用。要将类声明为数据类,请运用 data 要害字。

用法:

data class className ( list_of_parameters)

以下函数由编译器主动为数据类派生:

  • equals() : 假如两个目标具有相同的内容,则 equals() 函数回来 true。它的操作类似于“==”,尽管关于 FloatDouble 值来说它的工作方式不同。
  • hashCode() : hashCode() 函数回来目标的哈希码值。
  • copy() : copy() 函数用于复制一个目标,只改动它的一些特征,而其他的坚持不变。
  • toString() : 此函数回来包括一切数据类参数的字符串。

一起为了确保一致性,数据类有必要要满足以下要求:

  • 主结构函数至少需求一个参数。
  • val 或 var 有必要用于一切主结构函数参数。
  • 笼统、敞开、密封或内部数据类是不行能的。
  • 数据类只能完成接口。

Q6: 解释Kotlin中空安全的概念

Kotlin旨在从代码中消除空引证。假如程序在运转时抛出 NullPointerExceptions,可能会导致运用程序毛病或系统溃散。假如Kotlin 编译器发现空引证,它会抛出 NullPointerException。

Kotlin能够差异可空运用(能够包容null的引证)和非空引证(不能够包容null的引证)。Null不能存储在String变量中(此时是不行空 引证),假如咱们测验将null分配给变量,编译器则会报错

var a: String = "interview"
a = null //error

当然,咱们希望上面的字符串也能够保存 null 值,咱们能够运用 ‘?’ 将其声明为 nullable 类型。String要害字后的运算符如下:

var a: String? = "interview"
a = null // no compilation error

Kotlin 供给了 Safe Call (?.)、Elvis (?:) 和 Not Null Assertion (!!) 运算符,它们界说了遇到 null 时需求履行的操作。这使得代码更可靠,更不容易犯错。因而,Kotlin 经过运用可为空、不行为空的类型变量和不同的运算符来解决空值问题,然后强制履行空值安全。

Q7 : 解释 Kotlin中的 Safe call、Elvis 和 Not Null Assertion运算符

  • 安全调用(Safe call)运算符(?.)

    空值比较很简略,但嵌套的 if-else 表达式的数量可能会让人精疲力竭,代码感官上也不太好。因而,在 Kotlin中,有一个安全调用运算符 ?,它经过仅在指定引证包括非空值时履行操作来简化操作,它答应咱们运用单个表达式来履行空查看和办法调用。

    谱写Kotlin面试指南三部曲-基础篇
  • Elvis 运算符 ( ?: )

    当原始变量为 null 时,Elvis 运算符用于回来非空值或默许值。换句话说,假如 elvis 运算符不为空,则回来左面的表达式,不然回来右边的表达式。只要当左边表达式为 null 时,才会对右侧进行求值。

    谱写Kotlin面试指南三部曲-基础篇
  • 非空断语运算符 (!!)

    假如值为空,则非空断语 (!!) 运算符将其更改为非空类型并引发反常,任何想要 NullPointerException的开发者都能够运用此运算符显式请求它。

    fun main() {
        var sample : String?  = null
        println(str!!.length)
    }
    
    Exception in thread "main" kotlin.KotlinNullPointerException
    

Q8: 如安在 Kotlin 中衔接两个字符串?

以下是咱们能够在 Kotlin 中衔接两个字符串的不同办法:

  • 运用字符串插值

    运用字符串插值技能衔接两个字符串。根本上,咱们在第三个字符串的初始化顶用字符串代替它们的占位符。

    val s1 = "Jacky"
    val s2 = "Tallow"
    val s3 = "$s1 $s2" // stores "Jacky Tallow"
    
  • 运用 + 或 plus() 运算符

    咱们运用“+”运算符衔接两个字符串并将它们存储在第三个变量中。

    val s1 = "Jacky"
    val s2 = "Tallow"
    val s3 = s1 + s2 // stores "JackyTallow"
    val s4 = s1.plus(s2) // stores "JackyTallow"
    
  • 运用 StringBuilder

    咱们运用 StringBuilder 目标衔接两个字符串。首要,咱们附加第一个字符串,然后附加第二个字符串。

    val s1 = "Jacky"
    val s2 = "Tallow"
    val s3 =  StringBuilder()     
    s3.append(s1).append(s2)
    val s4 = s3.toString() // stores "JackyTallow"
    

Q9: Kotlin中List和Array类型之间有什么差异?

  • 它们之间的首要差异在于Array是固定大小的内存区域,List 的内部完成会进行数组的扩容和缩容操作
  • Array是可变的(能够经过对其的任何引证进行更改),可是List没有修正办法(它是只读视图MutableList或不行变列表完成)
  • 总的来说,Array适用于固定长度的场景,List适用于动态长度的场景,假如需求频繁增加和删去元素时,能够优先考虑运用 List

Q10: 能够在 Kotlin 中交换运用IntArrayArray<Int>吗?

显然咱们要知道,Kotlin中Array是归于Integer[]引证下的,而IntArray()Int[];

这也就意味着将一组数字放入Array中,它将始终被装箱(特别是经过Integer.valueOf()调用);而放入到IntArray中,它不会发生装箱,由于现已转化成了Java原始数组; 所以得出结论,咱们不能相互运用它们

谱写Kotlin面试指南三部曲-基础篇

Q11: 怎样理解 Kotlin 中的 Companion Object?

首要咱们要知道在Java中 static 要害字用于声明类成员并在不创立目标的状况下运用它们,即经过类名简略地调用它们,而在 Kotlin 中,没有所谓的“static”要害字。所以,假如咱们要完成静态成员函数的功用,就要用到伴生目标,这也称为目标扩展。

值得留意的是,咱们有必要在目标界说前运用Companion要害字来结构伴随目标

class CompanionClass {
    companion object CompanionObjectName {
      // code
    }
}
val obj = CompanionClass.CompanionObjectName

咱们也能够去掉CompanionObject的名字,换成companion这个词,这样companion目标的默许名字便是Companion,如下:

class CompanionClass {
   companion object {
     // code
   }
}
val obj = CompanionClass.Companion

这样以来,一切必需的静态成员函数和成员变量都能够保存在创立的伴生目标中

class Sample {
   companion object Test {
       var a: Int = 1
       fun testFunction() = println("Test Kotlin")
   }
}
fun main() {
   println(Sample.a)
   Sample.testFunction()
}

Q12: 差异 Kotlin 中的 open 和 public 要害字

  • 一方面,open要害字标明为扩展敞开。运用open要害字,任何其他类都能够从该类承继,但一般默许状况下,类不能在Kotlin中被承继。

  • 另一方面,public要害字是拜访润饰符,它是 Kotlin 中的默许拜访润饰符,假如没有指定可见性润饰符,则默许运用 public,这意味着咱们的声明在程序中的任何地方都能够拜访

Q13: 解释 Kotlin 中的“when”要害字

when要害字在Kotlin顶用于代替Java中的switch运算符,当满足特定条件时,有必要运转特定代码块,在 when 表达式中,它会逐个比较一切分支,直到找到匹配项。找到第一个匹配项后,它继续履行 when 块的结论并当即履行 when 块之后的代码。与 Java 或任何其他编程言语中的switch case不同,咱们不需求在每个 case 的结尾运用break 句子。

fun main() {
   var temp = "Interview"
   when(temp) {
       "Interview" -> println("面试进行中。。。")
       "Job" -> println("面试经过了")
       "Success" -> println("工作中困难的部分被解决了")
   }
}

Q14: 你对 Kotlin 中的 backing field 有什么理解?

backing field是一个主动生成的字段,它仅仅能够被用在具有至少一个默许拜访器 (getter、setter) 、或许在自界说拜访器中经过 field 标识符润饰的特点中。backing field能够防止拜访器的自递归而导致程序溃散的 StackOverflowError 反常。

​ 并且Kotlin中的类不能有field。可是,有时在运用自界说拜访器时有必要有一个 backing field 。为此,Kotlin供给了一个主动backing field,能够运用 field 标识符来拜访。

var marks: Int = someValue
       get() = field
       set(value) {
           field = value
       }

此处字段标识符充当对 get() 和 set() 办法中特点“标记”值的引证。因而,每逢咱们调用 get() 时,咱们都会回来该字段的值。同样,每逢咱们调用 set() 时,咱们都会将“marks”特点值设置为“value”

Q15: 你对 Kotlin 中的密封类有什么了解?

Kotlin 引入了一种在 Java 中没有的新类形式。这些被称为“密封类”。望文生义,密封类遵循受约束或有界的类层次结构。

当然这个回答的确有些拗口,换一种说法,密封类其实便是具有一组子类的类,里面一切子类都承继这个密封类,当提前知道一个类型将符合其间一个子类类型时,它就会被运用。类型安全(即,编译器将在编译期间验证类型,假如将过错的类型分配给变量则抛出反常)经过密封类来确保,这约束了能够在编译时而不是运转时匹配的类型。

语法如下:

sealed class className

密封类的另一个显着方面是它们的结构函数默许是私有的。并且由于密封类主动笼统,因而无法实例化

sealed class Person {
    class Eat : Person() {
        fun eatApple() {
            println("eat Apple")
        }
        fun eatRice() {
            println("eat Rice")
        }
    }
    class Sleep : Person() {
        fun startSleep() {
            println("start sleep")
        }
        fun endSleep() {
            println("end sleep")
        }
    }
}
fun main() {
    val personEat = Person.Eat()
    personEat.eatRice()
    personEat.eatApple()
    val personSleep = Person.Sleep()
    personSleep.startSleep()
    personSleep.endSleep()
}

​ 在上面的代码中,咱们创立了一个名为“Person”的密封类,并在其间创立了两个名为“Eat”和“Sleep”的子类。在主函数中,咱们创立两个子类的实例并调用它们的子办法。

Q16: Kotlin 中的Inline内联类是什么,咱们什么时分需求它?

有时事务逻辑需求围绕某种类型创立包装器。可是,由于额外的堆分配,它引入了运转时开支。此外,假如包装类型是原始类型,功用损失会很严重,由于原始类型一般会在运转时进行很多优化。

内联类为咱们供给了一种包装类型的办法,然后增加功用并自行创立新类型。与惯例(非内联)包装器相反,它们将受益于改善的功用,这种状况是由于数据被内联到它的用法中,并且在生成的编译代码中跳过了目标实例化。

inline class Name(val s: String) {
    val length: Int
        get() = s.length
    fun greet() {
        println("Hello, $s")
    }
}    
fun main() {
    val name = Name("Kotlin")
    name.greet()
    println(name.length)
}

关于内联类的一些留意事项

  • 在主结构函数中初始化单个特点是内联类的根本要求
  • 内联类答应咱们像普通类相同界说特点和函数
  • 不答应初始化块、内部类和backing field
  • 内联类只能从接口中承继
  • 内联类也是有用的 final

Q17: 你对 Kotlin 中的 lateinit 有什么理解?你什么时分会考虑运用它?

清楚明了,lateinit标明推迟初始化。假如咱们不想在结构函数中初始化一个变量,而是想稍后对其进行初始化,当然需求确保在运用它之前进行初始化,这个时分就能够运用lateinit要害字声明该变量。它在初始化之前不会分配内存。咱们不能将lateinit用于原始类型特点,如 Int、Long 等。

lateinit var test: String
fun doSomething() {
    test = "Some value"
    println("Length of string is "+test.length)
    test = "change value"
}
```

其实在日常开发中,对一些用例会十分有用,例如

  • Android:在生命周期办法中初始化的变量;
  • 运用 Dagger 进行 DI:注入的类变量在结构函数外部独立初始化;
  • 单元测试设置:测试环境变量在 – 注释办法中初始化@Before
  • Spring Boot 注释(例如。@Autowired);

Q18: 解释下Kotlin 中的慵懒初始化 lazy

​ 有一些类的目标初始化十分耗时,导致整个类的创立过程被推迟。而by lazy慵懒初始化有助于解决这类问题。当咱们运用慵懒初始化声明一个目标时,该目标仅在运用该目标时初始化一次。假如该目标没有被运用,则该目标不会被初始化。这使得代码更高效、更快速。

例如,假定咱们有一个SlowClass类,并且需求一个名为FastClass 的不同类中的该SlowClass 的目标:

class FastClass {
   private val slowObject: SlowClass = SlowClass()
}

咱们这儿生成的是一个大目标,会导致FastClass的开发变慢或许推迟。有时可能不需SlowClass目标。因而,by lazy要害字能够在这种状况下为咱们供给帮助:

class FastClass {
   private val slowObject: SlowClass by lazy {
       println("Slow Object initialised")
       SlowClass()
   } 
   fun access() {
       println(slowObject)
   }
}
fun main() {
   val fastClass = FastClass()
   println("FastClass initialised")
   fastClass.access()
   fastClass.access()
}

在上面的代码中,咱们运用慵懒初始化by lazy在 FastClass 的类结构中实例化了一个 SlowClass 的目标。SlowClass的目标只要在上面的代码中被拜访时才会生成,也便是咱们调用FastClass目标的access()办法时,整个main()办法中都存在同一个目标

Q19: 差异lateinit和 lazy? 什么时分应该运用 lateinit 以及什么时分应该运用lazy?

仍是用一个表格来展示lateinit和by lazy的差异:

lateinit by lazy
首要意图是将初始化推迟到稍后的时刻节点 首要意图是仅在稍后运用目标时才初始化目标。此外,在整个程序中维护目标的单个副本。
能够从项目程序中任何地方初始化目标 只要初始化器 lambda可用于初始化它
在这种状况下能够进行屡次初始化 在这种状况下只能进行一次初始化。
它不是线程安全的。在多线程的环境中,是否正确初始化取决于开发者。 默许状况下启用线程安全,确保初始化程序只被调用一次。
仅适用于var 仅适用于val
增加了 isInitialized 办法以验证该值之前是否已被初始化。 不行能撤销初始化一个特点。
不答应原始类型的特点 运转运用原始类型特点

咱们在决定是运用lateinit仍是推迟初始化来进行初始化的时分,需求咱们去遵循一些准则:

  • 假如特点是可变的,之后可能会更改,那么引荐运用lateinit
  • 假如特点是在外部设置的(例如,假如您需求传入一个外部变量来设置它),请运用 lateinit。仍然有一种办法能够运用 lazy,但不是那么明显。
  • 假如只打算初始化一次并由一切人共享,并且它们更多是在内部设置的(取决于类变量),那么慵懒初始化lazy是可行的办法。咱们仍然能够在战术含义上运用 lateinit,可是运用慵懒初始化会更好地封装咱们的初始化代码。
  • by lazy { … }的初始化默许是线程安全的,并且能确保by lazy { … }代码块中的代码最多被调用一次,而lateinit var默许是不确保线程安全的,它的状况完全取决于运用者的代码。

就像有些人说的相同,lateinit是手动档,而lazy是主动档

Q20: Kotlin 中 fold 和 reduce 的根本差异是什么?什么时分运用哪个?

​ 关于这两个函数首要要知道它们都是Kotlin调集中的聚合操作函数,下面我简略分隔来说说

  • fold承受一个初始值,第一次调用将接纳该初始值和调集的第一个元素作为参数。

    listOf(1, 2, 3).fold(0) { sum, element -> sum + element }
    

    这样意味着,我传了初始值是0,那么第一次调用的初始值参数为0,同理,传了初始值是10,第一次调用的初始值参数为10;所以假如有时分需求有必要指定默许值或参数,那么fold绝对是很好的挑选。

  • reduce不选用初始值,而是从调集的第一个元素开始作为累加器

    listOf(1, 2, 3).reduce { sum, element -> sum + element }
    

​ 关于一些不需求指定特定默许值的场景,比如说能够运用reduce完成调集求和累加,还能够将调集拼接成字符串等等

总的来说,fold() 承受一个初始值并将其用作第一步的累积值,而 reduce() 的第一步则将第一个和第二个元素作为第一步的操作参数

Q21: Kotlin 中有哪些不同类型的效果域函数(规范函数)?

这的确是kotlin陈词滥调的问题了,let,also,apply,with,run这些规范函数在日常开发中运用十分频繁,简略归纳一下:

  • let:扩展函数,let 默许当时这个目标作为闭包的it参数,回来值为函数最终一行或许return。
  • apply:扩展函数,在apply函数范围内能够恣意调用该目标的恣意办法,并回来该目标。
  • also : 默许当时这个目标作为闭包的it参数,回来它被调用的目标,能够对该目标进行相关操作,可用于在调用链上生成一些辅助逻辑。
  • with:非扩展函数,回来值是最终一行,这点类似let。能够直接调用目标的办法,这点类似apply。
  • run:扩展函数,run和with很像,能够调用目标的恣意函数,回来值是最终一行。

为了更加明晰的看出它们之间的首要差异,笔者供给了一个表格供参考

函数 目标引证 回来值 是否是扩展函数
let it Lambda 表达式结果
run this Lambda 表达式结果
run Lambda 表达式结果 不是:调用无需上下文目标
with this Lambda 表达式结果 不是;把上下文当成参数
apply this 上下文目标
aslo it 上下文目标

谱写Kotlin面试指南三部曲-基础篇

当然不同的规范函数的用处场景存在重叠,一切依据项目或团队中运用的特定约好来进行挑选,防止过度嵌套不同的规范函数,当然不得不链式调用它们的时分,要格外留意当时上下文的值以及this或许it的值。

Q22: Kotlin泛型中的out和in要害字?

提起Kotlin泛型不得不提到这两个要害字:in(逆变)和 out(协变),字面意思上便是in标明这个参数/变量只能用来输入,不能读取,out就反过来,只能用来输出,不能读取。详细怎样体现呢,下面咱们来简略说说

out(协变型)

假如咱们的泛型类仅仅运用泛型类型作为函数的输出,那么就运用out

interface Production<out T> {
    fun produce():T
}

这便是典型的出产者类接口,首要用来出产通用类型的输出,咱们就记住**(出产 = 输出 = out)**

in(逆变型)

假如咱们的泛型类仅运用泛型类型作为函数的输入,那么就运用in

interface Consumer<in T> {
    fun consume(item: T)
}

这便是典型的顾客类接口,首要运用的都是泛型类型,咱们就记住 (消费 = 输入 = in)

什么时分运用in和out?

上面咱们现已知道了in和out的根本描绘,可是它们的含义是什么呢?举个经典的比如,咱们界说一个炸鸡类目标

谱写Kotlin面试指南三部曲-基础篇
open class Food
open class FastFood : Food()
class Checken: FastFood()
炸鸡出产

能够进一步扩展它们别离进行出产食物,快餐KFC,炸鸡,如下代码所示:

class FoodStore : Production<Food> {
    override fun produce(): Food {
        println ("Produce 食物")
        return Food()
    }
}
class FastFoodStore : Production<FastFood> {
    override fun produce(): FastFood {
        println("Produce 快餐")
        return FastFood()
    }
}
class InOutChecken : Production<Checken> {
    override fun produce(): Checken {
        println ("Produce 炸鸡")
        return Checken()
    }
}

接着咱们让食物出产持有者,能够将这些全部都分配给它

val production1 : Production<Food> = FoodStore()
val production2 : Production<Food> = FastFoodStore() 
val production3 : Production<Food> = InOutChecken()

能够看到无论是炸鸡仍是快餐出产,它们都归于食物出产,因而就能够得出结论

运用了 out 要害字,咱们能够将子类型的类分配给超类型的类

留意一下,反过来就会犯错,由于食物或许快餐不仅仅只要炸鸡进行出产

炸鸡顾客

依据上述的Consumer通用接口,咱们来消费下食物,快餐和炸鸡,如下代码所示:

class Everybody : Consumer<Food> {
    override fun consume(item: Food) {
        println("Eat 食物")
    }
}
class ChinesePeople : Consumer<FastFood> {
    override fun consume(item: FastFood) {
        println("Eat 快餐")
    }
}
class Cantonese : Consumer<Checken> {
    override fun consume(item: Checken) {
        println("Eat 炸鸡")
    }
}

现在咱们让顾客持有炸鸡,然后将上面的类全部分配给它

val consumer1 : Consumer<Checken> = Everybody()
val consumer2 : Consumer<Checken> = ChinesePeople() 
val consumer3 : Consumer<Checken> = Cantonese()

在这儿,炸鸡的顾客是广东人,他也是中国人的一部分,一起也归于世界上的每一个人,由此咱们能够得出结论:

运用了in要害字,咱们能够将超类型的类分配给子类型的类

假如反过来就有会犯错,食物的顾客可能是中国人或广东人,但它不仅仅只要中国人或广东人,有可能是美国人,韩国人呢…

总结一下,关于什么时分运用in/out

  • SuperType 能够分配SubType,运用 in
  • SubType 能够分配给 SuperType,运用 out
谱写Kotlin面试指南三部曲-基础篇

更多Kotlin泛型概况学习能够去看 扔物线大佬Kotlin泛型的视频

Q23: Kotlin泛型中的*和Any的差异是什么?

  • 关于Any简略来说,它是Kotlin中一切类的共同基类,相当于Java中的Object,而Any?则标明答应传入空值。

  • 关于Kotlin泛型中的*号 :

    • 首要要说回outin两个要害字,运用要害字 out 来支撑协变,等同于 Java 中的上界通配符 ? extends;运用要害字 in 来支撑逆变,等同于 Java 中的下界通配符 ? super

      var textViews: List<out TextView>
      var textViews: List<in TextView>
      

      仅仅换了一种写法,和Java的效果是相同的。out 标明,我这个变量或许参数只用来输出,不用来输入,你只能读我不能写我;in 就反过来,标明它只用来输入,不用来输出,你只能写我不能读我。

    • 在 Kotlin 中的* 号,相当于 out Any,其实便是Java中?作为泛型通配符运用,依据上述表标明*号中的变量参数只能用来输出。

    这儿笔者仅仅简略归纳了下首要的差异,概况学习能够去看 扔物线大佬Kotlin泛型的视频,十分简明易懂

Q24: 了解过Kotlin的reified要害字么?它有什么效果?

什么是refied要害字

​ 由于咱们都知道Kotlin和Java相同都存在着泛型擦除问题,而Kotlin它知道Java所带来的这个问题,所以对此Kotlin留了一个后门,便是经过inline函数确保使得泛型类的类型实参在运转时能够保存,这样的操作 Kotlin 中把它称为实化,对应需求运用 reified 要害字。而 reified意为详细化,使得(笼统的东西)变得更加详细化,它是Kotlin所增强的一种泛型的运用方式;

当然,运用reified要害字必要条件如下:

  • 有必要是 inline 内联函数,运用 inline 要害字润饰
  • 泛型类界说泛型形参时有必要运用 reified 要害字润饰
reified背面的故事

​ 已然咱们知道reified和inline函数是相辅相成的,运用inline函数的最大一个好处便是函数调用的功用优化和提高,需求留意的是reflied运用 inline 函数并不是由于功用的问题,而是别的一个好处它能使泛型函数类型实参进行实化,在运转时能拿到类型实参的信息,相当于带实化参数的函数每次调用都生成不同类型实参的字节码,动态插入到调用点。由于生成的字节码的类型实参引证了详细的类型,而不是类型参数所以不会存在擦除问题;

​ 综上所述,这也是为啥reifiied要害字有必要运用inline要害字润饰的原因。换句话说,运用reifled能够确保泛型类的类型实参能够在运转中被保存,由所以详细的参数类型,可有用防止了泛型擦除的问题。

Q25: Kotlin的SAM转化是什么?

在Kotlin中,SAM的概念其实是从Java那边追溯过来的,在Java中,咱们把单一办法的接口叫做SAM(Single Abstract Method)接口,从Java8之后经过Lambda能够大大简化关于SAM接口的调用。所以SAM就代表的是单一笼统办法,“SAM类型”是指像RunnableCallable等接口;Lambda表达式其实就能够被认为是SAM类型,能够自由转化为它们。

这儿简略举个比如来看下

fun main() {
    //计划一:匿名类目标
    buyCar(object : IBuy {
        override fun onBuy(money: Double) {
            println("buyCar:$money")
        }
    })
    //计划二:SAM结构办法
    buyCar(IBuy {
        println("BuyCar:$it")
    })
    //计划三:SAM结构办法(引荐)
    buyCar({
        println("BuyCar:$it")
    })
    //计划四:SAM结构办法(引荐)
    buyCar {
        println("BuyCar: $it")
    }
}
//买一辆一千万的车
fun buyCar(buy: IBuy) {
    buy.onBuy(10000000.0)
}
fun interface IBuy {
    fun onBuy(money: Double)
}

​ 因而,咱们借助Lambda表达式对SamType调用的优化称为SAM转化(Single Abstract Method Conversions),Kotlin对此现已兼容了Java中的SAM转化,它仅仅将Java的SamType翻译成了Lambda,因而在kotlin的同名办法实践变成了一个高阶函数。