Kotlin面试题

Kotlin相关面试题

1、Kotlin中的var和val区别

  • 1、var(来自于variable)可变引用。并且被它修饰的变量的值是可以改变,具有可读和可写权限,相当于Java中非final的变量。
  • 2、val(来自于value)不可变引用。并且被它修饰的变量的值一般情况初始化一遍后期不能再次否则会抛出编译异常,相当于Java中的final修饰的常量。
  • 3、在Kotlin开发过程中尽可能多的使用val关键字声明所有的Kotlin的变量,仅仅在一些特殊情况使用var,我们都知道在Kotlin中函数是头等公民,并且加入很多函数式编程内容,而使用不可变的引用的变量使得更加接近函数式编程的风格。
  • 4、需要注意的是val引用的本身是不可变的,但是它指向的对象可能是可变的。
  • 5、var关键字允许改变自己的值,但是它的类型却是无法改变的。

2、Kotlin中默认值参数的作用以及原理

作用: 配合@JvmOverloads可解决Java调用Kotlin函数重载的问题,@JvmOverloads注解这样就会自动生成多个重载方法供Java调用

原理: Kotlin中参数的默认值是被编译到被调用的函数中的,而不是调用的地方,所以改变了默认值后需要重新编译这个函数。

  // $FF: synthetic method
  // $FF: bridge method
  @JvmOverloads
  @NotNull
  public static String joinString$default(Collection var0, String var1, String var2, String var3, int var4, Object var5) {
     if((var4 & 1) != 0) {
        var0 = (Collection)CollectionsKt.emptyList();//默认值空集合
     }

     if((var4 & 2) != 0) {
        var1 = ",";//默认值分隔符“,”
     }

     if((var4 & 4) != 0) {
        var2 = "";//默认前缀
     }

     if((var4 & 8) != 0) {
        var3 = "";//默认后缀
     }

     return joinString(var0, var1, var2, var3);
  }

  @JvmOverloads
  @NotNull
  public static final String joinString(@NotNull Collection collection, @NotNull String separator, @NotNull String prefix) {
     return joinString$default(collection, separator, prefix, (String)null, 8, (Object)null);
  }

  @JvmOverloads
  @NotNull
  public static final String joinString(@NotNull Collection collection, @NotNull String separator) {
     return joinString$default(collection, separator, (String)null, (String)null, 12, (Object)null);
  }

3、Kotlin中顶层函数、中缀函数、解构声明的实质原理

  • 1、顶层函数

    顶层文件会反编译成一个容器类。(类名一般默认就是顶层文件名+”Kt”后缀,注意容器类名可以自定义)

    顶层函数会反编译成一个static静态函数,如代码中的formateFileSize和main函数

注意: 通过Kotlin中的@file: JvmName(“自定义生成类名”)注解就可以自动生成对应Java调用类名,注意需要放在文件顶部,在package声明的前面

  • 2、中缀函数

    使用infix关键字修饰的函数

注意:

前面所讲to, into,sameAs实际上就是函数调用,如果把infix关键字去掉,那么也就纯粹按照函数调用方式来。比如1.to(“A”), element.into(list)等,只有加了中缀调用的关键字infix后,才可以使用简单的中缀调用例如 1 to “A”, element into list等

并不是所有的函数都能写成中缀调用,中缀调用首先必须满足一个条件就是函数的参数只有一个。然后再看这个函数的参与者是不是只有两个元素,这两个元素可以是两个数,可以是两个对象,可以是集合等。

  • 3、解构声明

    解构声明实际上就是将对象中所有属性,解构成一组属性变量,而且这些变量可以单独使用,为什么可以单独使用,是因为每个属性值的获得最后都编译成通过调用与之对应的component()方法,每个component()方法对应着类中每个属性的值,然后在作用域定义各自属性局部变量,这些局部变量存储着各自对应属性的值,所以看起来变量可以单独使用,实际上使用的是局部变量

注意:

解构声明中解构对象的属性是可选的,也就是并不是要求该对象中所有属性都需要解构,也就是可选择需要解构的属性。可以使用下划线”_”省略不需要解构的属性也可以不写改属性直接忽略。注意: 虽然两者作用是一样的,但是最后生成component()方法不一样,使用下划线的会占用compoent1()方法,age直接从component2()开始生成, 直接不写name,age会从component1()方法开始生成。

fun main(args: Array<String>) {
    val student = Student("mikyou", 18, 99.0)
    val (_, age, grade) = student//下划线_ 忽略name属性
    println("I'm $age years old, I get $grade score")//解构后的3个变量可以脱离对象,直接单独使用
}

4、扩展函数的本质

扩展函数实际上就是一个对应Java中的静态函数,这个静态函数参数为接收者类型的对象,然后利用这个对象就可以访问这个类中的成员属性和方法了,并且最后返回一个这个接收者类型对象本身。这样在外部感觉和使用类的成员函数是一样的。

public final class ExtendsionTextViewKt {//这个类名就是顶层文件名+“Kt”后缀,这个知识上篇博客有详细介绍
   @NotNull
   public static final TextView isBold(@NotNull TextView $receiver) {//扩展函数isBold对应实际上是Java中的静态函数,并且传入一个接收者类型对象作为参数
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      $receiver.getPaint().setFakeBoldText(true);//设置加粗
      return $receiver;//最后返回这个接收者对象自身,以致于我们在Kotlin中完全可以使用this替代接收者对象或者直接不写。
   }
}

