類型信息(RTTI和反射)——RTTI

運行時類型信息可讓你在程序運行時發現和使用類型信息。java

在Java中運行時識別對象和類的信息有兩種方式:傳統的RTTI,以及反射。下面就先來講下RTTI。編程

一、RTTI:數組

RTTI:在運行時,識別一個對象的類型。可是這個類型在編譯時必須已知安全

下面經過一個例子來看下RTTI的使用。這裏涉及到了多態的概念:讓代碼只操做基類的引用(面向對象編程中基本的目的),而實際上調用具體的子類的方法,一般會建立一個具體的對象(Circle,Square,或者Triangle,見下例),把它向上轉型爲Shape(忽略了對象的具體類型),並在後面的程序中使用匿名(即不知道具體類型)的Shape引用:服務器

20165985229239.png (515×293)

abstract class Shape {
  // this 調用當前類的toString()方法,返回實際的內容
  void draw(){ System.out.println(this + ".draw()"); }
  // 聲明 toString()爲abstract類型,強制集成在重寫該方法
  abstract public String toString();
}
 
class Circle extends Shape {
  public String toString(){ return "Circle"; }
}
 
class Square extends Shape {
  public String toString(){ return "Square"; }
}
 
class Triangle extends Shape {
  public String toString(){ return "Triangle"; }
}
 
public static void main(String[] args){
  // 把Shape對象放入List<Shape>的數組的時候會向上轉型爲Shape,從而丟失了具體的類型信息
  List<Shape> shapeList = Arrays.asList(new Circle(), new Square(), new Triangle());
  // 從數組中取出時,這種容器,實際上全部的元素都當成Object持有,會自動將結果轉型爲Shape,這就是RTTI的基本的使用。
  for(Shape shape : shapeList){
    shape.draw();
  }
}

輸出結果爲:dom

Circle.draw()
Square.draw()
Triangle.draw()

存入數組的時候,會自動向上轉型爲Shape,丟失了具體的類型,當從數組中取出的時候,(List容器將全部的事物都當作Object持有),會自動將結果轉型回Shape,這就是RTTI的基本用法測試

Java中全部的類型轉換都是在運行時進行正確性檢查的,也就是RTTI:在運行時,識別一個對象的類型。this

上面的轉型並不完全,數組的元素取出時由Object轉型爲Shape,而不是具體的類型。這是由於目前咱們只知道這個List<Shape>保存的都是Shape。編譯時這是由容器和Java泛型系統來確保這一點的,而在運行時由類型轉換操做來確保這一點的。spa

而可以經過Shape對象執行到子類的具體代碼就是由多態來決定的了,具體看Shape引用所指向的具體對象。.net

另外,使用RTTI,能夠查詢某個Shape引用所指向的對象的確切類型,而後選擇性的執行子類的方法。

二、Class對象:
要了解RTTI在Java中的工做原理,必須知道類型信息在運行時是如何表示的,這裏是由Class這個特殊對象完成的。

Class對象是用來建立類的全部的「常規」對象的。Java使用Class對象來執行其RTTI。

每當編譯一個新類,就會產生一個Class對象(.class文件)。運行這個程序的JVM將使用「類加載器」這個子系統。

類加載器子系統:包含一條類加載器鏈,但只有一個原生類加載器,它是JVM實現的一部分。原生類加載器加載可信類,包括Java API類,一般是從本地磁盤加載的。當須要以某種特定的方式加載類,以支持Web服務器應用,能夠掛接額外的類加載器。

2.一、加載類的時機:
當程序建立第一個對類的靜態成員的引用時,就會加載這個類。這證實其實構造器也是類的靜態方法,當使用new操做符建立類的新對象也會當作對類的靜態成員的引用。

可見Java程序時動態加載的,按需加載。須要用到Class時,類加載器首先會檢查這個類的Class對象是否已經加載,若是還沒有加載,默認的類加載器就會根據類名查找到.class文件。接下來是驗證階段:加載時,它們會接受驗證,以確保其沒有被破壞,而且不包含不良Java代碼。

2.二、Class相關方法,newInstance()
下面經過一個例子演示Class對象的加載:

