Kotlin与Java混合开发(3)✔️Java调用Kotlin

  • 访问 Kotlin 属性

  • 访问包级别成员

    • 使用 文件名 访问
    • 使用 @file:JvmName("自定义名") 访问
  • 实例字段、静态字段和静态函数

    • 实例字段
    • 静态字段
    • 静态函数
  • 可见性

  • 生成重载函数

  • 异常检查

  Java 调用 Kotlin 要比 Kotlin 调用 Java 要麻烦一些,但还是比较容易实现的。

一、访问 Kotlin 属性

  Kotlin 的一个属性对应 Java 中的一个私有字段、一个 setter 函数 和 一个 getter 函数,如果是只读属性则没有 setter 函数。那么 Java 访问 Kotlin 的属性是通过这些 getter 函数 和 setter 函数。

// User.kt
data class User(val name: String, var password: String)
// MainJava.java
public class MainJava {

    public static void main(String[] args) {
        User user = new User("小三", "123456");
        System.out.println(user.getName());
        System.out.println(user.getPassword());
        user.setPassword("abcdef");
        System.out.println(user.getPassword());
    }
}
// 运行结果
小三
123456
abcdef
Process finished with exit code 0

  var 声明的属性会生成 gettersetter 两个函数,但 val 声明的属性是只读的,所以只会生成 getter 一个函数。

二、访问包级别成员

  在同一个 Kotlin 文件中,那些 顶层属性和函数 (包括顶层扩展属性和函数) 都不隶属于某个类,但它们 隶属于该 Kotlin 文件中定义的包。在 Java 中访问它们时,把它们当成静态成员。

  • 使用 文件名 访问

// 1️⃣ 代码文件:kot/kotlin_module/src/main/java/cn/ak/kotmodule/kot/topper.kotlin.kt
// 1️⃣ 文件名为:topper.kotlin.kt

package cn.ak.kotmodule.kot

// 顶层函数
fun rectangleArea(width: Double, height: Double): Double = width * height

// 顶层属性
val area = 100.0

// 顶层扩展函数
fun User.printInfo() = println("{name=$name, password=$password}")

  上述是一个名为 topper.kotlin.kt 的 kotlin 源代码文件,见第1️⃣行注释说明。文件 topper.kotlin.kt 中声明了一个 顶层函数、一个 顶层属性 和 一个 User扩展函数topper.kotlin.kt 文件编译后会生成一个 Topper_kotlinKt.class 文件,因为 点(.) 字符不能构成 Java 类名,编译器会将其 替换下划线(_),所以在 Java 中访问 topper.kotlin.kt 对应的类名是 Topper_kotlinKt,见下面调用案例的第2️⃣~4️⃣行代码。

// MainJava.java
public class MainJava {

    public static void main(String[] args) {
        // 访问顶层函数
        Double area = Topper_kotlinKt.rectangleArea(100, 50); // 2️⃣
        System.out.println(area);
        // 访问顶层属性
        System.out.println(Topper_kotlinKt.getArea()); // 3️⃣
        // 访问扩展函数
        User user = new User("小三", "Lawrence");
        Topper_kotlinKt.printInfo(user); // 4️⃣
    }
}

  • 使用 @file:JvmName("自定义名") 访问

  如果你觉得上面使用 文件名 来调用函数和属性不够友好,但还不想修改 Kotlin 源文件名,那么可以在 Kotlin 源文件中使用 @JvmName 注解,指定生成的文件名,如下面代码第1️⃣行。

@file:JvmName("ExKotlin")  //1️⃣

package cn.ak.kotmodule.kot

// 顶层函数
fun rectangleArea(width: Double, height: Double): Double = width * height

// 顶层属性
val area = 100.0

// 顶层扩展函数
fun User.printInfo() = println("{name=$name, password=$password}")

  注意:@JvmName 注解必须放在 文件的第一行,否则会报编译错误。

// MainJava.java
public class MainJava {

    public static void main(String[] args) {
        // 访问顶层函数
        Double area = ExKotlin.rectangleArea(100, 50);
        System.out.println(area);
        // 访问顶层属性
        System.out.println(ExKotlin.getArea());
        // 访问扩展函数
        User user = new User("小三", "Lawrence");
        ExKotlin.printInfo(user);
    }
}

