Kotlin语法基础篇终章:面试中的一些基础知识

236 阅读7分钟

面试官:1.在Kotlin中函数类型和Lambda表达式之间有什么关系?

Lambda表达式是函数类型的实例,只是单单从语法上来看不太好理解,这就好比我们创建一个Student类型的属性,然后将Student的对象实例赋值给该属性:

val student: Student = Student()

val block:() -> Unit = { }

我们在Kotlin中定义一个函数类型事实上就是在Java中定义一个FunctionN类型的接口。比如我们的函数类型block:

val block: (() -> Unit)? = null

我们单独将这行代码反编译得到的Java代码就是:

private static final Function0 block;

面试官:2.在Kotlin中安全的类型转换该怎么写?

首先在Kotlin中类型转换我们使用关键字as,这和Java中写法有很大的区别,下面我们先来看一下这两种语言的不同写法:

// Kotlin写法
fun getMainActivity() = activity as MainActivity

// Java写法
protected MainActivity getMainActivity() {
    return (MainActivity) getActivity();
}

常规的类型转化可能会出现ClassCastException,而在Kotlin中有更加安全的写法: as?

fun getMainActivity() = activity as? MainActivity

as? 表示尝试转换成目标类型,如果尝试转换成目标类型失败则返回null。我们把这行代码反编译后可以得到如下Java代码:

public final MainActivity getMainActivity() {
   FragmentActivity activity = this.getActivity();
   if (!(activity instanceof MainActivity)) {
      activity = null;
   }

   return (MainActivity)activity;
}

可以看到 as?Java中,在转换之前会帮我们做一次 instanceof 的判断,如果需要转换的类型和目标类型不一致,我们就返回 null ,如果一致我们就强制转换。

面试官:3.在Kotlin中如何安全的判空?

Kotlin中安全的调用操作符我们使用 ?. ,例如我们这里有一个简单的示例:

fun main() {
    var student: Student? = null
    info(student)
}

fun info(student: Student?) {
    student?.name
}

事实上在Java中它是这么实现的:

public final class FunKt {
   public static final void main() {
      Student student = null;
      info((Student)student);
   }

   public static void main(String[] var0) { main() }

   public static final void info(@Nullable Student student) {
      if (student != null) {
         student.getName();
      }

   }
}

可以看到 ?. 这里还是使用了 if (student != null) 的判断逻辑。?. 操作符表示如果非空就正常返回,如果为空就返回null。这在链式调用中很有用,比如我们还想获取studentname属性的hashcode值,我们就可以这么写:

student?.name?.hashCode()

面试官:4.在Kotlin中一个类的无参主构造函数,在什么时候可以省略constructor关键字?

Koltin中一个类可以有一个主构造函数以及一个或多个次构造函数。如果主构造函数没有任何的注解或者可见性修饰符,可以省略constructor关键字。

// 可以省略
class Car { }

// 不可以省略
class Car private constructor() { }

// 不可以省略
class Car @Inject constructor() { }

// 不可以省略
class Car @Inject private constructor() { }

面试官:5.具体化的参数类型有了解过吗?什么是具体化的参数类型?

具体化的参数类型事实上是Kotlin结合泛型和内联函数的概念为我们提供的语法糖。在编译期间,会将我们的参数类型转化成具体的类型,这样我们在运行时就无需再考虑什么泛型的类型擦除机制了。在运行时不存在泛型了,就是一个具体的类型。具体化的参数类型,让 obj as T 或者 obj is T 这样的语法可以在实际开发中运用。下面我们就来看一个具体的例子:

fun main() {
    printInfo<BaseFragment>(MainFragment())
}

inline fun <reified T> printInfo(obj: T) {
    println(T::class.simpleName)

    if(obj is BaseFragment) {
        println("obj is BaseFragment")
    }
    
    println("${obj as BaseFragment}")
}

这里我们首先定义了一个内联的泛型函数printInfo(),然后使用reified的关键字修饰了泛型T。具体化的参数类型必须在内联函数中声明,然后在printInfo()方法内部我们打印了有关该类型的参数。反编译后,我们得到如下Java代码:

public final class GenericityKt {
   public static final void main() {
      Object obj = new MainFragment();
      String name = Reflection.getOrCreateKotlinClass(BaseFragment.class).getSimpleName();
      System.out.println(name);
      if (obj instanceof BaseFragment) {
         name = "obj is BaseFragment";
         System.out.println(name);
      }

      name = String.valueOf((BaseFragment)obj);
      System.out.println(name);
   }

   public static void main(String[] var0) { main(); }

   public static final void printInfo(Object obj) {
      Intrinsics.reifiedOperationMarker(4, "T");
      String name = Reflection.getOrCreateKotlinClass(Object.class).getSimpleName();
      System.out.println(name);
      if (obj instanceof BaseFragment) {
         name = "obj is BaseFragment";
         System.out.println(name);
      }

      if (obj == null) {
         throw new NullPointerException("null cannot be cast to non-null type com.study.myapplication.fragment.BaseFragment");
      } else {
         name = String.valueOf((BaseFragment)obj);
         System.out.println(name);
      }
   }
}

为了方便阅读,这里还是首先将反编译的代码进行了一些调整。分析这段反编译后的代码我们发现,在main()函数中没有再调用内联函数printInfo()了,而是将printInfo()函数中的代码逻辑直接替换到了main()函数中去实现的,类型参数也被我们具体的类型MainFragment替代了。如果对内联函数和具体化的参数类型还不熟悉的读者,可以去阅读一下这两篇文章:深入浅出泛型inline、noinline、crossinline

