Java 编程思想笔记:Learn 10

2018-05-13  本文已影响0人  智勇双全的小六

第 14 章 类型信息

运行时类型信息使得你可以在程序运行时发现和使用类型信息

Java 在运行时识别对象和类有两种方式:

14.1 为什么需要 RTTI

javaThought.png

面向对象编程中的基本目的是:让代码只操纵对基类(这里是 Shape)的引用。这样,如果要添加一个新类(从 Shape 派生的rhomboid 来扩展程序),不会影响到原来的代码。

在这个例子中 Shape 接口中动态绑定了 draw() 方法,目的就是让客户端程序员使用泛化的 Shape 引用来调用 draw(). draw() 在所有的派生类里面都会被覆盖,并且它是被动态绑定的,所以即便通过泛化的 Shape 引用来调用,也能产生正确的行为。这就是多态。

因此,通常会创建一个具体对象(Circle, Square, Triagnle), 把它向上转型成 Shape(忽略对象的具体类型),并在后面的程序使用匿名(即不知道具体类型)的 Shape 引用。

public class Shapes {
    public static void main(String[] args){
        List<Shape> shapeList = Arrays.asList(new Circle(), new Square(), new Triangle());

        for(Shape shape : shapeList){
            shape.draw();
        }
    }
}

abstract class Shape{
    void draw(){
        System.out.println(this + ".draw()");
    }
    abstract public String toString();
}

class Square extends Shape{

    @Override
    public String toString() {
        return "Square{}";
    }
}

class Triangle extends Shape{

    @Override
    public String toString() {
        return "Triangle{}";
    }
}

class Circle extends Shape{

    @Override
    public String toString() {
        return "Circle{}";
    }
}

基类中包含 draw() 方法,它通过传递 this 参数给 System.out.println, 间接地使用 toStirng() 打印标识类符(注意,toString() 被声明为 abstract, 以此强制继承者复写该方法,并可以防止对无格式的 shape 的实例化)。

如果某个对象出现在字符串表达式中(涉及 “+” 和字符串对象的表达式),toString() 方法会被自动调用,以生成表示该对象的 String。每个派生类都要覆盖(从Object继承来的)toString() 方法,这样 draw() 在不同情况下就打印出不同的消息。——这也就是多态。

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

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

在这个例子中,RTTI 类型转化并不彻底:Object被转型为Shape,而不是转型为 Circle / Square / Triangle。这是因为目前我们只知道这个List<Shape>保存的是 Shape。在编译时,将由容器和 Java 的泛型系统来强制确保这一点;而在运行时,由类型转化操作来确定这一点。

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

但是如果想知道某个泛化引用的确切类型,可以使用 RTTI,查询某个 Shape 引用所指向的对象的确切类型。

14.2 Class 对象

要理解 RTTI 在 Java 中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为 Class 对象的特殊对象完成的,它包含了与类有关的信息。事实上,Class 对象就是来创建类的所有的 “常规”对象的。Java 使用 Class 对象来执行其 RTTI,即使你正在执行的是类似转换这样的操作。Class 类还拥有大量的使用 RTTI 的其他方式。

类是程序的一部分,每个类都有一个 Class 对象。换言之,每当编写并编译了一个新类,就会产生一个 Class 对象(更恰当地说,是被保存在同名的 .class 文件中)。为了生成这个类的对象,运行这个程序的 Java 虚拟机将使用 “类加载器” 的子系统。

类加载器子系统实际上可以包含了哦一条类加载器链,但是只有一个是原生类加载器,它是 JVM 实现的一部分。原生类加载器加载是所谓的可信类,包括 Java API 类,通常是从本地盘加载的。

所有类都是在对其第一次使用时,动态加载到 JVM 中的。当程序创建第一个对类的静态成员时,就会加载这个类。这个证明构造器也是类的静态方法,即使在构造器之间并没有使用 static 关键字。因此,使用 new 操作符创建类的新对象也会被当作对类的静态成员的引用。

因此,Java 程序在它开始运行之前并非被完全加载,其各个部分是在必需时才加载的。类加载器首先查看这个类的 Class 对象是否已经加载。如果尚未加载,默认的类加载器就会根据类名查找 .class文件。在这个类的字节码被加载时,它们会接受验证,以确保没有被破坏。

一旦某个类的 Class 对象被载入内存,它就被用来创建这个类的所有对象。

public class SweetShop {