class A {
  // 靜態代碼庫,在第一次被加載時執行,經過打印信息知道該類何時被加載
  static { System.out.println("Loading A"); }
}
class B {
  static { System.out.println("Loading B"); }
}
class C {
  static { System.out.println("Loading C"); }
}
public class Load {
  public static void main(String[] args){
    System.out.println("execute main...");
    new A();
    System.out.println("after new A");
    try {
      Class.forName("com.itzhai.test.type.B");
    } catch (ClassNotFoundException e) {
      System.out.println("cloud not find class B");
    }
    System.out.println("after Class.forName B");
    new C();
    System.out.println("after new C");
  }
}

輸出結果爲:

execute main...
Loading A
after new A
Loading B
after Class.forName B
Loading C
after new C

可見,Class對象在須要的時候才被加載,注意到這裏的Class.forName()方法:

forName()方法是取得Class對象的引用的一種方法,經過這個方法得到恰當的Class對象的引用,就能夠在運行時使用類型信息了。

若是你已經有了一個感興趣的類型的對象,則能夠經過跟類Object提供的getClass()方法來得到Class引用。

下面是一段Class的使用的代碼:

interface X{}
interface Y{}
interface Z{}
class Letter {
  Letter(){};
  Letter(int i){};
}
class NewLetter extends Letter implements X, Y, Z{
  NewLetter(){ super(1); };
}
public class ClassTest {
 
  /**
   * 打印類型信息
   * @param c
   */
  static void printInfo(Class c){
    // getName()得到全限定的類名
    System.out.println("Class name: " + c.getName() + " is interface? " + c.isInterface());
    // 得到不包含包名的類名
    System.out.println("Simple name: " + c.getSimpleName());
    // 得到全限定類名
    System.out.println("Canonical name: " + c.getCanonicalName());
  }
 
  public static void main(String[] args){
    Class c = null;
    try {
      // 得到Class引用
      c = Class.forName("com.itzhai.test.type.NewLetter");
    } catch (ClassNotFoundException e) {
      System.out.println("Can not find com.itzhai.test.type.NewLetter");
      System.exit(1);
    }
    // 打印接口類型信息
    for(Class face : c.getInterfaces()){
      printInfo(face);
    }
    // 獲取超類Class引用
    Class up = c.getSuperclass();
    Object obj = null;
    try {
      // 經過newInstance()方法建立Class的實例
      obj = up.newInstance();
    } catch (InstantiationException e) {
      System.out.println("Can not instantiate");
    } catch (IllegalAccessException e) {
      System.out.println("Can not access");
    }
    // 打印超類類型信息
    printInfo(obj.getClass());
  }
}

輸出爲:

Class name: com.itzhai.test.type.X is interface? true
Simple name: X
Canonical name: com.itzhai.test.type.X
Class name: com.itzhai.test.type.Y is interface? true
Simple name: Y
Canonical name: com.itzhai.test.type.Y
Class name: com.itzhai.test.type.Z is interface? true
Simple name: Z
Canonical name: com.itzhai.test.type.Z
Class name: com.itzhai.test.type.Letter is interface? false
Simple name: Letter
Canonical name: com.itzhai.test.type.Letter

注意,在傳遞給forName()的字符串必須使用全限定名(包括包名)

經過printInfo裏面使用到的方法,你能夠在運行時發現一個對象完整的類繼承結構。

經過使用Class的newInstance()方法是實現「虛擬構造器」的一種途徑,用來建立Class的實例,獲得的是Object引用,可是引用時指向Letter對象。使用newInstance()來建立的類,必須帶有默認的構造器。(而經過反射API,能夠用任意的構造器來動態的建立類的對象)。

2.三、類字面常量:
除了使用forName()方法,Java還提供了另外一種方法來生成對Class對象的引用,即便用類字面常量:

NewLetter.class;

此方法簡單安全,編譯時就受到檢查,更高效。不只可用於普通類,也能夠用於接口數組以及基本數據類型。另外,對於基本數據類型的包裝器類,還有一個標準字段TYPE,TYPE字段是一個引用,執行對應的基本數據類型的Class對象。爲了統一,建議都使用.class這種形式。

2.四、使用.class與使用forName()方法建立對象引用的區別:
使用.class建立時,不會自動的初始化Class對象。建立步驟以下:

(1)加載 由類加載器執行:查找字節碼(一般是在classpath指定的路徑中查找,但並不是必須的),而後從這些字節碼中建立一個Class對象。

