根据面向对象的思想,任何事物都可以抽象成类,都能用类来描述。想到java类也是一个事物,它是用什么来描述的呢?答案是Class,即类对应的字节码文件。Class是一个用来描述java类的类,它包含类的名字、成员变量、所属包、成员方法、构造方法、注解和泛型类型等信息。要得到一个java类的字节码对象,有三种方法:1.类名.class 2.对象名.getClass(); 3.Class.forName("类名"); 我们已经知道,假如有一个Person类对象p1,可以通过它提供的方法获得p1 这个人的姓名年龄等信息,如果已经有了一个类的Class对象,如何从中获取该类的成员信息呢?
这就要用到java的反射机制,反射并不是jdk5的新特性。反射就是将java类中各个成分映射成相应的java类。反射的基础就是得到类的字节码文件,方法是上面提到的三种。用Class.forName()方法得到字节码文件的过程是:先在jvm中查找,如果已经存在一份字节码则直接返回,否则会先将该类的字节码文件加载进内存然后再返回。
通过反射机制获取指定类的构造方法,字段和方法:
package enhancedDay123;
import java.lang.reflect.*;
public class ReflectTest {
/**
* @param args
* 反射就是将java类中的各种成分映射成相应的java类,它不是jdk5的新特性
* 需求:通过反射机制获得ReflectPoint类中的域变量x和y
* 并通过get和set方法设置并改变其值
* 思路:反射的前提是先得到目标类的class文件,有三种方法:
* 1.类名.class 2.对象.getClass() 3.Class.forName(类名)
* 三种方式得到的结果一样。类加载器会先在虚拟机中查找有没有指定的类文件,如果有就直接返回,否则将类文件加载到虚拟机中再返回
*/
public static void main(String[] args) throws Exception{// 这里为了简化代码,并不抛出具体的异常
// 用类加载器获得ReflectPoint的类对应的class文件
Class classFile = Class.forName("cn.it.reflectTest.ReflectPoint");
// 首先,要得到一个类对象,首先要获得其构造方法
Constructor cons = classFile.getConstructor(int.class, int.class);
ReflectPoint rp = (ReflectPoint) cons.newInstance(4, 6);// 通过构造器创建一个实例对象
// 有了对象就可以通过反射机制获取对象内部的域变量和域方法
Field fieldX = classFile.getDeclaredField("x");// 由于x是ReflectPoint的私有域,所以要用暴力反射方式获得
fieldX.setAccessible(true); // 并设置其可获得性为真,这样就可以获得并修改x的值了
Field fieldY = classFile.getField("y");
System.out.println("x:"+fieldX.get(rp)+" y:"+fieldY.get(rp));// 获取rp这个对象的x和y的值并打印
// 进一步获得ReflectPoint的设值和获取值的方法,改变rp的x和y
Method methodGetX = classFile.getMethod("getX", null);// 用来获得getX方法,参数分别为要获得的方法名和参数类型
int rpX = (int)methodGetX.invoke(rp, null);// 调用rp这个对象的getX方法获得rp的x值
// 用来获得setX方法,参数分别为要获得的方法名和参数类型
Method methodSetX = classFile.getDeclaredMethod("setX", int.class);
methodSetX.invoke(rp, 18);// 调用rp这个对象的getX方法获得rp的x值
// 重新设置了x的值后再打印一下
System.out.println("改变后:x:"+fieldX.get(rp));// 获取rp这个对象的x和y的值并打印
// 程序运行结果为: x:4 y:6 改变后:x:18
}
}
这里用到的ReflectPoint类的定义如下:
package enhancedDay123;
//这是一个用于测试反射机制的类
public class ReflectPoint {
private int x; // 注意,这里的字段x为私有的,反射的方式和y不同
public int y;
public ReflectPoint(int x, int y){ // 构造方法
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object arg0) { // 覆盖equals方法
if(!(arg0 instanceof ReflectPoint)) // 如果要比较的对象根本就不是ReflectPoint类型就直接返回false,不用再比较
return false;
ReflectPoint rp = (ReflectPoint)arg0;
return this.x == rp.x && this.y == rp.y;
}
@Override
public int hashCode() { // 覆写hashCode方法,使对象的哈希值和字段有关
return (this.x*43 + this.y);
}
// 字段的getter和setter方法
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;
}
}
值得注意的是,如果要得到一个类的私有字段,需要通过getDeclaredField的暴力反射方法获得,并通过setAccessable(true)的方式使其值可获得。
由于获得构造方法比较费时,反射设有缓存,将构造的对象缓存起来,用的时候直接拿来用就可以了,但比较浪费内存,会导致程序性能严重下降。
类加载器用来加载一个类的字节码文件,它是反射的基础。上面已经说过类加载的方法,下面通过一个例子实现用Class的getResourceAsStream方法加载配置文件信息:
package enhancedDay123;
import java.io.InputStream;
import java.util.*;
public class ClassLoaderTest {
/**
* @param args
* 类加载器
* java提供的三个基本的类加载器:
* 1,BootStrap 它不是java类,用来加载其他类。加载JRE/lib/rt.jar
* 2.ExtClassLoader 加载JRE/lib/ext/*.jar
* 3.AppClassLoader 加载CLASSPATH指定的所有jar或目录
* 类加载器委托机制:当要加载一个类时,当前加载器并不加载,而是让上级加载,接着向上传递,直到BootStrap
* 没有上级加载器,就由BootStrap加载,如果没找到就再向下一级一级找,如果到发起者也找不到就抛类找不到异
* 常。这个机制可以保证加载的类优先使用java提供的,便于集中管理,防止内存中存在多份相同的类文件。
* @throws IOException
*/
public static void main(String[] args) throws Exception {
Properties prop = new Properties(); // 获取系统属性
InputStream is = ReflectPoint.class.getResourceAsStream("config.properties"); // 通过Class提供的方法得到指定文件输入流
prop.load(is); // 加载配置文件
is.close();// 关闭输入流
String clazzName = prop.getProperty("className");// 得到配置文件中的指定键对应的值
Class clazz = Class.forName("java.util." + clazzName);// 加载字节码文件
Collection coll = (Collection)clazz.newInstance();// 创建一个实例
ReflectPoint rp1 = new ReflectPoint(4, 8);// 创建ReflectPoint的对象
ReflectPoint rp2 = new ReflectPoint(65, 8);
ReflectPoint rp3 = new ReflectPoint(4, 3);
ReflectPoint rp4 = new ReflectPoint(4, 8);// 根据ReflectPoint的hashCode和equals方法知rp1和rp4相等
coll.add(rp4);// 向集合中添加元素
coll.add(rp3);
coll.add(rp2);
coll.add(rp1);
System.out.println(coll.size());// 打印集合中元素的个数
/*
* 若配置文件中className节点配置的是ArrayList,打印4
* 若配置文件中className节点配置的是HashSet,打印3,因为hashSet集合中不能存放相同的元素
* */
}
}
Java中的内存泄露和hashCode的作用:
如果改变了参与hashCode计算的成员,则可能会造成内存泄露。
比如,往HashSet中添加一个person对象后(对象的name和age参与hashCode的计算),自动根据计算的hashCode值为把象存放到指定的区域,如果改变这个对象的name或age,计算的哈希值就与存进集合时的哈希值不同,此时即使在contains方法使用该对象的当前引用作为参数区HashSet集合中检索对象也将返回找不到对象的结果,这也会导致无法从HashSet集合中remove这个对象,从而造成内存泄露。上面这个例子中,如果修改了集合中的元素的x和y 的值,再用remove移除元素,会发现元素还存在在集合中,即已经找不到指定的对象了,造成内存泄露。
hashCode的作用:(哈希值是根据内存地址计算出来的)
哈希算法将集合分成许多区域,当一个对象要存进来时,根据其哈希值决定存放到哪个区域(用哈希值对某个数取模),hashSet表中存储的元素保证唯一性也是根据hashCode去指定区域寻找是否有相同对象,提高了查找效率。
如果两个对象的equals方法返回相等,则hashCode也要相等,只有当对象要存入按哈希算法进行存取的对象的集合时,覆盖hashCode方法才有意义。