您的位置:首页 > 编程语言 > Java开发

Java成长记 反射

2017-02-22 22:49 316 查看
反射是Java中的一个非常重要的概念,在之前的写代码过程中,也不止一次的听说过反射和使用反射来实现某一目的。但是还是觉得有些云里雾里,不够清晰。今天就更系统的学习了反射的相关知识,因为这是Java高级开发必须了解的,对个人掌握Java开发有很好的提高作用。

在Java中,有两样东西并不是面向对象的,一是基本数据类型,如int a = 5,但他存在包装类来弥补,二是static静态的东西,他是属于类而不是对象的。这是两个特例,实际我们在面向对象开发学习的过程中,一直存有一个观念 在面向对象的世界里,万事万物皆对象。那么问题来了,我们所写的每一个类,比如String类,User类,他们是不是对象呢?又是谁的对象?

1 类是对象

类是对象,类是java.lang.Class类的实例对象。这个对象我们称之为该类的类类型(class type)。

1.1 类类型的三种表示方法(eg:A a = new A())

已知类名A

Class c1 = A.class;

实际上在告诉我们任何一个类都有一个隐含的静态变量class

已知类A的实例对象a

CLass c2 = a.getClass();

已知类A的全称,比如com.gai.A

Class c3 = Class.forName(“com.gai.A”);

1.2 一个类只可能是Class类的一个实例对象

比如代码上输出c1==c2和c1==c3,结果都为true,可知c1,c2,c3其实是同一实例对象。

1.3 通过类类型创建该类的对象实例

意思就是通过c1或c2或c3创建A类的实例对象

A a = (A) c1.newInstance();//获取A的实例


需要注意的是,实例化成功的先决条件是类A 必须有 无参构造函数

2 动态加载类

上面所说的Class.forName(“类的全称”);不仅表示了类的类类型,还代表了动态加载类。那么是按照什么区分动态加载类和静态加载类呢?我们必须要区分编译、运行。

编译时刻加载类是静态加载类,运行时刻加载类是动态加载类

2.1 静态加载类

因为我们现在基本都使用了IDE进行开发,编译和运行工作都由IDE代劳,所以这次我们用最原始的记事本来写个简单的示例来进行区分:

示例代码:

class Office
{
public static void main(String[] args)
{
//new 创建对象 是静态加载类,在编译时刻就需要加载所有可能使用到的类
if("Word".equals(args[0]))
{
Word w = new Word();
w.start();
}
if("Excel".equals(args[0]))
{
Excel e = new Excel();
e.start();
}
}
}
class Word {
public void start() {
system.out.println("Word...start()");
}
}


运行结果:



我们在cmd黑窗口中使用javac命令进行编译,会发现程序编译报错,我们都知道原因是因为不存在Excel类,所以编译失败。可是我们也不一定会用到Excel类啊,比如我只想使用Word,那为什么还会报错呢?原因就是这个时候程序进行的是类的静态加载。new创建对象是静态加载类,在编译时刻就需要加载所有可能使用到的类。比如此时我想使用Word类,但是因为Excle类不存在,所以程序报错,Word类也无法使用。

2.2 动态加载类

现实生活中,我们希望只要Word类存在我们就可以使用Word类,当我们使用Excel类的时候再报错告诉我们Excel类不存在。这就是静态加载存在的问题,那么该如何解决这个问题,实现类用则加载,不用就不加载的目的呢?答案就是采用动态加载类。

示例代码:

class OfficeBetter
{
public static void main(String[] args)
{
try
{
//动态加载类,在运行时刻加载
Class c = Class.forName(args[0]);
//通过类类型,创建该类的对象
Word word = (Word) c.newInstance();
word.start();
//Excel excel= (Excel) c.newInstance();
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
class Word {
public void start() {
system.out.println("Word...start()");
}
}


运行结果:



从cmd中就可以发现只有输入Excel时程序才会报错,提示Excel类不存在。

2.3 进一步优化(使用接口)

因为我们可以通过类类型的newInstance()方法创建该类对象,但是我们必须要往具体的类做强制类型转换。这里又存在一个问题,如果我们往Word做转换,加载的类是Excel怎么办?如果以后再加一个PPT类怎么办?我们都知道软件开发中有一个原则,那就是对拓展开放,对修改关闭。所以我们可以给他们统一标准,只要他们都是这个标准就可以了。

代码示例:

interface OfficeAble {
public void start();
}
class OfficeBetter
{
public static void main(String[] args)
{
...
OfficeAble word = (OfficeAble) c.newInstance();
word.start();
...
}
}
class Word implement OfficeAble {
...
}
class Excel implement OfficeAble {
...
}


以后要修改的时候,不需要再重新编译之前的代码,只需要添加新有的功能和代码。比如有一个PPT类,那么我们只需要新建一个PPT类实现OfficeAble接口即可。

2.4 基本数据类型的类类型

除了类存在类类型,我们还需要知道,基本数据类型也存在类类型,即存在class隐含成员变量。void关键字也存在类类型。

示例代码:

public class ClassDemo2 {
public static void main(String[] args) {
Class c1 = int.class;//int 的类类型
Class c2 = String.class;//String的类类型
Class c3 = double.class;//double数据类型的,类类型
Class c4 = Double.class;//Doouble类的类类型,注意区分
Class c5 = void.class;

System.out.println(c1.getName());//int
System.out.println(c2.getName());//打印类的全称 java.lang.String
System.out.println(c2.getSimpleName());//不包含包名的类的全称 String
System.out.println(c5.getName());//void
}
}


3 获取方法信息

3.1 Method类

因为万物皆对象,所以方法也是对象,是Method类的对象。这里主要是通过两个方法来获取当前类的方法对象。主要的区别在于能不能加载到自己private和protected的方法和加载到继承自父类的方法。

- getMethods() 获取所有的public的函数,包括父类继承而来的

- getDeclaredMethods() 获取的是所有该类自己声明的方法,不问访问权限

示例代码:

/**
* 打印类的成员函数信息
* @param obj 该对象所属类的信息
*/
public static void printMethodMessage(Object obj) {
//要获取类的信息,首先要获取类的类类型
Class c = obj.getClass();//传递的是哪个子类的对象 c就是该子类的类类型
//获取类的名称
System.out.println("类的名称是:"+c.getName());
/*
* Method类 方法对象
* 一个成员方法就是一个Method对象
* getMethods() 获取所有的public的函数,包括父类继承而来的
* getDeclaredMethods() 获取的是所有该类自己声明的方法,不问访问权限
*/
Method[] ms = c.getMethods();//c.getDeclaredMethods()
for (int i = 0; i < ms.length; i++) {
//得到方法的返回值类型的类类型 如返回值为String,则值为String.class
Class returnType = ms[i].getReturnType();
System.out.print(returnType.getName()+" ");
//得到方法的名称
System.out.print(ms[i].getName()+" (");
//获得参数类型--->得到的是参数列表的类型的类类型
Class[] paramTypes = ms[i].getParameterTypes();
for (Class class1 : paramTypes) {
System.out.print(class1.getName()+",");
}
System.out.println(")");
}
}


运行结果:略

3.2 getClass()源码解析

或许obj.getClass()获得当前类的类类型会感觉到迷惑,因为传进来的是一个Object对象,那么他的类类型不该是Object.class么?我们查看Object的源码

public final native Class<?> getClass();


可以发现这个方法由native进行修饰,这是一个本地方法,这里涉及到Java的JNI机制,有兴趣的可以去google一下,简单的说就是native本地方法 java来声明,c语言来实现,java来调用。所以传递的是哪个子类的对象 c就是该子类的类类型。

4 获取成员变量信息

4.1 Fieled类

成员变量也是对象,java.lang.reflect包中的Fieled类封装了关于成员变量的操作。与获取成员函数的方法类似,也有自己的方法获取成员变量:

- getFields() 方法获取的是所有的public的成员变量的信息

- getDeclaredFields() 获取的是该类自己声明的成员变量的信息

示例代码:

/**
* 打印类的成员变量信息
* @param obj 该对象所属类的信息
*/
public static void printFieldMessage(Object obj) {
Class c = obj.getClass();
/*
* 成员变量也是对象
* java.lang.reflect.Filed
* Fieled类封装了关于成员变量的操作
* getFields()方法获取的是所有的public的成员变量的信息
* getDeclaredFields()获取的是该类自己声明的成员变量的信息
*/
Field[] fs = c.getDeclaredFields();
for (Field field : fs) {
//获取成员变量的修饰符
String fieldModify = Modifier.toString(field.getModifiers());
//得到成员变量的类型的类类型
Class fieldType = field.getType();
//得到成员变量类型的名字
String typeName = fieldType.getName();
//得到成员变量的名称
String fieldName = field.getName();
System.out.println(fieldModify+" "+typeName+" "+fieldName);
}
}


运行结果:略

4.2 获取修饰符

有时候我们希望能够知道成员变量带有什么修饰符,或者是否存在某个修饰符,那么我们该如何做呢?具体代码实现的上面示例中已给出,现在简要谈谈原因,(感兴趣可以去看看叉叉哥的文章:Modifier解析

Member接口

Member表示一个类中的成员,包括成员变量、方法、构造方法三种实现,上面用到的Field就是Member的一种。Member接口有个方法:

int getModifiers() 返回由此 Member所表示的成员或构造方法的修饰符的int值。

Class类

Class类中也存在一个方法:

int getModifiers() 返回此类或接口以整数编码的修饰符的int值。

Modifier类

我们需要调用需要用到java.lang.reflect.Modifier这个类,Modifier提供了很多静态方法。如public static String toString(int mod)就可以输出该整数对应的所有的修饰符。public static boolean isPublic(int mod)就可以判断该整数对应的是不是包含public修饰符。

5 获取构造函数信息

5.1 Constructor类

类的构造函数也是成员变量,java.lang.reflect.Constructor中封装了构造函数的信息。同上,存在两种获取构造函数的方法:

- getConstructors() 方法获取的是所有的public的构造函数的信息

- getDeclaredConstructors() 获取的是该类自己声明的构造函数的信息

示例代码:

/**
* 打印类的构造函数信息
* @param obj 该对象所属类的信息
*/
public static void printConstructMessage(Object obj) {
Class c = obj.getClass();
/*
* 构造函数也是对象
* java.lang.reflect.Constructor中封装了构造函数的信息
* getConstructors()方法获取的是所有的public的构造函数的信息
* getDeclaredConstructors()获取的是该类自己声明的构造函数的信息
*/
Constructor[] constructors = c.getDeclaredConstructors();
for (Constructor constructor : constructors) {
System.out.print(constructor.getName()+" (");
//获取构造函数的参数列表--->得到的是参数列表的类类型
Class[] paramTypes = constructor.getParameterTypes();
for (Class class1 : paramTypes) {
System.out.print(class1.getName()+",");
}
System.out.println(")");
}
}


运行结果:略

5.2 工具类的使用

可以把上面的三个方法放到一个ClassUtil工具类中,保存到自己的代码工具箱中。可进行重用,话说程序员都是很懒的生物,编码过程中有个原则就是“不写重复的代码,不做重复的工作”,还得努力扩充自己的工具库啊。

6 方法反射的基本操作

如何获取某个方法

方法的名称和方法的参数才能唯一决定某个方法

方法反射的操作

method.invoke(对象,参数列表)

示例代码:

public class MethodDemo1 {

public static void main(String[] args) {
try {
//获取print(int,int)方法
//1.要获取一个方法就是获取该类的信息,获取类的信息首先要获取类的类类型
A a = new A();
Class c = a.getClass();
/*
* 2.获取方法 名称和参数的类类型列表来决定
* getMethod获取的是public的方法
* getDeclaredMethod自己声明的方法
*/
//          Method m = c.getMethod("print", new Class[]{int.class,int.class});
Method m = c.getMethod("print", int.class,int.class);//...称之为可变参数

//方法的反射操作 用m对象来进行方法调用,和a.print调用的效果完全相同
//方法如果没有返回值返回null,有返回值返回具体的返回值
//          Object o = m.invoke(a, new Object[]{10,20});
Object o = m.invoke(a, 10,20);

} catch (Exception e) {
e.printStackTrace();
}

}

}

class A {
public void print(int a, int b) {
System.out.println(a+b);
}
}


运行结果:30

有兴趣深入了解的可以去看看寂静沙滩的JAVA深入研究——Method的Invoke方法

7 通过反射了解集合泛型的本质

我们在编程过程中,经常使用到的一个概念就是泛型,最常用的就是在集合的使用中,我们通过指定泛型类型来限制集合所能添加的值的类型。比如

List<String> list = new ArrayList<String>();
list.add(100);//报错


提示参数类型不匹配,不能添加。那么,到底什么是泛型呢?他是为什么能够起到数据类型的限制的呢?现在我们通过Class,Method来认识泛型的本质。

示例代码:

List<String> list = new ArrayList<String>();
list.add("hello");

List list1 = new ArrayList();

Class c1 = list.getClass();
Class c2 = list.getClass();
System.out.println(c1 == c2);//true


运行结果:true

我们都知道一个类只有一个类类型实例,上面代码中c1==c2结果返回true,说明编译之后集合的泛型是去泛型化的,也就是Java中集合的泛型,是防止错误输入的,只在编译阶段有效,绕过编译就无效了,而类类型就类似于字节码文件,因为.class就是字节码,运行时进行判断的。所以我们必须要区分编译与运行。

那么我们可不可以在list中添加非String类型的数据呢?根据上面运行结果我们可以想到,因为泛型是在编译阶段起作用,只要我们绕过了编译就饶过了泛型,就可以添加了。这里就可以用反射来进行验证。

示例代码:

try {
Method m = c1.getMethod("add", Object.class);
m.invoke(list, 100);
System.out.println(list.size());//绕过了编译就饶过了泛型
System.out.println(list);
//          for (String string : list) {
//              System.out.println(string);
//          }//现在不能这样编译,因为会进行类型判断,报错
} catch (Exception e) {
e.printStackTrace();
}


运行结果:

2

[hello, 100]

从运行结果中我们知道我们成功的绕过了泛型,但是此时我们是不能用foreach进行遍历的,因为他会进行类型验证,会报错ClassCastException。所以,我们必须要牢记一点:

反射的操作都是绕过编译,运行时执行

有兴趣的可以去看看Cedar老师的视频反射——Java高级开发必须懂的。谢谢老师!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 面向对象 反射