5、扩展函数与成员函数的区别

  • 1、扩展函数和成员函数使用方式类似,可以直接访问被扩展类的方法和属性。(原理: 传入了一个扩展类的对象,内部实际上是用实例对象去访问扩展类的方法和属性)
  • 2、扩展函数不能打破扩展类的封装性,不能像成员函数一样直接访问内部私有函数和属性。(原理: 原理很简单,扩展函数访问实际是类的对象访问,由于类的对象实例不能访问内部私有函数和属性,自然扩展函数也就不能访问内部私有函数和属性了)
  • 3、扩展函数实际上是一个静态函数是处于类的外部,而成员函数则是类的内部函数。
  • 4、父类成员函数可以被子类重写,而扩展函数则不行

6、Kotlin中lambda表达式有几种?

Kotlin中的lambda表达式共两种:

  • 1、一般普通的lambda表达式: ()->R

  • 2、带接收者对象的lambda表达式: T.() -> R,以便于在lambda表达式内部可以直接使用T的方法和属性,实际上就是通过传入这个接收者对象T, 然后在内部通过这个对象直接来访问它的方法和属性(本质上和扩展方法类似)

7、Kotlin lambda表达式的变量捕获

我们都知道一个函数的局部变量生命周期是属于这个函数的,当函数执行完毕后,局部变量也就被销毁了,但是如果这个局部变量被lambda捕获了,那么使用这个局部变量的代码将会被存储起来等待稍后指引,也就是被捕获的局部变量可以延迟生命周期的,针对lambda表达式捕获final修饰的局部变量原理是局部变量的值和使用这个值的lambda代码会被一起存储起来;而针对于捕获非final修饰的局部变量原理是非final局部变量会被一个特殊包装器类包装起来,这样就可以通过包装器类实例去修改这个非final的变量,那么这个包装器类实例引用是final的会和lambda代码一起存储

8、Kotlin和Java内部类或lambda访问局部变量的区别

  • 在Java中内部类或lambda访问外部局部变量必须是final修饰,也就意味着在内部类内部或者lambda表达式的内部是无法去修改函数局部变量的值
public class DemoActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo);
        final int count = 0;//需要使用final修饰
        findViewById(R.id.btn_click).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                System.out.println(count);//在匿名OnClickListener类内部访问count必须要是final修饰
            }
        });
    }
}
  • 而在Kotlin中在函数内部定义lambda或者内部类,既可以访问final修饰变量,可以访问非final修饰的变量,也就意味着可以在lambda的内部直接修改函数局部变量的值。
class Demo2Activity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_demo2)
        val count = 0//声明final
        btn_click.setOnClickListener {
            println(count)//访问final修饰的变量这个是和Java是保持一致的。
        }
    }
}

访问非final修饰的变量,并修改它的值

class Demo2Activity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_demo2)
        var count = 0//声明非final类型
        btn_click.setOnClickListener {
            println(count++)//直接访问和修改非final类型的变量
        }
    }
}

9、为什么Kotlin在lambda内部可以修改外部的非final的变量

实际上kotlin在语法层面做了一个桥接包装它把所谓的非final的变量用一个Ref包装类包装起来,然后外部保留着Ref包装器的引用是final的,然后lambda会和这个final包装器的引用一起存储,随后在lambda内部修改变量的值实际上是通过这个final的包装器引用去修改的。

![](/Users/mikyou/Library/Application Support/marktext/images/2020-01-02-22-17-30-image.png)

 protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131361820);
      final IntRef count = new IntRef();//IntRef特殊的包装器类的类型,final修饰的IntRef的count引用
      count.element = 0;//包装器内部的非final变量element
      ((Button)this._$_findCachedViewById(id.btn_click)).setOnClickListener((OnClickListener)(new OnClickListener() {
         public final void onClick(View it) {
            int var2 = count.element++;//直接是通过IntRef的引用直接修改内部的非final变量的值,来达到语法层面的lambda直接修改非final局部变量的值
            System.out.println(var2);
         }
      }));
   }

10、Kotlin的lambda成员引用使用场景

  • 1、成员引用最常见的使用方式就是类名+双冒号+成员(属性或函数)

    fun main(args: Array<String>) {
        val persons = listOf(Person(name = "Alice", age = 18), Person(name = "Mikyou", age = 20), Person(name = "Bob", age = 16))
        println(persons.maxBy(Person::age))//成员引用的类型和maxBy传入的lambda表达式类型一致
    }
  • 2、省略类名直接引用顶层函数

    package com.mikyou.kotlin.lambda
    
    fun salute() = print("salute")
    
    fun main(args: Array<String>) {
        run(::salute)
    }
  • 3、成员引用用于扩展函数

    
    fun Person.isChild() = age < 18
    
    fun main(args: Array<String>){
        val isChild = Person::isChild
        println(isChild)
    }

11、可变集合与只读集合的区别

在Kotlin中把所有的集合设计成两大类,一个是只读集合另一个就是可变集合,区别在于只读集合只有读取集合以及获取集合元素方法,没有删除、更新、增加元素的方法。可变集合就是既有获取也有删除、更新、增加方法。

为什么会这样设计,其中有一个很重要的原因就是泛型型变安全,有了可变与只读划分,会让泛型型变更安全。

