Java反射系列(2):从Class获取父类方法说起

传送门

Java反射系列(1):入门基础

在比较早的时候,就讨论过java反射的一些用法及概念,今天主要来看一下反射的基石Class对象!

Class如何获取父类方法说起

先回答一个问题,你能立马反应过来吗?

先问一下问题,java如何获取Class对象的父类方法?如果你不是最近刚好用过类似的API,是不是一下子很难反应过来?可能还要去搜索一下,或者看下API找找才能知道怎么用

当然,我肯定是没有反应过来,当时为了做一个脱敏的功能,又回头特意去查了下:

先调用getSuperclass()获取父类对象,为Object

  • getSuperclass

    public <? super T> getSuperclass()

    返回表示此所表示的实体(类,接口,基本类型或void)的超类 。 如果这个表示Object类,接口,原始类型或void,则返回null。 如果此对象表示数组类,则返回表示Object类的对象。

    结果

    由该对象表示的类的超类。

 再获取父类对象的方法getMethods

  • getMethods

    public 方法[] getMethods()
                        throws SecurityException
    返回包含一个数组方法对象反射由此表示的类或接口的所有公共方法对象,包括那些由类或接口和那些从超类和超接口继承的声明。

    如果此对象表示具有多个具有相同名称和参数类型但具有不同返回类型的公共方法的类型,则返回的数组对于每个此类方法都有一个方法对象。

    如果此对象表示与类初始化方法的类型<clinit> ,则返回的阵列具有相应的方法对象。

    如果此对象表示一个数组类型,则返回的阵列具有方法对于每个由阵列类型从继承的公共方法对象Object 。 它不包含方法对象clone() 。

    如果此对象表示一个接口,那么返回的数组不包含任何隐含声明的方法,从Object 。 因此,如果在此接口或其任何超级接口中没有显式声明方法,则返回的数组的长度为0.(注意,表示类的对象始终具有从Object公共方法)。

    如果此对象表示原始类型或空值,则返回的数组的长度为0。

    由此对象表示的类或接口的超级接口中声明的静态方法不被视为类或接口的成员。

    返回的数组中的元素不会被排序,并且不是以任何特定的顺序。

    结果

    代表这个类的公共方法的 方法对象的数组

    异常

    SecurityException - 如果存在安全管理员 s ,并且调用者的类加载器与当前类的类加载器不同或者祖先,并且调用 s.checkPackageAccess(), 拒绝对该类的包的访问。

    从以下版本开始:

    JDK1.1

    See The Java™ Language Specification:

    8.2类成员,8.4方法声明

具体可以参考:Class Class<T> 

看一个代码例子,来点体感

在之前有讨论过oauth2相关的一些设计,里面说过对于token相关的操作,比如生成token,刷新token,根据token获取用户信息等,都会用到clientIdclientSecret这一对身份凭证

开放平台会为系统默认生成几个字段

  • 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

RTTI

对JAVA程序员来说,可能对RTTI比较陌生,因为这是个C++的概念

RTTI(Run-Time Type Identification),通过运行时类型信息程序能够使用基类指针或引用来检查这些指针或引用所指的对象的实际派生类型。

如果看过《Think in Java》,里面关于类型信息里面章节,除了讲解反射之外,还会提到RTTI,称之为"传统的方式"。那什么是RRIT呢?又有哪些特点?

什么是RTTI

以下引用自《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<Shape> 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<Shape>中时,会发生向上转型。但是在向上转型的过程中,也丢失了Shape对象的具体类型。

当从集合中取出元素时,这种容器,实际上它将所有的事物都当作Object持有,会自动将结果转型回Shape。这就是RTTI最基本的形式,因为在Java中,所有的类型转换都是在运行时进行正确性检查 的。这也是RTTI的含义:在运行时,识别一个对象的类型

但是RTTI转换并不彻底,Object被转型为Shape,并没有转型为Circle,Square,Triangle。因为List<Shape>保存的只是Shape,在编译时由容器和Java的泛型来强制保证这一点,而在运行时,由类型转换操作来确保这一点(因为Java的泛型是由语法糖来实现的,在运行期间会擦除,也就是获取不到对应的类型)

通过Li+st<Shape>就实现了多态的目的,程序尽可能只跟基类打交道,这样代码更容易写,也更容易读,并且更容易维护。这样似乎没有必要知道某个具体类型,在设计原则中,也倡导面对接口编程,而不是实现类编程

但是在有的时候,需要在运行时,知道某个具体的类型,比如,对于所有三角形,渲染成特殊颜色,那这个时候就要使用到RTTI了

对RTTI的一点澄清

说了这么多,可能对RTTI还不是很了解,尤其是没有C++经历的程序员。所以总的来说,就是不用过分区分RTTI与反射,甚至等同它们就可以了:因为《Think in Java》的作者,在写这本书的之前,写了一本《Think in C++》,对C++剖析的入木三分,赢的交口称赞,所以在java中也引入了RTTI这个概念来类比(不得不说,国外的大牛,真是厉害,真正做到语言不过是一门工具,一不开心就自己写一门语言出来,让大家卷)

Class对象

Class对象的产生

前面说了,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根类的一部分

 Class对象的几个常见方法

利用上面的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.classBoolean.TYPE
char.classCharacter.TYPE
byte.class     Byte.TYPE
short.classShort.TYPE       
long.classLong.TYPE
float.classFloat.TYPE
double.classDouble.TYPE
void.classVoid.TYPE

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值