您的位置:首页 > 职场人生

黑马程序员--十二、Java反射

2014-01-12 20:06 471 查看
----------------------
ASP.Net+Android+IOS开发、.Net培训、期待与您交流! ----------------------

反射概念

 反射就是把Java类中的各种成分映射成相应的java类。例如,一个Java类中用一个Class类的对象来表示,一个类中的组成部分:成员变量,方法,构造方法,包等等信息也用一个个的Java类来表示,就像汽车是一个类,汽车中的发动机,变速箱等等也是一个个的类。表示java类的Class类显然要提供一系列的方法,来获得其中的变量,方法,构造方法,修饰符,包等信息,这些信息就是用相应类的实例对象来表示,它们是Field、Method、Contructor、Package等等。 

反射的基石Class类

Class;

Class类描述了类的名字,类的访问属性,类所属包名,字段名称列表,方法名称列表等等;学习反射,首先要明白Class类。

如:

很多的人用什么类标示?那么很多的Java类用什么标示?

人—Person

Java类—Class

Person代表人,他的实例对象就是张三,李四等这样一个个具体的人;Class类代表Java类,他的各个实例对象又分别对应什么呢?

对应各个类在内存中的字节码,例如,Person类的字节码,ArrayList类的字节码等等;

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

什么是字节码?

 当源程序中用到某个类时,首先要从硬盘把这个类的那些二进制代码即:一个类编译成class放在硬盘上以后,就是一些二进制代码,要把这些二进制代码加载到内存中里面来,再用这些字节码去复制出一个一个对象来。

如何得到各个字节码对应的实例对象(Class类型)

1, 类名.class;例如System.class;

 

2, 对象.getClass();例如:new Data().getClass();

 

3, Class.forName(“包名.类名”);例如:Class.forName(“java.util.Data”);//  这种方式较为简单,只要知道类的完整名称即可。不需要使用该类,也不需要去调用具体的属性和行为。就可以获取到Class对象了;仅知道类名就可以获取到该类字节码对象的方式,更有利于扩展

九个预定义的Class:

1,八种基本类型(byte、short、int、long、float、double、char、boolean)的字节码对象和一种返回值为void类型的void.class。

2,Integer.TYPE是Integer类的一个常量,(但是int.class和Integer.class是不相等的false,它们是两个不同的字节码)它代表此包装类型包装的基本类型的字节码,所以和int.class是相等的。

基本数据类型的字节码都可以用与之对应的包装类中的TYPE常量表示

 

数组类型的Class实例对象用的方法是:Class.isArray();

注意:

只要是源程序中出现的类型,都有各自的Class实例对象,例如:int[],void…都是代表一个类型。

Class类中常用方法:

1, static Class forName(String className);//返回给定字符串名的类或接口相关类的Class对象;

2, Class getClass();//返回的是Object运行时的类,即返回Class对象即字节码对象;

3, Constructor getConstructor();//返回Constructor对象,反映此Class对象所代表的类指定公共构造方法;

4, Field getField(String name);//返回Filed对象,标示Class对象所代表的的类或接口指定公共成员字段;

5, Field[] getField();//返回Field对象数组,代表类中成员字段;

6, Method getMethod(String name,Class…parameterTypes);//返回一个Method对象,标示次Class对象锁代表的类的指定公共成员方法;

7,Method[] getMehtods();//返回一个包含某些Method对象的数组,是所代表的的类中的公共成员方法。

8,String getName()以String形式返回此Class对象所表示的实体名称。

9,String getSuperclass()返回此Class所表示的类的超类的名称

10,boolean isArray()判定此Class对象是否表示一个数组

11,boolean isPrimitive()判断指定的Class对象是否是一个基本类型。

12,T newInstance()创建此Class对象所表示的类的一个新实例。

通过Class对象来获取类实例

因为Class类没有构造方法,所以只能通过该类方法来获取类的实例对象

具体步骤:

1, 查找并加载指定名字的字节码文件存储进内存,并封装成Class对象;

String className = “包名.Person”;

Class clas = Class.forName(className);

2, 通过Class对象的newInstance()方法创建该Class对应类的实例对象;

