今天我们来看一下Java中的反射技术:

首先来了解一下Java中的反射的一些概念:

Java中的反射是1.2引入的

反射的基石:class类

Class类的各个实例对象分别对应各个类在内存中的字节码,例如Person类的字节码,ArrayList类的字节码,等等。

一个类被类加载器加载到内存中,占用一片存储空间,这个空间里面的内容就是类的字节码,不同的类的字节码是不同的,所以他们在内存中的内容是不同的,这一个个空间可分别用一个个对象来表示,这些对象显然具有相同的类型,这个类型就是Class类型

Java类用于描述一类事物的共性,该类事物有什么属性,没有什么属性,至于这个属性的值是什么,则是由这个类的实例对象确定的,不同的实例对象有不同的属性值,Java程序中的各个Java类,他们是否属于同一类事物,是不是可以用一个类来描述这类事物呢?这个类的名字就是Class,要注意与小写的class关键字的区别,Class类描述了哪些方面的信息呢?类的名字,类的访问属性,类所属的包名,字段名称的列表,方法名称的列表,学习反射,首先要明白Class这个类,下面来看一下实例:

public static void main(String[] args) throws Exception{
/**第一个例子了解Class类型的定义*/
String str1 = "abc";//字符串1
String str2 = "abcd";//字符串2
//三种方式获取Class类型
Class cls1 = str1.getClass();
Class cls2 = str2.getClass();
Class cls3 = String.class;
Class cls4 = Class.forName("java.lang.String");
System.out.println(cls1);//打印字符串1类型
System.out.println(cls2);//打印字符串2类型
System.out.println(cls3);
System.out.println(cls4);
System.out.println(cls1 == cls2);//字符串1和字符串2的类型是否相等
System.out.println(cls1 == cls3); //总之只要在源程序中出现的类型,都有各自的Class实例对象
System.out.println(cls1.isPrimitive());//是不是原始类型(同样也有类似的方法判断是不是枚举,数组等类型)
System.out.println(int.class.isPrimitive());
System.out.println(int.class == Integer.class);//int类型和Integer类型是不一样的,一个是基本类型,一个是对象类型
System.out.println(int.class == Integer.TYPE);//8中数据基本类型都对应与其对象类型中的TYPE字段
System.out.println(int[].class.isPrimitive());//数组类型不是原始类型,数组是一个对象类型即int[]是一个Object
}

运行结果:

对应输出的结果可以看出,三种获取String类型的方法得到的结果都是java.lang.String,同时还有就是str1字符串1和str2字符串2的类型也是一样的,即使类容不一样,这个就和上面说的一致了,同样的类型对应在内存中的字节码是一样的,即JVM只加载相同类型的字节码到内存只有一次,内存中同样的类型的字节码也是只有一份,所以即使对象的内容不一样,但是类型一样,字节码是一样的,这个不要和对象不一样的概念混淆了,

System.out.println(str1==str2)

这段代码输出的肯定是false,因为str1和str2是两个不同的对象,由此可以看出,类型是一样的Class对象是相等的,当然也可以用Class对象来判断两个对象是不是同一种类型

下面来看一下基本类型和对象类型的却别:

int是基本类型,Integer是对象类型,这个类型是不一样的,所以Class对象也是不一样的,同时Class对象中也提供了一些判断方法,比如判断是不是基本类型,枚举类型,数组类型等信息,还有一点要注意的就是基本类型数组是一个对象类型,所以下面的代码是没有错的:

Object obj = {1,2,3}

在来看一下8种数据类型对应的对象中的字段TYPE,先来看一下文档:

文档中说了,Java中的8中基本数据类型的字节码对应的就是其对象类型中的TYPE字段(这里还有一个Void类型),再看一下Integer中的TYPE字段的定义:

/**
* The {@code Class} instance representing the primitive type
* {@code int}.
*
* @since JDK1.1
*/
public static final Class<Integer> TYPE = (Class<Integer>) Class.getPrimitiveClass("int");

就是获取int类型的字节码

下面来看一下Class中的Constructor类(类的构造方法)

Class中的Constructor类是对象的构造方法反射出来对应的对象类型,具体看一下代码:

