java基础(11) 反射

本文深入解析Java反射机制,包括Class对象获取方式、通过反射创建对象、操作成员变量和方法,以及反射在配置文件和框架中的应用。同时,探讨了如何通过反射越过泛型检查。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

谈到反射,可能我们会觉得很陌生,因为平时的开发似乎根本没有用到这个概念。但结果恰恰相反,反射是与我们日常开发关联最密切的东西。

举个例子,在当我们使用eclipse写代码的时候,在任意一个对象的后面写一个点就会罗列出来该对象的所有属性和方法,这其实就是使用反射实现的。再比如使用框架(如SpringBoot)的时候需要写一些配置文件,这些配置文件也是通过反射注入到你的代码中的。例如这样:

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:8080/testdb?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123

所以反射最重要的用途就是开发各种框架。那么说了这么多,反射到底是什么呢?

我们先来看一段很常见的代码:

Person p = new Person();

p.setName("张三"); 

很明显,JVM对这段代码进行编译的时候,已经知道了要使用的类(Person),并且要用到Person类的setName()方法。

而反射恰恰相反——在编译期JVM不知道具体要使用什么类以及要用到这个类的什么方法、什么属性。换而言之,反射是JVM在运行期动态的加载类或者调用类的方法和属性(注意这里强调的是运行期)。

 

获取Class对象

要想运用反射就离不开Class对象。在不同的阶段,获取Class对象有不同的方式。

1、源码阶段:Person.java

  获取方式:Class.forName("org.hu.test.entity.Person")

  作用:可以用来读取配置文件,例如加载数据库驱动

