Kotlin与Java的相互调用详解

Kotlin与Java的相互调用详解文章目录一、Kotlin调Java1、访问属性2、将Kotlin中是关键字的Java标识符进行转义3、空安全与平台类型4、已映射类型5、Java数组6、Java可变参数7、受检异常8、对象方法8-1、wait()/notify()8-2、getClass(),获取类的Class对象8-3、clone()8-4、finalize()9、SAM转换9-1、SAM转换详解9-2、SAM转换的歧义消除9-3、Kotlin函数式接口9-4、SAM转换限制10、在Kotlin中使用JNI二、Java

Kotlin在设计之初,就考虑了与Java的互操作性。因此Java和Kotlin是可以很方便的进行互相调用的。虽然Kotlin完全兼容Java,但不代表Kotlin就是Java,它们在相互调用但时候,还是有一些需要注意的细节。

一、Kotlin 调 Java

首先,几乎所有的Java代码,都可以在Kotlin中调用而没有任何问题。如在Kotlin中使用集合类:

import java.util.*

fun demo(source: List<Int>) { 
   
    val list = ArrayList<Int>()
    // “for”-循环用于 Java 集合:
    for (item in source) { 
   
        list.add(item)
    }
    // 操作符约定同样有效:
    for (i in 0..source.size - 1) { 
   
        list[i] = source[i] // 调用 get 和 set
    }
}

只是在创建对象和使用对象方法的时候,可以有更简洁的方式去使用。

下面针对一些细节做详细介绍:

1、访问属性

如果要访问一个Java对象的私有属性,Java对象都会提供Getter 和 Setter方法,通过相关的Getter 和 Setter方法,就可以拿到属性的值。

而如果一个Java类为成员属性提供了Getter 和 Setter方法,则在Kotlin中使用该属性的时候,就可以直接通过属性名去访问,而不用调对应的Getter 和 Setter方法,如:

lateinit var tvHello: TextView

override fun onCreate(savedInstanceState: Bundle?) { 
   
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    tvHello = findViewById(R.id.tvHello)

	  // 为TextView设置显示内容
    tvHello.text = "hello,world!"
		// 获取TextView的显示内容
    Log.i(TAG, "onCreate: ${ 
     tvHello.text}")
}

请注意,如果 Java 类只有一个 setter方法,没有提供getter方法,它在 Kotlin 中不会作为属性可见,因为 Kotlin 目前不支持只写(set-only)属性。

这个时候,为属性赋值,就只能通过它的setter方法进行。

2、将 Kotlin 中是关键字的 Java 标识符进行转义

