Java——Class类型信息

学习初衷:

  • 想知道程序在运行的时候,如何识别对象和类的信息。
  • 在基本的框架中随处都可见有使用 Class 对象,弄清楚作用
  • 如果通过反射去调用类中的属性、方法、构造函数等

写在前头

如果看过一些成熟的框架,大家都应该知道 泛型、反射、注解等这些 Java 高级特性是无处不在的,多数都是使用这些特性让程序复用性更高更简洁。如果想要看懂一些成熟的框架源码,这些就是基础。

Java 是如何让我们在运行时识别对象和类信息的呢?

  • RTTI,它假定我们在编译时已经知道了所有类型
  • 反射机制

一、RTTI 和反射

每个认真读过 B 大的《Thinking in Java》都会有这么一个疑问:RTTI 是什么鬼东西?Java 官方文档中,都没见过这个词

先看一下知乎大佬们的回答呗。Java RTTI和反射的区别?

胖大解析得很清晰了。

所以 RTTI 和反射是两个不同体系描述同一件事情。RTTI 对应着 C++ 体系,反射对应 OO 面向对象体系。只不过两者用的是不同方式实现。但是相对而言,Java 的反射功能比 C++ 的 RTTI更为完善一点。

另外还提一下 Java 的反射机制的常用功能:

  • 能够在运行时获取对象的类型信息
  • 动态加载类
  • 动态访问及调用目标类型的字段(属性)、方法及其构造函数

二、Class 对象

类型信息在运行时是如何表示的?这项工作是由一个叫 Class 的特对象完成的,它包含了相关的信息。事实上,Class 对象就是用来创建 类的所有的 “常规” 对象的。