面试官:6.我们常说在Kotlin中万物皆对象,那么在Java中也是万物皆对象吗?

Java中不是的,Java中存在的基本数据类型是一种值类型,值类型通常被分配在栈上,它的变量直接包含变量的实例。值类型和基本数据类型包装类之间的相互转换关系,我们通常称之为装箱和拆箱。
所以说Java中的基本数据类型并不是对象,而基本数据类型包装类是对象。严谨一点来说,在Java中并不是万物皆对象。

面试官:7.可变类型的参数有了解过吗?在Java和Kotlin中分别是如何实现的?

可变类型的参数在Kotlin中我们使用vararg关键字来声明,在Java中我们又称之为可变数组。当我们一个函数中需要一组不确定数量并且类型相同的参数时,可变数组将会变得很有用,下面我们就来看一个简单例子:

fun main() {
    printInfo("a", "b", "c")
}

private fun printInfo(vararg array:String) {
    for(item in array) {
        println("item = $item")
    }
}

这里我们首先定义了一个printInfo()函数,该函数接收一个可变的类型参数array,在printInfo()函数内部我们通过for循环将array中的每一个元素进行了打印。然后我们通过反编译后获取Java的实现方式如下:

public final class TrendsParamsKt {
   public static final void main() {
      printInfo("a", "b", "c");
   }

   public static void main(String[] var0) { main(); } 

   private static final void printInfo(String... array) {
      String[] var3 = array;
      int var4 = array.length;

      for(int var2 = 0; var2 < var4; ++var2) {
         String item = var3[var2];
         String var5 = "item = " + item;
         System.out.println(var5);
      }

   }
}

到这里我们可以看到,其实使用vararg修饰的可变类型的参数在Java中是使用 ClassName...的方式来实现的。对于使用 for (item in array) 的方式遍历一个数组,是通过普通的for循环来实现的。

for(int  = 0; i < array.lenght; ++i)

需要注意的是,for in 对一个集合的遍历并不是这么实现的,而是使用Iterable迭代器中的hasNext()方法利用while循环来实现的。到这里还有个小知识点想和大家一起分享一下,如果我们有一个数组,并且有一个接受可变类型参数的函数,该数组中的元素类型和声明的可变类型参数标记的类型是同一类型,那么我们可以使用 (spread) * 操作符来将已有的数组内容传递给该函数。如下代码示例:

val a = arrayOf("a", "b", "c")
val list = listOf("d", *a)

而这种写法在Java中是这么实现的:

String[] a = new String[]{"a", "b", "c"};
SpreadBuilder s = new SpreadBuilder(2);
s.add("d");
s.addSpread(a);
List list = CollectionsKt.listOf((String[])s.toArray(new String[s.size()]));

面试官:8.对于Kotlin中的属性,自定义getter和setter访问器,对此你是怎么理解的?

对于一个(final)只读的val类型属性来说,它可以自定义get()方法,但是并不是所有的只读属性都需要自定义get(),如果该属性没有根据一个动态的条件实时改变,我觉得是没有必要自定义get()

// 可以不用自定义
val name get() = "name"

// 需要自定义
val name get() = if (...) name = else name =

虽然官网中有这么一句话:对于 JVM 平台:通过默认 gettersetter 访问私有属性会被优化, 所以不会引入函数调用开销。
但是我觉得如果对于一个默认就是无法更改的只读属性,完全没有必要自定义getter访问器。 对于一个可变的var属性来说,它的gettersetter访问器都是可以自定义的。这里自定义setter和自定义getter其实是有些类似的,总结下来就是按需实现。需要才自定义。

面试官:9.知道apply函数是怎么实现的吗?可以现场写一个apply函数吗?

首先我们来分析一下apply函数在调用时候的一些特点,然后我们再去一步步的实现它。 apply函数可以在项目中任意处访问,那么apply函数是一个顶层函数:

fun apply() { }

apply函数在访问的时候可以使用拖尾Lambda表达式,那么apply函数拥有一个函数类型的参数:

inline fun apply(block: () -> Unit) { block() }

apply函数可以在任意对象上调用,那么apply函数是一个扩展函数:

inline fun Any.apply(block: () -> Unit) { block() }

apply函数拥有一个调用者的返回值:

inline fun Any.apply(block: () -> Unit) : Any {
    block()
    return this
}

调用apply函数的Lambda表达式中拥有当前上下文的作用域:

inline fun Any.apply(block: Any.() -> Unit) : Any {
    block()
    return this
}

此时我们查看一下标准库Standard.kt文件中apply函数的实现:

public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

这里我们发现标准库里是用泛型函数来实现的,但是其它的实现逻辑都是一样的。如果真要让我说这两种方法的实现有什么区别的话。我觉得有两点,其一就是用泛型来实现更加的灵活,其二就是使用泛型就需要了解泛型的类型擦除机制。

总结:

到这里Kotlin语法基础知识的专栏就结束了。这里笔者一共写了16篇有关Kotlin语法基础知识的介绍,有写的好的地方,也有写的不好的地方,如果有哪里不对的地方,欢迎指正。写博客最主要的目的还是为了学习和记忆。欢迎大家一起来交流有关Kotlin的语法基础知识,我们下期再见~