《On Java 8》笔记 第十九章-类型信息##

RTTI

import java.util.stream.*;

abstract class Shape {
    void draw() { System.out.println(this + ".draw()"); }
    @Override
    public abstract 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) {
        Stream.of(
            new Circle(), new Square(), new Triangle())
            .forEach(Shape::draw);
    }
}

>输出结果:
Circle.draw()
Square.draw()
Triangle.draw()

这个例子中,在把 Shape 对象放入 Stream 中时就会进行向上转型(隐式),但在向上转型的时候也丢失了这些对象的具体类型。对 stream 而言,它们只是 Shape 对象。

严格来说,Stream 实际上是把放入其中的所有对象都当做 Object 对象来持有,只是取元素时会自动将其类型转为 Shape。这也是 RTTI 最基本的使用形式,因为在 Java 中,所有类型转换的正确性检查都是在运行时进行的。这也正是 RTTI 的含义所在:在运行时,识别一个对象的类型。

另外在这个例子中,类型转换并不彻底:Object 被转型为 Shape ,而不是 Circle、Square 或者 Triangle。这是因为目前我们只能确保这个 Stream 保存的都是 Shape:

  • 编译期,stream 和 Java 泛型系统确保放入 stream 的都是 Shape 对象(Shape 子类的对象也可视为 Shape 的对象),否则编译器会报错;
  • 运行时,自动类型转换确保了从 stream 中取出的对象都是 Shape 类型。

Class 对象

要理解 RTTI 在 Java 中的工作原理,首先必须知道类型信息在运行时是如何表示的。这项工作是由称为 Class对象 的特殊对象完成的,它包含了与类有关的信息。实际上,Class 对象就是用来创建该类所有"常规"对象的。Java 使用 Class 对象来实现 RTTI,即便是类型转换这样的操作都是用 Class 对象实现的。不仅如此,Class 类还提供了很多使用 RTTI 的其它方式。

类是程序的一部分,每个类都有一个 Class 对象。换言之,每当我们编写并且编译了一个新类,就会产生一个 Class 对象(更恰当的说,是被保存在一个同名的 .class 文件中)。为了生成这个类的对象,Java 虚拟机 (JVM) 先会调用"类加载器"子系统把这个类加载到内存中。

类加载器子系统可能包含一条类加载器链,但有且只有一个原生类加载器,它是JVM实现的一部分。原生类加载器加载的是”可信类”(包括 Java API 类)。它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,但是如果你有特殊需求(例如以某种特殊的方式加载类,以支持 Web 服务器应用,或者通过网络下载类),也可以挂载额外的类加载器。

所有的类都是第一次使用时动态加载到 JVM 中的,当程序创建第一个对类的静态成员的引用时,就会加载这个类。

因此,Java 程序在它开始运行之前并没有被完全加载,很多部分是在需要时才会加载。这一点与许多传统编程语言不同,动态加载使得 Java 具有一些静态加载语言(如 C++)很难或者根本不可能实现的特性。

类加载器首先会检查这个类的 Class 对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找 .class 文件(如果有附加的类加载器,这时候可能就会在数据库中或者通过其它方式获得字节码)。这个类的字节码被加载后,JVM 会对其进行验证,确保它没有损坏,并且不包含不良的 Java 代码(这是 Java 安全防范的一种措施)。

一旦某个类的 Class 对象被载入内存,它就可以用来创建这个类的所有对象。下面的示范程序可以证明这点:

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

class Gum {
	static {System.out.println("Loading Gum");}
}

public class SweetShop {
	public static void main(String[] args) {
		try {
			Class.forName("Cookie");	
		}catch (ClassNotFoundException e) { }
		System.out.println("After forName(\"Cookie\")");
		
		new Gum();
		System.out.println("After new Gum()");
		
		new Cookie();
		try {
			Class.forName("Gum");	
		}catch (ClassNotFoundException e) { }
		
		System.out.println("DONE!");
	}
}

>输出结果:
Loading Cookie
After forName("Cookie")
Loading Gum
After new Gum()
DONE!

其实构造器也是类的静态方法,虽然构造器前面并没有 static 关键字。所以,使用 new 操作符创建类的新对象,这个操作也算作对类的静态成员引用。
从输出中可以看到,Class 对象仅在需要的时候才会被加载,static 初始化是在类加载时进行的。