public static void main(String[] args) throws Exception{
/**获取String对象指定的构造方法(通过方法的参数类型,传递参数的Class对象)*/
Constructor constructor = String.class.getConstructor(StringBuffer.class);//得到String对象的一个参数是StringBuffer的构造方法
String str = (String) constructor.newInstance(new StringBuffer("abc"));//生成对象String,当然要传递一个StringBuffer参数
System.out.println(str);//打印值
/**总结:这种方法是要传递参数类型和参数的值,getConstructor(...)方法的参数是一个可变参数,因为构造方法可能有多个参数*/ /**获取String默认的构造方法生成String对象*/
String str1 = String.class.newInstance(); /**获取String对象的所有构造方法,并将构造方法的参数类型打印出来*/
Constructor[] constructors = Class.forName("java.lang.String").getConstructors();
for(int i=0;i<constructors.length;i++){
Type[] type = constructors[i].getGenericParameterTypes();
for(int j=0;j<type.length;j++)
System.out.print(type[j]+",");
System.out.println();
}
}

运行结果:

通过getConstructor(...)方法可以获取到String对象对应的参数类型的构造方法对象,因为一个对象可能有多个构造方法,所以getConstructor方法的参数是一个可变参数类型,可以同时传递多个类型,在生成String对象的时候,还需要传递参数值,这个和我们用常规方法产生String对象类似

同时也可以很方便的通过Class对象中的newInstance方法直接获取到String对象,这里是通过String对象的默认构造方法产生的对象,看一下newInstance的源代码:

public T newInstance()
throws InstantiationException, IllegalAccessException
{
if (System.getSecurityManager() != null) {
checkMemberAccess(Member.PUBLIC, ClassLoader.getCallerClassLoader());
}
return newInstance0();
}

在来看一下newInstance0方法的代码:

 private T newInstance0()
throws InstantiationException, IllegalAccessException
{
// NOTE: the following code may not be strictly correct under
// the current Java memory model. // Constructor lookup
if (cachedConstructor == null) {
if (this == Class.class) {
throw new IllegalAccessException(
"Can not call newInstance() on the Class for java.lang.Class"
);
}
try {
Class<?>[] empty = {};
final Constructor<T> c = getConstructor0(empty, Member.DECLARED);
// Disable accessibility checks on the constructor
// since we have to do the security check here anyway
// (the stack depth is wrong for the Constructor's
// security check to work)
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
c.setAccessible(true);
return null;
}
});
cachedConstructor = c;
} catch (NoSuchMethodException e) {
throw new InstantiationException(getName());
}
}
Constructor<T> tmpConstructor = cachedConstructor;
// Security check (same as in java.lang.reflect.Constructor)
int modifiers = tmpConstructor.getModifiers();
if (!Reflection.quickCheckMemberAccess(this, modifiers)) {
Class<?> caller = Reflection.getCallerClass(3);
if (newInstanceCallerCache != caller) {
Reflection.ensureMemberAccess(caller, this, null, modifiers);
newInstanceCallerCache = caller;
}
}
// Run constructor
try {
return tmpConstructor.newInstance((Object[])null);
} catch (InvocationTargetException e) {
Unsafe.getUnsafe().throwException(e.getTargetException());
// Not reached
return null;
}
}

代码有点长,我不需要全部看懂,只需要看其中的一部分即可:首先来看一下这段代码:

 if (cachedConstructor == null) {
if (this == Class.class) {
throw new IllegalAccessException(
"Can not call newInstance() on the Class for java.lang.Class"
);
}

cacheConstructor定义是:

private volatile transient Constructor<T> cachedConstructor;

从名字上可以了解到是一个缓存的概念,由此我们可以猜测,这里用到了缓存机制,同时也说明了,反射是很耗资源的。

在来看一下代码:

try {
return tmpConstructor.newInstance((Object[])null);
} catch (InvocationTargetException e) {
Unsafe.getUnsafe().throwException(e.getTargetException());
// Not reached
return null;
}

返回的时候用的是Constructor方法newInstance,同时传递的参数是null,因为默认的构造方法是无参的,从源代码中可以看出,Class中的newInstance方法实际上还是通过Constructor中的newInstance方法生成对象的。

上面还有就是打印出了String所有构造方法的参数类型,其中有一部分是空行的,这个就是String的无参的构造方法,什么也没有打印,打印出来的这些类型都是签名的。

下面在来看一下Field类,用来表示对象字段类型的

代码:

	public static void main(String[] args) throws Exception{
/**获取ReflectPoint类中的public的y字段(字段用名称来区分,方法用参数类型和个数和顺序来区分)*/
ReflectPoint point = new ReflectPoint(3,5);
Field fieldY = point.getClass().getField("y");
System.out.println("y字段的类型:"+fieldY.getGenericType());//获取字段的类型
System.out.println("y的值是:"+fieldY.get(point));//获取字段的值
/**获取ReflectPoint类中的private的x字段*/
Field fieldX = point.getClass().getDeclaredField("x");//没有用getField方法了,因为getField方法只能获取到public字段,getDeclaredField可以获取到所有的字段
fieldX.setAccessible(true);//因为x字段是private私有的,所以要暴力访问
System.out.println("x字段的类型:"+fieldX.getGenericType());
System.out.println("x的值是:"+fieldX.get(point));
/**将ReflectPoint对象中的String类型字段中的"j"字符换成"a"*/
Field[] fields = point.getClass().getDeclaredFields();
for(int i=0;i<fields.length;i++){
if(fields[i].getType() == String.class){
String oldValue = (String) fields[i].get(point);
String newValue = oldValue.replace("j", "a");
fields[i].set(point, newValue);
}
}
System.out.println(point);
}

下面是ReflectPoint类:

package com.reflect.demo;

public class ReflectPoint {

	private int x;//私有的
public int y;//公共的
public String str = "jiangwei";
public String str1 = "jiangzhi"; public ReflectPoint(){
} public ReflectPoint(int x, int y) {
super();
this.x = x;
this.y = y;
} public String getStr() {
return str;
} public void setStr(String str) {
this.str = str;
} public String getStr1() {
return str1;
} public void setStr1(String str1) {
this.str1 = str1;
} public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
} @Override
public String toString() {
return "ReflectPoint [x=" + x + ", y=" + y + ", str=" + str + ", str1="
+ str1 + "]";
} }

