《Kotlin 程序设计》第九章 Kotlin与Java混合调用

第九章 Kotlin与Java混合调用

正式上架:《Kotlin极简教程》Official on shelves: Kotlin Programming minimalist tutorial 京东JD:https://item.jd.com/12181725.html 天猫Tmall:https://detail.tmall.com/item.htm?id=558540170670

虽然 Kotlin 的开发很方便,但当你与他人协作时,总会碰到 Java 与 Kotlin 代码共存的代码项目。本章就教你如何优雅的实现 Kotlin 与 Java 混合编程。

1 使用工具互相转换

1.1 将 Java 转换为 Kotlin

如果你之前使用 Java 语言而没有 Kotlin 开发经验,不用担心,Intellij IDEA 会帮你一键转换,将 Java 代码转换成 Kotlin 代码。

1.2 将 Kotlin 转换为 Java

另外,通过IDEA的Kotlin插件,可以直接把Kotlin代码ByteCode反编译成Java代码(虽然这个反编译后的Java代码不是那么的的原汁原味)。

2 反射获取类的 Class

在 Java 或 Android 开发中,经常会直接调用一个类的 Class 文件。我们在Java中是直接这么调用的 Xyz.class

这样的方式,在Kotlin中稍微有点不同。在 M13 之前, Kotlin 代码使用JavaClass<Xyz>,而 M13 之后写法为 Xyz::class.java

3 在 Kotlin 中调用 Java 代码

3.1 void 与 Unit

如果一个 Java 方法返回 void,对应的在 Kotlin 代码中它将返回 Unit。在 Kotlin 中可以省略这个Unit返回类型。

3.2 Java 与 Kotlin 关键字冲突的处理

Java 有 static 关键字,在 Kotlin 中没有这个关键字,你需要使用@JvmStatic 替代这个关键字。

同样,在 Kotlin 中也有很多的关键字是 Java 中是没有的。例如

in
is
data

等。

如果 Java 中使用了这些关键字,需要加上反引号转义来避免冲突。例如

// Java 代码中有个方法叫 is()
public void is(){
 //...
}
// 转换为 Kotlin 代码需要加反引号转义
fun `is`() {
 //...
}

4 在 Java 中调用 Kotlin 代码

4.1 static 方法

上文已经提到过,在 Kotlin 中没有 static 关键字,那么如果在 Java 代码中想要通过类名调用一个 Kotlin 类的方法,你需要给这个方法加入@JvmStatic注解。

另外,你也可以通过对象object调用这个方法。

代码示例

StringUtils.isEmpty("hello");  
StringUtils.INSTANCE.isEmpty2("hello");

object StringUtils {
    @JvmStatic fun isEmpty(str: String): Boolean {
        return "" == str
    }

    fun isEmpty2(str: String): Boolean {
        return "" == str
    }
}

4.2 静态方法与伴生对象companion object

class StringUtils {
    companion object {
       fun isEmpty(str: String): Boolean {
            return "" == str
        }
    }
}

companion object, 表示外部类的一个伴生对象,你可以把他理解为外部类自动创建了一个对象作为自己的field。与上面的类似,Java 在调用时,可以这样写:

StringUtils.Companion.isEmpty();

4.3 包级别函数

与 Java 不同,Kotlin 允许函数独立存在,而不必依赖于某个类,这类函数我们称之为包级别函数(Package-Level Functions)。

为了兼容 Java,Kotlin 默认会将所有的包级别函数放在当前kt源文件的类中。比如说,HelloWorld.kt中的包级别的函数,默认会放到HelloWorldKt类中。

//@file:JvmName("MyExample")
package com.easy.kotlin

/**
 * Created by jack on 2017/5/29.
 */

import java.util.Date
import java.text.SimpleDateFormat

fun main(args: Array<String>) {
    println("Hello, world!")
    println(SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date()))
    println(helloKotlin())
}


fun helloKotlin():String {
    val words = mutableListOf<String>()
    words.add("Hello")
    words.add("Kotlin!")
    words.add(java.util.Date().toString())
    return words.joinToString(separator=" ")
}

