引言
如何学习Kotlin?如果有一定Java基础的话,最好的方式的就是写一段Kotlin代码,然后在IntelliJ IDEA中转换为Kotlin Bytecode,然后再Decompile成java code。
这样对照kotlin code和java code,可以更加直观形象的学习Kotlin。
个人是阅读《Kotlin in Action》这本书来学习Kotlin的,这本书很适合入门者,这本书可能唯一不足的地方就是没有加入Kotlin最新版的一些特性,比如协程。所以如果想学习新功能还需要去阅读Kotlin官方文档。
本文章不会描述一些Kotlin过于基础的内容,比如var
与val
、如何定义函数等等。更多的是一些使用技巧及原理总结。
本小节是Kotlin的一些基本语法,及其实现。
字符串
string templates(字符串模版)
在拼接字符串的时候Kotlin提供了更加直观的方式,用于取代使用"+"号拼接。
例如:
fun main(arg: Array<String>) {
var a="aaa"
var b="ccc"
println("hello $a and ${b.length} ")
}
使用$变量名
或者${表达式}
,则可以将变量或者表达式嵌入到字符串中。
在Kotlin Bytecode中可以看到,编译后的代码其实是创建了一个StringBuilder
对象拼接字符串。
函数
参数默认值
java的重载方法常见,比如我们在Android中自定义View的时候,就需要重载它的三个或者四个构造方法。在Kotlin中,我们可以通过指定参数的默认值避免无用的代码。
import android.content.Context
import android.util.AttributeSet
import android.view.View
class CusView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
}
我们可以看到CusView类的构造方法中attrs的默认值为null,defStyleAttr默认值为0,也就是说如果不指定attrs和defStyleAttr的值,那么就会把对应的属性设置为默认值。
kotlin中调用的时候可以采取以下几种方式:
val cusView1 = CusView(this)
val cusView2 = CusView(this,null)
val cusView3 = CusView(this,null,0)
val cusView4 = CusView(this,defStyleAttr = 0) // 可以指定命名参数,避免混淆
实现原理
为了能够更加清楚的展示原理,我们写一个稍微复杂的kotlin重载方法:
class Kotlin {
fun f(a: Int = 0, b: String = "hello", c: Int = 99, d: String = "hh", e: Float = 1.0f) {
}
}
看下对应的Java Code:
public final class Kotlin {
public final void f(int a, @NotNull String b, int c, @NotNull String d, float e) {
Intrinsics.checkParameterIsNotNull(b, "b");
Intrinsics.checkParameterIsNotNull(d, "d");
}
// $FF: synthetic method
public static void f$default(Kotlin var0, int var1, String var2, int var3, String var4, float var5, int var6, Object var7) {
if ((var6 & 1) != 0) {
var1 = 0;
}
if ((var6 & 2) != 0) {
var2 = "hello";
}
if ((var6 & 4) != 0) {
var3 = 99;
}
if ((var6 & 8) != 0) {
var4 = "hh";
}
if ((var6 & 16) != 0) {
var5 = 1.0F;
}
var0.f(var1, var2, var3, var4, var5);
}
}
这里面一共有两个方法,一个是f,如果我们的参数全部都有指定值时,则会调用这个方法;另外一个方法是
$default
,如果调用方法的参数没有全部都有指定值(即有的参数使用了默认值)时,会调用这个方法,在该方法内部使用了位运算来表示那个参数要使用默认值。具体来说,假设一共有啊a、b、c、d、e五个参数都指定了默认值,那么在调用该方法的时候会构造一个var6
,这个参数的二进制的后五位的每一位都对应了一个参数,如果指定了参数,那么这一位为0,否则为1。举例来说调用f(a = 0, b = "bb", d = "ff")
,由于a、b、d都指定了参数,那么第1位、第2位、第4位为0,第3位与第五位为1,那么var6
的值为10100即为20。如下代码:
fun ff() {
f(a = 0, b = "bb", d = "ff")
}
对应的java code:
public final void ff() {
f$default(this, 0, "bb", 0, "ff", 0.0F, 20, (Object)null);
}
@JvmOverloads
细心的读者可能会发现,CusView的构造方法由@JvmOverloads
来修饰,这个注解的作用是什么呢?
首先来看下不加这个注解时,kotlin代码对应的java code:
public final class CusView extends View {
private HashMap _$_findViewCache;
public CusView(@NotNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
Intrinsics.checkParameterIsNotNull(context, "context");
super(context, attrs, defStyleAttr);
}
// $FF: synthetic method
public CusView(Context var1, AttributeSet var2, int var3, int var4, DefaultConstructorMarker var5) {
if ((var4 & 2) != 0) {
var2 = (AttributeSet)null;
}
if ((var4 & 4) != 0) {
var3 = 0;
}
this(var1, var2, var3);
}
// 。。。有省略
}
那么这样就会有一个问题,java代码调用kotlin代码的时候,就必须指定全部参数,不能省略部分参数。因为并没有重载方法。那么如果需要重载方法,那么就需要加上@JvmOverloads
注解。这样编译器就会生成Java重载方法。如下:
public final class CusView extends View {
@JvmOverloads
public CusView(@NotNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
Intrinsics.checkParameterIsNotNull(context, "context");
super(context, attrs, defStyleAttr);
}
// $FF: synthetic method
@JvmOverloads
public CusView(Context var1, AttributeSet var2, int var3, int var4, DefaultConstructorMarker var5) {
if ((var4 & 2) != 0) {
var2 = (AttributeSet)null;
}
if ((var4 & 4) != 0) {
var3 = 0;
}
this(var1, var2, var3);
}
@JvmOverloads
public CusView(@NotNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0, 4, (DefaultConstructorMarker)null);
}
@JvmOverloads
public CusView(@NotNull Context context) {
this(context, (AttributeSet)null, 0, 6, (DefaultConstructorMarker)null);
}
}
扩展函数
扩展函数是类的成员函数,只不过它是定义在类外。
fun String.firstChar():Char= if (length==0) ' ' else get(0)
fun main() {
println("hello".firstChar())
}
// output:h
我们自己定义了一个String的扩展函数firstChar
,获取String的第一个字符。这样就相当于把String类动态添加了一个功能。是不是感觉跟代理模式有些相似?
原理
扩展函数是在当前类下增加了一个静态方法,以扩展类作为参数。
public final class LearnKotlinKt {
public static final void main() {
char var0 = firstChar("hello");
System.out.println(var0);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
public static final char firstChar(@NotNull String $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
return $receiver.length() == 0 ? ' ' : $receiver.charAt(0);
}
}
所以如果想从Java代码中调用扩展函数,需要通过类名.方法名
方式调用。
现在问题来了,如果定义的扩展函数与类中原本的函数方法重名,那么实际调用会使用哪一个方法呢?
答案是扩展函数会覆盖原有函数。
我们来看下:
fun main() {
println("hello".length())
}
fun String.length(): Int = -length
// output: -5
扩展函数能够覆盖吗?
如果父类和子类同时定义了一个扩展函数,那么子类的扩展函数能够覆盖父类吗?
我们看下实际例子:
open class Parent
class Stub : Parent()
fun Parent.say() = print("I am parent")
fun Stub.say() = print("I am stub")
fun main() {
var p: Parent = Stub()
p.say()
}
// output: "I am parent"
这里我们可以看到被调用的函数是由静态类型决定的,看下对应的java code 就更加直观了。
// Parent.java
import kotlin.Metadata;
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\b\u0016\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002¨\u0006\u0003"},
d2 = {"LParent;", "", "()V", "LearnKotlin"}
)
public class Parent {
}
// Stub.java
import kotlin.Metadata;
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002¨\u0006\u0003"},
d2 = {"LStub;", "LParent;", "()V", "LearnKotlin"}
)
public final class Stub extends Parent {
}
// HelloKt.java
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 2,
d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001\u001a\n\u0010\u0002\u001a\u00020\u0001*\u00020\u0003\u001a\n\u0010\u0002\u001a\u00020\u0001*\u00020\u0004¨\u0006\u0005"},
d2 = {"main", "", "say", "LParent;", "LStub;", "LearnKotlin"}
)
public final class HelloKt {
public static final void say(@NotNull Parent $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
String var1 = "I am parent";
System.out.print(var1);
}
public static final void say(@NotNull Stub $receiver) {
Intrinsics.checkParameterIsNotNull($receiver, "receiver$0");
String var1 = "I am stub";
System.out.print(var1);
}
public static final void main() {
Parent p = (Parent)(new Stub());
say(p);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
Infix标记
使用infix关键字修饰的函数在调用的时候可以省略点和括号。但是需要满足以下条件才可以:
1.必须是成员方法或者扩展方法
2.方法参数只能有一个
3.参数不能是可变参数以及不能有默认值
举个例子:
infix fun Int.power(x: Int): Int {
return Math.pow(this * 1.0, x * 1.0).toInt()
}
fun main() {
print(2 power 3)
print(2.power(3)) // same
}
我们为Int类增加了一个shl的扩展函数,由于使用infix修饰,那么在调用的时候可以省略掉点和括号。
需要注意的是infix函数的优先级要低于算术运算、强转和rangeTo操作符,但是高于位运算与逻辑运算。
类与接口
与java不同,继承类或者实现接口并不需要用extends
或者implements
关键字,而是使用:
。与java一样的是使用@override
注解表示重写父类或者接口的方法,但是在java中该注解不是强制的,而在Kotlin中是强制要求的。
接口
接口方法默认实现
接口中的方法可以有默认实现,但是与Java 8中的默认实现方法不同,不需要添加defalut
关键字。实际上Kotlin的1.0版本是以Java 6为目标设计的,所以其实不能直接使用java 8中的默认方法。那么我们来看下它的实现原理:
interface IFace {
fun show() {
println("default show")
}
fun hide()
}
class IFaceImpl : IFace {
override fun hide() {
println("hide")
}
}
fun main() {
IFaceImpl().show();
}
这里定义了一个接口IFace
,它有一个默认实现的方法show
,我们定义了它的子类并且调用了默认的方法。接下来看下对应的Java code:
// LearnKotlinKt.java
import kotlin.Metadata;
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 2,
d1 = {"\u0000\b\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001¨\u0006\u0002"},
d2 = {"main", "", "LearnKotlin"}
)
public final class LearnKotlinKt {
public static final void main() {
(new IFaceImpl()).show();
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
// IFace.java
import kotlin.Metadata;
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0002\bf\u0018\u00002\u00020\u0001J\b\u0010\u0002\u001a\u00020\u0003H&J\b\u0010\u0004\u001a\u00020\u0003H\u0016¨\u0006\u0005"},
d2 = {"LIFace;", "", "hide", "", "show", "LearnKotlin"}
)
public interface IFace {
void show();
void hide();
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 3
)
public static final class DefaultImpls {
public static void show(IFace $this) {
String var1 = "default show";
System.out.println(var1);
}
}
}
// IFaceImpl.java
import kotlin.Metadata;
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\b\u0010\u0003\u001a\u00020\u0004H\u0016¨\u0006\u0005"},
d2 = {"LIFaceImpl;", "LIFace;", "()V", "hide", "", "LearnKotlin"}
)
public final class IFaceImpl implements IFace {
public void hide() {
String var1 = "hide";
System.out.println(var1);
}
public void show() {
IFace.DefaultImpls.show(this);
}
}
可以看到在接口内部生成了一个静态final类DefaultImpls
,这个类实现了默认方法。而在调用接口默认方法的时候,也会通过这个类来调用:IFace.DefaultImpls.show(this)
。
如果要在java中实现kotlin的接口,那么要实现全部的方法,包括已经有默认实现的方法。
类
Nested classes(嵌套类)与Inner classes(内部类)
kotlin与java一样,都可以声明内部类,但是不同的是默认情况下内部的类并不持有外部类引用,类似于Java中的静态内部类,创建类的方式静态内部类也相似,所以默认其实是嵌套类。
如果想要持有外部类的应用,那么要将内部类添加上inner
修饰。
总结一下:
类型 | Java | Kotlin |
---|---|---|
Inner classes(内部类,持有外部类引用) | class | inner class |
nested classes(嵌套类,不持有外部类引用) | static class | class |
Data classes(数据类)
如果我们定义一个类只是为了能够存储一些数据(属性),我们在重写的toString
、equals
和hashCode
方法中,会使用到这些属性。使用Kotlin中的data classes可以自动为我们完成这些操作。来看下:
data class People(val name:String,val age:Int)
对应的java code:
import kotlin.Metadata;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000 \n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\b\n\u0002\b\t\n\u0002\u0010\u000b\n\u0002\b\u0004\b\u0086\b\u0018\u00002\u00020\u0001B\u0015\u0012\u0006\u0010\u0002\u001a\u00020\u0003\u0012\u0006\u0010\u0004\u001a\u00020\u0005¢\u0006\u0002\u0010\u0006J\t\u0010\u000b\u001a\u00020\u0003HÆ\u0003J\t\u0010\f\u001a\u00020\u0005HÆ\u0003J\u001d\u0010\r\u001a\u00020\u00002\b\b\u0002\u0010\u0002\u001a\u00020\u00032\b\b\u0002\u0010\u0004\u001a\u00020\u0005HÆ\u0001J\u0013\u0010\u000e\u001a\u00020\u000f2\b\u0010\u0010\u001a\u0004\u0018\u00010\u0001HÖ\u0003J\t\u0010\u0011\u001a\u00020\u0005HÖ\u0001J\t\u0010\u0012\u001a\u00020\u0003HÖ\u0001R\u0011\u0010\u0004\u001a\u00020\u0005¢\u0006\b\n\u0000\u001a\u0004\b\u0007\u0010\bR\u0011\u0010\u0002\u001a\u00020\u0003¢\u0006\b\n\u0000\u001a\u0004\b\t\u0010\n¨\u0006\u0013"},
d2 = {"LPeople;", "", "name", "", "age", "", "(Ljava/lang/String;I)V", "getAge", "()I", "getName", "()Ljava/lang/String;", "component1", "component2", "copy", "equals", "", "other", "hashCode", "toString", "LearnKotlin"}
)
public final class People {
@NotNull
private final String name;
private final int age;
@NotNull
public final String getName() {
return this.name;
}
public final int getAge() {
return this.age;
}
public People(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
super();
this.name = name;
this.age = age;
}
@NotNull
public final String component1() {
return this.name;
}
public final int component2() {
return this.age;
}
@NotNull
public final People copy(@NotNull String name, int age) {
Intrinsics.checkParameterIsNotNull(name, "name");
return new People(name, age);
}
// $FF: synthetic method
@NotNull
public static People copy$default(People var0, String var1, int var2, int var3, Object var4) {
if ((var3 & 1) != 0) {
var1 = var0.name;
}
if ((var3 & 2) != 0) {
var2 = var0.age;
}
return var0.copy(var1, var2);
}
@NotNull
public String toString() {
return "People(name=" + this.name + ", age=" + this.age + ")";
}
public int hashCode() {
String var10000 = this.name;
return (var10000 != null ? var10000.hashCode() : 0) * 31 + this.age;
}
public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (var1 instanceof People) {
People var2 = (People)var1;
if (Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age) {
return true;
}
}
return false;
} else {
return true;
}
}
}
Sealed Classes(密封类)
sealed classes(密封类)为创建的子类提供了严格的限制。将父类使用sealed
修饰,那么所有的子类必须与父类在统一文件中(在Kotlin 1.1之前,要求所有的子类必须定义在父类内)。
sealed classes是抽象类,所以无法被直接初始化。
sealed classes不允许有非private的构造方法,它们的构造方法默认是private。
如下代码:
sealed class Child
class Son: Child()
class Daughter: Child()
对应的java code:
// Child.java
import kotlin.Metadata;
import kotlin.jvm.internal.DefaultConstructorMarker;
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0000\b6\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002\u0082\u0001\u0002\u0003\u0004¨\u0006\u0005"},
d2 = {"LChild;", "", "()V", "LSon;", "LDaughter;", "LearnKotlin"}
)
public abstract class Child {
private Child() {
}
// $FF: synthetic method
public Child(DefaultConstructorMarker $constructor_marker) {
this();
}
}
// Daughter.java
import kotlin.Metadata;
import kotlin.jvm.internal.DefaultConstructorMarker;
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002¨\u0006\u0003"},
d2 = {"LDaughter;", "LChild;", "()V", "LearnKotlin"}
)
public final class Daughter extends Child {
public Daughter() {
super((DefaultConstructorMarker)null);
}
}
// Son.java
import kotlin.Metadata;
import kotlin.jvm.internal.DefaultConstructorMarker;
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002¨\u0006\u0003"},
d2 = {"LSon;", "LChild;", "()V", "LearnKotlin"}
)
public final class Son extends Child {
public Son() {
super((DefaultConstructorMarker)null);
}
}
Sealed Classes与when表达式结合使用更加能体现其价值。
Object
使用object来构造匿名内部类。
interface ClickListener {
fun onClick()
}
fun setOnClickListener(onClickListener: ClickListener) {
}
fun main() {
var count = 0;
setOnClickListener(object : ClickListener {
override fun onClick() {
count++
}
})
}
需要注意的是,kotlin的匿名内部类与java不同,并不需要将访问限制的final变量。查看对应的java code可以看到,其实在内部已经自动将变量封装成一个final引用对象。
public final class HelloKt {
public static final void setOnClickListener(@NotNull ClickListener onClickListener) {
Intrinsics.checkParameterIsNotNull(onClickListener, "onClickListener");
}
public static final void main() {
final IntRef count = new IntRef();
count.element = 0;
setOnClickListener((ClickListener)(new ClickListener() {
public void onClick() {
int var10001 = count.element++;
}
}));
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
使用object定义单例
object Single
对应的java code:
public static final class Single {
public static final Ks.Single INSTANCE;
private Single() {
}
static {
Ks.Single var0 = new Ks.Single();
INSTANCE = var0;
}
}
使用object定义伴生对象。
java中如果把方法或者属性使用static
来修饰,那么可以通过类名直接引用。Kotlin中没有static
关键字,但是可以通过companion object
来达到同样的效果。
class People {
companion object {
var sex = 0
fun say() {
println("say")
}
}
}
fun main() {
People.say()
println("${People.sex}")
}
看下对应的java code:
public class Ks extends ScriptTemplateWithArgs {
public final void main() {
Ks.People.Companion.say();
String var1 = String.valueOf(Ks.People.Companion.getSex());
System.out.println(var1);
}
public Ks(String[] var1) {
super(var1);
}
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0003\u0018\u0000 \u00032\u00020\u0001:\u0001\u0003B\u0005¢\u0006\u0002\u0010\u0002¨\u0006\u0004"},
d2 = {"LKs$People;", "", "()V", "Companion", "LearnKotlin"}
)
public static final class People {
private static int sex;
public static final Ks.People.Companion Companion = new Ks.People.Companion((DefaultConstructorMarker)null);
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\b\n\u0002\b\u0005\n\u0002\u0010\u0002\n\u0000\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\u0006\u0010\t\u001a\u00020\nR\u001a\u0010\u0003\u001a\u00020\u0004X\u0086\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\u0005\u0010\u0006\"\u0004\b\u0007\u0010\b¨\u0006\u000b"},
d2 = {"LKs$People$Companion;", "", "()V", "sex", "", "getSex", "()I", "setSex", "(I)V", "say", "", "LearnKotlin"}
)
public static final class Companion {
public final int getSex() {
return Ks.People.sex;
}
public final void setSex(int var1) {
Ks.People.sex = var1;
}
public final void say() {
String var1 = "say";
System.out.println(var1);
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
}
可以看到在People类内部,又生成了一个Companion类。当然我们可以更改Companion的名字,只需要在companion object
后面指定就可以。
当我们的java代码调用这个伴生对象中的方法或者属性时,需要通过People.Companion.say()
来调用,当然。如果指定了Companion的名字,需要把Companion替换成对应的名字。
但是这样会产生一个问题,java代码调用kotlin代码会产生性能问题,因为会涉及到两个类调用。如果想解决这个问题,那么需要在伴生对象的方法上加上@JvmStatic
注解,在属性名上加上@JvmField
注解。
class People {
companion object {
@JvmField
var sex = 0
@JvmStatic
fun say() {
println("say")
}
}
}
fun main() {
People.say()
println("${People.sex}")
}
看下对应的java code:
public class Ks extends ScriptTemplateWithArgs {
public final void main() {
Ks.People.Companion.say();
String var1 = String.valueOf(Ks.People.sex);
System.out.println(var1);
}
public Ks(String[] var1) {
super(var1);
}
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0003\u0018\u0000 \u00032\u00020\u0001:\u0001\u0003B\u0005¢\u0006\u0002\u0010\u0002¨\u0006\u0004"},
d2 = {"LKs$People;", "", "()V", "Companion", "LearnKotlin"}
)
public static final class People {
@JvmField
public static int sex;
public static final Ks.People.Companion Companion = new Ks.People.Companion((DefaultConstructorMarker)null);
@JvmStatic
public static final void say() {
Companion.say();
}
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\b\n\u0000\n\u0002\u0010\u0002\n\u0000\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\b\u0010\u0005\u001a\u00020\u0006H\u0007R\u0012\u0010\u0003\u001a\u00020\u00048\u0006@\u0006X\u0087\u000e¢\u0006\u0002\n\u0000¨\u0006\u0007"},
d2 = {"LKs$People$Companion;", "", "()V", "sex", "", "say", "", "LearnKotlin"}
)
public static final class Companion {
@JvmStatic
public final void say() {
String var1 = "say";
System.out.println(var1);
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
}
可以看到在People类内产生了一个age静态变量和say静态方法。
Backing Fields
在自定义属性的setter或者getter方法时,如果需要拿到当前属性值。系统为我们自动提供了一个字段field
。我们来看下:
var counter = 0 // Note: the initializer assigns the backing field directly
get() = field
set(value) {
if (value > 0) {
field = value
}
}
来看下对应的java code:
public final class HelloKt {
private static int counter;
public static final int getCounter() {
return counter;
}
public static final void setCounter(int value) {
if (value > 0) {
counter = value;
}
}
}
注意,field
不能被属性名替代的,这里我们做下实验:将filed
替换成属性名counter
:
var counter = 0 // Note: the initializer assigns the backing field directly
get() = counter
set(value) {
if (value > 0) {
counter = value
}
}
我们可以看到对应的java code其实产生了死循环:
public final class HelloKt {
public static final int getCounter() {
return getCounter();
}
public static final void setCounter(int value) {
if (value > 0) {
setCounter(value);
}
}
}
Lambda表达式
高阶函数
所谓高阶函数就是把函数当作参数传递或者返回值的函数。
函数类型的声明
我们来看一下实际的例子:
fun main() {
println(f(1, 2, { a, b -> a + b }))
println(f(1, 2, { a, b -> a - b }))
}
fun f(i: Int, j: Int, op: (Int, Int) -> Int): Int {
return op(i, j)
}
// output:3
// output:-2
这里的f函数是一个高阶函数,它有三个参数,一个是分别是i、j、和op,i和j都是Int类型,而op是一个函数类型,我们看下它的类型声明( Int, Int) -> Int
,括号里的Int
是函数类型声明的参数,-> Int
这里的Int指的是函数类型的返回值。那么这个函数参数的意义就是通过两个Int值的某些操作返回一个Int值。而这个所谓的某些操作就是我们要具体传递的方法。例如{ a, b -> a + b }
表示将两个Int值相加。{ a, b -> a - b }
表示将两个数相减。
所有的函数类型都会有一个括号括起来的参数类型列表和返回类型。比如(A, B) -> C
表示一个函数需要传递两个参数,类型分别是A和B,返回值类型是C。参数类型可以省略,但是返回值类型不能省略。如() -> Unit
。
函数类型可以额外有一个接受者类型。例如这个定义:A.(B) -> C
,表示这个函数可以被A类型调用,参数是B类型,返回值是C类型。在这个函数内部可以直接通过this
访问A类的属性或者方法,甚至直接省略this
。
函数类型也有几个特殊的例子:
(Int, Int) -> Int?
表示参数类型是Int
,返回值类型是Int?
((Int, Int) -> Int)?
表示参数类型是Int
,返回值类型是Int
,但是整个函数可以为null(Int) -> ((Int) -> Unit)
表示参数类型是Int
,返回值类型是另外一个函数类型,而这个函数类型的参数是Int
,返回值类型是Unit
(Int) -> (Int) -> Unit
的箭头优先级的顺序是从右向左,也就是它等价于(Int) -> ((Int) -> Unit)
函数类型的初始化
有以下几种方式获取一个函数类型的实例:
- 通过lambda表达式
{ a, b -> a + b }
- 通过一个匿名函数
fun(s: String): Int { return s.toIntOrNull() ?: 0 }
- 通过一个引用已经存在的声明
String::toInt
- 如果类实现了将函数类型接口,可以使用这个类的实例,例如:
class IntTransformer: (Int) -> Int {
override operator fun invoke(x: Int): Int = TODO()
}
val intFunction: (Int) -> Int = IntTransformer()
函数类型的引用
可以通过invoke方法调用,或者直接调用。举个例子:
fun main() {
val intPlus: (Int, Int) -> Int = { a, b -> a + b }
intPlus.invoke(2,3)
intPlus(4,5)
}
如果是带接受者的函数类型,可以把接受者作为第一个参数调用或者将接受者放在括号外类似于扩展函数的方式调用。例如:
import java.lang.StringBuilder
fun main() {
// 将string复制n份
val stringPlus: String.(Int) -> String = { it ->
var count = it
val builder = StringBuilder()
while (count-- > 0) {
builder.append(this)
}
builder.toString()
}
println("abs".stringPlus(3))
println(stringPlus("xyz",5))
println(stringPlus.invoke("mnp",4))
}
// output:
// absabsabs
// xyzxyzxyzxyzxyz
// mnpmnpmnpmnp
将Lambda表达式传递给java方法
我们知道java中的部分匿名内部类在kotlin中可以使用lambda来表示,那么lambda表达式对应的java code是不是就是一个匿名内部类呢?
我们来看几个例子:
假设我们定义了一个java 接口和设置这个接口的方法:
public interface OnClickListener {
void onClick();
}
public class HelloJava {
public void setListener(OnClickListener onClickListener){
}
}
现在在Kotlin中调用这个setListener
方法:
fun main() {
var s = "sss"
// 1
HelloJava().setListener { println("hi") }
// 2
HelloJava().setListener { println("hi") }
// 3
HelloJava().setListener { print("$s") }
// 4
HelloJava().setListener(object : OnClickListener {
override fun onClick() {
println("hi")
}
})
}
1、2、3都是使用lambda的方式,而4是使用匿名内部类的方法。
来看下这四种方法对应的java code:
public static final void main() {
final ObjectRef s = new ObjectRef();
s.element = "sss";
// 1
(new HelloJava()).setListener((OnClickListener)null.INSTANCE);
// 2
(new HelloJava()).setListener((OnClickListener)null.INSTANCE);
// 3
(new HelloJava()).setListener((OnClickListener)(new OnClickListener() {
public final void onClick() {
String var1 = String.valueOf((String)s.element);
System.out.print(var1);
}
}));
// 4
(new HelloJava()).setListener((OnClickListener)(new OnClickListener() {
public void onClick() {
String var1 = "hi";
System.out.println(var1);
}
}));
}
我们可以看到kotlin对应的java code中1、2两个是生成了两个INSTANCE
实例。而3、4生成的是匿名内部类。
现在有两个问题:
1.INSTANCE
实例对应的类是什么呢?
2.第一个lambda和第二个lambda表达式相同,那么它们生成的INSTANCE
对应的是同一个类甚至是同一个实例吗?
3.第三个lambda表达式为什么没有生成INSTANCE
实例呢?
首先我们来看下第一个lambda
中的相关kotlin bytecode代码:
L1
LINENUMBER 7 L1
NEW HelloJava
DUP
INVOKESPECIAL HelloJava.<init> ()V
GETSTATIC HelloKt$main$1.INSTANCE : LHelloKt$main$1; // 这里
CHECKCAST OnClickListener
INVOKEVIRTUAL HelloJava.setListener (LOnClickListener;)V
我们能看到INSTANCE
其实是HelloKt$main$1
的一个静态字段。
HelloKt$main$1
是一个单例,它的类名生成规则是类名$方法名$后缀
。
在阅读《Kotlin in Action》时,书中有这么一段话:
When you explicitly declare an object, a new instance is created on each invocation. With a lambda, the situation is different: if the lambda doesn’t access any variables from the function where it’s defined, the corresponding anonymous class instance is reused between calls.
当显式声明匿名内部类时,每次调用都会创建一个新实例。使用lambda表达式的时候是分情况的:如果lambda表达式没有引用范围外的变量,那么它创建的相应的匿名类会在不同调用之间重用。
最开始看这段话的时候,可能理解有些偏差。最开始的理解是:如果两次lambda内容相同,并且都没有引用外面的变量,那么这两个lambda会使用同一个类。
但是后来验证了以下,其实是错误的。
每次创建一个lambda都会创建一个新的类。类名的规则就是类名$方法名$后缀
,后缀是递增的。那么之前那段话里的reused
该怎么去理解呢?
这里的reused
其实是指的是使用同一个lambda表达式:
例如:
val function = { println("hi") }
HelloJava().setListener(function)
HelloJava().setListener(function)
这个代码确实会重用一个单例类。
如果lambda引用了范围外的变量,那么那还是会正常生成一个匿名内部类。引用的变量会通过一个包装类封装,因此在lambda中更改外部变量的值也是有效的。
inline
在上一小节可以看到lambda表达式会生成一个新的类,这样相比于直接执行相应的代码就会有性能上的不足。那么有没有办法既能高效执行代码,又可以将函数抽取出来呢?使用inline就可以。
当一个函数声明为inline的时候,那么函数体会直接替换到函数调用的地方。
来看个例子:
fun main() {
action(1, 2) { a, b -> a + b }
}
inline fun action(a: Int, b: Int, op: (Int, Int) -> Int): Int {
return op.invoke(a, b)
}
我们吧action函数声明为inline,来看下它的java code:
@Metadata(
mv = {1, 1, 15},
bv = {1, 0, 3},
k = 2,
d1 = {"\u0000\u0016\n\u0000\n\u0002\u0010\b\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a3\u0010\u0000\u001a\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u00012\u0006\u0010\u0003\u001a\u00020\u00012\u0018\u0010\u0004\u001a\u0014\u0012\u0004\u0012\u00020\u0001\u0012\u0004\u0012\u00020\u0001\u0012\u0004\u0012\u00020\u00010\u0005H\u0086\b\u001a\u0006\u0010\u0006\u001a\u00020\u0007¨\u0006\b"},
d2 = {"action", "", "a", "b", "op", "Lkotlin/Function2;", "main", "", "LearnKotlin"}
)
public final class HelloKt {
public static final void main() {
byte a$iv = 1;
int b$iv = 2;
int $i$f$action = false;
int var5 = false;
int var10000 = a$iv + b$iv;
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
public static final int action(int a, int b, @NotNull Function2 op) {
int $i$f$action = 0;
Intrinsics.checkParameterIsNotNull(op, "op");
return ((Number)op.invoke(a, b)).intValue();
}
}
可以看到在main()
方法中,没有直接调用action方法,而是把action里的逻辑直接替换处理,这样代码执行效率更高。
作为对比,可以再来看下action不声明为inline时的java code:
@Metadata(
mv = {1, 1, 15},
bv = {1, 0, 3},
k = 2,
d1 = {"\u0000\u0016\n\u0000\n\u0002\u0010\b\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a0\u0010\u0000\u001a\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u00012\u0006\u0010\u0003\u001a\u00020\u00012\u0018\u0010\u0004\u001a\u0014\u0012\u0004\u0012\u00020\u0001\u0012\u0004\u0012\u00020\u0001\u0012\u0004\u0012\u00020\u00010\u0005\u001a\u0006\u0010\u0006\u001a\u00020\u0007¨\u0006\b"},
d2 = {"action", "", "a", "b", "op", "Lkotlin/Function2;", "main", "", "LearnKotlin"}
)
public final class HelloKt {
public static final void main() {
action(1, 2, (Function2)null.INSTANCE);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
public static final int action(int a, int b, @NotNull Function2 op) {
Intrinsics.checkParameterIsNotNull(op, "op");
return ((Number)op.invoke(a, b)).intValue();
}
}
noinline
但是不是所有使用lambda的函数都可以被声明为inline,如果我们的lambda表达式在函数中被保存了下来,那么是不能声明为inline的。举个例子:
fun main() {
action(1, 2, { i, j -> i + j })
}
inline fun action(a: Int, b: Int, op: (Int, Int) -> Int): Int {
val lambda = op
return lambda.invoke(a, b)
}
这段代码是无法被编译通过的,回报如下错误:
Error:(10, 18) Kotlin: Illegal usage of inline-parameter ‘op’ in ‘public inline fun action(a: Int, b: Int, op: (Int, Int) -> Int): Int defined in root package in file Hello.kt’. Add ‘noinline’ modifier to the parameter declaration
假设我们一个函数参数中不只有一个lambda,而有的参数不应该被声明成inline,那么可以将这个参数用noinline
修饰。如:
inline fun action(a: Int, b: Int,
op1: (Int, Int) -> Int, noinline op2: (Int, Int) -> Int) {
}
集合函数式API
filter:从现有集合中选出满足条件的元素
map:将每一个元素转化为新的元素
all:所有元素是否全部满足条件
any:是否有一个元素满足条件
count:满足条件的元素的个数
find:找到一个满足条件的元素
groupBy:按条件分组,结果是map
flatMap:先根据作为实参给定的函数对集合中的每个元素做映射,然后把多个列表合并平铺成一个列表。
flatten:多个列表合并平铺成一个列表
举个例子:
fun main() {
val list = listOf<Int>(1, 2, 3, 4, 5, 6, 7)
val i = list.map { it * it }.filter { it > 20 }.find { it > 0 }
}
首先我们创建了一个list,使用map将每一个元素平方,然后筛选出大于20的元素,再从剩下的集合中找到一个大于0的元素。我们看下对应的java code:
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import kotlin.Metadata;
import kotlin.collections.CollectionsKt;
@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 2,
d1 = {"\u0000\b\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001¨\u0006\u0002"},
d2 = {"main", "", "LearnKotlin"}
)
public final class HelloKt {
public static final void main() {
List list = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4, 5, 6, 7});
Iterable $receiver$iv = (Iterable)list;
Collection destination$iv$iv = (Collection)(new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv, 10)));
Iterator var5 = $receiver$iv.iterator();
Object element$iv$iv;
int it;
boolean var8;
while(var5.hasNext()) {
element$iv$iv = var5.next();
it = ((Number)element$iv$iv).intValue();
var8 = false;
Integer var12 = it * it;
destination$iv$iv.add(var12);
}
$receiver$iv = (Iterable)((List)destination$iv$iv);
destination$iv$iv = (Collection)(new ArrayList());
var5 = $receiver$iv.iterator();
while(var5.hasNext()) {
element$iv$iv = var5.next();
it = ((Number)element$iv$iv).intValue();
var8 = false;
if (it > 20) {
destination$iv$iv.add(element$iv$iv);
}
}
$receiver$iv = (Iterable)((List)destination$iv$iv);
Iterator var13 = $receiver$iv.iterator();
Object var10000;
while(true) {
if (var13.hasNext()) {
Object var14 = var13.next();
int it = ((Number)var14).intValue();
int var16 = false;
if (it <= 0) {
continue;
}
var10000 = var14;
break;
}
var10000 = null;
break;
}
Integer i = (Integer)var10000;
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
通过java code我们可以看到在集合的操作过程中产生了一个临时集合destination$iv$iv
,这样对性能是有一定影响的。其实这些函数式API每进行一次操作,就会将所有元素赋值到临时数组中,实际上我们有更好的方式来减少性能损耗。就是使用asSequence
将集合转化为序列。
asSequence
将集合转换成序列之后,操作符分成了两类,一种是中间操作符,一种是末端操作符。中间操作符例如map、filter,末端操作符如asList,中间操作符的实现是懒加载,也就是说在遇到末端操作符之前是不会实际运行的。
还有一点,就是计算执行顺序不同。正常集合的操作是每进行一次函数式操作,都会产生一个中间集合。而序列的的顺序是,针对每一个元素都进行完整的所有操作。这样的好处在于在某些情况下可以提升程序执行效率。
举例来说:
对于集合来说:
val list = listOf<Int>(1, 2, 3, 4, 5, 6, 7)
val i = list.map { it * it }.find { it > 20 }
它的执行顺序如图:
而对于序列来说:
val list = listOf<Int>(1, 2, 3, 4, 5, 6, 7)
val i = list.asSequence().map { it * it }.find { it > 20 }
它的执行顺序如图:
很明显,序列的操作省去了元素6和7的map操作。执行效率更高。
集合的map等函数都是inline的,所以它们执行少量数据时效率很高,因为它们不会把lambda转换为对应的类。而序列的map等函数是非inline的,它们在执行大量数据时效率较高,因为不用产生临时对象。
with/apply/let/also/run
我们看下这几个函数的定义:
/**
* Calls the specified function [block] and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
/**
* Calls the specified function [block] with `this` value as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
/**
* Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
/**
* Calls the specified function [block] with `this` value as its receiver and returns `this` value.
*/
@kotlin.internal.InlineOnly
public c fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
/**
* Calls the specified function [block] with `this` value as its argument and returns `this` value.
*/
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
/**
* Calls the specified function [block] with `this` value as its argument and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
可以看到这几个函数都是inline
的。
分别看一下每个方法是如何使用的:
run函数(直接调用)
/**
* Calls the specified function [block] and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
run函数里直接传递一个lambda表达式,lambda的返回值就是run函数的返回值。
run函数(通过类调用)
/**
* Calls the specified function [block] with `this` value as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
通过T类来调用run函数,lambda表达式可以通过this
引用类中的方法或属性,this
可以省略。lambda的返回值就是run函数的返回值。
with函数
/**
* Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
使用T作为接受者,lambda表达式中可以使用this
引用类中的方法或属性,this
可以省略。with的返回值是lambda的最后一行。
apply函数
* Calls the specified function [block] with `this` value as its receiver and returns `this` value.
*/
@kotlin.internal.InlineOnly
public c fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
通过T类调用run方法,lambda表达式中可以使用this
引用类中的方法或属性,this
可以省略。apply的返回值是T。
also函数
/**
* Calls the specified function [block] with `this` value as its argument and returns `this` value.
*/
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
also方法从1.1版本开始,通过T类调用also方法,lambda表达式中需要传递T类作为参数,lambda表达式中使用it
引用类中方法或者属性。also方法返回值是T。
let函数
/**
* Calls the specified function [block] with `this` value as its argument and returns its result.
*/
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
let方法通过T类调用,lambda表达式中需要传递T类作为参数,lambda表达式中使用it
引用类中方法或者属性。also方法返回值是lambda的返回值。