运行结果如下:

获取ReflectPoint对象中的所有字段,如果字段是String类型的,就将他的内容中的"j"字符换成"a"字符

这里要注意的是:在获取字段x的时候不能像获取其他的字段一样,因为x是private的,所以会出现以下问题:

没有找到字段x,所以这时候需要用getDeclaredField方法,这个方法是获取类中所有的字段,而getField方法是获取public的字段,但是这时候运行还是报错:

提示说不能访问ReflectPoint类中的私有变量,这时候需要再加一段代码:

fieldX.setAccessible(true);//因为x字段是private私有的,所以要暴力访问

这时候运行就没有问题了,所以如果类中的字段不是public的时候,在获取类的字段时候要注意,首先要能拿到这个字段用getDeclaredField方法,然后是能够操作这个字段还要加一段代码:fieldX.setAccessible(true);这样就可以访问类中的所有字段了。

下面在来看一下Method类,是对应对象中方法

public static void main(String[] args) throws Exception{
Method methodCharAt = String.class.getMethod("charAt", int.class);
System.out.println(methodCharAt.invoke("abc", 2)); Method[] methods = String.class.getDeclaredMethods();
for(int i=0;i<methods.length;i++){
System.out.println("方法名:"+methods[i].getName());
}
}

运行结果:

String类中有很多的方法,这里没有全部截图出来,这个作用很大的,我们就可以控制一个方法的执行流程,比如我想在String的charAt方法执行前干一件事,执行后干一件事,这个就是AOP概念了,后面的代理模式也要用到这个方法,所以说这个Method的作用很大的。

下面来看一下怎么反射带有数组参数的方法

看一下代码:

public static void main(String[] args) throws Exception{
Method method = ReflectTest.class.getMethod("print", int[].class);
//invoke(Object obj,Object...args);
//JDK1.5:Object...
//JDK1.4:Object[]...
//先走1.4版本的,如果不符合就走1.5的版本,所以传递一个new Integer[]数组,因为Integer是Object,所以走JDK1.4版本的Object[] args
//这样对应的参数是args:1,2,3 args.length=3,但是方法print只有一个参数,所以会报异常
//如果将参数改成new int[]{1,2,3}的话,就不会了,因为int[]数组本身就是一个Object类型的,所以JDK1.4版本的走不通,所以走JDK1.5版本的
//JDK1.5版本中,编译器拿到int[](Object)直接传给args,这时候可变参数还是会转化成Object[]数组,但是这时候args.length=1,所以不会报错
method.invoke(null, new Integer[]{1,2,3});
} public static void print(int[] a){
for(int i=0;i<a.length;i++)
System.out.println(a[i]);
}

这个方法中就是通过反射带有数组类型的方法,然后用invoke方法执行,但是这里有个问题就是版本兼容的问题,我们知道可变参数是JDK1.5引入的,为了兼容向下版本,所以有可变参数的方法,首先会执行其对应的JDK低版本的方法,比如这里的invoke方法,invoke(Object obj,Object...args)这个是JDK1.5版本中定义的,在JDK1.4版本中的定义是invoke(Object obj,Object[]
args),可以查看源代码:

public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass(1); checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}

其中ma.invoke(Object,Object[] args)的,由此也说明了,可变参数和数组时相同的,互相传递参数是没有问题的。