12、Kotlin中定义函数还是属性场景

因为Kotlin中有了扩展函数和属性,那么啥时候定义函数啥时候定义属性呢

  • 函数或方法代表的是行为动作

  • 属性一般表示的是状态

13、Kotlin中变量初始化有几种?其中lateinit、by lazy、Delegates.notNull有什么区别。

与Java不同的是Kotlin声明定义一个变量,就需要对它做初始化操作,否则编译报错。而Java则不一样只需要定义,无须管你是否已经初始化了,所以这也就是为啥会出现NPE的问题。

那么Kotlin初始化变量有几种方法呢?

  • 对于val变量场景

    • 如果是基本数据类型,直接赋值初始化就可以

    • 如果是引用数据类型,可以使用by lazy ,延迟初始化。(注意: by lazy 只能用于val修饰的数据类型初始化)

  • 对于var变量场景

    • 如果是基本数据类型,直接开始赋值默认值就可以或者使用Delegates.notNull(存在属性初始化必须在属性使用之前的问题;)

    • 如果是引用数据类型,可以使用lateinit(仅仅适用于引用数据类型,但是存在属性初始化必须在属性使用之前的问题)或者使用Delegates.notNull(存在属性初始化必须在属性使用之前的问题;)

14、Kotlin中lambda表达式实质原理

Kotlin中的lambda表达式实际上最后会编译成一个class,这个类会继承Kotlin中Lambda的抽象类(Lambda),并且会去实现一个FunctionN系列的接口()这个N是根据lambda表达式传入参数的个数决定的,目前接口N的取值为 0 <= N <= 22,也就是lambda表达式中函数传入的参数最多也只能是22个),这个Lambda抽象类是实现了FunctionBase接口,该接口中有两个方法一个是getArity()获取lambda参数的元数,toString()实际上就是打印出Lambda表达式类型字符串,获取Lambda表达式类型字符串是通过Java中Reflection类反射来实现的。FunctionBase接口继承了Function,Serializable接口。

package com.mikyou.kotlin.lambda.simple;

import kotlin.jvm.internal.Lambda;

@kotlin.Metadata(mv = {1, 1, 10}, bv = {1, 0, 2}, k = 3, d1 = {"\000\n\n\000\n\002\020\b\n\002\b\004\020\000\032\0020\0012\006\020\002\032\0020\0012\006\020\003\032\0020\0012\006\020\004\032\0020\001H\n¢\006\002\b\005"}, d2 = {"<anonymous>", "", "a", "b", "c", "invoke"})
final class SumLambdaKt$main$sum$1 extends Lambda implements kotlin.jvm.functions.Function3<Integer, Integer, Integer, Integer> {
    public final int invoke(int a, int b, int c) {
        return a + b + c;
    }

    public static final SumLambdaKt$main$sum$1 INSTANCE =new SumLambdaKt$main$sum$1();

    SumLambdaKt$main$sum$1() {
        super(3);//这个super传入3,也就是前面getArity获得参数的元数和函数参数个数一致
    }
}

15、Kotlin中的@Metadata注解介绍以及生成流程

kotlin中的@Metadata注解是一个很特殊的注解,它记录了Kotlin代码中的一些信息,比如 class 的可见性,function 的返回值,参数类型,property 的 lateinit,nullable 的属性,typealias类型别名声明等。我们都知道Kotlin代码最终都要转化成Java的字节码的,然后运行JVM上。但是Kotlin代码和Java代码差别还是很大的,一些Kotlin特殊语言特性是独有的(比如lateinit, nullable, typealias),所以需要记录一些信息来标识Kotlin中的一些特殊语法信息。最终这些信息都是有kotlinc编译器生成,并以注解的形式存在于字节码文件中。

16、Kotlin中的@Metadata运行时注解吗?

@Metadata注解会一直保存在class字节码中,也就是这个注解是一个运行时的注解,在RunTime的时候会一直保留,那么可以通过反射可以拿到,并且这个@Metadata注解是Kotlin独有的,也就是Java是不会生成这样的注解存在于.class文件中,也就是从另一方面可以通过反射可以得知这个类是不是Kotlin的class.。

17、Kotlin中的@Metadata注解参数

注意: @Metadata注解中的k,mv,d1,d2..都是简写,为什么要这样做呢?说白了就是为了class文件的大小,尽可能做到精简。

参数简写名称 参数全称 参数类型 参数取值 参数含义
k kind Int 1-> class表示这个Kotlin文件是一个类或接口
2-> file表示这是一个Kotlin文件是一个.kt结尾的文件
3-> Synthetic class,表示这个kotlin文件是一个合成类
4-> Multi-file class facade
5-> Multi-file class part
表示当前metadata解编码的种类
mv metadata version IntArray - MetaData版本号
bv bytecode version IntArray - 字节码版本号
d1 data1 Array<String> - 主要记录Kotlin语法信息
d2 data2 Array<String> - 主要记录Kotlin语法信息
xs extra String String - 主要是为多文件的类(Multi-file class)预留的名称
xi extra Int Int 0 (表示一个多文件的类Multi-file class facade或者多文件类的部分Multi-file class part编译成-Xmultifile-parts-inherit)
1 (表示此类文件由Kotlin的预发行版本编译,并且对于发行版本不可见)
2 (表示这个类文件是一个编译的Kotlin脚本源文件)
pn fully qualified name of package String - 主要记录kotlin类完整的包名