    public static void main(String[] args){
        System.out.println("inside main");
        new Candy();
        System.out.println("after creating candy");
        try{
            Class.forName("Gum");
        }catch (ClassNotFoundException e){
            System.out.println("Could not find Gum");
        }
        System.out.println("After class for name Gum");
        new Cookie();
        System.out.println("After creating Cookie");
    }
}

class Candy{
    static {System.out.println("loading candy");}
}

class Cookie{
    static {System.out.println("loading Cookie");}
}

从输出中可以看出,Class 对象仅在需要的时候才被加载,static 初始化是在类加载时进行的。

Class.forName("Gum") 是 Class 类(所有Class 对象都属于这个类)的一个 static 成员。 Class 对象就和其他对象一样,我们可以获取并操作它的引用。(这也是类加载器的工作)。forName 是取得 Class 对象引用的一种方法。它是用一个包含目标类的文本名(注意区分大小写和拼写)的 String 作为参数,返回的是一个 Class 对象的引用,上面的代码忽略了返回值。对 forName() 的调用是为了它产生的副作用。如果类 Gum 还没被加载就加载它。在加载的过程中,Gum 的static 子句被执行。

在前面的例子里,如果Class.forName() 找不到你要加载的类,它会抛出异常 ClassNotFoundException.

无论何时,只要你想运行时使用类型信息,就必须首先获得对恰当 Class 对象的引用。Class.forName() 就是实现此功能的便捷途径,因为你不需要为了获得 Class 引用而持有该类型的对象。但是,如果你已经拥有了一个感兴趣的类型对象,那就可以通过调用 getClass() 方法来获取 Class 引用了,这个方法属于根类 Object的一部分,它将返回该对象的实际类型的Class 引用。Class 对象包含了很多有用的方法,下面是其中一部分:

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("com.zzjack.rdsapi_demo.javathought.FancyToy");
        } catch (ClassNotFoundException e){
            System.out.println("can not find Fancy");
        }
        printInfo(c);
        for(Class face : c.getInterfaces()){
            printInfo(face);
        }
        Class up = c.getSuperclass();
        Object obj = null;
        try{
            obj = up.newInstance();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        System.out.println(".....................");
        printInfo(obj.getClass());
    }
}

interface HasBatterirs{}

interface Waterproof{}

interface Shoots{}

class FancyToy extends Toy implements HasBatterirs, Waterproof, Shoots{
    FancyToy(){}
}

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

这个例子中体现的 Class 的方法如下:

Class.getName()  是获取的 对象 的完整链路名,
Class.isInterface() 是判断是否为接口
Class.getSimpleName() 仅仅获取调用对象的名称,
Class.getCanonicalName() 获取对象的全链路名
c = Class.forName() 获取对象的引用
c.getInterfaces() 获取对象的所有接口
c.getSuperClass() 获取父类
obj = c.newInstance() 把类实例化
obj.getClass() 获取这个实例的类对象

14.2.1 类字面常量

Java 还提供了另外一种方法来生成对 Class 对象的引用,即类字面常量。即 FancyToy.Class

这样不仅简单,而且更安全。因为在编译时就会受到检查,所以不用放到 try 语句块中,并且它根除了对 forName() 方法的调用,所以也更高效。

class type
boolean.class Boolean.TYPE
char.class Character.TYPE
byte.class Byte.TYPE
short.class Short.TYPE
int.class Integer.TYPE
long.class Long.TYPE
float.class Float.TYPE
double.class Float.TYPE
double.class Double.TYPE
void.class Void.TYPE

类字面常量不仅可以用于普通的类,还可以用于接口、数组和基本数据类型。另外,对于基本数据类型的包装类,还有一个标准字段 TYPE。TYPE 是一个引用,指向对应的基本数据类型的 Class 对象,如下图所示:

class type
boolean.class Boolean.TYPE
char.class Character.TYPE
byte.class Byte.TYPE
short.class Short.TYPE
int.class Integer.TYPE
long.class Long.TYPE
float.class Float.TYPE
double.class Float.TYPE
double.class Double.TYPE
void.class Void.TYPE

我建议使用 ".class" 的形式,以保持与普通类的一致性。
注意,有一点很有趣,当使用 “.class” 来创建对 Class 对象的引用时,不会自动地初始化该 Class 对象。为了使用类而做的准备工作实际包含3个步骤:

  1. 加载。这是由类加载器执行的。该步骤将查找字节码(通常在 classpath 所指定的路径中查找,但这并非是必要的),并从这些字节码中创建一个 Class 对象。
  2. 链接。在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用。
  3. 初始化。如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。

