在比较早的时候,就讨论过java反射的一些用法及概念,今天主要来看一下反射的基石Class对象!
先问一下问题,java如何获取Class对象的父类方法?如果你不是最近刚好用过类似的API,是不是一下子很难反应过来?可能还要去搜索一下,或者看下API找找才能知道怎么用
当然,我肯定是没有反应过来,当时为了做一个脱敏的功能,又回头特意去查了下:
先调用getSuperclass()获取父类对象,为Object
再获取父类对象的方法getMethods
getMethods
public 方法[] getMethods() throws SecurityException返回包含一个数组方法
对象反射由此表示的类或接口的所有公共方法类
对象,包括那些由类或接口和那些从超类和超接口继承的声明。如果此
类
对象表示具有多个具有相同名称和参数类型但具有不同返回类型的公共方法的类型,则返回的数组对于每个此类方法都有一个方法
对象。如果此
类
对象表示与类初始化方法的类型,则返回的阵列不具有相应的
方法
对象。如果此
类
对象表示一个数组类型,则返回的阵列具有方法
对于每个由阵列类型从继承的公共方法对象Object
。 它不包含方法
对象clone()
。如果此
类
对象表示一个接口,那么返回的数组不包含任何隐含声明的方法,从Object
。 因此,如果在此接口或其任何超级接口中没有显式声明方法,则返回的数组的长度为0.(注意,表示类的类
对象始终具有从Object
公共方法)。如果此
类
对象表示原始类型或空值,则返回的数组的长度为0。由此
类
对象表示的类或接口的超级接口中声明的静态方法不被视为类或接口的成员。返回的数组中的元素不会被排序,并且不是以任何特定的顺序。
结果
代表这个类的公共方法的
方法
对象的数组异常
SecurityException
- 如果存在安全管理员 s ,并且调用者的类加载器与当前类的类加载器不同或者祖先,并且调用s.checkPackageAccess()
, 则拒绝对该类的包的访问。从以下版本开始:
JDK1.1
See The Java™ Language Specification:
8.2类成员,8.4方法声明
具体可以参考:Class Class
在之前有讨论过oauth2相关的一些设计,里面说过对于token相关的操作,比如生成token,刷新token,根据token获取用户信息等,都会用到clientId,clientSecret这一对身份凭证
开放平台会为系统默认生成几个字段
- client_id:客户端应用id,这个类似微信的appid(这个也是微信开放平台颁发的)
- client_secret:应用身份密钥,类似微信的secret
所以对于这个几个接口,提取出一个BaseAuthReq
- public class BaseAuthReq
- {
-
- /** 应用ID */
- private String clientId;
-
- /** 应用身份密钥 */
- private String clientSecret;
-
- // get set 方法
- }
让原来的GetTokenReq继承BaseAuthReq
- public class GetTokenReq extends BaseAuthReq
- {
-
- /** 授权码类型 */
- private String grantType;
-
- /** 授权码 */
- private String code;
-
- // get set方法
- }
那么,现在可以通过上面的方法getSuperclass,getMethods获得父类对象的方法
- @Test
- public void test()
- {
- // 获取当前类的Class对象
- Class clazz = GetTokenReq.class;
- System.out.println("clazz:" + clazz.getName());
-
- // 获取父类
- Class superClass = clazz.getSuperclass();
- System.out.println("superClass:" + superClass.getName());
-
- // 父类上的所有方法
- Method[] methods = superClass.getDeclaredMethods();
- for (Method method : methods)
- {
- System.out.println("method:" + method.getName());
- }
-
- }
打印输出:
clazz:com.tw.tsm.auth.dto.request.GetTokenReq
superClass:com.tw.tsm.auth.dto.BaseAuthReq
method:getClientSecret
method:setClientSecret
method:setClientId
method:getClientId
对JAVA程序员来说,可能对RTTI比较陌生,因为这是个C++的概念:
RTTI(Run-Time Type Identification),通过运行时类型信息程序能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型。
如果看过《Think in Java》,里面关于类型信息里面章节,除了讲解反射之外,还会提到RTTI,称之为"传统的方式"。那什么是RRIT呢?又有哪些特点?
以下引用自《Think in Java》
Java有2种方式在运行时识别对象和类的信息:一种是"传统的"RTTI方式,它假定在编译时已经知道了所有的类型;另一种是"反射"机制,它允许在运行时发现和使用类的信息。
大致意思就是,通过RTTI,程序能在运行时识别对象和类的信息:运行机制就是通过Class对象来实现的
看一个多态的例子:基类为Shape(形状),子类有Circle(圆圈),Square(方形)和Triangle(三角形),画出它的类图
面向对象的基本目的就是,让代码只操纵基类,也就是Shape的引用。这样如果添加新类,比如Rhomboid来扩展程序,就不会影响到原来的代码,也就是OCP原则:对扩展开放,对修改关闭:如果一种设计良好的代码,可能会结合工厂+策略模式来达到扩展的目的
在Shape类中,有一个draw()方法,子类需要强制覆盖draw()方法,这样在调用方都只使用Shape类来调用draw(),由于它是动态绑定的,就能被正确的执行,这就是多态。
- abstract class Shape
- {
- void draw()
- {
- System.out.println(this + ".draw()");
- }
-
- abstract public String toString();
- }
-
- class Circle extends Shape
- {
- @Override
- public String toString()
- {
- return "Circle";
- }
- }
-
- class Square extends Shape
- {
- @Override
- public String toString()
- {
- return "Square";
- }
- }
-
- class Triangle extends Shape
- {
- @Override
- public String toString()
- {
- return "Triangle";
- }
- }
-
- public class Shapes
- {
- public static void main(String[] args)
- {
- List
shapeList = Arrays.asList(new Circle(), new Square(), new Triangle()); - shapeList.stream().forEach(shape -> shape.draw());
- }
- }
调用一下,打印出
Circle.draw()
Square.draw()
Triangle.draw()16
在上面例子中,当把Shape对象放入Li+st
当从集合中取出元素时,这种容器,实际上它将所有的事物都当作Object持有,会自动将结果转型回Shape。这就是RTTI最基本的形式,因为在Java中,所有的类型转换都是在运行时进行正确性检查 的。这也是RTTI的含义:在运行时,识别一个对象的类型
但是RTTI转换并不彻底,Object被转型为Shape,并没有转型为Circle,Square,Triangle。因为List
通过Li+st
但是在有的时候,需要在运行时,知道某个具体的类型,比如,对于所有三角形,渲染成特殊颜色,那这个时候就要使用到RTTI了
说了这么多,可能对RTTI还不是很了解,尤其是没有C++经历的程序员。所以总的来说,就是不用过分区分RTTI与反射,甚至等同它们就可以了:因为《Think in Java》的作者,在写这本书的之前,写了一本《Think in C++》,对C++剖析的入木三分,赢的交口称赞,所以在java中也引入了RTTI这个概念来类比(不得不说,国外的大牛,真是厉害,真正做到语言不过是一门工具,一不开心就自己写一门语言出来,让大家卷)
前面说了,Java是通过Class对象来执行RTTI的,实际上,每一个对象都有一个Class对象。每当编写并且编译了一个新类,就会产生一个Class对象,更确切的说,是编译的时候,把.java文件编译成了.class文件,然后jvm通过类加载器加载.class文件,最终生成对象,这里可以看下周志明的《深入理解Java虚拟机:JVM高级特性与最佳实践》第7章:虚拟机类加载机制
这里面会提到Class对象的加载的一个特性,即Class对象仅在需要的时候才被加载,static初始化是在类加载的时候进行的:比如,以前刚学习Java的时候(尤其是刚找工作那会儿,真是把代码背下来了,因为很多公司在做笔记卷子),会通过JDBC连接数据库,经典加载驱动:
- try{
- //加载MySql的驱动类
- Class.forName("com.mysql.jdbc.Driver") ;
- }catch(ClassNotFoundException e){
- System.out.println("找不到驱动程序类 ,加载驱动失败!");
- e.printStackTrace() ;
- }
其中的Class.forName(""),就是让jvm加载指定的类,只不过一般忽略了返回值:它会返回Class对象的一个引用,底层的forName0是一个native方法;如果已经有了一个类型的对象,就可以直接调用getClass()获取Class的引用,这个方法是Object根类的一部分
利用上面的forName()来看看下面这个例子
- interface Line
- {
- }
-
- abstract class Shape implements Line
- {
- void draw()
- {
- System.out.println(this + ".draw()");
- }
-
- abstract public String toString();
- }
-
- class Circle extends Shape
- {
- @Override
- public String toString()
- {
- return "Circle";
- }
- }
-
- class Square extends Shape
- {
- @Override
- public String toString()
- {
- return "Square";
- }
- }
-
- class Triangle extends Shape
- {
- @Override
- public String toString()
- {
- return "Triangle";
- }
- }
-
- public class Shapes
- {
- public static void main(String[] args)
- {
- try
- {
- // 加载Circle类
- Class clazz = Class.forName("com.tw.tsm.auth.Circle");
- printInfo(clazz);
-
- // 实例化对象
- Object superObj = clazz.newInstance();
- printInfo(superObj.getClass());
-
- // 获取父类-Shape
- Class superClazz = clazz.getSuperclass();
- printInfo(superClazz);
-
- // 获取父类上所有实现的接口Line
- Arrays.stream(superClazz.getInterfaces()).forEach(f -> {
- printInfo(f);
- });
- }
- catch (ClassNotFoundException | InstantiationException | IllegalAccessException e)
- {
- e.printStackTrace();
- }
- }
-
- public static void printInfo(Class clazz)
- {
- // 是否接口
- System.out.println("Class Name: " + clazz.getName() + " is interface?[" + clazz.isInterface() + "]");
- // 对象名称
- System.out.println("Simple name: " + clazz.getSimpleName());
- // 对象全限定路径名称
- System.out.println("Canonical name: " + clazz.getCanonicalName());
- System.out.println("=================================================");
- }
- }
刚才的Shape抽象类实现了Shape接口,printInfo打印出类上相关的信息
getSimpleName()返回不包含包名的类名,比如Circle
getCanonicalName()返回全限定的类名
isInterface()判定是否是一个接口
getSuperclass()在开头提到过,返回父类
newInstance()实例化一个对象,它隐含的功能就是:编程时不知道你的确切类型,但必须正确的创建对象
getInterfaces(),返回所有实现的接口列表
此外,队了Class.forName加载对象得到Class对象的引用之外,还可以通类字面量,也就是obj.class
Java还提供了另一种方法来生成对Class对象的引用,即类字面常量,比如
Circle.class
这样做比上面的Class.forName更安全,也更简单,因为不需要使用try...catch,也会在编译时进行检查。此外,类字面常量不仅可以用于普通的类,也可以应用于接口,数组与基本的数据类型。对于基本数据类型的包装器类,还有一个标准字段TYPE。TYPE字段是一个引用,指向对应的基本数据类型的Class对象
boolean.class | Boolean.TYPE |
char.class | Character.TYPE |
byte.class | Byte.TYPE |
short.class | Short.TYPE |
long.class | Long.TYPE |
float.class | Float.TYPE |
double.class | Double.TYPE |
void.class | Void.TYPE |