栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

第14章 类型信息

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

第14章 类型信息

前言

        运行时类型信息使得你可以在程序运行时发现和使用类型信息。
        本章将讨论Java 是如何让我们在运行时识别对象和类的信息的(如何在运行阶段,识别对象和类的信息)。主要有两种方式:一种是“传统的”RTTI,它假定我们在编译时已经知道了所有的类型,另一种是“反射”机制,它允许我们在运行时发现和使用类的信息。

RTTI:在Java中,所有的类型转换都是在运行时进行正确性检查的。这也是RTTI的含义:在运行时,识别一个对象的类型。

本章的主题围绕着”Java如何让我们在程序在运行的时候,识别出对象和类型信息“问题进行展开的。

14.1 为什么需要RTTI

--  多态中表现的类型转换是RTTI最基本的使用形式,但这种转换并不彻底。因为会涉及到具体类型(子类)信息丢失的问题。

1.1 代码证明具体类型丢失
package net.mrliuli.rtti;

import java.util.Arrays;
import java.util.List;



abstract class Shape{
    void draw(){
        System.out.println(this + ".draw()");
    }
    abstract public String toString();  //要求子类需要实现 toString()
}

class Circle extends Shape{
    @Override
    public String toString() {
        return "Circle";
    }
    public void drawCircle(){}
}

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()  // 向上转型为 Shape,此处会丢失原来的具体类型信息!!对于数组而言,它们只是Shape类对象!
        );
        for(Shape shape : shapeList){
            shape.draw();   // 数组实际上将所有事物都当作Object持有,在取用时会自动将结果转型回声明类型即Shape。
        }
        //shapeList.get(0).drawCircle(); //这里会编译错误:在Shape类中找不到符号drawCircle(),证实了具体类型信息的丢失!!
    }
}

        在这个例子中,当把Shape对象放入List的数组时会向上转型。但在向上转型为Shape的时候也丢失了Shape对象的具体类型。对于数组而言,它们只是Shape类的对象。

        在这个例子中,RTTI类型转换并不彻底:Object被转型为Shape,而不是转型为Cirele、Square或者Triangle。这是因为目前我们只知道这个Liste保存的都是Shape。在编译时,将由容器和Java的泛型系统来强制确保这一点,而在运行时,由类型转换操作来确保这一点。

        接下来就是多态机制的事情了,Shape对象实际执行什么样的代码,是由引用所指向的具体对象Circle、Square或者Triangle而决定的。通常,也正是这样要求的,你希望大部分代码尽可能少地了解对象的具体类型,而是只与对象家族中的一个通用表示打交道(在这个例子中Shape)。这样代码会更容易写,更容易读,且更便于维护,设计也更容易实现、理解和改变。所以“多态”是面向对象编程的基本目标。

1.2 RTTI的三种形式(重要)
  • 传统的类型转换(多态),由RTTI确保类型转换的正确性,如果执行了错误的类型转换,会抛出一个ClassCastException的异常。
  • 代表对象类型的Class对象,通过查询Class对象获取所需要的信息
  • 类型转换前用instanceof检查

详细讲解请参考:

Think in java(五)RTTI的的三种形式、类型信息、class.forname与.class的区别___2018__的博客-CSDN博客

14.2 Class对象

        要理解RTTI在Java中的工作原理,首先必须知道类型信息在运行时是如何表示的。

1.1 RTTI在Java中的工作原理

        要能够在运行时识别具体类型,说明必然有东西在运行时保存了具体类型信息,这个东西就是Class对象,一种特殊对象。即Class对象表示了运行时的类型信息,它包含了与类有关的信息。只要你想在运行时使用类型信息,就必须首先获得对恰当的Class对象的引用。

  •     事实上Class对象就是用来创建类的所有的“常规”对象的。
  •     每个类(接口、抽象类)都有一个Class对象。换言之,每当编写并且编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。
  •     也就是说,Class对象在.java文件编译成.class文件时就生成了,且就保存在这个.class文件中。
1.2 Class对象用来生成常规对象(常规对象:非Class对象)

         类是程序的一部分,每个类都有一个Class对象(包括接口、抽象类)。换言之,每当编写并且编译了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。为了生成这个类的对象,运行这个程序的Java虚拟机(JVM)将使用被称为“类加载器”的子系统(原生类加载器加载的是所调的可信类,包括lava API类,它们通常是从本地盘加载的)。

        所有的类都是在对其第一次使用时,动态加载到JVM中的。Java程序在它开始运行之前并非被完全加载,其各个部分是在必需时才加载的。当程序创建第一个对类的静态成员的引用时,就会加载这个类。这个证明构造器也是类的静态方法,即使在构造器之前并没有使用static关键字。

