快乐学习
前程无忧、中华英才非你莫属!

《Thinking in Java》_类型信息与反射机制

前面学习的多态给了我们一个很好的承诺:我们编写的代码只要与基类打交道,而不用为每一个新增加的子类写一份代码.但是这种思想在我们想要访问子类自己定义的方法时,就会有问题了.如下面的代码所示:

class Base1{
    void f(){
        System.out.println("Base.f()");
    }
}

class Sub extends Base1{
    void f(){
        System.out.println("Sub.f()");
    }
    void g(){
        System.out.println("Sub.g()");
    }
}

public class Test1 {
    public static void main(String[] args){
        Base1 b = new Sub();
        b.f();
        //b.g();//想要调用Sub类的g()函数,但是编译错误
       ((Sub)b).g(); ///强制向下转型后就可以调用了(注意因为"."优先级很低,必须加双括号)
    }/*Output
        Sub.f()
        Sub.g()
    */
}

在这里我们通过一个强制的向下转型来实现了正确的方法调用,但是问题在于:我们不能总是保证这种转型是正确的.所以我们需要一种机制来保证我们实行的是正确的向下转型.Java提供了这种机制,它有两种表现形式,一是被称为RTTI的静态检测,另一种是称为反射的动态获取一个类信息的形式.下面就是对这两种方式的总结.

一.从Class类说起
Java程序在运行时,Java运行时系统一直对所有的对象进行所谓的运行时类型标识。这项信息纪录了每个对象所属的类。虚拟机通常使用运行时类型信息选准正确方法去执行,用来保存这些类型信息的类是Class类。 虚拟机为每种类型管理一个独一无二的Class对象。
也就是说,每个类(型)都有一个唯一的Class对象.换而言之,每当编写并且编译了一个新类,就会产生一个Class对象(被保存在与类同名的.class文件中)运行程序时,Java虚拟机(JVM)首先检查是否所要加载的类对应的Class对象是否已经加载。如果没有加载,JVM就会根据类名查找.class文件,并将其Class对象载入。
但是Class类是没有构造器的,所以我们就不能显式的声明一个Class对象,而是要通过类和对象来得到其对应的Class对象.下面的代码介绍了三种获得Class对象的方式:

Class T1{}
public class Test4 {

    public static void main(String[] args) 
    {
          Class<?> t1=null,t2,t3;
          try{
              ///第一种方式: 通过Class类的静态方法forName()
              //传入一个代表类名的字符串(有可能还要包名),得到对应的Class对象
              t1=Class.forName("lkl.T1");
          }
          catch(ClassNotFoundException e){ //这种方式有可能出现ClassNotFoundException
              System.out.println("Class Not found");
          }

          ///第二种方式: 通过类的对象的getClass()方法得的对应的Class对象
          T1 t=new T1();
          t2=t.getClass();

          ///第三种方式: 通过类字面常量.class获得
          t3=T1.class;

          ///下面调用Class的getName()方法证明我们上面三种方式得到
          ///的Class对象都是同一个类T1的Class对象
          System.out.println(t1.getName());
          System.out.println(t2.getName());
          System.out.println(t3.getName());
    }/*Output
       lkl.T1
       lkl.T1
       lkl.T1
    */
}

但是上面的三种方式并不是完全等价的.首先我们先来了解一下java中的类加载的过程.java中为了使用一个类而做的准备工作实际上包括三个步骤:
(1).加载,这是由类加载器执行的.该步骤将查找字节码,并从这些字节码中创建一个Class对象.
(2).链接,在链接阶段将验证类中的字节码,为静态域分配空间,如果必要的话,得解析这个类创建的对其类的引用.
(3).初始化,如果该类有超类,则对其初始化,执行静态初始化器和静态初始化块.
主要的不同在与方式1(forName()方式)和方式3(.class方式)对初始化的处理.方式1中在加载这个类的时候就会同时进行初始化,而方式3将初始化延迟到了对静态方法(构造器其实也是静态的)或则非常数静态域进行首次引用时才执行.下面的代码说明了这一点:

///下面的静态初始化块用来验证初始化是否进行
class Candy{
    public static final int a=100;
    public static int b=111;
    static {System.out.println("Loading Candy");};
}

 class Gum{
    static {System.out.println("Loading Gum");};
}

 class Cookie{

 }
public class Test{

    public static void main(String[] args){
        System.out.println("inside main");
        Class<?> cc =Candy.class; ///使用.class方法获得Class对象时,不会对类进行初始化

        try{
            ///通过forName()静态方法进行加载会初始化
            Class.forName("lkl.Gum");///前面不加包名就会抛出异常
        }
        catch(ClassNotFoundException e){
            e.printStackTrace();
        }

        System.out.println("final static :  "+Candy.a); ///对常数静态域的调用仍不会引发初始化

        System.out.println("static:  "+Candy.b);///调用非常数静态域会引发初始化操作(初始化在调用之前发生)
    }
}/*
   inside main
   Loading Gum
   final static :  100
   Loading Candy
   static:  111
*/

Class类的重要性在于每个类对应的Class对象都是唯一的,Class对象中包含了很多这个类的信息.所以下面我们将看到无论在RTTI还是反射,Class都起到了核心的作用.

二.Java的RTTI
RTTI(Run-Time Type Identification,通过运行时类型识别)的含义就是在运行时识别一个对象的类型.运行时类型信息可以使得你在程序运行时发现和使用类型信息.通俗的来讲就是我们能够在程序运行时判断一个对象是属于那个类的,这样的话就可以解决我们开头提出的那个问题了.下面就来总结一下常用的RTTI的方式:
(1).直接进行类型转换,由编译器保证类型转换的正确性,如果执行了一个错误的转换就抛出一个ClassCastException异常.就像这样:((Sub)b).g();

(2).使用instanceof. instanceof是java的保留关键字之一,也是一个双目运算符,其调用形式是:a instanceof b,用于判断a是不是b的对象,如果是则返回true,否则返回false.要注意instanceof只能对有继承关系的类进行判定.这样的话我们上面开头的那段代码就可以改成下面这种更安全的形式了:

    if(b instanceof Sub){
            ((Sub)b).g();
        }

(3)直接使用Class对象进行判断.因为每个类/对象对应的Class对象都是唯一的,所以我们可以使用Class对象作为中介进行判断.但是这种判断是可能会出错的.如下面的代码所示:

import java.util.*;

class T1{
}

 class T2{
}

 class T3 extends T1{

 }
public class Test4{

    public static void main(String[] args){
         T1 t1= new T1();
         T2 t2= new T2();
         T3 t3= new T3();

         System.out.println("t1 是 T1的对象? "+(T1.class.equals(t1.getClass())));

         System.out.println("t2 是 T1的对象? "+(T1.class.equals(t2.getClass())));

         System.out.println("t3 是 T1的对象? "+(T1.class.equals(t3.getClass())));
         System.out.println("t1 是 T3的对象?  "+(T3.class.equals(t1.getClass())));
    }/*Output
      t1 是 T1的对象? true
      t2 是 T1的对象? false
      t3 是 T1的对象? false
      t1 是 T3的对象?  false
    */
}

(4)使用Class类提供的isInstance()方法.这种方式类似于instanceof,但是由于可以以函数形式调用,就可以使用的更加灵活(不用将代码组织成一大堆的instanceof).下面的代码示范了它的用法:

import java.util.*;

class T1{
}

 class T2{
}

 class T3 extends T1{

 }
public class Test2{