言归正传,现在的问题是我给invoke(null,new Integer[]{1,2,3})传递这样的参数,执行报错(因为print是static,所以不依赖于对象类型,第一个参数为null)

提示参数个数不匹配,这个很郁闷呀,为什么不对呢?print方法接受的是一个int[]类型的数组,我传递一个Integer[]数组,为什么会提示参数的个数不正确呢?下面来解释一下原因:

刚才也说道了,为了版本的兼容问题,所以先走JDK1.4版本的

JDK1.5版本是:Object... args

JDK1.4版本是:Object[] args

而Integer是Object对象,所以new Integer[]相当于是new Object[]所以会走JDK1.4版本的,那么此时args.length=3了,但是print参数的个数是1,所以会报错了

我现在把new Integer[]改成new int[],然后运行,尽然不报错了,这个又是为什么呢?

这个是因为Java中数组是一个Object,所以int[]是一个Object,但是Integer本身是一个Object,所以Integer[]是一个Object[]这个要注意,不要混淆,也就是说,Java中的基本类型的数据是一个Object,那么new int[]是一个Object,所以先走JDK1.4版本的Object[] args发现不匹配,所以在走JDK1.5版本的Object...args这个可以匹配的,可变参数args就传递一个Object(int[]),所以args.length=1这时候参数的个数匹配了,所以不报错了。

这里要注意两个问题:

一个是版本兼容的问题。

对于这个问题,我可以这样修改代码:

invoke(null,newObject[]{new Integer[]{1,2,3}});

invoke(null,(Object)new Integer[]{1,2,3});

这两种方式都可以,原理都是一样的,第一种方式是Object[] args直接走JDK1.4版本的,这时候args.length=1,所以不报错了,第二种方式是强制转化成Object,这样就表明了不走JDK1.4版本的Object[] args.而是走JDK1.5的Object... args的,这样args.length=1,解决方法的根据就是让args.length=1即可

这里还有另外的一种方式(这种方式和本文研究的问题是没有关系的,只是一种解决方案):

// 数组参数的调用方法
Method arrayInputMethod = clazz.getMethod("print",Integer[].class);
Object arrayObj = Array.newInstance(Integer.class, 3);//初始化大小为2,数组元素类型是Integer的数组
Array.set(arrayObj, 0, 1);
Array.set(arrayObj, 1, 2);
Array.set(arrayObj, 2, 3);
arrayInputMethod.invoke(object, arrayObj);

这样也可以解决上面的问题,但是这个和此问题没有关系,只是做一下拓展.

还有一个问题就是Java中的基本类型数组是Object对象,下面在来看一下案例:

public static void main(String[] args) throws Exception{
int[] intAry = {1,2,3};
String[] strAry = {"a","b","c"};
//将数组转化成list然后将内容打印出来
System.out.println(Arrays.asList(intAry));
System.out.println(Arrays.asList(strAry));
}

打印结果如下:

问题来了,为什么没有打印[1,2,3]呢,而String数组可以正常打印呢?,这个就是因为int[]是一个Object对象,看一下Arrays.asList方法的定义:

从文档中可以看到asList方法用到了可变参数,上面说到了,只要方法中的参数是可变参数类型的,要向下兼容,所以会有:

JDK1.5版本的asList(T...a)

JDK1.4版本的asList(T[] a)

这里的T是Object 所以int[]是一个Object,走JDK1.5版本的,所以只打印了一个数据就是int[]类型的对象的hashCode,而String数组走的是JDK1.4版本的,所以全部打印出来了,当然如果将int[]改成Integer[]也可以打印出来的。这个问题说明了,Java中的基本类型数组时一个Object.

今天就说到这里了,没有接触过反射的同学可能认为反射是个很神奇的东西,其实看过之后,感觉很简单的,这篇写的很爽,因为不难嘛!下一篇是泛型了,这个就有点蛋疼了!

