[Java面试基础]注解、反射详解

注解和运用注解编程

内容:

  • 注解的作用

  • 注解的本质

  • 反射注解信息

  • 元注解

  • 属性的数据类型及特别的属性:value数组

  • 反射机制概述

  • 类的加载与ClassLoader

  • 理解Class类并获取Class实例

注解的作用

格式

public @interface 注解名称{
    属性列表;
}

格式有点奇怪,我们稍后再研究。

分类

大致分为三类:自定义注解、JDK内置注解、还有第三方框架提供的注解

自定义注解就是我们自己写的注解。JDK内置注解,比如@Override检验方法重载,@Deprecated标识方法过期等。第三方框架定义的注解比如SpringMVC的@Controller等。

使用位置

实际开发中,注解常常出现在类、方法、成员变量、形参位置。当然还有其他位置,这里不提及。

作用

如果说注释是写给人看的,那么注解就是写给程序看的。它更像一个标签,贴在一个类、一个方法或者字段上。它的目的是为当前读取该注解的程序提供判断依据。比如程序只要读到加了@Test的方法,就知道该方法是待测试方法,又比如@Before注解,程序看到这个注解,就知道该方法要放在@Test方法之前执行。

级别

注解和类、接口、枚举是同一级别的。

注解的本质

@interface和interface这么相似,我猜注解的本质是一个接口。

为了验证这个猜测,我们做个实验。先按上面的格式写一个注解

[Java面试基础]注解、反射详解_第1张图片

编译后得到字节码文件

在这里插入图片描述

通过XJad工具反编译MyAnnotation.class

[Java面试基础]注解、反射详解_第2张图片

我们发现,@interface变成了interface,而且自动继承了Annotation

[Java面试基础]注解、反射详解_第3张图片

既然确实是个接口,那么我们自然可以在里面写方法

在这里插入图片描述

得到class文件后反编译

[Java面试基础]注解、反射详解_第4张图片

由于接口默认方法的修饰符就是public abstract,所以可以省略,直接写成:

[Java面试基础]注解、反射详解_第5张图片

虽说注解的本质是接口,但是仍然有很多怪异的地方,比如使用注解时,我们竟然可以给getValue赋值:

[Java面试基础]注解、反射详解_第6张图片

你见过给方法赋值的操作吗?(别闹了,你脑中想到的是给方法传参)。虽然这里的getValue可能不是指getValue(),底层或许是getValue()返回的一个同名变量。但不管怎么说,还是太怪异了。所以在注解里,类似于String getValue()这种,被称为“属性”。给属性赋值显然听起来好接受多了。

还可以为属性指定默认值:

[Java面试基础]注解、反射详解_第7张图片

[Java面试基础]注解、反射详解_第8张图片

当没有赋值时,属性将使用默认值,比如上面的defaultMethod(),它的getValue就是“no description"。

基于以上差异,以后还是把注解单独归为一类,不要当成接口使用。

反射注解信息

上文已经说过,注解就像一个标签,是贴在程序代码上供另一个程序读取的。所以三者关系是:

[Java面试基础]注解、反射详解_第9张图片

要牢记,只要用到注解,必然有三角关系:定义注解,使用注解,读取注解。仅仅完成前两步,是没什么卵用的。就好比你写了一本武林秘籍却没人去学,那么这门武功还不如一把菜刀。

所以,接下来我们写一个程序读取注解。读取注解的思路是:

[Java面试基础]注解、反射详解_第10张图片

反射获取注解信息:

[Java面试基础]注解、反射详解_第11张图片

这是因为注解其实有所谓“保留策略”的说法。大家学习JSP时,应该学过 和<%-- -->的区别:前者可以在浏览器检查网页源代码时看到,而另一个在服务器端输出时就被抹去了。同样的,注解通过保留策略,控制自己可以保留到哪个阶段。保留策略也是通过注解实现,它属于元注解,也叫元数据。

元注解