(2)連接 將驗證類中的字節碼,爲靜態域分配存儲空間,若是須要,將會解析這個類建立的對其餘類的全部的引用。

(3)初始化 若是該類具備超類,則對其初始化,執行靜態初始化器和靜態初始化塊。

初始化被延遲到了對靜態方法(構造器隱式的是靜態的)或者很是數靜態域進行首次引用時才執行的:

class Data1{
  static final int a = 1;
  static final double b = Math.random();
  static {
    System.out.println("init Data1...");
  }
}
 
class Data2{
  static int a = 12;
  static {
    System.out.println("init Data2...");
  }
}
 
class Data3{
  static int a = 23;
  static {
    System.out.println("init Data3...");
  }
}
 
public class ClassTest2 {
  public static void main(String[] args){
    System.out.println("Data1.class: ");
    Class data1 = Data1.class;
    System.out.println(Data1.a); // 沒有初始化Data1
    System.out.println(Data1.b); // 初始化了Data1
    System.out.println(Data2.a); // 初始化了Data2
    try {
      Class data3 = Class.forName("com.itzhai.test.type.Data3"); // 初始化了Data3
    } catch (ClassNotFoundException e) {
      System.out.println("can not found com.itzhai.test.type.Data3...");
    }
    System.out.println(Data3.a);
  }
}

輸出的結果爲:

Data1.class:
1
init Data1...
0.26771085109184534
init Data2...
12
init Data3...
23

初始化有效的實現了儘量的「惰性」。

2.五、下面是判斷是否執行初始化的一些狀況:
(1).class語法得到對類的引用不會引起初始化;

(2)Class.forName()產生了Class引用,當即進行了初始化;

(3)若是一個static final值是「編譯器常量」,那麼這個值不須要對類進行初始化就能夠被讀取;

(4)若是隻是把一個域設置爲static final還不足以確保這種行爲,例如上面的:

static final double b = Math.random();

(5)若是一個static域但不是final的,那麼在對它訪問時,老是要先進行連接(爲這個域分配存儲空間)和初始化(初始化該存儲空間);

2.六、泛化的Class引用:
Class引用表示的是它所指向的對象的確切類型,而該對象即是Class類的一個對象。在JavaSE5中,能夠經過泛型對Class引用所指向的Class對象進行限定,而且可讓編譯器強制執行額外的類型檢查:

Class intCls = int.class;
// 使用泛型限定Class指向的引用
Class<Integer> genIntCls = int.class;
// 沒有使用泛型的Clas能夠從新賦值爲指向任何其餘的Class對象
intCls = double.class;
// 下面的編譯會出錯
// genIntCls = double.class;

2.6.一、使用通配符?放鬆泛型的限定:

Class<?> intCls = int.class;
intCls = String.class;

在JavaSE5中,Class<?>優於平凡的Class,更建議使用Class<?>,即使它們是等價的,由於Class<?>的好處是它表示你並不是是碰巧或者疏忽,而是使用了一個非具體的類引用。

爲了限定Class的引用爲某種類型,或者該類型的子類型能夠將通配符與extends一塊兒使用,建立一個範圍:

Class<? extends Number> num = int.class;
// num的引用範圍爲Number及其子類,因此能夠按照以下賦值
num = double.class;
num = Number.class;

另外,可使用Class<? Super ziclass>。

 

2.6.二、泛型下的newInstance()方法:
使用了泛型後的Class,調用newInstance()返回的對象是確切類型的,可是當你使用getSuperclass()獲取泛型對應的超類的時候真正的類型會有一些限制:編譯器在編譯期就知道了超類的類型,可是,經過這個獲取到的超類引用的newInstance()方法返回的不是精確類型,而是Object

Dog dog = dogCls.newInstance();
abstract class Animal {
}
class Dog extends Animal{
}
 
// 下面的寫法是錯誤的,只能返回 Class<? super Dog>類型
// Class<Animal> animalCls = dogCls.getSuperclass();
Class<? super Dog> animalCls = dogCls.getSuperclass();
// 經過獲取的超類引用,只能建立返回Object類型的對象
Object obj = animalCls.newInstance();

2.6.三、新的轉型語法:cast()方法