三、实例字段、静态字段和静态函数

  Java 语言中所有的变量和函数都被封装到一个类中,类中包括实例函数(实例方法)、实例属性(成员变量)、静态属性(静态成员变量) 和 静态函数(静态方法),即:Java 中所有东西都放在类中 class 类名 {...}

  • 实例字段

  如果需要以 Java 实例成员变量形式(即:实例名.成员变量)访问 Kotlin 中的属性,则需要在该属性前加 @JvmField 注释,表明该属性被当作 Java 中的成员变量使用,访问可见性相同。另外,延迟初始化(lateinit) 属性在 Java 中被当作成员变量使用,访问可见性相同。

// kotlin
class Person {
    // 姓名
    @JvmField  // 1️⃣
    val name = "小三"
    // 年龄
    var age = 20
    // 生日
    lateinit var birthDate: Date // 2️⃣
}
// java
public class MainJava {

    public static void main(String[] args) {
        Person person = new Person();
        System.out.println(person.name);        // 3️⃣
        System.out.println(person.getAge());    // 看不到属性person.age
        System.out.println(person.birthDate);   // 4️⃣
    }
}

  kotlin 源码中第1️⃣行使用 @JvmField 注释声明 name 属性,所以代码第3️⃣行可以直接访问。kotlin 源码第2️⃣行 birthDate 声明为延时属性,而延时属性在 Java 源码中也可以直接访问。

  • 静态字段

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

  (1) 属性声明为顶层属性,Java 中将所有的顶层成员(属性和函数)都认为是静态的。

// kotlin
@file:JvmName("ExKotlin") 

// 顶层属性
@JvmField
val area = 100.0

const val MAX_COUNT = 100

  (2) 在 Kotlin 的声明对象和伴生对象中定义属性,这些属性需要使用 @JvmField 注解、lateinitconst 来修饰。什么是伴生对象?click me !

// kotlin
object Singleton {
    @JvmField
    val x = 10

    const val y = 100

    lateinit var birthDate: Date
}

class Size {
    var w = defaultHeight
    var h = 0

    companion object {
        @JvmField
        var defaultHeight = 100

        const val defaultWith = 100

        lateinit var defaultSize: Size
    }
}

调用上述示例代码:

// java
public class MainJava {

    public static void main(String[] args) {
        System.out.println("--------------------");
        System.out.println(ExKotlin.area);
        System.out.println(ExKotlin.MAX_COUNT);

        System.out.println("--------------------");
        System.out.println(Singleton.birthDate);
        System.out.println(Singleton.x);
        System.out.println(Singleton.y);

        System.out.println("--------------------");
        System.out.println(Size.defaultWith);
        System.out.println(Size.defaultHeight);
        System.out.println(Size.defaultSize);
    }
}
// 运行结果:
--------------------
100.0
100
--------------------
null
10
100
--------------------
100
100
null

Process finished with exit code 0
  • 静态函数

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

  (1) 函数声明为顶层函数,Java 中将所有 kotlin中的顶层函数(属性和函数)都认为是静态的。

// kotlin
@file:JvmName("ExKotlin") 

// 顶层函数
fun rectangleArea(width: Double, height: Double): Double = width * height

  (2) 在 Kotlin 的声明对象(object)伴生对象(companion object) 中定义函数,这些函数需要使用 @JvmStatic 来修饰。什么是伴生对象?click me

// kotlin
object Singleton {

    @JvmField
    var x = 10

    @JvmStatic
    fun printlnX() = println("x=$x")
}

class Area {

    companion object {

        @JvmField
        var defaultWith = 100

        @JvmStatic
        fun areaSize(height: Double) = defaultWith * height
    }
}

调用上述示例代码:

// java
public class MainJava {

    public static void main(String[] args) {
        System.out.println("---------- 顶层函数 ----------");
        System.out.println(ExKotlin.rectangleArea(100, 20));

        System.out.println("--------------------");
        Singleton.printlnX();

        System.out.println("--------------------");
        System.out.println(Area.areaSize(20));
    }
}
---------- 顶层函数 ----------
2000.0
--------------------
x=10
--------------------
2000.0

Process finished with exit code 0

四、可见性

  Java 和 Kotlin 都有4种可见性,但是除了 public 可以完全兼容外,其他的可见性都是有所区别的。

  • Java 可见性,默认为包私有


    Kotlin与Java混合开发(3)✔️Java调用Kotlin_第1张图片
    默认为包私有
  • Kotlin 可见性