18、Kotlin中lambda表达式编译的流程

首先,定义好的Kotlin Lambda表达式,通过Lambda表达式的类型,可以得到参数的个数以及参数的类型,也就是向Lambda抽象类的构造器传递arity元数,Lambda抽象类又把arity传递给FunctionBase,在编译时期会根据这个arity元数动态确定需要实现FunctionN接口,然后通过实现了相应的FunctionN接口中的invoke方法,最后lambda表达式函数体内代码逻辑将会在invoke方法体内。整个编译的流程完毕,也就在本地目录会生成一个.class字节码文件。从调用处反编译的代码就会直接调用.class字节码中已经生成的类中的INSTANCE静态实例对象,最后通过这个实例去调用invoke方法。

![](/Users/mikyou/Library/Application Support/marktext/images/2020-01-02-23-08-09-image.png)

19、Kotlin中lambda表达式(高阶函数)inline的作用

Kotlin中inline内联函数主要有两个作用:

  • 1、内联函数特别是在高阶函数lambda中使用inline,可以减少中间类生成以及中间类对象创建的开销。我们知道lambda最终会被编译成FunctionN系列类,然后调用的地方是需要创建这个FunctionN类实例对象,然后再调用这个对象的invoke方法,所以中间会生成新的类以及创建新的类对象实例。而inline函数则是把调用方法的代码直接插入到具体调用点。 无需生成额外中间类和中间类对象。

  • 2、内联函数还有一大作用就是reified实化类型参数,解决泛型类型在运行时泛型擦除带来问题。Kotlin和Java一样存在泛型擦除的问题。编译器把实现内联函数的字节码动态插入到每次的调用点。那么实化的原理正是基于这个机制,每次调用带实化类型参数的函数时,编译器都知道此次调用中作为泛型类型实参的具体类型。所以编译器只要在每次调用时生成对应不同类型实参调用的字节码插入到调用点即可。那么这样一来每次调用点地方都能拿到运行时泛型类型

21、Kotlin集合结构

22、Kotlin中的标准库函数run、with、T.let、T.also、T.apply、T.run区别已经各自使用场景。

23、使用过typealias吗?如何使用?typealias和import as 有什么区别?

目标对象(Target) 类型别名(Type Alias) Import As
Interfaces and Classes yes yes
Nullable Types yes no
Generics with Type Params yes no
Generics with Type Arguments yes no
Function Types yes no
Enum yes yes
Enum Members no yes
object yes yes
object Functions no yes
object Properties no yes
  • 类型别名(typealias)不会创建新的类型。他们只是给现有类型取了另一个名称而已.
  • typealias实质原理,大部分情况下是在编译时期采用了逐字替换的扩展方式,还原成真正的底层类型;但是不是完全是这样的,正如本文例子提到的那样。
  • typealias只能定义在顶层位置,不能被内嵌在类、接口、函数等内部
  • 使用import as对于已经使用Kotlin Android Extension 或者anko库的Android开发人员来说非常棒

24、集合和序列各自的使用场景

第一、数据集量级是足够大,建议使用序列(Sequences)

第二、对数据集进行频繁的数据操作,类似于多个操作符链式操作,建议使用序列(Sequences)

第三、对于使用first{},last{}建议使用序列(Sequences)

第四、对于访问索引元素返回传递给其他函数时使用集合

更细致:

  • 1、当不需要中间操作时,使用List
  • 2、当仅仅只有map操作时,使用sequence
  • 3、当仅仅只有filter操作时,使用List
  • 4、如果末端操作是first时,使用sequence

25、序列Sequence惰性求值原理(中间操作、末端(终端)操作)

序列操作: 基本原理是惰性求值,也就是说在进行中间操作的时候,是不会产生中间数据结果的,只有等到进行末端操作的时候才会进行求值。也就是上述例子中0~10中的每个数据元素都是先执行map操作,接着马上执行filter操作。然后下一个元素也是先执行map操作,接着马上执行filter操作。然而普通集合是所有元素都完执行map后的数据存起来,然后从存储数据集中又所有的元素执行filter操作存起来的原理。

集合普通操作: 针对每一次操作都会产生新的中间结果,也就是上述例子中的map操作完后会把原始数据集循环遍历一次得到最新的数据集存放在新的集合中,然后进行filter操作,遍历上一次map新集合中数据元素,最后得到最新的数据集又存在一个新的集合中。

//使用序列
fun main(args: Array<String>){
    (0..100)
        .asSequence()
        .map { it + 1 }
        .filter { it % 2 == 0 }
        .find { it > 3 }
}
//使用普通集合
fun main(args: Array<String>){
    (0..100)
        .map { it + 1 }
        .filter { it % 2 == 0 }
        .find { it > 3 }
}

序列内部的实现原理是采用状态设计模式,根据不同的操作符的扩展函数,实例化对应的Sequence子类对象,每个子类对象重写了Sequence接口中的iterator()抽象方法,内部实现根据传入的迭代器对象中的数据元素,加以变换、过滤、合并等操作,返回一个新的迭代器对象。这就能解释为什么序列中工作原理是逐个元素执行不同的操作,而不是像普通集合所有元素先执行A操作,再所有元素执行B操作。这是因为序列内部始终维护着一个迭代器,当一个元素被迭代的时候,就需要依次执行A,B,C各个操作后,如果此时没有末端操作,那么值将会存储在C的迭代器中,依次执行,等待原始集合中共享的数据被迭代完毕,或者不满足某些条件终止迭代,最后取出C迭代器中的数据即可。