初始化被延迟到了对静态方法(构造器隐式地是静态的)或者非常数静态域进行首次引用时才执行:

class Initable{
    static final int staticFinal = 47;
    static final int getStaticFinal2 =
            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 initable = Initable.class;
        System.out.println("After creating Initable ref");
        System.out.println(Initable.staticFinal);
        System.out.println(Initable2.staticNonFinal);
        System.out.println(Initable3.staticNonFinal);
        Class initable3 = Class.forName("Initable3");
        System.out.println("After creating Initable3 ref");
//        System.out.println(initable3.staticNonFinal);
    }
}

初始化有效地实现了尽可能的 “惰性”。从对 initable 引用的创建中可以看到,仅使用.class 语法来获得对类的引用不会引发初始化。但是,为了产生 Class 引用,Class.forName() 立即就进行了初始化,就像在对 initable3 引用的创建中所看到的。

如果一个 static final 值是 “编译期常量”,就像 Initable.staticFinal 那样,那么这个值不需要对 Initable 类进行初始化就可以被读取。但是,如果只是将一个域设置为 static 和 final 的,还不足以确保这种行为,例如,对 Initable.staticFinal2 的访问将强制进行类的初始化,因为它不是一个编译期常量。

如果一个 static 域不是 final的,那么对它访问时,总是要求在它被读取之前,要先进行链接(为这个域分配存储空间)和初始化(初始化该存储空间),就像在对 Initable2.staticNonFinal 的访问中所看到的那样。

14.2.2 泛化的 Class 引用

Class 引用总是指向某个 Class 对象,它可以制造类的实例,并包含可作用于这些实例的所有方法代码。它还包含该类的静态成员,因此,Class 引用表示的就是它所指向的对象的确切类型,而该对象便是 Class 类的一个对象。

但是在 java5 中,也可以通过泛型,使得它的类型更加具体,以下两种写法是相等的:

public class GenericClassReference{
  public static void main(String[] args){
      Class intClass = int.class;
      Class<Integer> integerClass = int.class;
      intClass = integerClass;
  }
}

普通类引用不会产生警告信息,尽管泛型类引用只能赋值为指向其声明的类型,但是普通的类引用可以被重新赋值为指向任何其他的 Class 对象。通过使用泛型语法,可以让编译器强制执行额外的类型检查。

Class<Number> genericNumberClass = int.class;

这看起来似乎是起作用的,因为 Integer 继承自 Number。但是它无法工作,因为 Integer Class 对象不是 Number Class 对象的子类。

为了在使用泛化的 Class 饮用时放松限制,可以使用通配符“?”,表示任何事物。使用通配符 “?” 来改写上面的例子:

public class WildcardClassReference{
  public static void main(String[] args){
    Class<?> intClass = int.class;
    intClass = double.class;
  }
}

在 Java5 中,Class<?> 优于平凡的 Class,即便它们是等价的。Class<?> 的好处在于它表示你并非疏忽,而是使用了一个非具体的类引用。

为了创建一个 Class 引用,它被限定为某种类型,或该类型的任何子类型,你需要将通配符与 extends 关键字相结合,创建一个范围。因此,与仅仅声明Class<Number>不同,现在做如下:

public class BoundedClassReference{
  public static void main(String[] args){
      Class<? extends Number> bounded = int.class;
      bounded = double.class;
      bounded = Number.class;
  }
}

向 Class 引用添加泛型语法的原因仅仅是为了提供编译期类型检查,如果你操作有误,稍后立即就会发现这一点。

下面的例子使用了泛型类语法。它存储了一个类引用,稍后又产生了一个 List,填充这个 List 对是使用了 newInstance() 方法,通过该引用生成的:

14.2.3 新的转型语法

public class ClassCasts {
    public static void main(String[] args){
        Building b = new House();
        Class<House> houseClass = House.class;
        House h = houseClass.cast(b);
        h = (House) b;
    }
}

class Building {}

class House extends Building{}

cast() 方法接受参数对象,并将其转型为Class引用的类型。

14.3 类型转化钱先做的检查

RTTI形式:

  1. 传统的类型转化,如 "(Shape)", 由于RTTI确保类型转化的正确性,如果执行了一个错误的类型转换,就会抛出一个 ClassCastException 异常。

  2. 代表对象的类型的 Class 对象。也就是向下转型,因为这个操作是安全的,可以由编译器自动完成

  3. 使用关键词 instanceof。返回1个布尔值,告诉我们对象是不是某个特定类型的实例。