类字面常量

当使用 .class 来创建对 Class 对象的引用时,不会自动地初始化该Class 对象。为了使用类而做的准备工作实际包含三个步骤:

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

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

import java.util.*;

class Initable {
    static final int STATIC_FINAL = 47;
    static final int STATIC_FINAL2 =
        ClassInitialization.rand.nextInt(5);
    static {
        System.out.println("Initializing Initable");
    }
}

class InitableSup {
    static int staticNonFinal = 147;
    static {
        System.out.println("Initializing InitableSup");
    }
}

class InitableSon extends InitableSup{
    static int staticNonFinal = 74;
    static {
        System.out.println("Initializing InitableSon");
    }
}

public class ClassInitialization {
    public static Random rand = new Random(47);
    public static void main(String[] args) throws Exception {
        System.out.println(Initable.STATIC_FINAL);
        Class initable = Initable.class;
        System.out.println("After creating Initable ref");

        Class son = InitableSon.class;
        System.out.println("After creating Son ref");
        System.out.println(InitableSon.staticNonFinal);
        System.out.println(son);
    }
}

>输出结果:
47
After creating Initable ref
After creating Son ref
Initializing InitableSup
Initializing InitableSon
74
class InitableSon

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

如果一个 static 域不是 final 的,那么在对它访问时,总是要求在它被读取之前,要先进行链接(为这个域分配存储空间)和初始化(初始化该存储空间),就像在对 Initable2.staticNonFinal 的访问中所看到的那样。
如果一个 static final 值是“编译期常量”(如 Initable.staticFinal),那么这个值不需要对 Initable 类进行初始化就可以被读取(Initable.staticFinal)。但是,如果只是将一个域设置成为 static 和 final,还不足以确保这种行为。例如,对 Initable.staticFinal2 的访问将强制进行类的初始化,因为它不是一个编译期常量,把上面代码替换如下:

public class ClassInitialization {
    public static Random rand = new Random(47);
    public static void main(String[] args) throws Exception {
    	// System.out.println(Initable.STATIC_FINAL);
        System.out.println(Initable.STATIC_FINAL2);
        System.out.println(InitableSup.staticNonFinal);
        Class initable = Initable.class;
        System.out.println("After creating Initable ref");

        Class son = InitableSon.class;
        System.out.println("After creating Son ref");
        System.out.println(InitableSon.staticNonFinal);
    }
}

>输出结果:
Initializing Initable
3
Initializing InitableSup
147
After creating Initable ref
After creating Son ref
Initializing InitableSon
74

泛化的 Class 引用

class Toy {
    public Toy() {}
}
class FancyToy extends Toy {
    public FancyToy() {}
}

public class WildcardClassReferences {
    public static void main(String[] args) {
        Class<FancyToy> ftClass = FancyToy.class;
        FancyToy fancyToy = ftClass.newInstance();

        Class<? super FancyToy> up = ftClass.getSuperclass();
        Object obj = up.newInstance();

        // This won't compile:
        // Class<Toy> up2 = ftClass.getSuperclass();
    }
}

如果你手头的是超类,那编译期将只允许你声明超类引用为“某个类,它是 FancyToy 的超类”,就像在表达式 Class<? super FancyToy> 中所看到的那样。而不会接收 Class 这样的声明。这看上去显得有些怪,因为 getSuperClass() 方法返回的是基类(不是接口),并且编译器在编译期就知道它是什么类型了(在本例中就是 Toy.class),而不仅仅只是“某个类,它是 FancyToy 的超类”。不管怎样,正是由于这种含糊性,up.newInstance 的返回值不是精确类型,而只是 Object。

类型转换检测

instanceof 有一个严格的限制:只可以将它与命名类型进行比较,而不能与 Class 对象作比较。
isAssignableFrom()方法与instanceof关键字的区别总结为以下两个点:

  • isAssignableFrom()方法是从类继承的角度去判断,instanceof关键字是从实例继承的角度去判断。
  • isAssignableFrom()方法是判断是否为某个类的父类,instanceof关键字是判断是否某个类的子类。
    使用方法:
父类.class.isAssignableFrom(子类.class)
子类实例 instanceof 父类类型