可见性 修饰符 类成员声明 顶层声明 说明
公有 public 所有地方可见 所有地方可见 public是默认修饰符
内部 internal 模块中可见 模块中可见 不同于java中的包
保护 protected 子类中可见 顶层声明中不能使用
私有 private 类中可见 文件中可见

  注意:kotlin 中没有 Java 的包私有可见性,而具有模块可见性 (internal)

  • 将 Java 可见性 和 Kotlin 可见性对比,可知 Kotlin 中没有默认包私有可见性,而 Java 中没有内部可见性。
    (1) Kotlin 私有可见性
      由于 Kotlin 私有可见性可以声明类中成员,也可以声明顶层成员。那么映射到 Java 分为两种情况:
      a、Kotlin 类中私有成员映射到 Java 类中私有实例成员。
      b、Kotlin 中私有顶层成员映射到 Java 中私有静态成员。
    (2) Kotlin 内部可见性
      由于 Java 中没有内部可见性,那么 Kotlin 内部可见性映射为 Java 公有可见性。
    (3) Kotlin 保护可见性
      Kotlin 保护可见性映射为 Java 保护可见性。
    (4) 公有可见性
      Kotlin 公有可见性映射为 Java 公有可见性。

  • 示例代码

// KotlinSeeable.kt

internal class Emplyee {
    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 class Department {
    protected var no: Int = 0   // 保护可见性Java端子类继承可见

    var name: String = ""   // 公有可见性Java端可见
}

internal const val MAX_IN_COUNT = 300   // 内部可见性Java端可见

private const val MIN_IN_COUNT = 0  // 私有可见性Java端不可见

  注意:Kotlin 中 属性函数 默认可见性为public。当 函数 没有被 open 修饰(即:默认情况)是可不被继承状态。

  • 通过 IDE 解释后的 Java 代码如下:
public final class Emplyee {
   private int no = 10;
   @Nullable
   private String job;
   private double salary;
   @NotNull
   public Department dept;

   public final int getNo$kotlin_module() {
      return this.no;
   }

   public final void setNo$kotlin_module(int var1) {
      this.no = var1;
   }

   @Nullable
   protected final String getJob() {
      return this.job;
   }

   protected final void setJob(@Nullable String var1) {
      this.job = var1;
   }

   private final void setSalary(double value) {
      if (value >= 0.0D) {
         this.salary = value;
      }

   }

   @NotNull
   public final Department getDept() {
      Department var10000 = this.dept;
      if (var10000 == null) {
         Intrinsics.throwUninitializedPropertyAccessException("dept");
      }

      return var10000;
   }

   public final void setDept(@NotNull Department var1) {
      Intrinsics.checkParameterIsNotNull(var1, "");
      this.dept = var1;
   }
}

public class Department {
   private int no;
   @NotNull
   private String name = "";

   protected final int getNo() {
      return this.no;
   }

   protected final void setNo(int var1) {
      this.no = var1;
   }

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

   public final void setName(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "");
      this.name = var1;
   }
}

public final class KotlinSeeableKt {
   public static final int MAX_IN_COUNT = 300;
   private static final int MIN_IN_COUNT = 0;
}

  注意:当 Kotlin 类函数被 Java 解释时,Kotlin 属性被映射为 Java 的成员变量,其修饰符均为 private,但其 gettersetter 方法的可见性会与 Kotlin 中属性可见性对应。lateint 修饰是个例,lateinit 修饰可见性保持一致。

  • Java 调用 Kotlin 示例
public class MainJava {

    public static void main(String[] args) {
        Emplyee emp = new Emplyee();
        // 访问kotlin中内部可见性的Emplyee成员属性no
        int no = emp.getNo$kotlin_module();

        Department dept = new Department();
        // 访问kotlin中公有可见性的Department成员属性name
        dept.setName("市场部");

        // 访问Kotlin中公有可见性的Employee成员属性dept
        emp.setDept(dept);
        System.out.println(emp.getDept());

        // 访问kotlin中内部可见性的顶层属性MAX_IN_COUNT
        System.out.println(KotlinSeeableKt.MAX_IN_COUNT);
    }
}

五、生成重载函数

  Kotlin 的函数参数可以设置默认值,看起来像多个函数重载一样。但 Java 中并不支持参数默认值,只能支持全部参数函数。为了解决这个问题,可以在 Kotlin 函数前使用 @JvmOverloads 注解,Kotlin 编译器会生成多个重载函数。@JvmOverloads 注解的函数可以是 构造函数成员函数顶层函数,但 不能是抽象函数

  • kotlin 示例代码:
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}咖啡"
}
  • IDE 解释 Java 代码:
public final class KotlinOverloadsKt {
   @JvmOverloads
   @NotNull
   public static final String makeCoffee(@NotNull String type) {
      Intrinsics.checkParameterIsNotNull(type, "type");
      return "制作一杯" + type + "咖啡";
   }

   // $FF: synthetic method
   @JvmOverloads
   @NotNull
   public static String makeCoffee$default(String var0, int var1, Object var2) {
      if ((var1 & 1) != 0) {
         var0 = "卡布奇诺";
      }

      return makeCoffee(var0);
   }

   @JvmOverloads
   @NotNull
   public static final String makeCoffee() {
      return makeCoffee$default((String)null, 1, (Object)null);
   }
}

// DisplayOverloading.java
public final class DisplayOverloading {
   @JvmOverloads
   public final void display(char c, int num) {
      String var3 = "" + c + ' ' + num;
      boolean var4 = false;
      System.out.println(var3);
   }

   // $FF: synthetic method
   @JvmOverloads
   public static void display$default(DisplayOverloading var0, char var1, int var2, int var3, Object var4) {
      if ((var3 & 2) != 0) {
         var2 = 1;
      }

      var0.display(var1, var2);
   }

   @JvmOverloads
   public final void display(char c) {
      display$default(this, c, 0, 2, (Object)null);
   }
}

// Animal.java
public final class Animal {
   private final int age;
   private final boolean sex;

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

   public final boolean getSex() {
      return this.sex;
   }

   @JvmOverloads
   public Animal(int age, boolean sex) {
      this.age = age;
      this.sex = sex;
   }

   // $FF: synthetic method
   @JvmOverloads
   public Animal(int var1, boolean var2, int var3, DefaultConstructorMarker var4) {
      if ((var3 & 2) != 0) {
         var2 = false;
      }

      this(var1, var2);
   }

   @JvmOverloads
   public Animal(int age) {
      this(age, false, 2, (DefaultConstructorMarker)null);
   }
}

  通过 IDE 解释后,可以看出 Kotlin 中的参数默认值,在 Java 中实际是通过函数的重载来实现的。

  • Java 调用 Kotlin 含有默认参数的函数:
public class MainJava {

    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);

        KotlinOverloadsKt.makeCoffee();
        KotlinOverloadsKt.makeCoffee("摩卡咖啡");
    }
}

注意:别忘了上面的 @file:JvmName("自定义名") 的使用。

六、异常检查

  Kotlin 中没有受检查异常,在函数后面也不会有异常声明。如果有如下的 Kotlin 代码:

@file:JvmName("ErrorKt")

package cn.ak.kotmodule.kot

import java.text.SimpleDateFormat
import java.util.*

fun readDate(): Date? {
    val str = "201D-18-19"
    val df = SimpleDateFormat("yyyy-MM-dd") // 1️⃣抛出异常
    // 从字符串中解析日期
    return df.parse(str)
}

  上面代码第1️⃣行会抛出 ParseException 异常,这是因为解析的字符串不是一个合法的日期。在 Java 中 ParseException 是受检查异常,如果在 Java 中调用 readDate 函数,由于 readDate 函数没有声明抛出 ParseException 异常,编译器不会检查要求 Java 程序捕获异常处理。Java 调用代码如下:

public class MainJava {

    public static void main(String[] args) {
        ErrorKt.readDate();
    }
}

  这样处理异常不符合 Java 的习惯,为此可以在 Kotlin 的函数前加上 @Throw 注解,修改 Kotlin 代码如下:

@Throws(ParseException::class)
fun readDate(): Date? {
    val str = "201D-18-19"
    val df = SimpleDateFormat("yyyy-MM-dd") // 抛出异常
    // 从字符串中解析日期
    return df.parse(str)
}

  注意在 readDate 函数前添加注解 @Throws(ParseException::class),其中 ParseException 是需要处理的异常类。
  那么 Java 代码可以修改为如下捕获异常形式:

public class MainJava {

    public static void main(String[] args) {
        try {
            ErrorKt.readDate();
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}

  当然在 Java 中除了 try-catch 捕获异常,还可以声明抛出异常。

你可能感兴趣的:(Kotlin与Java混合开发(3)✔️Java调用Kotlin)