Skip to content

Class 类

你有多少个名字?

普通人可能有两个:身份证上的大名叫什么,平时别人叫你什么。但 Class 对象更复杂——它有全限定名、简单名、还有规范名(Cannonical Name)。

不信?试试:

java
System.out.println(new ArrayList<String>().getClass().getName());      // java.util.ArrayList
System.out.println(new ArrayList<String>().getClass().getSimpleName()); // ArrayList

在反射的世界里,Class 对象就是一切的开始。理解它,就理解了一半的反射。

Class 是什么?

Class 是一个特殊的类。它是 JVM 对「类」这种概念的抽象。

当你写下 class Person {},JVM 会:

  1. 加载这个类的字节码
  2. 创建一个 Class 对象来描述这个类
  3. Class 对象会被缓存(同一个类只会有一个 Class 对象)

这个 Class 对象存储了一个类的所有元信息:字段列表、方法列表、构造方法、继承关系、注解...

重要结论Class 本身也是一个类,所以 Class.class 也是合法的。这是元类(Metaclass)的概念,Java 里没有暴露出来,但原理类似。

三种获取方式的选择

java
public class ClassAcquisitionStrategies {

    public static void main(String[] args) throws ClassNotFoundException {
        // 场景一:你已经知道类名,编译期就确定
        Class<String> clazz1 = String.class;
        System.out.println("编译期确定: " + clazz1.getSimpleName());

        // 场景二:你有个对象,想知道它的类型
        String s = "hello";
        Class<? extends String> clazz2 = s.getClass();
        System.out.println("从对象获取: " + clazz2.getSimpleName());

        // 场景三:类名是运行时才确定的(最灵活)
        String className = "java.util.LinkedList";
        Class<?> clazz3 = Class.forName(className);
        System.out.println("动态加载: " + clazz3.getSimpleName());
    }
}

什么时候用哪个?

获取方式何时使用特点
.class编译期知道类最快,IDE 还能帮你检查类是否存在
getClass()已有对象适合多态场景,能拿到真实类型
forName()字符串来源动态配置文件中读取、插件系统、热加载

有个经典陷阱:

java
public class ClassCastTrap {

    public static void main(String[] args) throws Exception {
        // 你可能以为两者相等...
        Class<String> c1 = String.class;
        Class<String> c2 = (Class<String>) Class.forName("java.lang.String");

        // 但泛型在编译后被擦除了,运行时无法区分 List<String> 和 List<Integer>
        Class<?> listString = Class.forName("java.util.List");
        Class<?> listInt = Class.forName("java.util.List");

        System.out.println("两者相等吗? " + (listString == listInt)); // true!
        // List<String> 和 List<Integer> 在运行时是同一个 Class 对象
    }
}

这就是泛型擦除——类型参数信息在编译后就丢失了。

获取类信息

Class 对象提供了一大堆「问句」:

java
import java.util.*;

public class ClassIntrospection {

    public static void main(String[] args) {
        Class<ArrayList<String>> clazz = ArrayList.class;

        // 名字系列
        System.out.println("getName():        " + clazz.getName());       // java.util.ArrayList
        System.out.println("getSimpleName():  " + clazz.getSimpleName()); // ArrayList
        System.out.println("getCanonicalName():" + clazz.getCanonicalName()); // java.util.ArrayList

        // 包信息
        Package pkg = clazz.getPackage();
        System.out.println("getPackage():     " + pkg.getName());

        // 继承关系
        System.out.println("getSuperclass(): " + clazz.getSuperclass().getSimpleName());

        // 实现接口
        System.out.println("getInterfaces(): " + Arrays.toString(clazz.getInterfaces()));

        // 修饰符
        int mods = clazz.getModifiers();
        System.out.println("isPublic():      " + java.lang.reflect.Modifier.isPublic(mods));
        System.out.println("isAbstract():    " + java.lang.reflect.Modifier.isAbstract(mods));
        System.out.println("isFinal():       " + java.lang.reflect.Modifier.isFinal(mods));
        System.out.println("Modifiers:       " + java.lang.reflect.Modifier.toString(mods));
    }
}

注意区分几种名字的输出:

java
Class<Map.Entry<String, Integer>> entryClass = Map.Entry.class;

