运行时类型信息使得你可以在程序运行时发现和使用类型信息

使得你从只能在编译期执行面向类型的操作的禁锢中解脱出来,并且可以使用某些强大的程序

RTTI

RRTI:通过运行时类型信息程序能够使用基类指针或引用来检查这些指针或引用所指的对象的实际派生类型。

Class对象

要理解RTTI在Java中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为Class对象的特殊对象完成的,它包含了与类有关的信息。事实上,Class对象就是用来创建类的所有的“常规”对象的。Java使用Class对象来执行其RTTI,即使你正在执行的是类似转型这样的操作。Class类还拥有大量的使用RTTI的其他方式。

类是程序的一部分,每个类都有一个Class对象。换言之,每当编写并且编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。为了生成这个类 的对象,运行这个程序的Java虚拟机(JVM)将使用被称为“类加载器”的子系统。

类加载器子系统实际上可以包含一条类加载器链,但是只有一个原生类加载器,它是JVM实现的部分。原生类加载器加载的是所谓的可信类,包括JavaAPI类,它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,但是如果你有特殊需求(例如以某种特殊的方式加载类,以支持Web服务器应用,或者在网络中下载类),那么你有一种方式可以挂接额外的类加载器。

所有的类都是在对其第一次使用时,动态加载到JVM中的。当程序创建第-一个对类的静态成员的引用时,就会加载这个类。这个证明构造器也是类的静态方法,即使在构造器之前并没有使用static关键字。因此,使用new操作符创建类的新对象也会被当作对类的静态成员的引用。

因此,Java程序在它开始运行之前并非被完全加载,其各个部分是在必需时才加载的。这一点与许多传统语言都不同。动态加载使能的行为,在诸如C++这样的静态加载语言中是很难或者根本不可能复制的。

类加载器首先检查这个类的Class对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名查找,class文件(例如,某个附加类加载器可能会在数据库中查找字节码)。在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良Java代码(这是Java中用于安全防范目的的措施之一-)。

一旦某个类的Class对象被载人内存,它就被用来创建这个类的所有对象。下面的示范程序可以证明这一点:
class Candy{
    static {
        System.out.println("loding Candy");
    }
}
class Gum extends Candy{
    static {
        System.out.println("loding Gum");
    }
}
class Cookie{
    static {
        System.out.println("loding Cookie");
    }
}
public class SweetShop {
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
        new Candy();
        try {
            Class cs=Class.forName("reflectClass.Gum");//输入全限定名
            List<Object> list=Arrays.asList(cs.getName(),cs.getSimpleName(),
            cs.getCanonicalName(),//返回全限定类名
            cs.getSuperclass());//返回其直接基类
            cs.getInterfaces();//返回包含的接口
            Gum g=(Gum)cs.newInstance();//返回Object,必须进行转型,,创建的类必须具有默认构造器
            System.out.println(list);
            Class cs3=Gum.class;//不会自动地初始化该class对象
        }catch(ClassNotFoundException e) {
            e.printStackTrace();
        }
        new Cookie();
    }
}


这里的每个类Candy、Gum和Cookie,都有一个static子句,该子句在类第次被加载时执行。这时会有相应的信息打印出来,告诉我们这个类什么时候被加载了。在main中,创建对象的代码被置于打印语句之间,以帮助我们判断加载的时间点。

从输出中可以看到,Class对象仅在需要的时候才被加载,static初始化是在类加载时进行的。特别有趣的一行是:Class.forName("Gum");

这个方法是Class类(所有Class对象都属于这个类)的一个static成员。Class对象就和其他对象-一样,我们可以获取并操作它的引用(这也就是类加载器的工作)。forName()是取得Class对象的引用的一种方法。它是用一个包含目标类的文本名(注意拼写和大小写)的String作输入参数,返回的是一个Class对象的引用,上面的代码忽略了返回值。对forName0的调用是为了它产生的“副作用”:如果类Gum还没有被加载就加载它。在加载的过程中,Gum的static子句被执行。