1.2.1 类加载器工作流程

类加载流程:加载、验证、准备、解析、初始化(静态方法、静态代码块)。

  1. 类加载器首先检查这个类的Class对象是否已经加载。
  2. 如果尚未加载,默认的类加载器就会根据类名查找.class文件(例如,某个附加类加载器可能会在数据库中查找字节码)。
  3. 在这个类的字节码被加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良Java代码(这是Java中用于安全防范目的的措施之一)。
1.2.2 获得Class对象引用的方法
  • 通过Class.forName(),就是一个便捷途径,这种方式不需要为了获得Class对象引用而持有该类型的对象。(即没有创建过或没有这个类型的对象的时候就可以获得Class对象引用。)
  • 如果已经有一个类型的对象,那就可以通过调用这个对象的getClass()方法来获取它的Class对象引用了。这个方法属于Object,返回表示该对象的实际类型的Class对象引用。
  • 使用类字面常量.class是获取Class对象引用的另一种方法。如 FancyToy.class。建议使用这种方法。
1.2.4 Class包含的有用的方法

以下程序展示Class包含的很多有用的方法:

  •     getName() 获取类的全限定名称
  •     getSimpleName() 获取不含包名的类名
  •     getCanonicalName() 获取全限定的类名
  •     isInterface() 判断某个Class对象是否是接口
  •     getInterfaces() 返回Class对象实现的接口数组
  •     getSuperClass() 返回Class对象的直接基类
  •     newInstance() 创建一个这个Class对象所代表的类的一个实例对象。

        Class引用在编译期不具备任何更进一步的类型信息,所以它返回的只是一个Object引用,但是这个Object引用指向的是这个Class引用所代表的具体类型。即需要转型到具体类型才能给它送Object以外的消息。
        newInstance()这个方法依赖于Class对象所代表的类必须具有可访问的默认的构造函数(Nullary constructor,即无参的构造器),否则会抛出InstantiationException 或IllegalAccessException 异常
 

package net.mrliuli.rtti;



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

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

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

public class ToyTest {
    static void printInfo(Class cc){
        System.out.println("Class name: " + cc.getName() + " is interface? [" + cc.isInterface() + "]");
        System.out.println("Simple name: " + cc.getSimpleName());
        System.out.println("Canonical name: " + cc.getCanonicalName());
    }
    public static  void main(String[] args){
        Class c = null;
        try{
            c = Class.forName("net.mrliuli.rtti.FancyToy");
        }catch (ClassNotFoundException e){
            System.out.println("Can't find FancyToy");
            System.exit(1);
        }
        printInfo(c);
        System.out.println("=============================");
        for(Class face : c.getInterfaces()){
            printInfo(face);
        }
        System.out.println("=============================");
        Class up = c.getSuperclass();
        Object obj = null;
        try{
            // Requires default constructor:
            obj = up.newInstance();
        }catch (InstantiationException e){
            System.out.println("Cannot instantiate");
            System.exit(1);
        }catch (IllegalAccessException e){
            System.out.println("Cannot access");
            System.exit(1);
        }
        printInfo(obj.getClass());
    }
}

1.2.5 初始化惰性

        初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常数静态域进行首次引用时才执行,即初始化有效地实现了尽可能 的“惰性”。
        以下程序证实了上述观点。注意,将一个域设置为static 和 final的,不足以成为“编译期量”或“常数静态域”,如 static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);就不是编译期常量,对它的引用将强制进行类的初始化。

package net.mrliuli.rtti;

import java.util.Random;

class Initable{
    static final int staticFinal = 47;      // 常数静态域
    static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);     // 非常数静态域(不是编译期常量)
    static{
        System.out.println("Initializing Initable");
    }
}

class Initable2{
    static int staticNonFinal = 147;    // 非常数静态域
    static {
        System.out.println("Initializing Initable2");
    }
}

class Initable3{
    static int staticNonFinal = 74;     // 非常数静态域
    static {
        System.out.println("Initializing Initable3");
    }
}

public class ClassInitialization {