2、字节码阶段:Peson.class

   获取方式:Person.class     

   作用:可以用作同步监视器(在这篇文章中有提及:Java基础(7) 多线程

3、对象阶段:Person p = new Person()

   获取方式:p.getClass()   

   作用:可以用来判断是否是同一个字节码文件,比较典型的是equals()方法

具体可以参考下面的例子:

package org.hu.test.entity;

public class Person {
	
	private String name;
	
	private Integer age;

	public Person() {
		super();
	}

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

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Integer getAge() {
		return age;
	}

	public void setAge(Integer age) {
		this.age = age;
	}

	@Override
	public String toString() {
		return "Person [name=" + name + ", age=" + age + "]";
	}
	
}
public class Test {
	 
	public static void main(String[] args) throws ClassNotFoundException {
 
		Class<?> clazz1 = Class.forName("org.hu.test.entity.Person");
		
		Class<Person> clazz2 = Person.class;
		
		Person p = new Person();
		Class<? extends Person> clazz3 = p.getClass();
		
		System.out.println(clazz1 == clazz2);  // true
		System.out.println(clazz2 == clazz3);  // true
		
	}
 
}

tip:三种方式获取的Class对象的泛型各不相同。

 

通过反射创建对象

大家都知道,创建对象需要调用类的构造方法,通过反射创建对象也不例外。

调用Class对象的newInstance()方法,就可以创建Class对象对应类的对象,而该方法的本质就是调用了无参构造方法。

public class Test {
	 
	public static void main(String[] args)
			throws InstantiationException, IllegalAccessException, ClassNotFoundException {
 
		Class<?> clazz = Class.forName("org.hu.test.entity.Person");
		Object obj = clazz.newInstance();
		Person p = (Person) obj;
		System.out.println(p);
		
	}
}

上面的代码运行起来没有问题。但是如果Person类没有空参构造方法,或者说Person类的空参构造方法是private的呢?所以使用Class对象的newInstance()方法是有限制的——只能调用可见的空参构造方法。这里提到的可见指的是权限修饰符的作用域(具体在这篇文章中有介绍:Java基础(4) 对象)。

所以如果想要通过反射创建对象时,没有达到上述要求,就需要另觅他径。

首先如果Person类没有空参构造,那么就可以使用Class对象提供getConstructor()方法来创建对象。该方法接受任意个Class类型参数并返回Constructor类型结果,换而言之,Person类的无参有参构造方法都可以通过方法调用。

Constructor类也提供newInstance()方法,相较于Class对象提供的newInstance()方法,Constructor类提供的newInstance()方法可以接受任意个Object类型。

getConstructor()方法和newInstance()方法的关系如下:

getConstructor(构造方法参数类型1,构造方法参数类型2,...) // 声明要使用的构造方法(该构造方法必须存在且可见)

newInstance(创建对象的参数1,创建对象的参数2,...)  // 传入参数顺序对应和getConstructor()方法中的参数类型

下面通过一个例子演示:

public class Test {

	public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException,
			IllegalAccessException, IllegalArgumentException, InvocationTargetException, ClassNotFoundException {

		Class<?> clazz = Class.forName("org.hu.test.entity.Person");
		// getConstructor()方法的参数顺序要严格按照已有构造方法的参数顺序来写, 否则会抛出NoSuchMethodException
		Constructor<?> c = clazz.getConstructor(String.class, Integer.class);
//		Constructor<?> c = clazz.getConstructor(Integer.class, String.class);    // 报错
		Person p = (Person) c.newInstance("张三", 23);
		System.out.println(p);

	}
}

其次如果Person类中的构造方法是不可见的,例如这样:

private Person() {
        super();
}

这时候Peron类是无法通过空参构造方法来创建对象的:

Person p = new Person();    // 报错:The constructor Person() is not visible

然而即使是这样,我们依然可以通过反射来调用Person类的私有空参构造方法创建对象。由于此时Peron类的空参构造方法相对于Test类是不可见的,所以这时候就需要使用Class对象的getDeclaredConstructor()方法,该方法可以获取对应类的任意一个已存在的构造方法。

但是仅调用getDeclaredConstructor()方法还是无法创建对象的,因为方法不可见还意味着对于Test类而言是无权调用该方法的。所以我们还需要设置setAccessible(true),使该方法可以被访问。具体可以参考下面的例子:

public class Test {

	public static void main(String[] args) throws Exception {

		Class<?> clazz = Class.forName("org.hu.test.entity.Person");
		Constructor<?> c = clazz.getDeclaredConstructor();
		c.setAccessible(true);
		Person p = (Person) c.newInstance();
		/**
		 * getModifiers() 获取该方法的修饰符信息
		 * Modifier.toString() 翻译修饰符信息
		 */
		System.out.println(Modifier.toString(c.getModifiers()));
		System.out.println(p);

	}
}

 

通过反射操作成员变量和成员方法

在反射面前,每一个类都是“赤裸裸”的。所以我们不仅可以通过反射来创建对象,也可以通过反射来操作对象的所有成员变量和成员方法。

Class对象的getField()方法可以获取任意类的一个成员变量,该方法接受一个String类型参数(代表类成员变量的字段名)并返回Field类型结果,通过Field对象提供的get、set方法就可以操作对象的成员变量。下来通过一个例子演示:

public class Test {
	 
	public static void main(String[] args) throws Exception    {
		
		Person p = new Person("张三", 21);
		Class<?> clazz = Class.forName("org.hu.test.entity.Person");
		Field field = clazz.getField("name");
		field.set(p, "李四");
		System.out.println(p);
	}
 
}

运行上面的代码,你就会发现程序报错了。如果上面讲解通过反射创建对象的时候你已经完全理解了,那么这里你应该很快就可以反应过来:因为Person类的成员变量“name”是对于Test类而言是不可见的。

所以这里我们应该使用getDeclaredField()方法来获取已声明的成员变量,然后设置setAccessible(true)使该成员变量可以被访问,就可以操作Person对象p的“name”成员变量:

public class Test {

	public static void main(String[] args) throws Exception {

		Person p = new Person("张三", 21);
		Class<?> clazz = Class.forName("org.hu.test.entity.Person");
		Field declaredField = clazz.getDeclaredField("name"); 
		declaredField.setAccessible(true); 
		declaredField.set(p, "李四");
		System.out.println(p);
		
	}
}

和成员变量一样,同为类的成员,成员方法也是可以通过反射操作的。目前Person类中没有成员方法,在Person类中添加如下两个方法:

public void eat() {
        System.out.println("i am eating");
}  
public void eat(String food) {
        System.out.println("eat " + food);
}

相信经过上面的通过反射创建对象和通过反射获取成员变量两个例子,你已经对权限修饰符有些印象了,这里就不挖坑了,两个方法都设置为public。

通过Class对象的getMethod()方法可以获取类的成员方法,该方法接受两个参数——String类型的成员方法名字和任意个Object类型的成员方法参数,返回结果是Method类型。调用Method对象的invoke()方法,就可以执行类的成员方法。具体看下面的例子:

public class Test {
	 
	public static void main(String[] args) throws Exception {
		
		Person p = new Person("张三", 21);
		Class<?> clazz = Class.forName("org.hu.test.entity.Person");
		Method m = clazz.getMethod("eat", String.class);
		m.invoke(p, "apple");
	}
}

 

反射与配置文件

在文章开篇的时候提过,框架的配置文件就是通过反射来实现的。下面通过一个例子来演示配置文件是如何作用于代码的。

大家应该都知道榨汁机,想喝什么果汁的时候,把水果丢进去,就可以得到一杯果汁。下面用程序来模拟榨汁:

public interface Fruit {
	
	void juicing();

}
public class Orange implements Fruit {

	@Override
	public void juicing() {
		System.out.println("Orange juice");
	}

}
public class Apple implements Fruit {

	@Override
	public void juicing() {
		System.out.println("Apple juice");
	}
	
}
public class Juicer {
	
	public void run(Fruit f) {
		f.juicing();
	}

}
public class Test {
	 
	public static void main(String[] args) {
 
		Juicer j = new Juicer();
		j.run(new Apple());
//		j.run(new Orange());
		
	}
}

通过上面的代码,当我们想喝苹果汁的时候,就可以向榨汁机j中丢一个苹果,想喝橘子汁的时候就向榨汁机j中放一个橘子。

但是大家可以发现一个问题,就是我们每次改变需求的时候(喝不同的果汁)就需要去修改源码,这样是很不方便的,也不利于维护。这里我们可以使用配置文件来解决这个问题。

在项目下新建配置文件config.properties(例子中是创建在项目根目录下),在文件中写入你想要获取的类的全名

org.hu.test.entity.Apple

然后我们就可以通过配置文件获取到类的名字,然后使用forName()方法来创建Class对象。后面如果出现需求改变,直接在配置文件修改就可以了。具体参考下面的例子:

public class Test {

	public static void main(String[] args) throws Exception {

		BufferedReader br = new BufferedReader(new FileReader("config.properties"));
		Class<?> clazz = Class.forName(br.readLine());
		Fruit f = (Fruit) clazz.newInstance();
		Juicer j = new Juicer();
		j.run(f);
	}
}

 

越过泛型检查

在之前的文章提到过,泛型在编译期有效,在运行期泛型会被擦除。而我们知道,反射在运行期进行的,所以通过反射就可以越过编译期的泛型检查。具体看下面一个例子:

public class Test {
	 
	public static void main(String[] args) throws Exception {
		
		ArrayList<Integer> list = new ArrayList<>();
		list.add(1);
		list.add(2);
//		list.add("abc");	// 错误:泛型检查,无法加入
		
		Class<?> clazz = Class.forName("java.util.ArrayList");
		Method method = clazz.getMethod("add", Object.class);
		method.invoke(list, "abc");
		System.out.println(list);
	}
}

 

<think>嗯,用户之前问过关于`int`和`integer`的区别,现在转向Java基础中的反射。可能用户正在学习Java,之前的问题涉及基本数据类型和包装类,现在深入到了更高级的主题。我需要先确定用户对Java基础掌握程度,反射通常是面向对象之后的内容,所以用户可能已经有一定的Java基础。 用户的需求是了解Java反射基础知识,可能需要知道反射是什么、为什么用它、以及如何使用。深层需求可能包括实际应用场景或常见问题,比如性能影响或者安全性考虑。需要结构清晰,分点解释,帮助用户逐步理解。 首先,反射的定义,必须用简单明了的语言,避免术语过多。然后核心类,比如Class、Constructor、Method、Field,这些是反射的关键类,需要举例说明。应用场景部分,用户可能想知道反射的实际用途,比如框架开发或动态加载,要举常见的例子如Spring的IoC。 接下来要讲实现步骤,获取Class对象的三种方式,实例化对象的方法,访问字段和调用方法。每个步骤都需要代码示例,但用户可能对代码的细节不太清楚,所以需要解释每个步骤的作用。例如,`Class.forName`可能会抛出异常,需要提及异常处理,但可能不需要深入,保持基础知识的介绍。 优缺点部分需要平衡,用户可能想知道为什么有时候不推荐使用反射,所以性能和安全问题是重点。最后,实际案例,比如动态加载类,或者简单的框架应用,帮助用户将理论联系实际。 还要注意用户可能存在的误区,比如滥用反射导致代码难以维护,或者性能问题。需要提醒在必要时使用,并遵循最佳实践。此外,检查是否需要提到Java版本的变化对反射的影响,比如模块化系统带来的访问限制,但可能用户刚入门,暂时不需要深入。 最后总结,强调反射的灵活性和代价,帮助用户权衡使用场景。整体结构需要逻辑清晰,从定义到实现再到应用,逐步深入,确保用户能够跟随思路,逐步掌握反射基础知识。</think>好的!以下是关于 **Java反射(Reflection)** 的基础知识介绍,以逐步分析的方式展开: --- ### 一、反射的定义 **反射(Reflection)** 是Java提供的一种**运行时动态机制**,允许程序在**不预先知道类结构**的情况下,通过`Class`对象获取类的信息(如字段、方法、构造器等),并操作这些成员。简单来说:**通过反射,程序可以“观察”和修改自身的行为**。 --- ### 二、反射的核心类 Java反射通过以下核心类实现: 1. **`Class`**:表示类或接口的元数据。 2. **`Constructor`**:表示类的构造方法。 3. **`Method`**:表示类的方法。 4. **`Field`**:表示类的字段(属性)。 5. **`Modifier`**:解析成员的修饰符(如`public`、`private`)。 --- ### 三、反射的核心用途 1. **动态获取类信息** 例如:获取类名、父类、接口、注解等。 ```java Class<?> clazz = String.class; System.out.println("类名:" + clazz.getName()); // 输出:java.lang.String ``` 2. **动态创建对象** 即使没有显式定义构造方法,也可以通过反射实例化对象。 ```java Class<?> clazz = Class.forName("java.util.ArrayList"); Object list = clazz.newInstance(); // 创建ArrayList实例(已过时,推荐用构造器) ``` 3. **动态调用方法/访问字段** 可以绕过访问权限(如调用私有方法)。 ```java Method method = clazz.getDeclaredMethod("privateMethod"); method.setAccessible(true); // 突破private限制 method.invoke(obj); ``` 4. **实现通用框架** 如Spring的依赖注入(IoC)、MyBatis的ORM映射等。 --- ### 四、反射的实现步骤(以调用方法为例) 1. **获取`Class`对象** 三种方式: ```java Class<?> clazz1 = Class.forName("全限定类名"); // 通过类路径加载 Class<?> clazz2 = Object.class; // 通过类字面量 Class<?> clazz3 = obj.getClass(); // 通过对象实例 ``` 2. **获取方法或字段** ```java Method method = clazz.getDeclaredMethod("方法名", 参数类型.class); // 包括私有方法 Field field = clazz.getDeclaredField("字段名"); // 包括私有字段 ``` 3. **设置访问权限(可选)** ```java method.setAccessible(true); // 突破private限制 ``` 4. **执行操作** ```java Object result = method.invoke(对象实例, 参数值); // 调用方法 field.set(对象实例, 值); // 修改字段值 ``` --- ### 五、反射的优缺点 | **优点** | **缺点** | |------------------------------|-----------------------------------| | 动态性高(如插件化开发) | 性能较差(比直接调用慢约10-100倍)| | 可访问私有成员(灵活性强) | 代码可读性降低 | | 通用性高(框架开发必备) | 绕过类型安全检查,可能引发异常 | --- ### 六、实际应用案例 1. **动态加载类**(如插件机制): ```java Class<?> pluginClass = Class.forName("com.example.Plugin"); Object plugin = pluginClass.newInstance(); Method runMethod = pluginClass.getMethod("run"); runMethod.invoke(plugin); ``` 2. **Spring框架的依赖注入**: ```java // 通过反射创建Bean并注入属性 Class<?> beanClass = Class.forName("com.example.UserService"); Object bean = beanClass.newInstance(); Field daoField = beanClass.getDeclaredField("userDao"); daoField.setAccessible(true); daoField.set(bean, new UserDaoImpl()); // 注入依赖 ``` --- ### 七、注意事项 1. **性能优化**:避免在频繁调用的代码中使用反射。 2. **安全限制**:Java 9+的模块系统可能限制反射访问(需在`module-info.java`中声明`opens`)。 3. **代码健壮性**:反射调用需处理大量异常(如`ClassNotFoundException`、`NoSuchMethodException`)。 --- ### 总结 反射Java中强大的动态编程工具,**核心价值是灵活性**,但需谨慎使用。掌握反射后,可以深入理解框架设计原理(如Spring、Hibernate),但日常开发中应优先考虑直接调用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值