Nivelle 开拓视野冲破艰险看见世界 身临其境贴近彼此感受生活

java基础(九)之类型信息

2017-06-10
nivelle

类型信息

RTTI:在运行时,识别一个对象的类型。

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

Class对象

Java中,运行时类型信息是由Class对象来完成的,它包含了与类有关的信息。事实上,class对象就是用来创建类的所有常规对象的。java使用Class对象来执行其RTTI,即使你正在执行的是类似转型这样的操作。

类是程序的一部分,每个类都有一个Class对象。每当编写且编译了一个新类,就会产生一个Class对象(被保存在一个同名的.class文件中)为了生成这个类的对象,运行这个程序的JAVA虚拟机(JVM)将使用被称为“类加载器”的子系统。它包含一条类加载器链,但是只有一个原生类加载器,用来加载可信类,包括Java API类,它们通常是从本地磁盘加载的。

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


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

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


package learn;

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

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

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

public class SweetShop {
  public static void main(String[] args) {
    System.out.println("inside main");
    new Candy();
    System.out.println("after create Candy");
    try {
      Class.forName("learn.Gum");
    } catch (ClassNotFoundException e) {
      System.out.println("cound't find Gum");
    }
    System.out.println("after class.forName(\"Gum\")");
    new Cookie();
    System.out.println("after creating Cookie");
  }
}

结论: inside main

Loading Candy

after create Candy

loading Gum

after class.forName(“Gum”)

loading Cookie

after creating Cookie


Class.forName(“Gum”),这个方法是Class类(所有Class对象都属于这个类)的一个static成员。Class对象就和其他对象一样,我们可以获取并操作它的引用(这也就是类加载器的工作)。forName()是取得Class对象的引用的一种方法,返回的是一个Class对象的引用。如果类还没有被加载,那么就加载它。

package learn;

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

class Toy{
  public Toy() {
    
  }
  public Toy(int i){}
}
class FancyToy extends Toy implements HasBatteries,Waterproof,Shoots{
  public 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("learn.FancyToy");
    } catch (ClassNotFoundException  e) {
      System.out.println("cant find facyToy");
      System.exit(1);
    }
    printInfo(c);
    for(Class face:c.getInterfaces()){
      printInfo(face);
    }
    Class up =c.getSuperclass();
    Object obj =null;
    try{
      obj =up.newInstance();
    }catch(InstantiationException e){
      System.out.println("cant instantiate");
      System.exit(1);
    }catch (IllegalAccessException e) {
      System.out.println("cant access");
      System.exit(1);
    }
    printInfo(obj.getClass());
  }
}


Class name:learn.FancyToy is interface?false

Simple name:FancyToy

Canonical name:learn.FancyToy

Class name:learn.HasBatteries is interface?true

Simple name:HasBatteries

Canonical name:learn.HasBatteries

Class name:learn.Waterproof is interface?true

Simple name:Waterproof

Canonical name:learn.Waterproof

Class name:learn.Shoots is interface?true

Simple name:Shoots

Canonical name:learn.Shoots

Class name:learn.Toy is interface?false

Simple name:Toy

Canonical name:learn.Toy

Class的newInstance()方法实现“虚拟机构造器”的一种途径,虚拟机构造器允许声明:我不知道确切类型,但是可也能创建自己。上面例子中,up仅仅是一个Class引用,在编译期不具备。当创建实例时,会得到Object引用,但是这个引用指向的是Toy对象。

类字面常量

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

就像下面这样:

FancyToy.class

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

类字面常量不仅可以应用于普通的类,也可以应用于接口、数组以及基本数据类型。对于基本数据类型的包装器类,还有一个标准字段TYPE.TYPE字段是一个引用,指向对象的基本数据类型的Class对象。

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 === Double.Type
void.class === Void.TYPE