    public static void main(String[] args){
         T1 t1= new T1();
         T2 t2= new T2();
         T3 t3= new T3();
       ///先取得该类对应的Class对象,然后以需要检测的对象作为参数传入isInstance()方法
         System.out.println("t1 是 T1的对象? "+(T1.class.isInstance(t1)));

         System.out.println("t2 是 T1的对象? "+(T1.class.isInstance(t2)));

         System.out.println("t3 是 T1的对象? "+(T1.class.isInstance(t3)));
         System.out.println("t1 是 T3的对象?  "+(T3.class.isInstance(t1)));
    }/*Output
       t1 是 T1的对象? true
       t2 是 T1的对象? false
       t3 是 T1的对象? true
       t1 是 T3的对象?  false
    */
}

三.Java的反射机制
上面的RTTI的方式虽然很有用,但是都有一个很大的缺陷:我们必须在编译时就知道我们所要使用的类型,这样才可以进行检测.就是说其实无论是那种方式都是在一个范围类进行穷举.但是有时候我们可能会从别的地方得到一个对象,然后这个对象对应的类(.class文件)在编译时是不能知道的,这时候我们要想了解这个对象对应类的信息上面的方式就失效了.这时候我们就需要用到java的反射了.
java的反射是不是一个很难理解的东西?不是这样的.当我们通过反射与一个未知类型的对象打交道时,JVM只是简单的检查这个对象,看看它属于那个特定的类(就像RTTI做的那样).在用它做其它事情之前必须要先加载那个类的Class对象.因此,那个类的.class文件对于JVM必须是可以获得的:要么在本地机器上,要么可以通过网络获得.所以RTTI与反射之间的真正区别只在于,对RTTI来说,编译器在编译期间就打开和检查.class文件,而对于反射机制来说,.class文件在编译期间是不可获取的,只能在运行的时候打开和检查.class文件.
所以首先无论是RTTI还是反射.class文件都是很重要的,然后我们要知道的当反射机制得到了.class文件后,就可以做很多的事情了并不是只能简单的判断一个未知对象属于那个类而已.
Class类和java.lang.reflect.*类库一起对反射的概念进行了支持,该类库包含了Field(对应类的变量),Mechod(对应类的方法)以及Constructor(对应类的构造器)类.这些类型的对象是由JVM在运行时创建的,用以表示未知类中对应的成员.这些类有很多有用的方法,可以通过API文档进行查阅.在下面的代码中我们会与它们打交道.
下面通过具体的例子,来介绍反射机制的一些用法.

(1).通过反射来创建类的对象.
通过反射来创建对象有以下以下两种方式:
1).使用Class对象的newInstance()方法来创建该Class对象对应的实例,这种方式要求该Class对象对应的类有默认构造器,而执行 newInstance()方法实际上是利用默认构造器来创建该类的实例,所以也就需要对应类要声明一个默认的构造器.如下面的代码所示:

import java.util.*;
import java.io.*;

///使用Class对象创建对应类的实例: 方式1
public class ObjectPool {

    ///定义一个对象池,前面是对象名,后面是实际对象
    private Map<String,Object> objectPool = new HashMap<>();

    ///根据传入的字符串类名,创建一个对象
    private Object createObject(String name) throws InstantiationException,ClassNotFoundException,IllegalAccessException
    {
          Class<?> clazz= Class.forName(name);
          return clazz.newInstance();
    }

    //根据指定文件来初始化对象池
    public void initPoo(String fileName) throws InstantiationException,ClassNotFoundException,IllegalAccessException
    {
        try( FileInputStream fis = new FileInputStream(fileName)){

            Properties props = new Properties();
            props.load(fis);
            //每取出一对key-value对,就根据value创建一个对象
            for(String name:props.stringPropertyNames()){
                objectPool.put(name, createObject(props.getProperty(name)));
            }
        }
        catch(IOException e){
            e.printStackTrace();
        }
    }

    public Object getObject(String name){
        return objectPool.get(name);
    }

    public static void main(String[] args) throws Exception 
    {
        ObjectPool po =new ObjectPool();
        po.initPoo("/Test/obj.txt");
        System.out.println(po.getObject("a"));
        System.out.println(po.getObject("b"));
    }
}

2).先使用Class对象获取指定的Constructor对象然后在调用Constructor对象的newInstance()方式来创建对应类的实例.通过这种方式就可以调用有参数的构造器.如下面的方式:

import java.lang.reflect.*;

//根据Class对象创建对应类的实例: 方式2
public class CreateJrame {

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

        //获取JFrame对应的Class对象
        Class<?> jframeClazz = Class.forName("javax.swing.JFrame");

        //获取JFrame中带一个字符串参数的构造器
        Constructor<?> ctor = jframeClazz.getConstructor(String.class);

        //调用Constructor的newInstance方法创建对象
        Object obj= ctor.newInstance("测试窗口");
        System.out.println(obj);
    }
}

下面会多次重复利用下面的一段代码,这里先贴出来,后面就只贴主类的代码了.

interface Person{
    public final static String name="lkl";
    public final static int age=20;
    public void sayHello(String name);
    public void sayHello(); 
}

class Student implements Person{
    private String sex="boy";
    public Student (String sex){
        this.sex=sex;
    }
    public Student(){
    }

    @Override
    public void sayHello(String name){
        System.out.println("hello! "+name+" I'm a "+sex);
    }
    @Override
    public void sayHello(){
        System.out.println("Hello! everyone! "+"I'm a "+sex);
    }

    public String getSex(){
        return sex;
    }

   private void setSex(String s){
        sex=s;
    }
}

(2).通过反射调用方法.
当获取某个类对应的Class对象后,就可以通过该Class对象的getMthods()方法或者getMethod()方法来获取所有方法或者是指定方法—前一个返回所有的方法.后一个返回指定的方法.这样每个Method对谐对应一个方法,然后就可以通过Method中的invoke()方法来调用该方法了.该方法的签名: Object invoke(Object obj,Object …args).其中obj是一个对象,args是执行该方法传入的参数.下面的代码中还示范了一些其它方法的使用:

import java.lang.reflect.*;

///通过反射调用其它类的方法,包括private方法
///基本步骤:1.获取对应的Class对象 2.调用getMethod()得到对应的方法  3.使用invoke()进行调用
//值得注意的是:getMethod()和getDeclaredMethod()方法的区别

public class Hello1 {
    public static void main(String[] args)  
    {
        try{
           Class<?> student = Class.forName("lkl.Student");

           ///我们通过方法名和参数列表得到一个Method对象,然后通过invoke()方法传入一个
           ///对象进行调用.值得注意的是这里传入的参数,这里之所以要传入参数是因为有的方法
           ///进行了重载仅凭方法名还不足以确定是哪一个方法,所以要传入形参辅助判断.但是
           ///这里的形参和普通函数的形参又是不一样的,它不会得到使用,所以我们传入其对应的Class对象

           //调用student的sayHello(String name)方法
           Method met =student.getMethod("sayHello",String.class);
           ///通过向invoke()中传入一个方法调用者,调用方法
           met.invoke(student.newInstance(),"Tom");

           ///调用student的无参的sayHello()方法
           Method met1 =student.getMethod("sayHello");
           met1.invoke(student.newInstance());

           ///通过反射调用Student的set,get()方法

           ///调用student的setSex(String sex)方法,必须通过下面的方法才能取得私有的方法
           Method met2 =student.getDeclaredMethod("setSex",String.class);

           ///利用Class对象创建一个Student对象,不过返回的是Object类型,需要转型
           Student stu=(Student)(student.newInstance());

           ///我们将setSex()设为了private,则在这里不能直接访问了
           ///但是我们可以通过Method的setAccessible()方法来改变它的访问权限
           met2.setAccessible(true); ///将setSex()设为可以访问,否则下一句就会出错
           met2.invoke(stu, "girl");

           //调用上面生成的stu对象的getSex()方法
           Method met3=student.getMethod("getSex");
           System.out.println(met3.invoke(stu));
    }
     catch(Exception e){
         e.printStackTrace();
     }
    }
}

