反射和注解
反射就是把Java类中的各个成分映射成一个个Java对象,从而在运行中可以知道该类的所有属性和方法,调用任意一个对象的任意一个方法和属性。这种动态获取信息以及动态调用对象方法的功能叫作反射机制。
权限非常高,能获取泛型,甚至能越级,需要谨慎使用
通过特定的方法,即使没有调用相关的类包,但是却可以使用
类加载机制
Java启动时,JVM会将一部分类(class文件夹)先加载,也就是不会全部加载,通过ClassLoader进行类加载,在加载过程中将类的信息提取出来(存放在元空间中,在JDK1.8之前会放在永久代),同时生成一个Class对象存放在内存(堆内存)中,此Class对象只会存在一个,与加载的类唯一对应。
由于双亲委派机制的存在,手动创建一个与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 开始支持,标识某注解可以在同一个声明上使用多次。
注解的使用
使用反射来获取注解