反编译看下Kotlin ByteCode:

// ================com/easy/kotlin/HelloWorldKt.class =================
// class version 50.0 (50)
// access flags 0x31
public final class com/easy/kotlin/HelloWorldKt {


  // access flags 0x19
  public final static main([Ljava/lang/String;)V
    @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0
    LDC "Hello, world!"
    INVOKESTATIC kotlin/io/ConsoleKt.println (Ljava/lang/Object;)V
    NEW java/text/SimpleDateFormat
    DUP
    LDC "yyyy-MM-dd HH:mm:ss"
    INVOKESPECIAL java/text/SimpleDateFormat.<init> (Ljava/lang/String;)V
    NEW java/util/Date
    DUP
    INVOKESPECIAL java/util/Date.<init> ()V
    INVOKEVIRTUAL java/text/SimpleDateFormat.format (Ljava/util/Date;)Ljava/lang/String;
    INVOKESTATIC kotlin/io/ConsoleKt.println (Ljava/lang/Object;)V
    INVOKESTATIC com/easy/kotlin/HelloWorldKt.helloKotlin ()Ljava/lang/String;
    INVOKESTATIC kotlin/io/ConsoleKt.println (Ljava/lang/Object;)V
    RETURN
   L0
    MAXSTACK = 3
    MAXLOCALS = 1

  // access flags 0x19
  public final static helloKotlin()Ljava/lang/String;
  @Lorg/jetbrains/annotations/NotNull;() // invisible
    INVOKESTATIC kotlin/collections/CollectionsKt.mutableListOf ()Ljava/util/List;
    ASTORE 0
   L0
    ALOAD 0
    LDC "Hello"
    INVOKEINTERFACE java/util/List.add (Ljava/lang/Object;)Z
    POP
    ALOAD 0
    LDC "Kotlin!"
    INVOKEINTERFACE java/util/List.add (Ljava/lang/Object;)Z
    POP
    ALOAD 0
    NEW java/util/Date
    DUP
    INVOKESPECIAL java/util/Date.<init> ()V
    INVOKEVIRTUAL java/util/Date.toString ()Ljava/lang/String;
    INVOKEINTERFACE java/util/List.add (Ljava/lang/Object;)Z
    POP
    ALOAD 0
    CHECKCAST java/lang/Iterable
    LDC " "
    CHECKCAST java/lang/CharSequence
    ACONST_NULL
    ACONST_NULL
    ICONST_0
    ACONST_NULL
    ACONST_NULL
    BIPUSH 62
    ACONST_NULL
    INVOKESTATIC kotlin/collections/CollectionsKt.joinToString$default (Ljava/lang/Iterable;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Ljava/lang/CharSequence;ILjava/lang/CharSequence;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/String;
    ARETURN
   L1
   L2
    LOCALVARIABLE words Ljava/util/List; L0 L2 0
    MAXSTACK = 9
    MAXLOCALS = 1

  // access flags 0x19
  public final static <clinit>()V
    RETURN
   L0
    MAXSTACK = 0
    MAXLOCALS = 0
}

我们可以看出,Kotlin中的包级别函数

fun helloKotlin()

被编译成成了public final static 方法:

public final static helloKotlin()

在 Java 中想要调用包级别函数时,需要通过这个public final class com/easy/kotlin/HelloWorldKt类来调用。

我们也可以通过注解@file:JvmName("MyExample")来自定义这个类名。这样当前文件中的所有包级别函数, 将被放到一个自动生成的文件名为 MyExample 的类中。

代码示例如下:

@file:JvmName("MyExample")
package com.easy.kotlin

/**
 * Created by jack on 2017/5/29.
 */

import java.util.Date
import java.text.SimpleDateFormat

fun main(args: Array<String>) {
    println("Hello, world!")
    println(SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date()))
    println(helloKotlin())
}


fun helloKotlin():String {
    val words = mutableListOf<String>()
    words.add("Hello")
    words.add("Kotlin!")
    words.add(java.util.Date().toString())
    return words.joinToString(separator=" ")
}

添加了如下代码

@file:JvmName("MyExample")
package com.easy.kotlin

反编译后:

// ================com/easy/kotlin/MyExample.class =================
// class version 50.0 (50)
// access flags 0x31
public final class com/easy/kotlin/MyExample {