Java高新技术第二篇:反射技术的更多相关文章

  1. 反射那些事儿——Java动态装载和反射技术

    一直以来反射都是只闻其声,却无法将之使用,近日尽心下来学习下,发现了很多精妙之处. Java动态装载和反射技术 一.类的动态装载 1.Java代码编译和执行的整个过程包含了以下三个重要的机制: ● J ...

  2. Java SE 第二篇

    二.  Java SE 第二篇 1.  Arrays 数组 // 声明一维数组,[]内不允许有值 int[] arr; int arr[]; // 创建一维数组对象,[]内必须有值 arr = new ...

  3. Java中类加载和反射技术实例

    我们知道一个对象在运行时有两种类型,一个是编译类型,一个是运行时类型.在程序运行时,往往是需要发现类和对象的真实的信息的.那么如何获的这种信息呢? 其一,如果我们在编译和运行时都知道类型的具体信息,这 ...

  4. 从.Net到Java学习第二篇——IDEA and start spring boot

    从.Net到Java学习第一篇——开篇 所谓工欲善其事,必先利其器,做java开发也一样,在比较了目前最流行的几个java IDE(eclipse,myeclipse.IDEA)之后,我果断选择IDE ...

  5. 8成以上的java线程状态图都画错了,看看这个-图解java并发第二篇

    本文作为图解java并发编程的第二篇,前一篇访问地址如下所示: 图解进程线程.互斥锁与信号量-看完还不懂你来打我 图形说明 在开始想写这篇文章之前,我去网上搜索了很多关于线程状态转换的图,我惊讶的发现 ...

  6. [转载] Java高新技术第一篇:类加载器详解

    本文转载自: http://blog.csdn.net/jiangwei0910410003/article/details/17733153 首先来了解一下字节码和class文件的区别: 我们知道, ...

  7. 【Bootstrap】Bootstrap和Java分页-第二篇

    目录 关于此文 配置xml-pager.tld 分页控件-Pager 分页action集成类-BaseController 实例-Dao 实例-service 实例-action 实例-JSP 实例- ...

  8. Java 学习 第二篇;面向对象 定义类的简单语法:

    1:基本知识 [public / protected / private] class 类名 { 零个到多个构造器定义; 零个到多个属性; 零个到多个方法; } 其中类中各个成员之间的顺序没有关系,且 ...

  9. Java高新技术第一篇:类加载器详解

    首先来了解一下字节码和class文件的区别: 我们知道,新建一个Java对象的时候,JVM要将这个对象对应的字节码加载到内存中,这个字节码的原始信息存放在classpath(就是我们新建Java工程的 ...

随机推荐

  1. mongoose 常用数据库操作 查询

    条件查询 Model.find(conditions, [fields], [options], [callback]) demo1 try.js var User = require(". ...

  2. openwrt ssh免密登录

    1 生成相关秘钥 dropbearkey -t rsa -f id_rsa dropbearkey -y -f id_rsa | grep "^ssh-rsa" >> ...

  3. 数组循环移动 空间复杂度O(1)

    ---恢复内容开始--- 题目大意: 输入元素个数,输入数组,输入右移步数,输出结果: 基本思路: 可以把数组(从下标为0开始存储)看成两部分,分别是[0,n-step-1],[n-step,n-1] ...

  4. vmware下搭建openwrt

    最近闲来无事,想研究下openwrt, 所以尝试着自己搭建一个来玩玩, 当然这里不是以源码编译的形式,那样太耗时. 首先官网下载已有的系统image,  路径如下 : https://archive. ...

  5. SQL 与,或,非

    SQL AND, OR and NOT(与,或不是运算符) AND&OR运算符用于根据一个以上的条件过滤记录. SQL AND & OR 运算符 WHERE子句可以与AND,OR和NO ...

  6. 【NOIP2019模拟2019.9.4】B(期望的线性性)

    题目描述: \(1<=n,ai<=5*10^5\) 题解: 我是弱智我不会期望线性. 设\(E(a[i])\)表示第i个期望被减的个数. \(E(a[1])=a[1]\) 不难发现\(E( ...

  7. PHP ftp_get_option() 函数

    定义和用法 The ftp_get_option() 函数返回 FTP 连接的各种运行时选项. 语法 ftp_get_option(ftp_connection,option) 参数 描述 ftp_c ...

  8. BZOJ 3236: [Ahoi2013]作业(莫队+树状数组)

    传送门 解题思路 莫队+树状数组.把求\([a,b]\)搞成前缀和形式,剩下的比较裸吧,用\(cnt\)记一下数字出现次数.时间复杂度\(O(msqrt(n)log(n)\),莫名其妙过了. 代码 # ...

  9. (转)Ubuntu下用eclipse cdt编写多线程程序的简单设置

    在Ubuntu下用eclipse cdt编写了一个多线程程序,但是总是出现pthread_create函数未定义! 查找了下原因,原来是要对eclipse进行一些简单的设置: 右键单击项目->P ...

  10. windows 驱动开发 DDK与WDK WDM的区别

    1.首先,先从基础的东西说起,开发WINDOWS下的驱动程序,需要一个专门的开发包,如:开发JAVA程序,我们可能需要一个JDK,开发WINDOWS应用程序,我们需要WINDOWS的SDK,现在开发W ...