Java 之路 (十四) -- 类型信息(RTTI、Class 对象的加载、Class 引用的获取与泛化、instanceof 关键字、反射基础与简单使用示例)

版权声明:本文出自 whdAlive 的博客,转载必须注明出处 https://blog.csdn.net/whdAlive/article/details/81708472

学习内容:

  • 传统的 RTTI
    • Class 对象的加载
    • 获取 Class 对象引用的方式
    • ClassforName
    • 类字面常量
    • Class 引用的泛化
    • instanceof 关键字
  • 反射
    • 相关类
    • 简单使用示例

前言

运行时类型信息使得我们可以在程序运行时发现和使用类型信息,Java 中运行时识别对象和类的信息有如下两种方式:

  1. 传统的 RTTI :假定我们在编译时已经知道所有的类型
    1. 传统的类型转换。
    2. 通过查询 Class 对象获取运行时所需的信息
    3. 通过 instanceof 得知对象是否是某个特定类型的实例
  2. 反射技术:允许我们运行时发现和使用类型的信息

1. RTTI

RTTI,Run-Time Type information

1.1 为什么需要 RTTI?

面向对象编程中基本的目的是:让代码只操纵对基类的引用,这样如果要添加一个新类来扩展程序,就不会影响到原来的代码。

于是我们往往会创建一个具体对象,之后将其向上转型为基类型对象,并在后面使用该基类型对象引用,但此时该对象已经丢失了其具体类型,我怎么调用具体类型的方法呢?此时 RTTI 就起作用了。RTTI 会在运行时,识别一个对象的类型,得到引用指向的对象的确切类型。再之后就是多态的事情了,针对不同类型类型执行不同代码。

1.2 Class 对象

Java 使用 Class 对象来执行其 RTTI,那么 Class 对象是什么呢?

类是程序的一部分,每一个类都有一个 Class 对象。换言之,每当编写并编译了一个新类,就会在同名的 .class 文件中产生一个 Class 对象。Class 对象包含了与类有关的信息。那创建 Class 对象具体做什么呢?答案是通过 Class 对象创建我们需要的关于这个类的所有对象(比如对象实例,或者静态变量的引用值等)。实际上,所有的类都是在对其第一次使用时,动态加载到 JVM 中的,其中,“类加载器” 会负责将 Class 对象加载到内存中,而一旦加载成功之后,他就可以用来创建这个类的所有对象。

1.2.1 Class 对象的加载

前面我们提到过,所有的类都是在对其第一次使用时,动态加载到 JVM 中的。实际上,当创建第一个对类的静态成员的引用时,就会加载这个类。这点也证明构造器是类的静态方法,虽然其没有显式指明 static,即使用 new 操作符创建类的新对象也会被当作对类的静态成员的引用。

因此,Java 程序在开始运行之前并非被完全加载,其各个部分在必需时才进行加载。

当使用该类时,类加载器首先会检查这个类的 Class 对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找 .class 文件,在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不含不良 Java 代码(这是 Java 安全防范的措施之一)。一旦某个类的 Class 对象被载入内存,那么它久被用来创建这个类的所有对象。

补充一点关于类加载器:

  1. 类加载器子系统实际上可以包含一条类加载器链,但是只有一个原生类加载器,它是Java 实现的一部分,用来加载 可信类,包括 Java API 类,通常是从本地盘中加载的。
  2. 如果有特殊需求(如以某种特殊的方式加载类,以支持 Web 服务器应用),那么可以挂接额外的类加载器,不过通常不需要添加额外的类加载器。

下面给出一个例子,证明加载的时间点:

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

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

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

public class SweetShop {
    public static void print(Object obj) {
        System.out.println(obj);
    }
    public static void main(String[] args) {  
        print("inside main");
        new Candy();
        print("After creating Candy");
        try {
            Class.forName("Gum");
        } catch(ClassNotFoundException e) {
            print("Couldn't find Gum");
        }
        print("After Class.forName(\"Gum\")");
        new Cookie();
        print("After creating Cookie");
    }
}

/*输出
inside main
Loading Candy
After creating Candy
Loading Gum
After Class.forName("Gum")
Loading Cookie
After creating Cookie

Process finished with exit code 0

*/

从结果上看,通过 new 创建 Candy 和 Cookie 的对象时,二者的 Class 对象被加载,也证明了 Class 对象仅在需要的时候才被加载。

比较特殊的一句代码:

Class.forName("Gum")

其中 forName 是 Class 类(所有 Class 对象都属于这个类)的一个 static 成员,是取得 Class 对象的引用的一种方法。此处并未使用获取返回的 Class 对象引用,它的作用在于要求 JVM 查找并加载类,加载的过程中,Gum 的 static 子句被执行,Gum 类即被加载。