Person p = (Person)clas.newInstance();

3,调用构造函数进行初始化

Person p = new Person();

示例:

package 加强Day01.黑马;

class Person{

private String name;

private int age;

Person(String name,int age){

this.name = name;

this.age  = age;

}

public String toString(){

return name+":"+age;

}

}

public class ReflectDemo {

 

public static void main(String[] args) throws Exception{

Class cla = Class.forName("加强Day01.黑马.Person");//获取Person类的Class对象

Person p = (Person)cla.newInstance();//通过newInstance方法获取对应类对象。

System.out.println(p.toString());

}

}

构造方法的反射应用

Constructor类

Constructor类代表某个类的构造方法;

如果指定的类中没有空参数的构造函数,或者要创建的类对象需要通过指定构造函数进行初始化,这是怎么办呢?

首先不能再使用Class类中newInstance方法,因为要通过指定的构造函数进行对象的初始化,那么就要用到Constructor来获取构造函数

获取构造方法例子:

1, 得到某个类所有的构造方法:

Constructor[] constructors = Class.forName(“java.lang.String”).getConstructor();

2, 得到某一个构造方法:

Constructor constrctor = 

Class.forName(“java.lang,String”).getConstructor(StringBuffer.class);

//获取方法时要用到的类型。

小知识点:

在getConstructor(可变参数)方法中接收的参数没有限制,为什么呢?

因为可以传入1.5新特性可变参数。

再问:那么没有可变参数再来接收多个参数怎么写呢?

我们可以通过数组类获取

得到Constructor对象后干什么呢?

查阅API文档java.lang.reflect.Constructor<T>可参见该类的方法

有了对象就可以创建实例对象(接上面示例):

1,通常方式:String str = new String(new StringBuffer(“abc”));

2,反射方式:String str = (String)constrctor.newInstance(new StringBuffer(“abc”));

Class.newInstance()方法:

例子:String obj = (String)Class.forName(“java.lang.String”).newInstance();

该方法内部是先得到默认的构造方法,然后用该构造方法创建实例对象。

该方法内部具体代码怎么写的呢?

实际上用到了缓存机制类保存默认构造方法的实例对象。

总结:

1, 在创建实例时候,newInstance()方法中的参数列表必须和getConstructor()方法中的参数列表一致。

2, newInstance()方法在构造一个实例对象时,每调用一次就构造一个对象;

3, 利用Constructor类来创建实例的好处是可以指定构造函数,而Class类只能利用无参构造函数创建实例对象。

Field类

Field类代表某个类中的一个成员变量;

常用方法:java.lang.reflect.Field

1, Field getField(String 变量);//只能获取公有和父类中公有的变量;

1724d

2, Field getDeclaredField(String 变量);//获取该类中的任意类型的变量,包括私有的;

3, setAccessible(true);//若是私有的字段,即之前变量是私有的,该方法可以将私有字段进行取消权限获取,也称为暴力访问;

4, set(Object obj,Object value);//将指定对象变量上的Field对象标示的字段设置为指定的新值;

5, Class<?> getType();//返回一个Class对象。

6, Object get(Object obj);//返回指定对象上Field表示的字段的值;

示例:

package 加强Day01.黑马;

public class ReflectDemoxy {

public int x;

private int y;

//使用快捷键快速生成构造函数

public ReflectDemoxy(int x, int y) {

super();//

this.x = x;

this.y = y;

}

}

public class ReflectDemo {

public static void main(String[] args) throws Exception{

Reflect();

}

public static void Reflect()throws Exception{

ReflectDemoxy ref = new ReflectDemoxy(3,5);

Field fid1 = ref.getClass().getField("x");//通过获取Class对象,得到一个字段,公有的;

//fid1它不代表一个值,不是某个变量上的某个值,它代表一个变量。

//作用是类上要用它去取是某个对象对应的值。

//若要取出这个变量值,我们应该明确是在某个类身上的值:fid1.get(ref)

System.out.println(fid1.get(ref));//取出指定类身上的变量。

Field fid2 = ref.getClass().getDeclaredField("y");

//这时可以看到任何类型的变量,但是对于私有字段不能取出,所以需要Field里的方法setAccessible();暴力访问;

fid2.setAccessible(true);

System.out.println(fid2.get(ref));

}

}

