0
点赞
收藏
分享

微信扫一扫

【Java】反射和注解

猫er聆听没落的旋律 2022-04-18 阅读 163
java

反射和注解

反射就是把Java类中的各个成分映射成一个个Java对象,从而在运行中可以知道该类的所有属性和方法,调用任意一个对象的任意一个方法和属性。这种动态获取信息以及动态调用对象方法的功能叫作反射机制。

权限非常高,能获取泛型,甚至能越级,需要谨慎使用

通过特定的方法,即使没有调用相关的类包,但是却可以使用

类加载机制

Java启动时,JVM会将一部分类(class文件夹)先加载,也就是不会全部加载,通过ClassLoader进行类加载,在加载过程中将类的信息提取出来(存放在元空间中,在JDK1.8之前会放在永久代),同时生成一个Class对象存放在内存(堆内存)中,此Class对象只会存在一个,与加载的类唯一对应。

img

由于双亲委派机制的存在,手动创建一个与JDK包名一样,同时类名也保持一致,那么JVM也不会加载这个类,原因是加载顺序从BootstrapClassLoader会先开始加载,此时加载的会是JDK自带的同名类,之后则不会再加载这个名字的类了。

获取类加载器:

public class Main_base {
    public static void main(String[] args) {
        System.out.println(Main_base.class.getClassLoader());   //查看当前类的类加载器
        System.out.println(Main_base.class.getClassLoader().getParent());  //父加载器
        System.out.println(Main_base.class.getClassLoader().getParent().getParent());  //爷爷加载器
        System.out.println(String.class.getClassLoader());   //String类的加载器
    }
}

class对象

获取class对象:

public static void main(String[] args) throws ClassNotFoundException {
    Class<String> clazz = String.class;   //使用class关键字,通过类名获取
    Class<?> clazz2 = Class.forName("java.lang.String");   //使用Class类静态方法forName(),通过包名.类名获取,注意返回值是Class<?>
    Class<?> clazz3 = new String("cpdd").getClass();  //通过实例对象获取
}
  • 使用class关键字,通过类名获取
  • 使用Class类静态方法forName(),通过包名.类名获取,注意返回值是Class<?>
  • 通过实例对象获取

注意Class类也是一个泛型类,只有第一种方法,能够直接获取到对应类型的Class对象,而以下两种方法使用了?通配符作为返回值,但是实际上都和第一个返回的是同一个对象,是相同的。JVM中每个类始终只存在一个Class对象,无论通过什么方法获取,都是一样的。

基本数据类型也有对应的Class对象(反射操作可能需要用到),而且不仅可以通过class关键字获取,其实本质上是定义在对应的包装类中的 TYPE 常量

public static void main(String[] args) {
    Class<?> clazz = int.class;   //基本数据类型有Class对象
    System.out.println(clazz);
}

但是包装类型的Class对象并不是基本类型Class对象

class对象有很多方法,包括强制类型转换成class对象代表的类,获得类信息等。

类型比较

正常情况下,我们使用instanceof进行类型比较:

public static void main(String[] args) {
    String str = "";
    System.out.println(str instanceof String);
}

它可以判断一个对象是否为此接口或是类的实现或是子类,而现在我们有了更多的方式去判断类型:

public static void main(String[] args) {
    String str = "";
    System.out.println(str.getClass() == String.class);   //直接判断是否为这个类型
}

如果需要判断是否为子类或是接口/抽象类的实现,我们可以使用asSubClass()方法:

public static void main(String[] args) {
    Integer i = 10;
    i.getClass().asSubclass(Number.class);   //当Integer不是Number的子类时,会产生异常
}

获取父类信息

通过getSuperclass()方法,我们可以获取到父类的Class对象:

public static void main(String[] args) {
    Integer i = 10;
    System.out.println(i.getClass().getSuperclass());
}

还可以获取父类原始类型TYPE、父类接口

TYPE可以获取泛型,有多个接口则会返回数组

创建类对象

通过class对象,拿到了类的定义,之后可以借此进行创建对象、调用方法、修改变量

    Class<Student> clazz = Student.class;
    Student student = clazz.newInstance();//通过class创建对象实例
    student.test();

但只能使用无参,当类无参构造函数被有参覆盖时会报错,不推荐使用