建议使用“.class”的形式,以保持与普通类的一致性。

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

  • 加载:类加载器执行。该步骤将查找字节码(通常在classpath所指定的路径中查找,但这并非必须的),并从这些字节码中创建一个Class对象。

  • 链接:在链接阶段将验证类中的字节码,为静态域分配存储空间,并且如果必需的话,将解析这个类创建的对其他类的所有引用。

  • 初始化:如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。

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

package learn;

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("Initiaizaing initable2");
    
  }
}

class Initable3 {

  static int staticNonFinal = 74;

  static {
  
    System.out.println("Initiaizaing initable3");
    
  }
}

public class ClassInitialization {

  public static Random rand = new Random(47);

  public static void main(String[] args) throws ClassNotFoundException {
  
    Class initable = Initable.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("learn.Initable3");
    
    System.out.println("after creating Initable3 ref");
    
    System.out.println(Initable3.staticNonFinal);
  }

}


结果: after creating Initable ref

47

initializing initable //直到调用静态量才进行了初始化操作

258

Initiaizaing initable2

147要进行链接()

Initiaizaing initable3// 调用forName调用时就进行了初始化

after creating Initable3 ref

74

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

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

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

泛化的Class引用

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

为了在使用泛化的Class引用时放松限制,我使用了通配符,它是java泛型的一部分。通配符就是“?”,表示“任何事物”。

在Java SE5中,Class<?>优于平凡的Class,即便它们是等价的,并且平凡的Class如你所见,不会产生编译器警告信息。它的好处表示你并非是碰巧或优于疏忽,而使用了一个非具体的类引用,你就是选择了非具体的版本。

为了创建一个Class引用,它被限定为某种类型,或该类型的任何子类型,你需要将通配符与extends 关键字相结合,创建一个范围。

package learn;

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<T> {

  private Class<T> type;

  public FilledList(Class<T> type) {
  
    this.type = type;
  }

  public List<T> create(int nElements) {
  
    List<T> result = new ArrayList<T>();
    try {
      for (int i = 0; i < nElements; i++) {
        result.add(type.newInstance());
      }
    } catch (Exception e) {
      throw new RuntimeException();
    }
    return result;
  }

  public static void main(String args[]) {
  
    FilledList<CountedInteger> fl = new FilledList<CountedInteger>(CountedInteger.class);
    
    List<CountedInteger> result = fl.create(15);
    
    for(CountedInteger countedInteger:result){
    
      System.out.println(countedInteger.toString());
      
    }
  }
}



结果:0,1,2,3,4,5,6,7,8,9,10,11,12,13,14


public class GenericToyTest {

  public static void main(String[] args) throws InstantiationException, IllegalAccessException {
  
    Class<FancyToy> ftClass = FancyToy.class;
    
    FancyToy fancyToy = ftClass.newInstance();
    
    Class<? super FancyToy> up = ftClass.getSuperclass();
    
    Object obj = up.newInstance();
    
  }
}

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

理解:

上界的list只能get,不能add(确切地说不能add出除null之外的对象,包括Object)

下界的list只能add,不能get。

上界 <? extend Fruit> ,表示所有继承Fruit的子类,但是具体是哪个子类,无法确定,所以调用add的时候,要add什么类型,谁也不知道。但是get的时候,不管是什么子类,不管追溯多少辈,肯定有个父类是Fruit,所以,我都可以用最大的父类Fruit接着,也就是把所有的子类向上转型为Fruit。

下界 <? super Apple>,表示Apple的所有父类,包括Fruit,一直可以追溯到老祖宗Object 。那么当我add的时候,我不能add Apple的父类,因为不能确定List里面存放的到底是哪个父类。但是我可以add Apple及其子类。因为不管我的子类是什么类型,它都可以向上转型为Apple及其所有的父类甚至转型为Object 。但是当我get的时候,Apple的父类这么多,我用什么接着呢,除了Object,其他的都接不住。

类型转换前先做检查

迄今为止,我们已知的RTTI形式包括:

  • 传统的类型转换。有RTTI确保类型转换的正确性,如果执行了一个错误的类型转换,就会抛出一个ClassCastException异常。

  • 代表对象的类型的Class对象。通过查询Class对象可以获取运行时所需的信息。

  • instanceof:它返回一个布尔值,告诉我们对象是不是某个特定类型的实例,或者是否实现了某个接口。

