《Kotlin从小白到大牛》第21章:Kotlin与Java混合编程

第21章 Kotlin与Java混合编程

Kotlin毕竟还是一种新的语言,所以很多项目、组件和框架还是用Java开发的,目前Kotlin不能完全取代Java,因此有时会使用Kotlin调用Java写好的组件或框架。Kotlin在设计之初充分地考虑了与Java的混合编程。本章介绍Kotlin与Java混合编程。

21.1 数据类型映射

Kotlin虽然最终会编译为字节码在Java虚拟机上运行,它的一些数据类型会编译为Java中的数据类型。Kotlin中的一些数据类型与Java的一些数据类型有一定的映射关系的,主要分为Java基本数据类型、Java包装类、Java常用类和Java集合类型几个方面。

21.1.1 Java基本数据类型与Kotlin数据类型映射
Java基本数据类型与Kotlin数据类型映射如表21-1所示,其中Kotlin这些数据类型都是基本数据类型,位于kotlin包中。
《Kotlin从小白到大牛》第21章:Kotlin与Java混合编程_第1张图片
21.1.2 Java包装类与Kotlin数据类型映射
Java包装类是对Java基本数据类型的包装,Java包装类可以有空值,所以映射到Kotlin数据类型时是可空类型,如表21-2所示。
《Kotlin从小白到大牛》第21章:Kotlin与Java混合编程_第2张图片
21.1.3 Java常用类与Kotlin数据类型映射
Java常用类是位于java.lang中一些核心类,它们映射到Kotlin数据类型时是非空或可空类型,如表21-3所示。
在这里插入图片描述
《Kotlin从小白到大牛》第21章:Kotlin与Java混合编程_第3张图片
21.1.4 Java集合类型与Kotlin数据类型映射
Java集合类型是映射到Kotlin数据类型如表21-4所示。从表21-4可见Java的集合不区分可变和不可变,而Kotlin中有这样的区别,在表21-4中还有一种平台类型,在混合编程时Kotlin将它们看作可空或非空,所以平台类型(Mutable)Iterator!表示的是Iterator、Iterator?、MutableIterator和MutableIterator?四种可能性。
《Kotlin从小白到大牛》第21章:Kotlin与Java混合编程_第4张图片

21.2 Kotlin调用Java

混合编程包含了两个方面:Kotlin调用Java和Java调用Kotlin。本节先介绍Kotlin调用Java,事实上Kotlin调用Java非常简单,因为Kotlin是主动的,Java是被动的,Kotlin在设计之初充分地考虑到Kotlin主动调用Java各种情况。
下面从几个方面分别介绍一下。
21.2.1 避免Kotlin关键字
或许Java程序员在给Java标识符命名是时并没有考虑到哪些Kotlin的关键字。但当Kotlin中调用这样的Java代码时,则需要将这些关键字用反引号括起来。例如Java标准输入流System.in,如果在Kotlin中调用则需要表示为System.in
示例Java代码如下:
//Java代码文件:chapter21/src/main/java/com/a51work6/section2/MyJavaClass.java
package com.a51work6.section2;

public class MyJavaClass {
public static MyJavaClass object =new MyJavaClass(); ①

@Override
public String toString() {
    return "MyJavaClass{}";
        }

}
调用的Kotlin代码如下:
//Kotlin代码文件:chapter21/src/main/kotlin/com/a51work6/section2/ch21.2.1.kt
package com.a51work6.section2

fun main(args: Array) {
val obj = MyJavaClass.object
println(obj)
}
在Java代码中使用object命名变量,见代码第①行。那么在Kotlin中调用它时需要使用反引号括起来,见代码第②行。

21.2.2 平台类型与空值
在21.1节介绍类型映射时介绍过程平台类型,这些类型在Java中声明了一个变量或返回的数据可能为空,也可能非空。Kotlin在调用它们时会放弃类型检查。
示例代码如下:
//Java代码文件:chapter21/src/main/java/com/a51work6/section2/Person.java
package com.a51work6.section2;

