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
  目录