在前面的例子里,如果Class.forName(找不到你要加载的类,它会抛出异常ClassNot-FoundException。这里我们只需简单报告问题,但在更严密的程序里,可能要在异常处理程序中解决这个问题。

无论何时,只要你想在运行时使用类型信息,就必须首先获得对恰当的Class对象的引用。Class.forNameO就是实现此功能的便捷途径,因为你不需要为了获得Class引用而持有该类型的对象。但是,如果你已经拥有了一个感兴趣的类型的对象,那就可以通过调用getClass0方法来获取Class引用了,这个方法属于根类Object的一部分,它将返回表示该对象的实际类型的Class引用。Class包含很多有用的方法
类字面常量 Java还提供了另一种方法来生成对Class对象的引用,即使用类字面常量。对上述程序来说,就像下面这样:

Gum.class ;
这样做不仅更简单,而且更安全,因为它在编译时就会受到检查(因此不需要置于try语句块中)。并且它根除了对forName0方法的调用,所以也更高效


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


我建议使用“.class"的形式,以保持与普通类的-致性。

注意,有一点很有趣,当使用“.class”来创建对Class对象的引用时,不会自动地初始化该Class对象。为了使用类而做的准备工作实际包含三个步骤:

1.加载,这是由类加载器执行的。该步骤将查找字节码(通常在classpath所指定的路径中查找,但这并非是必需的),并从这些字节码中创建个Class对象。

2.链接。在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用。

3.初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。

初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常数静态城进行首次引用时才执行:

class Initable{
    static final int  s1=47;
    static final int s2=ClassInitialization.rand.nextInt(1000);
    static {
        System.out.println("Initable");
    }
}

class Initable2{
    static int s3=147;
    static {
        System.out.println("Initable2");
    }
}
class Initable3{
    static int s4=74;
    static {
        System.out.println("Initable3");
    }
}


public class ClassInitialization {
    public static Random rand=new Random(47);
    
    public static void main(String[] args) throws ClassNotFoundException {
        Class d=Initable.class;//不进行初始化
        System.out.println("Initable "+Initable.s1);//因为有编译器常量,不必初始化就可以读取
        System.out.println("Initable "+Initable.s2);//强制进行初始化
        System.out.println("Initable2 "+Initable2.s3);//如果一个static域不是final,那么在对它进行访问时,要先进行链接(分配空间)和初始化(初始化存储空间)
        Class Initable =Class.forName("reflectClass.Initable3");
        System.out.println("Initable3 "+Initable3.s4);
        
    }
}


初始化有效地实现了尽可能的“惰性”。从对initable引用的创建中可以看到,仅使用.class语法来获得对类的引用不会引发初始化。但是,为了产生Class引用,Ciass.forName0立即就进行了初始化,就像在对initable3引用的创建中所看到的。

如果:个staticfinal值是“编译期常量”,就像Initable.staticFinal那样,那么这个值不需要对Initable类进行初始化就可以被读取。但是,如果只是将一个域设置为static和final的,还不足以确保这种行为,例如,对Initable.staticFinal2的访问将强制进行类的初始化,因为它不是一个编译期常量。

如果一个static域不是final的,那么在对它访问时,总是要求在它被读取之前,要先进行链接(为这个域分配存储空间)和初始化(初始化该存储空间),就像在对Initable2.staticNonFinal的访问中所看到的那样。 泛化的Class引用

Class引用总是指向某个Class对象,它可以制造类的实例,并包含可作用于这些实例的所有方法代码。它还包含该类的静态成员,因此,Class引用表示的就是它所指向的对象的确切类型,而该对象便是Class类的一一个对象。

但是,JavaSE5的设计者们看准机会,将它的类型变得更具体了一些,而这是通过允许你对Class引用所指向的Class对象的类型进行限定而实现的,这里用到了泛型语法。在下面的实例中,两种语法都是正确的:
Class intClass=int.class;
        Class<Integer> g=int.class;    
        g=Integer.class;
        intClass=double.class;


普通的类引用不会产生警告信息,你可以看到,尽管泛型类引用只能赋值为指向其声明的类型,但是普通的类引用可以被重新赋值为指向任何其他的Class对象。通过使用泛型语法,可以让编译器强制执行额外的类型检查。

如果你希望稍微放松一些这种限制,应该怎么办呢?乍一看,好像你应该能够执行类似下面这样的操作:

Class<Number>genericNumberClass=int.class;

这看起来似乎是起作用的,因为Integer继承自Number.但是它无法工作,因为IntegerClass对象不是NumberClass对象的子类(这种差异看起来可能有些诡异,我们将在第15章中深入讨论它)。

为了在使用泛化的Class引用时放松限制,我使用了通配符,它是Java泛型的一部分。通配符就是“?”,表示“任何事物”。因此,我们可以在上例的普通Class引用中添加通配符,并产生相同的结果:
Class<?> d=int.class;
d=double.class;


在JavaSE5中,Class<?>优于平凡的Class,即便它们是等价的,并且平凡的Class如你所见,不会产生编译器警告信息。Class<?>的好处是它表示你并非是碰巧或者由于疏忽,而使用了一个非具体的类引用,你就是选择了非具体的版本。

为了创建一个Class引用,它被限定为某种类型,或该类型的任何子类型,你需要将通配符与extends关键字相结合,创建一个范围。因此,与仅仅声明Class<Number>不同,现在做如下声明
Class<? extends Number> d2=int.class;
d2=double.class;
d2=Number.class;
向Class引用添加泛型语法的原因仅仅是为了提供编译期类型检查,因此如果你操作有误;稍后立即就会发现这点。在使用普通Class引用,你不会误人歧途,但是如果你确实犯了错误,那么直到运行时你才会发现它,而这显得很不方便。

下面的示例使用了泛型类语法。它存储了一个类引用,稍候又产生了一个List,填充这个List的对象是使用newInstance0方法,通过该引用生成的:

package reflectClass;

import java.util.ArrayList;
import java.util.List;

import org.junit.Test;

class CountedInteger {
    private static long counter = 1;
    private final long id = counter++;

    public CountedInteger(long counter) {
        this.counter = counter;
    }

    public CountedInteger() {
    }

    public String toString() {
        return Long.toString(id);
    }
}

public class FilledList<T> {
    private Class<T> type;

    public FilledList() {

    }

    public FilledList(Class<T> type) {
        this.type = type;
    }

    public List<T> create(int d) {
        List<T> result = new ArrayList<T>();
        try {
            for (int i = 0; i < d; i++) {
                result.add(type.newInstance());
            }
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return result;
    }

    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
        FilledList<CountedInteger> f = new FilledList<CountedInteger>(CountedInteger.class);// CountedInteger必须有默认构造器,否则,将得到异常,编译器不会产生任何警告
        System.out.println(f.create(15));
    
    }

  当你用泛型语法用于Class对象时,newinstance将返回该对象的确切类型,而不仅仅是之前的Object。但是这在某种程度是受限

Class<Number> d=Number.class;
Number n = d.newInstance();//会返回确切类型,不必进行类型转换

Class<? super Integer> n2=d.getSuperclass();
Object o=n2.newInstance();//返回Object


如果你手头的是超类,那编译器将只允许你声明超类引用是“某个类,它是FancyToy超类",就像在表达式Class<?SuperFancyToy>中所看到的,而不会接受Class<Toy>这样的声明。这看上去显得有些怪,因为getSuperClass0方法返回的是基类(不是接口),并且编译器在编译期就知道它是什么类型了一在本例中就是Toy.class而不仅仅只是“某个类,它是FancyToy超类"。不管怎样,正是由于这种含糊性,up.newInstance的返回值不是精确类型,而只是Object。

新的转型语法
javaSE添加了新的转型语法,cast Number n=new Integer(12);
Class<Integer> b=Integer.class;
Integer s=b.cast(n); s=(Integer)n;    cast方法接受参数对象,并将其转型为Class引用的类型。当然,如果你观察上面的代码,则会发现,与实现了相同功能的main中最后一行相比,这种转型好像做了很多额外的工作。新的转型语法对于无法使用普通转型的情况显得非常有用,在你编写泛型代码(你将在第15章中学习它)时,如果你存储了Class引用,并希望以后通过这个引用来执行转型,这种情况就会时有发生。这被证明是一一种罕见的情况一我发现在整个JavaSE5类库中,只有一处使用了cast (在com.sun.mirror.util,DeclarationFilter中)。

在JavaSE5中另一个没有任何用处的新特性就是Class.asSubclass,该方法允许你将一个类对象转型为更加具体的类型。.











优质内容筛选与推荐>>
1、POJ-3071 Football 概率DP
2、不要做沙和尚
3、IntelliJ IDEA 2019 快捷键终极大全,速度收藏!
4、华为云搭建windows+wordpress+xampp
5、Mac appium apk覆盖性安装的问题


长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

    阅读
    好看
    已推荐到看一看
    你的朋友可以在“发现”-“看一看”看到你认为好看的文章。
    已取消,“好看”想法已同步删除
    已推荐到看一看 和朋友分享想法
    最多200字,当前共 发送

    已发送

    朋友将在看一看看到

    确定
    分享你的想法...
    取消

    分享想法到看一看

    确定
    最多200字,当前共

    发送中

    网络异常,请稍后重试

    微信扫一扫
    关注该公众号





    联系我们

    欢迎来到TinyMind。

    关于TinyMind的内容或商务合作、网站建议,举报不良信息等均可联系我们。

    TinyMind客服邮箱:support@tinymind.net.cn