大多数状况下,你不须要关注这个问题。可是,若是你的代码中包含了部分Java代码,理解这些注解将帮助你解决不少棘手问题。java
产生这个问题的根本缘由在于:Kotlin语言与Java语言的设计思路不一样,部分特性属于Java语言独有,例如静态变量。部分特性属于Kotlin语言独有,例如逆变和协变。编程
为了抹平这些差别,Kotlin语言提供了一个绝佳的思路,经过添加注解能够改变Kotlin编译器生成的Java字节码,使之按照Java语言能够理解的方向进行,从而实现兼容。安全
问题答疑:Kotlin语言与Java字节码有什么关系?为何Kotlin编译器会生成Java字节码?bash
不论是Kotlin语言仍是Java语言都是创建在JVM平台上面的编程语言,其最终都须要编译成JVM能够识别的Java字节码才能被正确执行。这也是为何Kotlin语言与Java能够彻底互通的缘由之一,不要将Java与Java平台混为一谈。微信
接下来咱们先来看第一个注解,也是最经常使用到的一个注解:多线程
Kotlin编译器默认会将类中声明的成员变量编译成私有变量,Java语言要访问该变量必须经过其生成的getter方法。而使用上面的注解能够向Java暴露该变量,即便其访问变为公开(修饰符变为public)。并发
咱们来作一个实验:框架
1)新建Person.kt
,添加以下代码:编程语言
class Person {
@JvmField
var name: String? = null
}
复制代码
2)新建Client.java
,添加以下代码,尝试访问Person
类中的变量name
:ui
public class Client {
public static void main(String[] args) {
Person p = new Person();
// 在添加@JvmField注解以前,这样访问会报错
// 只能经过p.getName()的方式进行访问
String name = p.name;
}
}
复制代码
在添加@JvmField
属性前咱们试图经过p.name
的方式进行访问,编译器出现报错。由于,默认生成的成员变量name
是私有的。而添加该注解以后咱们竟然能够正常访问了。
因而可知,@JvmField
注解的确使生成的字节码发生了变化,咱们将字节码用Java代码来表示,具体发生的变化相似下面代码发生的变化:
添加注解以前
public final class Person {
private String name;
public final String getName() {
return this.name;
}
public final void setName(@Nullable String var1) {
this.name = var1;
}
}
复制代码
添加注解以后
public final class Person {
public String name;
}
复制代码
以上场景是将@JvmField
注解添加到普通变量上方,若是添加到伴随对象的成员变量上方,会发生什么呢?咱们来试试看:
class Person {
var name: String? = null
companion object {
@JvmField
val GENDER_MALE = 1
}
}
复制代码
public static void main(String[] args) {
// 未添加以前
// int gender = Person.Companion.getGENDER_MALE();
// 添加以后,可直接访问
int gender = Person.GENDER_MALE;
System.out.println(gender);
}
复制代码
一样地,添加注解以后咱们能够经过点语法直接对其进行访问。
因而可知,@JvmField
注解会使伴随对象在伴生类中生成静态成员变量,经过伴生类可直接对其进行访问。
@JvmField
注解可改变字节码的生成,其做用的目标是类成员变量或伴随对象成员变量。做用在类成员中可以使该变量对外暴露,经过点语法直接访问。即将私有成员变量公有化(public),并去掉setter/getter方法。做用在伴随对象成员变量中,可使该伴随对象中的变量生成在伴生对象中,成为伴生对象的公有静态成员变量,经过伴生类可直接访问。
那么问题来了,若是该注解做用在私有成员变量上方会发生什么呢?请你们自行验证。
这个注解与@JvmField
很是容易出现混淆,二者均可以做用在伴随对象成员变量上方,咱们来试试看,若是一样做用在伴随对象成员变量中,会出现什么状况。
添加@JvmField
注解的效果,上面咱们已经看到了,咱们直接将注解修改成@JvmStatic
试试看:
class Person {
var name: String? = null
companion object {
@JvmStatic
val GENDER_MALE = 1
}
}
复制代码
public static void main(String[] args) {
// 1) 这样访问报错
int gender = Person.GENDER_MALE;
// 2) 这样访问正常
int gender = Person.Companion.getGENDER_MALE();
// 3) 这样访问也正常
int gender = Person.getGENDER_MALE();
System.out.println(gender);
}
复制代码
切换到Java代码,你能够看到,我一共提供了三种访问方式。第一种访问方式是经过点语法直接访问,编译器报错,因而可知,@JvmStatic
注解并无在伴生类中生成静态的公有成员变量。第三种方式能够正常访问,证实该注解在伴生类中生成了静态的公有getter方法。第二种方式能够正常访问,证实该注解不会破坏伴随对象中原有成员的访问方式。
由此,咱们能够大胆猜想,@JvmStatic
注解的做用应该是生成静态的setter/getter方法,而不会改变属性(成员变量)的访问权限。
为了进一步验证咱们的猜测,咱们将val
修改成var
试试看。
public static void main(String[] args) {
// 1) 这样访问报错
int gender = Person.GENDER_MALE;
// 2) 这样访问正常
int gender = Person.Companion.getGENDER_MALE();
// 3) 这样访问也正常
int gender = Person.getGENDER_MALE();
// 4) 如下访问正常
Person.setGENDER_MALE(1);
System.out.println(gender);
}
复制代码
第四种方式调用正常,证实咱们的猜想没有错,@JvmStatic
仅会改变伴随对象或对象(object)中setter/getter方法的生成方式,而不会改变属性访问权限,这是与注解@JvmField
的本质区别。
注意:因为@JvmField
不只会改变属性的访问权限,同时也会改变setter/getter方法的生成,细心的同窗应该已经注意到了。一旦添加了@JvmField
注解,setter/getter方法也消失了(变量能够经过点语法直接访问,setter/getter方法也就不必存在了)。而@JvmStatic
仅仅是使setter/getter方法变为静态方法,同时生成位置放置到伴生类中。这与@JvmField
的处理方式有些冲突(@JvmField
会直接删除掉setter/getter方法)。为了不冲突,Kotlin语言禁止将这两个注解混淆使用。
以上是将@JvmStatic
与@JvmField
做用在伴随对象成员变量上的区别。实际上,@JvmStatic
不只能够修饰属性(成员变量),还能够修饰方法,修饰方法的做用与修饰属性的做用一致,都是将方法变成静态类型。
为了更直观地表示两种的区别,咱们用一个表格完整展现两个注解的区别:
注解 | 做用位置 | 做用 |
---|---|---|
@JvmField |
类属性或对象属性 | 使属性修饰符成为public |
@JvmStatic |
对象方法(包括伴生对象) | 使用方法成为静态类型,若是做用在伴生对象方法中,其方法会成为伴生类的静态方法 |
这个注解能够改变字节码中生成的类名或方法名称,若是做用在顶级做用域(文件中),则会改变生成对应Java类的名称。若是做用在方法上,则会改变生成对应Java方法的名称。
Test.kt
@file:JvmName("FooKt")
@JvmName("foo1")
fun foo() {
println("Hello, Jvm...")
}
复制代码
在Kotlin语言中,foo
是一个全局方法,为了兼容Java字节码,实际会根据文件名生成对应的Java类TestKt.java
,这是Kotlin编译器的一个隐藏规则。
而添加了上述注解以后,生成的类名与方法名均发生了变化,具体产生的变化至关于下面这段Java代码:
// 至关于下面的Java代码
public final class FooKt {
public static final void foo1() {
String var0 = "Hello, Jvm...";
System.out.println(var0);
}
}
复制代码
能够看到第一个注解@file:JvmName("FooKt")
的做用是使生成的类名变为FooKt
,第二个注解的做用是使生成的方法名称变为foo1
。
注意:该注解不能改变类中生成的属性(成员变量)的名称。
这里的注解中,咱们看到了一个特殊的前缀@file:
,这个注解前缀是Kotlin语言特有的一种标识,其做用是标记该注解最终会做用在生成的字节码的具体位置(属性、setter、getter等),关于这个部分,你们能够先跳过,下一篇文章将给你们详细讲解。
说完了上面这个注解,就不得不提到@JvmMultifileClass
这个注解,这个注解一般是与@JvmName
结合使用的。其使用场景比较单一,看下面的例子:
新建文件Util1.kt,添加以下代码:
@file:JvmName("Utils")
fun isEmpty(str: String?): Boolean {
return null == str || str.length <= 0
}
复制代码
新建文件Util2.kt,添加以下代码:
@file:JvmName("Utils")
fun isPhoneNumber(str: String): Boolean {
return str.startsWith("1") && str.length == 11
}
复制代码
编译以上代码,Kotlin编译器会提示错误Error:(1, 1) Kotlin: Duplicate JVM class name 'Utils' generated from: package-fragment, package-fragment
,即生成的类名出现了重复。但是,若是咱们就是但愿声明使用多个文件,但方法生成到同一个类中呢?@JvmMultifileClass
就是为解决这个问题而生的。
咱们在上面代码的基础上分别添加注解@JvmMultifileClass
试试看:
@file:JvmName("Utils")
@file:JvmMultifileClass
fun isEmpty(str: String?): Boolean {
return null == str || str.length <= 0
}
复制代码
@file:JvmName("Utils")
@file:JvmMultifileClass
fun isPhoneNumber(str: String): Boolean {
return str.startsWith("1") && str.length == 11
}
复制代码
添加注解@JvmMultifileClass
以后,报错消失了,反编译生成的字节码,咱们发生两个不一样文件中的方法合并到了同一个类Utils
中:
// 生成的代码至关于下面这段Java代码
public final class Utils {
public static final boolean isEmpty(@Nullable String str) {
return Utils__A1Kt.isEmpty(str);
}
public static final boolean isPhoneNumber(@NotNull String str) {
return Utils__A2Kt.isPhoneNumber(str);
}
}
复制代码
这个注解在处理多个文件声明,合并到一个类的场景中发挥着举足轻重的做用。若是你有这样的需求,必定要谨记这个注解。
因为Kotlin语言支持方法参数默认值,而实现相似功能Java须要使用方法重载来实现,这个注解就是为解决这个问题而生的,添加这个注解会自动生成重载方法。咱们来试一下:
@JvmOverloads
fun foo(x: Int, y: Int = 0, z: Int = 0): Int {
return x + y + z
}
复制代码
// 生成的代码至关于下面这段Java代码
public static final int foo(int x, int y, int z) {
return x + y + z;
}
public static final int foo(int x, int y) {
return foo(x, y, 0);
}
public static final int foo(int x) {
return foo(x, 0, 0);
}
复制代码
因而可知,经过这个注解能够影响带有参数默认值方法的生成,添加该注解将自动生成带有默认值参数数量的重载方法。这是一个很是有用的特性,方便Java端能够更高效地调用Kotlin端代码。
因为Kotlin语言不支持CE(Checked Exception),所谓CE,即方法可能抛出的异常是已知的。Java语言经过throws
关键字在方法上声明CE。为了兼容这种写法,Kotlin语言新增了@Throws
注解,该注解的接收一个可变参数,参数类型是多个异常的KClass实例。Kotlin编译器经过读取注解参数,在生成的字节码中自动添加CE声明。
为了便于理解,看一个简单的例子:
@Throws(IllegalArgumentException::class)
fun div(x: Int, y: Int): Float {
return x.toFloat() / y
}
复制代码
// 生成的代码至关于下面这段Java代码
public static final float div(int x, int y) throws IllegalArgumentException {
return (float)x / (float)y;
}
复制代码
能够看到,添加了@Throws(IllegalArgumentException::class)
注解后,在生成的方法签名上自动添加了可能抛出的异常声明(throws IllegalArgumentException),即CE。
这个注解在保证逻辑的严谨性方面很是有用,但若是你的工程中仅使用Kotlin代码,能够不用理会该注解。在Kotlin语言的设计哲学里面,CE被认为是一个错误的设计。
这个注解很容易理解,顾名思义,主要用于产生同步方法。Kotlin语言不支持synchronized
关键字,处理相似Java语言的并发问题,Kotlin语言建议使用同步方法进行处理。
Kotlin团队认为同步的逻辑应该交给代码处理,而不该该在语言层面处理:
但为了兼容Java,Kotlin语言支持使用该注解让编译器自动生成同步方法:
@Synchronized
fun start() {
println("Start do something...")
}
复制代码
// 生成的代码至关于下面这段Java代码
public static final synchronized void start() {
String var0 = "Start do something...";
System.out.println(var0);
}
复制代码
这个注解主要用于处理泛型参数,这涉及到两个新的知识点:逆变与协变。因为Java语言不支持协变,为了保证安全地相互调用,能够经过在泛型参数声明的位置添加该注解使用Kotlin编译器生成通配符形式的泛型参数(?extends ...
)。
看下面这段代码:
class Box<out T>(val value: T)
interface Base
class Derived : Base
fun boxDerived(value: Derived): Box<Derived> = Box(value)
fun unboxBase(box: Box<Base>): Base = box.value
复制代码
按照正常思惟,下面的两个方法转换到Java代码应该是这样:
Box<Derived> boxDerived(Derived value) { …… }
Base unboxBase(Box<Base> box) { …… }
复制代码
但问题是,Kotlin泛型支持型变,在Kotlin中,咱们能够这样写unboxBase(Box(Derived()))
,而在Java语言中,泛型参数类型是不可变的,按照上面的写法显然已经作不到了。
正确转换到Java代码应该是这样:
Base unboxBase(Box<? extends Base> box) { …… }
复制代码
为了使这样的转换正确生成,咱们须要在泛型参数的位置添加上面的注解:
fun unboxBase(box: Box<@JvmWildcard Base>): Base = box.value
复制代码
这个注解的做用与@JvmWildcard
偏偏相反,它是用来抑制通配符泛型参数的生成,即在不须要型变泛型参数的状况下,咱们能够经过添加这个注解来避免生成型变泛型参数。
fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value
复制代码
// 生成的代码至关于下面这段Java代码
Base unboxBase(Box<Base> box) { …… }
复制代码
正确使用上述注解,能够抹平Kotlin与Java泛型处理的差别,避免出现安全转换问题。
这两个注解刚好对应Java端的两个关键字volatile
与transient
,前者主要用于解决多线程脏数据问题,后者用于标记序列化对象中不参与序列化的属性。
这两个注解比较简单,就不举例说明了。在遇到相似须要与Java互通的场景时,只须要将其关键字替换为该注解便可。
以上就是咱们平常开发过程当中可以遇到的全部注解了,在Kotlin 1.3版本中,还增长了一个新的注解@JvmDefault
用于在接口中处理默认实现的方法。接口中容许有默认实现是从JDK 1.8版本开始的,为了兼容低版本JDK,Kotlin语言新增了该注解用于生成兼容性字节码,但该注解目前仍处于实验阶段,名称或行为都可能发生改变,建议你们先不要使用,推荐你们始终使用JDK 1.8及其以上版本。
若是在工程中必须存在部分Java代码,为了实现完美调用,必定要谨慎并正确地使用上述注解。要充分理解Kotlin编译器与Java编译器生成的字节码差别。
若是是因为现存Java库仅兼容Java字节码,致使部分框架在遇到Kotlin语言生成的字节码时会出现解析错误,不能正常使用。这个时候要尝试检查是否须要经过上述注解矫正字节码的生成,使Java库可以正常使用。
若是是新工程,建议你们所有使用Kotlin代码,避免出现上述注解,减小阅读上的困难。目前,Kotlin版本已经很是稳定了,请你们放心使用。
阅读更多技术文章,请关注微信公众号”欧阳锋工做室“
参与Kotlin技术讨论,请添加惟一官方QQ交流群:329673958