Kotlin与Java混合编程

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!表示的是Iterator、Iterator?、MutableIterator和MutableIterator?四种可能性。
(Mutable)Collection! 表示“可变或不可变、可空或不可空的 T 的 Java 集合”。
示例:

 

Kotlin 对 Java 数组的映射

与 Java 不同,Kotlin 中的数组是不型变的。
这意味着 Kotlin 不允许我们把一个 Array 赋值给一个 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() 和 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

Kotlin与Java的简单实例对比

打印日志

常量与变量

null声明

空判断

字符串拼接

换行

三元表达式

操作符

类型判断和转换(显式)

类型判断和转换(隐式)

区间

case语句

for循环

集合操作

遍历

方法(函数)定义

带返回值的方法(函数)

构造器


   转载规则


《Kotlin与Java混合编程》 congwiny 采用 知识共享署名 4.0 国际许可协议 进行许可。
评论
 上一篇
如何使用 Kotlin 作用域函数 如何使用 Kotlin 作用域函数
作用域函数Kotlin 提供了一系列用来在【给定对象上下文】中执行代码块的函数。当您在提供了lambda表达式的对象上调用此类函数时,它会形成一个临时范围。在此范围内,您可以在不使用其名称的情况下访问该对象。常用到的作用域函数有5个:let
2019-08-30
下一篇 
Kotlin协程 Kotlin协程
协程(Coroutine)协程引入异步加载图片 普通代码: val view = ... loadImageAsync(url, callback{ bitmap -> uiThread{
2019-08-11
  目录