import java.util.Date;

public class Person {
// 名字
private String name =“Tony”;
// 年龄
private int age = 18;
// 出生日期
private Date birthDate; ①

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

public Date getBirthDate() {
    return birthDate;
}

public void setBirthDate(Date birthDate) {
    this.birthDate = birthDate;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

}
//Kotlin代码文件:chapter21/src/main/kotlin/com/a51work6/section2/ch21.2.2.kt
package com.a51work6.section2

import java.util.*

fun main(args: Array) {
val person = Person()
val date = person.birthDate ②
println(“date = $date”) //null
val date1: Date? = person.birthDate ③
println(“date1 = $date1”) //null
val date2: Date = person.birthDate //抛出异常 ④
println(“date2 = $date2”)
}
上述代码编写了一个Java类Person,它的birthDate字段没有初始化所以为空值,见代码第①行。在Kotlin中通过属性访问Java中的setter或getter函数的,代码第②行读取birthDate属性赋值给变量date,此时date的类型是由编译器自动推导出来的,如图21-1所示IntelliJ IDEA IDE表示的平台类型是Date!,它可以接收空值。
《Kotlin从小白到大牛》第21章:Kotlin与Java混合编程_第5张图片
但是如果明确指定返回值类型,可以使用Date?或Date,见代码第③行和第④行。由于Date?是可空类型,date1可以接收空值,而date2是非空类型,不能接收空值因此代码第④行会发生异常。
Exception in thread "main"java.lang.IllegalStateException: person.birthDate must not be null
平台类型采用自动推导可以保证空值的安全。

21.2.3 异常检查
Kotlin和Java在异常检查上有很大的不同,Java有受检查异常,而Kotlin中没有受检查异常。那么当Kotlin调用Java中的一个函数时,这个函数声明抛出异常,那么Kotlin会如何处理呢?
示例代码如下:
//Kotlin代码文件:chapter21/src/main/kotlin/com/a51work6/section2/ch21.2.3.kt
package com.a51work6.section2

import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader

fun main(args: Array) {

try {       InputStreamReader(System.`in`).use { ir ->              ①
        BufferedReader(ir).use {reader ->                   ②
            // 从键盘接收了一个字符串的输入
            val command =reader.readLine()             ③
            println(command)
        }
    }
} catch (e: IOException) {  
    e.printStackTrace()
}

}
代码第①行~第③行是通过Java标准输入流从键盘读取字符串,相当于Kotlin中的readLine函数。这里创建了两个输入流代码,见第①行和代码第②行。一个读取数据的函数见代码第③行,它们都会抛出IOException异常。IOException在Java中是受检查异常,必须要进行捕获或抛出处理,而Kotlin中不用必须捕获。

21.2.4 调用Java函数式接口
在Java函数式接口是只有一个抽象函数的接口,也简称SAM( Single
Abstract Method缩写),在Kotlin中调用Java函数式接口非常的简洁,形式是“接口名{…}”。
示例代码如下:
//Java代码文件:chapter21/src/main/java/com/a51work6/section2/Calculable.java
package com.a51work6.section2;

//可计算接口
@FunctionalInterface
public interface Calculable { ①
// 计算两个int数值
int calculateInt(int a, int b);
}

//Kotlin代码文件:chapter21/src/main/kotlin/com/a51work6/section2/ch21.2.4.kt
package com.a51work6.section2

fun main(args: Array) {

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)}")      ⑤   

}
上述代码第①行是声明一个函数式接口Calculable,它只有一个抽象函数calculateInt。代码第②行和第③行是在Kotlin中实现Calculable接口,并实例化它,其中的Lambda表达式{ a, b -> a + b }和{ a, b -> a - b }是对抽象函数calculateInt的实现。代码第④行和第⑤行是调用函数calculateInt。

21.3 Java调用Kotlin

Java调用Kotlin要比Kotlin调用Java要麻烦一些,但还是比较容易实现的。下面从几个方面分别介绍一下。