  // access flags 0x19
  public final static main([Ljava/lang/String;)V
    ....

  // access flags 0x19
  public final static helloKotlin()Ljava/lang/String;
  ....
}

我们可以看到:类名变成了MyExample。

简洁,使用更少的代码做更多的事 在我看来,Kotlin很关键的一个优点就是简洁。相对于Java,使用Kotlin往往能够用更少的代码获得更多的功能。这有什么好处呢?很简单,写的代码越少,逻辑越清晰,所犯的错误就会越少。

  1. 数据类 数据类大量重复的getter和setter相信会是很多人在开发过程中吐槽的一个点。举一个很经典的例子,我们需要一个Person的数据类。

在Java中,需要这么写:

public class Person { private String name; private int age; private int sex; private float height;

public Person(String name, int age, int sex, float height) {
    this.name = name;
    this.sex = sex;
    this.age = age;
    this.height = height;
}

public String getName() {
    return name;
}

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

public int getAge() {
    return age;
}

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

public int getSex() {
     return sex;
}

public void setSex(int sex) {
    this.sex = sex;
}

public float getHeight() {
    return height;
}

public void setHeight(float height) {
    this.height = height;
}

@Override
public String toString() {
    return "Person{" +
            "name='" + name + '\'' +
            ", age=" + age +
            ", sex=" + (sex == 0 ? "男" : "女") +
            ", height=" + height +
            '}';

} 在Kotlin里,我们只需要一行代码就能完成以上的功能:

data class Person(var name: String, var age: Int, var sex: Int, var height: Float) Kotlin提供的数据类会让你自动获得所需的getter、setters、toString(),这很大程度上减少了大量重复的工作。当然,我们也可以很轻松的去覆盖这些函数,做自定义的事情,但是在大多数情况下,只需声明类和属性就已经足够了。

  1. 区间表达式 在Java中我们经常要写这样的代码,

for(int i = 0; i <= 10; i++){ System.out.println(i) } 但是在Kotlin中,支持 .. 操作符形式的区间表达式,我们转换成Kotlin就变成了这样:

for(i in 0..10){ println(i) } 是不是简洁优雅很多,不仅如此,还有更多相关的功能。

//倒序迭代 for(i in 10 downTo 0){ ... }

//步长为2的迭代 for(i in 0..10 setp 2){ ... }

//i在[0,10)区间,排除了10 for(i in 0 until 10){ ... }