if(x instanceof Dog){

  ((Dog)x).bark()
  
}


instanceof有比较严格的限制:只可将其与命名类型进行比较,而不能与Class对象作比较。

反射:运行时的类信息

如果不知道某个对象的确切类型,RTTI可以告诉你。但有个限制:这个类型在编译时必须已知,这样才能使用RTTI识别它,并利用这些信息做一些有用的事。换句话说,在编译时,编译器必须知道所有要通过RTTI来处理的类。

在编译时你的程序无法获知这个对象所属的类。例如从磁盘文件或者网络连接中获取一串字节。这个类是在编译器为你的程序生成代码之后很久之后才会出现。

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

当通过反射与一个未知类型的对象打交道时,JVM只是简单地检查这个对象,看它属于哪个特定的类 。在用它做其他事情之前必须先加载拿了类的Class对象。因此,哪个类的.class文件对于JVM来说必须是可获取的:要么在本地机器上,要么可以通过网络取得.

RTTI与反射的区别:

  • 对RTTI来说,编译器在编译时打开和检查.class文件。

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

动态代理

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

package learn;

interface Interface {

  void doSomethind();

  void somethingElse(String arg);
}

class RealObject implements Interface {

  @Override
  public void doSomethind() {
  
    System.out.println("doSomething");

  }

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

  }

}

class SimpleProxy implements Interface {

  private Interface proxied;

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

  @Override
  public void doSomethind() {
  
    System.out.println("SimpleProxy doSomething");
    
    proxied.doSomethind();

  }

  @Override
  public void somethingElse(String arg) {
  
    System.out.println("SimpleProxy doSomethingElse");
    
    proxied.somethingElse(arg);

  }

}

public class SimpleProxyDemo {

  public static void consumer(Interface iface) {
  
    iface.doSomethind();
    
    iface.somethingElse("bonobo");
    
  }

  public static void main(String[] args) {
  
    consumer(new RealObject());
    
    consumer(new SimpleProxy(new RealObject()));
  }

}


结果:

doSomething

somethingElsebonobo

SimpleProxy doSomething

doSomething

SimpleProxy doSomethingElse

somethingElsebonobo

在任何时刻,只要你想要将额外的操作从“实际”对象中分离到不同的地方,特别是当你希望能够很容易地做出修改,从没有使用额外的操作转为使用这些操作,或者反过来时,代理就显得很有用。

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

package learn;

import java.lang.reflect.InvocationHandler;

import java.lang.reflect.Method;

import java.lang.reflect.Proxy;

class DynamicProxyHandler implements InvocationHandler {

  private Object proxied;

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

}

public class SimpleDynamicProxy {

  public static void consumer(Interface iface) {
  
    iface.doSomethind();
    
    iface.somethingElse("bobobo");
    
  }

  public static void main(String args[]) {
  
    RealObject real = new RealObject();
    
    consumer(real);
    
    Interface proxy = (Interface) Proxy.newProxyInstance(Interface.class.getClassLoader(),
        new Class[] { Interface.class }, new DynamicProxyHandler(real));
        
    consumer(proxy);
   }

}


结论:

doSomething

somethingElsebonobo

SimpleProxy doSomething

doSomething

SimpleProxy doSomethingElse

somethingElsebonobo


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


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

空对象

有时引入空对象的思想会很有用,它可接受传递给它的锁代表的对象的消息,但是将返回表示为实际上并存在任何“真实”对象的值。通过这种方式,你可以假设所有对象都是有效的,而不必浪费编程精力去检查null。

空对象最有用之处在于它更靠近数据,因为对象表示的问题空间内的实体。

接口与类型信息

interface关键字的一种重要目标就是允许程序员隔离构件,进而降低耦合性。

如果你编写接口,那么就可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去————接口并非是对解耦的一种无懈可击的保障。


评论