所谓元注解,就是加在注解上的注解。作为普通程序员,常用的就是:

  • @Documented

用于制作文档,不是很重要,忽略便是

  • @Target

加在注解上,限定该注解的使用位置。不写的话,好像默认各个位置都是可以的。如果需要限定注解的使用位置,可以在自定义的注解上使用该注解。我们本次默认即可,不特别限定。

[Java面试基础]注解、反射详解_第12张图片

  • @Retention(注解的保留策略)

[Java面试基础]注解、反射详解_第13张图片

注解的保留策略有三种:SOURCE/ClASS/RUNTIME

[Java面试基础]注解、反射详解_第14张图片

  • 注解主要被反射读取
  • 反射只能读取内存中的字节码信息
  • RetentionPolicy.CLASS指的是保留到字节码文件,它在磁盘内,而不是内存中。虚拟机将字节码文件加载进内存后注解会消失
  • 要想被反射读取,保留策略只能用RUNTIME,即运行时仍可读取

[Java面试基础]注解、反射详解_第15张图片

[Java面试基础]注解、反射详解_第16张图片

重新运行程序,成功读取注解信息: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cMT0MFlx-1625639248605)[Java面试基础]注解、反射详解_第17张图片

注意,defaultMethod()反射得到的注解信息是:no description。就是MyAnnotion中getValue的默认值。

属性的数据类型及特别的属性:value和数组

属性的数据类型

  • 八种基本数据类型
  • String
  • 枚举
  • Class
  • 注解类型
  • 以上类型的一维数组
  • 自定义Junit框架
  • 山寨JPA

[Java面试基础]注解、反射详解_第18张图片

[Java面试基础]注解、反射详解_第19张图片

value属性

如果注解的属性只有一个,且叫value,那么使用该注解时,可以不用指定属性名,因为默认就是给value赋值:

[Java面试基础]注解、反射详解_第20张图片

[Java面试基础]注解、反射详解_第21张图片

[Java面试基础]注解、反射详解_第22张图片

注解的属性如果有多个,无论是否叫value,都必须写明属性的对应关系:

[Java面试基础]注解、反射详解_第23张图片

[Java面试基础]注解、反射详解_第24张图片

小结

  • 注解就像标签,是程序判断执行的依据。比如,程序读到@Test就知道这个方法是待测试方法,而@Before的方法要在测试方法之前执行
  • 注解需要三要素:定义、使用、读取并执行
  • 注解分为自定义注解、JDK内置注解和第三方注解(框架)。自定义注解一般要我们自己定义、使用、并写程序读取,而JDK内置注解和第三方注解我们只要使用,定义和读取都交给它们
  • 大多数情况下,三角关系中我们只负责使用注解,无需定义和执行,框架会将注解类读取注解的程序隐藏起来,除非阅读源码,否则根本看不到。平时见不到定义和读取的过程,光顾着使用注解,久而久之很多人就忘了注解如何起作用了!

[Java面试基础]注解、反射详解_第25张图片

自定义Junit框架

上一篇已经讲的很详细了,这里就直接上代码了。请大家始终牢记,用到注解的地方,必然存在三角关系,,并且别忘了设置保留策略为RetentionPolicy.RUNTIME。

[Java面试基础]注解、反射详解_第26张图片

MyBefore注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyBefore {
}

MyTest注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTest {
}

MyAfter注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAfter {
}

EmployeeDAOTest

//EmployeeDAO的测试类
public class EmployeeDAOTest {
	@MyBefore
	public void init() {
		System.out.println("初始化...");
	}

	@MyAfter
	public void destroy() {
		System.out.println("销毁...");
	}

	@MyTest
	public void testSave() {
		System.out.println("save...");
	}

	@MyTest
	public void testDelete() {
		System.out.println("delete...");
	}
}

MyJunitFrameWork