26、Kotlin属性代理背后原理

可以简单理解为属性的setter、getter访问器内部实现是交给一个代理对象来实现,相当于使用一个代理对象来替换了原来简单属性字段读写过程,而暴露外部属性操作还是不变的,照样是属性赋值和读取,只是setter、getter内部具体实现变了。

class Student{
    var name: String by Delegate()
}

class Delegate{
    operator fun <T> getValue(thisRef: Any?, property: KProperty<*>): T{
        ...
    }
    operator fun <T> setValue(thisRef: Any?, property: KProperty<*>, value: T){
        ...
    }
}

属性代理基本流程就是代理类中的getValue()方法包含属性getter访问器的逻辑实现,setValue()方法包含了属性setter访问器的逻辑实现。当属性name执行赋值操作时,会触发属性setter访问器,然后在setter访问器内部调用delegate对象的setValue()方法;执行读取属性name操作时,会在getter访问器中调用delegate对象的getValue方法.

27、Kotlin内置的属性代理:Delegates.notNull、Delegates.observable、Delegates.vetoables

  • Delegates.observable()的基本使用

Delegates.observable()主要用于监控属性值发生变更,类似于一个观察者。当属性值被修改后会往外部抛出一个变更的回调。它需要传入两个参数,一个是initValue初始化的值,另一个就是回调lamba, 回调出property, oldValue, newValue三个参数。

  • 3、Delegates.vetoable()的基本使用

    Delegates.vetoable()代理主要用于监控属性值发生变更,类似于一个观察者,当属性值被修改后会往外部抛出一个变更的回调。它需要传入两个参数,一个是initValue初始化的值,另一个就是回调lamba, 回调出property, oldValue, newValue三个参数。与observable不同的是这个回调会返回一个Boolean值,来决定此次属性值是否执行修改。

28、何时需要泛型类型形参约束

  1. 当你在某个类型上调用特定的函数或属性(即某个类型的类独有的函数和属性)
  2. 当你希望在函数返回时保留某个特定类型
需要调用成员(类的成员函数或属性) 不需要调用成员(类的成员函数或属性)
需要保留类型 使用带有类型参数约束的泛型 使用不带类型参数约束的泛型
不需要保留类型 使用非泛型和适当的抽象 使用Java中的原生态类型

30、Kotlin reified实化类参数的原理

我们都知道内联函数的原理,编译器把实现内联函数的字节码动态插入到每次的调用点。那么实化的原理正是基于这个机制,每次调用带实化类型参数的函数时,编译器都知道此次调用中作为泛型类型实参的具体类型。所以编译器只要在每次调用时生成对应不同类型实参调用的字节码插入到调用点即可。 总之一句话很简单,就是带实化参数的函数每次调用都生成不同类型实参的字节码,动态插入到调用点。由于生成的字节码的类型实参引用了具体的类型,而不是类型参数所以不会存在擦除问题。

31、Kotlin 中泛型型变-协变、逆变、不变

  • 1、两个具有相同的基础类型的泛型协变类型,如果类型实参具有子类型化关系,那么这个泛型类型具有一致方向的子类型化关系

  • 2、两个具有相同的基础类型的泛型逆变类型,如果类型实参具有子类型化关系,那么这个泛型类型具有相反方向的子类型化关系

  • 3、不变型就是没有子类型化关系,所以它会有一个局限性就是如果以它作为函数形参类型,外部传入只能是和它相同的类型,因为它根本就不存在子类型化关系说法,那也就是没有任何类型值能够替换它,除了它自己本身的类型

协变 逆变 不变
基本结构 Producer<out E> Consumer<in T> MutableList<T>
子类型化关系 保留子类型化关系 反转子类型化关系 无子类型化关系
有无型变点 协变点out 逆变点in 无型变点
类型形参存在的位置 修饰只读属性类型和函数返回值类型 修饰可变属性类型和函数形参类型 都可以,没有约束
角色 生产者输出为泛型形参类型 消费者输入为泛型形参类型 既是生产者也是消费者
表现特征 内部操作只读 内部操作只写 内部操作可读可写

来张图理解下

33、Kotlin中的契约Contract

Kotlin中的Contract契约是一种向编译器通知函数行为的方法。

Contract契约的限制:

  • 1、我们只能在顶层函数体内使用Contract契约,即我们不能在成员和类函数上使用它们。
  • 2、Contract调用声明必须是函数体内第一条语句
  • 3、就像我上面说得那样,编译器无条件地信任契约;这意味着程序员负责编写正确合理的契约。

34、内联类inline class

  • 基本定义

inline class 为了解决包装器类带来额外性能开销问题的一种特殊类。

  • 基本结构

基本结构很简单就是在普通class前面加上inline关键字

  • 使用限制

    • 1、内联类必须含有主构造器且构造器内参数个数有且仅有一个,形参只能是只读的(val修饰)。
    • 2、内联类不能含有init block
    • 3、内联类不能含有inner class

35、inline class与typealias的区别

