https://blog.csdn.net/mcryeasy/article/details/52344729
待優化整理 總結
Class類簡介
在java世界里,一切皆對象。從某種意義上來說,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)。Class對象對應着java.lang.Class類,如果說類是對象抽象和集合的話,那么Class類就是對類的抽象和集合。
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引用:
-
package com.cry;
-
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方法不同)。類對象的初始化階段被延遲到了對靜態方法或者非常數靜態域首次引用時才執行:
-
package com.cry;
-
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; //編譯報錯
雖然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。 |
-
package com.cry;
-
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一樣,但是在內部類、數組等類型的表示形式就不同了。
-
package com.cry;
-
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());
-
}
-
}
參考:
java中的Class對象和new關鍵字解析
Thinking in Java
原文地址:https://blog.csdn.net/dufufd/article/details/80537638