Kotlin与Java 混合编程
Kotlin与Java的类型映射
Kotlin 对 Java基本数据类型的映射
Kotlin 特殊处理一部分 Java 类型。这样的类型不是“按原样”从 Java 加载,而是 映射 到相应的 Kotlin 类型。 映射只发生在编译期间,运行时表示保持不变。
Java 的原生类型映射到相应的 Kotlin 类型:
Kotlin 对 Java 包装类型的映射
Java包装类是对Java基本数据类型的包装,Java包装类可以有空值,所以映射到Kotlin数据类型时是可空类型。
Kotlin 对 Java 常用类型的映射
Java 声明的类型在 Kotlin 中会被特别对待并称为”平台类型”。Kotlin语法中并没有平台类型的表示方式,但是在IntelliJ IDEA等IDE工具或一些文档中采用“数据类型!”方式表示。
比如String!表示法被Kotlin编译器用来表示来自Java代码的平台类型。你不能在自己的代码中使用这样的语法。而且感叹号只是强调类型的可空性是未知的。
Java常用类是位于java.lang中一些核心类,它们映射到Kotlin数据类型时是非空或可空类型。
Kotlin 对 Java 集合类型的映射
Java集合类型是映射到Kotlin数据类型如下表所示。
从表中可见Java集合不区分只读和可变类型,而Kotlin中有这样的区别。
在表中还有一种平台类型,在混合编程时Kotlin将它们看作可空或非空类型。
例如:
平台类型(Mutable)Iterator
(Mutable)Collection
示例:
Kotlin 对 Java 数组的映射
与 Java 不同,Kotlin 中的数组是不型变的。
这意味着 Kotlin 不允许我们把一个 Array
Kotlin 也禁止我们把一个子类的数组当做超类的数组传递给 Kotlin 的方法,
但是对于 Java,这是允许的(通过 Array<(out) String>! 这种形式的平台类型)。
在Java 平台上,数组会使用原生数据类型以避免装箱/拆箱操作的开销。
由于 Kotlin 隐藏了这些实现细节,因此需要一个变通方法来与 Java 代码进行交互。
对于每种原生类型的数组都有一个特化的类(IntArray、 DoubleArray、 CharArray 等等)来处理这种情况。
它们与 Array 类无关,并且会编译成 Java 原生类型数组以获得最佳性能。
Array<(out) T>! 表示“可空或者不可空的 T(或 T 的子类型)的 Java 数组”。
例子:
Kotlin 对 Java 泛型的映射
当带有泛型参数的 Java 类型导入 Kotlin 时,会执行一些转换:
- Java 的通配符转换成类型投影
- Foo<? extends Bar> 转换成 Foo<out Bar!>!,
- Foo<? super Bar> 转换成 Foo<in Bar!>!;
- Java的原始类型转换成星投影
- List 转换成 List<*>!,即 List<out Any?>!
和 Java 一样,Kotlin 在运行时不保留泛型,即对象不携带传递到他们构造器中的那些类型参数的实际类型。
例如 ArrayList
这使得执行 is-检测不可能照顾到泛型。 Kotlin 只允许 is-检测星投影的泛型类型:
if (a is List<Int>) // 错误:无法检查它是否真的是一个 Int 列表
// but
if (a is List<*>) // OK:不保证列表的内容
在Kotlin中调用Java
避免Kotlin关键字
在Java代码中给标识符命名时可能没有考虑到哪些Kotlin的关键字。
但当在Kotlin中调用这样的Java代码时,则需要将这些关键字用反引号括起来。
public class MyJavaClass {
public static MyJavaClass object = new MyJavaClass();
@Override
public String toString() {
return "MyJavaClass{}";
}
}
fun main(args: Array<String>) {
val obj = MyJavaClass.`object` //避免Kotlin关键字object
println(obj)
}
异常检查
Kotlin和Java在异常检查上有很大的不同,Java有受检查异常,而Kotlin中没有受检查异常。
那么当Kotlin调用Java中的一个函数时,这个函数声明抛出异常时,在Kotlin中不用必须捕获。
Java代码需要捕获异常:
try {
BufferedReader br = new BufferedReader(new FileReader("abc"));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Kotlin代码不需要捕获异常:
val br = BufferedReader(FileReader("abc"))
调用Java函数式接口
在Java函数式接口是只有一个抽象函数的接口,也简称SAM(Single Abstract Method缩写)。
在Kotlin中调用Java函数式接口非常的简洁,形式是“接口名{…}”。
//可计算接口
@FunctionalInterface
public interface Calculable {
// 计算两个int数值
int calculateInt(int a, int b);
}
val n1 = 10
val n2 = 5
// 实现加法计算Calculable对象
val f1 = Calculable { a, b -> a + b }
// 实现减法计算Calculable对象
val f2 = Calculable { a, b -> a - b }
// 调用calculateInt函数进行加法计算
println("$n1 + $n2 = ${f1.calculateInt(n1, n2)}")
// 调用calculateInt函数进行减法计算
println("$n1 - $n2 = ${f2.calculateInt(n1, n2)}")
Java可变参数
Java 类中声明一个具有可变数量参数(varargs)的方法:
public class JavaArrayExample {
public void removeIndicesVarArg(int... indices) {
//TODO
}
}
在Kotlin中,需要使用展开运算符 * 来传递 IntArray
val javaObj = JavaArrayExample()
val array = intArrayOf(0, 1, 2, 3)
javaObj.removeIndicesVarArg(*array)
目前Kotlin无法传递 null 给一个声明为可变参数的方法。
详细参考:Kotlin 中调用 Java
Java调用Kotlin
访问Kotlin属性
Kotlin一个属性对应Java中一个私有字段、一个setter函数和一个getter函数。(如果是只读属性的则没有setter函数。)
那么Java访问Kotlin的属性这是通过这些getter函数和setter函数。
/**
* var声明的属性会生成setter和getter函数,如果是val声明的属性是只读的,只生成getter函数。
*/
data class User(var name: String, var password: String)
User user = new User("Tom", "12345");
System.out.println(user.getName()); //Tom
user.setPassword("54321");
System.out.println(user.getPassword());//54321
访问包级别成员
在同一个Kotlin文件中,那些顶层属性和函数,包括顶层扩展属性和函数都不属于某个类,但它们属于该Kotlin文件中定义的包。
在Java中访问它们时,把它们当成静态成员。
@file:JvmName("PackageLevel")
package level
//顶层属性
val area = 100.0
//顶层函数
fun rectangleArea(width: Double, height: Double): Double {
val area = width * height
return area
}
//访问顶层函数
Double area = PackageLevel.rectangleArea(320.0, 480.0);
System.out.println(area);
//访问顶层属性
System.out.println(PackageLevel.getArea());
实例字段、静态字段和静态函数
Java语言中所有的变量和函数都封装到一个类中,类中包括实例函数、实例字段、静态字段和静态函数。
Java实例函数就是Kotlin类中声明的函数,而Java中的实例字段、静态字段和静态函数,Kotlin也是支持的。
实例字段
如果需要以Java实例字段形式(即:实例名.字段名)访问Kotlin中的属性,则需要在该属性前加@JvmField注解,表明该属性被当做Java中的字段使用,可见性相同。
如果一个属性有幕后字段(backing field)、非私有、没有 open /override 或者 const 修饰符并且不是被委托的属性,那么你可以用 @JvmField 注解该属性。
另外,延迟初始化(lateinit)属性在Java中当做字段使用,可见性相同。
class Person {
// 名字
@JvmField
var name = "Tony"
// 年龄
var age = 18
// 出生日期
lateinit var birthDate: Date
}
kt.Person p= new kt.Person();
System.out.println(p.name); //Tony
System.out.println(p.age); // 'age' has private access in 'kt.Person'
System.out.println(p.birthDate); //null
静态字段
如果需要以Java静态字段形式(即:类名.字段名)访问Kotlin中的属性,可以有两种实现方式:
- 属性声明为顶层属性,Java中将所有的顶层成员(属性和函数)都认为是静态的。
- 在Kotlin的声明对象和伴生对象中定义属性,这些属性需要使用@JvmField注解、lateinit或const来修饰
@file:JvmName("StaticField")
import java.util.*
object Singleton { //Singleton声明对象
@JvmField
val x = 10 //@JvmField注解Singleton对象中x属性
lateinit var birthDate: Date //声明延迟属性birthDate
}
class Account { //Account伴生对象
companion object {
//声明伴生对象中interestRate属性是const常量类型
const val interestRate = 0.018
}
}
//声明顶层常量MAX_COUNT
const val MAX_COUNT = 500
//访问静态字段
System.out.println(Singleton.x); //10
Singleton.birthDate = new Date();
System.out.println(Account.interestRate); //0.018
System.out.println(StaticField.MAX_COUNT); //500
静态函数
如果需要以Java静态函数形式(即:类名.函数名)访问Kotlin中的函数,可以有两种实现方式:
- 函数声明为顶层函数
- 在Kotlin的声明对象和伴生对象中定义函数,这些函数需要使用@JvmStatic来修饰。
@file:JvmName("StaticField")
import java.util.*
object Singleton { //Singleton声明对象
@JvmField
val x = 10 //@JvmField注解Singleton对象中x属性
lateinit var birthDate: Date //声明延迟属性birthDate
//@JvmStatic注解Singleton对象中displayX函数
@JvmStatic
fun displayX() {
println(x)
}
}
class Account { //Account伴生对象
companion object {
//声明伴生对象中interestRate属性是const常量类型
const val interestRate = 0.018
//JvmStatic注解伴生对象中interestBy函数
@JvmStatic
fun interestBy(amt: Double): Double {
return interestRate * amt
}
}
}
//声明顶层常量MAX_COUNT
const val MAX_COUNT = 500
//访问静态字段
System.out.println(Singleton.x); //10
Singleton.birthDate = new Date();
System.out.println(Account.interestRate); //0.018
System.out.println(StaticField.MAX_COUNT); //500
//访问静态函数
Singleton.displayX();
Account.interestBy(5000);
从Kotlin 1.3开始,@JvmStatic也适用于接口的伴随对象中定义的函数。
这些函数编译为接口中的静态方法。请注意,接口中的静态方法是在Java 1.8中引入的。
kt.ChatBot.greet("Tom");
ChatBot.greet("Tom");
public interface ChatBot {
//Java 1.8 接口里可以声明静态方法,并且可以实现。
static void greet(String username){
System.out.println("Hello, "+username);
}
}
package kt
interface ChatBot {
companion object {
@JvmStatic fun greet(username: String) {
println("Hello, $username")
}
}
}
可见性
在Java中可见性有:private、protected、default(包私有)和 public。
在 Kotlin 中有这四个可见性修饰符:private、 protected、 internal(内部可见) 和 public。
如果没有显式指定修饰符的话,默认可见性是 public。
Java和Kotlin都有4种可见性,但Kotlin中没有默认包私有可见性,而Java中没有内部可见性。
Java和Kotlin除了public完全兼容外,其他的可见性都是有所区别的。详细解释说明如下:
Kotlin私有可见性
由于Kotlin私有可见性可以声明类,类中成员,也可以声明顶层成员。
那么映射到Java分为三种情况:
- Kotlin类中私有成员映射到Java类中私有实例成员。
- Kotlin中私有顶层成员映射到Java中私有静态成员。
- Kotlin中声明的私有类映射到Java中的包私有类。
private class Person2 { val name: String = "Jack" }
public class PersonTest { public static void main(String[] args) { Person2 person2 = new Person2(); System.out.println(person2.getName());//Jack } }
Kotlin内部可见性
由于Java中没有内部可见性,那么在相同的模块下,Kotlin内部可见性映射为Java公有可见性。但不同模块下无法访问。
Java代码访问Kotlin代码:
Kotlin代码访问Kotlin代码:
Kotlin保护可见性
Kotlin保护可见性映射为Java保护可见性。
Kotlin公有可见性
Kotlin公有可见性映射为Java公有可见性。
Kotin类声明:
package kt.bean
// 员工类
internal class Employee {
internal var no: Int = 10 // 内部可见性Java端可见
protected var job: String? = null // 保护可见性Java端子类继承可见
private var salary: Double = 0.0 // 私有可见性Java端不可见
set(value) {
if (value >= 0.0) field = value
}
lateinit var dept: Department // 公有可见性Java端可见
}
// 部门类,open可以被继承
open class Department {
protected var no: Int = 0 // 保护可见性Java端子类继承可见
var name: String = "" // 公有可见性Java端可见
}
internal const val MAX_COUNTS = 500 // 内部可见性Java端可见
private const val MIN_COUNTS = 0 // 私有可见性Java端不可见
Java代码访问:
Employee emp = new Employee();
//访问Kotlin中内部可见性的Employee成员属性no
int no = emp.getNo$share_main();//虽然能编译通过,运行时报错:“java:找不到符号”
Department dept = new Department();
//访问Kotlin中公有可见性的Department成员属性name
dept.setName("市场部");
//访问Kotlin中公有可见性的Employee中成员属性dept
emp.setDept(dept);
System.out.println(emp.getDept());
//访问Kotlin中内部可见性的顶层属性MAX_COUNTS
System.out.println(EmployeeKt.MAX_COUNTS);
注意:Kotlin中内部可见性类成员,会生成比较复杂的函数名字,在IDE工具中这个函数语法存在,但是编译是无法通过。
这源自于Kotlin内部可见性与Java可见性的兼容问题,事实上在目前的Kotlin这个版本上Java不能访问Kotlin内部可见性类成员,但可以访问内部可见性的类和内部可见性是顶层成员。
生成重载函数
Kotlin的函数参数可以设置默认值,看起来像多个函数重载一样。但Java中并不支持参数默认值,只能支持全部参数函数。
为了解决这个问题可以在Kotlin函数前使用@JvmOverloads注解,Kotlin编译器会生成多个重载函数。
@JvmOverloads注解的函数可以是:构造函数、成员函数和顶层函数,但不能是抽象函数。
package over.load
class Animal @JvmOverloads constructor(
val age: Int,
val sex: Boolean = false
)
class DisplayOverloading {
@JvmOverloads
fun display(c: Char, num: Int = 1) {
println(c + " " + num)
}
}
@JvmOverloads
fun makeCoffee(type: String = "卡布奇诺"): String {
return "制作一杯${type}咖啡。"
}
//Animal构造函数添加@JvmOverloads注解,它会生成两个Java重载构造函数
Animal animal1 = new Animal(10, true);
Animal animal2 = new Animal(10);
DisplayOverloading dis1 = new DisplayOverloading();
//DisplayOverloading声明了成员函数,它有默认参数,函数前也添加@JvmOverloads注解,它会生成两个Java重载函数
dis1.display('A');
dis1.display('B', 20);
//JvmOverloadsKt声明了顶层函数,它也有默认参数,函数前也添加@JvmOverloads注解。它会生成两个Java静态重载函数
JvmOverloadsKt.makeCoffee();
JvmOverloadsKt.makeCoffee("摩卡咖啡");
异常检查
Kotlin中没有受检查异常,在函数后面也不会有抛出异常声明
package exception
import java.text.SimpleDateFormat
import java.util.*
// 解析日期
fun readDate(): Date? {
val str = "201A-18-18" //非法格式日期
val df = SimpleDateFormat("yyyy-MM-dd")
// 从字符串中解析日期
return df.parse(str)
}
Java代码中声明的readDate()函数
public static Date readDate() throws ParseException {
String str = "201A-18-18"; //非法格式日期
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
// 从字符串中解析日期
return df.parse(str); //抛出ParseException异常,这是因为解析的字符串不是一个合法的日期
}
在Java代码中分别调用readDate()
/*
在Java中ParseException是受检查异常,
如果在Java中调用Kotlin代码中的readDate函数,由于readDate函数没有声明抛出ParseException异常,
编译器不会检查要求Java程序捕获异常处理。
*/
ExceptionDemoKt.readDate();
/*
调用Java代码中的readDate函数,需要捕获异常
*/
try {
MyException.readDate();
} catch (ParseException e) {
e.printStackTrace();
}
在Java代码中,Kotlin这种处理异常的行为不符合Java的习惯,也不安全。
为此可以在Kotlin的函数前加上@Throws注解,修改Kotlin代码如下:
//在readDate函数前添加注解@Throws(ParseException::class),其中ParseException需要处理的异常类
@Throws(ParseException::class)
fun readDate(): Date? {
val str = "201A-18-18" //非法格式日期
val df = SimpleDateFormat("yyyy-MM-dd")
// 从字符串中解析日期
return df.parse(str)
}
try {
ExceptionDemoKt.readDate();
} catch (ParseException e) {
e.printStackTrace();
}
详细参考:Java 中调用 Kotlin