Class 对象和其他对象一样,我们可以获取并操作它的引用

1.2.2 获取 Class 对象的引用

无论何时,只要我们想在运行时使用类型信息,就必须首先获得对恰当的 Class 对象的引用。本小节就来介绍一下三种获取 Class 对象引用的方式,及其要点。

1.2.2.1 Class.forName()

forName() 是取得 Class 对象的引用的一种方法。它接收一个包含目标类的文本名的 String 作为输入参数,返回一个和 Class 对象的引用。

该方法会要求 JVM 查找并加载指定的类,然后 JVM 会执行静态代码段,之后返回 Class 对象的引用

如果 Class.forName 找不到要加载的类,会配出 ClassNotFoundException 异常。

Class.forName 的优势:

  • 不需要为了获得 Class 引用而持有该类型的对象
  • 当然,如果已经持有对象,那么就可以通过 getClass() 方法来获取 Class 引用,这个方法属于跟类 Object 的一部分,返回表示该对象的实际类型的 Class 引用。
public static void main(String[] args) {
    Class c = null;
    try{
      //通过Class.forName获取Gum类的Class对象
      c = Class.forName("Gum");
      System.out.println(c.getName());
    }catch (ClassNotFoundException e){
      e.printStackTrace();
    }

    //通过实例对象获取Gum的Class对象
    Gum gum = new Gum();
    Class c2=gum.getClass();

  }

关于 Class 的其他方法,详见 https://docs.oracle.com/javase/9/docs/api/java/lang/Class.html

1.2.2.2 类字面常量

Java 还提供了另一种方法来生产对 Class 对象的引用,即使用类字面常量,如下:

Class c = Gum.class;

该方式更简单,更安全,因为编译时会受到检查,因此不需要放在 try 语句块中,同时根除了 forName() 方法的调用,所以更高效。

更加实用的是类字面常量不仅仅你可以用于普通的类,也可以应用于接口、数组以及基本数据类型。对于基本数据类型的包装其类,还有一个标准字段 TYPE。TYPE 字段是一个引用,指向对应的基本数据类型的 Class 对象,等价关系如下所示:

boolean.class  ==  Boolean.TYPE;
char.class  == Character.TYPE;
byte.class ==  Byte.TYPE;
short.class ==  Short.TYPE;
int.class ==  Integer.TYPE;
long.class ==  Long.TYPE;
float.class ==  Float.TYPE;
double.class ==  Double.TYPE;
void.class ==  Void.TYPE;

需要注意的是:此方式 .class 创建对 Class 对象得到引用是,不会自动初始化该 Class 对象,加载的过程如下:

  1. 加载:由类加载器执行。该步骤将查找字节码,并从这些字节码中创建一个 Class 对象
  2. 链接:在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必须的话,将解析这个类创建的对其它类的所有引用。
  3. 初始化:如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。

也就是说,初始化被延迟到了对静态方法或者非常数静态域进行首次引用时才执行,如下:

import java.util.*;

class Initable {
    //编译期静态常量
    static final int staticFinal = 47;
    //非编期静态常量
    static final int staticFinal2 =
            ClassInitialization.rand.nextInt(1000);

    static {
        System.out.println("Initializing Initable");
    }
}

class Initable2 {
    //静态成员变量
    static int staticNonFinal = 147;

    static {
        System.out.println("Initializing Initable2");
    }
}

class Initable3 {
    //静态成员变量
    static int staticNonFinal = 74;

    static {
        System.out.println("Initializing Initable3");
    }
}

public class ClassInitialization {
    public static Random rand = new Random(47);

    public static void main(String[] args) throws Exception {
        //字面常量获取方式获取Class对象
        Class initable = Initable.class;
        System.out.println("After creating Initable ref");
        //不触发类初始化
        System.out.println(Initable.staticFinal);
        //会触发类初始化
        System.out.println(Initable.staticFinal2);
        //会触发类初始化
        System.out.println(Initable2.staticNonFinal);
        //forName方法获取Class对象
        Class initable3 = Class.forName("Initable3");
        System.out.println("After creating Initable3 ref");
        System.out.println(Initable3.staticNonFinal);
    }
}

/*结果
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
/*

*/

如果一个 static final 值是编译期常量,就像 Initable.staticFinal,那么这个值不需要对 Initable 类进行初始化就可以被读取。但这并不是说 static final 就能保证这一点,譬如Initable.staticFinal2 就需要进行类的初始化,因为它不是编译期常量。

如果一个 static 域不是 final 的,那么访问钱,需要先进性链接(分配空间)和初始化(初始化存储空间)。