entryClass.getName();        // java.util.Map$Entry(内部类用 $ 分隔)
entryClass.getSimpleName();  // Entry
entryClass.getCanonicalName() // java.util.Map.Entry(人类友好的名字)

创建实例

Class 对象能创建对应类的实例:

java
public class ClassInstantiation {

    public static void main(String[] args) throws Exception {
        // 方式一:newInstance() — JDK 9 已废弃
        // 原因:只能调用无参构造,如果无参构造不存在会抛异常
        @SuppressWarnings("deprecation")
        Object obj1 = Object.class.newInstance();  // 废弃警告

        // 方式二:getDeclaredConstructor().newInstance() — 推荐
        Object obj2 = Object.class.getDeclaredConstructor().newInstance();
        System.out.println("obj2: " + obj2);

        // 方式三:调用有参构造
        String s = String.class.getDeclaredConstructor(String.class)
                                       .newInstance("hello");
        System.out.println("s: " + s);

        // 方式四:获取所有构造方法
        System.out.println("=== String 的构造方法 ===");
        for (java.lang.reflect.Constructor<?> c : String.class.getDeclaredConstructors()) {
            System.out.println(c);
        }
    }
}

为什么废弃 newInstance()?

因为它「吞」了异常。如果无参构造抛出异常(比如构造失败了),newInstance() 会把它包装成 InvocationTargetException,而不是直接抛出原异常。这让调试变得困难。

获取成员

java
import java.lang.reflect.*;

public class MemberRetrieval {

    public String publicField;
    private String privateField;
    public static int staticField;

    public void publicMethod() {}
    private String privateMethod() { return ""; }

    public MemberRetrieval() {}
    public MemberRetrieval(String arg) {}

    public static void main(String[] args) {
        Class<MemberRetrieval> clazz = MemberRetrieval.class;

        System.out.println("=== 字段 ===");
        for (Field f : clazz.getDeclaredFields()) {
            String mods = Modifier.toString(f.getModifiers());
            System.out.println((mods.isEmpty() ? "" : mods + " ") +
                              f.getType().getSimpleName() + " " + f.getName());
        }

        System.out.println("\n=== 方法 ===");
        for (Method m : clazz.getDeclaredMethods()) {
            System.out.println(Modifier.toString(m.getModifiers()) + " " +
                              m.getReturnType().getSimpleName() + " " + m.getName());
        }

        System.out.println("\n=== 构造方法 ===");
        for (Constructor<?> c : clazz.getDeclaredConstructors()) {
            System.out.println(c.getName());
        }
    }
}

注意用的是 getDeclaredFields() / getDeclaredMethods(),不是 getFields() / getMethods()

方法返回值包含 Private 吗包含继承的吗
getFields()Field[]
getDeclaredFields()Field[]
getMethods()Method[]
getDeclaredMethods()Method[]

动态加载类的陷阱

Class.forName() 有个容易被忽视的特性:它会触发类初始化

java
public class ClassInitializationTrap {

    static class HeavyClass {
        static {
            System.out.println("HeavyClass 被初始化了!");
        }

        public static final String CONST = "initialized";  // 编译期常量,不触发
        public static String NON_CONST = "need init";      // 非 final,触发初始化
    }

    public static void main(String[] args) throws Exception {
        System.out.println("准备加载...");

        // 不会触发 static{} 块
        Class<?> c1 = Class.forName("java.lang.String");

        // 会触发 static{} 块(但如果是编译期常量,则不会)
        Class<?> c2 = Class.forName("ClassInitializationTrap$HeavyClass");

        System.out.println("加载完成");
    }
}

在 JDBC 驱动加载的场景下,这个特性是故意被利用的:

java
// JDBC 4.0 之前,需要手动加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");

// JDBC 4.0 之后,驱动类内部有 static{} 块,加载即注册
// 所以不需要显式 Class.forName()

要点回顾

  1. Class 对象是单例的 — 每个类在 JVM 中只有一个 Class 对象
  2. 获取方式有三种.classobj.getClass()Class.forName()
  3. forName() 会触发类初始化 — static{} 块会执行
  4. 泛型信息被擦除List<String>.classList<Integer>.class 是同一个对象
  5. getDeclaredXxx vs getXxx — 前者只获取本类声明,后者包括继承的

Class 是反射的起点,理解它就像理解 JVM 的「户口本系统」——每个类都有自己的档案,Class 对象就是那份档案。

基于 VitePress 构建