练习:把一个类里所有的String类型的字段里的值b变a;

package 加强Day01.黑马;

public class ReflectDemoxy {

public int x;

private int y;

public String str1 = "ball";

public String str2 = "abstract";

public String str3 = "haohaoxuexi";

//使用快捷键快速生成构造函数

public ReflectDemoxy(int x, int y) {

super();//

this.x = x;

this.y = y;

}

public String toString(){

return str1+":"+str2+":"+str3;

}

}

package 加强Day01.黑马;

import java.lang.reflect.Field;

public class ReflectTest {

public static void main(String[] args) throws Exception{

// new一个对象

ReflectDemoxy ref = new ReflectDemoxy(3,4);

//获取所有字段

Field[] fields = ref.getClass().getFields();

//对所有字段遍历并判断是否与String类的字节码相等;

for(Field field: fields){

//if(field.getType().equals(String.class))

if(field.getType() == String.class){

//获取对应字段的值的字符串;

String oldValue = (String)field.get(ref);

//将字符串里的b换成a

String newValue = oldValue.replace('b', 'a');

//改对象的字段

field.set(ref,newValue);

}

}

//打印替换后的结果:

System.out.println(ref);

System.out.println(ref);

System.out.println(ref);

}

}

Method类java.lang.reflect.Method

Method类代表某个类中的一个成员方法;

简单的说:

实际上我们是使用反射的方式拿到指定类的字节码里面的方法,再用这个方法去作用于某个对象。

常用方法:

1, Method[] getMethods();//只获取公共和父类中的方法;

2, Method[] getDeclaredMethods();//获取本类中所有方法包含私有

3, Method getMethod(“方法名”,参数.class);//若是空参数可以写null;因为重载,参数列表和类型可能不一致,参数也可接收可变参数。

4, Object invoke(Object obj,Object…obj(参数));//调用方法,

注意invoke是调用方法对象身上的方法:

砖家模式:谁拥有这个数据谁就是调用它的砖家,那么就应该把方法分配给它!

比如停车:

调用者:对象是车,只有车知道该怎么停下来,是车在调用刹车的动作;

指挥者:人在指挥车停车的动作,给车发出信号让车去执行,停车的动作只有车才能做到,车拥有这个停车的动作,它就是这个的砖家,那么就应该把停车方法分配给车,而不是给人;

思考:

当方法对象身上的方法即:invoke(null,1)第一个参数是null,不通过对象就调用了,说明这个方法是静态的,因为静态方法是不需要对象的。

示例:

package 加强Day01.黑马;

import java.lang.reflect.Method;

public class MethodTest {

public static void main(String[] args)throws Exception {

//获取字符串中某一个字符

String str = "abc";

//通常方式

System.out.println(str.charAt(1));

//反射方式

Class cla = Class.forName("java.lang.String");

Method charAtMethod = cla.getMethod("charAt",int.class);

System.out.println(charAtMethod.invoke(str, 1));

}

}

JDK1.4和JDK1.5的invoke方法的区别:

Jdk1.5:public Object invoke(Object obj,Object…obj);

Jdk1.4:public Object invoke(Object obj,Object[] args);

安装jdk1,4的语法,需要见那个一个数组作为参数传递给invoke方法时,数组中的每个元素分别对应被调用方法中的一个参数,所以,调用charAt方法的代码页可以用jdk1.4改写为charAt.invoke(“str”,new Object[]{1})的形式。

接上面示例:

System.out.println(charAtMethod.invoke(str, new Object[]{2}));

用反射方式执行某个类中的main方法

首先明确为什么要使用反射?

因为在写源程序时,或程序写好后,不知道使用者将传入什么类,类名是什么等,但是我们知道这个类中的方法有main这个方法。这时我们可以通过反射的方式把使用者传入的类名(可定义字符串变量作为传入类名的入口,通过这个变量代表类名),在内部通过传入类名来获取main方法,然后执行相应内容。