1.2.3 Class 引用的泛化

Class 引用总是指向某个 Class 对象,它可以制造类的实例,并包含可作用于这些实例的所有方法代码。因此,Class 引用表示的是其指向的对象的确切类型。

Java SE5 引入泛型后,可以通过泛型来对 Class 对象的类型进行限定使得 Class 对象类型更为具体。

public class GenericClassReferences {

    public static void main(String[] args){
        Class intClass = int.class;
        Class<Integer> genericIntClass = int.class;
        genericIntClass = Integer.class;
        //没有泛型的约束,可以随意赋值
        intClass= double.class;

        //编译期错误,无法编译通过
        //genericIntClass = double.class
    }
}

普通的类引用不会产生警告信息;而通过泛型语法,可以让编译器强制执行额外的类型检查,使得泛型类引用只能赋值为指向其声明的类型,在编译期就能发现这种错误。

如下的语句看起来是正确的,但是实际上无法工作。

Class<Number> genericNumberClass=Integer.class;

无法正常工作的原因在于 Integer Class 对象不是 Number Class 对象的子类(尽管 Integer 继承自 Number)。

为了适用泛化的 Class 引用是放松限制,适用通配符 “?”,其表示任何”事物”:

Class<?> intClass = int.class;
intClass = double.class;

上述代码是可行的,实际上Class

Class<? extends Number> bounded = Integer.class;
//赋予其他类型
bounded = double.class;
bounded = number.class;

上述代码同样可行,向 Class 添加泛型语法原因仅仅是为了提供编译期类型检查。

1.2.4 Class 的类型转换

Java SE5 中添加了用于 Class 引用的转型语法,即 cast() 方法:

class Building {}
class House extends Building {}

public class ClassCasts {
    public static void main(String[] args){
        //通过 cast()
        Building b= new House();
        Class<House> houseType = House.class;
        House h = houseType.cast(b);
        //直接强制转换
        h = (House)b;
    }
}

1.2.5 instanceof 关键字

关于 instanceof 关键字,用于判断对象是不是某个特定类型的实例,返回布尔值

if(x instanceof Dog){
    Dog dog = (Dog)x;
}

在将下转型为 Dog 之前,利用 instanceof 判断 x 是不是 Dog 类型的实例,如果返回 true 在进行类型转换。

1.2.5.1 instanceof 关键字与 isInstance 方法

isInstance 方法是 Class 类中的方法,也是用来判断对象类型的,二者效果相同。可以说二者是动态等价的。

来对比一下二者的用法:

obj.instanceof(class),判断对象是不是这种类型

  1. 一个对象是本身类的一个对象
  2. 一个对象是本身类父类(父类的父类)和接口(接口的接口)的一个对象
  3. 所有对象都是Object
  4. 凡是null有关的都是false null.instanceof(class)

class.inInstance(obj),判断对象能不能被转化为这个类

  1. 这个对象是本身类的一个对象
  2. 一个对象能被转化为本身类所继承类(父类的父类等)和实现的接口(接口的父接口)强转
  3. 所有对象都能被Object的强转
  4. 凡是null有关的都是false(class.inInstance(null))

同时对于检查 Class 对象是否相等时,”==“ 和 equals() 方法也是一样的。

例子如下:

class A {}

class B extends A {}

public class C {
    static void test(Object x) {
        print("Testing x of type " + x.getClass());
        print("x instanceof A " + (x instanceof A));
        print("x instanceof B " + (x instanceof B));
        print("A.isInstance(x) " + A.class.isInstance(x));
        print("B.isInstance(x) " +
              B.class.isInstance(x));
        print("x.getClass() == A.class " +
              (x.getClass() == A.class));
        print("x.getClass() == B.class " +
              (x.getClass() == B.class));
        print("x.getClass().equals(A.class)) " +
              (x.getClass().equals(A.class)));
        print("x.getClass().equals(B.class)) " +
              (x.getClass().equals(B.class)));
    }

    public static void main(String[] args) {
        test(new A());
        test(new B());
    }
}

/*输出
Testing x of type class com.zejian.A
x instanceof A true
x instanceof B false //父类不一定是子类的某个类型
A.isInstance(x) true
B.isInstance(x) false
x.getClass() == A.class true
x.getClass() == B.class false
x.getClass().equals(A.class)) true
x.getClass().equals(B.class)) false
---------------------------------------------
Testing x of type class com.zejian.B
x instanceof A true
x instanceof B true
A.isInstance(x) true
B.isInstance(x) true
x.getClass() == A.class false
x.getClass() == B.class true
x.getClass().equals(A.class)) false
x.getClass().equals(B.class)) true
*/

