反射对于大多数学者来说是一个难点,但它也是一个重点。之前,我对这个也不太理解,所以,特写一篇博客来记录我学习它的整个过程,也好自己以后好复习。
为了方便后面的理解,我通过一张图简短说下程序从编写到加载到内存的整个过程。
上面是整个加载过程,理解上面过程后,理解Class类就比较容易了。
Class类是反射的基石,可见它在反射中的重要地位,其实我个人理解嘛,整个反射过程就是直接通过操作底层的字节码来操作相应的类。在张孝祥老师视频中,是这样给他定义的:JAVA程序中各个java类都属于同一类事物,描述这些事物的java类名就是Class。说简单点,它就是java中所有类的统称,用它来描述每一个类的各种属性。其实,每一个类都有一个Class对象,每当编写并编译程序时,就会生成一个CLASS对象。正如上图所示。
当我们听过Class创建一个实例对象时,其实有两种方式,如果JVM的内存中没有所对应的字节码,它就会通过类加载器,将硬盘上的.CLASS文件加载到内存中。如果内存中有已经有了相应的字节码,则直接用该字节码复制出相应的对象。每一个实例对象是对相应类字节码的一份引用。
所有的类都是在第一次使用时,动态的加载到JVM中,当程序创建第一个对类的静态成员的引用时,就会去加载这个类,顺便说下,这种方式可以证明构造方法也是类的静态方法。类加载器在加载.class文件到内存时,会对这个文件进行相应的检查,检查它是否已经被加载到内存里,并且还会对这个文件进行验证,验证它是否被破坏了。
创建Class实例的三种方式:
1、对象.getClass()
2、类名.class
3、Class.forName()
public static void main(String[] args) throws Exception{
// TODO Auto-generated method stub
String s1="adc";
Class s2=s1.getClass();
Class s3=String.class;
Class s4=Class.forName("java.lang.String");
System.out.println(s2==s3);
System.out.println(s3==s4);//判断他们是否属于同一份字节码
System.out.println(int.class==Integer.class);//因为Integer.class是基本类型包转类,和Int不一样,所以不相同,结果为false
System.out.println(int.class==Integer.TYPE);//Integer.TYPE返回的是他所包装的基本类型的字节码,结果为true
System.out.println(int.class.isPrimitive());//判断他是否为基本类型,结果为true
System.out.println(int[].class.isArray());//判断时候为数组,结果为true
}
以上基本说明了三种方式的应用,我着重介绍后两种的区别
public class Initable {类一
static final int staticFinal=47;
static final int staticFianl2=new Random().nextInt(100);
static{
System.out.println("Initializing Initalbe");
}
public class Initable2 {类二
static int staticNonFinal=147;
static{
System.out.println("Initializing Initable2");
}
}
public class Initable3 {类三
static int staticNonFinal=74;
static{
System.out.println("Initializing Initable3");
}
}
public class ClassInitialization {
/**
* @param args
*/主函数
public static void main(String[] args) throws Exception{
// TODO Auto-generated method stub
Class initable=Initable.class;
System.out.println("After creating Initable ref");
System.out.println(Initable.staticFinal);
System.out.println(Initable.staticFianl2);
System.out.println(Initable2.staticNonFinal);
Class initable3=Class.forName("com.ade.interview.Initable3");
System.out.println("After creating Initable3 ref");
System.out.println(Initable3.staticNonFinal);
}
}
运行结果
After creating Initable ref
47
Initializing Initalbe
42
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
先说对象.getClass()这个用法不用说,它需要一个实例对象。
Class.forName(String name)这种方式,首先它是返回的一个对字节码的引用,当我们通过它获得对一份字节码的引用或者创建Class对象时,其实它有两种方式,如果JVM的内存中没有所对应的字节码,它就会通过类加载器,将硬盘上的.CLASS文件加载到内存中,并返回。如果内存中有已经有了相应的字节码,则直接返回一份字节码。
其中name必须为完整类名的引用,即必须包名+类名,否则会报异常。
此外,它是在加载字节码时,就对类中的静态成员进行初始化。
而类名.class就不一样,当通过.class来创建对象的引用时,不会自动地初始化该Class对象,它的实际工作步骤如下:
1、加载。这是由类加载器执行的,该步骤将查找的字节码(通常在classpath所指定的路径中查找)并从这些字节码中创建一个class对象
2、链接。在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必要的话,将解析这个类创建的对其他类的所有引用
3、初始化,如果该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化块。
初始化被延迟到了对静态方法或者非常数静态域进行首次引用是才执行;
注意:如果一个static final值时“编译期常量”,那么这个值不需要对Initable类进行初始化就可以被读取,但是如果不是编译期常量,则就会进行初始化。即如果一个static域而不是final的,那么在对它访问时,总是要求在它读取之前,要进行链接(为这个域分配空间)和初始化(初始化该存储空间)。
反射:引用张老师的一句话:反射就是把java类中的各种成分映射成相应的类。通过反射直接操作字节码来操作相应的方法或者字段
查看JDK文档,可以看到它的相关方法基本都是和类,接口相关的
通过反射操作构造函数
Constructor constructor=String.class.getConstructor(StringBuffer.class);//--->获取指定的构造方法
String str2=(String)constructor.newInstance(new StringBuffer("abc"));//--->通过指定的构造方法创造对象
System.out.println(str2.charAt(2));
注意我们创建实例对象时,可以通过Class.forName("xxx").newInstance(),但是这种方式是调用的默认构造函数创建的,如果要通过传入指定参数到构造函数,则可通过上面方式
通过反射操作字段
ReflectPoint p=new ReflectPoint(2,4);
Field Fieldx=p.getClass().getField("y");//获取字节码里面的成员变量,不是指某个特殊的对象
System.out.println(Fieldx.get(p));//获取某个对象的成员变量值
Field Fieldy=p.getClass().getDeclaredField("x");//获取字节码里所有声明的成员变量,不管是私有还是公有,但私有变量是可见但不可访问
Fieldy.setAccessible(true);//设置其私有成员可访问,强制性的访问
System.out.println(Fieldy.get(p));//如果返回的值为基本类型,则自动包装为对象;
changeStringvalue(p);
System.out.println(p);
public static void changeStringvalue(Object ob)throws Exception{//通过反射改变字段值
Field[] fields=ob.getClass().getFields();
for(Field field:fields){
if(field.getType()==String.class){//两者属于同一份字节码,同一份字节码的引用,所有相等
String oldValue=(String)field.get(ob);
String newValue=oldValue.replace('b', 'a');
field.set(ob, newValue);
}
}
}
通过反射操作方法
Method methodCharAt=String.class.getMethod("charAt",int.class);//利用反射调用类的方法
System.out.println(methodCharAt.invoke(s1, 2));
Method methodMain=p.getClass().getMethod("main", String[].class);//调用ReflectPoint中的main方法
System.out.println(methodMain.invoke(null,(Object)new String[]{"111","222","333","444"}));//调用静态方法时,第一个参数为null表示不指定特定对象
//如果不转化为Object类,则编译器会把字符串数组进行拆包,当为多个参数,而非当为一个字符串数组对待,所以就会出现参数不匹配异常。这样做得原因是因
//为jdk1.5可变参数(实际上封装为一个字符串数组)为了兼容jdk1.4的字符串数组。
通过反射操作数组
int[]a1=new int[]{1,2,3};//对于数组,只要类型和维数相同,则引用的是同一份字节码
int[]a2=new int[3];
String[]s5=new String[]{"111","222","333"};
String[]s8=new String[4];
int[][]a3=new int[3][];
System.out.println(a1.getClass()==a2.getClass());
// System.out.println(a1.getClass()==a3.getClass());类型不相同,维数不相同的,编译都无法通过
System.out.println(Arrays.asList(a1));//由于a1属于object类,而非Object[]类,所以,不能按照jdk1.4标准进行list化,而jkd1.5中的asList()接收的是可变参数,把a1当做一个参数处理,而int[]数组没有重写toString()方法,故只能调用Object的toString()方法
System.out.println(Arrays.asList(s5));//因为在jdk1.4asList(Object[]obj)里面所对应的对象数组,故按照jdk1.4进行数组化
System.out.println(Arrays.asList("ddd","ccc","ddc"));
printChange(s5);
printChange("acb");
//数组反射的应用
public static void printChange(Object obj){
Class cla=obj.getClass();
if(cla.isArray()){
int len=Array.getLength(obj);//得到数组的长度
for(int i=0;i<len;i++){
System.out.println(Array.get(obj, i));
}
}
else{
System.out.println(obj);
}
}
//利用反射进行数组填充
//InputStream is=new FileInputStream("config.properties");//相对于根目录路径,相对路径
InputStream is=p.getClass().getResourceAsStream("config.properties");//使用内加载器加载配置资源,相对于.class文件的路径
Properties ps=new Properties();
ps.load(is);//加载到内存中
is.close();//关闭资源防止内存泄露,这里的内存泄露指的是没有利用的资源关闭掉
String className=ps.getProperty("className");
Collection conllections=(Collection)Class.forName(className).newInstance();
ReflectPoint p1=new ReflectPoint(3,3);
ReflectPoint p2=new ReflectPoint(3,5);
ReflectPoint p3=new ReflectPoint(4,4);
//ReflectPoint p4=new ReflectPoint(4,4);
conllections.add(p1);
conllections.add(p2);
conllections.add(p3);
conllections.add(p1);
System.out.println(conllections.size());
利用反射填充非泛型指定类型的数据
ArrayList<Integer>conllection=new ArrayList();
conllection.add(1);
conllection.getClass().getMethod("add", Object.class).invoke(conllection, "abc");//注意填充的是字符串
System.out.println(conllection.get(1));
通过反射获取泛型参数的类型,由于泛型具有擦除作用,故不能直接通过反射获取其泛型参数
Method method=ReflectDemo.class.getMethod("applyVector",Vector.class);
Type[]type=method.getGenericParameterTypes();
ParameterizedType ptype=(ParameterizedType)type[0];
System.out.println(ptype.getActualTypeArguments()[0]);//得到泛型参数
System.out.println(ptype.getRawType());//得到原始类型
//通过方法反射获取相应的泛型参数类型
public static void applyVector(Vector<Date>vi){
}
以上是一些根据老师的讲课,总结的一些关于反射的应用,我自己总结,所谓反射就是直接通过操作底层的字节码完成对类的属性和方法的调用,在反射面前,类的所有私有,公有成员,都可以被调用,也就是私有和公有差不多一样。此外,反射所操作的都是字节码,即它相应的参数也是字节码形式。
希望对大家有帮助,下面介绍代理,代理也是一个难点,但由于我们要学Spring框架以及他在实际应用中很广,所以,我也专门用一篇博客记录我的整个学习过程。