(面试常问)java的反射机制

在面向对象的世界里,万事万物皆对象;其实,在java语言中,静态的成员、普通数据类型(例如int a=5)是不属于对象的,静态的成员是属于类的而不是属于对象的,而普通数据类型虽然不是属于对象的,但是它们有对应的包装类。所以,在面向对象世界里,最好看作万事万物皆对象。

类也是对象,是java.lang.Class类的对象

例如我们我们自定义的person类也是对象,是Class类的实例对象,官方的叫法是该类的类类型;从java.lang.Class类的源码可知,该类的构造方法是私有的,那么怎么表示这个实例对象呢?或者说如何获取某个类的Class对象呢,有以下三种方式:

public class reflectDemo1 {
	public static void main(String[] args) {
		
		//第一种方式:调用Class类的静态方法forName,并传入类的全限定类名
		Class c1=null;
		try {
			c1 = Class.forName("reflect.person");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		//第二种方法:直接通过类的class属性,表示任何一个类都有一个隐含的静态成员变量
		Class c2 = person.class;
		//第三种方式,通过该类的对象调用getClass方法,该方法是Object类的,所以每个类都有该方法
		person p=new person();
		Class c3=p.getClass();
		
		//c1,c2,c3都代表person类的类类型,一个类只可能是Class的实例对象,所以三种方式创建的Class对象都是一样的
		System.out.println(c1==c2);//true
		System.out.println(c1==c3);//true
	}
}

class person{
}

还可以通过类的Class对象来直接创建该类的实例对象,但是前提是该类必须要无参构造方法;需要进行强制类型转换。

try {
		person p1 = (person)c1.newInstance();//调用的是无参构造方法
	} catch (InstantiationException | IllegalAccessException e) {
		e.printStackTrace();
	}

反射的好处

反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并获得类信息;Class.forName(“类全称”) 不仅表示了类的类类型,还代表了动态加载类

  • 编译时刻加载类:静态加载类。new创建对象是静态加载类,在编译时刻就需要加载所有可能使用到的类。
  • 运行时刻加载类:动态加载类。通过动态加载类,在运行时加载可解决该问题,写一个接口(功能性类)。

下面分别演示静态加载类和动态加载类

静态加载类->分析该代码出现的问题:如果Student类和teacher类其中一个不存在,那么该程序无法通过编译,就算你只需要使用到Student类,并且Student类也是存在的,但是由于teacher类不存在,那么程序照样无法通过;这就是静态加载类的最大弊端,假如我们的程序有上万个功能模块,要是其中一个模块通过new出来的类不存在,虽然我们可能不用到它,但是它连编译都没通过,代价岂不是太大了。通过动态加载类可以解决问题。

tips:这也更好的解释了我上篇博客所说的耦合度相关的知识,通过new出来的对象属于编译时刻加载类,耦合性太强。应该尽量避免使用new来创建对象。https://blog.csdn.net/can_chen/article/details/104939433

public static void main(String[] args) {	
		if("student".equals(args[0])){
			student s=new Student();
		}
		if("teacher".equals(args[0])){
			teacher t=new teacher();
		}
	}

动态加载类->分析该代码:使用Class.forName(“类的全限定类名”)创建对象,是属于动态加载类,解决了静态加载类出现的弊端,这种方式在运行期间才读取类的全限定类名字符串,就算没有Student类,编译照样可以通过,直到运行期要使用到才会出现异常。

public static void main(String[] args) {	
		try {
			Class c=Class.forName(args[0]);
			Student s = (Student)c.newInstance();
		} catch (Exception e) {
			e.printStackTrace();
		}	
	}
}

代码优化: 因为通过类的Class对象来创建类的对象,需要进行强制类型转换,所以我们定义一个接口,接收所有的对象,再让具体的对象去实现它就可以了

public class reflectDemo2 {