    public static Random rand = new Random(47);
    public static void main(String[] args) throws Exception {
        Class initalbe = Initable.class;                // 使用类字面常量.class获取Class对象引用,不会初始化
        System.out.println("After creating Initable ref");
        System.out.println(Initable.staticFinal);       // 常数静态域首次引用,不会初始化
        System.out.println(Initable.staticFinal2);      // 非常数静态域首次引用,会初始化
        System.out.println(Initable2.staticNonFinal);   // 非常数静态域首次引用,会初始化
        Class initable3 = Class.forName("net.mrliuli.rtti.Initable3");      // 使用Class.forName()获取Class对象引用,会初始化
        System.out.println("After creating Initable3 ref");
        System.out.println(Initable3.staticNonFinal);   // 已初始化过
    }

}
1.3 泛化的Class引用 1.3.1 Class对象类型限制

        Class引用总是指向某个Class对象,此时,这个Class对象可以是各种类型的,当使用泛型语法对Class引用所指向的Class对象的类型进行限定时,这就使得Class对象的类型变得具体,这样编译器编译时也会做一些额外的类型检查工作。如

package net.mrliuli.rtti;

public class GenericClassReferences {
    public static void main(String[] args){
        Class intClass = int.class;
        Class genericIntClass = int.class;
        genericIntClass = Integer.class;    // Same thing
        intClass = double.class;
        // genericIntClass = double.class;  // Illegal, genericIntClass 限制为Integer 的Class对象
    }
}
1.3.2 使用通配符?放松对Class对象类型的限制

        通配符?是Java泛型的一部分,?表示“任何事物”。以下程序中Class intClass = int.class; 与 Class intClass = int.class; 是等价的,但使用Class优于使用Class,因为它说明了你是明确要使用一个非具体的类引用,才选择了一个非具体的版本,而不是由于你的疏忽。

package net.mrliuli.rtti;


public class WildcardClassReferences {
    public static void main(String[] args){
        Class intClass = int.class;
        intClass = double.class;
    }
}
1.3.3 类型范围

 将通配符与extends关键字相结合如Class,就创建了一个范围,使得这个Class引用被限定为Number类型或其子类型。

package net.mrliuli.rtti;


public class BoundedClassReferences {
    public static void main(String[] args){
        Class bounded = int.class;
        bounded = double.class;
        bounded = Number.class;
        // Or anything derived from Number
    }
}

  泛型类语法示例:

package net.mrliuli.rtti;

import java.util.ArrayList;
import java.util.List;



class CountedInteger{
    private static long counter;
    private final long id = counter++;
    public String toString(){
        return Long.toString(id);
    }
}

public class FilledList {
    private Class type;
    public FilledList(Class type){
        this.type = type;
    }
    public List create(int nElements){
        List result = new ArrayList();
        try{
            for(int i = 0; i < nElements; i++){
                result.add(type.newInstance());
            }
        }catch(Exception e){
            throw new RuntimeException(e);
        }
        return result;
    }
    public static void main(String[] args){
        FilledList fl = new FilledList(CountedInteger.class);   // 存储一个类引用
        System.out.println(fl.create(15));      // 产生一个list
    }
}

总结,使用泛型类后

  • 使得编译期进行类型检查
  • .newInstance()将返回确切类型的对象,而不是Object对象
14.3 类型转换前先做检查

RTTI有三种形式。

  • 传统的类型转换,由RTTI确保类型转换的正确性,如果执行了错误的类型转换,会抛出一个ClassCastException的异常。
  • 代表对象类型的Class对象,通过查询Class对象获取所需要的信息
  • 类型转换前用instanceof检查

前面我们讲了前两种,现在我们开始讲解第三种instanceof的形式:

        它返回一个布尔值,告诉我们对象是不是某个特定类型或其子类。如if(x instanceof Dog)语句会检查对象x是否从属于Dog类。
        还一种形式是动态的instanceof:Class.isInstance()方法提供了一种动态地测试对象的途径。Class.isInstance()方法使我们不再需要instanceof表达式。

14.4 注册工厂

14.5 instanceof 与 Class 的等价性

        在查询类型信息时,以instanceof的形式(即以instanceof的形式或isInstance0的形式,它们产生相同的结果)与直接比较Class对象有一个很重要的差别。下面的例子展示了这种差别:

//: typeinfo/FamilyVsExactType.java
// The difference between instanceof and class
package typeinfo;
import static net.mindview.util.Print.*;

class Base {}
class Derived extends Base {}	

public class FamilyVsExactType {
  static void test(Object x) {
    print("Testing x of type " + x.getClass());
    print("x instanceof Base " + (x instanceof Base));
    print("x instanceof Derived "+ (x instanceof Derived));
    print("Base.isInstance(x) "+ Base.class.isInstance(x));
    print("Derived.isInstance(x) " +
      Derived.class.isInstance(x));
    print("x.getClass() == Base.class " +
      (x.getClass() == Base.class));
    print("x.getClass() == Derived.class " +
      (x.getClass() == Derived.class));
    print("x.getClass().equals(Base.class)) "+
      (x.getClass().equals(Base.class)));
    print("x.getClass().equals(Derived.class)) " +
      (x.getClass().equals(Derived.class)));
  }
  public static void main(String[] args) {
    test(new Base());
    test(new Derived());
  }	
} 