  1. Lamda表达式 Java在Java8才支持Lamda语法,在Kotlin里完全支持。有对比才有差距,看例子:

Java中:

btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //dosomething }}); Kotlin中:

btn.setOnClickListener { //dosomething }

  1. 类扩展 类扩展是一个超级强大的功能,我终于可以摆脱大量的Util工具类了。:)

看一个例子:

fun MutableList<Int>.swap(index1: Int, index2: Int) { val tmp = this[index1] // 'this' 对应该列表 this[index1] = this[index2] this[index2] = tmp } }

//使用 val l = mutableListOf(1, 2, 3) l.swap(0, 2) // 'swap()' 内部的 'this' 得到 'l' 的值 是不是功能强大且代码简洁优雅?当然,扩展并不能真正的修改它所扩展的类。通过定义一个扩展,我们并没有在一个类中插入新的方法,仅仅是可以通过该类型的变量用点表达式来调用这个新函数。 口说无凭,我们来看看Kotlin编译后的字节码:

定义:

public final class ExtensionKt { // access flags 0x19 // signature (Ljava/util/List<Ljava/lang/Integer;>;II)V // declaration: void swap(java.util.List<java.lang.Integer>, int, int) public final static swap(Ljava/util/List;II)V @Lorg/jetbrains/annotations/NotNull;() // invisible, parameter 0 L0 ALOAD 0 LDC "$receiver" INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)V L1 LINENUMBER 6 L1 ALOAD 0 ILOAD 1 INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object; CHECKCAST java/lang/Number INVOKEVIRTUAL java/lang/Number.intValue ()I ISTORE 3 L2 LINENUMBER 7 L2 ALOAD 0 ILOAD 1 ALOAD 0 ILOAD 2 INVOKEINTERFACE java/util/List.get (I)Ljava/lang/Object; INVOKEINTERFACE java/util/List.set (ILjava/lang/Object;)Ljava/lang/Object; POP L3 LINENUMBER 8 L3 ALOAD 0 ILOAD 2 ILOAD 3 INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; INVOKEINTERFACE java/util/List.set (ILjava/lang/Object;)Ljava/lang/Object; POP L4 LINENUMBER 9 L4 RETURN L5 LOCALVARIABLE tmp I L2 L5 3 LOCALVARIABLE $receiver Ljava/util/List; L0 L5 0 LOCALVARIABLE index1 I L0 L5 1 LOCALVARIABLE index2 I L0 L5 2 MAXSTACK = 4 MAXLOCALS = 4

// compiled from: extension.kt }

使用:

L1 LINENUMBER 11 L1 ALOAD 1 ICONST_0 ICONST_2 INVOKESTATIC ExtensionKt.swap (Ljava/util/List;II)V L2 可以看出,Kotlin在编译时会以所在文件名extension创建静态类ExtensionKt,并将定义的扩展方法swap编译成静态方法供外部调用。

在Java中形式大致是这样:

public final class ExtensionKt { public static final void swap(@NotNull List list, int index1, int index2) { int tmp = ((Number)list.get(index1)).intValue(); list.set(index1, receiver.get(index2)); list.set(index2, Integer.valueOf(tmp)); } } 所以在底层,其实就是我们平时所写的Util工具类,但是Kotlin默认帮我们实现了,我们只需更简洁的编写调用就好了。

安全,避免NPE空指针异常

  1. 空安全 这是一个让人又爱又恨的特性。一直以来,NullPointException空指针异常在开发中是最低级也最致命的问题。我们往往需要进行各种null的判断以试图去避免NPE的发生。Kotlin基于这个问题,提出了一个空安全的概念,即每个属性默认不可为null。

举个例子。

var a: String = "abcd" a = null //编译错误 如果要允许为空,我们需要手动声明一个变量为可空字符串类型,写为String?

var a: String? = "abcd" a = null //编译成功 那怎么实现默认不可为Null呢? 我们来看一下字节码:

@Lorg/jetbrains/annotations/NotNull;() // invisible L0 LINENUMBER 10 L0 GETSTATIC PersonKt.a : Ljava/lang/String; ARETURN L1

//init static <clinit>()V L0 LINENUMBER 10 L0 LDC "abcd" PUTSTATIC PersonKt.a : Ljava/lang/String; RETURN MAXSTACK = 1 MAXLOCALS = 0 可以看到Kotlin对变量进行了NotNull注解。翻译成Java代码:

@NotNull String a = "abcd" 不仅如此,为了避免NPE异常,Kotlin做了一件很有趣的事:当你允许属性可空时,Kotlin编译器将不允许你在未经检查的情况下引用它。

var person: Person? = null person.name = "shinelw" //编译失败 person?.name = "shinelw" //编译成功 如上面的代码所示,当person对象可为null时,必须强制使用 ?. 来进行null检查。看看 ?. 在字节码里的样子,

LINENUMBER 13 L1
ALOAD 0
DUP
IFNULL L2
LDC "shinelw"
INVOKEVIRTUAL Person.setName (Ljava/lang/String;)V
GOTO L3

L2 POP L3 可见,在person获取name属性的时候进行了null的判断,翻译成java代码:

Person person = (Person)null; if(person != null) { person.setName("shinelw"); } 这么看来,空安全特性的确带来了巨大的好处,极大程度上避免了空指针异常的出现。然而,世上没有十全十美的东西。空安全在开发过程中给我带来了很多幸福的烦恼。举个例子,以前用Java是这样的:

public class A { String a; String b; String c; } 现在呢,Kotlin中是这样的:

class A { var a: String? = null var b: String? = null var c: String? = null } 看出区别了吗?

在Kotlin中我们需要在定义变量是就必须给出初始值。开发过程中,很多情况下变量在定义时尚不知道要赋何值的,Kotlin强制初始化赋值让整个代码看起来很“怪异”。对我来说,如果一个变量可为null时,它应该是隐含地就默认给予了null值。

我希望应该是这样的,

class A { var a: String? //默认值为null var b: String? //默认值为null var c: String? //默认值为null } 虽然说Kotlin提供了lateinit类型懒加载的方式进行初始化,但是也并不能很好的支持全部情况,它只能用于var的属性,并且只能在属性没有自定义getter或者setter时候使用。属性的类型必须是非空值,并且它不能使原始类型。

当然,我们换个角度,从语言设计的角度来说,Kotlin这么设计又是很合理的。所有属性要求强制显式的初始化能够更容易的推理代码,明确每个属性在何时何地初始化。

总的来说,空安全机制所做的事情就是,让我们原本在逻辑代码中进行大量判空的工作转移到了初始化上,并很大程度地减轻了工作量。

更优雅,遵循Effective Java设计

  1. 类默认不可继承 《Effective Java》提出一种观点:组合优于继承,避免滥用继承。要么为继承而设计,并提供文档说明,要么就禁止继承。至于为什么这么说呢,我见过一句话很形象:摊开来的代码,比叠起来的代码,更加一目了然。详细可以自行阅读《Effective Java》。

Kotlin默认类是final类型的,即每个类默认不可继承。只有你真的需要继承的时候,再通过open声明使用,声明方式如下:

open class A

  1. 更有效地使用构建器模式 我们建议使用构建器模式,当Java的构造器存在多个可选的参数时,情况就会变得很复杂,代码冗长,也更容易出错。Kotlin提供了一种更有效的构造器方式,通过默认参数的功能实现。

data class Person(var name: String, var age: Int = 18, var sex: Int = 0, var height: Float = 1.8f var weight: Float = 60f)

//创建对象 val person: Person = Person(name="shinelw", age = 10, height = 1.7f)

  1. 单例模式 Kotlin默认提供了单例模式的模板,通过object关键字即可实现。

object Singleton { //各种函数 fun a(){...} ... }

//使用时 Singleton.a() 完全不需要手动构建,看上去很好。但是我有一点质疑,它是不是线程安全的,是不是懒加载的。随后通过Kotlin编译器得到字节码,然后再反编译回Java代码。是长这样子的:

public class Singleton { public static final Singleton INSTANCE;

public final void a() { }

private Singleton() { INSTANCE = (Singleton)this; }

static { new Singleton(); } } 糟糕的是,从上面代码可以看出,Kotlin的object只是一个最简单的饿汉式的单例模式。在第一次加载类到内存的时候就会初始化,虽然它是线程安全的,但是不完美,对吗?

如果你是一个追求完美的人,下面是类似于静态内部类方式实现的单例模式,懒加载且线程安全。缺点是跟Java一样,需要手动构建。:)

class Singleton private constructor(){ companion object { fun getInstance(): Singleton { return SingletonHolder.instance } } private object SingletonHolder { val instance: Singleton = Singleton() }

//各种函数..
fun a(){}

}

  1. 重载必须使用override Java中对于重载的注解@Override不是强制的,一旦项目代码很复杂,这将是一场灾难。当你分不清哪些是重载方法时,对方法进行参数修改是灾难性的。Kotlin基于这点,要求重载方法时必须加上override关键字。如果没写,编译器将会报错,强制你加上。

override fun a(){...} 完全兼容,与Java互操作 这是Kotlin与Scala相比,优势突出的一点。我们可以在Kotlin中调用现存的Java代码,并且也能在Java代码中顺利的调用Kotlin代码。这意味着我们可以马上在现有的Java项目中使用上Kotlin,同时所有之前旧的Java也一样有效。

这是很关键,也是我之所以很看好Kotlin的一个原因。

至于怎么相互调用操作,请大家看官方文档关于Java互操作的部分。

这里只说一个方面,关于空安全方面。

因为Java中的任何应用都可以为null,但是在Kotlin中是默认不可为null的,这使得Kotlin对来自Java的对象要求严格空安全是不现实的。Java声明的类型在Kotlin中会被特别对待,称之为平台类型。对这种类型的空检查会放宽,因此他们的安全保证与Java中是相同的。

看下面的例子:

public class Person{ public String getName(){ return null; } } val person = Person() val name = person.name // 编译通过 运行值为null name本是非null变量,因为调用Java对象所以变成平台类型,放宽了类型空检查。

当然,如果你想要延续Kotlin严格空安全机制的话,可是有办法滴。我们需要在编写Java代码时加上@NotNull注解,这个很熟悉吧,在介绍空安全机制的时候说过Kotlin在实现默认非null属性就是这么实现的。然后代码就变成了这样,

public class Person{ public @NotNull String getName(){ return null; } } 然后在运行的时候就会报以下的错误:

Exception in thread "main" java.lang.IllegalStateException: @NotNull method Person.getName must not return null at Person.$$$reportNull$$$0(Person.java) at Person.getName(Person.java:9) at MainKt.main(main.kt:7) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147) 总结 总而言之,经过短短一个月的Kotlin使用,在实际项目中开发展现出的特性应用是让我感到兴奋的。作为一名开发者,在我眼里,Kotlin设计出来不是抛开Java谈的,而是在Java的毛病的基础上,进行的再开发,拥有很多其他语言优秀的特性,同时完全兼容Java。毕竟,对于一家大企业来讲,为了一门新语言完全摒弃一个很成熟的项目进行再开发是不现实的。相反的是,对于项目中Java难于处理的逻辑,Kotlin的优势一览无余,相辅相成,Kotlin和Java配合使用时目前最完美的方案。

但不可否认的是,Kotlin真的让人感到潜力十足,值得大家去试一试。

参考资料:

1.http://kotlinlang.org/docs/reference/java-interop.html

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Python自动化测试

python接口测试之序列化与反序列化(四)

在python中,序列化可以理解为:把python的对象编码转换为json格式的字符串,反序列化可以理解为:把json格式字符串解码为python...

1464
来自专栏DT乱“码”

ClassPathXmlApplicationContext方式读取配置文件

public interface BeanFactory {   public Object getBean(String id); }   //实现类Clas...

2055
来自专栏Java学习网

Java Web中pageContext的20个实用方法——开发常用

pageContext属性默认在当前页面是共享的 pageContext表示一个javax.servlet.jsp.PageContext对象。pageCont...

2445
来自专栏Android知识点总结

Java中的字符流

1082
来自专栏数据结构与算法

3339: Rmq Problem

Description image.png Input image.png Output image.png Sample Input 7 5 ...

34011
来自专栏Android开发指南

Android回调接口的写法

1.4K5
来自专栏一个会写诗的程序员的博客

第12章 元编程与注解、反射第12章 元编程与注解、反射

反射(Reflection)是在运行时获取类的函数(方法)、属性、父类、接口、注解元数据、泛型信息等类的内部信息的机制。这些信息我们称之为 RTTI(Run-T...

992
来自专栏Java帮帮-微信公众号-技术文章全总结

Java基础-20(01)总结,递归,IO流

1:递归(理解) (1)方法定义中调用方法本身的现象 举例:老和尚给小和尚讲故事,我们学编程 (2)递归的注意事项; A:要有出口,否则就是死递归 B...

3569
来自专栏ImportSource

面试官问他Java中“a”+“b”究竟会产生几个对象?

有人在群里提问说“刚才面试官问他:‘Java中“a”+“b”究竟会产生几个对象?’”。

1362
来自专栏技术小黑屋

关于Java中枚举Enum的深入剖析

在编程语言中我们,都会接触到枚举类型,通常我们进行有穷的列举来实现一些限定。Java也不例外。Java中的枚举类型为Enum,本文将对枚举进行一些比较深入的剖析...

1313

扫码关注云+社区

领取腾讯云代金券