动态代理

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

class RealObject implements Interface {
  @Override
  public void doSomething() {
    System.out.println("doSomething");
  }
  @Override
  public void somethingElse(String arg) {
    System.out.println("somethingElse " + arg);
  }
}

class SimpleProxy implements Interface {
  private Interface proxied;
  SimpleProxy(Interface proxied) {
    this.proxied = proxied;
  }
  @Override
  public void doSomething() {
    System.out.println("SimpleProxy doSomething");
    proxied.doSomething();
  }
  @Override
  public void somethingElse(String arg) {
    System.out.println(
      "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()));
  }
}
>输出结果:
doSomething
somethingElse bonobo
SimpleProxy doSomething
doSomething
SimpleProxy somethingElse bonobo
somethingElse bonobo

当你希望将额外的操作与“真实对象”做分离时,代理可能会有所帮助,尤其是当你想要轻松地启用额外的操作时,反之亦然(设计模式就是封装变更—所以你必须改变一些东西以证明模式的合理性)。例如,如果你想跟踪对RealObject中方法的调用,或衡量此类调用的开销,该怎么办?这不是你要写入到程序中的代码,而且代理使你可以很轻松地添加或删除它。

import java.lang.reflect.*;

class DynamicProxyHandler implements InvocationHandler {
  private Object proxied;
  DynamicProxyHandler(Object proxied) {
    this.proxied = proxied;
  }
  @Override
  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,
      new DynamicProxyHandler(real));
    consumer(proxy);
  }
}
>输出结果:
doSomething
somethingElse bonobo
**** proxy: class $Proxy0, method: public abstract void
Interface.doSomething(), args: null
doSomething
**** proxy: class $Proxy0, method: public abstract void
Interface.somethingElse(java.lang.String), args:
[Ljava.lang.Object;@6bc7c054
  bonobo
somethingElse bonobo

可以通过调用静态方法Proxy.newProxyInstance()来创建动态代理,该方法需要一个类加载器(通常可以从已加载的对象中获取),希望代理实现的接口列表(不是类或抽象类),以及接口InvocationHandler的一个实现。动态代理会将所有调用重定向到调用处理程序,因此通常为调用处理程序的构造函数提供对“真实”对象的引用,以便一旦执行中介任务便可以转发请求。

invoke()方法被传递给代理对象,以防万一你必须区分请求的来源—但是在很多情况下都无需关心。但是,在invoke()内的代理上调用方法时要小心,因为通过接口的调用是通过代理重定向的。

通常执行代理操作,然后使用Method.invoke()传递必要的参数将请求转发给代理对象。这在一开始看起来是有限制的,好像你只能执行一般的操作。但是,可以过滤某些方法调用,同时传递其他方法调用:

import java.lang.reflect.*;

class MethodSelector implements InvocationHandler {
  private Object proxied;
  MethodSelector(Object proxied) {
    this.proxied = proxied;
  }
  @Override
  public Object
  invoke(Object proxy, Method method, Object[] args)
  throws Throwable {
    if(method.getName().equals("interesting"))
      System.out.println(
        "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 {
  @Override
  public void boring1() {
    System.out.println("boring1");
  }
  @Override
  public void boring2() {
    System.out.println("boring2");
  }
  @Override
  public void interesting(String arg) {
    System.out.println("interesting " + arg);
  }
  @Override
  public void boring3() {
    System.out.println("boring3");
  }
}

class SelectingMethods {
  public static void main(String[] args) {
    SomeMethods proxy =
      (SomeMethods)Proxy.newProxyInstance(
        SomeMethods.class.getClassLoader(),
        new Class,
        new MethodSelector(new Implementation()));
    proxy.boring1();
    proxy.boring2();
    proxy.interesting("bonobo");
    proxy.boring3();
  }
}
>输出结果:
boring1
boring2
Proxy detected the interesting method
interesting bonobo
boring3

在这个示例里,我们只是在寻找方法名,但是你也可以寻找方法签名的其他方面,甚至可以搜索特定的参数值。

Optional类…

原文链接:
https://lingcoder.github.io/OnJava8/#/book/19-Type-Information?id=第十九章-类型信息
https://blog.csdn.net/qq_36666651/article/details/81215221

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值