黑马程序员--反射
------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------
一、概述
1、反射机制它是Java成为动态语言的一个关键特性,在运行中的Java程序对自身进行检查或者说是自省(Introspection),并
能直接操作程序的内部属性。这个机制允许程序在执行时可以通过反射APIs取得任何一个已知名称的类的信息,并能
在执行时改变类的成员变量(fields)的内容或执行方法(methods)。
2、应用
当我们对一个已经投入使用的应用程序进行维护、升级时,通常会提供一个配置文件,来供以后实现此程序的类进行功能扩展。
对外提供配置文件,让后期出现的子类直接将类名配置到配置文件中。该应用程序直接读取配置文件中的内容,并查找和给定
名称相同的类文件。操作步骤:a 加载这个类;b 创建该类对象;c 调用该类内容。
当应用程序使用的类不明确时,通过让使用者将具体子类存储到配置文件,用反射机制获取类中内容。
优点:反射机制大大提高了程序的扩展性。
二、反射的相关对象
1、概述
反射:就是把Java类中的各种成分映射成相应的Java类。如:一个Java类中用一个Class类的对象来表示;一个类中组成部分:成员变量、方法、构造方法
、包等信息也用相应的Java类来表示。像汽车是一个类,汽车中的发动机、变速箱等也是一个个类。表示Java类的Class类显然要
提供一系列的方法,以获取其中的变量、方法、构造函数、修饰符、包等信息。这些信息就是用相应类的实例对象来表示,
它们是Filed、Method、Contructor、Package等。
简单说就是:动态获取类中信息,就是Java反射。它用来对指定名称的字节码文件进行加载并获取其中的内容并调用。
反射使程序代码能够访问装载到JVM中的类的内部信息,主要包括获取已装载类的字段、方法和构造函数的信息,允许编写处理类
的代码,这些类是在程序运行时临时确定的,而非源代码中事先选定。
2、反射的基石--Class类
2.1 所有的类文件都有共同属性,将这些类文件的共性内容封装成一个类,这个类就是Class(描述字节码文件的对象)。
Class类中包含的属性有:field(字段)、method(方法)、construction(构造函数)。
而field中有修饰符、类型、变量名等复杂的描述内容,因此也可以将字段封装成一个对象。用来获取类中field的内容,这个
对象的描述叫Field。同理方法和构造函数也被封装成对象Method、Constructor。要想对一个类进行内容的获取,必须先获取该
字节码文件的对象。该对象是Class类型。
2.2 Class类描述了哪些方面的信息呢?
包含:类的名字;类的访问属性;类所属的包名;字段名称的列表、方法名的列表等。
2.3 Person类代表人,它的实例对象就是张三、李四这样一个个具体的人,Class类代表Java类,它的实例对象对应什么?
Class类的实例对象对应的是各个类在内存中的字节码(class)。如:Person类的字节码,ArrayList类的字节码等。
一个类被加载器加载到内存中,占用一片存储空间,这个空间里面的内容就是类的字节码,不同的类的字节码不同,
所以它们在内存中的内容是不同的。这一个个的空间可分别用一个个对象来表示。同时,这些对象显然具有相同的类型---Class类型。
PS:
1 字节码(Byte-code):是一种包含执行程序、由一系列操作代码/数据对组成的二进制文件。在Java中就是class文件。
2 Class和class的区别:Class是所有Java类的总称;class是Java类的实例对象。
2.4 获取Class对象的三种方式:
a 类名.class; 如:System.class;缺点:需要明确具体的类及其对象,还要调用getClass方法。
b 对象.getClass(); 如:new Date().getClass();缺点:需求明确具体的类。
c Class.forName("类名"); 如:Class.forName("java.util.Date");
2.5 预定义的Class实例对象
2.5.1 包括:八个基本数据类型(boolean、byte、char、short、int、long、float 和 double)和关键字 void;
2.5.2 基本数据类型的字节码都可以拥与之对应的包装类中的TYPE常量表示。
如:基本数据类型int,它对应的包装类是Integer类,所以:int.class == Integer.TYPE。
2.5.3 只要是在源程序中出现的类型,都有各自的Class实例对象。如:int[].class、void.class。
数组类型的Class实例对象,可以用Class.isArray()方法判断是否为数组类型。
2.6 Class类中的常见方法:
2.6.1 static Class forName(String className):返回与给定字符串名的类、接口相关联的Class对象;
2.6.2 Class getClass():返回的是Object运行时的类;
2.6.3 Constructor getConstructor():返回此Class对象所表示的类的指定公共构造方法;
2.6.4 Field getField(String name):返回此Class对象所代表的类或接口的指定的公共成员字段;
2.6.5 Field[] getFields():返回包含类中成员字段的数组;
2.6.6 Method getMethod(String name,Class... parameterTypes):返回此Class对象所代表的类的指定公共成员方法;
2.6.7 Method[] getMethods():返回包含类中公共成员方法的数组;
2.6.8 String getName():返回此Class对象字符串形式的实体名称;
2.6.9 String getSuperclass():返回此Class对象的父类名称;
2.6.10 boolean isArray():判断此Class对象是否表示一个数组;
2.6.11 boolean isPrimitive():判断指定的Class对象是否是一个基本类型;
2.6.12 T newInstance():创建此Class对象所表示的类的一个新对象。
package cn.itheima;
public class Person
{
private String name;
public int age;
public Person()
{
System.out.println("Person is run");
}
public Person(String name,int age)
{
this.age = age;
this.name = name;
}
public String toString()
{
return name+":"+age;
}
}
class CreateClassDemo
{
public static void main(String[] args)
{
createPersonClass();
}
//通过Class对象创建类实例方法
public static void createPersonClass() throws Exception
{
//获取Person类的Class对象
String className = "cn.itheima.Person";
Class clazz = Class.forName(className);
//通过newInstance方法获取类的无参构造函数实例
Person p = (Person)clazz.newInstance();
}
}
3、Constructor类
3.1 它代表某个类中的一个构造方法。适用于类中缺少空参数构造函数或需指定构造函数的初始化。
3.2 获取构造方法:
a 获取某一个构造方法:
Constructor constructor =
Class.forName("java.lang.String").getConstructor(String.class,int.class);
b 获取类中所有构造方法:
Constructor[] constructors =
Class.forName("java.lang.String").getConstructors();
c 创建实例对象:
一般方式:Person p = new Person("lisi",30);
反射方式:Person p = (Person)constructor.newInstance("lisi",30);
PS:
1 创建实例时newInstance方法中的参数列表必须与获取构造方法中的getConstructor的参数列表一致;
2 newInstance():构造一个实例对象,每调用一次就构造一个对象;
3 利用Constructor类来创建类实例的优点:可指定构造函数;而Class类只能利用无参构造函数创建类实例对象;
//通过Constructor对象创建类实例
public static void createPersonClass_2() throws Exception
{
String className = "cn.itheima.Person";
Class clazz = Class.forName(className);
//获取指定构造函数的类实例
Constructor con = clazz.getConstructor(String.class,int.class);
Person p = (Person)con.newInstance("lisi",30);
System.out.println(p.toString);
}
4、Field类
4.1 Field类:代表某个类中的一个成员变量。
4.2 常见方法:
Field getField(String s):只能获取公有和父类中公有成员变量;
Field getDeclaredField(String s):获取该类中任意成员变量,包括私有;
setAccessible(ture):暴力访问:如果是私有字段,要先将该私有字段进行取消权限检查的能力;
set(Object obj, Object value):将指定对象变量上此Field对象表示的字段设置为指定的新值;
Object get(Object obj):返回指定对象上Field表示的字段的值。
public static void getPersonField() throws Exception
{
//给变量赋值必须先有对象
Class clazz = Class.forName("cn.itheima.Person");
Person p = (Person)clazz.newInstance();
//获取所有的成员变量
Field[] fs = clazz.getFields();
for(Field f : fs)
{
System.out.println(f);
}
//获取指定的成员变量
Field fa = clazz.getField("age");
Field fn = clazz.getDeclaredField("name");
//显示改变后的值
fa.set(p,20);
System.out.println(fa.get(p));
//暴力访问
fn.setAccessible(true);
fn.set(p,"zhangsan");
System.out.println(fn.get(p));
}
5、Method类
Method类:代表某个类中的一个成员方法。
5.1 特点:专家模式:谁调用这个数据,谁就是调用它的专家。
总结:变量使用方法:方法本身知道如何实现执行的过程,即"方法对象"调用方法,才执行了方法的各个细节。
5.2 常见方法:
Method[] getMethods():只获取公共和父类中的方法。
Method[] getDeclaredMethods():获取本类中包含私有。
Method getMethod("方法名",参数.class(如果是空参可以写null));
Object invoke(Object obj ,参数):调用方法。如果方法是静态,invoke方法中的对象参数可以为null。
如:
一般方式:System.out.println(str.charAt(1));
反射方式:System.out.println(charAt.invoke(str,1));若第一个参数为null,代表Method方法对应的是一个静态方法。
//获取Person类中的方法
public static void getPersonMethod() throws Exception
{
Class clazz = Class.forName("cn.itheima.Person");
Person p = (Person)clazz.newInstance();
//获取单个方法
Method me = clazz.getMethod("toString",null);
Object returnValue = me.invoke(p,null);
System.out.println(returnValue);
//获取所有方法
Method[] mes = clazz.getMethods();
for(Method me : mes)
{
System.out.println(me);
}
}
5.3 jdk1.4和jdk1.5的invoke方法的区别:
jdk1.5:public Object invoke(Object obj,Object...args)
jdk1.4:public Object invoke(Object obj,Object...[] args),
即按jdk1.4的语法,需要将一个数组作为参数传递给invoke方法时,数组中的每个元素分别对应被调用方法中的一个参数,
所以,调用charAt方法的代码也可以用jdk1.4改写为:charAt.invoke("str",new Object[]{1})形式。
5.4 用反射方式执行类中的main方法:
5.4.1 写一个能够根据用户提供的类名,去执行该类中的main方法的程序。可以用普通方式和反射方式,为什么要用反射去调用呢?
答:一般方式调用方法必须有明确的对象;反射不用。
5.4.2 启动Java程序的main方法的参数是一个字符串数组,即public static void main(String[] args),通过反射方式来调用
这个main方法时,如何为invoke方法传递参数呢?
答:按jdk1.5的语法,整个数组是一个参数,而按jdk1.4的语法,数组中的每个元素对应一个参数,当把一个字符串数组作为参数
传递给invoke方法时,Jdk1.5因为要兼容jdk1.4,会按jdk1.4的语法进行处理,即把数组打散成为若干个单独的参数。所以,在给
main方法传递参数时,不能使用代码:mainMethod.invoke(null,new String[]{"xxx"}),javac只能把它当作jdk1.4的语法进行理解,
因此会出现参数类型不对的问题。
解决办法:
mainMethod.invoke(null,new Object[]{new String[]{"xxx"});
mainMethod.invoke(null,(Object)new String[]{"xxx"});,此时编译器会做特殊处理,
编译时不把参数当作数组看待,也就不会将数组打散成若干参数了。
5.6 数组的反射
5.6.1 数组的反射:具有相同维数和元素类型的数组属于同一个类型,即具有相同的Class实例对象。
5.6.2 代表数组的Class实例对象的getSuperClass()方法返回的父类为Object类对应的Class。
5.6.3 Object与String有父子关系;Object[]与String[]没有父子关系;所以Object x ="abc";能
强制转换成String x ="abc";new Object[]{"aaa","bb"}不能强制转换成new String[]{"aaa","bb"};
5.6.4 如何得到某个数组中的某个元素的类型,
例:
int a = new int[3];//我想通过反射知道a的具体类型,这个无法办到。
Object[] obj=new Object[]{"ABC",1};//因为反射后为Object[],它的类型是任意的。
无法得到某个数组的具体类型,只能得到其中某个元素的类型,
如:
Obj[0].getClass().getName();//得到的是java.lang.String。
5.6.5 Array工具类用于完成对数组的反射操作。
Array.getLength(Object obj):获取数组的长度;
Array.get(Object obj,int x):获取数组中的元素;
5.6.6 基本类型的一维数组可以被当作Object类型使用,不能当作Object[]类型使用;
非基本类型的一维数组,既可以当做Object类型使用,又可以当做Object[]类型使用。
5.7 HashCode的分析
5.7.1 HashCode():一种元素查找方式。它将集合分为若干存储区域,每个对象可以计算出一个哈希码,可将哈希码分组,每组
分别对应某个存储区域,根据一个对象的哈希码就可以确定该对象应该存储的区域。
5.7.2 HashSet类:采用哈希算法存取对象的集合。它内部采用对某个数字n进行取余的方式,对哈希码进行分组和划分对象的存储区域。
Object类中定义了一个hashCode()方法来返回每个Java对象的哈希码,当从HashSet集合中查找某个对象时,Java系统首先
调用对象的hashCode()方法获得该对象的哈希码,然后根据哈希码找到对应的存储区域,最后取出该存储区域内的每个元素
与该对象进行equals方法比较,这样不用遍历集合中的所有元素就可以得到结论。因此HashSet类有较好的检索功能,但是其
存储效率较低,因为要通过分区存储。
提示:1,通常一个类的两个实例对象用equals()方法比较的结果相等时,他们的哈希码也必须相等,但反之则不成立。
即:equals方法比较结果不相等的对象可以有相同的哈希码,或哈希码相同的两个对象equals方法比较的结果可以不等。
2,当一个对象被存储进HashSet集合中后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值
与最初存储进HashSet集合中的哈希值就不相同了。这时,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合
中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,从而造成内存泄露。
package cn.itheima;
public class ReflectPoint {
private int x;
public int y;
public ReflectPoint(int x, int y) {
super();
this.x = x;
this.y = y;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ReflectPoint other = (ReflectPoint) obj;
if (x != other.x)
return false;
if (y != other.y)
return false;
return true;
}
}
三、反射的作用
反射的作用--->实现框架功能1、框架与框架要解决的核心问题
我做房子卖个用户,用户自己安装门窗和空调,我做的房子就是框架,用户需要使用我的框架,把门窗插入我提供的框架中。
框架与工具类有区别:工具类被用户的类调用,而框架则是调用用户提供的类。
2、框架要解决的核心问题
因为写框架时无法知道要被调用的类名,在程序中无法直接new某个类的实例对象,所以,要借助反射。
3、应用
package cn.itheima;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Properties;
public class ReflectTest2 {
public static void main(String[] args)throws Exception
{
//导入未知文件,这个文件由用户自己定义
InputStream ips = new FileInputStream("config.properties");
Properties props = new Properties();
props.load(ips);
ips.close();//关闭流对象,对象ips由虚拟机关闭。
//获取类名
String className = props.getProperty("className");
Collection collections = (Collection)Class.forName(className).newInstance();
//Collection collections = new ArrayList();//可重复
//Collection collections = new HashSet();//不可重复
ReflectPoint pt1 = new ReflectPoint(3,3);
ReflectPoint pt2 = new ReflectPoint(5,4);
//pt1和pt3有两个独立的内存地址,哈希值不同。但当equals覆盖后,哈希值相同。
ReflectPoint pt3 = new ReflectPoint(3,3);
collections.add(pt1);
collections.add(pt2);
collections.add(pt3);
collections.add(pt1);
//pt1.y = 7;
//collections.remove(pt1);//此时的哈希值就不是原始值,所以删除不了原来的pt1.
System.out.println(collections.size());
}
}