public class MyJunitFrameWork {
	public static void main(String[] args) throws Exception {
		// 1.先找到测试类的字节码:EmployeeDAOTest
		Class clazz = EmployeeDAOTest.class;
		Object obj = clazz.newInstance();
		// 2.获取EmployeeDAOTest类中所有的公共方法
		Method[] methods = clazz.getMethods();
		/* 3.迭代出每一个Method对象
                     判断哪些方法上使用了@MyBefore/@MyAfter/@MyTest注解
                */
		List<Method> mybeforeList = new ArrayList<>();
		List<Method> myAfterList = new ArrayList<>();
		List<Method> myTestList = new ArrayList<>();
		for (Method method : methods) {
			if(method.isAnnotationPresent(MyBefore.class)){
				//存储使用了@MyBefore注解的方法对象
				mybeforeList.add(method);
			}else if(method.isAnnotationPresent(MyTest.class)){
				//存储使用了@MyTest注解的方法对象
				myTestList.add(method);
			}else if(method.isAnnotationPresent(MyAfter.class)){
				//存储使用了@MyAfter注解的方法对象
				myAfterList.add(method);
			}
		}

		// 执行方法测试
		for (Method testMethod : myTestList) {
			// 先执行@MyBefore的方法
			for (Method beforeMethod : mybeforeList) {
				beforeMethod.invoke(obj);
			}
			// 测试方法
			testMethod.invoke(obj);
			// 最后执行@MyAfter的方法
			for (Method afterMethod : myAfterList) {
				afterMethod.invoke(obj);
			}
		}
	}
}

执行结果:

[Java面试基础]注解、反射详解_第27张图片

山寨JPA

要写山寨JPA需要两个技能:注解+反射。

注解已经学过了,反射还有一个进阶内容,之前那篇反射文章里没有提到。至于是什么内容,一两句话说不清楚。慢慢来吧。

首先,要跟大家介绍泛型中几个定义(记住最后一个):

  • ArrayList中的E称为类型参数变量
  • ArrayList中的Integer称为实际类型参数
  • 整个ArrayList称为泛型类型
  • 整个ArrayList称为参数化的类型ParameterizedType

好,接下来看这个问题:

class A<T>{
	public A(){
               /*
                我想在这里获得子类B、C传递的实际类型参数的Class对象
                class java.lang.String/class java.lang.Integer
               */
	}
}

class B extends A<String>{

}

class C extends A<Integer>{

}

我先帮大家排除一个错误答案:直接T.class是错误的。

[Java面试基础]注解、反射详解_第28张图片

所以,你还有别的想法吗?

我觉得大部分人可能都想不到,这不是技术水平高低的问题,而是知不知道API的问题。知道就简单,不知道想破脑袋也没辙。

我们先不直接说怎么做,一步步慢慢来。

请先看下面代码:

public class Test {
	public static void main(String[] args) {
		new B();
	}
}

class A<T>{
	public A(){
                //this是谁?A还是B?
		Class clazz = this.getClass();
		System.out.println(clazz.getName());
	}
}

class B extends A<String>{

}

请问,clazz.getName()打印的是A还是B?

答案是:B。因为从头到尾,我们new的是B,这个Demo里至始至终只初始化了一个对象,所以this指向B。

好的,到了这里,我们迈出了第一步:在泛型父类中得到了子类的Class对象!

我们再来分析:

class A<T>{
	public A(){
                //clazz是B.class
		Class clazz = this.getClass();
	}
}

class B extends A<String>{

}

现在我们已经在class A中得到B类的Class对象。而我们想要得到的是父类A中泛型的Class对象。且先不说泛型的Class对象,我们先考虑,如何获得通过子类Class对象获得父类Class对象?

查阅API文档,我们发现有这么个方法:

[Java面试基础]注解、反射详解_第29张图片

Generic Super Class,直译就是“带泛型的父类”。也就是说调用getGenericSuperclass()就会返回泛型父类的Class对象。这非常符合我们的情况。试着打印一下:

[Java面试基础]注解、反射详解_第30张图片

打印发现,A的Class对象是ParameterizedType的类型的。