if (x instanceof Dog){
  ((Dog)x)bark()
}

在将x转型成一个Dog之前,上面的if语句会检查对象x是否属于Dog类。

14.3.1 使用类字面常量

pass

14.6 反射:运行时的类信息

Class 类与 java.lang.reflect 类库一起对反射的概念进行了支持,该类库包含了 Field / Method / Constructor 类(每个类都实现了Member接口。)这些类型的对象是由 JVM 在运行时创建的,泳衣表示未知类里的对应的成员。这样就可以使用 Constructor 创建新的对象,用 get 和 set 方法读取和修改与Field对象关联的字段,用 invoke 方法调用与Method 对象关联的方法。另外,还可以调用 getFields()、getMethods() 和 getConstructors() 等很便利的方法,以返回表示字段、方法以及构造器的对象的数组。这样,匿名对象的类信息就能在运行时被完全确定下来。

重要的是,要认识到反射机制与RTTI的真正区别。对RTTI来说,编译器在编译时打开和检查 .class 文件。(换句话说,我们可以用“普通”方式调用对象的所有方法)。而对于反射机制来说,.class 文件在编译时不可获取的,所以在运行时打开和检查 .class 文件。

14.6.1 类方法提取器

public class ShowMethods {

    private static String usage = "useage:" +
            "Show Method qualified class.name\n" +
            "to show all methods in class or";

    private static Pattern p = Pattern.compile("\\w+ \\.");

    public static void main(String[] args){
        if(args.length < 1){
            System.out.println(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){
                    System.out.println(
                            p.matcher(method.toString()).replaceAll("")
                    );
                }
                for(Constructor constructor : ctors){
                    System.out.println(
                            p.matcher(constructor.toString()).replaceAll("")
                    );
                }
            } else{
                for(Method method : methods){
                    if(method.toString().indexOf(args[1]) != -1){
                        System.out.println(
                                p.matcher(method.toString()).replaceAll("")
                        );
                        lines++;
                    }
                }
            }
        }catch (ClassNotFoundException ex){
            System.out.println("No such class: " + ex);
        }
    }
}

Class 的 getMethods() 和 getConstructors() 方法分别返回 Method 对象的数组和Construcotr 对象的数组。这两个类都提供了深层方法,用以解析其对象所代表的方法,并获取其名字、输入参数以及返回值。

14.7 动态代理

代理是基本的设计模式之一,它是你为了提供额外的或不同的操作,而插入的用来替代 “实际” 对象的对象。

public 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()));
    }
}

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

class SimpleProxy implements Interface{
    private Interface proxied;

    public SimpleProxy(Interface proxied){
        this.proxied = proxied;
    }

    public void somethingElse(String arg){
        System.out.println("SimpleProxy somethingElse " + arg);
        proxied.somethingElse(arg);
    }

    public void doSomething(){
        System.out.println("SimpleProxy doSomething");
        proxied.doSomething();
    }
}

class RealObject implements Interface{
    public void doSomething(){
        System.out.println("do something");
    }

    public void somethingElse(String arg){
        System.out.println("somethingElse " + arg);
    }
}

简单的代理,就是 consumer 接受的 interface,所以它能够接受任何实现了 Interface 的类。SimpleProxied 也是一个实现了 Interface 的类,它接受一个实现了 Interface 的类作为构造函数的参数。说白了,consumer 和 SimpleProxied 一共把 Interface 包了两层。

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(" " + args);
          }
      }
      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 RealObejct();
      consumer(real);
      // Insert a proxy and call again
      Interface proxy = (Interface)Proxy.newProxyInstance(
         Interace.class.getClassLoader(),
         new Class[] { Inteface.class},
         new DynamicProxyHandler(real));
      consumer(proxy);
  }
}

通过调用静态方法 Proxy.newProxyInstance() 可以创建动态代理,这个方法需要得到:

动态代理可以将所有调用重定向到调用处理器,因此通常会向调用处理器的构造器传递一个 “实际”对象,从而使得调用着处理器在执行其中介任务时,可以将请求转发。

14.9 接口与类型信息

Interface 关键字的一种重要目标是允许程序员隔离构件,进而降低耦合性。但是通过类型信息,这种耦合性还是会传播出去——接口并非是对解耦的一种无懈可击的保证。

具体没看懂,大致就是说还是可以通过反射拿到私有属性。

上一篇下一篇

猜你喜欢

热点阅读