[Java基础]Class对象
Class对象
class对象通常存放在方法区
在程序运行期间,Java运行时系统始终为所有对象维护一个运行时类型标识。这个信息会跟踪每个对象所属的类。虚拟机利用运行时类型信息选择要执行的正确的方法。不过,可以使用一个特殊的Java类访问这些信息。保存这些信息的类名为Class,这个名字有些让人困惑。
Object类中的getclass()方法将会返回一个class类型的实例。
Employee e;
Class cl=e.getClass();
就像 Employee对象描述一个特定员工的属性一样,class对象会描述一个特定类的属性。可能最常用的Class方法就是getName。这个方法将返回类的名字。
例如,下面这条语句:
System.out.println(e.getClass().getName()+""+ e.getName());
如果e是一个员工,则会输出:Employee Harry Hacker
如果e是经理,则会输出:Manager Harry Hacker
如果类在一个包里,包的名字也作为类名的一部分
var generator = new Random();
Class cl= generator.getClass();
String name =cl.getName();
//name is set to "iava.util.Random
还可以使用静态方法forName获得类名对应的class 对象
String className ="java.util.Random";
Class cl=Class.forName(className);
如果类名保存在一个字符串中,这个字符串会在运行时变化,就可以使用这个方法。如果className是一个类名或接口名,这个方法可以正常执行。否则,forName方法将抛出一个检查型异常(checked exception)。无论何时使用这个方法,都应该提供一个异常处理器exception handler)。
有三种方法可以获得class对象的实例
- Class.forName(""),传入类名的全限定符
- 每一个对象都有getClass()方法,例如a.getClass()
- 直接使用类名.class,例如MyClass.Class
Class<?> c= Class.forName("classA");
System.out.println(c.getName());
classA a = new classA();
Class<?> d = a.getClass();
System.out.println(d.getName());
Class<?> e = classA.class;
System.out.println(e.getName());
classA
classA
classA
提示;在启动时,包含main方法的类被加载。它会加载所有需要的类。这些被加载的类又要加载它们需要的类,以此类推。对于一个大型的应用程序来说,这将会花费很长时间,用户会因此感到不耐烦。可以使用下面这个技巧给用户一种启动速度比较快的假象。不过,要确保包含 main方法的类没有显式地引用其他的类。
首先,显示一个启动画面;
然后,通过调用Class.forName手工地强制加载其他类。获得class类对象的第三种方法是一个很方便的快捷方式。如果T是任意的Java类型或 void 关键字),T.class 将代表匹配的类对象。例如:Class cl1=Random.class;// if you import java.util.*;Class cl2=int.class:Class cl3 = Double[].class;请注意,一个lass对象实际上表示的是一个类型,这可能是类,也可能不是类。例如int不是类,但int.class 是一个(lass 类型的对象。注释:Class类实际上是一个泛型类。例如,Employee.class的类型是Class
注释:有一个已经废弃的(lass.toInstance方法,它也可以用无参数构造器构造一个实例。不过,如果构造器抛出一个检查型异常,这个异常将不做任何检查重新抛出。这违反了编译时异常检查的原则。与之不同,(onstructor.newInstance会把所有构造器异常包装到一个InvocationTargetException中。C++注释:newInstance方法相当于C++中的虚拟构造器概念。不过,C++中的虚拟构造器不是一个语言特性,而是需要一个专业库支持的习惯用法。(lass 类类似于C++中的type info类,getClass方法则等价于typeid运算符。不过,Java的Class比type info功能更全面。C++的 type info只能给出表示类型名的一个字符串,而不能创建那个类型的新对象。第5章继死201srl java.lang.class 10static Class forName(String className)返回一个 Class 对象,表示名为 className 的类。Constructor getConstructor(Class...parameterTypes)1.1生成一个对象,描述有指定参数类型的构造器。参见5.7.7节更多地了解如何提供参数类型。java.lang,reflect .Constructor 110bject newInstance(0bject... params)将params传递到构造器,来构造这个构造器声明类的一个新实例。参见5.7.7节更多地了解如何提供参数。java.lang.Throwable 1.0void printStackTrace()将 Throwable 对象和堆栈轨迹打印到标准错误流。
利用反射分析类的能力#
下面简要介绍反射机制最重要的内容--检查类的结构。在java.lang.reflect包中有三个类Field、Method和Constructor分别用于描述类的字段、方法和构造器。这三个类都有一个叫做 getName的方法,用来返回字段、方法或构造器的名称,Field类有一个getType方法,用来返回描述字段类型的一个对象,这个对象的类型同样是Class。Method和Constructor类有报告参数类型的方法,Method类还有一个报告返回类型的方法。这三个类都有一个名为getModifiers的方法,它将返回一个整数,用不同的 0/1位描述所使用的修饰符,如public和static。
另外,还可以利用java.lang.reflect包中Modifier类的静态方法分析 getModifiers返回的这个整数。例如,可以使用Modifier类中的isPublic、isPrivate 或isFinal 判断方法或构造器是public、private还是final。我们需要做的就是在 getModifiers 返回的整数上调用Modifier类中适当的方法,另外,还可以利用Modifier.tostring方法将修饰符打印出来。Class类中的getFields、getMethods和getConstructors方法将分别返回这个类支持的公共字段、方法和构造器的数组,其中包括超类的公共成员。class类的getDeclareFieldsgetDeclareMethods和getDeclaredConstructors方法将分别返回类中声明的全部字段、方法和构造器的数组,其中包括私有成员、包成员和受保护成员,但不包括超类的成员。
使用反射在运行时分析对象#
从前面一节中,我们已经知道如何查看任意对象数据字段字段的名字和类型:获得对应的 Class 对象。在这个Class对象上调用getDeclaredFields。本节将进一步查看字段的具体内容。当然,在编写程序时,如果知道想要查看的字段名和类型,查看对象中指定字段的内容是一件很容易的事情。而利用反射机制可以查看在编译如isAbstract方法就是检查modifiers值中对应修饰符abstract的二进制位。--译者注日第5章 继死209时还不知道的对象字段。要做到这一点,关键方法是Field类中的get方法。如果f是一个Field类型的对象(例如,通过 getDeclaredFields 得到的对象),obj是某个包含f字段的类的对象,f.get(obj)将返回一个对象,其值为oj的当前字段值。这样说起来显得有点抽象,下面来看一个程序。var harry=new Employee("Harry Hacker",50000,10,1,1989);Class cl=harry.getClass();//the class obiect representing EmployeeField f= cl.getDeclaredField("name");// the name field of the Employee classObject v=f.get(harry);// the value of the name field of the harry object, i.e.//the String object "Harry Hacker'当然,不仅可以获得值,也可以设置值。调用f.set(obj,value)将把对象obj的f表示的字段设置为新值。实际上,这段代码存在一个问题。由于name是一个私有字段,所以get和set方法会抛出一个IllegalAccessException。只能对可以访问的字段使用get和set方法。Java安全机制允许查看一个对象有哪些字段,但是除非拥有访问权限,否则不允许读写那些字段的值。反射机制的默认行为受限于Java的访问控制。不过,可以调用Field、Method或Constructor对象的setAccessible方法覆盖Java的访问控制。例如f.setAccessible(true);//now OK to call f.get(harry)setAccessible方法是Accessible0bject类中的一个方法,它是Field、Method和Constructor类的公共超类。这个特性是为调试、持久存储和类似机制提供的。本节稍后将利用它编写一个通用的 toString 方法。如果不允许访问,setAccessible调用会抛出一个异常。访问可以被模块系统(见卷I的第9章)或安全管理器(卷Ⅱ的第10章)拒绝。安全管理器并不常用。不过,在Java9中,由于Java API是模块化的,每个程序都包含模块。
使用反射编写泛型数组代码#
调用任意方法和构造器#
在C和C++中,可以通过一个函数指针执行任意函数。从表面上看,Java 没有提供方法指针,也就是说,Java没有提供途径将一个方法的存储地址传给另外一个方法,以便第二个方法以后调用。事实上,Java的设计者曾说过:方法指针是很危险的,而且很容易出错。他们认为 Java 的接口(interface)和lambda表达式(将在下一章讨论)是一种更好的解决方案。
不过,反射机制允许你调用任意的方法。回想一下,可以用Field类的 get方法查看一个对象的字段。与之类似,Method类有一个invoke 方法,允许你调用包装在当前Method对象中的方法。
invoke方法的签名是:
0bject invoke(0bject obj, 0bject... args)
第一个参数是隐式参数,其余的对象提供了显式参数。对于静态方法,第一个参数可以忽略,即可以将它设置为null。例如,假设用m表示Employee类的getName方法,下面这条语句显示了如何调用这个方法:String n=(String)ml.invoke(harry);
如果返回类型是基本类型,invoke方法会返回其包装器类型。例如,假设m2表示Employee类的 getsalary方法,那么返回的对象实际上是一个Double,必须相应地完成强制类型转换。可以使用自动拆箱将它转换为一个double:double s=(Double)m2.invoke(harry);
如何得到 Method对象呢?当然,可以调用 getDeclareMethods方法,然后搜索返回的 Method对象数组,直到发现想要的方法为止。也可以调用Class类的etMethod方法得到想要的方法。
它与 getField方法类似。getfield方法根据表示字段名的字符串,返回一个Field 对象。不过有可能存在若干个同名的方法,因此要准确地得到想要的那个方法必须格外小心。有鉴于还必须提供想要的方法的参数类型。getMethod的签名是:此,Method getMethod(String name, Class... parameterTypes)例如,下面说明了如何获得 Employee类的getName方法和raisesalary方法的方法指针Method m1=Employee.class.getMethod("getName");Method m2=Employee.class.getMethod("raiseSalary",double.class);可以使用类似的方法调用任意的构造器。将构造器的参数类型提供给assgetConstructor方法,并把参数值提供给Constructor.newInstance方法:Class cl=Random.class;//or any other class with a constructor that//accepts a long parameterConstructor cons=cl.getConstructor(long.class);0bject obj= cons.newInstance(42L);到此为止,我们已经了解了使用 Method对象的规则。下面来看如何具体使用。程序清单5-18中的程序会打印一个数学函数(如 Math.sqrt或Math.sin)的值的表格。打印的结果如下所示:
public static native double java.lang.Math.sqrt(double)1.00001.00002.00001.41421.73213.00002.00004.00002.23615.00002.44956.00007.00002.64582.82848.00003.00009.000010.00003.1623
当然,打印表格的代码与具体要打印表格的数学函数无关。
double dx=(to-from)/(n-1);
for(double x=from;x<=to;x+= dx)
double y=(Double)f.invoke(null,x);
System.out.printf("%10.4f%10.4f%n",x,y);
在这里,f是一个Method类型的对象。由于正在调用的方法是一个静态方法,所以invoke的第一个参数是 null。要打印 Math.sqrt 函数的值表格,需要将f设置为:Math.class.getMethod("sqrt",double.class)这是Math类的一个方法,名为sqrt,有一个double类型的参数。
这个例子清楚地表明,利用method对象可以实现C语言中函数指针(或C#中的委托)所能完成的所有操作。同C中一样,这种编程风格不是很简便,而且总是很容易出错。如果在调用方法的时候提供了错误的参数会发生什么?invoke方法将会抛出一个异常。另外,invoke的参数和返回值必须是0biect类型。这就意味着必须来回进行多次强制类型转换。这样一来,编译器会丧失检查代码的机会,以至于等到测试阶段才会发现错误,而这个时候查找和修正错误会麻烦得多。不仅如此,使用反射获得方法指针的代码要比直接调用方法的代码慢得多。
有鉴于此,建议仅在绝对必要的时候才在你自己的程序中使用Method对象。通常更好的做法是使用接口以及Java8引人的lambda表达式(第6章中介绍)。特别要强调:我们建议Java开发者不要使用回调函数的Method对象。可以使用回调的接口,这样不仅代码的执行速度更快,也更易于维护。
Class 类介绍
Class 对象是一种描述对象类的元对象,它主要用于 Java 的反射功能。
在 Java 中存在两种对象,一种是我们 new 出来的实例对象,另一种既是 jvm 生成的用来保存对应类的信息的 Class 对象。当我们定义好一个类文件并编译成 .class 字节码后,编译器同时为我们定义了一个 Class 对象并将它保存 .class 文件中。jvm 在类加载的时候将 .class 文件和对应的 Class 对象加载到内存中。
Class 没有公共构造方法。Class 对象是在加载类时由 Java 虚拟机通过调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个 Class 对象。
每个类都有且只有一个 Class 对象。运行程序时,jvm 首先检查类对应的 Class 对象是否已经加载。如果没有加载就会根据类名查找 .class 文件,并将其 Class 对象载入。虚拟机为每种类型管理一个独一无二的 Class 对象,但可以根据 Class 对象生成多个对象实例。某个类的 Class 对象被载入内存,它就会被用来创建这个类的所有对象。
获取 Class 对象#
由于 java.lang.Class 的构造方法是私有的,我们没法通过 new 的方式进行创建。有以下三种方式获取 Class 对象:类名.class、Class.forName() 静态方法 及 对象.getClass()。
类名.class 方式(字面常量方式)
public class Test {
static {
System.out.println("Run static initialization block.");
}
}
// 类名获取,注意这里不会执行类中的静态代码块内容
Class t = Test.class;
执行时 jvm 会先检查 Class 对象是否装入内存,如果没有装入内存,则将其装入内存,然后返回 Class 对象,如果已装入内存,则直接返回。
在加载 Class对象后,不会对 Class 对象进行初始化,这个特性很重要,我们在使用的过程中也推荐这种方式。
Class.forName() 方式
try {
Class t = Class.forName("com.test.Test"); //会打印出 Run static initialization block.
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
这种方式在加载 Class 对象后,会对类进行初始化,即执行类的静态代码块。
对象.getClass() 方式
Test t = new Test();
Class test = t.getClass();
通过类的实例获取,显然实例已经被创建,静态代码块肯定会被执行。
总结一下这三种方式,其中实例类的 getClass 方法和 Class 类的静态方法 forName 都将会触发类的初始化阶段,而字面常量获取 Class 对象的方式则不会触发初始化。这一点需要在使用的过程中特别注意。
Hotpot JVM Class 对象是在方法区还是堆中
JDK6 中 Class 实例在方法区。但 JDK7/8 创建的 Class 实例在 Java heap 中,并且 JDK8 移除了永久代,转而使用元空间 MetaSpace 来实现方法区。
instanceof 关键字与 isInstance 方法
//instanceof关键字
if(obj instanceof Animal){
Animal animal = (Animal) obj;
}
//isInstance方法
if(Animal.class.isInstance(obj)){
Animal animal = (Animal) obj;
}
两种方法的执行效果是一样的,需要注意的是 instanceOf 关键字只被用于对象引用变量,检查左边对象是不是右边类或接口的实例化。如果被测对象是 null 值,则测试结果总是 false。而 isInstance 方法则是 Class 类的 Native 方法,其中 obj 是被测试的对象或者变量,如果 obj 是调用这个方法的 class 或接口的实例,则返回 true。如果被检测的对象是 null 或者基本类型,那么返回值是 false;
封装类的 TYPE#
基本类型的 Class 对象和封装类的 TYPE 是同一个 Class 对象
System.out.println(boolean.class == Boolean.TYPE);//true
// Boolean 类中有如下定义
/**
* The Class object representing the primitive type boolean.
*
* @since JDK1.1
*/
@SuppressWarnings("unchecked")
public static final Class<Boolean> TYPE = (Class<Boolean>) Class.getPrimitiveClass("boolean");
Class 对象和 Java 反射
某种程度上讲,Class 类是 Java 反射机制的起源和入口,反射正是在运行期间通过 Class 对象访问类的属性、方法(包括构造方法)以及创建类实例的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!