RTTI(Run-Time Type Identification)运行时类型识别
假设有一个Animal
类,那么在声明时,可以将一个Animal
的实例对象赋值给Object
,因为所有类都继承了Object
Object obj = new Animal();
// 假设Animal有这么一个实例方法,这么编译器会报错
obj.run();
复制代码
此时类型会丢失,若Animal
类中有一个run
方法,是无法使用obj.run()
去调用的
因为Java
是一个强类型语言,声明了obj
是Object
类型的,因此没有run
方法
在任何时刻,任何一个对象都清楚的知道自己是什么类型
只需要调用Object.getClass
方法,将一个String实例
赋值给Object
Object str = new String("sdkjflsdjfksd");
Class<?> aClass = str.getClass();
System.out.println(aClass);
复制代码
此时会打印出一个具体类型的名字class java.lang.String
,由此拿到了类的真实信息
类型与Class与JVM的关系
在编写程序时,写的是xx.java
文件,称为源代码,想要在JVM
中执行,必须先经过一个编译过程(Compile)
变成xx.class
文件,也叫字节码文件,这是Java
跨平台的基石,JVM
可以跑xx.class
先看一张JVM(Java Virtual Mechine )
的示意图,所有的对象都是在堆上分配的,假设要new
一个Animal
类,那么在堆上就会多一个Animal
的对象。那么Animal
是如何被创建出来的?因此我们需要一份类的说明书,所有需要new
出来的对象都根据这份说明书去装配。
如果Animal
类中有一个静态方法或者静态属性(static
),那么这个静态属性或方法是归属于这份类说明书的,所以可以Animal.[静态方法或静态属性名]
去使用,而通过new
出来的对象不会拥有Class
的静态属性和方法
instanceof
了解了这些知识点后,就可以理解关键字instance
的原理了。因为对象任何时刻都获取到对应真实的类说明书,先拿到instanceof
关键字前的实例类型是由什么说明书装配的,再拿这份说明去对比instanceof
关键字后面的说明书是不是同一份。
System.out.println(("test" instanceof String));
// true
复制代码
强制类型转换
这里强制把一个String
转成Integer
类型,因为这个行为本身是不安全的,所以编译器允许,但是运行时会报错,因为obj
知道自己是个String
,强行转换时会被JVM给拒绝掉操作,并抛出一个Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
异常
Object obj = new String("abc");
Integer i = (Integer) obj;
// 执行就报错
复制代码
Class对象是怎么来的?
存放Class说明书的地方,在Java7
之前叫永久代,Java8
之后叫做元空间
说明书从哪里来,这个问题包含了内置类的和我们手动编写的类
一张Class对象的生命周期图奉上
可以了解到一个class
对象是怎么被加载到JVM
里的 答案是被在第一次被使用的时候加载
如new Animal()
的时候:(Animal
可能没有写extends
,但会隐式的存在Animal extends Object
)
在加载Animal
的Class
之前,由于它的父类是Object
,所以得先加载Object
的Class
, 如果继承关系多,则依次类推,越上层的Class
越早被加载,因为子类需要用到(先有父亲才能有儿子)
做一个小实验去验证
- 在任意磁盘目录下创建一个
Test.java
文件,文件内容如下,为方便演示所以使用静态内部类继承了Test
public class Test{
public static void main(String[] args){
Object obj = new Test.Child();
}
static class Child extends Test{}
}
复制代码
- 在当前文件所在目录使用
javac Test.java
编译出Test.class
文件 - 执行文件并添加一个参数增加输出内容,并将标准输出内容按关键字过滤掉
java -verbose:class Test | grep Test
复制代码
不出意外的话,会看到下面这么两条信息,我们想去new
一个Child
,Loader
的顺序是先加载Test
,再加载Test$Child
,加载完后在元空间中就会存在这两份说明书,new
操作就可以找到对应的Class
对象装配成对象实例。此时还可以看到,这两个类是从系统中哪里来的。
这里还有一个更优先加载的是Object
的Class
,只是被过滤掉了,会从环境变量配置的目录中jre
文件夹下的lib
文件夹中的rt.jar(runtime的缩写)
中去找到Object
的字节码文件
jar
包其实就是zip
包,将后缀名改名zip
就可以unzip
命令解压出来
[Loaded Test from file:/D:/development/test/]
[Loaded Test$Child from file:/D:/development/test/]
javap
命令可以查看class
文件的内容,把字节码翻译成元空间的那份说明书javap Test.class
Compiled from "Test.java"
public class Test {
public Test();
public static void main(java.lang.String[]);
}
复制代码
执行这个加载Class动作的类就是Classloader(类加载器)
Classloader
负责从外部系统中加载一个类- 这个类对应的Java文件并不一定需要存在,因为
JVM
跑的是字节码文件,只要能凭空的创造出一个符合JVM
规范的字节码文件也能加载 - 字节码也不一定需要存在可以动态生成,也叫动态字节码增强,比如可以从网上获取资源,文件的本质是字节流,这种分离提供了
JVM
中可以执行其他语言的可能 - 这是Java世界丰富多彩的基石
- 这个类对应的Java文件并不一定需要存在,因为
Classloader的双亲委派加载模型
Classloader
对象都有一个父亲(parent
),可以通过getParent
获取,还有一个方法叫loadClass
,任何一个类加载器被要求加载类时,会调用loadClass
方法,但都会先问一遍自己的父亲索要,如果父亲返回了就不会再自己去加载,没有返回才会尝试自己加载,源码可以简单看看了解大概在做什么就行
@CallerSensitive
public final ClassLoader getParent() {
if (parent == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Check access to the parent class loader
// If the caller's class loader is same as this class loader,
// permission check is performed.
checkClassLoaderPermission(parent, Reflection.getCallerClass());
}
return parent;
}
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
复制代码
这里编写了一段恶意代码,一个包名相同但构造函数抛出异常的String
类
package java.lang;
public class String {
public String() {
throw new IllegalArgumentException();
}
}
复制代码
因为有双亲委派机制,在加载这个类的时候,会问他的父亲是否加载过这个类,出于安全考虑,基本类都是由最顶端的启动类加载器进行加载,所以这段有恶意代码的String
类不会被加载
虽然叫双亲(
parent
)委派,但实际是单亲 : )
强大的反射
反射为我们提供了如下特点
- 根据参数动态的创建一个对象
- 根据参数动态调用一个方法
- 根据参数动态获取一个属性(或设置)
根据参数传递的名字,创建一个对应的对象
public class Check {
public static void main(String[] args) throws ClassNotFoundException,
NoSuchMethodException, InvocationTargetException,
InstantiationException, IllegalAccessException {
Class klass = Class.forName(args[0]);
Object obj = klass.getConstructor().newInstance();
}
}
复制代码
示例中,只要在执行参数中传入一个Class
的全限定类名,比如java.lang.String
,就能生成一个对应的实例对象(能找到该类的话)
Class.forName
方法根据名字返回类的对象,对应着上文JVM
示例的图,传入Animal
的全限定类名,就能找到这份说明书- 除了
Class.forName
以外,实例对象的getClass
方法,[Class名].class
都可以获取Class
对象 - 调用
getConstructor
方法时,会返回public
的构造器,与之对应的有一个getDeclaredConstructor
会返回包括非public
的构造器,想要获取的构造器有入参时,则需要在getConstructor(String.class)
获取构造器时传入参数类型的Class对象 - 调用
newInstance
可以看成new XXX()
操作,有入参要求则传入对应参数。甚至可以不通过构造器,直接使用Class.newInstance()
实例化一个对象,但这个方法在JDK9
中被废弃了,不建议使用 getConstructors
和getDeclaredConstructors
方法都是获得Constructor
数组的作用
根据参数传递的名字,调用对应的方法
- 调用方法可以使用
Method
的invoke
方法,此方法需要传入对象的实例和对应的入参(如果有)
要注意getMethod
和getDeclaredMethod
的区别
- 前者
getMethod
只能获取所有权限为public
的方法,包括其父类方法 - 后者
getDeclaredField
可以获取到包括非public的方法(private
),但只限在本类,无法获得继承父类的任何方法。 - 如果要获取有参数的方法,则在获取方法时,需要传入对应的参数类型
Class
对象 getMethods
和getDeclaredMethods
方法都是获得Method
数组的作用
public class Animal {
public void run(String name) {
System.out.println("run with:" + name);
}
public static void main(String[] args) throws Exception {
Animal animal = new Animal();
Method run = animal.getClass().getMethod("run",String.class);
run.invoke(animal,"test");
}
}
复制代码
方法示例中,通过反射获取了run
方法并传入参数调用
public class Animal {
public void run(String name) {
System.out.println("run with:" + name);
}
private void sleep(){
System.out.println("sleep");
}
public static void main(String[] args) throws Exception {
Animal animal = new Animal();
Method run = animal.getClass().getDeclaredMethod("sleep");
run.invoke(animal);
}
}
复制代码
方法示例中,通过反射获取了sleep
方法并调用
根据参数传递的名字,获取对应的属性(或设置属性)
- 获取属性调用
Field
的get
方法,需要传入对象实例 - 设置属性调用
Field
的set
方法,需要传入对象实例和即将要赋予的值
要注意getField
和getDeclaredField
的区别
- 前者
getField
只能获取所有权限为public
的字段,包括其父类字段 - 后者
getDeclaredField
可以获取到包括非public的字段(private
),但只限在本类,无法获得继承父类的任何字段。获取到private
权限属性,此时还无法进行访问,会抛出权限异常,对Field
对象调用其setAccessible(true)
才能正常访问 getFields
和getDeclaredFields
方法都是获得Field
数组的作用
public class Animal {
public String head = "head";
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
Animal animal = new Animal();
System.out.println(animal.getClass().getField("head").get(animal));
}
}
复制代码
示例中,在执行参数中传入head
,利用反射能获取到对应的属性(get)
还有set方法
public class Animal {
public String head = "head";
private Long id = 1L;
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
Animal animal = new Animal();
Field id = animal.getClass().getDeclaredField("id");
id.setAccessible(true);
id.set(animal, 100L);
System.out.println(id.get(animal));
}
}
复制代码
示例中,获取到一个私有属性id
后,需要先设置访问权限,通过set
方法将1
改成100
,最后输出
注解 Annotation
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。 Java 语言中的类、方法、变量、参数和包等都可以被标注。Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。 当然它也支持自定义 Java 标注。
在反射中使用较多的还有getAnnotation
等相关获取注解的方法,Class,Method,Field
可通过相关方法获取注解信息,注解在Spring
世界中应用的非常广泛
声明一个注解的格式,和接口比较类似,在interface
前加多一个@
符号,在代码中就可以@Test
使用此注解
public @interface Test{
}
复制代码
注解和反射是分不开的,注解就是Class
说明书中的一小段信息标记,一个类比,Class
就是一件衣服,衣服上有个吊牌写着不可水洗,只能干洗,这个吊牌所携带的信息就是注解,除了类以外,方法,字段等都可以使用注解 注解仅仅只是一段信息,它本身无法进行工作,如果没有东西去处理注解,那么注解就没有意义
一些内置的注解:元注解,就是可以用在注解上的注解
@Target
,此注解的作用为指定当前注解能作用在哪些地方,当没有@Target
时,注解可以用在任何地方,有@Target
时,注解只能用于指定的范围内
// 声明注解只能用在字段
@Target(ElementType.FIELD)
public @interface Cache {
}
// 使用注解
public class Animal{
@Cache
String name;
}
复制代码
@Target
注解可接收ElementType
枚举数组,这里传入了ElementType.FIELD
枚举,意为此注解只能用在属性字段上 java.lang.annotation.ElementType
枚举
ElementType.TYPE
可用在类,接口,注解以及枚举类型上
@Cache
public class Animal{
}
复制代码
ElementType.FIELD
可用在字段声明上(包括枚举常量),
public enum StatusEnum {
@Cache OK,FAILD
}
复制代码
ElementType.METHOD
可用在方法声明上,不包括构造函数
public class Animal{
@Cache
void run(){}
}
复制代码
ElementType.PARAMETER
可用在方法参数声明上
public class Animal{
void run(@Cache String name){}
}
复制代码
ElementType.CONSTRUCTOR
可用在构造函数声明上
public class Animal{
@Cache
public Animal(){}
}
复制代码
ElementType.LOCAL_VARIABLE
可用在方法中的局部变量(无法通过反射获取该注解信息),没什么卵用
public class Animal{
void run(){
@Cache
String address = "where";
}
}
复制代码
ElementType.ANNOTATION_TYPE
可用在注解上的注解 : )
@Cache
public @interface Log{
}
复制代码
ElementType.PACKAGE
可用在包声明上,在每个包下都可以新建一个package-info.java
@Cache
package main.java.Test;
复制代码
ElementType.TYPE_PARAMETER
可用在类型参数上
public class Test<@Cache T>{}
复制代码
ElementType.TYPE_PARAMETER
可用在使用类型的任意语句中
String str = new @Cache String("test");
复制代码
@Retention
此注解作用为标注注解以何种策略进行保留
@Retention
注解可接收RetentionPolicy
枚举数组
ElementType.SOURCE
注解会在编译时丢弃,也就是注解只保留在Java源代码中ElementType.CLASS
注解经过编译后会保留在字节码当中,JVM
运行时不会保留,默认为此注解ElementType.RUNTIME
注解经过编译后会保留在字节码当中,并且原型是会保留到JVM
当中,想要使用反射进行操作,类型必须为ElementType.RUNTIME
@Documented
注解,作用是在使用javadoc
生成时是否记录,用的不多,因为javadoc
用的也不多 : )
介绍俩个JDK自带的注解
@Override
子类重写父类方法的注解,重写时不加也可以工作,但是加了能减少错误的方法,比如重写时方法名写错了,我们以为重写了,但实际没有,此时编译器就能检查到并给出对应的报错@Deprecated
被标记的地方(比如方法),调用时IDE会提示这是一个被废弃的方法,但仍然可以使用,只是提示使用者不该再使用@SuppressWarnings
允许选择性的消除某些不想让编译器检查的警告,接受一个String
,可传入all,uncheck,unused
等@FunctionalInterface
用于标注对应的接口时函数接口
注解的属性
复用上面用过的类比,Class
就是一件衣服,衣服上有个吊牌就是Annotation
, 可以在这个吊牌标记一些属性,比如传入属性可以水洗
,可以干洗
,根据不同的需求传入不同的参数以此来复用这个吊牌(Annotation
)
注解可用的属性类型有基本数据类型
,String
,类(Class)
,以及它们俩的数组,如果不传参数,则使用时()
可加可不加
这个注解定义了一个属性为洗涤方式washMethod
,类型为String[]
,并使用"干洗"
作为这个属性的默认值
public @interface Clothes {
String[] washMethod() default "水洗";
}
复制代码
使用时如果不传,这个吊牌的washMethod
属性就是水洗,传参则属性为传入的参数
@Clothes(washMethod = {"随便洗","用力洗"})
public class Animal {
}
复制代码
注解有个特点,如果属性名为value
,只传入value
时,属性名可以不写,但如果要传入多个参数,则需要写完整的传值;如@Clothes(value=1)
// 定义value
public @interface Clothes {
String[] washMethod() default "水洗";
String value() default "";
}
// 只传入value时,可以不写名字(当然想写也可以)
@Clothes("test")
public class Animal {
}
// 传入多个时,则必须写完整
@Clothes(value = "test", washMethod = {"随便洗", "水洗"})
public class Animal {
}
复制代码
如何将注解与反射应用到实践中,可以看看我写的这篇博客 手写一个Spring的IOC容器 ,里面讲解了使用反射与注解,实现了自动装配Bean
的过程 : )