直接看下代碼:

Animal animal = new Dog();
Class<Dog> dogCls = Dog.class;
Dog dog = dogCls.cast(animal);
// 或者直接使用下面的轉型方法
dog = (Dog)animal;

能夠發現,使用cast()方法的作了額外的工做,這種轉換方法能夠用在如下的狀況中:在編寫泛型代碼的時候,若是存儲了Class引用,並但願之後經過這個Class引用來執行轉型,就可使用cast()方法。

三、類型檢查
3.一、類型轉換前先作檢查
編譯器容許你自由的作向上轉型的賦值操做,而不須要任何顯示的轉型操做,就好像給超類的引用賦值那樣。

然而若是不使用顯示的類型轉換,編譯器就不容許你執行向下轉換賦值,這個時候咱們不妨先來檢查一下對象是否是某個特定類型的實例,使用到了關鍵字 instanceof:

if(x instanceof Dog)
  ((Dog) x).bark();

3.二、RTTI的形式:

因此,到目前爲止,咱們知道RTTI的形式包括:

(1)傳統的類型轉換 (Shape)

(2)表明對象的類型的Class對象

(3)關鍵字instanceof

3.三、動態的instanceof方法:
Class.isInstance方法提供給了一種動態測試對象的途徑。

下面演示下 instanceofClass.isInstance 的用法:

Attribute:

public interface Attribute {
 
}

Shape:

/**
 * 建立一個抽象類
 */
public abstract class Shape{
  // this調用了當前類的toString方法得到信息
  public void draw() { System.out.println(this + ".draw()"); }
  // 聲明toString()方法爲abstract,從而強制繼承者須要重寫該方法。
  abstract public String toString();
}

Circle:

public class Circle extends Shape implements Attribute{
  public String toString(){ return "Circle"; }
}

Square:

public class Square extends Shape{
  public String toString(){ return "Square"; }
}

Triangle:

public class Triangle extends Shape{
  public String toString(){ return "Triangle"; }
}

類型檢查:

// instanceOf
Circle c = new Circle();
// 判斷是否超類的實例
System.out.format("Using instanceof: %s is a shape? %b\n",
    c.toString(), c instanceof Shape);
// 判斷是否Circle的實例
System.out.format("Using instanceof: %s is a circle? %b\n",
    c.toString(), c instanceof Circle);
// 判斷是否超類的實例
System.out.format("Using Class.isInstance: %s is a shape? %b\n",
    c.toString(), Shape.class.isInstance(c));
// 判斷是否接口的實例
System.out.format("Using Class.isInstance: %s is a Attribute? %b\n", 
    c.toString(), Attribute.class.isInstance(c));

能夠發現,instanceof 或者 Class.isInstance 方法判斷了是否繼承體系的實例,即除了判斷自己,還判斷是否超類或接口的實例。

下面演示下使用動態的Class.isInstance的用法:

首先建立一個抽象的形狀生成器類:

public abstract class ShapeCreator {
  private Random rand = new Random(10);
  // 返回一個對象類型數組,由實現類提供,後面會看到兩種實現形式,基於forName的和基於類字面常量的.class
  public abstract List<Class<? extends Shape>> types();
  // 隨機生成一個對象類型數組中的類型對象實例
  public Shape randomShape(){
    int n = rand.nextInt(types().size());
    try {
      return types().get(n).newInstance();
    } catch (InstantiationException e) {
      e.printStackTrace();
      return null;
    } catch (IllegalAccessException e) {
      e.printStackTrace();
      return null;
    }
  }
  // 生成一個隨機數組
  public Shape[] createArray(int size){
    Shape[] result = new Shape[size];
    for(int i=0; i<size; i++){
      result[i] = randomShape();
    }
    return result;
  }
  // 生成一個隨機數組,泛型的ArrayList
  public ArrayList<Shape> arrayList(int size){
    ArrayList<Shape> result = new ArrayList<Shape>();
    Collections.addAll(result, createArray(size));
    return result;
  }
}

接下來編寫一個該抽象類的實現:

/**
 * forName的生成器實現
 * @author arthinking
 *
 */
public class ForNameCreator extends ShapeCreator{
 