[Java面试基础]注解、反射详解_第31张图片

这里我们不去关心Type、ParameterizedType还有Class之间的继承关系,总之以我们多年的编码经验,子类对象的方法总是更多。所以毫不犹豫地向下转型:

[Java面试基础]注解、反射详解_第32张图片

成了!现在我们能在父类中得到子类继承时传来的泛型的Class对象。接下来正式开始编写山寨JPA。

User

package myJPA;

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

	public User(String name, Integer age) {
		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;
	}
}

BaseDao

package myJPA;

import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.jdbc.core.JdbcTemplate;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;

public class BaseDao<T> {

	private static BasicDataSource datasource = new BasicDataSource();

	//静态代码块,设置连接数据库的参数
	static{
		datasource.setDriverClassName("com.mysql.jdbc.Driver");
		datasource.setUrl("jdbc:mysql://localhost:3306/test");
		datasource.setUsername("root");
		datasource.setPassword("123456");
	}

	//得到jdbcTemplate
	private JdbcTemplate jdbcTemplate = new JdbcTemplate(datasource);
	//泛型参数的Class对象
	private Class<T> beanClass;

	public BaseDao() {
		/*this指代子类
           通过子类得到子类传给父类的泛型Class对象,假设是User.class
         */
		beanClass = (Class) ((ParameterizedType) this.getClass()
				.getGenericSuperclass())
				.getActualTypeArguments()[0];
	}

	public void add(T bean) {
		//得到User对象的所有字段
		Field[] declaredFields = beanClass.getDeclaredFields();

		//拼接sql语句,表名直接用POJO的类名
                //所以创建表时,请注意写成User,而不是t_user
		String sql = "insert into "
				+ beanClass.getSimpleName() + " values(";
		for (int i = 0; i < declaredFields.length; i++) {
			sql += "?";
			if (i < declaredFields.length - 1) {
				sql += ",";
			}
		}
		sql += ")";

		//获得bean字段的值(要插入的记录)
		ArrayList<Object> paramList = new ArrayList<>();
		for (int i = 0; i < declaredFields.length; i++) {
			try {
				declaredFields[i].setAccessible(true);
				Object o = declaredFields[i].get(bean);
				paramList.add(o);
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			}
		}
		int size = paramList.size();
		Object[] params = paramList.toArray(new Object[size]);

		//传入sql语句模板和模板所需的参数,插入User
		int num = jdbcTemplate.update(sql, params);
		System.out.println(num);
	}
}

UserDao

package myJPA;

public class UserDao extends BaseDao<User> {
	@Override
	public void add(User bean) {
		super.add(bean);
	}

测试类

package myJPA;

public class TestUserDao {
	public static void main(String[] args) {
		UserDao userDao = new UserDao();
		User user = new User("hst", 20);
		userDao.add(user);
	}
}

[Java面试基础]注解、反射详解_第33张图片

[Java面试基础]注解、反射详解_第34张图片

桥多麻袋!这个和JPA有半毛钱关系啊!上一篇的注解都没用上!!

不错,细心的朋友肯定已经发现,我的代码实现虽然不够完美,但是最让人蛋疼的还是:要求数据库表名和POJO的类名一致,不能忍…

于是,我决定抄袭一下JPA的思路,给我们的User类加一个Table注解,用来告诉程序这个POJO和数据库哪张表对应:

@Table注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Table {
	String value();
}

新的User类(类名加了@Table注解)

package myJPA;

@Table("t_user")
public class User {
	private String name;
	private Integer age;

	public User(String name, Integer age) {
		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;
	}

}
 

新的测试类

package myJPA;

import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.jdbc.core.JdbcTemplate;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;

public class BaseDao<T> {

	private static BasicDataSource datasource = new BasicDataSource();

	//静态代码块,设置连接数据库的参数
	static{
		datasource.setDriverClassName("com.mysql.jdbc.Driver");
		datasource.setUrl("jdbc:mysql://localhost:3306/test");
		datasource.setUsername("root");
		datasource.setPassword("123456");
	}

