目录
***:Class.forName()和ClassLoader.loadClass()有什么区别?
***:程序判断题(forName和loadClass执行,与static代码块的执行关系)
前言
带着问题学java系列博文之java基础篇。从问题出发,学习java知识。
Java的反射机制
反射机制的概念
Java中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。
***:动态语言
动态语言,是指程序在运行时可以改变其结构:新的函数可以引进,已有的函数可以被删除等结构上的变化。比如常见的 JavaScript 就是动态语言,除此之外 Ruby,Python 等也属于动态语言,而 C、 C++则不属于动态语言。 从反射角度说 JAVA 属于半动态语言。
***:什么场合需要用到反射?
java程序中对象在运行时总是会出现两种类型:编译时类型和运行时类型。编译时类型由声明对象的类型来决定,运行时类型则由实际赋值给对象的类型决定。例如:Person p = new Student(); 其编译时类型为Person,运行时类型为Student。很显然,从Person类型是无法获取Student类型的具体方法。此外,有时候程序在运行时还可能接收到外部传入的对象,该对象的编译时类型为 Object,但是程序又需要调用该对象的运行时类型的方法。为了解决这些问题, 程序需要在运行时发现对象和类的真实信息。然而,如果编译时根本无法预知该对象和类属于哪些类,程序只能依靠运行时信息来发现该对象和类的真实信息,此时就必须使用到反射了。
编程中实际使用反射的案例主要有:数据库连接时根据全类名加载驱动;动态代理时通过反射获取被代理对象的方法;注解处理器获取注解的内容;诸多框架(spring的ioc)等
Java反射API
class类:反射的核心类,可以获取类的属性、方法等信息;
Field类:表示类的成员变量,可以用来获取和设置类之中的属性值;
Method类:表示类的方法,可以用来获取类中的方法信息或者执行方法;
Constructor类:表示类的构造方法,可以用来初始化对象;
getDeclaredFields():Class对象的方法,获取该类的所有属性(包含私有属性),注意配合setAccessible()开放安全限制;
getFields():Class对象的方法,获取该类的属性(仅public修饰的属性);
getDeclaredMethods():Class对象的方法,获取该类的所有方法(包含私有方法);
getMethods():Class对象的方法,获取该类的方法(仅public修饰的方法);
getConstructor(可选参数):Class对象的方法,获取该类的指定参数的构造方法;
反射的步骤
- 获取想要操作的类的 Class 对象,他是反射的核心,通过 Class 对象我们可以任意调用类的方法;
- 调用 Class 类中的方法(反射的使用阶段);
- 使用反射 API 来操作这些信息
***:获取Class对象的3种方法
在讲三种方法之前,我们先了解一下java的三大阶段:
- Source源代码阶段:javac编辑类文件为字节码, 其中成员变量是一类,构造方法一类,成员方法一类;
- Class类对象阶段:进入内存,封装成class对象:成员变量Field【】;构造方法 Constructor【】;成员方法 Method【】;
- Runtime运行时阶段:解析为具体的对象实例;
其中从阶段1到阶段2就是反射的过程。获取Class对象的三种方法也是分别与这三个阶段对应的:
//获取class对象的三种方式
private static void testGetClass() {
Class<Person> personClass = null;
//Class.forName() 和 ClassLoader.loadClass() 的区别:
// 1.前者的过程是:加载,连接,初始化 2.后者的过程是加载
// 因此前者会进入类对象阶段,执行初始化类的静态变量和静态代码块;后者则不会
try {
//第一种方法,对应第一个阶段
personClass = (Class<Person>) Class.forName("com.zst.javabasedemo.collection.Person");
System.out.println(personClass);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try {
Class<?> aClass = ClassLoader.getSystemClassLoader().loadClass("com.zst.javabasedemo.collection.Person");
System.out.println("classloader:"+aClass);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
//第二种方法,对应第二个阶段
Class personClass1 = Person.class;
System.out.println(personClass1);
//第三种方法,对应第三阶段
Person person = new Person("zhangsan",28);
Class personClass2 = person.getClass();
System.out.println(personClass2);
//注意,通过对比,三种方式获取的对象是相同的,说明同一个字节码文件在一次程序运行中,加载且仅加载一次,内存中仅保存一份该class对象
System.out.println("class1 == class2 == class3 ? "+(personClass == personClass1 && personClass == personClass2));
}
- 第一种方法:Class.forName(全类名)或者ClassLoader.loadClass(全类名)
这种方法对应的是三大阶段的第一个阶段,此时是类文件,需要进行加载类文件,获取class对象。这种方法是最安全、性能最好的,推荐使用。
- 第二种方法:Person.class
这种方法对应的是第二个阶段,此时jvm已经加载了类文件进内存,已持有Person类对象,所以此时只需要直接获取类对象的class属性即可。
- 第三种方法:person.getClass()
这种方法对应的是第三个阶段,此时jvm已经解析初始化完成,创建了具体的对象实例,所以只需要通过对象实例的getClass方法获取该对象实例的class属性即可。
如代码范例中的注释,三种方法获取到的class对象是完全相同的,这也说明了同一份字节码文件在一次程序运行过程中,jvm加载且仅加载一次,内存中仅有一份该class对象。
***:Class.forName()和ClassLoader.loadClass()有什么区别?
Class.forName()的执行会直接走完三大阶段,首先加载字节码文件,然后连接,再初始化(加载->连接->初始化 是java的类加载机制,详见《Java基础篇--JVM》);而初始化阶段,jvm会为该类对象分配内存空间,给变量赋默认值,执行静态代码块,类对象进入内存;
ClassLoader.loadClass()的执行只会加载字节码文件,最多由于参数resolve,再执行一步连接,没有类对象进入内存;
Class.forName(String,boolean,ClassLoader),可以通过参数指定类加载器;而ClassLoader.loadClass()就是由当前执行方法的类加载器加载。
***:程序判断题(forName和loadClass执行,与static代码块的执行关系)
public class Person {
public static String name;
public int age;
static {
name = "default";
System.out.println("static is running");
}
}
public void testForName() throws Exception{
Class<Person> person = (Class<Person>) Class.forName("com.zst.javabasedemo.test.Person");
System.out.println("----------------------");
Field name = person.getField("name");
System.out.println(name.get(person));
}
public void testLoadClass() throws Exception {
Class<Person> person2 = (Class<Person>) ClassLoader.getSystemClassLoader().loadClass("com.zst.javabasedemo.test.Person");
System.out.println("----------------------");
Field nameField = person2.getField("name");
System.out.println(nameField.get(person2));
}
testForName()方法执行的结果是什么?testLoadClass()方法执行的结果是什么?
出现上图的现象,就是因为forName()方法会直接走过加载、连接和初始化,静态代码块会执行,所以先输出static is running;
而loadClass()方法仅仅是加载(最多到连接),静态代码块不会执行,只有等到nameField.get(person2)时,才会执行初始化,所以先输出分隔线。
***:反射创建对象的两种方法
- Class.newInstance()
获得Class对象后,直接调用Class对象的静态方法newInstance()创建对象实例。注意,该方法可以正确执行的前提是Class对象对应的类有默认的空构造器。
- Constructor.newInstance()
通过Class对象获取对应类的构造器Constructor,然后再调用Constructor的静态方法newInstance()创建对象实例。这种方法的好处是可以选择带参数的构造器,直接在创建对象实例时给对应属性赋初值。注意,这种方法获取构造器时,要确保参数类型与类中的带参构造器完全一致,否则将抛出NoSuchMethodException异常;且在执行newInstance()方法时,也要传入相同类型的初值。
java注解
注解的概念
Annotation(注解)是 Java 提供的一种对程序中元素关联信息和元数据(metadata)的途径和方法。 Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的 Annotation对象,然后通过该 Annotation 对象来获取注解中的元数据信息。
四种标准元注解
元注解的作用就是负责注解其他注解。Java5定义了4个标准的元注解类型,被用来提供对其它注解的类型作说明。
- @Target:指定注解的作用范围
可设置的范围由枚举类ElementType限定,主要有TYPE(限定注解可用于类、接口、枚举、Annotation),FIELD(限定注解可用于属性),METHOD(限定注解可用于方法),PARAMETER(限定注解可用于参数),CONSTRUCTOR(限定注解可用于构造器),PACKAGE(限定注解可用于包)等
- @Retention:指定注解的有效阶段
可设置的阶段由枚举类RetentionPolicy限定,主要有SOURCE(源代码阶段),CLASS(class类对象阶段)和RUNTIME(运行时阶段),该注解可以描述注解的生命周期,表明注解生命周期
- @Documented:描述javadoc
该注解表明javadoc生成api文档的时候将保留注解信息
- @Inherited:表明注解可以被继承
该注解表明子类可以继承父类的这个注解,通过反射同样可以拿到注解的元数据
***:如何实现自定义注解?
/**
* 注解的本质是一个继承了java.lang.annotation.Annotation的接口
* 它的抽象方法就是定义属性,支持8种基本类型,枚举,注解,(以上类型)数组
* 如果只有一个方法,即默认一个属性,此时可以不用再写名称
* 也可以指定方法默认值,则可以在注解使用时不需要都赋值
* 注解中有一个重要的属性value,当只配置一个值时,可以默认不写名称“value”
*/
@Target(ElementType.METHOD) //元注解 ,标明注解的使用范围是方法
@Retention(RetentionPolicy.RUNTIME) //元注解,标明注解保留的时间范围是运行时(对应java3个阶段,source,class,runtime)
@Documented //元注解,标注javadoc api文档将保留注解信息
@Inherited //元注解,具有继承性(父类使用了该注解,子类将继承该注解,通过反射可以拿到)
public @interface MyAnno {
String name();
int age();
String[] sports() default {"足球","篮球"};
}
如上代码,实现了一个自定义的注解MyAnno。其实自定义注解很简单,首先注解的本质是一个接口(继承了Annotation接口),我们只需要用@interface 注解修饰接口类即可,无需单独继承Annotation接口;然后就是利用四个元注解,限定自定义注解的作用范围、有效阶段、是否保留进api文档以及是否可以继承;最后就是根据需要,自定义抽象方法,抽象方法就是定义元数据。
那定义了注解,如何让注解发挥实际的作用呢?这时就需要注解处理器,自定义注解就需要我们自己实现自定义注解处理器。
***:(实战)模拟spring框架,简单实现IOC
1.首先自定义两个注解:Autowired和Bean,用于属性自动注入和自动初始化对象实例,并存入bean容器
/**
* 自定义注入注解
* 用于属性自动注入
* 可被继承
* 默认元数据name=“”
* 当给name赋值时,则根据name自动注入;否则按照属性的名称(名称限定:属性的类型名、首字母小写)自动注入
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Autowired {
String name() default "";
}
/**
* 自定义bean初始化注解
* 用于自动初始化bean实例
* 该注解用于类,表示该类被bean工厂接管,会自动创建单例的实例
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Bean {
}
2.然后实现自定义注解处理器,用于处理自定义的两个注解。主要是两个方法:initBean和loadAnno。initBean用于bean实例化,并存入bean容器;loadAnno用于自动注入属性。
/**
* 自定义注解处理器,用于处理自定义的注解Autowired和Bean
*/
public class MyAnnoLoad {
//bean容器
private static ConcurrentHashMap<String,Object> beanMap = new ConcurrentHashMap<>();
/**
* 属性注入
* @param field 属性对象
* @param bean 要注入的对象实例
* @param name 注入的属性在bean容器中的名称
*/
public static void loadAnno(Field field,Object bean,String name){
try {
//开放权限
field.setAccessible(true);
//如果指定bean名称,则按照指定的名称去注入,未指定则按照属性名来注入(所以要求属性名是类型名首字母小写)
if (null == name || name.trim().length() == 0 ) {
//容器中未找到,说明没有这个对象实例,先初始化一个
if (!beanMap.containsKey(field.getName())) {
initBean(field.getType(), field.getName());
}
//将对象实例注入到属性
field.set(bean, beanMap.get(field.getName()));
} else {
if (!beanMap.containsKey(name)){
initBean(field.getType(),name);
}
System.out.println("Autowired注解指定了name:"+name);
field.set(bean,beanMap.get(name));
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 初始化bean
* @param clazz Class对象
* @param name bean在容器中的名称
* @return
*/
public static Object initBean(Class<?> clazz,String name){
Object bean = null;
try {
//仅对使用Bean注解的类进行初始化对象
if (clazz.isAnnotationPresent(Bean.class)){
bean = clazz.newInstance();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//遍历该类的成员属性,如果发现存在Autowired注解的属性,则先注入该属性
if (field.isAnnotationPresent(Autowired.class)){
//得到注解类,获取注解的元数据
Autowired autowired = field.getAnnotation(Autowired.class);
loadAnno(field,bean,autowired.name());
}
}
}
//未指定name参数,则默认类型名首字母小写作为key,保存进beanMap
if (null == name || name.trim().length() == 0){
name = toLowerCaseFirstOne(clazz.getSimpleName());
}
beanMap.put(name,bean);
System.out.println("key:"+name+",bean:"+bean);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return bean;
}
/**
* 对string字串的首字母进行小写转换
* @param s
* @return
*/
public static String toLowerCaseFirstOne(String s){
if(Character.isLowerCase(s.charAt(0)))
return s;
else
return (new StringBuilder()).append(Character.toLowerCase(s.charAt(0))).append(s.substring(1)).toString();
}
}
3.使用注解实现业务逻辑,这里完全模拟spring mvc,从mapper到service,再到controller,首先用bean注解,让框架自动初始化bean,然后使用Autowired注解实现属性的注入。代码范例简单的实现了spring的IOC,且支持根据自定义名称和默认类名两种方式注入。
/**
* 模拟数据库操作类mapper
*/
@Bean
public class UserMapper {
public List<String> getAll(){
List<String> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
users.add("user"+i);
}
return users;
}
}
/**
* 模拟service
* 利用注入的mapper实现具体的业务逻辑
*/
@Bean
public class MyService {
//使用自定义注解,注入属性
@Autowired
private UserMapper userMapper;
public List<String> findAll(){
return userMapper.getAll();
}
}
/**
* 模拟controller
* 利用注入的service,实现具体的业务逻辑
*/
@Bean
public class MyController {
//使用自定义注解注入属性,且指定了name
@Autowired(name = "service")
private MyService myService;
/**
* 模拟服务启动,首先加载bean实例,注入属性
* 再模拟业务方法调用
* @param args
*/
public static void main(String[] args) {
MyController controller = (MyController) MyAnnoLoad.initBean(MyController.class,null);
controller.findAll();
}
public void findAll(){
List<String> userList = myService.findAll();
for (String name : userList) {
System.out.println(name);
}
System.out.println("执行成功");
}
}
执行结果如下图:
可以看到框架自动初始化了三个对象实例,且按照键值对保存进了bean容器(ConcurrentHashMap);由于在contronller中指定了名称来注入,所以结果中也有所体现,存入容器时,使用的key就是指定的名称,否则就是该类型名的首字母小写作为key。然后通过autowired注解依次注入属性,所以controller调用service,service调用userMapper都是可以的,成功执行了userMapper.getAll()。
以上系个人理解,如果存在错误,欢迎大家指正。原创不易,转载请注明出处!