Class類 和 class物件(執行時的型別資訊)
什麼是類?可以理解為。class檔案
某種意義上來說,java有兩種物件:例項物件和Class物件。每個類的執行時的型別資訊就是用Class物件表示的。它包含了與類有關的資訊。其實我們的例項物件就通過Class物件來建立的。Java使用Class物件執行其RTTI(執行時型別識別,Run-Time Type Identification),多型是基於RTTI實現的
每一個類都有一個Class物件,每當編譯一個新類就產生一個Class物件,基本型別 (boolean, byte, char, short, int, long, float, and double)有Class物件,陣列有Class物件,就連關鍵字void也有Class物件(void.class)
System.out.println(int.class.getName()); System.out.println(char.class.getName()); System.out.println(short.class.getName()); System.out.println(long.class.getName()); System.out.println(byte.class.getName()); System.out.println(float.class.getName()); System.out.println(double.class.getName()); System.out.println(boolean.class.getName()); System.out.println(void.class.getName()); System.out.println(char[].class.getName());//[C System.out.println(char[][].class.getName());//[[C
Class類沒有公共的構造方法,Class物件是在類載入的時候由Java虛擬機器以及通過呼叫類載入器中的 defineClass 方法自動構造的,因此不能顯式地宣告一個Class物件。一個類被載入到記憶體並供我們使用需要經歷如下三個階段:
請簡述 一個類被載入到記憶體並供我們使用 的過程:
-
載入,這是由類載入器(ClassLoader)執行的。通過一個類的全限定名來獲取其定義的二進位制位元組流(Class位元組碼),將這個位元組流所代表的靜態儲存結構轉化為方法去的執行時資料介面,根據位元組碼在java堆中生成一個代表這個類的java.lang.Class物件。
-
連結。在連結階段將驗證Class檔案中的位元組流包含的資訊是否符合當前虛擬機器的要求,為靜態域分配儲存空間並設定類變數的初始值(預設的零值),並且如果必需的話,將常量池中的符號引用轉化為直接引用。
-
初始化。到了此階段,才真正開始執行類中定義的java程式程式碼。用於執行該類的靜態初始器和靜態初始塊,如果該類有父類的話,則優先對其父類進行初始化。
所有的類都是在對其第一次使用時,動態載入到JVM中的(懶載入)。當程式建立第一個對類的靜態成員的引用時,就會載入這個類。使用new建立類物件的時候也會被當作對類的靜態成員的引用。因此java程式程式在它開始執行之前並非被完全載入,其各個類都是在必需時才載入的。這一點與許多傳統語言都不同。動態載入使能的行為,在諸如C++這樣的靜態載入語言中是很難或者根本不可能複製的。
在類載入階段,類載入器首先檢查這個類的Class物件是否已經被載入。如果尚未載入,預設的類載入器就會根據類的全限定名查詢.class檔案。在這個類的位元組碼被載入時,它們會接受驗證,以確保其沒有被破壞,並且不包含不良java程式碼。一旦某個類的Class物件被載入記憶體,我們就可以它來建立這個類的所有物件。
如何獲得Class物件
有三種獲得Class物件的方式:
- Class.forName(“類的全限定名”)
- 例項物件.getClass()
- 類名.class (類字面常量)
Class.forName 和getClass()
我們先看看如下的例子:
package com.cry;
class Dog {
static {
System.out.println("Loading Dog");
}
}
class Cat {
static {
System.out.println("Loading Cat");
}
}
public class Test {
public static void main(String[] args){
System.out.println("inside main");
new Dog();
System.out.println("after creating Dog");
try {
Class cat=Class.forName("com.cry.Cat");
} catch (ClassNotFoundException e) {
System.out.println("Couldn't find Cat");
}
System.out.println("finish main");
}
}
/* Output:
inside main
Loading Dog
after creating Dog
Loading Cat
finish main
*/
上面的Dog、Cat類中都有一個靜態語句塊,該語句塊在類第一次被載入時候被執行。這時會有相應的資訊打印出來,告訴我們這個類什麼時候被載入了。從輸出中可以看到,Class物件僅在需要的時候才被載入,static初始化是在類載入時進行的。
Class.forName方法是Class類的一個靜態成員。forName在執行的過程中發現如果類Dog還沒有被載入,那麼JVM就會呼叫類載入器去載入Dog類,並返回載入後的Class物件。Class物件和其他物件一樣,我們可以獲取並操作它的引用。在類載入的過程中,Dog類的靜態語句塊會被執行。如果Class .forName找不到你要載入的類,它會丟擲ClassNotFoundException異常。
Class.forName的好處就在於,不需要為了獲得Class引用而持有該型別的物件,只要通過全限定名就可以返回該型別的一個Class引用。如果你已經有了該型別的物件,那麼我們就可以通過呼叫getClass()方法來獲取Class引用了,這個方法屬於根類Object的一部分,它返回的是表示該物件的實際型別的Class引用:
class Dog {
static {
System.out.println("Loading Dog");
}
}
public class Test {
public static void main(String[] args) {
System.out.println("inside main");
Dog d = new Dog();
System.out.println("after creating Dog");
Class c = d.getClass();
System.out.println("finish main");
}
}
/* Output:
inside main
Loading Dog
after creating Dog
finish main
*/
利用new操作符建立物件後,類已經裝載到記憶體中了,所以執行getClass()方法的時候,就不會再去執行類載入的操作了,而是直接從java堆中返回該型別的Class引用
類字面常量
java還提供了另一種方法來生成對Class物件的引用。即使用類字面常量,就像這樣:Cat.class,這樣做不僅更簡單,而且更安全,因為它在編譯時就會受到檢查(因此不需要置於try語句塊中)。並且根除了對forName()方法的呼叫,所有也更高效。類字面量不僅可以應用於普通的類,也可以應用於介面、陣列及基本資料型別。
注意:基本資料型別的Class物件和包裝類的Class物件是不一樣的:
Class c1 = Integer.class;
Class c2 = int.class;
System.out.println(c1);
System.out.println(c2);
System.out.println(c1 == c2);
/* Output
class java.lang.Integer
int
false
*/
但是在包裝類中有個一個欄位TYPE,TYPE欄位是一個引用,指向對應的基本資料型別的Class物件,如下所示,左右兩邊相互等價:
用.class來建立對Class物件的引用時,不會自動地初始化該Class物件(這點和Class.forName方法不同)。類物件的初始化階段被延遲到了對靜態方法或者非常數靜態域首次引用時才執行:
class Dog {
static final String s1 = "Dog_s1";
static String s2 = "Dog_s2";
static {
System.out.println("Loading Dog");
}
}
class Cat {
static String s1 = "Cat_s1";
static {
System.out.println("Loading Cat");
}
}
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println("----Star Dog----");
Class dog = Dog.class;
System.out.println("------");
System.out.println(Dog.s1);
System.out.println("------");
System.out.println(Dog.s2);
System.out.println("---start Cat---");
Class cat = Class.forName("com.cry.Cat");
System.out.println("-------");
System.out.println(Cat.s1);
System.out.println("finish main");
}
}
/* Output:
----Star Dog----
------
Dog_s1
------
Loading Dog
Dog_s2
---start Cat---
Loading Cat
-------
Cat_s1
finish main
*/
從上面我們可以看到,如果僅使用.class語法來獲得對類的Class引用是不會引發初始化的。但是如果使用Class.forName來產生引用,就會立即進行了初始化,就像Cat所看到的。
如果一個欄位被static final修飾,我們稱為”編譯時常量“,就像Dog的s1欄位那樣,那麼在呼叫這個欄位的時候是不會對Dog類進行初始化的。因為被static和final修飾的欄位,在編譯期就把結果放入了常量池中了。但是,如果只是將一個域設定為static 或final的,還不足以確保這種行為,就如呼叫Dog的s2欄位後,會強制Dog進行類的初始化,因為s2欄位不是一個編譯時常量。
通過javap -c -v對Dog的位元組碼進行反彙編:
{
static final java.lang.String s1;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String Dog_s1
static java.lang.String s2;
flags: ACC_STATIC
com.cry.Dog();
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/cry/Dog;
static {};
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: ldc #2 // String Dog_s2
2: putstatic #3 // Field s2:Ljava/lang/String;
5: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #5 // String Loading Dog
10: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: return
LineNumberTable:
line 6: 0
line 9: 5
line 10: 13
}
從上面可以看出s1在編譯後被ConstantValue屬性修飾 ConstantValue: String Dog_s1,表示即同時被final和static修飾。而s2並沒有被ConstantValue修飾,因為它不是一個編譯時常量。在static{}中表示類的初始化操作,在操作中我們看到只有s2欄位進行了賦值,而卻沒有s1的蹤影,因此呼叫s1欄位是不會觸發類的初始化的。
小結
一旦類被載入了到了記憶體中,那麼不論通過哪種方式獲得該類的Class物件,它們返回的都是指向同一個java堆地址上的Class引用。jvm不會建立兩個相同型別的Class物件:
package com.cry;
class Cat {
static {
System.out.println("Loading Cat");
}
}
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println("inside main");
Class c1 = Cat.class;
Class c2= Class.forName("com.cry.Cat");
Class c3=new Cat().getClass();
Class c4 =new Cat().getClass();
System.out.println(c1==c2);
System.out.println(c2==c3);
System.out.println("finish main");
}
}
/* Output:
inside main
-------
Loading Cat
true
true
finish main
*/
從上面我們可以看出執行不同獲取Class引用的方法,返回的其實都是同一個Class物件。
其實對於任意一個Class物件,都需要由它的類載入器和這個類本身一同確定其在就Java虛擬機器中的唯一性,也就是說,即使兩個Class物件來源於同一個Class檔案,只要載入它們的類載入器不同,那這兩個Class物件就必定不相等。這裡的“相等”包括了代表類的Class物件的equals()、isAssignableFrom()、isInstance()等方法的返回結果,也包括了使用instanceof關鍵字對物件所屬關係的判定結果。所以在java虛擬機器中使用雙親委派模型來組織類載入器之間的關係,來保證Class物件的唯一性。
泛型Class引用
Class引用表示的就是它所指向的物件的確切型別,而該物件便是Class類的一個物件。在JavaSE5中,允許你對Class引用所指向的Class物件的型別進行限定,也就是說你可以對Class物件使用泛型語法。通過泛型語法,可以讓編譯器強制指向額外的型別檢查:
public final class Class<T> implements java.io.Serializable,
GenericDeclaration,
Type,
AnnotatedElement {
Class<Integer> c1 = int.class;
c1=Integer.class;
//c1=Double.class; 編譯報錯
雖然int.class和Integer.class指向的不是同一個Class物件引用,但是它們基本型別和包裝類的關係,int可以自動包裝為Integer,所以編譯器可以編譯通過。
泛型中的型別可以持有其子類的引用嗎?不行:
Class<Number> c1 = Integer.class; //編譯報錯
- 1
雖然Integer繼承自Number,但是編譯器無法編譯通過。
為了使用泛化的Class引用放鬆限制,我們還可以使用萬用字元,它是Java泛型的一部分。萬用字元的符合是”?“,表示“任何事物“:
Class<?> c1 = int.class;
c1= double.class;
Class
Class<? extends Number> c1 = Integer.class;
c1 = Number.class;
c1 = Double.class;
// c1=String.class; 報錯,不屬於Number類和其子類
萬用字元?不僅可以與extend結合,而且還可以與super關鍵字相結合,表示被限定為某種型別,或該型別的任何父型別:
Class<? super Integer> c1 = Integer.class;
c1 = Number.class;
c1 = Object.class;
c1=Integer.class.getSuperclass();
向Class引用新增泛型語法的原因僅僅是為了提供編譯期型別檢查。
Class類的方法
方法名 | 說明 |
---|---|
forName() | (1)獲取Class物件的一個引用,但引用的類還沒有載入(該類的第一個物件沒有生成)就載入了這個類。 (2)為了產生Class引用,forName()立即就進行了初始化。 |
Object-getClass() | 獲取Class物件的一個引用,返回表示該物件的實際型別的Class引用。 |
getName() | 取全限定的類名(包括包名),即類的完整名字。 |
getSimpleName() | 獲取類名(不包括包名) |
getCanonicalName() | 獲取全限定的類名(包括包名) |
isInterface() | 判斷Class物件是否是表示一個介面 |
getInterfaces() | 返回Class物件陣列,表示Class物件所引用的類所實現的所有介面。 |
getSupercalss() | 返回Class物件,表示Class物件所引用的類所繼承的直接基類。應用該方法可在執行時發現一個物件完整的繼承結構。 |
newInstance() | 返回一個Oject物件,是實現“虛擬構造器”的一種途徑。使用該方法建立的類,必須帶有無參的構造器。 |
getFields() | 獲得某個類的所有的公共(public)的欄位,包括繼承自父類的所有公共欄位。 類似的還有getMethods和getConstructors。 |
getDeclaredFields | 獲得某個類的自己宣告的欄位,即包括public、private和proteced,預設但是不包括父類宣告的任何欄位。類似的還有getDeclaredMethods和getDeclaredConstructors。 |
import java.lang.reflect.Field;
interface I1 {
}
interface I2 {
}
class Cell{
public int mCellPublic;
}
class Animal extends Cell{
private int mAnimalPrivate;
protected int mAnimalProtected;
int mAnimalDefault;
public int mAnimalPublic;
private static int sAnimalPrivate;
protected static int sAnimalProtected;
static int sAnimalDefault;
public static int sAnimalPublic;
}
class Dog extends Animal implements I1, I2 {
private int mDogPrivate;
public int mDogPublic;
protected int mDogProtected;
private int mDogDefault;
private static int sDogPrivate;
protected static int sDogProtected;
static int sDogDefault;
public static int sDogPublic;
}
public class Test {
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
Class<Dog> dog = Dog.class;
//類名列印
System.out.println(dog.getName()); //com.cry.Dog
System.out.println(dog.getSimpleName()); //Dog
System.out.println(dog.getCanonicalName());//com.cry.Dog
//介面
System.out.println(dog.isInterface()); //false
for (Class iI : dog.getInterfaces()) {
System.out.println(iI);
}
/*
interface com.cry.I1
interface com.cry.I2
*/
//父類
System.out.println(dog.getSuperclass());//class com.cry.Animal
//建立物件
Dog d = dog.newInstance();
//欄位
for (Field f : dog.getFields()) {
System.out.println(f.getName());
}
/*
mDogPublic
sDogPublic
mAnimalPublic
sAnimalPublic
mCellPublic //父類的父類的公共欄位也打印出來了
*/
System.out.println("---------");
for (Field f : dog.getDeclaredFields()) {
System.out.println(f.getName());
}
/** 只有自己類宣告的欄位
mDogPrivate
mDogPublic
mDogProtected
mDogDefault
sDogPrivate
sDogProtected
sDogDefault
sDogPublic
*/
}
}
getName、getCanonicalName與getSimpleName的區別:
getSimpleName:只獲取類名
getName:類的全限定名,jvm中Class的表示,可以用於動態載入Class物件,例如Class.forName。
getCanonicalName:返回更容易理解的表示,主要用於輸出(toString)或log列印,大多數情況下和getName一樣,但是在內部類、陣列等型別的表示形式就不同了。
public class Test {
private class inner{
}
public static void main(String[] args) throws ClassNotFoundException {
//普通類
System.out.println(Test.class.getSimpleName()); //Test
System.out.println(Test.class.getName()); //com.cry.Test
System.out.println(Test.class.getCanonicalName()); //com.cry.Test
//內部類
System.out.println(inner.class.getSimpleName()); //inner
System.out.println(inner.class.getName()); //com.cry.Test$inner
System.out.println(inner.class.getCanonicalName()); //com.cry.Test.inner
//陣列
System.out.println(args.getClass().getSimpleName()); //String[]
System.out.println(args.getClass().getName()); //[Ljava.lang.String;
System.out.println(args.getClass().getCanonicalName()); //java.lang.String[]
//我們不能用getCanonicalName去載入類物件,必須用getName
//Class.forName(inner.class.getCanonicalName()); 報錯
Class.forName(inner.class.getName());
}
}