	//得到jdbcTemplate
	private JdbcTemplate jdbcTemplate = new JdbcTemplate(datasource);
	//泛型参数的Class对象
	private Class<T> beanClass;

	public BaseDao() {
		//得到泛型参数的Class对象,假设是User.class
		beanClass = (Class) ((ParameterizedType) this.getClass()
				.getGenericSuperclass())
				.getActualTypeArguments()[0];
	}

	public void add(T bean) {
		//得到User对象的所有字段
		Field[] declaredFields = beanClass.getDeclaredFields();

		//拼接sql语句,【表名从User类Table注解中获取】
		String sql = "insert into "
				+ beanClass.getAnnotation(Table.class).value() 
				+ " values(";
		for (int i = 0; i < declaredFields.length; i++) {
			sql += "?";
			if (i < declaredFields.length - 1) {
				sql += ",";
			}
		}
		sql += ")";

		//获得bean字段的值(要插入的记录)
		ArrayList<Object> paramList = new ArrayList<>();
		for (int i = 0; i < declaredFields.length; i++) {
			try {
				declaredFields[i].setAccessible(true);
				Object o = declaredFields[i].get(bean);
				paramList.add(o);
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			}
		}
		int size = paramList.size();
		Object[] params = paramList.toArray(new Object[size]);

		//传入sql语句模板和模板所需的参数,插入User
		int num = jdbcTemplate.update(sql, params);
		System.out.println(num);
	}
}

这下真的是山寨JPA了~

注解的使用场景

我相信博文讲到这里大家都很熟悉了注解,但是有不少同学肯定会问,注解到底有什么用呢?

对啊注解到底有什么用?

我们不妨将目光放到 Java 官方文档上来。

文章开始的时候,我用标签来类比注解。但标签比喻只是我的手段,而不是目的。为的是让大家在初次学习注解时能够不被那些抽象的新概念搞懵。既然现在,我们已经对注解有所了解,我们不妨再仔细阅读官方最严谨的文档。

注解是一系列元数据,它提供数据用来解释程序代码,但是注解并非是所解释的代码本身的一部分。注解对于代码的运行效果没有直接影响。

注解有许多用处,主要如下:
- 提供信息给编译器: 编译器可以利用注解来探测错误和警告信息
- 编译阶段时的处理: 软件工具可以用来利用注解信息来生成代码、Html文档或者做其它相应处理。
- 运行时的处理: 某些注解可以在程序运行的时候接受代码的提取

当开发者使用了Annotation 修饰了类、方法、Field 等成员之后,这些 Annotation 不会自己生效,必须由开发者提供相应的代码来提取并处理 Annotation 信息。这些处理提取和处理 Annotation 的代码统称为 APT(Annotation Processing Tool)。

现在,我们可以给自己答案了,注解有什么用?给谁用?给 编译器或者 APT 用的。

反射

关于反射的使用例子,可以看之前的一篇博客:Java反射的使用入门详解

反射机制概述

静态语言 VS 动态语言

  • 动态语言
    • 是一类在运行时可以改变其结构的语言:例如新的函数、对象,已有函数可以被删除或是其他结构上的变化。通俗点说就是在运行时代码可以根据某些条件改变自身结构。
    • 主要动态语言:Object-C、C#、JavaScript、PHP、Python等
  • 静态语言
    • 与动态语言相对应,运行时结构不可变得语言,如Java、C、C++
    • Java不是动态语言,但Java可以称之为准动态语言。即是说Java具有一定的动态性,我们可以利用反射机制获得类似动态语言的特性。该动态性让编程的时候更加灵活~

反射

  • 反射是Java被视为动态语言的关键,反射机制允许程序在执行期间借助于Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
  • 加载完类后,在堆内存的方法去就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了完整的结构信息。我们可以通过这个对象看到这个类的结构。这个对象就像镜子,透过镜子看到类的结构,所以我们形象称之为:反射。

关于反射的具体使用案例,详情请看我之前写过的一篇博客。

Java反射机制提供的功能

