本課程的目標是幫你更有效的使用Java。其中討論了一些高級主題,包括對象的創建、并發、序列化、反射以及其他高級特性。本課程將為你的精通Java的旅程提供指導。
1. 引言
在TIOBE 編程語言排名中,Sun 公司于1995年開發的Java語言是世界上使用最廣泛的編程語言之一。作為一種通用編程語言,因為強大的工具包和運行時環境、簡單的語法、豐富的平臺支持(一次編寫,到處運行)以及的異常活躍的社區支持,Java語言對軟件開發工程師極具吸引力。
在這一系列的文章中,涵蓋了Java相關的高級內容,因此假設讀者已具有基本語言知識。這并不是一個完整的參考手冊,而是讓你的技能更上一層樓的詳盡指南。
本課程中包含了大量的代碼片段,在有些對方為了做對比,會同時提供Java 7和Java 8的示例。
2. 實例構造
作為一種面向對象語言,對象的創建也許就是Java語言中最重要的概念之一。構造方法是在對象實例初始化過程中具有舉足輕重的地位,并且Java提供了多種方式來定義構造方法。
2.1 隱式(產生的)構造方法
Java允許在定義類時不聲明任何的構造方法,并這并不代表類沒有構造方法。我們看下面類的定義:
1
2
3
4
|
package com.javacodegeeks.advanced.construction; public class NoConstructor { } |
這個類未定義構造方法,但是Java編譯器會為其隱式生成一個,從而使我們可以使用new關鍵字來創建新的對象實例。
1
|
final NoConstructor noConstructorInstance = new NoConstructor(); |
2.2 無參構造方法
無參構造方法是最簡單的通過顯式聲明來替代Java編譯生成構造方法的方式。
1
2
3
4
5
6
7
|
package com.javacodegeeks.advanced.construction; public class NoArgConstructor { public NoArgConstructor() { // Constructor body here } } |
在使用new關鍵字創建新的對象實例時,上面的構造方法就會被調用。
2.3 有參構造方法
有參構造方法最有意思并且廣泛使用,通過指定參數來定制新實例的創建。下面的例子中定義了一個有兩個參數的構造方法。
1
2
3
4
5
6
7
|
package com.javacodegeeks.advanced.construction; public class ConstructorWithArguments { public ConstructorWithArguments( final String arg1, final String arg2) { // Constructor body here } } |
這種場景中,當使用new關鍵字來創建實例時,需要同時提供構造方法上定義的兩個參數。
1
2
|
final ConstructorWithArguments constructorWithArguments = new ConstructorWithArguments( "arg1" , "arg2" ); |
有趣的是構造方法之間可以通過this關鍵字互相調用。在實踐中,推薦通過使用this把多個構造方法鏈起來以減少代碼重復,并從基礎上使對象具有單一的初始化入口。作為示例,下面的代碼中定義了只有一個參數的構造方法。
1
2
3
|
public ConstructorWithArguments( final String arg1) { this (arg1, null ); } |
2.4 初始化代碼塊
除了構造方法,Java還提供了通過初始化代碼塊進行初始化的邏輯。這種用法雖然少見,但多了解一些也沒害處。
1
2
3
4
5
6
7
|
package com.javacodegeeks.advanced.construction; public class InitializationBlock { { // initialization code here } } |
另一方面,初始化代碼塊也可被看作是無參的隱式構造方法。在一個具體的類中可以定義多個初始化代碼塊,在執行的時候按照他們在代碼中的位置順序被調用,如下面的代碼所示:
1
2
3
4
5
6
7
8
9
10
11
12
|
package com.javacodegeeks.advanced.construction; public class InitializationBlocks { { // initialization code here } { // initialization code here } } |
實始化代碼塊并不是為了取代構造方法,相反它們可以同時出現。但是要記住,初始化代碼快會在構造方法調用之前被執行。
1
2
3
4
5
6
7
8
9
10
|
package com.javacodegeeks.advanced.construction; public class InitializationBlockAndConstructor { { // initialization code here } public InitializationBlockAndConstructor() { } } |
2.5 保證構造默認值
Java提供了確定的初始化保證,程序員可以直接使用初始化結果。未初始化的實例以及類變量(static)會自動初始化為相應的默認值。
類型 默認值
boolean False
byte 0
short 0
int 0
long 0L
char \u0000
float 0.0f
double 0.0d
對象引用 null
表 1
我們通過下面的例子來驗證上表中的默認值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package com.javacodegeeks.advanced.construction; public class InitializationWithDefaults { private boolean booleanMember; private byte byteMember; private short shortMember; private int intMember; private long longMember; private char charMember; private float floatMember; private double doubleMember; private Object referenceMember; public InitializationWithDefaults() { System.out.println( "booleanMember = " + booleanMember ); System.out.println( "byteMember = " + byteMember ); System.out.println( "shortMember = " + shortMember ); System.out.println( "intMember = " + intMember ); System.out.println( "longMember = " + longMember ); System.out.println( "charMember = " + Character.codePointAt( new char [] { charMember }, 0 ) ); System.out.println( "floatMember = " + floatMember ); System.out.println( "doubleMember = " + doubleMember ); System.out.println( "referenceMember = " + referenceMember ); } } |
當使用new關鍵字實例化對象之后:
final InitializationWithDefaults initializationWithDefaults = new InitializationWithDefaults(),
可從控制臺中看到輸出結果如下:
1
2
3
4
5
6
7
8
9
|
booleanMember = false byteMember = 0 shortMember = 0 intMember = 0 longMember = 0 charMember = 0 floatMember = 0.0 doubleMember = 0.0 referenceMember = null |
2.6 可見性
構造方法遵從Java的可見性規則,并且可以通過訪問控制修飾符決定在其他類中是否能調用該構造方法。
修飾符 包可見性 子類可見性 公開可見性
public 可見 可見 可見
protected 可見 可見 不可見
<無修飾符> 可見 不可見 不可見
private 不可見 不可見 不可見
表2
2.7 垃圾回收
Java(準確的說是JVM)擁有自動的垃圾回收機制。簡單來講,當有新對象創建時,會自動為其分配內在;然后當對象不再被引用后,他們會被自動銷毀,相應的內存也會被回收。
Java垃圾回收采用分代回收的機制,并基于"大多數對象生命短暫"的假設(即在對象創建之后很快就不會被再引用,所以可以被安全的銷毀)。大多程序員習慣性的認為Java中對象創建的效率很低所以要盡可能避免新對象的創建。事實上,這種認識是不對的。在Java中創建對象的開銷是相當低的,并且速度很快。真正代來巨大開銷的是不必要的長期存活的對象,因此他們最終會被遷移到老年代,并導致stop-the-world發生。
2.8 對象終結器(Finalizers)
前面我們講述的都是構造方法和對象初始化相關的主題,但還未提及他們的反面:對象銷毀。主要是因為Java使用垃圾回收機制來管理對象的生命周期,所以銷毀不必要的對象并釋放所需內存就成了垃圾回收的職責了。
不過,Java還是提供了另外一種類似于析構函數的終結器(finalizer)的特性,擔任多種資源清理的責任。Finalizer一般被看作是危險的事情(因為它會帶來多種副作用和性能問題)。通常并不需要finalizer因此要盡量避免使用它(除了極少見的包含大量本地對象(native objects)的場景)。Java 7中引入的try-with-resources語法和AutoCloseable接口可當作finalizer的替代選擇,并可寫出如下簡潔的代碼:
1
2
3
|
try ( final InputStream in = Files.newInputStream( path ) ) { // code here } |
3. 靜態初始化
上面我們學習了類實例的構造與初始化,除此之外,Java還支持類級別的初始化構造,稱作靜態初始化。靜態初始化與上面介紹的初始化代碼塊類似,只是多了額外的static關鍵字修飾。需要注意的是靜態初始化只會在類加載時執行一次。示例如下:
與初始化代碼塊類似,可以在類中定義多個靜態初始化塊,它們在類中的位置決定在初始化時執行的順序。示例如下;
1
2
3
4
5
6
7
8
9
10
11
|
package com.javacodegeeks.advanced.construction; public class StaticInitializationBlocks { static { // static initialization code here } static { // static initialization code here } } |
因為靜態初始化塊可以被多個并行執行的線程觸發(當類被初始加載時),JVM運行時保證初始化的代碼以線程安全的方式只被執行一次。
4. 構造器模式
這些年多種容易理解的構造器(創建者)模式被引入到Java社區。下面我們會學習其中比較流行的幾個:單例模式、輔助類模式、工廠模式以及依賴注入(也稱為控制反轉)。
4.1 單例模式
單例是一種歷史悠久卻在軟件開發社區中飽受爭議的模式。單例模式的核心理念是保證在任何時候給定的類只有一個對象被創建。雖然聽起來很簡單,但人們對如何以正確且線程安全的方式創建對象進行了大量的討論。下面的代碼中展示了簡單版本的單例模式實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package com.javacodegeeks.advanced.construction.patterns; public class NaiveSingleton { private static NaiveSingleton instance; private NaiveSingleton() { } public static NaiveSingleton getInstance() { if ( instance == null ) { instance = new NaiveSingleton(); } return instance; } } |
上面的代碼至少有一個問題:在多線程并發場景中可能會創建出多個對象。一種合理的實現方式(但不能延遲加載)是使用類的static`final`屬性。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
final property of the class . package com.javacodegeeks.advanced.construction.patterns; public class EagerSingleton { private static final EagerSingleton instance = new EagerSingleton(); private EagerSingleton() { } public static EagerSingleton getInstance() { return instance; } } |
如果你不想浪費寶貴的資源,希望單例對象只在真正需要的時候才被創建,那么就要使用顯式的同步方式,不可這種方法可能會降低多線程環境下的并發性(更多關于Java并發的細節將會在Java進階9-并發最佳實踐中詳細介紹)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package com.javacodegeeks.advanced.construction.patterns; public class LazySingleton { private static LazySingleton instance; private LazySingleton() { } public static synchronized LazySingleton getInstance() { if ( instance == null ) { instance = new LazySingleton(); } return instance; } } |
現在,在很多場景下單例模式不再被認為是一種好的選擇,因為他們會使代碼不易于測試。另外依賴注入模式的產生也使單例模式變得不再必要。
4.2 工具類/輔助類
工具類/輔助類模式在Java開發者當中相當流行。它的核心理念就是使用不可實例化的類(通過聲明private構造方法)、可選的final(更多關于聲明final類的細節將會在Java進階3-類和接口的設計中詳細介紹)關鍵字以及靜態方法。示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package com.javacodegeeks.advanced.construction.patterns; public final class HelperClass { private HelperClass() { } public static void helperMethod1() { // Method body here } public static void helperMethod2() { // Method body here } } |
很多經驗豐富的開發者認為這種模式會讓工具類成為各種不相關方法的容器。因為有些方法沒有合適的放置位置卻需要被其他類使用,就會被誤放入工具類中。在大多數場景中也應該避免這種設計:總會有更好的功能復用的方式,保持代碼清晰簡潔。
4.3 工廠模式
工廠模式被證明是開發者的極其強大的利器,在Java中有多種實現方式:工廠方法和抽象工廠。最簡單的例子就是使用static方法返回特定類的實例(工廠方法),如下:
1
2
3
4
5
6
7
8
9
10
|
package com.javacodegeeks.advanced.construction.patterns; public class Book { private Book( final String title) { } public static Book newBook( final String title ) { return new Book( title ); } } |
雖然使用這種方法能提高代碼的可讀性,但經常爭議的一點是難以給newBook工廠方法賦予更豐富的場景。另外一種實現工廠模式的方法是采用接口或抽象類(抽象工廠)。如下,我們定義一個工廠接口:
1
2
3
|
public interface BookFactory { Book newBook(); } |
根據圖片館的不同,我們可以有多種不同的newBook實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class Library implements BookFactory { @Override public Book newBook() { return new PaperBook(); } } public class KindleLibrary implements BookFactory { @Override public Book newBook() { return new KindleBook(); } } |
現在,BookFactory的不同實現屏蔽掉了具體Book的不同,卻提供了通用的newBook的方法。
4.4 依賴注入
依賴注入(也稱為控制反轉)被類設計者認為是一種良好的設計實踐:如果一些類實例依賴其他類的實例,那些被依賴的實例應該通過構造方法(或者setter方法、策略等方式)提供(注入),而不應該是由實例自己去創建。先看一下下面的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
|
package com.javacodegeeks.advanced.construction.patterns; import java.text.DateFormat; import java.util.Date; public class Dependant { private final DateFormat format = DateFormat.getDateInstance(); public String format( final Date date ) { return format.format( date ); } } |
Dependant類需要一個DateFormat類的實例并通過在實例化對象時通過DateFormat.getDateInstance()的方式獲得。更好的方式應該通過構造方法的參數來完成同樣的事情:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package com.javacodegeeks.advanced.construction.patterns; import java.text.DateFormat; import java.util.Date; public class Dependant { private final DateFormat format; public Dependant( final DateFormat format ) { this .format = format; } public String format( final Date date ) { return format.format( date ); } } |
在上面的例子中,類實例的所有依賴都由外部提供,這樣就很容易調整DateFormat,并易于編寫測試代碼。