1. 程式人生 > 實用技巧 >【go語言學習】切片slice

【go語言學習】切片slice

來源:廖雪峰的官方網站

除了int等基本型別外,Java的其他型別全部都是class(包括interface)。例如:

String

Object

Runnable

Exception

...

仔細思考,我們可以得出結論:class(包括interface)的本質是資料型別(Type)。無繼承關係的資料型別無法賦值:

Number n = new Double(123.456); // OK

String s = new Double(123.456); // compile error!

而class是由JVM在執行過程中動態載入的。JVM在第一次讀取到一種class型別時,將其載入進記憶體。

每載入一種class,JVM就為其建立一個Class型別的例項,並關聯起來。注意:這裡的Class型別是一個名叫Class的class。它長這樣:

public final class Class {
private Class() {}
}

以String類為例,當JVM載入String類時,它首先讀取String.class檔案到記憶體,然後,為String類建立一個Class例項並關聯起來:

Class cls = new Class(String);

這個Class例項是JVM內部建立的,如果我們檢視JDK原始碼,可以發現Class類的構造方法是private,只有JVM能建立Class例項,我們自己的Java程式是無法建立Class例項的。

所以,JVM持有的每個Class例項都指向一個數據型別(class或interface):

┌───────────────────────────┐
│  Class Instance   │──────>   String
├───────────────────────────┤
│name = "java.lang.String"  │
└───────────────────────────┘
┌───────────────────────────┐
│  Class Instance   │──────>   Random
├───────────────────────────┤
│name = "java.util.Random"  │
└───────────────────────────┘
┌───────────────────────────┐
│  Class Instance   │──────>   Runnable
├───────────────────────────┤
│name = "java.lang.Runnable"│
└───────────────────────────┘
一個Class例項包含了該class的所有完整資訊:

┌───────────────────────────┐
│  Class Instance   │──────>   String
├───────────────────────────┤
│name = "java.lang.String"  │
├───────────────────────────┤
│package = "java.lang"  │
├───────────────────────────┤
│super = "java.lang.Object" │
├───────────────────────────┤
│interface = CharSequence...│
├───────────────────────────┤
│field = value[],hash,...   │
├───────────────────────────┤
│method = indexOf()...  │
└───────────────────────────┘

由於JVM為每個載入的class建立了對應的Class例項,並在例項中儲存了該class的所有資訊,包括類名、包名、父類、實現的介面、所有方法、欄位等,因此,如果獲取了某個Class例項,我們就可以通過這個Class例項獲取到該例項對應的class的所有資訊。

這種通過Class例項獲取class資訊的方法稱為反射(Reflection)。

如何獲取一個class的Class例項?有三個方法:

方法一:直接通過一個class的靜態變數class獲取:

Class cls = String.class;

方法二:如果我們有一個例項變數,可以通過該例項變數提供的getClass()方法獲取:

String s = "Hello";
Class cls = s.getClass();

方法三:如果知道一個class的完整類名,可以通過靜態方法Class.forName()獲取:

Class cls = Class.forName("java.lang.String");

因為Class例項在JVM中是唯一的,所以,上述方法獲取的Class例項是同一個例項。可以用==比較兩個Class例項:

Class cls1 = String.class;

String s = "Hello";
Class cls2 = s.getClass();

boolean sameClass = cls1 == cls2; // true

注意一下Class例項比較和instanceof的差別:

Integer n = new Integer(123);

boolean b1 = n instanceof Integer; // true,因為n是Integer型別
boolean b2 = n instanceof Number; // true,因為n是Number型別的子類

boolean b3 = n.getClass() == Integer.class; // true,因為n.getClass()返回Integer.class
boolean b4 = n.getClass() == Number.class; // false,因為Integer.class!=Number.class

用instanceof不但匹配指定型別,還匹配指定型別的子類。而用==判斷class例項可以精確地判斷資料型別,但不能作子型別比較。

通常情況下,我們應該用instanceof判斷資料型別,因為面向抽象程式設計的時候,我們不關心具體的子型別。只有在需要精確判斷一個型別是不是某個class的時候,我們才使用==判斷class例項。

因為反射的目的是為了獲得某個例項的資訊。因此,當我們拿到某個Object例項時,我們可以通過反射獲取該Object的class資訊:

void printObjectInfo(Object obj) {
Class cls = obj.getClass();
}

要從Class例項獲取獲取的基本資訊,參考下面的程式碼:

// reflection
public class Main {
public static void main(String[] args) {
    printClassInfo("".getClass());
    printClassInfo(Runnable.class);
    printClassInfo(java.time.Month.class);
    printClassInfo(String[].class);
    printClassInfo(int.class);
}

static void printClassInfo(Class cls) {
    System.out.println("Class name: " + cls.getName());
    System.out.println("Simple name: " + cls.getSimpleName());
    if (cls.getPackage() != null) {
        System.out.println("Package name: " + cls.getPackage().getName());
    }
    System.out.println("is interface: " + cls.isInterface());
    System.out.println("is enum: " + cls.isEnum());
    System.out.println("is array: " + cls.isArray());
    System.out.println("is primitive: " + cls.isPrimitive());
}

}

注意到陣列(例如String[])也是一種Class,而且不同於String.class,它的類名是[Ljava.lang.String。此外,JVM為每一種基本型別如int也建立了Class,通過int.class訪問。

如果獲取到了一個Class例項,我們就可以通過該Class例項來建立對應型別的例項:

// 獲取String的Class例項:

Class cls = String.class;
// 建立一個String例項:
String s = (String) cls.newInstance();

上述程式碼相當於new String()。通過Class.newInstance()可以建立類例項,它的侷限是:只能呼叫public的無引數構造方法。帶引數的構造方法,或者非public的構造方法都無法通過Class.newInstance()被呼叫。

動態載入
JVM在執行Java程式的時候,並不是一次性把所有用到的class全部載入到記憶體,而是第一次需要用到class時才載入。例如:

// Main.java
public class Main {
public static void main(String[] args) {
    if (args.length > 0) {
        create(args[0]);
    }
}

static void create(String name) {
    Person p = new Person(name);
}
}

當執行Main.java時,由於用到了Main,因此,JVM首先會把Main.class載入到記憶體。然而,並不會載入Person.class,除非程式執行到create()方法,JVM發現需要載入Person類時,才會首次載入Person.class。如果沒有執行create()方法,那麼Person.class根本就不會被載入。

這就是JVM動態載入class的特性。

動態載入class的特性對於Java程式非常重要。利用JVM動態載入class的特性,我們才能在執行期根據條件載入不同的實現類。例如,Commons Logging總是優先使用Log4j,只有當Log4j不存在時,才使用JDK的logging。利用JVM動態載入特性,大致的實現程式碼如下:

// Commons Logging優先使用Log4j:
LogFactory factory = null;
if (isClassPresent("org.apache.logging.log4j.Logger")) {
factory = createLog4j();
} else {
factory = createJdkLog();
}

boolean isClassPresent(String name) {
try {
Class.forName(name);
return true;
} catch (Exception e) {
return false;
}
}

這就是為什麼我們只需要把Log4j的jar包放到classpath中,Commons Logging就會自動使用Log4j的原因。

小結
JVM為每個載入的class及interface建立了對應的Class例項來儲存class及interface的所有資訊;

獲取一個class對應的Class例項後,就可以獲取該class的所有資訊;

通過Class例項獲取class資訊的方法稱為反射(Reflection);

JVM總是動態載入class,可以在執行期根據條件來控制載入class。