(3).使用反射访问属性值
通过Class对象的getFields()或getField()方法可以获得该类全部或指定的Field.Filed提供了以下的两组方法来读取或设置Field值:
getXxx(Object obj): 获取obj对象该Field的属性值.此处的Xxx对应8个基本类型,如果该属性是引用类型,就不需要Xxx.
setXxx(Object obj,Xxx val): 将value对象的该Field设置为val值.Xxx的解释如上.
下面的代码示范了这些知识点:

import java.lang.reflect.*;

///通过反射访问属性值,可以访问private变量
///基本步骤: 1.获取Class对象 2.使用Class的getField()方法获取该Class变量对应的Field对象
///3.通过该Field对象的set(),get()方法操作这个变量
class Person1{
    private String name;
    private int age;
    @Override
    public String toString(){
        return "Person[name: "+name+" , age: "+age+"]";
    }
}

public class Test3 {

    public static void main(String[] args)throws Exception
    {
            Person1 p =new Person1();
           Class<?> person= p.getClass();

           ///获取Person名为name的Filed
           ///使用getDeclaredFiled,表明可以获得给中访问控制的filed
           Field nameFiled = person.getDeclaredField("name");

            //设置通过反射访问该Filed时取消访问权限检查
           nameFiled.setAccessible(true);

           //调用set方法为p对象的name field设置值
           nameFiled.set(p, "lkl");

           Field ageFiled=person.getDeclaredField("age");

           ageFiled.setAccessible(true);

           ageFiled.set(p, 21);

           ///打印出修改效果
           System.out.println(p);

    }
}

(5).通过反射获取获取当前类继承的接口和基类

interface Person{
    public final static String name="lkl";
    public final static int age=20;
    public void sayHello(String name);
    public void sayHello(); 
}

class Student implements Person{
    private String sex="boy";
    public Student (String sex){
        this.sex=sex;
    }
    public Student(){
    }

    @Override
    public void sayHello(String name){
        System.out.println("hello! "+name+" I'm a "+sex);
    }
    @Override
    public void sayHello(){
        System.out.println("Hello! everyone! "+"I'm a "+sex);
    }

    public String getSex(){
        return sex;
    }

   private void setSex(String s){
        sex=s;
    }
}

public class Hello {

    public static void main(String[] args) throws Exception
    {///下面的两个函数返回都是Class对象
            Class<?> student =Class.forName("lkl.Student");
            ///获取student对应类的父类,这里默认是Object
            System.out.println(student.getSuperclass().getName());

            //获取student对应类实现的接口,返回的是Class对象数组
           Class<?> intes[] =student.getInterfaces();
           for(Class<?> c : intes){
               System.out.println(c.getName());
           }
    }
}

(6).通过反射操作数组
在java.lang.reflect包下还提够了一个Array类,Array对象可以代表所有的数组.程序可以通过使用Array来动态创建数组,操作数组元素等.主要是三个方法:
static Object newInstance()创建一个指定类型长度的数组
static xxx getXxx(Object arr,int index) 获取数组arr第index个元素
static void setXxx(Object arr,int index,int val) 将数组的第index个元素的值设为val.
对于上面的Xxx,表示基本的数据类型,如果是引用类型则不需要Xxx部分.
如下面代码所示:

package lkl;
import java.lang.reflect.*;

///通过反射创建数组
public class ArrayTest {

    public static void main(String[] args){
        try{
            ///创建一个元素类型为String,长度为10的数组
            Object arr = Array.newInstance(String.class, 10);
            Array.set(arr, 5, "java");
            Array.set(arr, 7, "C++");
            System.out.println(Array.get(arr, 5));
            System.out.println(Array.get(arr, 7));
        }
        catch(Exception ex){
            ex.printStackTrace();
        }
    }
}
打赏
赞(0) 打赏
未经允许不得转载:同乐学堂 » 《Thinking in Java》_类型信息与反射机制

特别的技术,给特别的你!

联系QQ:1071235258QQ群:710045715

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫打赏

微信扫一扫打赏

error: Sorry,暂时内容不可复制!