阅读完需:约 32 分钟
一、注解的本质
注解实际上就是一种代码标签,它作用的对象是代码。它可以给特定的注解代码标注一些额外的信息。然而这些信息可以选择不同保留时期,比如源码期、编译期、运行期。然后在不同时期,可以通过某种方式获取标签的信息来处理实际的代码逻辑,这种方式常常就是我们所说的反射。
二、注解的定义
在Kotlin中注解核心概念和Java一样,注解就是为了给代码提供元数据。并且注解是不直接影响代码的执行。一个注解允许你把额外的元数据关联到一个声明上,然后元数据就可以被某种方式(比如运行时反射方式以及一些源代码工具)访问。
三、注解的声明(标签的声明)
在Kotlin中的声明注解的方式和Java稍微不一样,在Java中主要是通过 @interface关键字来声明,而在Kotlin中只需要通过 annotation class 来声明, 需要注意的是在Kotlin中编译器禁止为注解类指定类主体,因为在Kotlin中注解只是用来定义关联的声明和表达式的元数据的结构。
- 1、Kotlin注解声明
package com.mikyou.annotation
//和一般的声明很类似,只是在class前面加上了annotation修饰符
annotation class TestAnnotation(val value: String)
- 2、Java注解声明
package com.mikyou.annotation;
//java中的注解通过@interface关键字进行定义,它和接口声明类似,只不过在前面多加@
public @interface TestAnnotation {
String value();
}
四、注解的应用
1、在上一步我们知道了如何声明和定义标签了,那么接下来就是用这个标签,如何把我们定义好的标签贴到指定的代码上。在Kotlin中使用注解和Java一样。要应用一个注解都是 @注解类名。
@Target(AnnotationTarget.FUNCTION)
@Retention(value = AnnotationRetention.RUNTIME)
annotation class TestAnnotation(val value: Int)//和一般的声明很类似,只是在class前面加上了annotation修饰符
class Test {
@TestAnnotation(value = 1000)
fun test() {//给test函数贴上TestAnnotation标签(添加TestAnnotation注解)
//...
}
}
2、在很多常见的Java或Kotlin框架中大量使用了注解,比如我们最常见的JUnit单元测试框架
class ExampleUnitTest {
@Test //@Test注解就是为了告诉JUnit框架,这是一个测试方法,当做测试调用。
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
3、在Kotlin中注解类中还可以拥有注解类作为参数,不妨来下Kotlin中对 @Deprecated这个注解源码定义,以及它的使用。@Deprecated注解在原来的Java基础增强了一个ReplaceWith功能. 可以直接在使用了老的API时,编译器可以根据ReplaceWith中的新API,自动替换成新的API。这一点在Java中是做不到的,你只能点击进入这个API查看源码来正确使用新的API。
//@Deprecated注解比Java多了ReplaceWith功能, 这样当你在调用remove方法,编译器会报错。使用代码提示会自动IntelliJ IDEA不仅会提示使用哪个函数提示替代它,而且会快速自动修正。
@Deprecated("Use removeAt(index) instead.", ReplaceWith("removeAt(index)"), level = DeprecationLevel.ERROR)//定义的级别是ERROR级别的,这样当你在调用remove方法,编译器会报错。
@kotlin.internal.InlineOnly
public inline fun <T> MutableList<T>.remove(index: Int): T = removeAt(index)
@Deprecated注解的remove函数使用
//Deprecated注解的使用
fun main(args: Array<String>) {
val list = mutableListOf("a", "b", "c", "d", "e")
list.remove(3)//这里会报错, 通过remove函数注解定义,这个remove函数在定义的level是ERROR级别的,所以编译器直接抛错
}
最后来看下 @Deprecated
注解的定义
@Target(CLASS, FUNCTION, PROPERTY, ANNOTATION_CLASS, CONSTRUCTOR, PROPERTY_SETTER, PROPERTY_GETTER, TYPEALIAS)
@MustBeDocumented
public annotation class Deprecated(
val message: String,
val replaceWith: ReplaceWith = ReplaceWith(""),//注解类中构造器可以使用注解类作为函数参数
val level: DeprecationLevel = DeprecationLevel.WARNING
)
@Target()
@Retention(BINARY)
@MustBeDocumented
public annotation class ReplaceWith(val expression: String, vararg val imports: String)
注意: 注解类中只能拥有如下类型的参数: 基本数据类型、字符串、枚举、类引用类型、其他的注解类(例如Deprecated注解类中的ReplaceWith注解类)
五、Kotlin中的元注解
和Java一样在Kotlin中,一个Kotlin注解类自己本身也可以被注解,可以给注解类加注解。我们把这种注解称为元注解,可以把它理解为一种基本的注解,可以把它理解为一种特殊的标签,用于标注标签的标签。
Kotlin中的元注解类定义于 kotlin.annotation
包中,主要有:
- @Target
- @Retention
- @Repeatable
- @MustBeDocumented
4种元注解
相比Java中5种元注解:
- @Target
- @Retention
- @Repeatable
- @Documented
- @Inherited
少了 @Inherited 元注解。
@Target元注解
- 1、介绍
Target顾名思义就是目标对象,也就是这个标签作用于哪些代码中目标对象,可以同时指定多个作用的目标对象。
- 2、源码定义
@Target(AnnotationTarget.ANNOTATION_CLASS)//可以给标签自己贴标签
@MustBeDocumented
//注解类构造器参数是个vararg不定参数修饰符,所以可以同时指定多个作用的目标对象
public annotation class Target(vararg val allowedTargets: AnnotationTarget)
- 3、@Target元注解作用的目标对象
在@Target注解中可以同时指定一个或多个目标对象,那么到底有哪些目标对象呢?这就引出另外一个AnnotationTarget枚举类
public enum class AnnotationTarget {
CLASS, //表示作用对象有类、接口、object对象表达式、注解类
ANNOTATION_CLASS,//表示作用对象只有注解类
TYPE_PARAMETER,//表示作用对象是泛型类型参数(暂时还不支持)
PROPERTY,//表示作用对象是属性
FIELD,//表示作用对象是字段,包括属性的幕后字段
LOCAL_VARIABLE,//表示作用对象是局部变量
VALUE_PARAMETER,//表示作用对象是函数或构造函数的参数
CONSTRUCTOR,//表示作用对象是构造函数,主构造函数或次构造函数
FUNCTION,//表示作用对象是函数,不包括构造函数
PROPERTY_GETTER,//表示作用对象是属性的getter函数
PROPERTY_SETTER,//表示作用对象是属性的setter函数
TYPE,//表示作用对象是一个类型,比如类、接口、枚举
EXPRESSION,//表示作用对象是一个表达式
FILE,//表示作用对象是一个File
@SinceKotlin("1.1")
TYPEALIAS//表示作用对象是一个类型别名
}
@Retention元注解
- 1、介绍
Retention对应的英文意思是保留期,当它应用于一个注解上表示该注解保留存活时间,不管是Java还是Kotlin一般都有三种时期: 源代码时期(SOURCE)、编译时期(BINARY)、运行时期(RUNTIME)。
- 2、源码定义
@Target(AnnotationTarget.ANNOTATION_CLASS)//目标对象是注解类
public annotation class Retention(val value: AnnotationRetention = AnnotationRetention.RUNTIME)//接收一个参数,该参数有个默认值,默认是保留在运行时期
- 3、@Retention元注解的取值
@Retention元注解取值主要来源于AnnotationRetention
枚举类
public enum class AnnotationRetention {
SOURCE,//源代码时期(SOURCE): 注解不会存储在输出class字节码中
BINARY,//编译时期(BINARY): 注解会存储出class字节码中,但是对反射不可见
RUNTIME//运行时期(RUNTIME): 注解会存储出class字节码中,也会对反射可见, 默认是RUNTIME
}
@MustBeDocumented元注解
- 1、介绍
该注解比较简单主要是为了标注一个注解类作为公共API的一部分,并且可以保证该注解在生成的API文档中存在。
- 2、源码定义
@Target(AnnotationTarget.ANNOTATION_CLASS)//目标对象只能是注解类
public annotation class MustBeDocumented
@Repeatable元注解
- 1、介绍
这个注解决定标注的注解在一个注解在一个代码元素上可以应用两次或两次以上。
- 2、源码定义
@Target(AnnotationTarget.ANNOTATION_CLASS)//目标对象只能是注解类
public annotation class Repeatable
为啥Kotlin去掉了Java中的@Inherited元注解
- 1、Java中的@Inherited元注解介绍
Inheried顾名思义就是继承的意思,但是这里需要注意并不是表示注解类可以继承,而是如果一个父类被贴上@Inherited元注解标签,那么它的子类没有任何注解标签的话,这个子类就会继承来自父类的注解。类似下面的例子:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}
@TestAnnotation
class Animal {
//...
}
class Cat extends Animal{//也会拥有来自父类Animal的@TestAnnotation注解
//...
}
- 2、Kotlin为啥不需要@Inherited元注解
我们都知道在Java中,无法找到子类方法是否重写了父类的方法。因此不能继承父类方法的注解。然而Kotlin目前不需要支持这个@Inherited元注解,因为Kotlin可以做到,如果反射提供了override
标记而且很容易做到。
六、注解的使用场景
- 1、提供信息给编译器: 编译器可以利用注解来处理一些,比如一些警告信息,错误等
- 2、编译阶段时处理: 利用注解信息来生成一些代码,在Kotlin生成代码非常常见,一些内置的注解为了与Java API的互操作性,往往借助注解在编译阶段生成一些额外的代码。
- 3、运行时处理: 某些注解可以在程序运行时,通过反射机制获取注解信息来处理一些程序逻辑。
七、Kotlin中的预置注解
在Kotlin中最大的一个特点就是可以和Java做到极高的互操作性,我们知道Kotlin的语法和Java语法还是有很大的不同,要想做到与Java做到很大兼容性可能需要携带一些额外信息,供编译器或者运行时做类似兼容转换。其中注解就起到了很大的作用,在Kotlin内置很多个注解为了解决Java中的调用Kotlin API的一些调用习惯和控制API的调用。它们就是Kotlin中的@Jvm系列的注解,咱们一一来看下它们都有哪些。
@JvmDefault
- 1、作用
我们都知道在Kotlin中的接口中可以增加非抽象成员,那么该注解就是为非抽象的接口成员生成默认的方法。
使用-Xjvm-default = enable
,会为每个@JvmDefault
注解标注的方法生成接口中的默认方法。在此模式下,使用@JvmDefault
注解现有方法可能会破坏二进制兼容性,因为它将有效地从DefaultImpls
类中删除该方法。
使用-Xjvm-default = compatibility
,除了默认接口方法之外,还会生成兼容性访问器。在DefaultImpls
类中,它通过合成访问器调用默认接口方法。在此模式下,使用@JvmDefault
注解现有方法是二进制兼容的,但在字节码中会产生更多方法。从接口成员中移除此注解会使在两种模式中的二进制不兼容性发生变化。
- 2、源码定义
@SinceKotlin("1.2")//从Kotlin的1.2版本第一次出现该注解
@RequireKotlin("1.2.40", versionKind = RequireKotlinVersionKind.COMPILER_VERSION)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)//目标对象是函数和属性
annotation class JvmDefault
- 3、使用注解前后反编译java代码对比
未使用 @JvmDefault
注解
interface ITeaching {
fun speak() = println("open the book")
}
class ChineseTeacher : ITeaching
fun main(args: Array<String>) {
ChineseTeacher().speak()
}
反编译成Java代码
public interface ITeaching {
void speak();
public static final class DefaultImpls {//可以看到在接口为speak函数生成一个DefaultImpls静态内部类
public static void speak(ITeaching $this) {
String var1 = "open the book";
System.out.println(var1);
}
}
}
public final class ChineseTeacher implements ITeaching {
public void speak() {
ITeaching.DefaultImpls.speak(this);//注意:这里却是直接调用ITeaching中静态内部类DefaultImpls的speak方法。
}
}
public final class JvmDefaultTestKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
(new ChineseTeacher()).speak();//这里会调用ChineseTeacher中的speak方法
}
}
使用 @JvmDefault
注解
interface ITeaching {
@JvmDefault//注意: 可能一开始使用该注解会报错,需要在gradle中配置jvm参数:-jvm-target=1.8 -Xjvm-default=enable
fun speak() = println("open the book")
}
class ChineseTeacher : ITeaching
fun main(args: Array<String>) {
ChineseTeacher().speak()
}
反编译成Java代码
public interface ITeaching {
@JvmDefault
default void speak() {//添加注解后外层的静态内部类被消除
String var1 = "open the book";
System.out.println(var1);
}
}
public final class ChineseTeacher implements ITeaching {//内部并没有类似上面的speak方法调用的桥接委托
}
public final class JvmDefaultTestKt {
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
(new ChineseTeacher()).speak();
}
}
总而言之,在没有添加 @JvmDefault注解,Kotlin会自动生成一个叫做 DefaultImpl静态内部类,用于保存静态方法的默认实现,并使用自身接收器类型来模拟属于对象的方法。然后,对于扩展该接口的每种类型,如果类型没有实现方法本身,则在编译时,Kotlin将通过调用将方法连接到默认实现。
这样一来确实带来一个很大好处就是在JDK1.8之前的版本JVM上提供了在接口上也能定义具体的实现方法功能。但是这样也存在一些问题:
第一问题: 比如它和现在的Java的处理方式不兼容,这样会导致互操作性极度下降。我们甚至可以在Java中直接去调用自动生成的DefaultImpls
,类似这样的调用ITeaching.DefaultImpls.speak(new ChineseTeacher());
,这样内部的细节居然也能暴露给外部,这样更会调用者一脸懵逼。
第二问题: Java 8中存在默认方法的主要原因之一是能够向接口添加方法而无需侵入每个子类。 然而Kotlin实现不支持这个原因是必须在每个具体类型上生成默认调用。 向接口添加新方法导致必须重新编译每个实现者。
基于上述问题,Kotlin推出了 @JvmDefault注解
@JvmField
- 1、作用
可以应用于一个字段,把这个属性暴露成一个没有访问器的公有Java字段;以及Companion Object对象中。
- 2、源码定义
@Target(AnnotationTarget.FIELD)//作用对象是字段,包括属性的幕后字段
@Retention(AnnotationRetention.BINARY)//注解保留期是源码阶段
@MustBeDocumented
public actual annotation class JvmField
- 3、注解使用
使用场景一:
我们知道在Kotlin中默认情况下,Kotlin类不会公开字段而是会公开属性.Kotlin会为属性的提供幕后字段,这些属性将会以字段形式存储它的值。一起来看个例子
//Person类中定义一个age属性,age属性默认是public公开的,但是反编译成Java代码,你就会看到它的幕后字段了。
class Person {
var age = 18
set(value) {
if (value > 0) field = value
}
}
反编译成Java代码
public final class Person {
private int age = 18;//这个就是Person类中的幕后字段,可以看到age字段是private私有的。
//外部访问通过setter和getter访问器来操作。由于Kotlin自动生成setter、getter访问器,所以外部可以直接类似公开属性操作,
//实际上内部还是通过setter、getter访问器来实现
public final int getAge() {
return this.age;
}
public final void setAge(int value) {
if (value > 0) {
this.age = value;
}
}
}
但是如果在Kotlin需要生成一个公开的字段怎么实现呢?那就要借助@JvmField
注解了,它会自动将该字段的setter、getter访问器消除掉,并且把这个字段修改为public
class Person {
@JvmField
var age = 18
}
反编译成的Java代码
public final class Person {
@JvmField
public int age = 18;//消除了setter、getter访问器,并且age字段为public公开
}
使用场景二:
@JvmField
另一个经常使用的场景就是用于Companion Object
伴生对象中。
未使用@JvmField
注解
class Person {
companion object {
val MAX_AGE = 120
}
}
反编译成Java代码
public final class Person {
private static final int MAX_AGE = 120;//注意: 这里默认是private私有的MAX_AGE,所以在Java中调用无法直接通过Person类名.变量名访问
public static final Person.Companion Companion = new Person.Companion((DefaultConstructorMarker)null);
public static final class Companion {
//在Java中调用无法直接通过Person类名.变量名访问,
//而是通过静态内部类Companion的getMAX_AGE间接访问,类似这样Person.Companion.getMAX_AGE();
public final int getMAX_AGE() {
return Person.MAX_AGE;
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
但是如果使用该注解就能直接通过Person类名.变量名访问
class Person {
companion object {
@JvmField
val MAX_AGE = 120
}
}
//在Java中调用
public static void main(String[] args) {
System.out.println(Person.MAX_AGE);//可以直接调用,因为它已经变成了public了
}
反编译成Java代码
public final class Person {
@JvmField
public static final int MAX_AGE = 120;//公有的MAX_AGE的,外部可以直接调用
public static final Person.Companion Companion = new Person.Companion((DefaultConstructorMarker)null);
public static final class Companion {
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
@JvmMultifileClass
- 1、作用
该注解主要是为了生成多文件的类
- 2、源码定义
@Target(AnnotationTarget.FILE)
@MustBeDocumented
@OptionalExpectation
public expect annotation class JvmMultifileClass()
- 3、注解使用
在Kotlin分别定义两个顶层函数在两个不同文件中,可通过该注解将多个文件中的类方法合并到一个类中。
//存在于IOUtilA文件中
@file:JvmName("IOUtils")
@file:JvmMultifileClass
package com.mikyou.annotation
import java.io.IOException
import java.io.Reader
fun closeReaderQuietly(input: Reader?) {
try {
input?.close()
} catch (ioe: IOException) {
// ignore
}
}
//存在于IOUtilB文件中
@file:JvmName("IOUtils")
@file:JvmMultifileClass
package com.mikyou.annotation
import java.io.IOException
import java.io.InputStream
fun closeStreamQuietly(input: InputStream?) {
try {
input?.close()
} catch (ioe: IOException) {
// ignore
}
}
//在Java中使用
public class Test {
public static void main(String[] args) {
//即使存在于不同文件中,但是对于外部Java调用仍然是同一个类IOUtils
IOUtils.closeReaderQuietly(null);
IOUtils.closeStreamQuietly(null);
}
}
@JvmName
- 1、作用
将改变由Kotlin默认生成的Java方法、字段或类名
- 2、源码定义
//作用的目标有: 函数、属性getter方法、属性setter方法、文件
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.FILE)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
public actual annotation class JvmName(actual val name: String)//有个name参数,将生成传入指定name的名称
- 3、注解使用
class Student {
@get:JvmName(name = "getStudentName")//修改属性的getter函数名称
@set:JvmName(name = "setStudentName")//修改属性的setter函数名称
var name: String = "Tim"
@JvmName("getStudentScore")//修改函数名称
fun getScore(): Double {
return 110.5
}
}
//修改生成的类名,默认Kotlin会生成以文件名+Kt后缀组合而成的类名
@file:JvmName("IOUtils")//注意:该注解一定要在第一行,package顶部
package com.mikyou.annotation
import java.io.IOException
import java.io.Reader
fun closeReaderQuietly(input: Reader?) {
try {
input?.close()
} catch (ioe: IOException) {
// ignore
}
}
反编译后的Java代码
public final class Student {
@NotNull
private String name = "Tim";
@JvmName(name = "getStudentName")
@NotNull
//已经修改成传入getStudentName
public final String getStudentName() {
return this.name;
}
@JvmName(name = "setStudentName")
//已经修改成传入setStudentName
public final void setStudentName(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
this.name = var1;
}
@JvmName(name = "getStudentScore")
//已经修改成传入getStudentScore
public final double getStudentScore() {
return 110.5D;
}
}
@JvmOverloads
- 1、作用
指导Kotlin编译器为带默认参数值的函数(包括构造函数)生成多个重载函数。
- 2、源码定义
//作用对象是函数和构造函数
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR)
@MustBeDocumented
@OptionalExpectation
public expect annotation class JvmOverloads()
- 3、注解使用
该注解使用最多就是用于带默认值函数的重载,在Android中我们在自定义View的时候一般会重载多个构造器,需要加入该注解,如果不加默认只定义一个构造器,那么当你直接在XML使用这个自定义View的时候会抛出异常。
class ScrollerView @JvmOverloads constructor(
context: Context,
attr: AttributeSet? = null,
defStyle: Int = 0
) : View(context, attr, defStyle) {
//...
}
反编译后的Java代码
public final class ScrollerView extends View {
@JvmOverloads
public ScrollerView(@NotNull Context context, @Nullable AttributeSet attr, int defStyle) {
Intrinsics.checkParameterIsNotNull(context, "context");
super(context, attr, defStyle);
}
// $FF: synthetic method
@JvmOverloads
public ScrollerView(Context var1, AttributeSet var2, int var3, int var4, DefaultConstructorMarker var5) {
if ((var4 & 2) != 0) {
var2 = (AttributeSet)null;
}
if ((var4 & 4) != 0) {
var3 = 0;
}
this(var1, var2, var3);
}
@JvmOverloads
public ScrollerView(@NotNull Context context, @Nullable AttributeSet attr) {
this(context, attr, 0, 4, (DefaultConstructorMarker)null);
}
@JvmOverloads
public ScrollerView(@NotNull Context context) {
this(context, (AttributeSet)null, 0, 6, (DefaultConstructorMarker)null);
}
//...
}
@JvmPackageName
- 1、作用
更改从使用该注解标注的文件生成的.class文件的JVM包的完全限定名称。 这不会影响Kotlin客户端在此文件中查看声明的方式,但Java客户端和其他JVM语言客户端将看到类文件,就好像它是在指定的包中声明的那样。 如果使用此批注对文件进行批注,则它只能包含函数,属性和类型声明,但不能包含。
- 2、源码定义
@Target(AnnotationTarget.FILE)//作用于文件
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
@SinceKotlin("1.2")//Kotlin1.2版本加入
internal annotation class JvmPackageName(val name: String)
- 3、注解使用
//以Collection源码为例
@file:kotlin.jvm.JvmPackageName("kotlin.collections.jdk8")
package kotlin.collections
可以看到该类会编译生成到kotlin.collections.jdk8
包名下
@JvmStatic
- 1、作用
能被用在对象声明或者Companion object伴生对象的方法上,把它们暴露成一个Java的静态方法
- 2、源码定义
//作用于函数、属性、属性的setter和getter
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@MustBeDocumented
@OptionalExpectation
public expect annotation class JvmStatic()
- 3、注解使用 @JvmStatic这个注解一般经常用于伴生对象的方法上,供给Java代码调用
class Data {
companion object {
fun getDefaultDataName(): String {
return "default"
}
}
}
//在java中调用,只能是Data.Companion.getDefaultDataName()调用
public class Test {
public static void main(String[] args) {
System.out.println(Data.Companion.getDefaultDataName());
}
}
反编译后Java代码
public final class Data {
public static final Data.Companion Companion = new Data.Companion((DefaultConstructorMarker)null);
public static final class Companion {
@NotNull
public final String getDefaultDataName() {
return "default";
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
使用@JvmStatic
注解后
class Data {
companion object {
@JvmStatic
fun getDefaultDataName(): String {
return "default"
}
}
}
//在java中调用,可以直接这样Data.getDefaultDataName()调用
public class Test {
public static void main(String[] args) {
System.out.println(Data.getDefaultDataName());
}
}
反编译后的Java代码
public final class Data {
public static final Data.Companion Companion = new Data.Companion((DefaultConstructorMarker)null);
@JvmStatic
@NotNull
//注意它会在Data类内部自动生成一个getDefaultDataName,然后内部还是通过Companion.getDefaultDataName()去调用。
public static final String getDefaultDataName() {
return Companion.getDefaultDataName();
}
public static final class Companion {
@JvmStatic
@NotNull
public final String getDefaultDataName() {
return "default";
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
@JvmSuppressWildcards和@JvmWildcard
- 1、作用
用于指示编译器生成或省略类型参数的通配符,JvmSuppressWildcards用于参数的泛型是否生成或省略通配符,而JvmWildcard用于返回值的类型是否生成或省略通配符
- 2、源码定义
//作用于类、函数、属性、类型
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.TYPE)
@MustBeDocumented
@OptionalExpectation
//指定suppress为true表示不生成,false为生成通配符,默认是true不生成
public expect annotation class JvmSuppressWildcards(val suppress: Boolean = true)
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.BINARY)
@MustBeDocumented
public actual annotation class JvmWildcard
- 3、注解使用
interface ICovert {
fun covertData(datas: List<@JvmSuppressWildcards(suppress = false) String>)//@JvmSuppressWildcardsd用于参数类型
fun getData(): List<@JvmWildcard String>//@JvmWildcard用于返回值类型
}
class CovertImpl implements ICovert {
@Override
public void covertData(List<? extends String> datas) {//参数类型生成通配符
}
@Override
public List<? extends String> getData() {//返回值类型生成通配符
return null;
}
}
@JvmSynthetic
- 1、作用
它在生成的类文件中将适当的元素标记为合成,并且编译器标记为合成的任何元素都将无法从Java语言中访问。
- 2、什么是合成属性(Synthetic属性)?
JVM字节码标识的ACC_SYNTHETIC属性用于标识该元素实际上不存在于原始源代码中,而是由编译器生成。
- 3、合成属性能做什么?
它一般用于支持代码生成,允许编译器生成不应向其他开发人员公开但需要支持实际公开接口所需的字段和方法。我们可以将其视为超越private或protected级别。
- 4、源码定义
//作用于函数、属性的setter,getter以及字段
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.FIELD)
@OptionalExpectation
public expect annotation class JvmSynthetic()
- 5、注解使用
class Synthetic {
@JvmSynthetic
val name: String = "Tim"
var age: Int
@JvmSynthetic
set(value) {
}
@JvmSynthetic
get() {
return 18
}
}
反编译后的Java代码
public final class Synthetic {
// $FF: synthetic field
@NotNull
private final String name = "Tim";
@NotNull
public final String getName() {
return this.name;
}
// $FF: synthetic method//我们经常看到这些注释,就是通过@Synthetic注解生成的
public final int getAge() {
return 18;
}
// $FF: synthetic method
public final void setAge(int value) {
}
}
通过反编译代码可能看不到什么,我们直接可以通过javap -v xxx.class查阅生成的字节码文件描述
public final int getAge();
descriptor: ()I
flags: ACC_PUBLIC, ACC_FINAL, ACC_SYNTHETIC//添加ACC_SYNTHETIC标识
Code:
stack=1, locals=1, args_size=1
0: bipush 18
2: ireturn
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this Lcom/mikyou/annotation/Synthetic;
LineNumberTable:
line 12: 0
public final void setAge(int);
descriptor: (I)V
flags: ACC_PUBLIC, ACC_FINAL, ACC_SYNTHETIC//添加ACC_SYNTHETIC标识
Code:
stack=0, locals=2, args_size=2
0: return
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/mikyou/annotation/Synthetic;
0 1 1 value I
LineNumberTable:
line 9: 0
@Throws
- 1、作用
用于Kotlin中的函数,属性的setter或getter函数,构造器函数抛出异常
- 2、源码定义
//作用于函数、属性的getter、setter函数、构造器函数
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.SOURCE)
public annotation class Throws(vararg val exceptionClasses: KClass<out Throwable>)//这里是异常类KClass不定参数,可以同时指定一个或多个异常
- 3、注解使用
@Throws(IOException::class)
fun closeQuietly(output: Writer?) {
output?.close()
}
@Transient
该注解充当了Java中的transient关键字
@Strictfp
该注解充当了Java中的strictfp关键字
@Synchronized
该注解充当了Java中的synchronized关键字
@Volatile
该注解充当了Java中的volatile关键字
@JvmRecord
在JDK 16日发布的包括计划,以稳定称为一个新的Java类的类型记录。为了提供Kotlin的所有好处并保持其与Java的互操作性,Kotlin引入了实验记录类支持。
您可以使用Java声明的记录类,就像Kotlin中具有属性的类一样。无需其他步骤。
从1.4.30开始,您可以使用数据类的@JvmRecord
注释在Kotlin中声明记录类:
@JvmRecord
data class User(val name: String, val age: Int)
要尝试JVM记录的预览版,请添加编译器选项-Xjvm-enable-preview
和-language-version 1.5
。
例:仿 Retrofit 反射读取注解请求网络
data class User(
var login: String,
var location: String,
var bio: String)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Api(val url: String)
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Path(val url: String = "")
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class Get(val url: String = "")
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class PathVariable(val name: String = "")
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class Query(val name: String = "")
@Api("https://api.github.com")
interface GitHubApi {
@Api("users")
interface Users {
@Get("{name}")
fun get(name: String): User
@Get("{name}/followers")
fun followers(name: String): List<User>
}
@Api("repos")
interface Repos {
@Get("{owner}/{repo}/forks")
fun forks(owner: String, repo: String)
}
}
object RetroApi {
const val PATH_PATTERN = """(\{(\w+)\})"""
val okHttp = OkHttpClient()
val gson = Gson()
val enclosing = {
cls: Class<*> ->
var currentCls: Class<*>? = cls
sequence {
while(currentCls != null){
// enclosingClass获取下一个class
// yield将对象添加到正在构建的sequence序列当中
currentCls = currentCls?.also { yield(it) }?.enclosingClass
}
}
}
//内联特化
inline fun <reified T> create(): T {
val functionMap = T::class.functions.map{ it.name to it }.toMap() //【函数名,函数本身】的Pair转成map
val interfaces = enclosing(T::class.java).takeWhile { it.isInterface }.toList() //拿到所有接口列表
println("interfaces= $interfaces")// 输出 [GitHubApi$Users, GitHubApi]
//foldRight从interfaces序列的右边开始拼
val apiPath = interfaces.foldRight(StringBuilder()) {
clazz, acc ->
// 拿到每个接口类的Api注解的url参数值,如果url参数为空,则使用类名作为url值
acc.append(clazz.getAnnotation(Api::class.java)?.url?.takeIf { it.isNotEmpty() } ?: clazz.name)
.append("/")
}.toString()
println("apiPath= $apiPath") // https://api.github.com/users/
//动态代理
return Proxy.newProxyInstance(RetroApi.javaClass.classLoader, arrayOf(T::class.java)) {
proxy, method, args ->
//所有函数中的抽象函数 即接口的方法
functionMap[method.name]?.takeIf { it.isAbstract }?.let {
function ->
//方法的参数
val parameterMap = function.valueParameters.map {
//参数名和参数的值放在一起
it.name to args[it.index - 1] //valueParameters包含receiver 因此需要index-1来对应args
}.toMap()
println("parameterMap= $parameterMap") //{name=bennyhuo}
//{name} 拿到Get注解的参数 如果注解参数不为空就使用注解参数,如果为空使用方法名称
val endPoint = function.findAnnotation<Get>()!!.url.takeIf { it.isNotEmpty() } ?: function.name
println("endPoint= $endPoint") //{name}/followers
//正则找到endPoint中的所有符合"{owner}/{repo}/forks"其中{xxx}的结果
val compiledEndPoint = Regex(PATH_PATTERN).findAll(endPoint).map {
matchResult ->
println("matchResult.groups= ${matchResult.groups}") // [MatchGroup(value={name}, range=0..5), MatchGroup(value={name}, range=0..5), MatchGroup(value=name, range=1..4)]
println("matchResult.groups1.range= ${matchResult.groups[1]?.range}") // 0..5
println("matchResult.groups2.value= ${matchResult.groups[2]?.value}") // name
matchResult.groups[1]!!.range to parameterMap[matchResult.groups[2]!!.value]
}.fold(endPoint) {
acc, pair ->
//acc的初始值就是endPoint即{name}/followers
println("acc= ${acc}") // {name}/followers
println("pair= ${pair}") // (0..5, bennyhuo) pair是一个 range to name
acc.replaceRange(pair.first, pair.second.toString()) // 把{name}/followers中的0到5的位置的字符串{name}替换成bennyhuo
}
println("compiledEndPoint= ${compiledEndPoint}") //bennyhuo/followers
//拼接api和参数
val url = apiPath + compiledEndPoint
println("url ==== $url")
println("*****************")
okHttp.newCall(Request.Builder().url(url).get().build()).execute().body()?.charStream()?.use {
gson.fromJson(JsonReader(it), method.genericReturnType)//返回json的解析结果
}
}
} as T
}
}
fun main() {
//interface com.bennyhuo.kotlin.annotations.eg.GitHubApi
//println("enclosingClass=${GitHubApi.Users::class.java.enclosingClass}")
val usersApi = RetroApi.create<GitHubApi.Users>()
val user = usersApi.get("bennyhuo")
val followers = usersApi.followers("bennyhuo").map { it.login }
println("user ====== $user")
println("followers ======== $followers")
}
例:注解加持反射版 Model 映射
例子是在前面反射一节实现的model映射例子的基础上,通过添加注解方式处理那些字段名称不是相同风格的情况,比如两个对象中的avatar_url
和 avatarUrl
的相互映射。
//不写默认是RUNTIME
//@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class FieldName(val name: String)
@Target(AnnotationTarget.CLASS)
annotation class MappingStrategy(val klass: KClass<out NameStrategy>)
interface NameStrategy {
fun mapTo(name: String): String
}
//下划线转驼峰
object UnderScoreToCamel : NameStrategy {
// html_url -> htmlUrl
override fun mapTo(name: String): String {
//先转成字符数组,然后fold操作
return name.toCharArray().fold(StringBuilder()) { acc, c ->
when (acc.lastOrNull()) { //上一次的acc不是空
'_' -> acc[acc.lastIndex] = c.toUpperCase() //上一次结果的最后一个字符是下划线就把下划线位置替换成当前字符的大写字母
else -> acc.append(c) // 否则直接拼接
}
//返回acc
acc
}.toString()
}
}
//驼峰转下划线
object CamelToUnderScore : NameStrategy {
override fun mapTo(name: String): String {
//先转成字符数组,然后fold操作
return name.toCharArray().fold(StringBuilder()) { acc, c ->
when {
c.isUpperCase() -> acc.append('_').append(c.toLowerCase()) //如果是大写字母直接拼一个下划线再拼上小写
else -> acc.append(c)
}
//返回acc
acc
}.toString()
}
}
//使用定义的策略注解,驼峰转下划线
@MappingStrategy(CamelToUnderScore::class)
data class UserVO(
val login: String,
//@FieldName("avatar_url") //这种是单个字段上面添加注解,只能一个一个添加
val avatarUrl: String,
var htmlUrl: String
)
data class UserDTO(
var id: Int,
var login: String,
var avatar_url: String,
var url: String,
var html_url: String
)
fun main() {
val userDTO = UserDTO(
0,
"Bennyhuo",
"https://avatars2.githubusercontent.com/u/30511713?v=4",
"https://api.github.com/users/bennyhuo",
"https://github.com/bennyhuo"
)
val userVO: UserVO = userDTO.mapAs()
println(userVO)
val userMap = mapOf(
"id" to 0,
"login" to "Bennyhuo",
"avatar_url" to "https://api.github.com/users/bennyhuo",
"html_url" to "https://github.com/bennyhuo",
"url" to "https://api.github.com/users/bennyhuo"
)
val userVOFromMap: UserVO = userMap.mapAs()
println(userVOFromMap)
}
inline fun <reified From : Any, reified To : Any> From.mapAs(): To {
return From::class.memberProperties.map { it.name to it.get(this) }
.toMap().mapAs()
}
inline fun <reified To : Any> Map<String, Any?>.mapAs(): To {
return To::class.primaryConstructor!!.let {
it.parameters.map { parameter ->
parameter to (this[parameter.name]
// let(this::get)等价于let{this[it]} userDTO["avatar_url"]
?: (parameter.annotations.filterIsInstance<FieldName>().firstOrNull()?.name?.let(this::get))
// 拿到UserVO类的注解MappingStrategy的kclass即CamelToUnderScore,它是一个object calss, objectInstance获取实例,然后调用mapTo把avatarUrl转成avatar_url,最后调用userDTO["avatar_url"]
?: To::class.findAnnotation<MappingStrategy>()?.klass?.objectInstance?.mapTo(parameter.name!!)?.let(this::get)
?: if (parameter.type.isMarkedNullable) null
else throw IllegalArgumentException("${parameter.name} is required but missing."))
}.toMap().let(it::callBy)
}
}
这里如果注解上不写@Retention(AnnotationRetention.RUNTIME)
默认就是运行时类型。
下面两种写法是等价的:
parameter.annotations.filterIsInstance<FieldName>()
parameter.findAnnotation<FieldName>()
下面两种写法是等价的:
let(this::get)
let{
this[it]
}
mapAs()方法中做了几件事:
- 尝试直接从当前Map中获取To对象的同名参数值,
- 尝试从To对象的字段上面的注解来获取需要转换的参数名,再根据名字获取Map中的值
- 尝试获取To对象的类注解得到处理类,调用处理类方法驼峰转下划线,再根据名字获取Map中的值
- 以上大招都没有获取到,如果To对象的字段可接受空值,就赋值null, 否则就抛异常