//:~
  • instanceof 和 isInstance() 保持了类型的概念,它指的是“你是这个类吗,或者你是这个类的派生类吗?”
  • == 和 equals() 没有考虑继承——它要么是这个确切的类型,要么不是。

14.6 反射:运行时的类信息

发射的概念:在程序运行时,动态的获取类的属性和方法。

        Class类与jaya.lang.reflect类库一起对反射的概念进行了支持,该类库包含了Field、Method以及Constructor类(每个类都实现了Member接口)。这些类型的对象是由JVM在运行时创建的,用以表示未知类里对应的成员。这样你就可以使用Constructor创建新的对象,用get0和setO方法读取和修改与Field对象关联的字段,用invoke0)方法调用与Method对象关联的方法。另外,还可以调用getFields0、getMethods0和getConstructors0等很便利的方法,以返回表示字段、方法以及构造器的对象的数组(在JDK文档中,通过查找Class类可了解更多相关资料)。这样,匿名对象的类信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情。

        而对于反射机制来说,.class文件在编译时是不可获取的,所以是在运行时打开和检查.class文件。

1.1 类方法提取器

        反射机制提供了一种方法,使我们能够编写可以自动展示完整接口的简单工具。面就是其工作方式:

//: typeinfo/ShowMethods.java
// Using reflection to show all the methods of a class,
// even if the methods are defined in the base class.
// {Args: ShowMethods}
import java.lang.reflect.*;
import java.util.regex.*;
import static net.mindview.util.Print.*;

public class ShowMethods {
  private static String usage =
    "usage:n" +
    "ShowMethods qualified.class.namen" +
    "To show all methods in class or:n" +
    "ShowMethods qualified.class.name wordn" +
    "To search for methods involving 'word'";
  private static Pattern p = Pattern.compile("\w+\.");
  public static void main(String[] args) {
    if(args.length < 1) {
      print(usage);
      System.exit(0);
    }
    int lines = 0;
    try {
      Class c = Class.forName(args[0]);
      Method[] methods = c.getMethods();
      Constructor[] ctors = c.getConstructors();
      if(args.length == 1) {
        for(Method method : methods)
          print(
            p.matcher(method.toString()).replaceAll(""));
        for(Constructor ctor : ctors)
          print(p.matcher(ctor.toString()).replaceAll(""));
        lines = methods.length + ctors.length;
      } else {
        for(Method method : methods)
          if(method.toString().indexOf(args[1]) != -1) {
            print(
              p.matcher(method.toString()).replaceAll(""));
            lines++;
          }
        for(Constructor ctor : ctors)
          if(ctor.toString().indexOf(args[1]) != -1) {
            print(p.matcher(
              ctor.toString()).replaceAll(""));
            lines++;
          }
      }
    } catch(ClassNotFoundException e) {
      print("No such class: " + e);
    }
  }
} //:~

 

14.7 动态代理

        代理是基本的设计模式之一,它是你为了提供额外的或不同的操作,而插人的用来代替“实际”对象的对象。这些操作通常涉及与“实际”对象的通信,因此代理通常充当着中间人的角色。下面是一个用来展示代理结构的简单示例:

//: typeinfo/SimpleProxyDemo.java
import static net.mindview.util.Print.*;

interface Interface {
  void doSomething();
  void somethingElse(String arg);
}

class RealObject implements Interface {
  public void doSomething() { print("doSomething"); }
  public void somethingElse(String arg) {
    print("somethingElse " + arg);
  }
}	

class SimpleProxy implements Interface {
  private Interface proxied;
  public SimpleProxy(Interface proxied) {
    this.proxied = proxied;
  }
  public void doSomething() {
    print("SimpleProxy doSomething");
    proxied.doSomething();
  }
  public void somethingElse(String arg) {
    print("SimpleProxy somethingElse " + arg);
    proxied.somethingElse(arg);
  }
}	