	public static void main(String[] args) {
		try {
			Class c = Class.forName(args[0]);
			IBase base = (IBase) c.newInstance();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

interface IBase {
}

Class对象的获取功能

  • 获取成员方法以及对成员方法的操作
//获取所有public修饰的方法(包括继承的类以及接口,例如Object类的方法也会获取到)
Method[] getMethods()  
//根据方法名以及方法参数的Class对象获取指定的public方法
Method getMethod(String name,<?>... parameterTypes)  
//获取所有方法,包括private修饰的方法
Method[] getDeclaredMethods()  
//根据方法名以及方法参数的Class对象获取指定的方法
Method getDeclaredMethod(String name,<?>... parameterTypes) 
//执行方法,参数包括类的实例对象以及实际参数
Object invoke(Object obj, Object... args)  
//获取方法名称
String getName()

举个栗子

public static void main(String[] args) throws Exception{
		Class c=student.class;
		student s=new student();
		Method m=c.getMethod("testC",int.class);
		m.invoke(s,10);//执行方法
		System.out.println(m.getName());//方法名
		System.out.println(m.getReturnType());//方法的返回类型
		Class[] array = m.getParameterTypes();//方法的参数
		for(Class x:array){
			System.out.println(x.getName());
		}
	}
  • 获取成员变量以及对成员变量的操作
//获取所有public修饰的成员变量
Field[] getFields() 
//获取指定名称的 public修饰的成员变量
Field getField(String name) 
// 获取所有的成员变量,不考虑修饰符
Field[] getDeclaredFields() 
//获取指定名称的成员变量,不考虑修饰符
Field getDeclaredField(String name)  
//设置值,参数为实际的对象类型以及具体的参数值
void set(Object obj, Object value) 
//获取值
get(Object obj) 
//忽略访问权限修饰符的安全检查,暴力反射,即如果要对private的成员变量进行操作,必须先进行该设置
setAccessible(true)

举个栗子

public static void main(String[] args) throws Exception{
		Class c=student.class;
		student s=new student();
		Field[]fields  = c.getDeclaredFields();
		for(Field f:fields){
			f.setAccessible(true);//暴力反射,否则会抛出异常
			System.out.println(f.getName());//获取变量名
			System.out.println(f.get(s));//获取变量值
		}
	}
  • 获取构造方法以及对构造方法的操作
//获取所有public修饰的构造方法
Constructor<?>[] getConstructors()  
//获取指定参数的public构造方法
Constructor<T> getConstructor(<?>... parameterTypes)  
//获取指定参数的构造方法,不考虑修饰符
Constructor<T> getDeclaredConstructor(<?>... parameterTypes)  
//获取所有的构造方法,不考虑修饰符
Constructor<?>[] getDeclaredConstructors()  
//使用构造方法创建对象:
T newInstance(Object... initargs)  

//另外,如果使用空参数构造方法创建对象,操作可以简化:直接使用Class对象的newInstance方法

举个栗子

public static void main(String[] args) throws Exception{
		Class c=student.class;
		Constructor constructor = c.getConstructor(int.class,String.class);
		student s = (student)constructor.newInstance(10,"hh");
	}

补充: 我们曾认为类的私有属性或私有方法外部类是无法访问的,只能在本类进行访问,通过学习了反射,我们可以知道,在反射机制面前,所有的属性和方法都将暴露无疑,只是对于私有成员的访问,我们要先执行一个操作:通过setAccessible(true)方法设置忽略访问权限修饰符的安全检查,然后就可以为所欲为的操作了。

反射与泛型

关于java的泛型,在之前已经学习过了,但是关于泛型的很多知识点,只是知道,但是并不理解,学习完反射机制后,再回过头来看泛型,有了更深一步的认知,所以在这里将反射与泛型结合起来整理。

1.回顾泛型

JDK1.5引入泛型,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为【泛型类】、【泛型接口】、【泛型方法】。

我们先来看看未使用泛型的情况:

//未使用泛型
ArrayList list = new ArrayList();
list.add("aaa");
String str1 = (String) list.get(0);

未使用泛型,那么ArrayList集合类可以存放任何类型的对象,get方法获取集合中的元素,返回类型是Object类,这时候就需要进行强制类型转换,使用强制类型转换是不安全的,例如我们从集合中获取的对象是Integer类型的,但是我们却把它强转成String类型,虽然编译期没有任何问题,但是运行时就会出现类型转换异常了。

我们再来看看使用泛型后的情况:

//使用泛型
ArrayList<String> list = new ArrayList<String>();
list.add("aaa");//正确
list.add(10);//编译无法通过
String str1 = list.get(0);

使用泛型的好处:

  • 简单易用
  • 消除了强制类型转换,使得代码可读性好,减少了很多出错的机会
  • 保证类型安全,多数情况下我们想要一个集合只存放一种类型的对象,通过泛型就可以实现,编译时可以进行类型检查,提高 Java 程序的类型安全。

关于java泛型的类型擦除:
Java的泛型是伪泛型。为什么说Java的泛型是伪泛型呢?因为,在编译期间,所有的泛型信息都会被擦除掉。泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

既然说泛型的类型在编译期就进行类型擦除了,那为什么编译期还能进行类型检查呢?

java编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,再进行编译的。

因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?

使用泛型后,在获取值时,java会进行自动类型转换

关于泛型的类型擦除和内部原理,推荐一篇博客:https://blog.csdn.net/lonelyroamer/article/details/7868820

2.通过反射了解泛型的本质

我们知道,由于泛型的类型擦除机制,java泛型只作用在编译阶段,通过以下代码可以进一步得以验证

ArrayList  list1=new ArrayList();//该集合可以存放任何的对象
Class c1 = list1.getClass();
ArrayList<Integer> list2=new ArrayList<Integer>();//该集合只能存放Integer类型的对象
Class c2 = list2.getClass();
System.out.println(c1==c2);//true

由于反射的操作都是在编译之后,即运行期起作用的,那么就可以通过反射来跳过编译期的类型检查,具体实现看以下代码:

public static void main(String[] args) {

	    ArrayList<Integer> list=new ArrayList<Integer>();//该集合只能存放Integer类型的对象
	    list.add(100);
	    Class c = list.getClass();//获取ArrayList类的类类型
	    try {
			Method m = c.getMethod("add", Object.class);
			m.invoke(list, "aa");//绕过编译期也就绕过泛型了,可以加入字符串对象
			System.out.println(list);//[100, aa]
			Integer integer = list.get(1);//运行时会出现ClassCastException异常
		} catch (Exception e) {
			e.printStackTrace();
		} 
	}

推荐一个当时学反射知识的视频(力推!):https://www.imooc.com/learn/199

反射与注解

通过反射可以获取注解中定义的属性值,要动态的获取类的信息可以通过在配置文件中保存类的全限定类名,然后读取配置文件,再通过反射创建对象、执行方法等;同样,可以使用注解来代替配置文件,将类的全限定类名等信息保存在注解的属性中,并通过反射获取注解中定义的属性值,一样可以实现。

自定义注解

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface pro {
	String className();//类的全限定类名 
	String methodName();//方法名
}

测试

@pro(className="annotation.person",methodName="show")
public class ReflectTest {

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

		//获取类的Class对象
		Class c=ReflectTest.class;
		//获取该类上的注解
		pro an = (pro) c.getAnnotation(pro.class);
		//获取注解的属性值
		String className = an.className();
		String methodName = an.methodName();
		System.out.println(className);
		System.out.println(methodName);
		
		Class c1 = Class.forName(className);
		Method method = c1.getMethod(methodName);
		person p=new person();//创建一个person对象实例
		method.invoke(p);
	}
}

猜你喜欢

转载自blog.csdn.net/can_chen/article/details/104944462
今日推荐