需求:写一个程序,这个程序能够根据用户提供的类名,去执行该类中的main方法;

分析:

在启动Java程序的main方法参数是一个字符串数组,即public static void main(String[] args),通过反射方式来调用这个main方法时,如何为invoke方法传递参数呢?

按jdk1.5的语法,整个数组是一个参数,而按照jdk1.4的语法,数组中的每个元素对象对应一个参数,当把一个字符串数组作为参数传递给invoke方法时,javac会执行哪种语法来处理呢?

Jdk1.5兼容jdk1.4语法,所以会按照1.4语法去处理,即把数组打散成若干个单独的参数,所以在给main方法传递参数时,不能使用代码mainMethod.invoke(null,new String[]{“…”})javac只有把它当做jdk1.4语法进行理解,而不会当做jdk1.5语法来解释,因此会出现参数类型不对的问题。

解决办法:

1, mainMethod.invoke(null,new Object[]{new String[]{“xxx”}});

2, mainMethod.invoke(null,(Object)new String[]{“xxx”});。编译器会做特殊处理,不会吧它当数组看,也就不会数组打散成若干个参数了。

示例:

测试类

package 加强Day01.黑马;

public class MainTest {

public static void main(String[] args) {

// 测试类

for(String arg:args){

System.out.println(arg);

}

}

}

 

package 加强Day01.黑马;

import java.lang.reflect.Method;

//写一个程序,这个程序能够根据用户提供的类名,去执行该类中的main方法

public class ReflectMain{

public static void main(String[] args)throws Exception{

//普通方式,因为静态所以直接类名.main()

MainTest.main(new String[]{"111","222","333"});

System.out.println(".............");

//反射方式:

String strName = args[0];//传进来的参数不确定类型不确定;

Class classMain = Class.forName(strName);//获取字节码对象

Method method = classMain.getMethod("main",String[].class);

//调用方法对象的方法invoked,去执行

//方法一:使用Object超类

method.invoke(null,(Object)new String[]{"111","222","333"});

//方法二:将数组打包,那个编译器拆包后还一个String[]数组。

method.invoke(null, new Object[]{new String[]{"111","222","333"}});

}

}

数组的反射

1,具有相同位数和元素类型的数组属于同一个类型,即具有相同的Class示例对象。

示例:

int[] a1 = new int[3];

int[] a2 = new int[4];

int[][] a3 = new int[2][3];

String[] a4 = new String[3];

System.out.println(a1.getClass()==a2.getClass());

System.out.println(a1.getClass()==a3.getClass());

System.out.println(a1.getClass()==a4.getClass());

System.out.println(a1.getClass().getName());

System.out.println(a4.getClass().getName());

打印结果是:

True

False

False

