小白都能学会的Java注解与反射机制

网友投稿 685 2022-05-30

目录

前言

什么是注解

内置注解

自定义注解

Java8 注解

Java反射机制

java.lang.Class 类

反射操作泛型

反射操作注解

性能分析

前言

Java注解和反射是很基础的Java知识了,为何还要讲它呢?因为我在面试应聘者的过程中,发现不少面试者很少使用过注解和反射,甚至有人只能说出@Override这一个注解。我建议大家还是尽量能在开发中使用注解和反射,有时候使用它们能让你事半功倍,简化代码提高编码的效率。很多优秀的框架都基本使用了注解和反射,在Spring AOP中,就把注解和反射用得淋漓尽致。

什么是注解

Java注解(Annotation)亦叫Java标注,是JDK5.0开始引入的一种注释机制。 注解可以用在类、接口,方法、变量、参数以及包等之上。注解可以设置存在于不同的生命周期中,例如SOURCE(源码中),CLASS(Class文件中,默认是此保留级别),RUNTIME(运行期中)。

注解以@注解名的形式存在于代码中,Java中内置了一些注解,例如@Override,当然我们也可以自定义注解。注解也可以有参数,例如@MyAnnotation(value = “陈皮”)。

@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }

1

2

3

4

那注解有什么作用呢?其一是作为一种辅助信息,可以对程序做出一些解释,例如@Override注解作用于方法上,表示此方法是重写了父类的方法。其二,注解可以被其他程序读取,例如编译器,例如编译器会对被@Override注解的方法检测判断方法名和参数等是否与父类相同,否则会编译报错;而且在运行期可以通过反射机制访问某些注解信息。

内置注解

Java中有10个内置注解,其中6个注解是作用在代码上的,4个注解是负责注解其他注解的(即元注解),元注解提供对其他注解的类型说明。

自定义注解

使用@interface关键字自定义注解,其实底层就是定义了一个接口,而且自动继承java.lang.annotation.Annotation接口。

我们自定义一个注解如下:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface MyAnnotation { String value(); }

1

2

3

4

5

我们使用命令javap反编译我们定义的MyAnnotation注解的class文件,结果显示如下。虽然注解隐式继承了Annotation接口,但是Java不允许我们显示通过extends关键字继承Annotation接口甚至其他接口,否则编译报错。

D:\>javap MyAnnotation.class Compiled from "MyAnnotation.java" public interface com.nobody.MyAnnotation extends java.lang.annotation.Annotation { public abstract java.lang.String value(); }

1

2

3

4

5

注解的定义内容如下:

格式为public @interface 注解名 {定义内容}

内部的每一个方法实际是声明了一个参数,方法的名称就是参数的名称。

返回值类型就是参数的类型,而且返回值类型只能是基本类型(int,float,long,short,boolean,byte,double,char),Class,String,enum,Annotation以及上述类型的数组形式。

如果定义了参数,可通过default关键字声明参数的默认值,若不指定默认值,使用时就一定要显示赋值,而且不允许使用null值,一般会使用空字符串或者0。

如果只有一个参数,一般参数名为value,因为使用注解时,赋值可以不显示写出参数名,直接写参数值。

import java.lang.annotation.*; /** * @Description 自定义注解 * @Author Mr.nobody * @Date 2021/3/30 * @Version 1.0 */ @Target(ElementType.METHOD) // 此注解只能用在方法上。 @Retention(RetentionPolicy.RUNTIME) // 此注解保存在运行时期,可以通过反射访问。 @Inherited // 说明子类可以继承此类的此注解。 @Documented // 此注解包含在用户文档中。 public @interface CustomAnnotation { String value(); // 使用时需要显示赋值 int id() default 0; // 有默认值,使用时可以不赋值 }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