通过获取类的构造方法(构造器)来创建对象实例,会更加合理,我们可以使用getConstructor()方法来获取类的构造方法,同时我们需要向其中填入参数,也就是构造方法需要的类型:

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Class<Student> clazz = Student.class;
    Student student = clazz.getConstructor(String.class).newInstance("what's up");
    student.test();//getConstructor(String.class)内可通过传入参数的类型指定构造方法
}

static class Student{

    private Student(String str){}

    public void test(){
        System.out.println("萨日朗");
    }
}

还有获得全部构造方法的方法。
private构造方法无法通过以上方法调用,但是:

Class<Student> clazz = Student.class;
Constructor<Student> constructor = clazz.getDeclaredConstructor(String.class);
constructor.setAccessible(true);   //修改访问权限
Student student = constructor.newInstance("what's up");
student.test();

先获取构造方法后修改访问权限,从而调用

调用类方法

public static void main(String[] args) throws ReflectiveOperationException {
    Class<?> clazz = Class.forName("com.test.Student");
    Object instance = clazz.newInstance();   //创建出学生对象
    Method method = clazz.getMethod("test", String.class);   //通过方法名和形参类型获取类中的方法
    
    method.invoke(instance, "what's up");   //通过Method对象的invoke方法来调用方法
}
  • 由于使用了<?>通配符,返回的并不是原类型,而是Object类
  • newInstance也只能调用public构造,创建实例
  • 如果调用的是静态方法,则invoke可不输入实例

Method和Constructor都和Class一样,他们存储了方法的信息,包括方法的形式参数列表,返回值,方法的名称等内容,我们可以直接通过Method对象来获取这些信息:

public static void main(String[] args) throws ReflectiveOperationException {
    Class<?> clazz = Class.forName("com.test.Student");
    Method method = clazz.getDeclaredMethod("test", String.class);   //通过方法名和形参类型获取类中的方法
    
    System.out.println(method.getName());   //获取方法名称
    System.out.println(method.getReturnType());   //获取返回值类型
}

当方法的参数为可变参数时,可变参数实际上就是一个数组,因此我们可以直接使用数组的class对象表示:

Method method = clazz.getDeclaredMethod("test", String[].class);

修改类属性

通过反射可以访问和修改类中成员字段的值
getField()获得类定义的指定字段

 Field field = clazz.getField("i");   //获取类的成员字段i
    field.set(instance, 100);   //将类实例instance的成员字段i设置为100

当访问private字段时,同样可以按照上面的操作进行越权访问

反射几乎可以把一个类的信息获取到,任何属性,任何内容,都可以被反射修改,无论权限修饰符是什么,但是final修饰时还是会修改失败,但是它可以直接去掉final修饰符

自定义类加载到ClassLoader

通过将类文件编译成.calss二进制文件,使用自己编写的加载器的defineClass,读取文件并在main中使用反射加载类,从而成功加载外部class,并且可以使用方法和创建对象,而不需要import

通过这种方式,就可以实现外部加载甚至是网络加载一个类,只需要把类文件传递即可,这样就无需再将代码写在本地,而是动态进行传递,不仅可以一定程度上防止源代码被反编译(只是一定程度上,想破解代码有的是方法),而且在更多情况下,还可以对二进制.class文件内容的byte[]进行加密,保证在传输过程中的安全性。

注解

比如@Override表示重写父类方法(不加效果也是一样的,此注解在编译时会被自动丢弃)注解本质上也是一个类,只不过它的用法比较特殊。

注解可以被标注在任意地方,包括方法上、类名上、参数上、成员属性上、注解定义上等,就像注释一样,它相当于我们对某样东西的一个标记。而与注释不同的是,注解可以通过反射在运行时获取,注解也可以选择是否保留到运行时。

预设注解

JDK预设了以下注解,作用于代码:

  • @Override - 检查(仅仅是检查,不保留到运行时)该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings - 指示编译器去忽略注解中声明的警告(仅仅编译器阶段,不保留到运行时)
  • @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
  • @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。

元注解

元注解是作用于注解上的注解,用于我们编写自定义的注解:

  • @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
  • @Documented - 标记这些注解是否包含在用户文档中。
  • @Target - 标记这个注解应该是哪种 Java 成员。
  • @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
  • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

注解的使用

使用反射来获取注解

举报

相关推荐

0 条评论