【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。