Kotlin语法糖--其他小知识
今儿起我们开始学习一下之前零零碎碎涉及到的一些知识
-
解构声明
有时候我们把一个对象解构成很多变量,这样看起来就比较方便,就像这样
val (name, age) = aValue
这种就叫解构声明。一个解构声明可以同时创建多个变量,例如这里的name与age,然后我们可以独立使用它们
println("$name + $age")
一个解构声明会被编译成以下代码
println("${aValue.component1()} + ${aValue.component2()}")
其中的component1与component2函数是Kotlin中的约定原则之一,当然也可以用component3、component4等等
我们在实现解构声明的时候,componentN函数需要使用operator关键字进行标记,你可以写在扩展函数中或者类的内部函数中,就像这样
private operator fun A.component2(): Int {
return age
}
class A {
val name = "Hello"
val age = 10
operator fun component1(): String {
return name;
}
}
在数据类中使用也是相当广泛
data class DataB(val name1: String, val age1: Int)
class B {
fun getDataB() : DataB {
return DataB("World", 20)
}
}
val (name1: String, age1: Int) = B().getDataB()
println("$name1 + $age1")
系统标准库也提供很多这样的扩展,比如对Map的迭代
@kotlin.internal.InlineOnly
public inline operator fun <K, V> Map.Entry<K, V>.component1(): K = key
@kotlin.internal.InlineOnly
public inline operator fun <K, V> Map.Entry<K, V>.component2(): V = value
使用的时候这样
val map: Map<String, String> = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3", "key4" to "value4")
for ((key, value) in map) {
println("$key + $value")
}
-
下划线用于不需要使用的变量
val (_, age) = aValue
-
在Lambda表达式中解构
如果Lambda表达式具有Pair类型(或者Map.Entry以及任何其他具有相应componentN函数的类型)的参数,那么可以将他们放在括号中来引入多个新参数来取代单个新参数
a {
it -> it.name+" "+it.age
}
a {
(name, age) -> name+" "+age
}
a {
name+" "+age
}
-
集合
与Java不同,Kotlin区分可变集合与不可变集合。这样有助于消除bug和设计良好的API
Kotlin的List<out T>类型是提供一个只读操作(如get、size)等的接口,可以修改List的是MutableList<T>。这一模式同样适用于Set<out T>/MutableSet<T>以及Map<K, out V>/MutableMap<K, V>
需要使用Kotlin标准库方法,如listOf、mutableListOf、setOf、mutableSetOf、mapOf、mutableMapOf
需要注意的是这些类型都是协变的,所以不可变类型子类集合可以直接赋值到父类集合中去。关于协变,在之前的泛型中已经专门说明了,此处不再赘述
可变集合不是协变的
只读集合是协变的
集合中有很多可用的扩展方法,你可以在使用过程中细细体会
-
区间
区间表达式由具有操作符..形式的rangeTo函数辅以in和!in组成。区间是为任何可比较类型定义的,但对于整数原生类型,Kotlin对它有一个优化的实现
val i=2
if (i in 1..10) {
println("查询到")
}
整形区间(IntRange、LongRange、CharRange)有一个额外的特性:他们可以用于迭代。编译器负责将其转换为类似Java的基于索引的for循环
for (j in 1..10) {
println(j)
}
如果要倒序输出,则使用downTo函数
for (j in 10 downTo 1) {
println(j)
}
如果想步进的话,直接用step函数
for (j in 1..10 step 2) {
println(j)
}
如果要创建一个不包含结束元素的区间,使用until函数
for (j in 1 until 10) {
println(j)
}
-
类型的检查与转换
-
is和!is操作符
我们可以使用is操作符或者其否定形式!is来检查对象是否符合给定的类型
val obj: Any? = null
when (obj) {
is String -> {
}
is Int -> {
}
else -> {
}
}
-
智能转换
在大多数情况下,不需要在Kotlin中使用显式转换操作符,因为编译器跟踪不可变值的is并检查,并在需要时自动自行安全的转换。我觉得Java在这点上,做的实在是太差了
来看看Java的类型转换
ArrayList<Object> objects=new ArrayList<>();
objects.add("123");
for (Object object : objects) {
if (object instanceof String) {
((String) object).length();
}
}
请注意,在已经判断过并且确定是String之后,Java依然要求将object强转为String,才能使用String的方法
再来看看Kotlin
var objs: MutableList<Any?> = mutableListOf()
objs.add("123")
for (obj in objs) {
if (obj is String) {
obj.length
}
}
怎么样,感觉牛逼吧。编译期足够聪明,知道转换后类型是安全的
智能转换同样适用于when以及while表达式
-
转换操作符
Kotlin中不安全的转换由中缀操作符as完成。
val value = obj as String
但是在之前的那个集合中,如果add进去的是一个null,那么这样强转就会失败,转换操作符会抛出异常,所以我们就得这样
val value: String? = obj as String?
为了安全的转换,我们就是用中缀操作符as?,它在失败的时候返回null
val value: String? = obj as? String
需要注意的是,尽管as?的右边是一个非空类型的String,但是其转换结果是可空的
-
This表达式
我们使用this表达式来表示当前的接收者:
- 在类的成员中,this指的是该类当前的对象
- 在扩展函数或者带接收者的函数字面值中,this表示在点左侧传递的接收者参数
- 如果this没有限定符,那么它指的是最内层包含它的作用域,如果要引用其他作用域中的this,那么请使用标签限定符this@lable,其中@lable是一个代指this来源的标签
请注意以下图片中高亮部分
C类对象CIn类对象
a函数的接收者,一个Int
a函数的接收者,一个Int
sum函数的接收者
a函数的接收者,一个Int
-
相等性
Kotlin中有两种相等性的类型
- 引用相等(2个引用指向同一个对象,由===(以及其否定形式!==)操作判断)
- 结构相等(用equals()或者==(以及其否定形式!=)检查)
-
空安全性
-
可空类型与非空类型
Kotlin旨在帮助我们从代码层面上消除NPE,类型系统区分一个引用是否可以容纳null还是不能容纳。例如String类型的常规变量是不能容纳null的,如果想要允许容纳,那么我们就得声明一个变量为可空字符串,写作String?
可空字符串
如果你直接访问可空类型字符串,会提示错误
不能直接访问
我们来看看如何访问可空类型
-
在条件中检查null
很传统
val length = if (b != null) b.length else 0
-
安全的调用
b?.length
如果b为非空,则返回b.length,否则返回null。这个表达式类型为Int?
如果只要对非空值执行某个操作,那么你可以尝试let
b?.let {
val length = b.length
}
这样在b为null的情况下,lambda表达式中的内容将不会执行
-
Elvis操作符
val length = b?.length ?: 0
如果?:左边的表达式不为null,那么Elvis操作符就返回左侧表达式的结果,反之返回右侧表达式的结果。这里如果b不为null,length值为b.length,否则为0
throw跟return在Kotlin中都是表达式,所以他们也可以出现在Elvis操作符右侧
fun check() : Int? {
val b: String? = null
val length = b?.length ?: return null
val c: String? = null
val length2 = c?.length ?: throw Exception()
return length2
}
-
!!操作符
如果你确定你的值不会出现null,你可以写成这样
b!!.length
此时如果b出现null,那么就会出现NPE啦
-
注解
注解的相关概念这里就不多说了,有疑问请查阅资料或者看看我之前的博客,我们直接谈怎么用吧
先来一段Java的注解声明
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.CONSTRUCTOR})
public @interface MyAnnotation {
String name() default "Jack";
int age();
}
用Kotlin翻译一下
@Target(AnnotationTarget.CLASS, AnnotationTarget.FIELD, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.RUNTIME)
annotation class MyKAnnotation(val name: String = "Jack", val age: Int)
运行结果都是一样的
注解
使用的时候几乎没啥区别
@MyKAnnotation(name = "Hello", age = 20)
public class AnnotationClass {
@MyKAnnotation(name = "WORLD", age = 30)
String value="value";
@MyKAnnotation(age = 10)
public void function() {}
}
看看Kotlin的
@MyKAnnotation(name = "Hello", age = 20)
class KAnnotationClass @MyKAnnotation(name = "Hello KAnnotationClass", age = 20) constructor(val value_: String) {
@MyKAnnotation(name = "WORLD", age = 30)
val value = 10
@MyKAnnotation(age = 10)
fun function() {}
}
请注意,如果需要对类的主构造函数进行标注,则需要在构造函数声明中添加constructor关键字,并将注解添加在其前面
注解参数不可以有可空类型,因为JVM不支持将null作为注解属性的值进行存储
如果注解作为另一个注解的参数,则其名称前不需要加“@”字符作为前缀
@Target(AnnotationTarget.CLASS, AnnotationTarget.FIELD, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.RUNTIME)
annotation class MyKAnnotation2(val kan: MyKAnnotation)
@MyKAnnotation2(kan = MyKAnnotation("old", 100))
class K2AnnotationClass @MyKAnnotation(name = "Hello KAnnotationClass", age = 20) constructor(@field:MyKAnnotation(name = "Hello KAnnotationClass", age = 20) val value_: String)
-
反射
我们依然通过对比Java与Kotlin来学习,俗话说没对比就没伤害嘛
先看一个普通的类,一个私有方法加一个私有对象
class ReflectStudyA {
private String aValue="123";
private void toIntValue(String value) {
System.out.println(Integer.parseInt(value));
}
}
我想在代码中的任何地方调用toIntValue方法,并且将aValue的值作为入参。如果是Java,得这样写
try {
Class reflectStudyAClass = Class.forName("com.renyu.kotlin.chapter9.ReflectStudyA");
ReflectStudyA reflectStudyA = (ReflectStudyA) reflectStudyAClass.getDeclaredConstructor().newInstance();
Field aValueField = reflectStudyAClass.getDeclaredField("aValue");
aValueField.setAccessible(true);
String aValue = (String) aValueField.get(reflectStudyA);
Method method = reflectStudyAClass.getDeclaredMethod("toIntValue", new Class[] {String.class});
method.setAccessible(true);
method.invoke(reflectStudyA, aValue);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
直接改成Kotlin,其实也没多大变化
val reflectStudyAClass = ReflectStudyA::class.java
var reflectStudyA: ReflectStudyA = reflectStudyAClass.getDeclaredConstructor().newInstance()
val aValueField = reflectStudyAClass.getDeclaredField("aValue")
aValueField.isAccessible = true
val aValue = aValueField.get(reflectStudyA)
val reflectStudyAMethod = reflectStudyAClass.getDeclaredMethod("toIntValue", String::class.java)
reflectStudyAMethod.isAccessible=true
reflectStudyAMethod.invoke(reflectStudyA, aValue)
要知道KClass与Java的Class是不一样的。最基本的反射功能是获取Kotlin类运行时的引用,要获取对静态已知的Kotlin类的引用,可以使用类字面值语法,比如MyClass::class。该引用是KClass类型的值,如果要在Kotlin编码中获得Java类的引用,就得在KClass实例上使用.java属性
对比一下KClass的写法
val reflectStudyAClass = ReflectStudyA::class
for (constructor in reflectStudyAClass.constructors) {
var reflectStudyA: ReflectStudyA = constructor.javaConstructor!!.newInstance()
for (declaredMemberProperty in reflectStudyAClass.declaredMemberProperties) {
if (declaredMemberProperty.name == "aValue") {
declaredMemberProperty.isAccessible = true
val aValue = declaredMemberProperty.get(reflectStudyA)
for (function in reflectStudyAClass.functions) {
if (function.name == "toIntValue") {
function.isAccessible = true
function.javaMethod!!.invoke(reflectStudyA, aValue)
}
}
}
}
}
总得来说并没有变的多好,这个是因为我们使用的是Java中的对象。这次我们全部使用Kotlin来重写一遍
class ReflectStudyB {
var aValue = "234"
fun toIntValue(value: String) {
println(value)
}
}
我把private去掉了,然后你会发现眼前一亮
val reflectStudyBClass = ReflectStudyB::class
val reflectStudyB = reflectStudyBClass.createInstance()
val aValue_: KMutableProperty1<ReflectStudyB, String> = ReflectStudyB::aValue
val aValue = aValue_.get(reflectStudyB)
val toIntValueFun = ReflectStudyB::toIntValue
toIntValueFun.call(reflectStudyB, aValue)
这里有很多我们不明白的地方,没关系,我们一个个的来研究
-
函数引用
一般情况下我们这样命名一个函数
fun abc(value: String) : Int {
return value.toInt()
}
调用它也很自然的这样
abc("2")
但是我们也可以把它当做一个值传递,例如传给一个另函数,为此我们使用::操作符
fun getValue(funValue: (String) -> Int, value: String) {
println(funValue(value))
}
arrays.forEach {
getValue(::abc, it)
}
这里的::abc是函数类型(String) -> Int的一个值,所以直接传递到getValue方法中
如果我们需要使用类的成员函数或扩展函数,那么需要对这种写法限定,例如
fun String.bcd() : Int {
return this.toInt()
}
这种情况下,我们需要使用String进行限定
arrays.forEach {
getValue(String::bcd, it)
}
-
属性引用
函数可以引用,同样属性也可以引用
var xValue = 3
::xValue.set(24)
println(::xValue.get())
表达式::xValue类型为KProperty<Int>,它允许我们使用get()读取它的值,或者使用name属性来获取属性名称。对于可变属性,类型则为KMutableProperty<Int>,并且有一个set()方法
如果要访问类的成员的属性,我们要这样限定
class ABC {
var yValue = 3
}
val funValue = ABC::yValue
var abc = ABC()
funValue.set(abc, 12)
funValue.get(abc)
println(abc.yValue)
扩展属性其实也是差不多的
val String.x1Value: String
get() = "Hello "+this
println(String::x1Value.get("author"))
-
构造函数引用
构造函数可以像其他函数和属性那样引用,通过::操作符并添加类名来引用构造函数
class CDE {
var value: String? = null
constructor(value: String) {
this.value = value
}
fun printValue() {
println(value)
}
}
通过这个方法引用构造函数
fun getCDE(cde: (String) -> CDE, value: String) : CDE {
return cde(value)
}
看看引用部分
var cde: CDE = getCDE(::CDE, "aaa")
cde.printValue()