21.3.1 访问Kotlin属性
Kotlin一个属性对应Java中一个私有字段、一个setter函数和一个getter函数,如果是只读的则没有setter函数。那么Java访问Kotlin的属性这是通过这些getter函数和setter函数。
示例代码如下:
//Kotlin代码文件:chapter21/src/main/kotlin/com/a51work6/section3/User.kt
package com.a51work6.section3

data class User(var name: String, var password: String) ①

//Java代码文件:chapter21/src/main/java/com/a51work6/section3/Ch21_3_1.java
package com.a51work6.section3;

public class Ch21_3_1 {
public static void main(String[]args) {
User user = new User(“Tom”, “12345”); ②
System.out.println(user.getName());//Tom ③
user.setPassword(“54321”); ④
System.out.println(user.getPassword());//54321 ⑤
}
}
上述代码第①是声明Kotlin数据类,其中有两个属性,var声明的属性会生成setter和getter函数,如果是val声明的属性是只读的,只生成getter函数。
代码第②行是实例化User对象,代码第③行是读取name属性,代码第④行是为属性password赋值,代码第⑤行是读取password属性。

21.3.2 访问包级别成员
在同一个Kotlin文件中,那些顶层属性和函数,包括顶层扩展属性和函数都不隶属于某个类,但它们隶属于该Kotlin文件中定义的包。在Java中访问它们时,把它们当成静态成员。
示例代码如下:
//代码文件:chapter21/src/main/kotlin/com/a51work6/section3/s2/ch21.3.2.kt ①
@file:JvmName(“PackageLevelDemo”) ②

package com.a51work6.section3.s2

//顶层函数
fun rectangleArea(width: Double, height: Double): Double { ③
val area = width * height
return area
}

//顶层属性
val area = 100.0 ④

//Java代码文件:chapter21/src/main/java/com/a51work6/section3/Ch21_3_2.java
package com.a51work6.section3;

//import com.a51work6.section3.s1.Ch21_3_2Kt;
import com.a51work6.section3.s1.PackageLevelDemo;

public class Ch21_3_2 {
public static void main(String[]args) {
//访问顶层函数
//Double area = Ch21_3_2Kt.rectangleArea(320.0, 480.0); ⑤
Double area =PackageLevelDemo.rectangleArea(320.0, 480.0); ⑥
System.out.println(area);

    //访问顶层属性
    //System.out.println(Ch21_3_2Kt.getArea());              ⑦       

System.out.println(PackageLevelDemo.getArea()); ⑧
}
}
上述代码第①行~第④行是一个Kotlin源代码文件,文件名ch21.3.2.kt其中声明了一个顶层函数(见代码第③行)和一个顶层属性(见代码第④行)。ch21.3.2.kt文件编译之后生成Ch21_3_2Kt.class文件,因为点(.)字符不能构成Java类名,编译器会将其替换为下划线(_),所以在Java中访问ch21.3.2.kt对应的类名是Ch21_3_2Kt,见代码第⑤行和代码第⑦行。
如果觉得Ch21_3_2Kt这样的类名不友好,还不想修改Kotlin源文件名,那么可以在Kotlin源文件中使用@JvmName注解,指定生成的文件名,见代码第②行@file:JvmName(“PackageLevelDemo”),其中PackageLevelDemo是生成之后的类名。那么在Java中使用PackageLevelDemo类名就可以访问ch21.3.2.kt文件中的顶层函数和属性了,见代码第⑥行和代码第⑦行。