/** * @Description 测试注解 * @Author Mr.nobody * @Date 2021/3/30 * @Version 1.0 */ public class TestAnnotation { // @CustomAnnotation(value = "test") 只能注解在方法上,这里会报错 private String str = "Hello World!"; @CustomAnnotation(value = "test") public static void main(String[] args) { System.out.println(str); } }

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

Java8 注解

在这里讲解下Java8之后的几个注解和新特性,其中一个注解是@FunctionalInterface,它作用在接口上,标识是一个函数式接口,即只有有一个抽象方法,但是可以有默认方法。

@FunctionalInterface public interface Callback { public R call(P param); }

1

2

3

4

5

还有一个注解是@Repeatable,它允许在同一个位置使用多个相同的注解,而在Java8之前是不允许的。

@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Repeatable(OperTypes.class) public @interface OperType { String[] value(); }

1

2

3

4

5

6

// 可以理解@OperTypes注解作为接收同一个类型上重复@OperType注解的容器 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface OperTypes { OperType[] value(); }

1

2

3

4

5

6

@OperType("add") @OperType("update") public class MyClass { }

1

2

3

4

5

注意,对于重复注解,不能再通过clz.getAnnotation(Class annotationClass)方法来获取重复注解,Java8之后,提供了新的方法来获取重复注解,即clz.getAnnotationsByType(Class annotationClass)方法。

package com.nobody; import java.lang.annotation.Annotation; /** * @Description * @Author Mr.nobody * @Date 2021/3/31 * @Version 1.0 */ @OperType("add") @OperType("update") public class MyClass { public static void main(String[] args) { Class clz = MyClass.class; Annotation[] annotations = clz.getAnnotations(); for (Annotation annotation : annotations) { System.out.println(annotation.toString()); } OperType operType = clz.getAnnotation(OperType.class); System.out.println(operType); OperType[] operTypes = clz.getAnnotationsByType(OperType.class); for (OperType type : operTypes) { System.out.println(type.toString()); } } } // 输出结果为 @com.nobody.OperTypes(value=[@com.nobody.OperType(value=[add]), @com.nobody.OperType(value=[update])]) null @com.nobody.OperType(value=[add]) @com.nobody.OperType(value=[update])

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

在Java8中,ElementType枚举新增了两个枚举成员,分别为TYPE_PARAMETER和TYPE_USE,TYPE_PARAMETER标识注解可以作用于类型参数,TYPE_USE标识注解可以作用于标注任意类型(除了Class)。

Java反射机制

我们先了解下什么是静态语言和动态语言。动态语言是指在运行时可以改变其自身结构的语言。例如新的函数,对象,甚至代码可以被引进,已有的函数可以被删除或者结构上的一些变化。简单说即是在运行时代码可以根据某些条件改变自身结构。动态语言主要有C#,Object-C,JavaScript,PHP,Python等。静态语言是指运行时结构不可改变的语言,例如Java,C,C++等。

Java不是动态语言,但是它可以称为准动态语言,因为Java可以利用反射机制获得类似动态语言的特性,Java的动态性让它在编程时更加灵活。

反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性以及方法等。类在被加载完之后,会在堆内存的方法区中生成一个Class类型的对象,一个类只有一个Class对象,这个对象包含了类的结构信息。我们可以通过这个对象看到类的结构。

比如我们可以通过Class clz = Class.forName("java.lang.String");获得String类的Class对象。我们知道每个类都隐式继承Object类,Object类有个getClass()方法也能获取Class对象。

Java反射机制提供的功能

在运行时判断任意一个对象所属的类

在运行时构造任意一个类的对象

在运行时判断任意一个类具有的成员变量和方法

在运行时获取泛型信息

在运行时调用任意一个对象的成员变量和方法

在运行时获取注解

生成动态代理

Java反射机制的优缺点

优点:实现动态创建对象和编译,有更加的灵活性。

缺点:对性能有影响。使用反射其实是一种解释操作,即告诉JVM我们想要做什么,然后它满足我们的要求,所以总是慢于直接执行相同的操作。

Java反射相关的主要API

java.lang.Class:代表一个类

java.lang.reflect.Method:代表类的方法

java.lang.reflect.Field:代表类的成员变量

java.lang.reflect.Constructor:代表类的构造器

我们知道在运行时通过反射可以准确获取到注解信息,其实以上类(Class,Method,Field,Constructor等)都直接或间接实现了AnnotatedElement接口,并实现了它定义的方法,AnnotatedElement接口的作用主要用于表示正在JVM中运行的程序中已使用注解的元素,通过该接口提供的方法可以获取到注解信息。

java.lang.Class 类

在Java反射中,最重要的是Class这个类了。Class本身也是一个类。当程序想要使用某个类时,如果此类还未被加载到内存中,首先会将类的class文件字节码加载到内存中,并将这些静态数据转换为方法区的运行时数据结构,然后生成一个Class类型的对象(Class对象只能由系统创建),一个类只有一个Class对象,这个对象包含了类的结构信息。我们可以通过这个对象看到类的结构。每个类的实例都会记得自己是由哪个Class实例所生成的。

通过Class对象可以知道某个类的属性,方法,构造器,注解,以及实现了哪些接口等信息。注意,只有class,interface,enum,annotation,primitive type,void,[] 等才有Class对象。

package com.nobody; import java.lang.annotation.ElementType; import java.util.Map; public class TestClass { public static void main(String[] args) { // 类 Class myClassClass = MyClass.class; // 接口 Class mapClass = Map.class; // 枚举 Class elementTypeClass = ElementType.class; // 注解 Class overrideClass = Override.class; // 原生类型 Class integerClass = Integer.class; // 空类型 Class voidClass = void.class; // 一维数组 Class aClass = String[].class; // 二维数组 Class aClass1 = String[][].class; // Class类也有Class对象 Class classClass = Class.class; System.out.println(myClassClass); System.out.println(mapClass); System.out.println(elementTypeClass); System.out.println(overrideClass); System.out.println(integerClass); System.out.println(voidClass); System.out.println(aClass); System.out.println(aClass1); System.out.println(classClass); } } // 输出结果 class com.nobody.MyClass interface java.util.Map class java.lang.annotation.ElementType interface java.lang.Override class java.lang.Integer void class [Ljava.lang.String; class [[Ljava.lang.String; class java.lang.Class

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

获取Class对象的方法

如果知道具体的类,可通过类的class属性获取,这种方法最安全可靠并且性能最高。Class clz = User.class;

通过类的实例的getClass()方法获取。Class clz = user.getClass();

如果知道一个类的全限定类名,并且在类路径下,可通过Class.forName()方法获取,但是可能会抛出ClassNotFoundException。Class clz = Class.forName("com.nobody.User");

内置的基本数据类型可以直接通过类名.Type获取。Class clz = Integer.TYPE;

通过类加载器ClassLoader获取

Class类的常用方法

public static Class forName(String className):创建一个指定全限定类名的Class对象

public T newInstance():调用Class对象所代表的类的无参构造方法,创建一个实例

public String getName():返回Class对象所代表的类的全限定名称。

public String getSimpleName():返回Class对象所代表的类的简单名称。

public native Class getSuperclass():返回Class对象所代表的类的父类的Class对象,这是一个本地方法

public Class[] getInterfaces():返回Class对象的接口

public Field[] getFields():返回Class对象所代表的实体的public属性Field对象数组

public Field[] getDeclaredFields():返回Class对象所代表的实体的所有属性Field对象数组

public Field getDeclaredField(String name):获取指定属性名的Field对象

public Method[] getDeclaredMethods():返回Class对象所代表的实体的所有Method对象数组

public Method getDeclaredMethod(String name, Class… parameterTypes):返回指定名称和参数类型的Method对象

myClassClass.getDeclaredConstructors();:返回所有Constructor对象的数组

public ClassLoader getClassLoader():返回当前类的类加载器

在反射中经常会使用到Method的invoke方法,即public Object invoke(Object obj, Object... args),我们简单说明下:

第一个Object对应原方法的返回值,若原方法没有返回值,则返回null。

第二个Object对象对应调用方法的实例,若原方法为静态方法,则参数obj可为null。

第二个Object对应若原方法形参列表,若参数为空,则参数args为null。

若原方法声明为private修饰,则调用invoke方法前,需要显示调用方法对象的method.setAccessible(true)方法,才可访问private方法。

反射操作泛型

泛型是JDK 1.5的一项新特性,它的本质是参数化类型(Parameterized Type)的应用,也就是说所操作的数据类型被指定为一个参数,在用到的时候再指定具体的类型。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。

在Java中,采用泛型擦除的机制来引入泛型,泛型能编译器使用javac时确保数据的安全性和免去强制类型转换问题,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。并且一旦编译完成,所有和泛型有关的类型会被全部擦除。

Java新增了ParameterizedType,GenericArrayType,TypeVariable和WildcardType等几种类型,能让我们通过反射操作这些类型。

ParameterizedType:表示一种参数化类型,比如Collection

GenericArrayType:表示种元素类型是参数化类型或者类型变量的数组类型

TypeVariable:是各种类型变量的公共父接口

WildcardType:代表种通配符类型表达式

package com.nobody; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Map; public class TestReflectGenerics { public Map test(Map map, Person person) { return null; } public static void main(String[] args) throws NoSuchMethodException { // 获取test方法对象 Method test = TestReflectGenerics.class.getDeclaredMethod("test", Map.class, Person.class); // 获取方法test的参数类型 Type[] genericParameterTypes = test.getGenericParameterTypes(); for (Type genericParameterType : genericParameterTypes) { System.out.println("方法参数类型:" + genericParameterType); // 如果参数类型等于参数化类型 if (genericParameterType instanceof ParameterizedType) { // 获得真实参数类型 Type[] actualTypeArguments = ((ParameterizedType) genericParameterType).getActualTypeArguments(); for (Type actualTypeArgument : actualTypeArguments) { System.out.println(" " + actualTypeArgument); } } } // 获取方法test的返回值类型 Type genericReturnType = test.getGenericReturnType(); System.out.println("返回值类型:" + genericReturnType); // 如果参数类型等于参数化类型 if (genericReturnType instanceof ParameterizedType) { // 获得真实参数类型 Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments(); for (Type actualTypeArgument : actualTypeArguments) { System.out.println(" " + actualTypeArgument); } } } } class Person {} // 输出结果 方法参数类型:java.util.Map class java.lang.String class java.lang.Integer 方法参数类型:class com.nobody.Person 返回值类型:java.util.Map class java.lang.String class com.nobody.Person

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

小白都能学会的Java注解与反射机制

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

反射操作注解

在Java运行时,通过反射获取代码中的注解是比较常用的手段了,获取到了注解之后,就能知道注解的所有信息了,然后根据信息进行相应的操作。下面通过一个例子,获取类和属性的注解,解析映射为数据库中的表信息。

package com.nobody; import java.lang.annotation.*; public class AnalysisAnnotation { public static void main(String[] args) throws Exception { Class aClass = Class.forName("com.nobody.Book"); // 获取类的指定注解,并且获取注解的值 Table annotation = aClass.getAnnotation(Table.class); String value = annotation.value(); System.out.println("Book类映射的数据库表名:" + value); java.lang.reflect.Field bookName = aClass.getDeclaredField("bookName"); TableField annotation1 = bookName.getAnnotation(TableField.class); System.out.println("bookName属性映射的数据库字段属性 - 列名:" + annotation1.colName() + ",类型:" + annotation1.type() + ",长度:" + annotation1.length()); java.lang.reflect.Field price = aClass.getDeclaredField("price"); TableField annotation2 = price.getAnnotation(TableField.class); System.out.println("price属性映射的数据库字段属性 - 列名:" + annotation2.colName() + ",类型:" + annotation2.type() + ",长度:" + annotation2.length()); } } // 作用于类的注解,用于解析表数据 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @interface Table { // 表名 String value(); } // 作用于字段,用于解析表列 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @interface TableField { // 列名 String colName(); // 列类型 String type(); // 长度 int length(); } @Table("t_book") class Book { @TableField(colName = "name", type = "varchar", length = 15) String bookName; @TableField(colName = "price", type = "int", length = 10) int price; } // 输出结果 Book类映射的数据库表名:t_book bookName属性映射的数据库字段属性 - 列名:name,类型:varchar,长度:15 price属性映射的数据库字段属性 - 列名:price,类型:int,长度:10

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

性能分析

前面我们说过,反射对性能有一定影响。因为反射是一种解释操作,它总是慢于直接执行相同的操作。而且Method,Field,Constructor都有setAccessible()方法,它的作用是开启或禁用访问安全检查。如果我们程序代码中用到了反射,而且此代码被频繁调用,为了提高反射效率,则最好禁用访问安全检查,即设置为true。

package com.nobody; import java.lang.reflect.Method; public class TestReflectSpeed { // 10亿次 private static int times = 1000000000; public static void main(String[] args) throws Exception { test01(); test02(); test03(); } public static void test01() { Teacher t = new Teacher(); long start = System.currentTimeMillis(); for (int i = 0; i < times; i++) { t.getName(); } long end = System.currentTimeMillis(); System.out.println("普通方式执行10亿次消耗:" + (end - start) + "ms"); } public static void test02() throws Exception { Teacher teacher = new Teacher(); Class aClass = Class.forName("com.nobody.Teacher"); Method getName = aClass.getDeclaredMethod("getName"); long start = System.currentTimeMillis(); for (int i = 0; i < times; i++) { getName.invoke(teacher); } long end = System.currentTimeMillis(); System.out.println("反射方式执行10亿次消耗:" + (end - start) + "ms"); } public static void test03() throws Exception { Teacher teacher = new Teacher(); Class aClass = Class.forName("com.nobody.Teacher"); Method getName = aClass.getDeclaredMethod("getName"); getName.setAccessible(true); long start = System.currentTimeMillis(); for (int i = 0; i < times; i++) { getName.invoke(teacher); } long end = System.currentTimeMillis(); System.out.println("关闭安全检查反射方式执行10亿次消耗:" + (end - start) + "ms"); } } class Teacher { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } //输出结果 普通方式执行10亿次消耗:13ms 反射方式执行10亿次消耗:20141ms 关闭安全检查反射方式执行10亿次消耗:8233ms

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

通过实验可知,反射比直接执行相同的方法慢了很多,特别是当反射的操作被频繁调用时效果更明显,当然通过关闭安全检查可以提高一些速度。所以,放射也不应该泛滥成灾的,而是适度使用才能发挥最大作用。

Java 数据结构

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Python 统计基础:(二)如何描述您的数据
下一篇:C 语言编程 — 使用 assert 断言进行程序设计
相关文章