  private static List<Class<? extends Shape>> types =
      new ArrayList<Class<? extends Shape>>();
  private static String[] typeNames = {
    "com.itzhai.javanote.entity.Circle",
    "com.itzhai.javanote.entity.Square",
    "com.itzhai.javanote.entity.Triangle"
  };
 
  @SuppressWarnings("unused")
  private static void loader(){
    for(String name : typeNames){
      try {
        types.add((Class<? extends Shape>)Class.forName(name));
      } catch (ClassNotFoundException e) {
        e.printStackTrace();
      }
    }
  }
  // 初始化加載所需的類型數組
  static {
    loader();
  }
  public List<Class<? extends Shape>> types() {
    return types;
  }
}

最後寫一個統計形狀個數的類,裏面用到了instanceof:

public class ShapeCount {
 
  static class ShapeCounter extends HashMap<String, Integer>{
    public void count(String type){
      Integer quantity = get(type);
      if(quantity == null){
        put(type, 1);
      } else {
        put(type, quantity + 1);
      }
    }
  }
 
  // 演示經過instanceof關鍵字統計對象類型
  public static void countShapes(ShapeCreator creator){
    ShapeCounter counter = new ShapeCounter();
    for(Shape shape : creator.createArray(20)){
      if(shape instanceof Circle)
        counter.count("Circle");
      if(shape instanceof Square)
        counter.count("Square");
      if(shape instanceof Triangle){
        counter.count("Triangle");
      }
    }
    System.out.println(counter);
  }
 
  public static void main(String[] args){
    countShapes(new ForNameCreator());
  }
}

改寫一下抽象類的實現,從新用類字面常量實現:

/**
 * 字面量的生成器實現
 */
public class LiteralCreator extends ShapeCreator{
 
  public static final List<Class<? extends Shape>> allType =
      Collections.unmodifiableList(Arrays.asList(Circle.class, Triangle.class, Square.class));
 
  public List<Class<? extends Shape>> types(){
    return allType;
  }
 
  public static void main(String[] args){
    System.out.println(allType);
  }
 
}

如今使用Class.isInstance統計形狀的個數以下:

/**
 * 經過使用Class.instanceof動態的測試對象,移除掉原來的ShapeCount中單調的instanceof語句
 *
 */
public class ShapeCount2 {
 
  private static final List<Class<? extends Shape>> shapeTypes = LiteralCreator.allType;
 
  static class ShapeCounter extends HashMap<String, Integer>{
    public void count(String type){
      Integer quantity = get(type);
      if(quantity == null){
        put(type, 1);
      } else {
        put(type, quantity + 1);
      }
    }
  }
 
  // 演示經過Class.isInstance()統計對象類型
  public static void countShapes(ShapeCreator creator){
    ShapeCounter counter = new ShapeCounter();
    for(Shape shape : creator.createArray(20)){
      for(Class<? extends Shape> cls : shapeTypes){
        if(cls.isInstance(shape)){
          counter.count(cls.getSimpleName());
        }
      }
    }
    System.out.println(counter);
  }
 
  public static void main(String[] args){
    countShapes(new ForNameCreator());
  }
}

如今生成器有了兩種實現,咱們在這裏能夠添加一層外觀,設置默認的實現方式:

/**
 * 如今生成器有了兩種實現,咱們在這裏添加一層外觀,設置默認的實現方式
 */
public class Shapes {
 
  public static final ShapeCreator creator =
      new LiteralCreator();
  public static Shape randomShape(){
    return creator.randomShape();
  }
  public static Shape[] createArray(int size){
    return creator.createArray(size);
  }
  public static ArrayList<Shape> arrayList(int size){
    return creator.arrayList(size);
  }
}

3.四、instanceof與Class的等價性:

instanceof和isInstance()生成的結果徹底同樣,保持了類型的概念,判斷是否一個類或者是這個類的派生類。

equals()與==也是同樣的,而使用這個比較實際的Class對象,就沒有考慮繼承。

System.out.println(new Circle() instanceof Circle); // true
System.out.println(Shape.class.isInstance(new Circle())); // true
System.out.println((new Circle()).getClass() == Circle.class); // true
System.out.println((new Circle().getClass()).equals(Shape.class)); // false

 

本文摘自《Java編程思想》第14章類型信息,參考博客http://www.jb51.net/article/83784.htm

相關文章
相關標籤/搜索