class SimpleProxyDemo {
  public static void consumer(Interface iface) {
    iface.doSomething();
    iface.somethingElse("bonobo");
  }
  public static void main(String[] args) {
    consumer(new RealObject());
    consumer(new SimpleProxy(new RealObject()));
  }
} //:~

        因为consumer(接受的Interface,所以它无法知道正在获得的到底是RealObject还是SimpleProxy,因为这二者都实现了Interface。但是SimpleProxy已经被插入到了客户端和RealObject之间,因此它会执行操作,然后调用RealObject上相同的方法。

        在任何时刻,只要你想要将额外的操作从“实际〞对象中分离到不同的地方,特别是当你希望能够很容易地做出修改,从没有使用额外操作转为使用这些操作,或者反过来时,代理就显得很有用(设计模式的关键就是封装修改——因此你需要修改事务以证明这种模式的正确性)。例如,如果你希望跟踪对RealObject中的方法的调用,或者希望度最这些调用的开销,那么你应该怎样做呢?这些代码肯定是你不希望将其合并到应用中的代码,因此代理使得你可以很容易地添加或移除它们。

        Java的动态代理比代理的思想更向前迈进了一步,因为它可以动态地创建代理并动态地处理对所代理方法的调用。在动态代理上所做的所有调用都会被重定向到单一的调用处理器上,它的工作是揭示调用的类型并确定相应的对策。下面是用动态代理重写的SimpleProxyDemojava:

//: typeinfo/SimpleDynamicProxy.java
import java.lang.reflect.*;

class DynamicProxyHandler implements InvocationHandler {
  private Object proxied;
  public DynamicProxyHandler(Object proxied) {
    this.proxied = proxied;
  }
  public Object
  invoke(Object proxy, Method method, Object[] args)
  throws Throwable {
    System.out.println("**** proxy: " + proxy.getClass() +
      ", method: " + method + ", args: " + args);
    if(args != null)
      for(Object arg : args)
        System.out.println("  " + arg);
    return method.invoke(proxied, args);
  }
}	

class SimpleDynamicProxy {
  public static void consumer(Interface iface) {
    iface.doSomething();
    iface.somethingElse("bonobo");
  }
  public static void main(String[] args) {
    RealObject real = new RealObject();
    consumer(real);
    // Insert a proxy and call again:
    Interface proxy = (Interface)Proxy.newProxyInstance(
      Interface.class.getClassLoader(),
      new Class[]{ Interface.class },
      new DynamicProxyHandler(real));
    consumer(proxy);
  }
} //:~

        通过调用静态方法Proxy.newProxyInstanceO可以创建动态代理,这个方法需要得到一个类加载器(你通常可以从已经被加载的对象中获取其类加载器,然后传递给它),一个你希望该代理实现的接口列表(不是类或抽象类),以及InvocationHlandler接口的一个实现。动态代理可以将所有调用重定向到调用处理器,因此通常会向调用处理器的构造器传递给一个“实际”对象的引用,从而使得调用处理器在执行其中介任务时,可以将请求转发。

        invoke0方法中传递进来了代理对象,以防你需要区分请求的来源,但是在许多情况下,你并不关心这一点。然而,在invokeO内部,在代理上调用方法时需要格外当心,因为对接口的调用将被重定向为对代理的调用。

        通常,你会执行被代理的操作,然后使用Method.invokeO将请求转发给被代理对象,并传人必需的参数。这初看起来可能有些受限,就像你只能执行泛化操作一样。但是,你可以通过传递其他的参数,来过滤某些方法调用:

//: typeinfo/SelectingMethods.java
// Looking for particular methods in a dynamic proxy.
import java.lang.reflect.*;
import static net.mindview.util.Print.*;

class MethodSelector implements InvocationHandler {
  private Object proxied;
  public MethodSelector(Object proxied) {
    this.proxied = proxied;
  }
  public Object
  invoke(Object proxy, Method method, Object[] args)
  throws Throwable {
    if(method.getName().equals("interesting"))
      print("Proxy detected the interesting method");
    return method.invoke(proxied, args);
  }
}	

interface SomeMethods {
  void boring1();
  void boring2();
  void interesting(String arg);
  void boring3();
}

class Implementation implements SomeMethods {
  public void boring1() { print("boring1"); }
  public void boring2() { print("boring2"); }
  public void interesting(String arg) {
    print("interesting " + arg);
  }
  public void boring3() { print("boring3"); }
}	

class SelectingMethods {
  public static void main(String[] args) {
    SomeMethods proxy= (SomeMethods)Proxy.newProxyInstance(
      SomeMethods.class.getClassLoader(),
      new Class[]{ SomeMethods.class },
      new MethodSelector(new Implementation()));
    proxy.boring1();
    proxy.boring2();
    proxy.interesting("bonobo");
    proxy.boring3();
  }
} //:~

        这里,我们只查看了方法名,但是你还可以查看方法签名的其他方面,甚至可以搜索特定的参数值。

        动态代理并非是你日常使用的工具,但是它可以非常好地解决某些类型的问题。你在《Thinking in Patterns》(查看www.MindView.net)和Erich Gamma等人撰写的《DesignPatterns》°这两本书中了解到有关代理和其他设计模式的更多知识。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1031873.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号