21.3.3 实例字段、静态字段和静态函数
Java语言中所有的变量和函数都封装到一个类中,类中包括实例函数、实例字段、静态字段和静态函数。Java实例函数就是Kotlin类中声明的函数,而Java中的实例字段、静态字段和静态函数,Kotlin也是支持的。
![在这里插入图片描述](https://img-blog.csdnimg.cn/202006111013在这里插入图片描述
1.实例字段
如果需要以Java实例字段形式(即:实例名.字段名)访问Kotlin中的属性,则需要在该属性前加@JvmField注解,表明该属性被当做Java中的字段使用,可见性相同。另外,延迟初始化(lateinit)属性在Java中当做字段使用,可见性相同。
示例代码如下:
//Kotlin代码文件:chapter21/src/main/kotlin/com/a51work6/section3/s3/Person.kt
package com.a51work6.section3.s3

import java.util.*

class Person {
// 名字
@JvmField
var name = “Tony” ①
// 年龄
var age = 18
// 出生日期
lateinit var birthDate: Date ②
}

//Java代码文件:chapter21/src/main/java/com/a51work6/section3/Ch21_3_3.java
package com.a51work6.section3;

import com.a51work6.section3.s3.Person;

public class Ch21_3_3 {
public static void main(String[]args) {
Person p = new Person();
System.out.println(p.name); //Tony ③
System.out.println(p.birthDate); //null ④
}
}
上述代码第①行使用@JvmField 注解声明name 属性,代码第②行声明延迟属性birthDate。代码第③行和代码第④行是访问字段。

2.静态字段
如果需要以Java静态字段形式(即:类名.字段名)访问Kotlin中的属性,可以有两种实现方式:

  1. 属性声明为顶层属性,Java中将所有的顶层成员(属性和函数)都认为是静态的,具体访问方式21.3.2节已经介绍了,这里不再赘述。
  2. 在Kotlin的声明对象和伴生对象中定义属性,这些属性需要使用@JvmField注解、lateinit或const来修饰。
    示例代码如下:
    //代码文件:chapter21/src/main/kotlin/com/a51work6/section3/s3/ch21.3.3.kt
    @file:JvmName(“StaticFieldDemo”) ①

package com.a51work6.section3.s3

import java.util.*

object Singleton { //Singleton声明对象
@JvmField
val x = 10 ②

lateinit var birthDate: Date        ③      

}

class Account { //Account伴生对象
companion object {
const val interestRate = 0.018 ④
}
}

const val MAX_COUNT = 500 ⑤

//Java代码文件:chapter21/src/main/java/com/a51work6/section3/Ch21_3_3.java

//访问静态字段
System.out.println(Singleton.x); //10 ⑥
Singleton.birthDate = new Date();
System.out.println(Account.interestRate); //0.018
System.out.println(StaticFieldDemo.MAX_COUNT); //500 ⑦
上述代码第①行设置生成之后的文件名为StaticFieldDemo。代码第②行是@JvmField注解Singleton对象中x属性,代码第③行是声明延迟属性birthDate。代码第④行声明伴生对象中interestRate属性是const常量类型。代码第⑤行是声明顶层常量MAX_COUNT。
代码第⑥行~第⑦行是在Java中访问静态字段。

3.静态函数
如果需要以Java静态函数形式(即:类名.函数名)访问Kotlin中的函数,可以有两种实现方式:

  1. 函数声明为顶层函数,这种访问方式21.3.2节已经介绍了,这里不再赘述。
  2. 在Kotlin的声明对象和伴生对象中定义函数,这些函数需要使用@JvmStatic来修饰。
    示例代码如下:
    //代码文件:chapter21/src/main/kotlin/com/a51work6/section3/s3/ch21.3.3.kt
    @file:JvmName(“StaticFieldDemo”)

package com.a51work6.section3.s3

import java.util.*

object Singleton { //Singleton声明对象
@JvmField
val x = 10
lateinit var birthDate: Date

@JvmStatic
fun displayX() {     ①
    println(x)
}

}

class Account {
companion object { //Account伴生对象
const val interestRate = 0.018
@JvmStatic
fun interestBy(amt: Double):Double { ②
return interestRate * amt
}
}
}

const val MAX_COUNT = 500

//Java代码文件:chapter21/src/main/java/com/a51work6/section3/Ch21_3_3.java

//访问静态函数
Singleton.displayX(); ③
Account.interestBy(5000); ④
上述代码第①行@JvmStatic注解Singleton对象中displayX函数,代码第②行@JvmStatic注解伴生对象中interestBy函数。代码第③行和第④行是调用静态函数。

21.3.4 可见性
Java和Kotlin都有4种可见性,但是除了public完全兼容外,其他的可见性都是有所区别的。为了便于比较,首先介绍一下Java可见性。Java可见性有:私有、包私有、保护和公有,具体规则如表21-5所示。
《Kotlin从小白到大牛》第21章:Kotlin与Java混合编程_第6张图片
将表21-5与Kotlin可见性修饰符使用规则表(见表11-1)对照,Kotlin中没有默认包私有可见性,而Java中没有内部可见性。详细解释说明如下:
1.Kotlin私有可见性
由于Kotlin私有可见性可以声明类中成员,也可以声明顶层成员。那么映射到Java分为两种情况:

  1. Kotlin类中私有成员映射到Java类中私有实例成员。
  2. Kotlin中私有顶层成员映射到Java中私有静态成员。
    2.Kotlin内部可见性
    由于Java中没有内部可见性,那么Kotlin内部可见性映射为Java公有可见性。
    3.Kotlin保护可见性
    Kotlin保护可见性映射为Java保护可见性。
    4.Kotlin公有可见性
    Kotlin公有可见性映射为Java公有可见性。

下面通过示例介绍一下,被调用的Kotlin源代码文件Employee.kt:
//代码文件:chapter21/src/main/kotlin/com/a51work6/section3/s4/Employee.kt
package com.a51work6.section3.s4

// 员工类
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_COUNT = 500 // 内部可见性Java端可见
private const val MIN_COUNT = 0 // 私有可见性Java端不可见
调用的Java源代码文件Ch21_3_4.java:
//Java代码文件:chapter21/src/main/java/com/a51work6/section3/Ch21_3_4.java
package com.a51work6.section3;

import com.a51work6.section3.s4.Department;
import com.a51work6.section3.s4.Employee;
import com.a51work6.section3.s4.EmployeeKt;

public class Ch21_3_4 {

public static void main(String[]args) {

    Employee emp = new Employee();
    //访问Kotlin中内部可见性的Employee成员属性no
    //int no =emp.getNo$production_sources_for_module_chapter21_main();     ①
    
    Department dept = new Department();
    //访问Kotlin中公有可见性的Department成员属性name
    dept.setName("市场部");                      ②
    
    //访问Kotlin中公有可见性的Employee中成员属性dept
    emp.setDept(dept);                              ③       

System.out.println(emp.getDept());

    //访问Kotlin中内部可见性的顶层属性MAX_COUNT       

System.out.println(EmployeeKt.MAX_COUNT); ④
}
}

class SubDepartment extends Department { ⑤
void display() {
//继承Kotlin中Department类保护可见性的成员属性no
System.out.println(this.getNo()); ⑥
//继承Kotlin中Department类公有可见性的成员属性name
System.out.println(this.getName()); ⑦
}
}
上述代码第①行是访问Kotlin中内部可见性的Employee成员属性no,Java把它映射成为公有的,但是它的函数名不是getNo,而getNo$production_sources_for_module_chapter21_main。
《Kotlin从小白到大牛》第21章:Kotlin与Java混合编程_第7张图片
公有可见性的成员可以访问,见代码第②行和代码第③行。内部可见性是顶层成员也可以访问,见代码第④行。
代码第⑤行声明一个类SubDepartment,它继承了来自于Kotlin的父类Department。代码第⑥行访问从父类继承下来的no属性。代码第⑦行是访问从父类继承下来的name属性。

21.3.5 生成重载函数
Kotlin的函数参数可以设置默认值,看起来像多个函数重载一样。但Java中并不支持参数默认值,只能支持全部参数函数。为了解决这个问题可以在Kotlin函数前使用@JvmOverloads注解,Kotlin编译器会生成多个重载函数。@JvmOverloads注解的函数可以是:构造函数、成员函数和顶层函数,但不能是抽象函数。
示例代码如下:
//代码文件:chapter21/src/main/kotlin/com/a51work6/section3/s5/Animal.kt
package com.a51work6.section3.s5

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}咖啡。”
}