一些 Kotlin 关键字在 Java 中是有效标识符:inobjectis 等等。 如果一个 Java 库使用了 Kotlin 关键字作为方法,属性,你仍然可以通过反引号(`)字符转义它来调用该方法:

public class User { 
   

    public Object object;
    
    public void is(){ 
   

    }
}

fun test(){ 
   
    val user = User()
    user.`is`() // 调用is方法,需要加上反引号
    user.`object` = Object() // 访问属性名,需要加上反引号
}

3、空安全与平台类型

平台类型:Java中,所有的引用都可能为null,然而在Kotlin中,对null是有着严格的检查与限制的,这就使得某个来自于Java的引用在Kotlin中变得不再适合;

基于这个原因,在Kotlin中,将来自于Java的声明类型称为平台类型

对于这种类型(平台类型)来说,Kotlinnull检查就得到一定的缓和,变得不再那么严格了。这样就使得空安全的语义要求变得与Java一致。

当我们调用平台类型引用的方法时,Kotlin就不会在编译期间施加空安全的检查,使得编译可以正常通过;但是在运行期间则有可能抛出异常,因为平台类型引用值有可能为null

如:

Java类

public class User { 
   
    public String name;// name属性在没有赋值的时候,是可能为空的
}

Kotlin类

在使用Java的User类的时候,User类中的属性会被Kotlin当作是:平台类型,意思是,哪怕name属性是空的,也可以直接调用属性的相关方法,从而有可能导致空指针的发生。


fun test() { 
   
    val user = User() 

    if (user.name.equals("李四")) { 
    
        Log.i(TAG, "test: 坏人")
        return
    }
}

如上面的代码,User对象创建后,没有给name属性赋值,然后直接就调用了name的比较方法,编译是可以通过的,但运行的时候就会报空指针异常

解决方法:

为了避免调用Java代码可能产生的空指针,我们可以在使用平台类型变量的时候,通过“ ?. ”的方式访问平台类型相关的属性方法,从而触发Kotlin断言机制,达到预防空指针的目的,如:

fun test() { 
   
    val user = User()

    // 通过 ?. 的方式去方法平台类型的属性和方法,Kotlin会检测是否为空,如果为空,就不调用对象方法,从而避免空指针
    if (user.name?.length == 2) { 
   
      println("test: 坏人")
    }
    // 编译期允许,运行时可能失败,还是可能会发生空指针,与直接调用没有本质区别
  	// 如果name是null,则运行时,这里的赋值就会报空指针问题
    val userName2:String = user.name 
}

如果我们使用了不可空类型,编译器会在赋值时生成一个断言,这会防止Kotlin的不可空变量持有null值;同样,这一点也适用于Kotlin方法参数传递,我们在将一个平台类型值传递给方法的一个不可空参数时,也会生成一个断言。

总体来说,Kotlin会竭尽所能防止null的赋值蔓延到程序的其他地方,而是在发生问题之处就立刻通过断言来解决。

注意:使用问号的声明方式,即:

 val userName: String? = user.name 

4、已映射类型

Kotlin 特殊处理一部分 Java 类型。这样的类型不是“按原样”从 Java 加载,而是 映射 到相应的 Kotlin 类型。 映射只发生在编译期间,运行时表示保持不变。

  • Java 的基础数据类型映射到相应的 Kotlin 类型

    Java 类型 Kotlin 类型
    byte kotlin.Byte
    short kotlin.Short
    int kotlin.Int
    long kotlin.Long
    char kotlin.Char
    float kotlin.Float
    double kotlin.Double
    boolean kotlin.Boolean
  • 一些非原生的内置类型也会作映射

    Java 类型 Kotlin 类型
    java.lang.Object kotlin.Any!
    java.lang.Cloneable kotlin.Cloneable!
    java.lang.Comparable kotlin.Comparable!
    java.lang.Enum kotlin.Enum!
    java.lang.Annotation kotlin.Annotation!
    java.lang.CharSequence kotlin.CharSequence!
    java.lang.String kotlin.String!
    java.lang.Number kotlin.Number!
    java.lang.Throwable kotlin.Throwable!
  • Java 的装箱原始类型映射到可空的 Kotlin 类型

    Java type Kotlin type
    java.lang.Byte kotlin.Byte?
    java.lang.Short kotlin.Short?
    java.lang.Integer kotlin.Int?
    java.lang.Long kotlin.Long?
    java.lang.Character kotlin.Char?
    java.lang.Float kotlin.Float?
    java.lang.Double kotlin.Double?
    java.lang.Boolean kotlin.Boolean?
  • Java 的数组按下文所述映射:

    Java 类型 Kotlin 类型
    int[] kotlin.IntArray!
    String[] kotlin.Array<(out) String>!

5、Java数组

Java 平台上,数组会使用原生数据类型以避免装箱/拆箱操作的开销。 由于 Kotlin 隐藏了这些实现细节,因此需要一个变通方法来与 Java 代码进行交互。 对于每种原生类型的数组都有一个特殊的类(IntArrayDoubleArrayCharArray 等等)来处理这种情况。 它们与 Array 类无关,并且会编译成 Java 原生类型数组以获得最佳性能。

假设有一个接受 int 数组索引的 Java 方法:

public class JavaArrayExample { 
   

    public void removeIndices(int[] indices) { 
   // 
        // 在此编码……
    }
}

在 Kotlin 中你可以这样传递一个原生类型的数组:

val javaObj = JavaArrayExample()
val array = intArrayOf(0, 1, 2, 3)// 构建一个int数组
javaObj.removeIndices(array)  // 将 int[] 传给方法

或者

val javaObj = JavaArrayExample()
val array = IntArray(10)// 构建一个大小为10的int数组
javaObj.removeIndices(array)  // 将 int[] 传给方法

这样声明的数组,还是代表的基础数据类型的数组,不会存在基本数据类型的装箱与拆箱操作,性能时非常高的。Kotlin提供了原生类型数组如下:

Java类型 Kotlin 类型
int[] IntArray!
long[] LongArray!
float[] FloatArray!
double[] DoubleArray!
char[] CharArray!
short[] ShortArray!
byte[] ByteArray!
boolean[] BooleanArray!
String[] Array<(out) String>!

6、Java 可变参数

Kotlin在调用Java中有可变参数的方法时,如果需要传递数组参数时,则需要使用展开运算符* 来传递数组参数:

public class User { 
   
  
  // 可变参数
    public void setChildren(String... childrenName) { 
   
        for (int i = 0; i < childrenName.length; i++) { 
   
            System.out.println("child name=" + childrenName[i]);
        }
    }
}
fun test2() { 
   
    val user = User()
    user.setChildren("tom") // 手动传一个参数

    user.setChildren("tom", "mike")// 传两个参数

    val nameArray = arrayOf("张三", "李四", "王五")
    // user.setChildren(nameArray) // 报错,无法通过编译
    user.setChildren(*nameArray)// 传数组参数
	  user.setChildren(null) // 传null也可以,
}

null也可以,在查看转换的Java代码时候可以看到,传null的时候,是创建了一个String数组,包含了一个null的元素而已,如:

@Test
public final void test2() { 
   
   User user = new User();
   user.setChildren(new String[]{ 
   (String)null});
}

7、受检异常

Kotlin 中,所有异常都是非受检的,这意味着编译器不会强迫你捕获其中的任何一个。 因此,当你调用一个声明受检异常的 Java 方法时,Kotlin 不会强迫你做任何事情:

public class User { 
   

    public void setChildren(String... childrenName) throws Exception { 
   
        for (int i = 0; i < childrenName.length; i++) { 
   
            System.out.println("child name=" + childrenName[i]);
        }
    }
}

fun test2() { 
   
    val user = User()
    user.setChildren("tom") // 编译可以通过
}

如果是Java调用setChildren方法的时候,需要用try catch捕获异常,或者向上抛出异常,否则无法通过编译,但Kotlin不会强制你捕获异常。

具体可以参考:浅谈Kotlin的Checked Exception机制

8、对象方法

Java 类型导入到 Kotlin 中时,类型 java.lang.Object 的所有引用都成了 Any。 而因为 Any 不是平台指定的,它只声明了 toString()hashCode()equals() 作为其成员, 所以为了能用到 java.lang.Object 的其他成员,Kotlin 要用到扩展函数

8-1、wait()/notify()

类型 Any 的引用没有提供 wait()notify() 方法。通常不鼓励使用它们,而建议使用 java.util.concurrent。 如果确实需要调用这两个方法的话,那么可以将引用转换为 java.lang.Object

(user as java.lang.Object).wait()

8-2、getClass(),获取类的Class对象

要取得对象的 Java 类,请在类引用上使用 java 扩展属性:

val intent1 = Intent(this, MainActivity::class.java)

也可以使用扩展属性:javaClass,如:

val intent2 = Intent(this, MainActivity.javaClass)

8-3、clone()

Any 基类是没有声明**clone()**方法的,如果想覆盖 clone(),需要继承 kotlin.Cloneable

class Example : Cloneable { 
   
    override fun clone(): Any { 
    …… }
}

8-4、finalize()

要覆盖 finalize(),所有你需要做的就是简单地声明它,而不需要 override 关键字:

class C { 
   
    protected fun finalize() { 
   
        // 终止化逻辑
    }
}

根据 Java 的规则,finalize() 不能是 private 的。

9、SAM 转换

9-1、SAM转换详解

这里首先介绍两个概念:

  • 函数式接口:只有一个抽象方法的接口叫函数式接口,也叫做:单一抽象方法接口

  • SAM:即 Single Abstract Method Conversions,字面意思为:单一抽象方法转换,即把 单一抽象方法接口 转成 lambda表达式 的过程叫做 单一抽象方法转换。

    函数式接口 可以用 lambda 表达式代替。

如在Android中,如果要为一个 View 设置一个点击监听事件,我们会这样做:

view.setOnClickListener(new View.OnClickListener() { 
   
    @Override
    public void onClick(View v) { 
   
        System.out.println("click");
    }
});

这其实就是给ViewsetOnClickListener方法传一个OnClickListener类型的匿名内部类的对象。

在Kotlin中,也可以通过匿名内部类实现类似的功能:

tvHello.setOnClickListener(object : View.OnClickListener { 
   
    override fun onClick(v: View?) { 
   
        println("click");
    }
})

但通过查看OnClickListener的源码可以看出,OnClickListener接口是一个 函数式接口,既然是一个函数式的接口,就可以使用带接口类型前缀的lambda表达式替代手动创建实现函数式接口的类。如:

view.setOnClickListener(View.OnClickListener { 
   
 	 System.out.println("click");
})

而通过 SAM 转换, Kotlin 可以将其签名与接口的单个抽象方法的签名匹配的任何 lambda 表达式转换为实现该接口的类的实例,所以上面代码通过SAM可以进一步简化为:

view.setOnClickListener({ 
   
 	 System.out.println("click");
})

又因为Kotlin高阶函数的特性,如果lambda表达式是一个方法的最后一个参数,则可以把lambda表达式移到方法的小括号外面,即:

view.setOnClickListener() { 
   
 	 System.out.println("click");
}

如果方法只有一个参数且是lambda表达式,则方法调用的小括号也可以省略,所以最终的调用方式可以是:

view.setOnClickListener { 
   
 	 System.out.println("click");
}

9-2、 SAM 转换的歧义消除

假设有这样一个Java类,声明了两个重载方法,参数都是一个 函数式接口,如:

public class SamInterfaceTest { 
   

  // 函数式接口1
    public interface SamInterface1 { 
   
        void doWork(int value);
    }

  // 函数式接口2
    public interface SamInterface2 { 
   
        void doWork(int value);
    }

    private SamInterface1 samInterface1;//
    private SamInterface2 samInterface2;

    public void setSamInterface(SamInterface1 samInterface1) { 
   
        this.samInterface1 = samInterface1;
    }

    public void setSamInterface(SamInterface2 samInterface2) { 
   
        this.samInterface2 = samInterface2;
    }
}

在Kotlin中通过SAM的方式去调用这个方法setSamInterface的时候,就会报错:

image-20210319201345600

原因就是 SamInterface1 和 SamInterface2 的唯一抽象方法的函数类型都是:(Int)->Unit,而把函数式接口进行 SAM 转换的话,lambda表达式的函数类型也是:(Int)->Unit,这就导致Kotlin编译器无法确定到底该调用哪个方法,即SAM转换产生了歧义。

虽然这种情况比较奇葩,但也不排除会遇到,这个时候就需要我们消除歧义,消除歧义的方法有如下三种:

  1. 带接口类型前缀的lambda表达式
  2. 把lambda表达式进行强转
  3. 实现接口的匿名类

代码实现如下:

fun testSam() { 
   
    val sam = SamInterfaceTest()
    // 方式1,带接口类型前缀的lambda表达式
    sam.setSamInterface(SamInterfaceTest.SamInterface1 { 
   
        println("do something 1")
    })
    // 方式2,把lambda表达式进行强转
    sam.setSamInterface({ 
   
        println("do something 2")
    } as SamInterfaceTest.SamInterface2)

    // 方式3,实现接口的匿名类
    sam.setSamInterface(object : SamInterfaceTest.SamInterface1 { 
   
        override fun doWork(value: Int) { 
   
            println("do something 3")
        }
    })
}

通过上面三种方式,就可以明确知道要调用哪个方法,从而消除歧义。

推荐使用:方式1,代码比较优雅,优雅很重要。

9-3、Kotlin函数式接口

Kotlin 1.4 之前,针对Java的函数式接口,Kotlin可以直接使用SAM转换,但对于 Kotlin 的函数式接口,却不能通过SAM转换,只能通过匿名内部类的方式实现接口参数的传递。

官方的解释是 Kotlin 本身已经有了函数类型高阶函数等支持,所以不需要了再去转换了。如果你想使用类似的需要用 lambda 做参数的操作,应该自己去定义需要指定函数类型的高阶函数。

如:Kotlin1.4之前:

image-20210323145829391

而在Kotlin 1.4(包含1.4)之后,Kotlin就开始支持函数式接口的SAM转换了,但对声明的接口有一定的限制,即接口必须使用 fun 关键字进行声明,如:

// 使用fun关键字,且接口只有一个抽象方法,这样的接口就是可以进行SAM转换的函数式接口
fun interface SamInterface { 
   
    fun test(value: Int)
  fun
}

针对Kotlin函数式接口的转换:

class SamInterfaceTestKt { 
   
    fun testSam(obj: SamInterface) { 
   
        print("$obj")
    }
}

// 测试
fun testKtSam(){ 
   
    val samKt = SamInterfaceTestKt()
    samKt.testSam(SamInterface { 
    // 带接口类型前缀的lambda表达式

    })

    samKt.testSam { 
   // lambda表达式

    }
}

所以在Kotlin1.4之后,不管是Java的函数式接口,还是Kotlin的函数式接口,都可以进行SAM转换了。

9-4、SAM转换限制

SAM 转换的限制主要有两点 :

  1. 只支持Java接口

    在Kotlin1.4之后,该限制就不存在了

  2. 只支持接口,不支持抽象类

    这个官方没有多做解释。我想大概是为了避免混乱吧,毕竟如果支持抽象类的话,需要做强转的地方就太多了。而且抽象类本身是允许有很多逻辑代码在内部的,直接简写成一个 Lambda 的话,如果出了问题去定位错误的难度也加大了很多。

10、在Kotlin中使用JNI

Kotlin使用external表示函数是native(C/C++)代码实现。

external fun foo(x: Int): Double

二、Java 调 Kotlin

1、属性

一个Kotlin属性会编译为3部分Java元素

  • 一个getter方法,名称通过加前缀 get 算出
  • 一个setter方法,名称通过加前缀 set 算出(只适用于 var 属性);
  • 一个私有的字段(field),其名字与Kotlin的属性名一样

如果Kotlin属性名以is开头,那么命名约定会发生一些变化:

  • getter方法与属性名一样
  • setter方法则是将is替换为set
  • 一个私有的字段,其名字与Kotlin的属性名一样

举例说明:

Kotlin类:

class TestField { 
   
    val age: Int = 18

    var userName: String = "Tom"

    var isStudent: String = "yes"
}

编译生成的字节码对应的Java文件:

public final class TestField { 
   
  // final 类型的属性,只有getter方法,没有setter方法
   private final int age = 18;
   @NotNull
   private String userName = "Tom";
   @NotNull
   private String isStudent = "yes";

   public final int getAge() { 
   
      return this.age;
   }

   @NotNull
   public final String getUserName() { 
   
      return this.userName;
   }

   public final void setUserName(@NotNull String var1) { 
   
      Intrinsics.checkNotNullParameter(var1, "<set-?>");
      this.userName = var1;
   }

   @NotNull
   public final String isStudent() { 
   
      return this.isStudent;
   }

   public final void setStudent(@NotNull String var1) { 
   
      Intrinsics.checkNotNullParameter(var1, "<set-?>");
      this.isStudent = var1;
   }
}

从上面的代码可以看出:

  • 通过val声明的属性,是final类型的,final 类型的属性,只有getter方法,没有setter方法
  • 以is开头的属性,getter方法与属性名一样,setter方法把is替换为set。注意:这种规则适用于任何类型,而不单单是Boolean类型
  • Kotlin属性,都会对应一个Java中的私有字段。

2、包级函数

2-1、基础介绍

我们知道,在Kotlin中,可以在Kotlin文件中声明一个,声明属性,声明方法,这些都是允许的。而Kotlin编译器在编译的时候,会生成一个Kotlin文件对应的Java类,类名是:Kotlin文件名+Kt

  • 文件中声明的属性,会变成该类中的静态私有属性,并提供getter和setter方法
  • 文件中声明的方法,会变成该类中的静态公有方法
  • 文件中声明的类,会生成对应的Java类

举例:

Kotlin文件KotlinFile.kt

package com.mei.ktx.test

/** * 在Kotlin文件中,声明一个类 */
class ClassInFile

/** * 在Kotlin文件中,声明一个方法 */
fun checkPhone(num: String): Boolean { 
   
    println("号码:$num")
    return true
}

/** * 在Kotlin文件中,声明一个变量 */
var appName: String = "KotlinTest"

Kotlin编译器生成的对应的Java类:KotlinFileKt.java

public final class KotlinFileKt { 
   
  
   // Kotlin文件中声明的属性,变成了Java类中的私有静态属性
   @NotNull
   private static String appName = "KotlinTest";

   // Kotlin文件中声明的方法,变成了Java类中的共有静态方法 
   public static final boolean checkPhone(@NotNull String num) { 
   
      Intrinsics.checkNotNullParameter(num, "num");
      String var1 = "号码:" + num;
      boolean var2 = false;
      System.out.println(var1);
      return true;
   }

   @NotNull
   public static final String getAppName() { 
   
      return appName;
   }

   public static final void setAppName(@NotNull String var0) { 
   
      Intrinsics.checkNotNullParameter(var0, "<set-?>");
      appName = var0;
   }
}

Kotlin文件中声明的类,会生成一个独立的Java类,名称不变。

public final class ClassInFile { 
   
}

所以Java调用Kotlin文件中的方法和属性的时候,需要通过对应的Java类名去调用:

public static void main(String[] args) { 
   
  // 通过类名直接调用
    KotlinFileKt.checkPhone("123456");
    System.out.println(KotlinFileKt.getAppName());
}

这里需要注意的是:Kotlin编译器自动生成的以Kt结尾的类,如:KotlinFileKt,是无法通过new关键字来创建对象的,因为在生成的字节码中没有构造方法的声明。

2-2、修改生成的类名

Kotlin文件所生成的Java类名,除了编译器默认生成的之外,还可以由自己指定,通过注解:@JvmName

如,Kotlin文件:

@file:JvmName("AppUtils") // 指定类名,需要在包名声明之前指定

package com.mei.ktx.test


/** * 在Kotlin文件中,声明一个类 */
class ClassInFile

/** * 在Kotlin文件中,声明一个方法 */
fun checkPhone(num: String): Boolean { 
   
    println("号码:$num")
    return true
}

/** * 在Kotlin文件中,声明一个变量 */
var appName: String = "KotlinTest"

生成的Java类为:AppUtils

public final class AppUtils { 
   
   @NotNull
   private static String appName = "KotlinTest";

   public static final boolean checkPhone(@NotNull String num) { 
   
      Intrinsics.checkNotNullParameter(num, "num");
      String var1 = "号码:" + num;
      boolean var2 = false;
      System.out.println(var1);
      return true;
   }

   @NotNull
   public static final String getAppName() { 
   
      return appName;
   }

   public static final void setAppName(@NotNull String var0) { 
   
      Intrinsics.checkNotNullParameter(var0, "<set-?>");
      appName = var0;
   }
}

注意:

  • 使用注解:@file:JvmName(“类名”)
  • 在文件包名声明之前,指定类名

2-3、类名冲突解决

通过上面介绍,我们知道可以为Kotlin文件指定类名,但如果多个相同包名下的Kotlin文件所指定的类名相同,这就会造成类重复定义,导致编译不过。这个时候就可以借助注解:@JvmMultifileClass ,把多个相同的类,合并成一个。

KotlinFile1.kt

@file:JvmName("LoginUtils")
@file:JvmMultifileClass

package com.mei.ktx.test

fun checkPwd(password: String): Boolean { 
   
    println("密码:$password")
    return true
}

KotlinFile2.kt

@file:JvmName("LoginUtils")
@file:JvmMultifileClass

package com.mei.ktx.test

fun checkName(phone: String): Boolean { 
   
    println("号码:$phone")
    return true
}

这样就没有冲突了,通过LoginUtils类就可以直接调用声明的方法:

public static void main(String[] args) { 
   
    LoginUtils.checkName("abc");
    LoginUtils.checkPwd("123456");
}

注意

  • 在指定相同的类名的Kotlin文件中,都要加入该注解:@file:JvmMultifileClass

  • 通常不建议自己指定类名。

3、实例字段

使用 @JvmField 注解对Kotlin中的属性进行标注时,表示它是一个实例字段(instance field),Kotlin编译器在编译的时候,就不会为这个属性生成对应的settergetter方法,但可以直接访问这个属性,相当于是这个属性被声明称:public 了。

Kotlin类:

class Person { 
   

    var name: String = "张三"

    @JvmField
    var age: Int = 18
}

Java使用:

public static void main(String[] args) { 
   
    Person person = new Person();
    System.out.println("name=" + person.getName() + ";age=" + person.age);
}

因为age被注解:@JvmField 修饰了,所以在Java类中,age字段就被当成时public类型的,可以自己访问,且没有生成对应的gettersetter方法。

从生成的Java类中也可以看出来:

public final class Person { 
   
   @NotNull
   private String name = "张三";
  
   @JvmField
   public int age = 18; // 共有属性

   @NotNull
   public final String getName() { 
   
      return this.name;
   }

   public final void setName(@NotNull String var1) { 
   
      Intrinsics.checkNotNullParameter(var1, "<set-?>");
      this.name = var1;
   }
}

使用限制:如果一个属性有幕后字段(backing field)、非私有、没有 open /override 或者 const 修饰符并且不是被委托的属性,那么你可以用 @JvmField 注解该属性

感觉没啥用。

4、静态字段

4-1、Kotlin静态字段声明

Kotlin静态字段声明:在具名对象或伴生对象中声明的 Kotlin 属性,就是静态字段。它会在该具名对象或包含伴生对象的类中具有静态幕后字段

如:伴生对象

class Person { 
   
    companion object { 
   
        var aliasName = "人" // 声明的静态字段
    }
}

对应的Java类:

public final class Person { 
   
   private static String aliasName;
   @NotNull
   public static final Person.Companion Companion = new Person.Companion((DefaultConstructorMarker)null);

   public static final class Companion { 
   

      @NotNull
      public final String getAliasName() { 
   
         return Person.aliasName;
      }

      public final void setAliasName(@NotNull String var1) { 
   
         Intrinsics.checkNotNullParameter(var1, "<set-?>");
         Person.aliasName = var1;
      }
   }
}

从上面的Java代码也可以看出,这样声明的静态字段,是私有的静态字段,在Java中调用使用这样的静态字段,需要通过生成的伴生类:Companion 对象去获取和赋值,因为自动有生成gettersetter方法。

public static void main(String[] args) { 
   
  	System.out.println("alias=" + Person.Companion.getAliasName());
}

4-2、静态字段公有化

通过上面声明的静态字段,默认是私有的静态字段,但我们可以通过如下方法,将私有字段变为公有字段:

  • 使用@JvmField 注解 修饰字段
  • 使用 lateinit 修饰符 修饰字段
  • 使用 const 修饰符 修饰字段

如:

class Person { 
   
    companion object { 
   
      // 使用const修饰
        const val TAG = "Person"

      // 使用lateinit修饰
        lateinit var aliasName: String

      // 使用注解
        @JvmField
        var age: Int = 18
    }
}

通过上面三种方式修饰的静态字段,都是公有的静态字段,这个时候访问的时候,就可以直接通过类名去访问,不需要借助伴生类:Companion去访问。如:

 public static void main(String[] args) { 
   
     System.out.println("alias=" + Person.TAG);
     Person.aliasName = "人";
     System.out.println("alias=" + Person.aliasName);
     System.out.println("alias=" + Person.age);

     System.out.println("alias=" + Person.Companion.getAliasName());
 }

通过lateinit修饰的静态字段,虽然是公有的静态字段,但在伴生对象中,还是会生成对应的setter和getter方法。

区别:

  • const和**@JvmField**修饰的静态字段,无法通过伴生对象访问,也不会生成对应的setter和getter方法。
  • lateinit修饰的静态字段,可以通过伴生对象访问,也可以直接通过类名访问,且伴生类中还会生成对应的setter和getter方法。

5、静态方法

如上所述,Kotlin 将包级函数表示 为静态方法。

Kotlin 在具名对象或伴生对象中定义的函数,默认情况下不是静态的,如果想声明一个静态的函数,则可以用 @JvmStatic 注解修饰方法,这样声明的方法就是静态方法。

调用方式:

  • 可以直接通过类名调用
  • 也可以通过伴生对象调用。

Kotlin中,在伴生对象中声明静态方法:

class Person { 
   

    companion object { 
   

        fun notStaticMethod() { 
   
            println("不是静态方法")
        }

        @JvmStatic
        fun staticMethod() { 
   
            println("是静态方法")
        }
    }
}

上面代码,在伴生对象中声明了两个方法,通过注解:@JvmStatic 修饰的是静态方法,在Java中可以直接通过类名调用,也可以通过伴生对象调用。

@Test
public void test2(){ 
   
    Person.Companion.staticMethod(); // 通过伴生对象调静态方法
    Person.staticMethod();// 通过类名调用静态方法
    Person.Companion.notStaticMethod();// 通过版本对象,调用非静态方法
}

@JvmStatic 注解也可以应用于对象或伴生对象的属性,使得该属性在该类中也有静态的 getter 和 setter 方法。

6、签名冲突

通过注解:@JvmName,可以解决函数签名冲突的问题。

6-1、泛型檫除导致的签名冲突

最突出的例子是由于类型擦除引发的:

fun List<String>.filterValid(): List<String> { 
   
    return arrayListOf("hello", "world")
}

fun List<Int>.filterValid(): List<Int> { 
   
    return arrayListOf(1, 2, 3)
}

在Kotlin文件中,定义上面两个扩展函数,是无法通过编译的,会提示报错:

image-20210317201523211

即Kotlin在编译成字节码的时候,泛型会被檫除,导致两个方法在JVM看来,方法签名是一样的,都是:filterValid(Ljava/util/List;)Ljava/util/List;

这样JVM会认为这两个方法是同一个方法,但却被重复定义了。

解决办法是,通过注解 @JvmName 给方法重新指定一个名字,如:

fun List<String>.filterValid(): List<String> { 
   
    return arrayListOf("hello", "world")
}

@JvmName("filterValidInt") // 重新指定方法名称
fun List<Int>.filterValid(): List<Int> { 
   
    return arrayListOf(1, 2, 3)
}
val list=list(1,2,3)
list.

这样就可以编译通过了。

在 Kotlin 中它们可以用相同的名称 filterValid 来访问,而在 Java 中,它们分别是 filterValidfilterValidInt

Java中调用:

@Test
public void test3() { 
   
    List<String> stringList = new ArrayList<>();
    System.out.println(ListExternalKt.filterValid(stringList));
    List<Integer> integerList = new ArrayList<>();
    System.out.println(ListExternalKt.filterValidInt(integerList));
}

Kotlin中调用:

fun main() { 
   
    val stringList = arrayListOf<String>()
    println(stringList.filterValid())

    val intList = arrayListOf<Int>()
    println(intList.filterValid())// Kotlin调用的时候,直接就可以用方法名,而不是用重定义的方法名
}

输出:

image-20210317202535691

6-2、属性的getter和setter方法与类中的现有方法冲突

同样的技巧也适用于属性 x 和函数 getX() 共存:

val x: Int
    @JvmName("getXValue")
    get() = 15

fun getX() = 10

如需在没有显式实现 getter 与 setter 的情况下更改属性生成的访问器方法的名称,可以使用**@get:JvmName** 与 @set:JvmName

class Person { 
   
    @get:JvmName("getXValue")
    @set:JvmName("setXValue")
    var x: Int = 20
}

Java中调用:

@Test
public void test3() { 
   
    Person person = new Person();
    person.setXValue(20);
}

7、生成重载

通常,如果你写一个有默认参数值的 Kotlin 函数,在Kotlin编译器生成的字节码中,只会有这么一个完整参数的方法,则Java调用这个方法的时候,需要传完整的参数,不可缺少。

如:Kotlin中定义了一个 Fruit 类,有一个两个参数的主构造函数,其中有一个参数有默认值

class Fruit constructor(var name: String, var type: Int = 1) { 
    // 有默认参数的构造函数

  // 有默认参数的方法
    fun setFuture(color: String, size: Int = 1) { 
   
        
    }

}

如果是在Kotlin中创建这个Fruit对象,则可以只传一个参数,默认参数可以不传。

但如果是在Java中创建这个Fruit对象,则两个参数都必须传,因为Java是不支持默认参数的。Fruit生成的字节码中,也只有这一个构造函数。如:

image-20210317205850874

只传一个参数的话,Java编译不通过。

那可不可以让编译器帮我们生成多个重载的方法,当然是可以的。即可以使用 @JvmOverloads 注解来实现。

如:

给方法增加**@JvmOverloads**注解:

class Fruit @JvmOverloads constructor(var name: String, var type: Int = 1) { 
   

    @JvmOverloads
    fun setFuture(color: String, size: Int = 1) { 
   

    }
}

这个时候在Java中创建Fruit对象,调用setFuture方法,都可以只传一个参数了:

image-20210317210141719

本质原因是,Kotlin编译器在生成字节码的时候,为增加了 @JvmOverloads注解的方法,增加了多个重载方法,如查看Fruit类的Java代码如下:

public final class Fruit { 
   
   @NotNull
   private String name;
   private int type;

  // 两个参数的setFuture方法
   @JvmOverloads
   public final void setFuture(@NotNull String color, int size) { 
   
      Intrinsics.checkNotNullParameter(color, "color");
   }

   // $FF: synthetic method
   public static void setFuture$default(Fruit var0, String var1, int var2, int var3, Object var4) { 
   
      if ((var3 & 2) != 0) { 
   
         var2 = 1;
      }

      var0.setFuture(var1, var2);
   }

  // 一个参数的setFuture方法
   @JvmOverloads
   public final void setFuture(@NotNull String color) { 
   
      setFuture$default(this, color, 0, 2, (Object)null);
   }


  // 两个参数的构造函数
   @JvmOverloads
   public Fruit(@NotNull String name, int type) { 
   
      Intrinsics.checkNotNullParameter(name, "name");
      super();
      this.name = name;
      this.type = type;
   }

   // $FF: synthetic method
   public Fruit(String var1, int var2, int var3, DefaultConstructorMarker var4) { 
   
      if ((var3 & 2) != 0) { 
   
         var2 = 1;
      }

      this(var1, var2);
   }

  // 一个参数的构造函数
   @JvmOverloads
   public Fruit(@NotNull String name) { 
   
      this(name, 0, 2, (DefaultConstructorMarker)null);
   }
}

正是因为Kotlin编译器帮我们生成了对用的重载方法,我们才可以调用。

8、受检异常

我们知道,Kotlin是没有受检异常的,所以Kotlin 函数的 Java 签名不会声明抛出异常。 于是如果我们有一个这样的 Kotlin 函数:

FileUtils文件:

fun writeToFile() { 
   
    println("写入文件")
    throw IOException() // 在Kotlin方法中,抛出了一个IO异常
}

然后我们想要在 Java 中调用它并捕捉这个异常:

image-20210317212415434

如果我们尝试去捕获这个IO异常,Java就会报错。原因是 writeToFile()未在 throws 列表中声明 IOException。所以在调用这个方法的时候,不能捕获到IOException

为了解决Kotlin异常无法向上抛的问题,Kotlin提供了注解:@Throws 来解决这个问题

使用如下:

给需要向上抛异常的方法,增加**@Throws注解,并在注解上指明异常的类型,这里的类型是KClass**类型。

@Throws(IOException::class)
fun writeToFile() { 
   
    println("写入文件")
    throw IOException() // 在Kotlin方法中,抛出了一个IO异常
}

通过注解**@Throws**,就可以把异常向上抛了,这样在Java调用Kotlin方法的时候,就可以捕获对应的异常了,如:

image-20210317213141629

增加注解后,Java可以正常捕获到IOException异常了。

9、空安全

Java调用Kotlin函数时,无法防止将null作为非空参数传递给函数。所以Kotlin为所有期望非空参数的public函数生成运行时检查。这样会在Java代码中立即出现NullPointerException异常。

fun checkPhone(num: String): Boolean { 
   
    println("号码:$num")
    return true
}

Kotlin中的checkPhone方法,参数是非空类型的,在Java中调用这个方法时,如果传一个null的话,在运行时就会报空指针异常,如:

image-20210318104121651

可以看到运行的时候,就报异常了。同时,在编译器也给我们提醒了,当传null的时候,报黄了。

如果Kotlin方法定义的时候,参数声明为可空类型,那么在Java中调用的时,传一个null,运行时就不会报空指针异常了:

// 参数声明为可空类型
fun checkPhone(num: String?): Boolean { 
   
    println("号码:$num")
    return true
}

三、Android KTX使用

1、简述

Android KTX 是包含在 Android Jetpack 及其他 Android 库中的一组 Kotlin 扩展程序。KTX 扩展程序可以为 JetpackAndroid 平台及其他 API 提供简洁的惯用 Kotlin 代码。为此,这些扩展程序利用了多种 Kotlin 语言功能,其中包括:

  • 扩展函数
  • 扩展属性
  • Lambda
  • 命名参数
  • 参数默认值
  • 协程

通过KTX中的扩展API,可以帮助我们用更少的代码实现复杂的功能,就像是工具类一样,帮助我们减少了重复代码的编写,而只需要关注自己的核心代码实现。

例如:通常使用 SharedPreferences 时,您必须先创建一个编辑器,然后才能对偏好设置数据进行修改。在完成修改后,您还必须应用或提交这些更改,如以下示例所示:

sharedPreferences
        .edit()  // create an Editor
        .putBoolean("key", value)
        .apply() // write to disk asynchronously

其实对于开发者来说,获取 Editor 对象,最后的提交操作,对于每一次存/取来说,都是重复的操作,冗余的代码,对开发者应该屏蔽才对。真正需要关心的是存/取操作。

那么这些代码可不可以省略不写呢?当然可以,在Java中,我们就会通过封装一个工具类,来执行这些存/取操作,一行代码就搞定。

而在Kotlin中,就可以使用Google提供的 KTX库来实现,如:

sharedPreferences.edit { 
    putBoolean("key", value) }

上面的edit方法,是Android KTX Core 库中,为SharedPreferences增加的扩展函数,在调用这个扩展函数的时候,需要传一个lambda表达式,在这个lambda表达式中,就可以直接调用Editor类中的put*相关方法,进行数据的保存而不用关心其他的任何操作,这即节省了代码又提高了开发效率。

下面看一下Android KTX Core库中,为SharedPreferences增加的扩展函数edit的源码:

@SuppressLint("ApplySharedPref")
inline fun SharedPreferences.edit(
    commit: Boolean = false, // 是否通过commit方法提交数据,默认通过apply方法提交数据
    action: SharedPreferences.Editor.() -> Unit // 表达式
) { 
   
    val editor = edit()// 获取Editor对象
    action(editor) // 执行lambda表达式,即执行用户的代码
    if (commit) { 
   
        editor.commit()// 提交数据
    } else { 
   
        editor.apply()
    }
}

通过上面的源码可以看出,edit方法,帮我们实现了需要重复编写的代码,让开发者只关注于自己的功能实现,从而减少代码量并提升效率。

2、项目中使用Android KTX

上面的针对SharedPreferences的扩展函数,定义在Android KTX Core核心库中,而Google针对不同的功能库,都提供了不同的扩展库,以更好的服务各个功能库,如:

扩展库名称 依赖 描述
Core KTX implementation “androidx.core:core-ktx:1.3.2” 核心扩展库
Collection KTX implementation “androidx.collection:collection-ktx:1.1.0” 集合扩展库
Fragment KTX implementation “androidx.fragment:fragment-ktx:1.3.1” Fragment扩展库
Lifecycle KTX implementation “androidx.lifecycle:lifecycle-runtime-ktx:2.3.0” 声明周期扩展库
LiveData KTX implementation “androidx.lifecycle:lifecycle-livedata-ktx:2.3.0” LiveData扩展库
ViewModel KTX implementation “androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0” ViewModel扩展库

上面列举了一些常用的扩展库,还有其他扩展库没有列举出来,如果想要查看的话,可以去官网查看:Android KTX

下面介绍一些常用扩展库的常规用法:

2-1、Android KTX Core 核心库

上面的针对SharedPreferences的扩展函数,定义在Android KTX Core核心库中,如果需要在项目中使用,则需要在module工程的build.gradle文件中,添加依赖:

dependencies { 
   
    implementation "androidx.core:core-ktx:1.3.2"
}

引入该库之后,就可以使用相关的扩展API了。

(1)、动画相关

image-20210318162913554

针对动画的监听增加了一些扩展函数,避免了实现接口和实现方法,使用如下:

fun startAnimation() { 
   
    val animation = ValueAnimator.ofFloat(0f, 360f)
        .setDuration(500)

    animation.doOnCancel { 
    // 监听取消回调

    }
    animation.doOnEnd { 
   // 监听动画的结束

    }
    animation.start()
}

个人感觉比较鸡肋,每个扩展函数都会为动画增加一个监听对象,比如上面调用了两个扩展函数,就给animation对象增加了两个监听对象,感觉不划算,还增加了回调的成本。

(2)、Context相关

方法列表:https://developer.android.com/kotlin/ktx/extensions-list?hl=zh-cn#androidxcorecontent

比较实用的:解析自定义属性

Context.withStyledAttributes(set: AttributeSet? = null, attrs: IntArray, @AttrRes defStyleAttr: Int = 0, @StyleRes defStyleRes: Int = 0, block: TypedArray.() -> Unit)

使用:

class CusTextView @JvmOverloads constructor(
    context: Context?,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : TextView(context, attrs, defStyleAttr) { 
   

    init { 
   
      // 通过这个方法,可以在lambda表达式中,直接解析自定义属性,挺实用的
        context?.withStyledAttributes(attrs, R.styleable.ActionBar) { 
   
            cusBgColor = getColor(R.styleable.CusTextView_cusBgColor, 0)
        }
    }
}

通过ContextwithStyledAttributes方法,可以在lambda表达式中,直接解析自定义属性,挺实用的。

(3)、Canvas 相关

扩展函数 功能描述
Canvas.[withClip](https://developer.android.com/reference/kotlin/androidx/core/graphics/package-summary?hl=zh-cn#(android.graphics.Canvas).withClip(android.graphics.Rect, kotlin.Function1))(clipRect: Rect, block: Canvas.() -> Unit) 按照指定的大小,裁剪画布,在执行block之前,
1. 先调用Canvas.save和Canvas.clip方法,
2. 接着调用block,
3. 最后执行Canvas.restoreToCount方法
相当于Kotlin编译器帮我们做了画布的保存裁剪与恢复操作,开发者只需要关心绘制就好
Canvas.withRotation 旋转画布,然后执行block绘制,最后恢复画布状态。
Canvas.withScale 缩放画布,然后执行block绘制,最后恢复画布状态。
Canvas.withTranslation 平移画布,然后执行block绘制,最后恢复画布状态。
Canvas.withSkew 斜拉画布,然后执行block绘制,最后恢复画布状态。
Canvas.withSave 保存原图层,然后执行block绘制,最后恢复画布状态
Canvas.withMatrix 画布执行举证变换,然后执行block绘制,最后恢复画布状态

Canvas这一系列的扩展函数,帮我们省去了画布的状态保存和恢复,并执行相应的操作,让开发者只关注于绘制本身,非常实用。

class CusTextView @JvmOverloads constructor(
    context: Context?,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : TextView(context, attrs, defStyleAttr) { 
   

    override fun onDraw(canvas: Canvas?) { 
   
        super.onDraw(canvas)
      // 把画布先裁剪成一个大小为100的正方形,然后给这个正方形绘制一个绿色的背景色,我们只关注绘制颜色本身,而不用去管画布
      // 的裁剪,画布状态的保存与恢复
        canvas?.withClip(Rect(0, 0, 100, 100)) { 
   
            drawColor(Color.GREEN)
        }
    }
}

上面代码中,把画布先裁剪成一个大小为100的正方形,然后给这个正方形绘制一个绿色的背景色,我们只关注绘制颜色本身,而不用去管画布的裁剪,画布状态的保存与恢复,使用起来非常的简单。

在自定义View的时候,这些方法帮助很大。

(4)、SparseArray集合

KTX Core 为SparseArray相关的类,增加了很多的扩展函数,如:

  • 遍历元素:SparseArray.forEach(action: (key: Int, value: T) -> Unit)

  • 获取元素,有默认值:SparseArray.[getOrDefault](https://developer.android.com/reference/kotlin/androidx/core/util/package-summary?hl=zh-cn#(android.util.SparseArray).getOrDefault(kotlin.Int, androidx.core.util.android.util.SparseArray.getOrDefault.T))(key: Int, defaultValue: T)

  • 集合判空:SparseLongArray.isEmpty()

如:

fun test() { 
   
    val map = SparseArray<String>()
    if (map.isNotEmpty()) { 
   
      map.forEach { 
    key, value ->
          println("key=$key,value=$value")
      }
    }
}

通过扩展函数,很方便的就可以便利SparseArray集合。

(5)、View和ViewGroup

View的扩展函数:

ViewGroup扩展函数:

其他的就不介绍了,大家可以去看官网,有详细的列举出每个包下的扩展API,忘记的时候也可以去查看一下,官网地址为:

KTX扩展API列表

今天的文章Kotlin与Java的相互调用详解分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/29721.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注