关于inline class和typealias有很大相同点不同点,相同点在于: 他们两者看起来貌似都引入一种新类型,并且两者都将在运行时表现为基础类型。不同点在于: typealias仅仅是给基础类型取了一个别名而已,而inline class是基础类型一个包装器类。换句话说inline class才是真正引入了一个新的类型,而typealias则没有。

36、inline class内联类的开销

  • 引用超类型时会触发自动装箱操作

    例如,假设我们有一个可以记录日志的服务接口:

    interface LogService {    
        fun log(any: Any)
    }

    由于这个log()函数可以接收一个Any类型的实参,一旦你传入一个Points的实例给这个函数,那么这个实例就会触发自动装箱操作。

    val points = Points(5)
    logService.log(points) // <--- Autoboxing happens here(此处发生自动装箱操作)
  • 自动装箱与泛型

  • 自动装箱与可空性

37、Kotlin是如何解决空指针(NPE)问题的

抓住问题的本质,Kotlin做一个很伟大的举措那就是类型的拆分,将Kotlin中所有的类型拆分成两种: 一种是非空类型,另一种则是可空类型;其中非空类型变量不允许null值的赋值操作,换句话说就是String非空类型只存在String类的实例不存在null值,所以针对String非空类型的值你可以大胆使用String类所有相关方法,不存在二义性。 当然也会存在null情况,那就可以使用可空类型,在使用可空类型的变量的时候编译器在编译时期会做针对可空类型做一定判断,如果存在可空类型的变量操作该对应类的方法,就提示你需要做额外判空处理,这时候开发者就根据提示去做判空处理了,想象下都这样处理了,你的Kotlin代码还会出现空指针吗?(但是有一点很重要就是定义了一个变量你需要明确它是可空还是非空,如果定义了可空类型你就需要对它负责,并且编译器也会提示帮助你对它做额外判空处理。)

38、Kotlin中的Compaion Object的作用

  • 它们是真正的Kotlin对象,包括名称和类型,以及一些额外的功能。
  • 他们甚至可以不用于仅仅为了提供静态成员或方法场景。可以有更多其他选择,比如他们可以用作单例对象或替代顶层函数的功能。

39、by lazy工作原理

首先,by属性代理就是在定义的目标属性的setter,getter访问器被代理对象给代理了,当目标属性执行赋值或获取值时触发相应的setter,getter。setter,getter内部委托给代理对象去执行。实际上有点相当于hack属性访问器。如果目标属性定义成val那么只会代理getter,如果定义成var既会代理getter也会代理setter. by lazy实际是调用一个lazy函数,是以lambda为参数的函数。lazy函数有两个参数,一个是mode延迟初始化的线程模式(有三种: SYNCHRONIZED(默认)PUBLICATIONNONE),另一个参数是传入的lazy函数的lambda语句块。

mode延迟初始化的线程模式对应三个不同实现类:

SYNCHRONIZEDSynchronizedLazyImpl

  • 初始化操作仅仅在首先调用的第一个线程上执行
  • 然后,其他线程将引用缓存后的值。
  • 默认模式就是(LazyThreadSafetyMode.SYNCHRONIZED)

PUBLICATIONSafePublicationLazyImpl

  • 它可以同时在多个线程中调用,并且可以在全部或部分线程上同时进行初始化。
  • 但是,如果某个值已由另一个线程初始化,则将返回该值而不执行初始化。

NONEUnsafeLazyImpl

  • 只需在第一次访问时初始化它,或返回存储的值。
  • 不考虑多线程,所以它不是线程安全的。

Lazy实现的默认行为SYNCHRONIZED

  • 使用synchronized()同步块执行初始化块
  • 由于初始化可能已经由另一个线程完成,它会进行双重锁检测(DCL),如果已经完成了初始化,则返回存储的值。
  • 如果它未初始化,它将执行lambda表达式并存储返回值。那么随后这个initializer将会置为null,因为初始化完成后就不再需要它了。

40、Kotlin中性能优化之高阶函数与lambda表达式

Kotlin支持将函数赋值给一个变量并把它们作为函数参数传递给其他函数。接受其他函数作为参数的函数称为高阶函数

  • 捕获lambda带来Function实例对象创建

    • 对于捕获表达式情况,每次将lambda作为参数传递,然后执行后进行垃圾回收,就会每次创建一个新的Function实例;
    • 对于非捕获表达式(也即是纯函数)情况,将在下次调用期间创建并复用单例函数实例。

    优化建议: 如果要调用捕获lambda表达式来减少对象创建,请尽量使用inline高阶函数而不是标准高阶函数。

  • 装箱带来的性能开销

    当函数涉及输入值或返回值是基本类型(如Int或Long)时,调用在高阶函数中作为参数传递的函数实际上将涉及系统的装箱和拆箱。这可能会对性能上产生不可忽视的影响,特别是在Android上。

    fun transaction(db: Database, body: (Database) -> Int): Int {
        db.beginTransaction()
        try {
            val result = body(db)
            db.setTransactionSuccessful()
            return result
        } finally {
            db.endTransaction()
        }
    }
    
    //decompiled
    class MyClass$myMethod$1 implements Function1 {
       // $FF: synthetic method
       // $FF: bridge method
       public Object invoke(Object var1) {
          return Integer.valueOf(this.invoke((Database)var1));//被装箱成Integer对象
       }
    
       public final int invoke(@NotNull Database it) {
          Intrinsics.checkParameterIsNotNull(it, "it");
          return it.delete("Customers", null, null);
       }
    }

    优化建议:

    在编写涉及使用基本类型作为输入或输出值的参数函数的标准(非内联)高阶函数时要小心。反复调用此参数函数将通过装箱和拆箱操作对垃圾收集器施加更大的压力*。使用inline内联

  • 内联函数的优化

    • 声明lambda表达式时,不会实例化Function对象
    • 没有装箱或拆箱操作将应用于基于原始类型的lambda输入和输出值
    • 没有方法将添加到总方法计数中
    • 不会执行实际的函数调用,这可以提高对此使用该函数带来的CPU占用性能
  • 内联函数使用注意项

    • 内联函数不能直接调用自身或通过其他内联函数调用自身
    • 在类中声明公有的内联函数只能访问该类的公有函数和成员变量
    • 代码的大小会增加。内联多次引用代码较长的函数可以使生成的代码更大,如果这个代码较长的函数本身引用其他代码较长的内联函数,则会更多。