  • 运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象实例
  • 在运行是判断任意一个类所具有的成员变量和方法
  • 在运行是获取泛型信息
  • 在运行时处理注解
  • 在运行时调用任意一个对象的成员变量和方法
  • 生产动态代理
  • 。。。

一般开发中常用两个:

  • 创建实例
  • 反射调用方法

反射的优点缺点

  • 优点:
    • 可以实现动态创建对象和编译,体现出很大的灵活性
  • 缺点
    • 对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且满足我们的要求。这类操作总是慢于直接执行相同的操作。

类的加载与ClassLoader

复习一波类的加载过程:

  1. 加载loading : 根据查找路径找到相应的.class文件读入内存,并为之创建一个Java.lang.Class对象,此过程由类加载器完成。
  2. 验证Vertify:检查加载的class文件正确性,是否符合JVM规范
  3. 准备Prepare:给类中静态变量分配内存空间
    • 为类变量分配内存并且设置该类变量的默认初值——空值
    • 这里不包括分配内存给用final修饰的static变量,因为final在编译时就分配了
    • 这里不会为实例变量分配初始化,类变量会分配在方法区中,实例变量时会随着对象一起分配在队中
  4. 解析resovle:vm讲常量池中符号引用(常量名)替换成直接引用(地址)的过程。
  5. 初始化:对静态变量和静态代码块执行初始化工作。
    • 执行类构造器()方法的过程。类构造器()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的预计合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
    • 当初始化一个类的时候,如果发现父类还没有初始化,则需要先触发器父类的初始化。
    • 虚拟机会保证一个类的()方法在多线程环境中被正常同步。

现在来看看JVM是如何构建一个实例的

[Java面试基础]注解、反射详解_第35张图片

首先看到,1、加载类,包括ClassLoader加载所需创建实例的A类的.class文件到缓存。

先来看看类加载器核心工作代码:

核心方法只有loadClass(),告诉它需要加载的类名,它会帮你加载:

   protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查是否已经加载该类
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 如果尚未加载,则遵循父优先的等级加载机制(所谓双亲委派机制)
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 模板方法模式:如果还是没有加载成功,调用findClass()
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    // 子类应该重写该方法
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

加载.class文件大致可以分为3个步骤:

  1. 检查是否已经加载,有就直接返回,避免重复加载

  2. 当前缓存中确实没有该类,那么遵循父优先加载机制,加载.class文件

    复习一波双亲委派机制:

    • 如果一个类加载器收到了类加载请求,其并不会自己先去加载,而是委托给父类加载器加载
    • 如果父类加载器还有父类,则会进一步向上委托
    • 如果父类加载器能完成加载任务,就成功返回,若不行才交给子类尝试加载。
  3. 上面两步都失败了,调用findClass()方法加载

需要注意的是,ClassLoader类本身是抽象类,而抽象类是无法通过new创建对象的。所以它的findClass()方法写的很随意,直接抛了异常,反正你无法通过ClassLoader对象调用。也就是说,父类ClassLoader中的findClass()方法根本不会去加载.class文件。

正确的做法是,子类重写覆盖findClass(),在里面写自定义的加载逻辑。比如:

@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
	try {
		/*自己另外写一个getClassData()
                  通过IO流从指定位置读取xxx.class文件得到字节数组*/
		byte[] datas = getClassData(name);
		if(datas == null) {
			throw new ClassNotFoundException("类没有找到:" + name);
		}
		//调用类加载器本身的defineClass()方法,由字节码得到Class对象
		return defineClass(name, datas, 0, datas.length);
	} catch (IOException e) {
		e.printStackTrace();
		throw new ClassNotFoundException("类找不到:" + name);
	}
}

defineClass()是ClassLoader定义的方法,目的是根据.class文件的字节数组byte[] b造出一个对应的Class对象。我们无法得知具体是如何实现的,因为最终它会调用一个native方法:

反正,目前我们关于类加载只需知道以下信息:

[Java面试基础]注解、反射详解_第36张图片

可以看到,整个.class文件最终都成为字节数组byte[] b,里面的构造器、方法等各个“组件”,其实也是字节。

理解Class类并获取Class实例

Class类

对象照镜子后可以得到的信息:某个类属性、方法、构造器、某个类到底实现了哪些接口。对于每个类而言,JRE都为其保留一个不变的Class类型的对象。那么来了解一下Class类

  • Class 本身也是一个类
  • Class对象只能由系统建立对象
  • 一个加载的类在JVM中只会有一个Class实例
  • 一个Class对象对应的是一个加载到JVM中的一个.class文件(Class类对象和.class文件的关系)
  • 每个类的实例都会记得自己是由那个Class实例所产生
  • 。通过Class可以完整地得到一个类中的所有被加载的结构
  • Class类是Reflection的根源,针对任何想动态加载、运行的类、唯有先获得相应的Class对象

如何获取Class类的实例

  1. 若已知具体的类,通过类的class属性获取,该方法安全可靠,性能最高

    Class clazz = Person.class;
    
  2. 已知某个类的实例,调用该实例的getClass();

    Class clazz = person.getClass();
    
  3. 已知一个类的全类名,且该类在类路径下,可通过Class的静态方法forNmme()

    Class clazz = Class.forName("com.lowfree.Person");
    
  4. 还可以通过子类利用class.getSuperclass();

哪些类型可以有Class对象?

  • class: 外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
  • interface
  • []: 数组
  • enum
  • annotation:注解 @Interface
  • primitive type:基本数据类型
  • void

理解Class类与反射API

现在来分析Class类源码以及提供给反射的API:

字段、方法、构造器对象:[Java面试基础]注解、反射详解_第37张图片

注解数据在这里插入图片描述

反省信息在这里插入图片描述

而且,针对字段、方法、构造器,因为信息量太大了,JDK还单独写了三个类,比如Method类:

[Java面试基础]注解、反射详解_第38张图片

也就是说,Class类准备了很多字段用来表示一个.class文件的信息,对于字段、方法、构造器等,为了更详细地描述这些重要信息,还写了三个类,每个类里面都有很详细的对应。

[Java面试基础]注解、反射详解_第39张图片

也就是说,原本UserController类中所有信息,都被**“解构”**后保存在Class类、Method类等的字段中。

大概了解完Class类的字段后,我们看看Class类的方法。

  • 构造器

[Java面试基础]注解、反射详解_第40张图片

可以发现,Class类的构造器是私有的,我们无法手动new一个Class对象,只能由JVM创建。JVM在构造Class对象时,需要传入一个类加载器,然后才有我们上面分析的一连串加载、创建过程。

  • Class.forName()方法

[Java面试基础]注解、反射详解_第41张图片

反正还是类加载器去搞呗。

  • newInstance()

[Java面试基础]注解、反射详解_第42张图片

也就是说,newInstance()底层就是调用无参构造对象的newInstance()。

所以,本质上Class对象要想创建实例,其实都是通过构造器对象。如果没有空参构造对象,就无法使用clazz.newInstance(),必须要获取其他有参的构造对象然后调用构造对象的newInstance()。

  • 创建类的对象:调用Class对象的newInstance()方法
    1. 类必须有一个无参的构造器
    2. 类的构造器的访问权限需要足够

思考:难道没有无参的构造器就不能创建对象了吗?只要在操作的时候明确的调用类中的构造器,并将参数传递进去之后,才可以实例化操作。

  • 步骤:
    1. 通过Class类的getDeclaredConstructor(Class … parameterTypes) 取得本类的指定形参类型的构造器
    2. 向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数
    3. 通过Constructor 实例化对象

调用指定的方法