类是程序中的一部分,每个类都有一个 Class 对象,换句话说,每当编写了一个新类,经过编译后,都会产生一个 Class 对象(更恰当的锁,是被保存在同名的 .class 文件中。

当我们需要生成一个类的对象的时候,需要运行这个程序的 Java 虚拟机(JVM)中的 “类加载器” 子系统。

所有的类都是在对其第一次使用的时,动态加载到 JVM 的。因此 Java 程序在它开始运行之前并非被完全加载的,其中的各个部分在必需的时候才加载。

类加载器首先检查这个类的 Class 对象是否已经加载过了。如果没有加载过,jvm 会根据名称查找对应的 .class 文件,将其加载进内存。而且在加载的时候,会进行检查,看这个 .class 文件有没有给人改过什么的。

说了那么多,头都晕了。联合代码来看下吧

package com.base.eg14.eg14_2;


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

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


/** * @author YZH * @date 2019/2/15 15:21 */
public class SweetShop {

    public static void main(String[] args) {
        System.out.println("inside main");

        new Candy();

        System.out.println("After creating candy");
        try {
            Class.forName("com.base.eg14.eg14_2.Gum");
        } catch (ClassNotFoundException e) {
            System.out.println("Couldn't find Gum");
        }
    }
}

Candy、Gum 都有一个 static 块,在类加载的时候就会执行 static 块中的程序,那我们就可以知道他是什么时候执行的了。

运行的结果如下:

inside main
Loading Candy
After creating candy
Loading Gum

可以看到并不是程序启动就加载类了,而是在需要的时候才进行加载。

另外,Class.forName(String className)这个方法的作用是通过项目的相对路径获取对应的Class应用。说白了就是标出路径,让 jvm 根据这个路径加载 .class 文件为其创建 Class 对象。而且这个方法有一个好处,如果发现了还没加载,会帮我们加载。

无论何时,只要你想在运行时使用类型信息,就必须首先获取对应的 Class 对象的应用。Class.forName(String className)就是实现这个功能的便捷方法,因为你不需要为了获取Class应用而引入该类型的对象。

毫不过分地说,Class 就是理解 Java 的神器,在成熟框架的源码中随处可见。

接下来看一下 Class 对象的一些常用功能吧:

package com.base.eg14.eg14_2;

interface HasBatteries{}
interface Waterproof{}
interface Shoots{}

class Toy{
    Toy(){}
    Toy(int i){}
}

class FancyToy extends Toy implements HasBatteries,Waterproof,Shoots{
    FancyToy(){
        super(1);
    }
}

/** * @author YZH * @date 2019/2/15 15:59 */
public class ToyTest {

    static void printInfo(Class cc){
        System.out.println("Class name " + cc.getName() + " is interface ? [" + cc.isInterface() + "]");
        System.out.println("Simple Name is " + cc.getSimpleName());
        System.out.println("Canonical name : " + cc.getCanonicalName());
    }

    public static void main(String[] args) {
        Class c = null;
        try {
            c = Class.forName("com.base.eg14.eg14_2.FancyToy");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        printInfo(c);

        for (Class face : c.getInterfaces()){
            printInfo(face);
        }

        Class up = c.getSuperclass();
        Object ob = null;
        try {
            ob = up.newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        printInfo(ob.getClass());
    }
}

其中的功能,参照 jdk 文档来看吧。妙不可言~

三、ClassLoader

先说明一下 ClassLoader 的作用

ClassLoader主要对类的请求提供服务,当JVM需要某类时,它根据名称向ClassLoader要求这个类,然后由ClassLoader返回这个类的class对象。

ClassLoader负责载入系统的所有资源(Class,文件,图片,来自网络的字节流等),通过ClassLoader从而将资源载入JVM中。每个class都有一个引用,指向自己的ClassLoader

3.1 获取 ClassLoader 的方法

  • 使用当前类的ClassLoader
    • this.getClass.getClassLoader();
  • 使用当前线程的ClassLoader
    • Thread.currentThread().getContextClassLoader();
  • 使用系统ClassLoader,即系统的入口点所使用的ClassLoader
    • ClassLoader.getSystemClassLoader();

3.2 通过ClassLoader的方式载入类

先看一下加载类的方式有哪些

  • 直接 new

  • 使用Class静态方法 Class.forName();

  • 使用 ClassLoader

前面两种方式,我们都已经知道了。

先看一下第三方方式加载类是怎么回事:

 /* Step 1. Get ClassLoader */
 ClassLoader cl = this.getClass.getClassLoader();;  // 获得ClassLoader
 
 /* Step 2. Load the class */
 Class cls = cl.loadClass("com.alexia.B"); // 使用第一步得到的ClassLoader来载入B
    
 /* Step 3. new instance */
 B b = (B)cls.newInstance(); // 有B的类得到一个B的实例

注:有人心里可能会想,对于类的载入方式我们都会选择最简单的第一种方式,后两种方式完全是多余。

实则不然,直接new的方式也是有局限的,举个最简单的例子:Java中有包名的类怎么引用默认包中的类?当然说这个是因为有包名的类不能直接用new引用默认包中的类,那么怎么办呢?答案是使用反射机制,即使用第二种方式来加载类(具体请看这里)。而且,用new()和用*newInstance()*创建类的实例是不同的,主要区别简单描述如下:

从JVM的角度看,我们使用关键字new创建一个类的时候,这个类可以没有被加载。但是使用*newInstance()*方法的时候,就必须保证:

(1)这个类已经加载;

(2)这个类已经链接了(即为静态域分配存储空间,并且如果必须的话将解析这个类创建的对其他类的所有引用)。而完成上面两个步骤的正是Class的静态方法forName()所完成的,这个静态方法调用了启动类加载器,即加载javaAPI的那个加载器。

可以看出,newInstance()实际上是把new这个方式分解为两步

  • 即首先调用Class加载方法加载某个类
  • 然后实例化。

这样分步的好处是显而易见的。我们可以在调用class的静态加载方法forName时获得更好的灵活性,提供给了一种降耦的手段。

3.3 通过 ClassLoader 载入文件

下面列出几项载入文件的方式

第一种方式:使用 IO 流完成读取

File f = new File("C:/test/com/aleixa/config/sys.properties"); // 使用绝对路径
//File f = new File("com/alexia/config/sys.properties"); // 使用相对路径
InputStream is = new FileInputStream(f); 

第二种方式:使用 ClassLoader

InputStream is = null;
is = this.getClass().getClassLoader().getResourceAsStream(
       "com/alexia/config/sys.properties");

第三种方式:使用 ResourceBundle

ResourceBundle bundle = ResourceBundle.getBoundle("com.alexia.config.sys"); 

第三中方式多用于载入用户的配置文件。

如果是属性配置文件,也可以通过java.util.Properties.load(is)将内容读到 Properties 里

另外,Properties 默认认为is的编码是 ISO-8859-1,如果配置文件是非英文的,可能出现乱码问题。

ps:
参考资料:《Java 编程思想》、网上文章

你可能感兴趣的:(java)