现在我们开始来讲解一下Java中的反射,因为知识有点多,我打算分两三个章节发出来。
坚持自己的梦想,即使没有翅膀也能飞翔
什么是反射
反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。
对反射更简单的解释就是:Java是个千年难遇的美女,但是美女有很多事情是规定不让你做的。而反射就是一把枪,有枪在手,你想让美女干嘛就干嘛。
为什么需要反射
一句话概括就是使用反射可以赋予JVM动态编译的能力,否则类的元数据信息只能用静态编译的方式实现,例如热加载,Tomcat的classloader等等都没法支持。
反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。通过Class实例获取Class信息的方法称为反射。
Java的反射机制是在编译并不确定是哪个类被加载了,而是在程序运行的时候才加载、探知、自审。使用在编译期并不知道的类。这样的特点就是反射。
反射有什么用
Java的反射用的最多的地方就是在框架里面。举个例子,比如说有两个程序员,他们俩个都在创建两个类,第一个程序员现在需要第二个程序员创建的类,可是现在第二个程序员创建的类还没有完成。问:现在第一个程序员可以成功编译吗?
答案当然是不行的,可是现在第一个程序员又需要第二个程序员的类,这个时候,反射就可以完成这种想法。因为反射是在类运行的时候获取对象的各个信息,所以第一个程序员的类就可以完成编译了。
小总结:
- 在写代码的时候,如果我们使用了未定义的类,编译就会报错,Java的反射机制就可以解决该问题
- 反射使得代码更具通用性和灵活性。
Class类
除了基本类型之外,Java的其他类型全部都是class(包括interface)。如:
- String
- Object
- Runnable
- Exception
- …
class(包括interface)的本质是数据类型(Type)。无继承关系的数据类型无法赋值:
Number n = new Double(123.456);//yes
String s = new Double(123.456);//error
前面我们说了通过Class实例获取Class信息的方法称为反射,那我们要如何获取一个class的Class实例呢?
- 方法一:直接通过一个class的静态变量class获取:
Class c = String.class;
- 方法二:如果我们有一个实例变量,可以通过该实例提供的getClass()方法获取:
String s = "Hello!";
Class c = s.getClass();
- 方法三:如果知道一个class的完整类名,可以通过静态方法Class.forName()获取:
Class c = Class.forName("java.lang.String");
Class实例比较和instanceof的差别:
Integer n = new Integer(123);
boolean b1 = n instanceof Integer; // true,因为n是Integer类型
boolean b2 = n instanceof Number; // true,因为n是Number类型的子类
boolean b3 = n.getClass() == Integer.class; // true,因为n.getClass()返回Integer.class
boolean b4 = n.getClass() == Number.class; // false,因为Integer.class!=Number.class
Integer是int的包装类,Java为每个原始类型都提供了包装类型,从Java5开始引入了自动装箱/拆箱机制,使得二者可以相互转换。以后我会写一篇文章来具体介绍一下。
instanceof关键字的作用:是判断一个对象是否是一个具体类的实例,我们在重写equals方法中要先判断是否是同一对象,之后再判断一个对象是否是另一个的实例,如果是判断各个属性值以判断是否是同一对象,不是一定不是同一对象。
动态加载类
JVM在执行Java程序的时候,并不是一次性把所有用到的class全部加载到内存,而是第一次需要用到class时才加载。
在获得类类型中,有一种方法是 Class.forName(“类的全称”),有以下要点:
- 不仅表示了类的类类型,还代表了动态加载类
- 编译时刻加载类是静态加载类,运行时刻加载类是动态加载类
再说动态加载类之前,我们先了解一下静态加载类:
class Test1
{
public static void main(String[] args) {
// new 创建对象,是静态加载类,在编译时刻就需要加载所有的可能使用到的类
if ("word".equals(args[0])) {
Word w = new Word();
w.start();
}
if("Excel".equals(args[0])) {
Excel e = new Excel();
e.start();
}
}
}
如果运行该程序,会报错,因为 new 创建对象,是静态加载类,在编译时刻就需要加载所有的可能使用到的类,但我们并没有 Word 和 Excel 类。
现在我们通过 Class.forName(“具体包名”) 来动态加载类。
class Test2{
public static void main(String[] args) {
try {
// 动态加载类,在运行时刻加载
// args 是使用命令行的参数列表 形如: java 指定的程序名 参数
// args[0]即参数列表的第一个参数
Class c = Class.forName(args[0]);
//通过类类型,创建该类对象
OfficeAble oa = (OfficeAble) c.newInstance();
oa.start();
}
catch (Exception e) {
e.printStackTrace();
}
}
}
执行 javac 后我们发现程序并没有报错,因为该类动态加载 OfficeAble ,编译过程不加载,只在运行时刻加载。
通过Class实例能干嘛呢,下面我们就来讲讲
通过Class实例获取字段信息
Class类提供了以下几个方法来获取字段:
- Field getField(name):根据字段名获取某个public的field(包括父类)
- Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
- Field[] getField():获取所有的public的field(包括父类)
- Field[] getDeclaredFields():获取当前类所有的field(不包括父类)
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 获取public字段"score":
System.out.println(stdClass.getField("score"));
// 获取继承的public字段"name":
System.out.println(stdClass.getField("name"));
// 获取private字段"grade":
System.out.println(stdClass.getDeclaredField("grade"));
}
}
class Student extends Person {
public int score;
private int grade;
}
class Person {
public String name;
}
上述代码首先获取Student的Class实例,然后分别获取public字段、继承的public字段以及private字段。
一个Field对象包含了一个字段的所有信息:
- getName():返回字段名称,如:“name”;
- getType():返回字段类型,也是一个Class实例,如:String.class;
- getModifiers():返回字段的修饰符,他是一个int。
获取字段值
利用反射拿到字段的一个field实例只是第一步,我们还可以拿到一个实例对应的该字段的值。
实例一:
public class Main {
public static void main(String[] args) throws Exception {
Object p = new Person("Xiao Ming");
Class c = p.getClass();
Field f = c.getDeclaredField("name");
Object value = f.get(p);
System.out.println(value); // "Xiao Ming"
}
}
class Person {
public String name;//得是用public,要是使用private会报错
//除了public还可以在调用Object value = f.get(p);前,先写一句:f.setAccessible(true);
public Person(String name) {
this.name = name;
}
}
结果为:
Xiao Ming
这里可能会有人说,使用f.setAccessible(true);可以直接调用private字段,那封装岂不是没意义了吗。
正常情况下,我们总是通过p.name来访问Person的name字段,编译器会根据public、protected和private决定是否允许访问字段,这样就达到了数据封装的目的。
而反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值
设置字段值
我们获取字段值之后,自然可以设置字段的值。设置字段值是通过Field.set(Object, Object)实现的。其中第一个Object参数是指定的实例,第二个Object参数是待修改的值。
实例二:
public class Main {
public static void main(String[] args) throws Exception {
Person p = new Person("Xiao Ming");
System.out.println(p.getName()); // "Xiao Ming"
Class c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true);
f.set(p, "Xiao Hong");
System.out.println(p.getName()); // "Xiao Hong"
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
结果为:
Xiao Ming
Xiao Hong
//将Xiao Ming变成了Xiao Hong Hong
大家要是对uni-app和Java的其他知识有兴趣可以去看看我主页喔,虽然发表的不多,但是我会努力的
反射二在这,点击跳转