//Java代码文件:chapter21/src/main/java/com/a51work6/section3/Ch21_3_5.java
package com.a51work6.section3;

import com.a51work6.section3.s5.Animal;
import com.a51work6.section3.s5.AnimalKt;
import com.a51work6.section3.s5.DisplayOverloading;

public class Ch21_3_5 {
public static void main(String[]args) {

    Animal animal1 = new Animal(10,true);         ④      
    Animal animal2 = new Animal(10);                 ⑤
    
    DisplayOverloading dis1 = new DisplayOverloading();
    dis1.display('A');                 ⑥
    dis1.display('B', 20);            ⑦
    
    AnimalKt.makeCoffee();                        ⑧
    AnimalKt.makeCoffee("摩卡咖啡");        ⑨
    
}

}
上述代码第①行声明了一个Animal类,它有一个主构造函数代码默认参数,主构造函数前添加@JvmOverloads注解,它会生成两个Java重载构造函数,见Java代码第④行和第⑤行。
代码第②行是声明了成员函数,它有默认参数,函数前也添加@JvmOverloads注解,它会生成两个Java重载函数,见Java代码第⑥行和第⑦行。
代码第③行是声明了顶层函数,它也有默认参数,函数前也添加@JvmOverloads注解。它会生成两个Java静态重载函数,见Java代码第⑧行和第⑨行。