41、Kotlin中性能优化之伴生对象Compaion

  • 从其伴生对象访问私有类字段

    class MyClass private constructor() {
    
        private var hello = 0
    
        companion object {
            fun newInstance() = MyClass()
        }
    }
    
    //
    ALOAD 1
    INVOKESTATIC be/myapplication/MyClass.access$getHello$p (Lbe/myapplication/MyClass;)I
    ISTORE 2

    编译时,伴生对象会被实现为单例类。这意味着就像任何需要从其他类访问其私有字段的Java类一样,从伴随对象访问外部类的私有字段(或构造函数)将生成其他合成getter和setter方法。对类字段的每次读取或写入访问都将导致伴随对象中的静态方法调用。

    在Java中,我们可以通过使用package可见性来避免生成这些方法。然后在Kotlin中的没有package可见性。使用publicinternal可见性将会导致Kotlin生成默认的getter和setter实例方法,以致于外部可以访问到这些字段,然而调用实例方法在技术上往往比调用静态方法更为昂贵。

    优化建议:

    如果需要从伴生对象重复读取或写入类字段,则可以将其值缓存在局部变量中,以避免重复的隐藏方法调用.

  • 访问伴生对象中声明的常量

    class MyClass {
        companion object {
            private val TAG = "TAG"
        }
    
        fun helloWorld() {
            println(TAG)//类中访问伴生对象的常量
        }
    }

    出于同样的原因,访问伴生对象中声明的私有常量实际上会在伴生对象实现类中生成一个额外的合成getter方法

    GETSTATIC be/myapplication/MyClass.Companion : Lbe/myapplication/MyClass$Companion;
    INVOKESTATIC be/myapplication/MyClass$Companion.access$getTAG$p (Lbe/myapplication/MyClass$Companion;)Ljava/lang/String;
    ASTORE 1

    但是更糟糕的是,合成方法实际上不会返回值,它调用的是一个Kotlin生成的getter实例方法.如下所示

    ALOAD 0
    INVOKESPECIAL be/myapplication/MyClass$Companion.getTAG ()Ljava/lang/String;
    ARETURN

优化建议:

从伴随对象中读取“静态”常量,与Java相比,在Kotlin中增加了两到三个额外的间接级别,并且将为这些常量中的每一个生成两到三个额外的方法。

1、始终使用const关键字声明基本类型和字符串常量以避免这种情况

2、对于其他类型的常量,不能使用const,因此如果需要重复访问常量,可能需要将值缓存在局部变量中

3、此外,更推荐将公有的全局常量存储在它们自己的对象中而不是伴随对象中。

42、Kotlin中性能优化之局部函数

就是像正常定义普通函数的语法一样,在其他函数体内部声明该函数。这些被称为局部函数,它们能访问到外部函数的作用域。

fun someMath(a: Int): Int {
    fun sumSquare(b: Int) = (a + b) * (a + b)

    return sumSquare(1) + sumSquare(2)
}

局部函数最大的局限性: 局部函数不能被声明成内联的(inline)并且函数体内含有局部函数的函数也不能被声明成内联的(inline). 在这种情况下没有任何有效的方法可以帮助你避免函数调用的开销。

经过编译后,这些局部函数会将被转化成Function对象, 就类似lambda表达式一样,并且同样具有上篇文章part1中讲到的关于非内联函数存在很多的限制。反编译后的java代码:

public static final int someMath(final int a) {
   Function1 sumSquare$ = new Function1(1) {
      // $FF: synthetic method
      // $FF: bridge method
      //注: 这是Function1接口生成的泛型合成方法invoke
      public Object invoke(Object var1) {
         return Integer.valueOf(this.invoke(((Number)var1).intValue()));
      }

      //注: 实例的特定方法invoke
      public final int invoke(int b) {
         return (a + b) * (a + b);
      }
   };
   return sumSquare$.invoke(1) + sumSquare$.invoke(2);
}

在每次方法被调用期间仍会创建一个新的Function对象。但是这个可以通过将局部函数改写为非捕获的方式来避免这种情况:

优化建议:

局部函数是私有函数的替代品,其附加好处是能够访问外部函数的局部变量。然而这种好处会伴随着为外部函数每次调用创建Function对象的隐性成本,因此首选使用非捕获的局部函数

  • 尽可能使用非null的原生类型,以此来提高代码可读性和性能。

