MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java类的注解与反射

2021-10-144.9k 阅读

Java 类的注解

什么是注解

在 Java 中,注解(Annotation)是一种元数据形式,它为我们在代码中添加额外信息提供了一种形式化的方式。这些信息可以在编译期、类加载期或者运行时被读取和使用。从本质上讲,注解就像是代码中的标签,用于标识特定的元素(类、方法、字段等),并附带一些相关的配置或说明。

Java 注解体系从 JDK 5.0 开始引入,它提供了一种更加灵活和强大的方式来处理代码中的元数据,相较于传统的使用 XML 配置文件来管理元数据,注解直接嵌入到代码中,使得代码和元数据的关联性更加紧密,易于维护和理解。

内置注解

Java 提供了一些内置的注解,这些注解在日常开发中经常会用到。

  1. @Override:这个注解用于标识子类中重写父类的方法。它的作用主要是帮助编译器进行检查,确保子类中定义的方法确实是对父类中相应方法的重写。如果使用了 @Override 注解,但方法实际上并没有正确重写父类的方法,编译器会报错。例如:
class Animal {
    public void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog barks");
    }
}
  1. @Deprecated:当一个类、方法或字段被标记为 @Deprecated,它表示这个元素已经过时,不建议再使用。虽然仍然可以使用,但编译器会发出警告。通常,当一个元素有更好的替代方案时,会将其标记为 @Deprecated。例如:
public class OldUtil {
    @Deprecated
    public static void oldMethod() {
        System.out.println("This is an old method");
    }
}

public class NewUtil {
    public static void newMethod() {
        System.out.println("This is a new method");
    }
}

public class Main {
    public static void main(String[] args) {
        OldUtil.oldMethod(); // 编译器会发出警告
        NewUtil.newMethod();
    }
}
  1. @SuppressWarnings:该注解用于抑制编译器产生的警告信息。有时候,我们的代码可能会产生一些编译器认为的潜在问题警告,但实际上我们知道这些警告不会对程序的正确性产生影响,这时就可以使用 @SuppressWarnings 注解来抑制这些警告。它可以接受一个参数数组,指定要抑制的警告类型。例如:
import java.util.ArrayList;
import java.util.List;

public class SuppressWarningExample {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("Hello");
        String str = (String) list.get(0);
        System.out.println(str);
    }
}

在这个例子中,我们使用了 @SuppressWarnings("unchecked") 来抑制未检查类型转换的警告,因为我们知道这个列表只会包含字符串类型的数据。

自定义注解

除了使用内置注解,我们还可以根据自己的需求定义注解。定义自定义注解需要使用 @interface 关键字。自定义注解可以包含成员变量,这些成员变量的类型只能是基本数据类型、String、Class、enum、Annotation 以及它们的数组类型。

  1. 定义简单的自定义注解
public @interface MyAnnotation {
    String value();
}

public class AnnotatedClass {
    @MyAnnotation("Hello, Annotation!")
    public void myMethod() {
        System.out.println("This method is annotated");
    }
}

在这个例子中,我们定义了一个名为 MyAnnotation 的注解,它有一个名为 value 的成员变量。在 AnnotatedClass 类的 myMethod 方法上使用了这个注解,并为 value 成员变量赋值。 2. 定义带有默认值的注解成员变量

public @interface AnotherAnnotation {
    String message() default "Default message";
    int number() default 0;
}

public class AnotherAnnotatedClass {
    @AnotherAnnotation
    public void anotherMethod() {
        System.out.println("This method is annotated with default values");
    }

    @AnotherAnnotation(message = "Custom message", number = 10)
    public void customMethod() {
        System.out.println("This method is annotated with custom values");
    }
}

这里 AnotherAnnotation 注解有两个成员变量 message 和 number,都有默认值。在 anotherMethod 方法上使用注解时,使用的是默认值;而在 customMethod 方法上使用注解时,为成员变量指定了自定义的值。 3. 定义可重复的注解:从 Java 8 开始,支持定义可重复的注解。首先需要定义一个容器注解,然后在自定义注解上使用 @Repeatable 注解来指定容器注解。例如:

import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Tags {
    Tag[] value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(Tags.class)
public @interface Tag {
    String value();
}

public class RepeatableAnnotationExample {
    @Tag("tag1")
    @Tag("tag2")
    public void repeatableMethod() {
        System.out.println("This method has repeatable annotations");
    }
}

在这个例子中,Tag 注解是可重复的,Tags 是它的容器注解。在 repeatableMethod 方法上可以多次使用 Tag 注解。

注解的保留策略和目标

  1. 保留策略(Retention Policy):保留策略决定了注解在什么时候存在。Java 中有三种保留策略,通过 @Retention 注解来指定。
    • RetentionPolicy.SOURCE:注解只在源文件中存在,编译时会被丢弃。例如一些用于代码生成工具的临时注解,不需要在编译后的字节码或运行时存在。
    • RetentionPolicy.CLASS:注解在编译后的字节码中存在,但在运行时 JVM 不会加载。这是默认的保留策略,大多数情况下,用于一些编译期处理的注解会使用这种策略,比如 Lombok 注解,它在编译期生成字节码,运行时不需要额外的处理。
    • RetentionPolicy.RUNTIME:注解在运行时仍然存在,可以通过反射机制读取。这种策略适用于那些需要在运行时根据注解信息进行动态处理的场景,比如 Spring 框架中对 bean 的定义和依赖注入就大量使用了运行时保留的注解。
  2. 目标(Target):目标指定了注解可以应用到哪些元素上,通过 @Target 注解来指定。常见的目标类型有:
    • ElementType.TYPE:可以应用到类、接口、枚举等类型上。
    • ElementType.METHOD:可以应用到方法上。
    • ElementType.FIELD:可以应用到字段(成员变量)上。
    • ElementType.CONSTRUCTOR:可以应用到构造函数上。
    • ElementType.PARAMETER:可以应用到方法参数上。

例如,下面的自定义注解只能应用到方法上,并且在运行时存在:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RuntimeMethodAnnotation {
    String description();
}

Java 类的反射

什么是反射

反射(Reflection)是 Java 提供的一种强大的机制,它允许程序在运行时检查、获取和操作类的信息以及对象的状态和行为。通过反射,我们可以在运行时动态地加载类、创建对象、调用方法、访问和修改字段等,而不需要在编译期就确定具体的类。

反射机制是 Java 语言的一个重要特性,它使得 Java 程序具有更高的灵活性和扩展性,许多框架(如 Spring、Hibernate 等)都大量依赖反射来实现其功能。从本质上讲,反射是 Java 对自身类型系统的一种自省能力,它允许我们在运行时像操作数据一样操作类型。

反射相关的类

在 Java 中,反射相关的类主要位于 java.lang.reflect 包下,其中几个核心类包括:

  1. Class 类:每个类在内存中都有一个对应的 Class 对象,它包含了类的所有信息,如类名、父类、接口、字段、方法等。我们可以通过多种方式获取 Class 对象,例如:
// 方式一:通过对象的 getClass() 方法
String str = "Hello";
Class<?> stringClass1 = str.getClass();

// 方式二:通过类的 .class 语法
Class<?> stringClass2 = String.class;

// 方式三:通过 Class.forName() 静态方法
try {
    Class<?> stringClass3 = Class.forName("java.lang.String");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}
  1. Field 类:用于表示类的字段(成员变量)。通过 Class 对象的 getFields() 方法可以获取所有的公共字段,getDeclaredFields() 方法可以获取所有声明的字段(包括私有字段)。例如:
class Person {
    private String name;
    public int age;

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

public class ReflectionFieldExample {
    public static void main(String[] args) {
        try {
            Class<?> personClass = Class.forName("Person");
            // 获取所有公共字段
            java.lang.reflect.Field[] publicFields = personClass.getFields();
            for (java.lang.reflect.Field field : publicFields) {
                System.out.println("Public field: " + field.getName());
            }

            // 获取所有声明的字段
            java.lang.reflect.Field[] declaredFields = personClass.getDeclaredFields();
            for (java.lang.reflect.Field field : declaredFields) {
                System.out.println("Declared field: " + field.getName());
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
  1. Method 类:用于表示类的方法。通过 Class 对象的 getMethods() 方法可以获取所有的公共方法,getDeclaredMethods() 方法可以获取所有声明的方法(包括私有方法)。例如:
class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    private int subtract(int a, int b) {
        return a - b;
    }
}

public class ReflectionMethodExample {
    public static void main(String[] args) {
        try {
            Class<?> calculatorClass = Class.forName("Calculator");
            // 获取所有公共方法
            java.lang.reflect.Method[] publicMethods = calculatorClass.getMethods();
            for (java.lang.reflect.Method method : publicMethods) {
                System.out.println("Public method: " + method.getName());
            }

            // 获取所有声明的方法
            java.lang.reflect.Method[] declaredMethods = calculatorClass.getDeclaredMethods();
            for (java.lang.reflect.Method method : declaredMethods) {
                System.out.println("Declared method: " + method.getName());
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
  1. Constructor 类:用于表示类的构造函数。通过 Class 对象的 getConstructors() 方法可以获取所有的公共构造函数,getDeclaredConstructors() 方法可以获取所有声明的构造函数(包括私有构造函数)。例如:
class Product {
    private String name;
    private double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    private Product(String name) {
        this.name = name;
        this.price = 0.0;
    }
}

public class ReflectionConstructorExample {
    public static void main(String[] args) {
        try {
            Class<?> productClass = Class.forName("Product");
            // 获取所有公共构造函数
            java.lang.reflect.Constructor<?>[] publicConstructors = productClass.getConstructors();
            for (java.lang.reflect.Constructor<?> constructor : publicConstructors) {
                System.out.println("Public constructor: " + constructor);
            }

            // 获取所有声明的构造函数
            java.lang.reflect.Constructor<?>[] declaredConstructors = productClass.getDeclaredConstructors();
            for (java.lang.reflect.Constructor<?> constructor : declaredConstructors) {
                System.out.println("Declared constructor: " + constructor);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

使用反射创建对象

  1. 通过默认构造函数创建对象:如果类有默认的无参构造函数,可以使用 Class 对象的 newInstance() 方法来创建对象。例如:
class User {
    public User() {
        System.out.println("User object created");
    }
}

public class ReflectionObjectCreationExample {
    public static void main(String[] args) {
        try {
            Class<?> userClass = Class.forName("User");
            User user = (User) userClass.newInstance();
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}
  1. 通过指定构造函数创建对象:如果类没有默认构造函数,或者我们想使用带参数的构造函数来创建对象,可以通过获取对应的 Constructor 对象,然后调用其 newInstance() 方法,并传入相应的参数。例如:
class Employee {
    private String name;
    private int id;

    public Employee(String name, int id) {
        this.name = name;
        this.id = id;
    }
}

public class ReflectionParameterizedConstructorExample {
    public static void main(String[] args) {
        try {
            Class<?> employeeClass = Class.forName("Employee");
            java.lang.reflect.Constructor<?> constructor = employeeClass.getConstructor(String.class, int.class);
            Employee employee = (Employee) constructor.newInstance("John", 101);
        } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | java.lang.reflect.InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

使用反射访问和修改字段

  1. 访问公共字段:通过 Field 对象的 get() 方法可以获取字段的值,前提是该字段是公共的。例如:
class Student {
    public String name;
    public int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class ReflectionFieldAccessExample {
    public static void main(String[] args) {
        try {
            Class<?> studentClass = Class.forName("Student");
            Student student = new Student("Alice", 20);
            java.lang.reflect.Field nameField = studentClass.getField("name");
            java.lang.reflect.Field ageField = studentClass.getField("age");

            String studentName = (String) nameField.get(student);
            int studentAge = ageField.getInt(student);

            System.out.println("Student name: " + studentName);
            System.out.println("Student age: " + studentAge);
        } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}
  1. 访问和修改私有字段:要访问和修改私有字段,需要先通过 setAccessible(true) 方法打破 Java 的访问控制。例如:
class Secret {
    private String message;

    public Secret(String message) {
        this.message = message;
    }
}

public class ReflectionPrivateFieldAccessExample {
    public static void main(String[] args) {
        try {
            Class<?> secretClass = Class.forName("Secret");
            Secret secret = new Secret("This is a secret");
            java.lang.reflect.Field messageField = secretClass.getDeclaredField("message");
            messageField.setAccessible(true);

            String originalMessage = (String) messageField.get(secret);
            System.out.println("Original message: " + originalMessage);

            messageField.set(secret, "The secret has been changed");
            String newMessage = (String) messageField.get(secret);
            System.out.println("New message: " + newMessage);
        } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

使用反射调用方法

  1. 调用公共方法:通过 Method 对象的 invoke() 方法可以调用对象的方法,前提是该方法是公共的。例如:
class MathUtils {
    public int multiply(int a, int b) {
        return a * b;
    }
}

public class ReflectionMethodInvocationExample {
    public static void main(String[] args) {
        try {
            Class<?> mathUtilsClass = Class.forName("MathUtils");
            MathUtils mathUtils = new MathUtils();
            java.lang.reflect.Method multiplyMethod = mathUtilsClass.getMethod("multiply", int.class, int.class);

            int result = (int) multiplyMethod.invoke(mathUtils, 3, 4);
            System.out.println("Multiplication result: " + result);
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | java.lang.reflect.InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}
  1. 调用私有方法:与访问私有字段类似,调用私有方法也需要先通过 setAccessible(true) 方法打破访问控制。例如:
class Hidden {
    private void privateMethod() {
        System.out.println("This is a private method");
    }
}

public class ReflectionPrivateMethodInvocationExample {
    public static void main(String[] args) {
        try {
            Class<?> hiddenClass = Class.forName("Hidden");
            Hidden hidden = new Hidden();
            java.lang.reflect.Method privateMethod = hiddenClass.getDeclaredMethod("privateMethod");
            privateMethod.setAccessible(true);

            privateMethod.invoke(hidden);
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | java.lang.reflect.InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

注解与反射的结合使用

在实际开发中,注解和反射常常结合使用,以实现一些高级的功能,如框架的配置、依赖注入等。下面通过一个简单的示例来展示它们的结合使用。

假设我们要开发一个简单的依赖注入框架,使用注解来标记需要注入的字段,然后通过反射来实现依赖的注入。

  1. 定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject {
}
  1. 定义需要注入的类
class DatabaseConnection {
    public void connect() {
        System.out.println("Connected to the database");
    }
}
  1. 定义使用依赖的类
class UserService {
    @Inject
    private DatabaseConnection databaseConnection;

    public void performAction() {
        if (databaseConnection != null) {
            databaseConnection.connect();
            System.out.println("User service performing action");
        } else {
            System.out.println("Database connection is null");
        }
    }
}
  1. 实现依赖注入
import java.lang.reflect.Field;

public class DependencyInjector {
    public static void injectDependencies(Object object) {
        Class<?> clazz = object.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(Inject.class)) {
                try {
                    field.setAccessible(true);
                    Class<?> fieldType = field.getType();
                    Object fieldInstance = fieldType.newInstance();
                    field.set(object, fieldInstance);
                } catch (InstantiationException | IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  1. 测试依赖注入
public class Main {
    public static void main(String[] args) {
        UserService userService = new UserService();
        DependencyInjector.injectDependencies(userService);
        userService.performAction();
    }
}

在这个例子中,我们定义了一个 @Inject 注解来标记需要注入的字段。然后在 DependencyInjector 类中,通过反射获取类的字段,检查是否有 @Inject 注解,如果有则创建相应类型的对象并注入到字段中。最后在 Main 类中进行测试,通过调用 DependencyInjector 的 injectDependencies 方法实现了 UserService 类中 DatabaseConnection 字段的依赖注入。

这种结合使用注解和反射的方式,使得我们可以在代码中以一种声明式的方式表达依赖关系,同时通过反射在运行时动态地满足这些依赖,大大提高了代码的灵活性和可维护性,这也是许多现代 Java 框架(如 Spring)的核心实现原理之一。