21.3.6 异常检查
Kotlin中没有受检查异常,在函数后面也不会有抛出异常声明。如果有如下Kotlin代码:
//代码文件:chapter21/src/main/kotlin/com/a51work6/section3/s6/ExceptionDemo.kt
package com.a51work6.section3.s6

// 解析日期
fun readDate(): Date? {
val str =“201A-18-18” //非法格式日期
val df =SimpleDateFormat(“yyyy-MM-dd”) //抛出异常 ①
// 从字符串中解析日期
return df.parse(str)
}
上述代码第①行会抛出ParseException异常,这是因为解析的字符串不是一个合法的日期。在Java中ParseException是受检查异常,如果在Java中调用readDate函数,由于readDate函数没有声明抛出ParseException异常,编译器不会检查要求Java程序捕获异常处理。Java调用代码如下:
//Java代码文件:chapter21/src/main/java/com/a51work6/section3/Ch21_3_6.java
package com.a51work6.section3;

import com.a51work6.section3.s6.ExceptionDemoKt;

public class Ch21_3_6 {
public static void main(String[]args) {
ExceptionDemoKt.readDate();
}
}
这样处理异常不符合Java的习惯,为此可以在Kotlin的函数前加上@Throws注解,修改Kotlin代码如下:
//代码文件:chapter21/src/main/kotlin/com/a51work6/section3/s6/ExceptionDemo.kt
package com.a51work6.section3.s6

// 解析日期
@Throws(ParseException::class)
fun readDate(): Date? {
val str = “201A-18-18” //非法格式日期
val df =SimpleDateFormat(“yyyy-MM-dd”)
// 从字符串中解析日期
return df.parse(str)
}
注意在readDate函数前添加注解@Throws(ParseException::class),其中ParseException需要处理的异常类。
那么Java代码可以修改为如下捕获异常形式:
public class Ch21_3_6 {
public static void main(String[]args) {
try {
ExceptionDemoKt.readDate();
} catch (ParseException e) {
e.printStackTrace();
}
}
}
当然在Java中除了try-catch捕获异常,还可以声明抛出异常。

本章小结

通过对本章内容的学习,广大读者可以了解Kotlin与Java的混合编程,其中包括:数据类型映射、Kotlin调用Java和Java调用Kotlin。

你可能感兴趣的:(Kotlin从小白到大牛)