43、Kotlin中性能优化之数组使用

在Kotlin中存在3种类型的数组:

  • IntArray,FloatArray以及其他原生类型的数组。
    最终会编译成 int[],float[]以及其他对应基本数据类型的数组

  • Array<T>: 非空对象引用类型的数组
    这里会涉及到原生类型的装箱过程

  • Array<T?>: 可空对象引用类型的数组
    很明显,这里也会涉及到原生类型的装箱过程

如果你需要一个非null原生类型的数组,最好使用IntArray而不是Array<Int>以避免装箱过程带来性能开销

44、Kotlin中性能优化之可变数量的参数(Varargs)

类似Java, Kotlin允许使用可变数量的参数声明函数。只是声明的语法有点不一样而已:

fun printDouble(vararg values: Int) {
    values.forEach { println(it * 2) }
}

//传递单个数组
val values = intArrayOf(1, 2, 3)
printDouble(*values)
//会编译成如下形式
int[] values = new int[]{1, 2, 3};
printDouble(Arrays.copyOf(values, values.length));
//传递数组和参数的混合
val values = intArrayOf(1, 2, 3)
printDouble(0, *values, 42)
//会编译如下形式
int[] values = new int[]{1, 2, 3};
IntSpreadBuilder var10000 = new IntSpreadBuilder(3);
var10000.add(0);
var10000.addSpread(values);
var10000.add(42);
printDouble(var10000.toArray());

优化建议:

在使用现有数组中的值时,在Kotlin中调用具有可变数量参数的函数也会增加创建新临时数组的成本。对于重复调用该函数的性能至关重要的代码,请考虑添加具有实际数组参数而不是vararg的方法

45、Kotlin中性能优化之谨慎使用代理属性

在类中声明的每个代理属性都涉及到其关联的代理对象创建的性能开销,并向该类中添加一些metadata元数据。必要的时候,可以尝试为不同属性复用同一个代理实例。在你声明大量代理属性的时候,还需要考虑代理属性是否你的最佳选择。

  • 泛型代理

    private var maxDelay: Long by SharedPreferencesDelegate<Long>()//原生类型Long属性代理,每次读取或写入该属性时都避免不了装箱和拆箱的发生

但是,如果像上面例子那样使用具有原生类型属性的泛型代理的话,即便声明的原生类型为非null,每次读取或写入该属性时都避免不了装箱和拆箱的发生

优化建议:

对于非null原生类型的代理属性,最好使用为该特定值类型创建特定的代理类,而不是泛型代理,以避免在每次访问该属性时产生的装箱开销

  • 标准库代理: by lazy (这是一种将昂贵的初始化操作延迟到实际需要使用之前的巧妙方法,可以在保持代码可读性的同时又提高了性能。)

    //lazy(initializer: () -> T) 是一个为只读属性返回代理对象的函数,该属性是通过在其首次被读取的时,lazy函数参数lambda initializer执行来初始化的。
    private val dateFormat: DateFormat by lazy {
        SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
    }

需要注意到的是,lazy()函数不是内联函数,并且作为参数传递的lambda将编译成独立的Function类,并且不会在返回的代理对象内进行内联。通常会被人忽略的是lazy()另一重载函数实际上还隐藏一个可选的模式参数来确定应该返回3种不同类型的代理中的一种:

默认的模式是 LazyThreadSafetyMode.SYNCHRONIZED 将执行相对开销昂贵的双重锁的检查,这是为了保证在多线程环境下读取属性时,初始化块可以安全运行。

优化建议:

使用lazy()代理可以按需延迟昂贵的初始化,此外可以指定线程安全的模式以避免不必要的双重锁检查。

46、Kotlin中性能优化之Range区间

  • 尝试在没有间接声明过程区间检查中使用直接声明区间的方式,来避免额外区间对象的创建分配,另外,可以将它们声明成常量以此来复用他们。

  • 若要在for循环中进行迭代,最好使用区间表达式,该区间表达式只涉及到对 ..downTo()untill()的单个函数调用,以避免创建临时progression对象的开销。

  • 要对Range进行迭代,最好使用简单的for循环,而不是在其上调用forEach()函数,以避免迭代器对象的开销。

  • 当遍历未实现Collection接口的自定义集合时,最好直接在for循环中编写自己的索引范围,而不是依靠函数或属性来生成区间,以避免分配区间对象。


   转载规则


《Kotlin面试题》 mikyou 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Kotlin的独门秘籍Reified实化类型参数(下篇) Kotlin的独门秘籍Reified实化类型参数(下篇)
简述:今天我们开始接着原创系列文章,首先说下为什么不把这篇作为翻译篇呢?我看了下作者的原文,里面讲到的,这篇博客都会有所涉及。这篇文章将会带你全部弄懂Kotlin泛型中的reified实化类型参数,包括它的基本使用、源码原理、以及使用场景。
2019-10-27
下一篇 
如何在你的Kotlin代码中移除所有的!!非空断言 如何在你的Kotlin代码中移除所有的!!非空断言
翻译说明: 原标题: How to remove all !! from your Kotlin code 原文地址: https://android.jlelse.eu/how-to-remove-all-from-your-kotlin
2019-10-27
  目录