通过反射,调用类中的方法,通过Method类完成。

  1. 通过Class类的getMethod(String name, Class … parameterTypes) 方法取得一个Method对象,并设置此方法操作时所需要的参数类型。

  2. 之后用Object invoke(Object obj, Object[] args)进行调用,并向方法中传递要设置的Obj对象的参数信息。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ijuFyEnS-1625639248621)(Java面试基础_注解反射.assets/image-20210707123858469.png)]

Object invoke(Object obj ,Object ... args)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yKWMLfU1-1625639248621)(Java面试基础_注解反射.assets/image-20210707124808361.png)]

tips:关闭检测:setAccessible(true)可以提升反射性能

最后,介绍两个反射调用方法的难点。

再此之前,先来理清楚Class、Field、Method、Constructor四个对象的关系:

[Java面试基础]注解、反射详解_第43张图片

Field、Method、Constructor对象内部有对字段、方法、构造器更详细的描述:

[Java面试基础]注解、反射详解_第44张图片

OK,理清关系后我们继续来看看反射调用方法时的两个难点。

  • 难点一:为什么根据Class对象获取Method时,需要传入方法名+参数的Class类型

[Java面试基础]注解、反射详解_第45张图片

为什么要传name和ParameterType?

因为.class文件中有多个方法,比如

[Java面试基础]注解、反射详解_第46张图片

所以必须传入name,以方法名区分哪个方法,得到对应的Method。

那参数parameterTypes为什么要用Class类型,我想和调用方法时一样直接传变量名不行吗,比如userName, age。

答案是:我们无法根据变量名区分方法

User getUser(String userName, int age);
User getUser(String mingzi, int nianling);

这不叫重载,这就是同一个方法。只能根据参数类型。

我知道,你还会问:变量名不行,那我能不能传String, int。

不好意思,这些都是基本类型和引用类型,类型不能用来传递。我们能传递的要么值,要么对象(引用)。而String.class, int.class是对象,且是Class对象。

实际上,调用Class对象的getMethod()方法时,内部会循环遍历所有Method,然后根据方法名和参数类型匹配唯一的Method返回。

[Java面试基础]注解、反射详解_第47张图片

难点二:调用method.invoke(obj, args);时为什么要传入一个目标对象?

上面分析过,.class文件通过IO被加载到内存后,JDK创造了至少四个对象:Class、Field、Method、Constructor,这些对象其实都是0101010的抽象表示。

以Method对象为例,它到底是什么,怎么来的?我们上面已经分析过,Method对象有好多字段,比如name(方法名),returnType(返回值类型)等。也就是说我们在.java文件中写的方法,被“解构”以后存入了Method对象中。所以对象本身是一个方法的映射,一个方法对应一个Method对象。

对象的本质就是用来存储数据的。而方法作为一种行为描述,是所有对象共有的,不属于某个对象独有。比如现有两个Person实例

Person p1 = new Person();
Person p2 = new Person();

对象 p1保存了"hst"和18,p2保存了"cxy"和20。但是不管是p1还是p2,都会有changeUser(),但是每个对象里面写一份太浪费。既然是共性行为,可以抽取出来,放在方法区共用。

但这又产生了一个棘手的问题,方法是共用的,JVM如何保证p1调用changeUser()时,changeUser()不会跑去把p2的数据改掉呢?

所以JVM设置了一种隐性机制,每次对象调用方法时,都会隐性传递当前调用该方法的对象参数,方法可以根据这个对象参数知道当前调用本方法的是哪个对象!

[Java面试基础]注解、反射详解_第48张图片

同样的,在反射调用方法时,本质还是希望方法处理数据,所以必须告诉它执行哪个对象的数据。

[Java面试基础]注解、反射详解_第49张图片

所以,把Method理解为方法执行指令吧,它更像是一个方法执行器,必须告诉它要执行的对象(数据)。

当然,如果是invoke一个静态方法,不需要传入具体的对象。因为静态方法并不能处理对象中保存的数据。

你可能感兴趣的:(Java基础,Java,注解,反射,面试)