[I

[Ljava.lang.String;

分析结果:

从这里可以看出,当同事一维数组或二维数组,并都是int类型相同,所以为true。由于字节码一定是唯一的并且字节码文件就代表这个类,所以getClass()获取的字节码文件对象是一个,地址值也是一样。

数组类型的父类Object

任何元素类型的数组他们的直接父类就一个java.lang.Object;

我们可以通过Class类里面的方法getSuperclass();返回超类

示例:

package 加强Day01.黑马;

import java.lang.reflect.*;

import java.util.Arrays;

public class ReflectArraysTest {

 

public static void main(String[] args) {

int[] a1 = new int[]{1,2,3,};

int[] a2 = new int[4];

int[][] a3 = new int[2][3];

String[] a4 = new String[]{"a","b","c"};

System.out.println(a1.getClass()==a2.getClass());

//System.out.println(a1.getClass()==a3.getClass());

//System.out.println(a1.getClass()==a4.getClass());

System.out.println(a1.getClass().getName());

System.out.println(a4.getClass().getName());

//获取超类类名

System.out.println(a1.getClass().getSuperclass());

System.out.println(a4.getClass().getSuperclass());

//判断是否成立?是否属于对应类型

Object obj1 = a1;

Object obj2 = a4;

//Object[] obj3 = a1;不能从int转Object,类型不匹配,基本数据类型不能转

Object[] obj4 = a3;//等效于数组里面装是一个数组,而该数组是属于Object的。

Object[] obj5 = a4;//String是属于Object的

System.out.println(Arrays.asList(a1));

System.out.println(Arrays.asList(a4));

 /* Arrays.asList()方法处理int[]和String[]时的差异。  

         * 打印Arrays.asList(a1);还是跟直接打印a1是一样的  

            打印Arrays.asList(a4);就会把a4的元素打印出来。  

            这是因为此方法在JDK1.4版本中,接收的Object类型的数组,  

            而a4是String类型可以作为Object数组传入。但是a1是基本数据类型不可以作为Object数组传入,所以只能按照JDK1.5版本可变参数来处理。  

            在JDK1.5版本中,传入的是一个可变参数,所以a1就被当作是一个object,也就是一个参数,  

            而不是数组传入,所以打印的结果还是跟直接打印a1一样,就无法直接显示数组中的元素。  

         */ 

}

}打印结果:

true

[I

[Ljava.lang.String;

class java.lang.Object

class java.lang.Object

[[I@1bab50a]

[a, b, c]

数组反射应用

若我们需要获取数组里面的值,或设置值,得到数组的长度,用反射怎么做?

在Class类中有Array这个类

演示数组反射的作用:

步骤:

1,先获取Class对象

2,用Class类的isArray()方法判断是否是数组

3,不是数组就直接打印,是数组就用Array类的getLength(obj)静态方法获取长度

4,for循环遍历数组并用Array类的静态方法get(obj,index)获取数组中的对应角标的元素

示例接上面:

ArrayTest(a4);

ArrayTest("abc");

}//数组的反射应用:

public static void ArrayTest(Object obj){//可以接收一堆(数组)或一个类型

Class clazz = obj.getClass();//通过元素获取Class对象

//判断是否是数组类型的字节码

if(clazz.isArray()){

int len = Array.getLength(obj);//通过Array的里的方法获取长度。

for(int x=0; x<len;x++){

System.out.println(Array.get(obj, x));

}

}

else{

System.out.println(obj);

}

}


反射的作用--》实现框架功能

框架与框架要解决的核心问题

比如;开发商建设房子给用户,然后由用户自己安装门窗大家电等;

开发商做的房子就是框架,用户使用框架,把门窗插入到框架中。

框架与工具类的区别:

框架是调用用户提供的类,工具是被用户的类调用;

框架要解决的核心问题

比如我在写框架时候,你这个用户可能还在上小学,还不会写程序,那么我写的框架程 序怎么才能调用你以后写的类(门窗)呢?

因为在写程序时无法知道要被调用的类名,所以,在程序中无法直接new某个类的实例对象了,而要用反射的方式来做Class.forName();

简单的框架程序代码实现:

分析步骤:

1, 定义配置文件config.properties;,在配置文件中定义好配置信息如:

className=java.util.ArrayList,等号左边是键,右边是值;

2, 创建文件读取流读取配置文件的信息,写出配置文件的绝对路径;

3, 通过Properties类的load方法将配置信息存储到集合中;

4, 关闭读取流;

5, 通过Properties类中的getProperty()方法获取value值即某个类名;

6, 后面步骤就可以通过反射的方式用Class类来获取实例

示例:

在源程序中不出现这个类,而通过配置文件来配,在再通过反射来获取该类的实例对象

package 加强Day01.黑马;

import java.io.FileInputStream;

import java.io.InputStream;

import java.util.Collection;

import java.util.Properties;

 

public class ReflectDemo3 

public static void main(String[] args)throws Exception{

// 创建文件读取流,读取配置文件信息

InputStream ins = new FileInputStream("config.Properties");

//将流中的数据利用集合Properties类中的load方法存储进集合

Properties pro =new Properties();

pro.load(ins);

//关闭读取流对象;

ins.close();

//通过集合getProperty(键)方法获取value 即某类名

String className = pro.getProperty("className");

//获取Class对象,通过反射方式创建实例对象;

Class clazz = Class.forName(className);

Collection coll = (Collection)clazz.newInstance();

}

}

类加载器

概述:

类加载器就是将.class文件加载进内存,也可以将普通文件中的信息加载进内存;

具体加载方式:

Java源文件(.java)在经过java编译器编译后被转换成java字节码(.class文件)。类加载器就负责读取java字节代码,并转换成java.lang.Class类的一个实例。每个这样的实例用来表示一个java类,再通过该实例的newInstance()方法就可以创建出该类的一个对象。

 

基本上所有的类加载器都是 java.lang.ClassLoader类的一个实例。

Java.lang.ClassLoader类介绍

作用:

1, 就是根据一个指定的类的名称,找到或生成其对应的字节码,然后从这些字节码中定义一个java类,即Class类实例对象。

2, ClassLoader还负责加载java应用所需的资源,比如图像文件和配置文件等,将配置文件放到.class文件目录中一同打包,类加载器就会一同加载。

加载资源文件

方法一:资源文件是由类加载器ClassLoader来加载进内存,具体就是使用getClassLoader()方法获取加载器,然后用类加载器的getResourceAsStream(String name)方法,将配置文件即资源文件加载进内存。

注意:利用类加载器来加载配置文件,需要把配置文件放置的包名一起写上,不然会报异常,这种方法只有读取功能。

方法二:在java.lang.Class类中也提供了getResourceAsStream(String name)方法来加载资源文件,这个时候是配置文件是相对类文件的当前目录,所以不用写包名。其实该方法内部也还是调用了ClassLoader的方法的。

如:Person.class.getResourceAsStream(“配置文件名”);

常用方法:

getParent();// 返回该类加载器的父类加载器。

loadClass(String name);// 加载名称为name的类,返回Class实例对象

findClass(String name);// 查找名为name的类,返回的也是Class实例。

findLoadedClass(String name);// 查找名为name的已被加载过的类。

defineClass(String name,bytr[] b,int off,int len);//把字节数组b中的类容转换成java类。

关于配置文件存放路径的疑问

根据上面示例,和配置文件的加载,我们可以了解到:

1,配置文件如果和classPath目录没有联系,那么要写上绝对路径;

2,配置文件如果和classPath目录有联系,比如是在该目录中或其子目录中,那么可以省略前面路径写文件名即可。

示例:加载资源文件

package 加强Day01.黑马;

import java.io.FileInputStream;

import java.io.InputStream;

import java.util.Collection;

import java.util.Properties;

public class ReflectClassLoader {

public static void main(String[] args) throws Exception{

// 首先创建一个Properties对象来存储配置文件

Properties pro = new Properties();

//简单的相对性路径读取流演示,即当配置文件和.java在同一目录

//InputStream ins1 = new FileInputStream("config.properties");

//类加载器能加载.class文件,那么在classPath环境变量下附带的配置文件必须也能加载,

//注意它也只能在该环境变量下加载,且使用类加载器时候不能以“/”打头;

//先将配置文件转为字节码文件对象,使用Class类中的getClassLoader返回该类的类加载器,

//再在classPath目录下逐一的查找给定名称的资源配置文件,使用方法getResourceAsStream(Name),返回InputStream

//InputStream ins2 = 

//ReflectClassLoader.class.getClassLoader().getResourceAsStream("加强Day01/黑马/config.Properties");

//另外Class类还提供了一个方法,用加载类的加载器去加载相同包目录下的文件;

InputStream ins3 = ReflectClassLoader.class.getResourceAsStream("/加强Day01/黑马/propertes/configProperties");

//将流里的数据存储到定义好的缓冲区

pro.load(ins3);

//关闭读取流对象

ins3.close();

//通过键获取值即文件类型

 String className = pro.getProperty("className");    

     Class clazz = Class.forName(className);    

    

     Collection collection = (Collection)clazz.newInstance();    

     HashCodeTest hct1=new HashCodeTest(1,2);    

     HashCodeTest hct2=new HashCodeTest(3,4);    

     HashCodeTest hct3=new HashCodeTest(1,2);    

            

    collection.add(hct1);    

    collection.add(hct2);    

    collection.add(hct3);    

    collection.add(hct1);    

    System.out.println(collection.size());

}

}

----------------------
ASP.Net+Android+IOS开发、.Net培训、期待与您交流! ----------------------
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: