文章目录
注解与反射
心得:注解与反射是真的真的非常重要,在框架中大量应用,其相互配合编写的**注解处理器**,功能异常强大。
理解反射个人觉得最重要的是理解Java类的加载过程,也就是笔记中二的4567点,知道底层怎么来的之后学习起来就很快了。
一、注解
1、基本概念
- 从JDK5.0开始有的
- Annotation不是程序本身,但可以对程序作出解释,和注解comment区别开。和注释不同,他可以被其它程序(如编译器)等读取
- 可以通过反射获得注解内容
- 基本格式:@注解名(参数)
- 作用范围:类、方法、变量、参数和包等
2、内置注解
内置注解一共有7个,可以根据包的位置分为两类
java.lang包下的注解
- @Override 检查方法是否为重写方法。
- @Deprecated 标记过时的方法
- @SuppressWarnings 抑制警告
java.lang.annotaion包下的注解
也称为元注解(meta-annotation),是作用在其它注解的注解
- @Target 说明被标记的注解是应用于何处的注解,即说明其它注解的作用范围。不加默认是所有都生效
- @Retention(英文原意:保留) 说明被标记的注解保留程度,是在代码SOURCE中,还是在CLASS文件中,或者运行时RUNTIME。不加默认为ClASS
- @Documented 说明被标记的注解是否可以包含是用户文档doc中
- @Inherited 说明被标记的注解如果应用在父类中,子类也可以使用,即具有继承性
Java7之后加的注解
- @SafeVarargs 7开始,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告
- @FunctionalInterface 8开始,标识一个匿名函数或函数式接口。
- @Repeatable 8开始,标识某注解可以在同一个声明上使用多次。
3、自定义注解
/**用@interface自定义注解,会自动继承java.lang.annotation.Annotation接口,
使其成为一个注解接口
用反射可知,注解本质是一个接口类型
**/
public @interface 注解名 {
/*
每一个方法实际上是声明了一个配置参数
方法的名称为参数的名称
返回类型是参数的类型,返回类型只能是 《基本类型、Class、String、enum以及Annotation,还有它们的数组》
*/
String[] values();//即类型 + 参数名 + ()
int age default 3;//可以用default设置默认值。经常用0和空字符串来默认定义。
//如果有参数,而参数又没有默认值,则在使用注解的时候,一定要传入参数
}
二、反射
1、反射概述
-
反射是一种计算机处理方式,是提供封装程序集、模块和类型的对象。有程序可以访问、检测和修改它本身状态或行为的这种能力。能提供封装程序集、类型的对象(程序集包含模块,而模块包含类型,类型又包含成员)。
简记:反射是分析类的一种能力
-
Java是运行时结构不可变的,是静态语言。
-
Java不是动态语言,但可以称为“准动态”语言,即Java有一定的动态性。可以通过反射获得类似动态语言的特性,使Java更加灵活。
-
是一种解释操作,会使程序性能降低
-
正常方式:引入需要的“包类”名称》通过new实例化》取得实例化对象
反射方式:已有实例化对象》得到Class对象》取到完整的“包类”名称
2、获得反射对象
所有类的实例对象,获得的类对象全都是同一个对象。
即一个类在内存中只有一个Class对象,是一个模板。
一个类被加载后,类的整个结构都会被封闭在Class对象中.
要得到父类类型,要通过子类的Class对象获得,不能直接由子类直接获得。
/**方法一
用Class对象的静态方法得到**/
Class.forName("完全限定名,如java.lang.String");
/**方法二
用实例化对象的getClass()方法**/
实例对象.getClass();//继承自Object
/**方法三 最推荐,安全可靠,性能高
直接从类名 + class获得**/
类名.class;//继承于Object
/**方法四
基本包装类型有TYPE属性**/
Integer.TYPE
3、类型的Class对象
Class c1 = User.class;//普通对象类型
Class c2 = Comparable.class;//接口类型
Class c3 = Target.class;//注解类型,本质是接口类型
Class c4 = RetentionPolicy.class;//枚举类型,本质是对象类型
Class c5 = int.class;//基本数据类型
Class c6 = Integer.class;//基本数据包装类型,本质是对象类型
Class c7 = void.class;//空类型
Class c8 = int[].class;//一维数组,本质是对象类型
Class c9 = int[][].class;//二维数组,本质是对象类型
//相同类型下,如果是同维的数组,则长度无关,Class是相同的;如果是不同维的,则Class是不同的
===============================================
输出如下:
class User
interface java.lang.Comparable
interface java.lang.annotation.Target
class java.lang.annotation.RetentionPolicy
int
class java.lang.Integer
void
class [I
class [[I
4、Java内存分析
- 栈:
- 存放基本数据类型(会包括基本类型的具体数值)
- 存放对象引用
- 堆:
- 存放new的对象和数组(本质是对象),即存放实体
- 可以被所有的线程共享,不会存放别的对象的引用
- 方法区:
- 是一个特殊堆(好像又说不是堆,具体以后分析)
- 包含了所有的Class和static变量
- 可以被所有的线程共享
5、类加载过程
推荐书籍:《深入理解Java虚拟机》
分为三个过程:
- 加载(Load):将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象
- 链接(Link):将Java类的二进制代码合并到JVM的运行状态之中的过程,分为三步
- 验证:确保加载的类信息符合JVM规范,没有安全方面的问题
- 准备:正式为类变量(static)分配内存,并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配 (跟实际代码相关,其它两个跟虚拟机相关)
- 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程
- 初始化(Initialize):JVM负责对类进行初始化
- 执行类构造器 clinit() 方法的过程。类构造器 clinit() 方法是由编译时期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)
- 当初始化一个类的时候,如果发现父类还没有进行初始化,则需要先触发其父类的初始化
- 虚拟机会保证一个类的 clinit() 方法在多线程环境中被正确加锁和同步
6、类初始化
类的主动引用:
一定会发生类的初始化
- 当虚拟机启动,先初始化main方法所在类
- new一个类的对象
- 调用类的静态成员(除了final常量)和静态方法
- 反射调用时。Class.forName会,但 类.class 不会,初始化被延迟到了对静态方法或者是非常数静态域进行首次引用时才执行。
- 当初始化一个类,如果其父类没有被初始化,则会先初始化它的父类
类的被动引用:
不会发生类的初始化
- 当访问一个静态域,只有真正声明这个域的类才会被初始化。如:当通过子类名引用父类的静态变量,不会慌到子类的初始化
- 通过数组定义类的引用。如User[] userList = new User[5]; 原因是,这只是声明了User类型的数组,并开辟了内存空间(在栈中),没有具体的值。
- 引用常量不会触发类的初始化(常量在链接阶段就存放调用类的常量池中了)
7、类加载器
基本概念
- 类加载器的作用是用来把类装载进内存的。
- 类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。
- 自底向上检查类是否已经被加载(双亲委派机制),自顶向下尝试加载类。
类加载器分类
一共有四种类型的类加载器,自下而上权限增大
- BootstrapClassLoader引导类加载器 用c++编写的,是JVM自带的类加载器,负责Java平台核心库,用来装载核心类库。该加载器无法直接获取。
- Extension ClassLoader扩展类加载器 负责jre/lib/ext目录下的jar包或-D java.ext.dirs指定目录下的jar包装入工作库
- System(Application) Classloader系统类加载器 负责Java -classpath 或 -D java.class.path 所指的目录下的类与jar包装入工作,是最常用的加载器
- 自定义类加载器
/**逐层获取类加载器:通过ClassLoader类的静态方法**/
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
ClassLoader extensionClassLoader = systemClassLoader.getParent();
ClassLoader bootStrapClassLoader = extensionClassLoader.getParent();
System.out.println(systemClassLoader);
System.out.println(extensionClassLoader);
System.out.println(bootStrapClassLoader);
/**输出如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@45ee12a7
null --说明是引导加载器加载的
**/
/***********************************************************************************/
/**得到具体类的类加载器:通过Class对象的getClassLoader()方法得到**/
System.out.println(User.class.getClassLoader());
System.out.println(Class.forName("java.lang.String").getClassLoader());
/**输出如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
null --说明是引导加载器加载的
**/
/***********************************************************************************/
/**得到系统可以加载类的路径**/
System.out.println(System.getProperty("java.class.path"));
/**输出如下:
/*C:\Program Files\Java\jdk1.8.0_181\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\deploy.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\access-bridge-64.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\cldrdata.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\dnsns.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jaccess.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\jfxrt.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\localedata.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\mysql-connector-java-5.1.40-bin.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\nashorn.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\servlet-api.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunec.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunjce_provider.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunmscapi.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\sunpkcs11.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\ext\zipfs.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\javaws.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\jfxswt.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\management-agent.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\plugin.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_181\jre\lib\rt.jar;
C:\Users\aj\IdeaProjects\JavaSE\out\production\JavaSE;
C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.1\lib\idea_rt.jar
**/
8、获得类的运行结构
Class c1 = Class.forName(“com.hao.User”);
获得类名
c1.getName();//得到完全限定名
c1.getSimpleName();//只得到类名
获得字段
Field[] fields = c1.getDeclaredFields();//得到所有声明的字段
Field[] fields = c1.getFields();//得到public修饰的字段
c1.getDeclaredField("name");//指定名字
c1.getField("name");
获得构造器
c1.getConstructors();//得到所有public构造器
c1.getDeclaredConstructors();//得到所有构造器
//传入参数是因为有重载的存在
c1.getConstructor(String.class, int.class);//得到指定的public构造器
c1.getDeclaredConstructor(String.class, int.class);//得到指定的构造器
获得方法
c1.getMethods();//得到本类及父类,父父类直到Object的所有public方法
c1.getDecleardMethods();//得到本类所有方法
//传入参数为方法名(构造方法因为与类名一样,所以不用该参数)
//还有方法的形参,因为重载
//如果没有参数,则传入null
c1.getMethod("setName", String.class);
c1.getDecleardMethod("setName", String.class);
除了获得所有公有方法的操作外,其它都不涉及到父类。
9、反射调用
创建对象
/**方法一:
(1)类必须有一个无参数构造器
(2)类的构造器的权限要足够
否则,要用构造器反射来创建对象
**/
c1.newInstance();
/**方法二**/
Constructor constructor = c1.getDeclaredConstructor(String.class, int.class);
User user = constructor.newInstance("你好", 123);
/**constructor.setAccessible(true);如果是私有的**/
操作字段
Field name = c1.getDeclaredField("name");
name.setAccessible(true);//如果是私有的
//设置方法,参数是 具体对象 + 设置的值
name.set(user, "我是");
//访问器方法,返回类型是Object类型,可以强转
name.get(user);
操作方法
Method getName = c1.getDeclaredMethod("setName", String.class);
setName.invoke(user, "名字");//返回类型为Object,可以强制转换为方法的实际类型
权限访问
- Method、Field和Constructor对象都有setAccessible()方法
- 作用是启动和禁用安全检查的开关,true为允许执行访问
- 设置为true可以提高反射的效率,如果代码要频繁使用。
10、操作泛型
三、双剑合一
利用反射,可以编写**注解处理器。**
基本方法
所有注解能标注的对象,都有相应的得到注解的方法。这里只举例类获得对象的方法。
c1.getAnnotation(MyValidation.class);
c1.getDeclaredAnnotation(MyValidation.class);
c1.getAnnotations();
c1.getDeclaredAnnotations();
//判断是否存在这样一个注解
field.isAnnotationPresent(FieleAnnotation.class);
实例一:表单验证
对前端送来的用户对象进行验证(只是示例用法)
自定义注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface MyValidation {
int maxSize();
int minSize();
String tips() default "格式不正确";
}
加了注解的用户类
class User {
@MyValidation(maxSize = 10, minSize = 6, tips = "账号名要在6到10位之间")
private String name;
@MyValidation(maxSize = 8, minSize = 8, tips = "密码只能是8位")
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
测试
public class TestMain3{
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
User user = new User();
user.setName("beautiful");
user.setPassword("12345");
//假设前端送来这么一个User对象
Class c1 = user.getClass();
Field[] fields = c1.getDeclaredFields();//拿到所有字段
for (Field field : fields) {
field.setAccessible(true);
MyValidation myValidation = field.getAnnotation(MyValidation.class);//拿到所有字段的MyValidation注解
String value = (String)field.get(user);//拿到值
if (value.length() > myValidation.maxSize() || value.length() < myValidation.minSize()) {
//判断长度
System.out.println(myValidation.tips());//给出提示信息
}
}
}
}
结果
密码只能是8位
Process finished with exit code 0
实例二:ORM
ORM: Object relationship Mapping 对象关系映射,即对象与数据库表记录进行映射
假设数据库有一个table_user表,用于存放用户信息,其中有栏目name和password,现在可以通过程序自动把用户对象信息填入对应表对应栏目中。
自定义注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface TableAnnotaion {
String value();//值代表数据库的表名
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface FieleAnnotation {
String column();//字段要对应的数据库表中的栏目
String type();//栏目的类型
}
用了注解的用户类
@TableAnnotaion("table_user")
class User {
@FieleAnnotation(column = "name", type = "varchar")
private String name;
@FieleAnnotation(column = "password", type = "varchar")
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
测试
public class TestMain3 {
public static void main(String[] args) throws IllegalAccessException {
User user = new User();
user.setName("beautiful");
user.setPassword("12345");
//假设前端送来这么一个User对象,要将其写入数据库
Class<? extends User> c1 = user.getClass();
TableAnnotaion tableAnnotaion = c1.getAnnotation(TableAnnotaion.class);
String tableName = tableAnnotaion.value();//从类注解中拿到类对应的表名
Field[] fields = c1.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String fieldValue = (String)field.get(user);//拿到字段值
FieleAnnotation fieldAnnotation = field.getAnnotation(FieleAnnotation.class);
String column = fieldAnnotation.column();//从字段注解中拿到数据库栏目
String type = fieldAnnotation.type();//从字段注解中拿到数据库栏目类型
System.out.println(c1.getSimpleName() + "对象的" + field.getName() + "字段,值为" + fieldValue + "要写入表" + tableName + "中的" + column + "栏目" + "类型为" + type );
}
}
}
输出
User对象的name字段,值为beautiful要写入表table_user中的name栏目类型为varchar
User对象的password字段,值为12345要写入表table_user中的password栏目类型为varchar