2. 反射

2.1 基础知识

在Java中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。

前面我们提到过,RTTI 有限制:所有类型必须在编译时已知,这样才能通过 RTTI 识别。换句话说,编译时,编译器必须知道所有要通过 RTTI 处理的类。

而反射也是有限制的:目标对象的类对应的 .class 文件必须可获取。

当通过反射与一个位置类型的对象打交道时,JVM 只是简单的检车这个对象,看它属于哪个特定的类(就像 RTTI 那样)。在用它做其他事情之前,必须先加载那个类的 Class 对象。

因此,RTTI 和 反射之间真正的区别只在于:

  • 对于 RTTI,编译器在编译期间打开和检查 .class 文件
  • 对于反射机制,.class 文件在编译期间是不可获取的,所以在运行时打开和检查 .class 文件。

2.2 类库的支持

Class 类与 java.lang.reflect 类库一起对反射的概念进行了支持,该类库包含了 Field、Method 以及 Constructor 类(每个类都是先了 member 接口)。这些类型的对象是由 JVM 在运行时创建的,用以表示未知类里对应的成员。

  • Class 类:反射的核心类,可以获取类的属性、方法等信息

  • Constructor 类:表示类的构造方法,利用它可以在运行时动态创建对象

  • Field 类:表示类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private)
  • Method 类:表示类的成员方法,通过它可以动态调用对象的方法(包含private)

相关 API,见 https://docs.oracle.com/javase/9/docs/api/overview-summary.html

2.3 反射的使用示例

使用反射,由三个步骤

  1. 获取 Class 对象
  2. 获取 Class 类中的信息
  3. 使用反射的相关 API 做具体操作

假定目标类如下:

package com.whdalive.reflection;

public class Person {
    private String name;
    private int age;

    public Person() {

    }
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    //getter和setter方法
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }

    public String toString(){
        return "姓名:"+name+"  年龄:"+age;
    }
}

步骤1. 获取 Class 对象

三种方式:(具体解释 ↑)

  1. Class c = e.getClass();
  2. Class c = Example.class;
  3. Class c = Class.forName("类的全路径");

示例:

Class c = Class.forName("reflection.Person");

步骤2. 获取 Class 类中的信息

package com.whdalive.reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Test {
    public static void main(String[] args) {
        showInfo();
    }
    private static void showInfo() {
        try {
            //获取Person类的Class对象
            Class clazz=Class.forName("com.whdalive.reflection.Person");

            //获取Person类的所有方法信息
            Method[] method=clazz.getDeclaredMethods();
            for(Method m:method){
                System.out.println(m.toString());
            }

            //获取Person类的所有成员属性信息
            Field[] field=clazz.getDeclaredFields();
            for(Field f:field){
                System.out.println(f.toString());
            }

            //获取Person类的所有构造方法信息
            Constructor[] constructor=clazz.getDeclaredConstructors();
            for(Constructor c:constructor){
                System.out.println(c.toString());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


/*输出

public java.lang.String com.whdalive.reflection.Person.toString()
public java.lang.String com.whdalive.reflection.Person.getName()
public void com.whdalive.reflection.Person.setName(java.lang.String)
public void com.whdalive.reflection.Person.setAge(int)
public int com.whdalive.reflection.Person.getAge()
private java.lang.String com.whdalive.reflection.Person.name
private int com.whdalive.reflection.Person.age
public com.whdalive.reflection.Person()
public com.whdalive.reflection.Person(java.lang.String,int)

*/

步骤3. 使用反射的相关 API 做具体操作

这里只展示创建一个 Person 对象,并将其初始化。

package com.whdalive.reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Test {

    public static void main(String[] args) {
        create();
    }

    private static void create() {
         try {
                //获取Person类的Class对象
                Class clazz=Class.forName("com.whdalive.reflection.Person"); 
                /**
                 * 第一种方法创建对象
                 */
                //创建对象
                Person p=(Person) clazz.newInstance();
                //设置属性
                p.setName("张三");
                p.setAge(16);
                System.out.println(p.toString());
                /**
                 * 第二种方法创建
                 */
                //获取构造方法
                Constructor c=clazz.getDeclaredConstructor(String.class,int.class);
                //创建对象并设置属性
                Person p1=(Person) c.newInstance("李四",20);
                System.out.println(p1.toString());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

/*输出
姓名:张三  年龄:16
姓名:李四  年龄:20
*/

以上即为 反射的简单介绍。也许日后我会再深入分析反射的应用场景。hhh


全文完,共勉。

猜你喜欢

转载自blog.csdn.net/whdAlive/article/details/81708472