《Java編程思想》讀書筆記(二)

三年之前就買了《Java編程思想》這本書,但是到現在爲止都還沒有好好看過這本書,這次希望能夠堅持通讀完整本書並整理好自己的讀書筆記,上一篇文章是記錄的第一章到第十章的內容,這一次記錄的是第十一章到第十六章的內容,寫《Java編程思想》讀書筆記一的時間還是2022-01-26,沒注意又拖這麼久了,本文還是會把自己感興趣的知識點記錄一下,相關實例代碼:https://gitee.com/reminis_com/thinking-in-java

第十一章:持有對象

如果一個程序只包含固定數量的且其生命週期都是已知的對象,那麼這是一個非常簡單的程序。

​ 通常,程序總是根據運行時才知道的某些條件去創建新對象,在此之前,不會知道所需對象的數量,甚至不知道確切的對象。例如我們熟知的數組,它是編譯器支持的類型,數組是保存一組對象的最有效的方式,如果你想保存一組基本類型的數據,也推薦使用這種方式。但是數組具有固定的尺寸,而在更一般的情況下中,你在寫程序時並不知道需要多少個對象,或者是否需要更復雜的方式來存儲對象,因此數組尺寸固定這一限制顯得過於受限了。

​ Java實用類庫提供了一套相當完整的容器類來解決這個基本問題,Java容器類都可以自動地調整自己的尺寸。Java容器類庫的用途是“保存對象”,並將其劃分成兩個不同的概念:

  • Collection:一個獨立元素的序列,這些元素都服從一條或多條規則。List必須按照插入的順序保存元素,而Set不能有重複元素。Queue按照排隊規則來確定對象的產生順序(通常與它們被插入的順序相同)
  • Map:一組成對的“鍵值對”對象,允許你使用鍵來查找值
  1. 添加一組元素

    1. Collections.addAll()方法接受一個Collection對象,以及一個數組或用逗號分隔的列表,將元素添加到Collection中。Collection.addAll()成員方法只能接受另一個Collection對象作爲參數因此它不如Arrays.asList()或Collections.addAll()靈活,這兩個方法都是使用的可變參數列表。
    2. 你也可以直接使用Arrays.asList()的輸出作爲List,但是在這種情況下,其底層表示的是數組,因此不能調整尺寸,如果你試圖使用add()或delete()方法在這中列表中添加或刪除元素,在運行時就會獲得“java.lang.UnsupportedOperationException(不支持的操作)”異常。
    import java.util.*;
    
    public class AddingGroup {
    
        public static void main(String[] args) {
            Collection<Integer> collection =
                    new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
            Integer[] moreInts = { 6, 7, 8, 9, 10 };
            collection.addAll(Arrays.asList(moreInts));
    
            Collections.addAll(collection, 11, 12, 13, 14, 15);
            Collections.addAll(collection, moreInts);
    
            List<Integer> list = Arrays.asList(16, 17, 18, 19, 20);
            list.set(1, 99);
    //        list.add(21); // java.lang.UnsupportedOperationException
        }
    }
    
  2. List:List承諾可以將元素維護在特定的序列中。List接口在Collection的基礎上添加了大量的方法,使得可以在List的中間插入和移除元素,

    • ArrayList:長於隨機訪問元素,但是在List的中間插入和移除元素時較慢

    • LinkedList:它通過代價較低的在List中間進行插入和刪除操作,提供了優化的順序訪問。LinkedList在隨機訪問方面相對較慢,但它的特性集較ArrayList更大。此外,LinkedList還提供了可以使其用作棧、隊列或雙端隊列的方法。這些方法中有些彼此之間只是名稱有些差異,或者只存在些許差異,以使得這些名字在特定用法的上下文環境中更加適用(特別是Queue中),如下:

      • getFirst()和element()完全一樣,都是返回列表的頭(第一個元素),而不移除它。如果List爲空,則拋出NoSuchElementException。peek()方法與這兩個方法只是稍有差異,它在列表爲空時返回null。

      • removeFirst()和remove()也是完全一樣的,它們移除並返回列表的頭,而在列表爲空時拋出NoSuchElementException。poll()方法稍有差異,它在列表爲空時返回null。

      • addFirst()與add()和addLast()相同,它們都將某個元素插入到列表的尾(端)部。

      • removeLast()移除並返回列表的最後一個元素。

      • 棧(Stack):通常是指“後進先出”(LIFO)的容器,即最後一個“壓入”棧的元素,第一個“彈出”棧。LinkedList具有能夠直接實現棧的所有功能的方法,因此可以直接將LinkedList作爲棧使用,不過,有時一個真正的“棧”更能把事情講清楚:

        public class Stack<T> {
            private LinkedList<T> storage = new LinkedList<>();
            
            public void push(T v) {
                storage.addFirst(v);
            }
            
            public T peek() {
                return storage.getFirst();
            }
             
            public T pop() {
                return storage.removeFirst();
            }
            
            public boolean isEmpty() {
                return storage.isEmpty();
            }
        
            @Override
            public String toString() {
                return storage.toString();
            }
        }
        
  3. 迭代器:任何容器類,都必須有某種方式可以插入元素並將它們再次取回。畢竟,持有事務是容器最基本的動作。迭代器統一了對容器的訪問方式。迭代器通常被稱爲輕量級對象,創建它的代價小,並且Java的Iterator只能單向移動,這個Iterator只能用來:

    • 使用iterator()方法要求容器返回一個Iterator。Iterator將準備好返回序列的第一個元素
    • 使用next()獲得序列中的下一個元素
    • 使用hasNext()檢查序列中是否還有元素
    • 使用remove()將迭代器新近返回的元素刪除

    注意:

    • 由於Iterator可以移除由next()產生的最後一個元素,這意味着在調用remove()之前必須先調用next()。
    • ListIterator是一個更強大的Iterator的子類型,但它只能用於各種List類的訪問。儘管Iterator只能向前移動,但是ListIterator可以雙向移動。
  4. Set:Set不保存重複的元素,Set具有與Collection完全一樣的接口,因此沒有任何額外的功能。HashSet所維護的順序與TreeSet或LinkedHashSet都不同,HashSet使用的是散列函數,LinkedHashSet因爲查詢速度的原因也使用了散列,但是看起來它使用了鏈表來維護元素的插入順序,它按照被添加的順序保存元素。TreeSet默認會按照比較結果的升序保存對象。

  5. Map:將對象映射到其它對象的能力是一種解決編程問題的殺手鐗。例如,考慮一個程序,他將用來檢查Java的Random類得隨機性。理想狀態下,Random可以產生理想的數字分佈,但要想測試它,則需要生成大量的隨機數,並對落入各種不同範圍的數字進行計數,Map可以很容易的解決該問題,在本列中,鍵是Random產生的數字,值是該數字出現的次數。

    public class Statistics {
    
        public static void main(String[] args) {
            Random rand = new Random(47);
            Map<Integer, Integer> map = new HashMap<>();
            for (int i = 0; i < 10000; i++) {
                // 隨機生成一個0~20範圍內的數字
                int r = rand.nextInt(20);
                Integer freq = map.get(r);
                map.put(r, freq == null ? 1 : freq + 1);
            }
            System.out.println(map);
        }
    }
    
    //output: {0=481, 1=502, 2=489, 3=508, 4=481, 5=503, 6=519, 7=471, 8=468, 9=549, 10=513,
    // 11=531, 12=521, 13=506, 14=477, 15=497, 16=533, 17=509, 18=478, 19=464}
    
  6. Queue(隊列):隊列是一個典型的先進先出(FIFO)的容器。即從容器的一端放入事物,從另一端取出,並且事務放入容器的順序和取出的順序是相同的。隊列常被當作一種可靠的將對象從程序的某個區域傳輸到另一個區域的途徑。LinkedList提供了方法支持隊列的行爲,並且它是實現了Queue接口,因此LinkedList可以用作Queue的一種實現。

    public class QueueDemo {
    
        public static void printQ(Queue queue) {
            while (queue.peek() != null) {
                System.out.print(queue.remove() + " ");
            }
            System.out.println();
        }
    
        public static void main(String[] args) {
            Queue<Integer> queue = new LinkedList<>();
            Random random = new Random(47);
            for (int i = 0; i < 10; i++) {
                queue.offer(random.nextInt(i + 10));
            }
            printQ(queue);
            Queue<Character> qc = new LinkedList<>();
            for (char c : "Reminis".toCharArray()) {
                qc.offer(c);
            }
            printQ(qc);
        }
    }
    /**
     * output:
     * 8 1 1 1 5 14 3 1 0 1 
     * R e m i n i s 
     */
    

    offer()方法是與Queue相關的方法之一,它在允許的情況下,將一個元素插入到隊尾,或者返回false。peek()和element()都將在不移除的情況下返回隊頭,但是peek()方法在隊列爲空時返回null,而element()在隊列爲空時拋出NoSuchElementException異常。poll()和remove()方法都將移除並返回隊頭,但是poll()方法在隊列爲空時返回null,而remove()在隊列爲空時拋出NoSuchElementException異常。

    • PriorityQueue: 先進先出描述了最典型的隊列規則。隊列規則是指在給定一組隊列中的元素的情況下,確定一個彈出隊列的元素的規則。先進先出聲明的是下一個元素應該是等待時間最長的元素。

    • 優先級隊列聲明下一個彈出元素是最需要的元素(具有最高的優先級)。例如,在飛機場,飛機臨近起飛時,這架飛機的乘客可以在辦理登機手續時排到隊頭。如果構建了一個消息系統。某些消息比其他消息更重要,因而應該更快地得到處理,那麼它們何時得到處理就與它們何時到達無關。PriorityQueue添加到Java SE5中,是爲了提供這種行爲的一種自動實現。

    • 當你在PriorityQueue上調用offer()方法來插入一個對象時,這個對象會在隊列中被排序。默認的排序將使用對象在隊列中的自然順序,但是你可以通過提供自己的Comparator來修改這個順序。PriorityQueue可以確保當你調用peek()、poll()和remove()方法時,獲取的元素將是隊列中優先級最高的元素。

    • 讓PriorityQueue與Integer、String和Character這樣的內置類型一起工作易如反掌。在下面的示例中,第一個值集與前一個示例中的隨機值相同,因此你可以看到它們從PriorityQueue中彈出的順序與前一個示例不同∶

      public class PriorityQueueDemo {
          public static void main(String[] args) {
              PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
              Random rand = new Random(47);
              for (int i = 0; i < 10; i++) {
                  priorityQueue.offer(rand.nextInt(i + 10));
              }
              QueueDemo.printQ(priorityQueue);
      
              List<Integer> ints = Arrays.asList(25, 22, 20, 18, 14, 9, 3, 1, 1, 2, 3, 9, 14, 18, 21, 23, 25);
              priorityQueue = new PriorityQueue<>(ints);
              QueueDemo.printQ(priorityQueue);
      
              priorityQueue = new PriorityQueue<>(ints.size(), Collections.reverseOrder());
              priorityQueue.addAll(ints);
              QueueDemo.printQ(priorityQueue);
      
              String fact = "EDUCATION SHOULD ESCHEW OBFUSCATION";
              List<String> strings = Arrays.asList(fact.split(""));
              PriorityQueue<String> stringPQ = new PriorityQueue<>(strings);
              QueueDemo.printQ(stringPQ);
      
              stringPQ = new PriorityQueue<>(strings.size(), Collections.reverseOrder());
              stringPQ.addAll(strings);
              QueueDemo.printQ(stringPQ);
      
              Set<Character> charSet = new HashSet<>();
              for (char c : fact.toCharArray()) {
                  charSet.add(c);
              }
              PriorityQueue<Character> charPQ = new PriorityQueue<>(charSet);
              QueueDemo.printQ(charPQ);
          }
      }
      /**
       * output:
       * 
       * 0 1 1 1 1 1 3 5 8 14 
       * 1 1 2 3 3 9 9 14 14 18 18 20 21 22 23 25 25 
       * 25 25 23 22 21 20 18 18 14 14 9 9 3 3 2 1 1 
       *       A A B C C C D D E E E F H H I I L N N O O O O S S S T T U U U W 
       * W U U U T T S S S O O O O N N L I I H H F E E E D D C C C B A A       
       *   A B C D E F H I L N O S T U W 
       */
      

      你可以看到,重複是允許的,最小的值擁有最高的優先級(如果是String,空格也可以算作值,並且比字母的優先級高)。爲了展示你可以使用怎樣的方法通過提供自己的Comparator象來改變排序,第三個對PriorityQueue的構造器調用,和第二個對PriorityQueue 的調用使用了由Collection.reverseOrder()(新添加到JavaSE5中的)產生的反序Comparator。

      最後一部分添加了一個HashSet來消除重複的Character,這麼做只是爲了增添點樂趣 Integer、String和Character可以與PriorityQueue一起工作,因爲這些類已經內建了自排序。如果你想在PriorityQueue中使用自己的類,就必須包括額外的功能以產生自然排序,或者必須提供自己的Comparator。

    總結:

    Java提供了大量持有對象的方式∶

    1)數組將數字與對象聯繫起來。它保存類型明確的對象,查詢對象時,不需要對結果做類型轉換。它可以是多維的,可以保存基本類型的數據。但是,數組一旦生成,其容量就不能改變。

    2)Collection保存單一的元素,而Map保存相關聯的鍵值對。有了Java的泛型,你就可以指定容器中存放的對象類型,因此你就不會將錯誤類型的對象放置到容器中,並且在從容器中獲取元素時,不必進行類型轉換。各種Collection和各種Map都可以在你向其中添加更多的元素時,目動調整其尺寸。容器不能持有基本類型,但是自動包裝機制會仔細地執行基本類型到容器中所持有的包裝器類型之間的雙向轉換。

    3)像數組一樣,List也建立數字索引與對象的關聯,因此,數組和List都是排好序的容器。Lis能夠自動擴充容量。

    4)如果要進行大量的隨機訪問,就使用ArrayList;如果要經常從表中間插入或刪除元素,則應該使用LinkedList。

    5)各種Queue以及棧的行爲,由LinkedList提供支持。

    6) Map是一種將對象(而非數字)與對象相關聯的設計。HashMap設計用來快速訪問;而TreMap保持"鍵"始終處於排序狀態,所以沒有HashMap快。LinkedHashMap保持元素插入的順序,但是也通過散列提供了快速訪問能力。

    7)Set不接受重複元素。HashSet提供最快的查詢速度,而TreeSet保持元素處於排序狀態。linkedHashSet以插入順序保存元素

    8)新程序中不應該使用過時的Vector、Hashtable和Stack。

第十二章:通過異常處理錯誤

Java的基本理念是“結構不佳的代碼不能運行”。

​ 發現錯誤的理想時機是在編譯階段,也就是在你試圖運行程序之前。然而,編譯期間並不能找出所有的錯誤,餘下的問題必須在運行期間解決。Java使用異常來提供一致的錯誤報告模型,使得構件能夠與客戶端代碼可靠地溝通問題。

​ 異常情形是指阻止當前方法或作用域繼續執行的問題。把異常情形和普通問題區分相當重要,所謂普通問題是指,在當前環境能夠得到足夠的信息,總能處理這個錯誤。而對於異常 情形,就不能繼續下去了,因爲在當前環境下無法獲得必要信息來解決問題。你所能做的就是從當前環境中跳出,並且把問題交給上一級環境,這就是拋出異常時所發生的事情。

​ 與使用Java中的其它對象一樣,我們總是用new在堆上創建異常對象,這也伴隨着存儲空間的分配和構造器的調用。所有標準異常類都有兩個構造器:一個是默認構造器;另一個是接受字符串作爲參數,以便能把相關信息放入異常對象的構造器,如throw new NullPointerException("t = null");。此外,能夠拋出任意類型的Throwable對象,它是異常類型的根類。通常對於不同類型的錯誤,要拋出相應的異常。錯誤信息可以保存在異常對象內部或者使用異常類的名稱來暗示。

捕獲異常:要明白異常是如何被捕獲的,必須首先理解監控區域的概念。它是一段可能產生異常的代碼,並且後面跟着處理這些異常的代碼。在try塊裏“嘗試”各種(可能產生異常的)方法調用。當然,拋出的異常必須在某處得到處理,這個地點就是異常處理程序 ,而且針對每個要捕獲的異常,得準備相應的處理程序。異常處理程序緊跟在try塊之後,以關鍵字catch表示:

try {
	// code that might generate exceptions
} catch (Type1 t1) {
	// handle exception of Type1
} catch (Type2 t2) {
	// handle exception of Type2
} catch (Type3 t3) {
	// handle exception of Type3
}

​ 每個catch子句(異常處理程序)看起來就像是接受一個且僅接受一個特殊類型的參數的方法。異常處理程序必須緊跟在try塊之後,當異常被拋出時,異常處理機制將負責搜尋參數與異常類型相匹配的第一個處理程序。然後進入catch子句執行,此時認爲異常得到了處理。一旦catch子句執行結束,則處理程序的查找過程執行結束。注意,只有匹配的catch子句才能得到執行。

​ 使用finally進行清理:對於一些代碼,可能會希望無論try塊中的異常是否拋出,它們都能得到執行。這通常適用於除內存之外的資源恢復到它們初始狀態時,這種需要清理的資源包括:已經打開的文件或網絡連接,在屏幕上畫的圖形,甚至可以是外部世界的某個開關,如下例所示:

public class Switch {
  private boolean state = false;
  public boolean read() { return state; }
  public void on() { state = true; print(this); }
  public void off() { state = false; print(this); }
  public String toString() { return state ? "on" : "off"; }
}

/**
 * 程序的目的是要確保main()結束的時候開關必須是關的,所以在每個try塊和異常處理程序的末尾
 * 都加入了對sw.off()方法的調用。但也可能有這種情況:異常被拋出,但沒被異常處理程序捕獲,
 * 這時sw.off()就得不到調用。但是有了finally,只要把try塊中清理代碼移放在一處即可:
 */
public class OnOffSwitch {
  private static Switch sw = new Switch();
    
  public static void f() throws OnOffException1,OnOffException2 {}
    
  public static void main(String[] args) {
    try {
      sw.on();
      // Code that can throw exceptions...
      f();
      sw.off();
    } catch(OnOffException1 e) {
      System.out.println("OnOffException1");
      sw.off();
    } catch(OnOffException2 e) {
      System.out.println("OnOffException2");
      sw.off();
    }
  }
}

/**
 * 這裏sw.off()被移到一處,並且保證在任何情況下都能得到執行。
 */
public class WithFinally {
  static Switch sw = new Switch();
  public static void main(String[] args) {
    try {
      sw.on();
      // Code that can throw exceptions...
      OnOffSwitch.f();
    } catch(OnOffException1 e) {
      System.out.println("OnOffException1");
    } catch(OnOffException2 e) {
      System.out.println("OnOffException2");
    } finally {
      sw.off();
    }
  }
}

異常使用指南

  1. 在恰當的級別處理問題。(在知道該如何處理的情況下才捕獲異常。)

  2. 解決問題並且重新調用產生異常的方法。

  3. 進行少許修補,然後繞過異常發生的地方繼續執行。

  4. 用別的數據進行計算,以代替方法預計會返回的值。

  5. 把當前運行環境下能做的事情儘量做完,然後把相同的異常重拋到更高層。

  6. 把當前運行環境下能做的事情儘量做完,然後把不同的異常拋到更高層。

  7. 終止程序。

  8. 進行簡化。(如果你的異常模式使問題變得太複雜,那用起來會非常痛苦也很煩人。)

  9. 讓類庫和程序更安全。(這既是在爲調試做短期投資,也是在爲程序的健壯性做長期投資。)

第十三章:字符串

​ 由於字符串在我們開發中使用頻率是相當高的,本章內容也主要介紹了一些關於字符串常用的API,需要注意的是String對象是不可變的,String類中每一個看起來會修改String值的方法,實際上都是創建了一個全新的String對象,以包含修改後的字符串內容,而最初的String對象則絲毫未動。

重載“+”與StringBuilder

​ 不可變性會帶來一定的效率問題,爲String對象重載的“+”操作符就是一個例子。重載的意義是,一個操作符在應用於特定的類時,被賦予了特殊的意義(用於String的“+”與“+=”是Java中僅有的兩個重載過的操作符,而Java並不允程序員重載任何操作符,但C++允許程序員任意重載操作符)。操作符可以用來連接String:

public class Concatenation {
    public static void main(String[] args) {
        String mango = "mango";
        String s = "abc" + mango + "def" + 47;
        System.out.println(s);
    }
}
// output: abcmangodef47

​ 我們可以通過JDK自帶的javap來反編譯以上代碼,命令如下:javap -c Concatenation.class,這裏的-c表示將要生成JVM字節碼。

public class strings.Concatenation {
  public strings.Concatenation();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String mango
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: ldc           #5                  // String abc
      12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      15: aload_1
      16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: ldc           #7                  // String def
      21: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      24: bipush        47
      26: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      29: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      32: astore_2
      33: getstatic     #10                 // Field java/lang/System.out:Ljava/io/PrintStream;
      36: aload_2
      37: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      40: return
}

​ 我們從生成後的字節碼中可以看到,編譯器自動引入了java.lang.StringBuilder類,雖然我們在源代碼中並沒有使用StringBuilder類,但是編譯器卻自主主張地使用了它,因爲它更高效。

​ 現在,也許你會覺得可以隨意使用String對象,反正編譯器會爲你自動地優化性能,讓我們更深入地看看編譯器能爲我們優化到什麼程度。下面程序採用兩種方式生成一個String:方法一使用了多個String對象,方法二在代碼中使用了StringBuilder。

public class WitherStringBuilder {

    /**
     * 使用String進行字符串拼接
     * @param fields 字符串數組
     * @return 拼接後的字符串
     */
    public String implicit(String[] fields) {
        String result = "";
        for (int i = 0; i < fields.length; i++) {
            result += fields[i];
        }
        return result;
    }

    /**
     * 使用StringBuilder進行字符串拼接
     * @param fields 字符串數組
     * @return 拼接後的字符串
     */
    public String explicit(String[] fields) {
        StringBuilder result = new StringBuilder();
        for (int i = 0; i < fields.length; i++) {
            result.append(fields[i]);
        }
        return result.toString();
    }
}

現在運行javap -c WitherStringBuilder.class,可以看到 兩個方法對應的字節碼,首先是implict()方法:

​ 注意從第8行到第35行構成了一個循環體。第8行∶對堆棧中的操作數進行"大於或等於的整數比較運算",循環結束時跳到第38行。第35行∶返回循環體的起始點(第5行)。要注意的重點是;StringBuilder是在循環之內構造的,這意味着每經過循環一次,就會創建一個新的StringBuilder 對象。

下面是explicit()方法對應的字節碼∶

​ 可以看到.不僅循環部分的代碼更簡短、更簡單,而目它只生成了一個StringBuilder對象。顯式地創建StringBuilder還允許你預先爲其指定大小。如果你已經知道最終的字符串大概有多長,那預先指定StringBuilder的大小可以避免多次重新分配緩衝。

​ 
因此,當你爲一個類編寫toString()方法時,如果字符串操作比較簡單,那就可以信賴編譯器,它會爲你合理地構造最終的字符串結果。但是,如果你要在toString()方法中使用循環,那麼最好自己創建一個StringBuilder對象,用它來構造最終的結果。請參考以下示例∶

public class UsingStringBuilder {

    private static Random random = new Random(47);

    @Override
    public String toString() {
        StringBuilder result = new StringBuilder("[");
        for (int i = 0; i < 25; i++) {
            result.append(random.nextInt(100));
            result.append(", ");
        }
        result.delete(result.length() - 2, result.length());
        result.append("]");
        return result.toString();
    }

    public static void main(String[] args) {
        UsingStringBuilder usb = new UsingStringBuilder();
        System.out.println(usb);
    }
}


​ StringBuilder提供了豐富而全面的方法,包括insert()、repleace()、substring()甚至reverse(),但是最常用的還是append()和toString()。還有delete()方法,上面的例子中我們用它刪除最後一個逗號與空格,以便添加右括號。

StringBuilder是Java SE5引入的,在這之前Java用的是StringBuffer。後者是線程安全的,因此開銷也會大些,所以在Java SE5/6中,字符串操作應該還會更快一點。

第十四章:類型信息

運行時類型信息使得你可以在程序運行時發現和使用類型信息。

它使你從只能在編譯期執行面向類型的操作的整錮中解脫了出來。並且可以使用某些非常強大的程序。對RTTI的需要,揭示了面向對象設計中許多有趣(並且複雜)的問題,同時也提出瞭如何組織程序的問題。

​ 
本章將討論 Java 是如何讓我們在運行時識別對象和類的信息的。主要有兩種方式∶一種是"傳統的"RTTI,它假定我們在編譯時已經知道了所有的類型;另一種是"反射"機制,它允許我們在運行時發現和使用類的信息。在Java中,所有的類型轉換都是在運行時進行正確檢查的,這也是RTTI名字的含義:在運行時,識別一個對象的類型。

Class對象

​ 要理解RTTI在Java中的工作原理,首先必須知道類型信息在運行時是如何表示的。這項工作是由稱爲Class對象的特殊對象完成的,它包含了與類有關的信息。事實上,Class對象就是用來創建類的所有的”常規“對象的。Java使用Class對象來執行其RTTI,即使你正在執行的是類似轉型這樣的操作。Class類還擁有大量的使用RTTI的其他方式。
類是程序的一部分,每個類都有一個Class對象。換言之,每當編寫並且編譯了一個新類,就會產生一個Class對象(更恰當地說,是被保存在一個同名的.class文件中)。爲了生成這個類的對象,運行這個程序的Java虛擬機(JVM)將使用被稱爲"類加載器"的子系統。

​ 
類加載器子系統實際上可以包含一條類加載器鏈,但是隻有一個原生類加載器,它是JVM 實現的一部分。原生類加載器加載的是所謂的可信類,包括Java API類,它們通常是從本地盤加載的。在這條鏈中,通常不需要添加額外的類加載器,但是如果你有特殊需求(例如以某種特殊的方式加載類,以支持Web服務器應用,或者在網絡中下載類),那麼你有一種方式可以掛接額外的類加載器。
所有的類都是在對其第一次使用時,動態加載到JVM中的。當程序創建第一個對類的靜態成員的引用時,就會加載這個類。這個證明構造器也是類的靜態方法,即使在構造器之前並沒有使用static關鍵字。因此,使用new操作符創建類的新對象也會被當作對類的靜態成員的引用。
因此,Java程序在它開始運行之前並非被完全加載,其各個部分是在必需時才加載的。這一點與許多傳統語言都不同。動態加載使能的行爲,在諸如C++這樣的靜態加載語言中是很難或者根本不可能複製的。

​ 
類加載器首先檢查這個類的Class對象是否已經加載。如果尚未加載,默認的類加載器就會根據類名查找.class文件(例如,某個附加類加載器可能會在數據庫中查找字節碼)。在這個類的字節碼被加載時,它們會接受驗證,以確保其沒有被破壞,並且不包含不良Java代碼(這是Java 中用於安全防範目的的措施之一)。

​ 
一旦某個類的Class對象被載入內存,它就被用來創建這個類的所有對象。下面的示範程序可以證明這一點∶

class Candy {
    static {
        System.out.println("Loading Candy");
    }
}

class Gum {
    static {
        System.out.println("Loading Gum");
    }
}

class Cookie {
    static {
        System.out.println("Loading Cookie");
    }
}


public class SweetShop {
    public static void main(String[] args) {
        System.out.println("inside main");
        new Candy();
        System.out.println("After creating Candy");
        try {
            Class.forName("typeinfo.Gum");
        } catch (ClassNotFoundException e) {
            System.out.println("Couldn't find Gum");
        }
        System.out.println("After Class.forName(\"Gum\")");
        new Cookie();
        System.out.println("After creating Cookie");
    }
}/** output:
 * inside main
 * Loading Candy
 * After creating Candy
 * Loading Gum
 * After Class.forName("Gum")
 * Loading Cookie
 * After creating Cookie
 */

​ 這裏的每個類Candy、Gum和Cookie,都有一個static子句,該子句在類第一次被加載時執行。這時會有相應的信息打印出來,告訴我們這個類什麼時候被加載了。在main()中,創建對象的代碼被置於打印語句之間,以幫助我們判斷加載的時間點。
從輸出中可以看到,Cass對象僅在需要的時候才被加載。

類字面常量

​ Java還提供了另一種方法來生成對Class對象的引用,即使用類字面常量。對上述程序來說,就像下面這樣∶
Gum.class;
這樣做不僅更簡單,而且更安全,因爲它在編譯時就會受到檢查(因此不需要置於try語句塊中)。並且它根除了對forName()方法的調用,所以也更高效。

​ 
類字面常量不僅可以應用於普通的類,也可以應用於接口、數組以及基本數據類型。另外,對於基本數據類型的包裝器類,還有一個標準字段TYPE。TYPE字段是一個引用,指向對應的基本數據類型的Class對象,如下所示∶

基本類型 包裝類型
boolean.class Boolean.TYPE
char.class Charactor.TYPE
byte.class Byte.TYPE
short.class Short.TYPE
int.class Integer.TYPE
long.class Long.TYPE
float.class Float.TYPE
double.class Double.TYPE
void.class Void.TYPE

書中建議使用".class"的形式,以保持與普通類的一致性。

注意,有一點很有趣,當使用".class"來創建對Class對象的引用時,不會自動地初始化該Class對象。爲了使用類而做的準備工作實際包含三個步驟∶

1. 加載,這是由類加載器執行的。該步驟將查找字節碼(通常在classpath所指定的路徑中查找,但這並非是必需的),並從這些字節碼中創建一個Class對象。

2. 鏈接。在鏈接階段將驗證類中的字節碼,爲靜態域分配存儲空間,並且如果必需的話,將解析這個類創建的對其他類的所有引用。

3. 初始化。如果該類具有超類,則對其初始化,執行靜態初始化器和靜態初始化塊。初始化被延遲到了對靜態方法(構造器隱式地是靜態的)或者非常數靜態域進行首次引用時才執行∶

class Initable {
    // 編譯期常量,Initable不需要被初始化就可以讀取
    static final int staticFinal = 47;
    static final int staticFinal2 = ClassInitialization.rand.nextInt(1000);
    static {
        System.out.println("Initializing Initable");
    }
}

class Initable2 {
    static int staticNonFinal = 147;
    static {
        System.out.println("Initializing Initable2");
    }
}

class Initable3 {
    static int staticNonFinal = 74;
    static {
        System.out.println("Initializing Initable3");
    }
}

public class ClassInitialization {
    public static Random rand = new Random(47);

    public static void main(String[] args) throws Exception {
        Class initable = Initable.class;
        System.out.println("After creating Initable ref");

        // 編譯期常量,這個值不需要對Initable類初始化就可以被讀取
        System.out.println(Initable.staticFinal);

        // 對Initable.staticFinal2的訪問將強制進行類得初始化,因爲他不是一個編譯器常量
        System.out.println(Initable.staticFinal2);

        // 如果一個static域不是final的,那麼在對它訪問時,總是要求被讀取之前,要先進行鏈接(爲這個域分配存儲空間)和初始化(初始化該存儲空間)
        System.out.println(Initable2.staticNonFinal);

        // Class.forName()立即就進行了初始化
        Class initable3 = Class.forName("typeinfo.Initable3");
        System.out.println("After creating Initable3 ref");
        System.out.println(Initable3.staticNonFinal);
    }
}/* output:
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
*/

類型轉換前先做檢查

​ 關於關鍵字instanceof,它返回一個布爾值,告訴我們對象是不是某個特定類型的實例,可以用提問的方式使用它,就像這樣 :

if (x instanceof Dog) {
    ((Dog)x).bark();
}

​ instanceof侷限性:只可將其與命名類型進行比較,而不能與Class對象作比較。Class.isInstance()方法提供了一種動態地測試對象的途徑。我們可以創建出一個Class對象的數組,然後將目標對象與這數組中的對象進行逐一比較來代替一大堆的instanceof表達式。

instanceOf與Class的等價性

​ 在查詢類型信息時,以instanceof的形式(即以instanceof的形式或isInstance()的形式,它們產生相同的結果)與直接比較Class對象有一個很重要的差別。下面的例子展示了這種差別∶

class Base {}
class Derived extends Base {}

public class FamilyVsExactType {
    static void test(Object x) {
        System.out.println("Testing x of type " + x.getClass());
        System.out.println("x instanceof Base " + (x instanceof Base));
        System.out.println("x instanceof Derived "+ (x instanceof Derived));
        System.out.println("Base.isInstance(x) "+ Base.class.isInstance(x));
        System.out.println("Derived.isInstance(x) " +
                Derived.class.isInstance(x));
        System.out.println("x.getClass() == Base.class " +
                (x.getClass() == Base.class));
        System.out.println("x.getClass() == Derived.class " +
                (x.getClass() == Derived.class));
        System.out.println("x.getClass().equals(Base.class)) "+
                (x.getClass().equals(Base.class)));
        System.out.println("x.getClass().equals(Derived.class)) " +
                (x.getClass().equals(Derived.class)));
    }

    public static void main(String[] args) {
        test(new Base());
        test(new Derived());
    }
}/* output:
Testing x of type class typeinfo.Base
x instanceof Base true
x instanceof Derived false
Base.isInstance(x) true
Derived.isInstance(x) false
x.getClass() == Base.class true
x.getClass() == Derived.class false
x.getClass().equals(Base.class)) true
x.getClass().equals(Derived.class)) false
Testing x of type class typeinfo.Derived
x instanceof Base true
x instanceof Derived true
Base.isInstance(x) true
Derived.isInstance(x) true
x.getClass() == Base.class false
x.getClass() == Derived.class true
x.getClass().equals(Base.class)) false
x.getClass().equals(Derived.class)) true
*/

​ test()方法使用了兩種形式的instanceof作爲參數來執行類型檢查。然後獲取Class引用,並用和equals()來檢查Class對象是否相等。使人放心的是,instancof和isInstance()生成的結果完全一樣,equals()和也一樣。但是這兩組測試得出的結論卻不相同。instanceof保持了類型的概念,它指的是"你是這個類嗎,或者你是這個類的派生類嗎?"而如果用==比較實際的Class
對象,就沒有考慮繼承—它或者是這個確切的類型,或者不是。

反射:運行時的類信息

​ 如果不知道某個對象的確切類型,RTTI可以告訴你。但是有一個限制;這個類型在編譯時必須已知,這樣才能使用RTTI識別它,並利用這些信息做一些有用的事。換句話說,在編譯時,編譯器必須知道所有要通過RTTI來處理的類。

​ 當通過反射與一個未知類型的對象打交道時,JVM只是簡單地檢查這個對象,看它屬於哪個特定的類(就像RTTI那樣)。在用它做其他事情之前必須先加載那個類的Class對象。因此,那個類的.class文件對於JVM來說必須是可獲取的∶要麼在本地機器上,要麼可以通過網絡取得。所以RTTI和反射之間真正的區別只在於,對RTTI來說,編譯器在編譯時打開和檢查.class文件。(換句話說,我們可以用"普通"方式調用對象的所有方法。)而對於反射機制來說,.class文件在編譯時是不可獲取的,所以是在運行時
打開和檢查.class文件。

​ 通常你不需要直接使用反射工具,但是它們在你需要創建更加動態的代碼時會很有用。反射在Java中是用來支持其他特性的,例如對象序列化和JavaBean(它們在本書稍後部分都會提到)。但是,如果能動態地提取某個類的信息有的時候還是很有用的。請考慮類方法提取器,代碼如下:

public class ShowMethods {
    private static String usage =
            "usage:\n" +
                    "ShowMethods qualified.class.name\n" +
                    "To show all methods in class or:\n" +
                    "ShowMethods qualified.class.name word\n" +
                    "To search for methods involving 'word'";
    private static Pattern p = Pattern.compile("\\w+\\.");
    public static void main(String[] args) {
        args = new String[]{"typeinfo.ShowMethods"};
        if(args.length < 1) {
            System.out.println(usage);
            System.exit(0);
        }
        int lines = 0;
        try {
            Class<?> c = Class.forName(args[0]);
            Method[] methods = c.getMethods();
            Constructor[] ctors = c.getConstructors();
            if(args.length == 1) {
                for(Method method : methods)
                    System.out.println(
                            p.matcher(method.toString()).replaceAll(""));
                for(Constructor ctor : ctors)
                    System.out.println(p.matcher(ctor.toString()).replaceAll(""));
                lines = methods.length + ctors.length;
            } else {
                for(Method method : methods)
                    if(method.toString().indexOf(args[1]) != -1) {
                        System.out.println(
                                p.matcher(method.toString()).replaceAll(""));
                        lines++;
                    }
                for(Constructor ctor : ctors)
                    if(ctor.toString().indexOf(args[1]) != -1) {
                        System.out.println(p.matcher(
                                ctor.toString()).replaceAll(""));
                        lines++;
                    }
            }
        } catch(ClassNotFoundException e) {
            System.out.println("No such class: " + e);
        }
    }
}/** output:
 public static void main(String[])
 public final void wait() throws InterruptedException
 public final void wait(long,int) throws InterruptedException
 public final native void wait(long) throws InterruptedException
 public boolean equals(Object)
 public String toString()
 public native int hashCode()
 public final native Class getClass()
 public final native void notify()
 public final native void notifyAll()
 public ShowMethods()
 */

​ Class的getMethods()和getConstructors()方法分別返回Method對象的數組和Constructor 對象的數組。這兩個類都提供了深層方法,用以解析其對象所代表的方法,並獲取其名字、輸入參數以及返回值。但也可以像這裏一樣,只使用toString(生成一個含有完整的方法特徵簽名的字符串。代碼其他部分用於提取命令行信息,判斷某個特定的特徵簽名是否與我們的目標字符串相符(使用indexOf(O)),並使用正則表達式去掉了命名修飾詞。

​ 
Class.forName()生成的結果在編譯時是不可知的,因此所有的方法特徵簽名信息都是在執行時被提取出來的。如果研究一下JDK文檔中關於反射的部分,就會看到,反射機制提供了足夠的支持,使得能夠創建一個在編譯時完全未知的對象,並調用此對象的方法。

動態代理

​ 代理是基本的設計模式之一,它是你爲了提供額外的或不同的操作,而插入的用來代替"實際"對象的對象。這些操作通常涉及與"實際"對象的通信,因此代理通常充當着中間人的角色。下面是一個用來展示代理結構的簡單示例∶

interface Interface {
    void doSomething();
    void somethingElse(String arg);
}

class RealObject implements Interface {
    public void doSomething() {
        System.out.println("doSomething"); }

    public void somethingElse(String arg) {
        System.out.println("somethingElse " + arg);
    }
}

class SimpleProxy implements Interface {
    private Interface proxied;
    public SimpleProxy(Interface proxied) {
        this.proxied = proxied;
    }
    public void doSomething() {
        System.out.println("SimpleProxy doSomething");
        proxied.doSomething();
    }
    public void somethingElse(String arg) {
        System.out.println("SimpleProxy somethingElse " + arg);
        proxied.somethingElse(arg);
    }
}


public class SimpleProxyDemo {
    public static void consumer(Interface iface) {
        iface.doSomething();
        iface.somethingElse("bonobo");
    }

    public static void main(String[] args) {
        consumer(new RealObject());
        consumer(new SimpleProxy(new RealObject()));
    }
}/** output:
 doSomething
 somethingElse bonobo
 SimpleProxy doSomething
 doSomething
 SimpleProxy somethingElse bonobo
 somethingElse bonobo
 */

​ 因爲consumer()接受的Interface,所以它無法知道正在獲得的到底是RealObject還是SimpleProxy,因爲這二者都實現了Interface。但是SimpleProxy已經被插入到了客戶端和RealObject之間,因此它會執行操作,然後調用RealObject上相同的方法。

​ 在任何時刻,只要你想要將額外的操作從"實際"對象中分離到不同的地方,特別是當你希望能夠很容易地做出修改,從沒有使用額外操作轉爲使用這些操作,或者反過來時,代理就顯得很有用(設計模式的關鍵就是封裝修改——因此你需要修改事務以證明這種模式的正確性)。例如,如果你希望跟蹤對RealObject中的方法的調用,或者希望度量這些調用的開銷,那麼你應該怎樣做呢?這些代碼肯定是你不希望將其合併到應用中的代碼,因此代理使得你可以很容易地添加或移除它們。

​ 
Java的動態代理比代理的思想更向前邁進了一步,因爲它可以動態地創建代理並動態地處理對所代理方法的調用。在動態代理上所做的所有調用都會被重定向到單一的調用處理器上,它的工作是揭示調用的類型並確定相應的對策。下面是用動態代理重寫的SimpleProxyDemojava∶

class DynamicProxyHandler implements InvocationHandler {

    private Object proxied;

    public DynamicProxyHandler(Object proxied) {
        this.proxied = proxied;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object invoke = method.invoke(proxied, args);
        System.out.println("**** proxy: " + proxy.getClass() +
                ", method: " + method + ", args: " + args);
        if(args != null)
            for(Object arg : args)
                System.out.println("  " + arg);
        return invoke;
    }
}

public class SimpleDynamicProxy {

    public static void consumer(Interface iface) {
        iface.doSomething();
        iface.somethingElse("bonobo");
    }

    public static void main(String[] args) {
        RealObject real = new RealObject();
        consumer(real);
        // Insert a proxy and call again:
        Interface proxy = (Interface) Proxy.newProxyInstance( // 通過調用靜態方法Proxy.newProxyInstance()可以創建動態代理
                Interface.class.getClassLoader(), // 需要一個類加載器參數
                new Class[]{ Interface.class }, // 需要一個你希望該代理實現的接口列表(不是類或抽象類)
                new DynamicProxyHandler(real)); // 以及一個InvocationHandler接口的實現
                // 動態代理可以將所有的調用重定向到調用處理器,因此通常會向調用處理器的構造器傳遞一個“實際”對象的引用
                // 從而使得調用處理器在執行其中介任務時,可以將請求轉發
        consumer(proxy);
    }
}/** output:
doSomething
somethingElse bonobo
doSomething
**** proxy: class typeinfo.proxy.$Proxy0, method: public abstract void typeinfo.proxy.Interface.doSomething(), args: null
somethingElse bonobo
**** proxy: class typeinfo.proxy.$Proxy0, method: public abstract void typeinfo.proxy.Interface.somethingElse(java.lang.String), args: [Ljava.lang.Object;@2503dbd3
  bonobo
  */

​ 通過調用靜態方法Proxy.newProxyInstance()可以創建動態代理,這個方法需要得到一個類加載器(你通常可以從已經被加載的對象中獲取其類加載器,然後傳遞給它),一個你希望該代理實現的接口列表(不是類或抽象類),以及InvocationHandler接口的一個實現。動態代理可以將所有調用重定向到調用處理器,因此通常會向調用處理器的構造器傳遞給一個"實際"對象的引用,從而使得調用處理器在執行其中介任務時,可以將請求轉發。

​ invoke()方法中傳遞進來了代理對象,以防你需要區分請求的來源,但是在許多情況下,你並不關心這一點。然而,在invoke())內部,在代理上調用方法時需要格外當心,因爲對接口的調用將被重定向爲對代理的調用。
通常,你會執行被代理的操作,然後使用Method.invoke())將請求轉發給被代理對象,並傳入必需的參數。

第十五章:泛型

我們希望達到的目的是編寫更通用的代碼,要使代碼能夠應用於“某種不具體的類型”,而不是一個具體的接口或類。

簡單泛型

​ 有許多原因促進了泛型的出現,而最引人注目的一個原因,就是爲了創建容器類。有些情況下,我們確實希望容器能夠同時持有多種類型的對象。但是,通常而言,我們只會使用容器類來存儲一種類型的對象。泛型的主要目的之一就是用來指定容器要持有什麼類型的對象,而且由編譯器來保證類型的正確性。

一個元組庫類

​ 僅一次方法調用就能返回多個對象,你應該經常需要這樣的功能吧。可是return語句只允許返回單個對象,因此,解決辦法就是創建一個對象,用它來持有想要返回的多個對象。當然,可以在每次需要的時候,專門創建一個類來完成這樣的工作。可是有了泛型,我們就能夠一次性地解決該問題,以後再也不用在這個問題上浪費時間了。同時,我們在編譯期就能確保類型安全。

​ 
這個概念稱爲元組(tuple),它是將一組對象直接打包存儲於其中的一個單一對象。這個容器對象允許讀取其中元素,但是不允許向其中存放新的對象。(這個概念也稱爲數據傳送對象,或信使。)

​ 
通常,元組可以具有任意長度,同時,元組中的對象可以是任意不同的類型。不過,我們希望能夠爲每一個對象指明其類型,並且從容器中讀取出來時,能夠得到正確的類型。要處理不同長度的問題,我們需要創建多個不同的元組。下面的程序是一個2維元組,它能夠持有兩個對象∶

public class TwoTuple<A, B> {
    public final A first;

    public final B second;

    public TwoTuple(A a, B b) {
        first = a;
        second = b;
    }
    // 注意:元組隱含地保持了其中元素的次序
    public String toString() {
        return "(" + first + ", " + second + ")";
    }
}

​ 構造器捕獲了要存儲的對象,而toString()是一個遍歷函數,用來顯示列表中的值。注意:元組隱含地保持了其中元素的次序。

我們可以利用繼承機制實現長度更長的元組,從下面的例子中可以看到,增加類型參數是件很簡單的事情:

/**
 * 三維元組
 */
public class ThreeTuple<A, B, C> extends TwoTuple<A, B> {

    public final C third;

    public ThreeTuple(A a, B b, C third) {
        super(a, b);
        this.third = third;
    }

    @Override
    public String toString() {
        return "(" + first + ", " + second + ", " + third + ")";
    }
}

/**
 * 四維元組
 */
public class FourTuple<A,B,C,D> extends ThreeTuple<A,B,C> {
    public final D fourth;
    public FourTuple(A a, B b, C c, D d) {
        super(a, b, c);
        fourth = d;
    }
    public String toString() {
        return "(" + first + ", " + second + ", " +
                third + ", " + fourth + ")";
    }
}

/**
 * 五維元組
 */
public class FiveTuple<A,B,C,D,E> extends FourTuple<A,B,C,D> {
    public final E fifth;
    public FiveTuple(A a, B b, C c, D d, E e) {
        super(a, b, c, d);
        fifth = e;
    }
    public String toString() {
        return "(" + first + ", " + second + ", " +
                third + ", " + fourth + ", " + fifth + ")";
    }
}

​ 爲了使用元組,你只需要定義一個長度適合的元組,將其作爲方法的返回,然後在return語句中創建該元組,並返回即可。

class Amphibian {}
class Vehicle {}

public class TupleTest {
    static TwoTuple<String,Integer> f() {
        // Autoboxing converts the int to Integer:
        return new TwoTuple<>("hi", 47);
    }

    static ThreeTuple<Amphibian,String,Integer> g() {
        return new ThreeTuple<>(new Amphibian(), "hi", 47);
    }

    static FourTuple<Vehicle,Amphibian,String,Integer> h() {
        return new FourTuple<>(new Vehicle(), new Amphibian(), "hi", 47);
    }

    static FiveTuple<Vehicle,Amphibian,String,Integer,Double> k() {
        return new FiveTuple<>(new Vehicle(), new Amphibian(), "hi", 47, 11.1);
    }

    public static void main(String[] args) {
        TwoTuple<String,Integer> ttsi = f();
        System.out.println(ttsi);
        // ttsi.first = "there"; // Compile error: final
        System.out.println(g());
        System.out.println(h());
        System.out.println(k());
    }
}

一個堆棧類

public class LinkedStack<T> {
    private static class Node<U> {
        U item;
        Node<U> next;
        Node() { item = null; next = null; }
        Node(U item, Node<U> next) {
            this.item = item;
            this.next = next;
        }
        boolean end() { return item == null && next == null; }
    }

    // 末端哨兵
    private Node<T> top = new Node<T>();

    public void push(T item) {
        top = new Node<T>(item, top);
    }

    public T pop() {
        T result = top.item;
        if(!top.end())
            top = top.next;
        return result;
    }

    public static void main(String[] args) {
        LinkedStack<String> lss = new LinkedStack<>();
        for(String s : "Phasers on stun!".split(" "))
            lss.push(s);
        String s;
        while((s = lss.pop()) != null)
            System.out.println(s);
    }
}

​ 內部類Node也是一個泛型,它擁有自己的類型參數。

​ 這個例子使用了一個末端哨兵(end sentinel)來判斷堆棧何時爲空。這個末端哨兵是在構造LinkedStack時創建的。然後,每調用一次push()方法,就會創建一個Node<T>對象,並將其鏈接到前一個Node<T>對象。當你調用pop()方法時,總是返回top.item,然後丟棄當前top所指的Node<T>,並將top轉移到下一個Node<T>,除非你已經碰到了未端哨兵,這時候就不再移動top了。如果已經到了末端,客戶端程序還繼續調用pop()方法,它只能得到null,說明堆棧已經空了。

泛型方法

​ 到目前爲止,我們看到的泛型,都是應用於整個類上。但同樣可以在類中包含參數化方法,而這個方法所在的類可以是泛型類,也可以不是泛型類。 也就是說,是否擁有泛型方法,與其所在的類是否是泛型沒有關係。

​ 泛型方法使得該方法能夠獨立於類而產生變化。以下是一個基本的指導原則:無論何時,只要你能做到,你就應該儘量使用泛型方法。也就是說,如果使用泛型方法可以取代將整個類泛型化,那麼就應該只使用泛型方法,因爲它可以使事情更清楚明白。另外,對於一個static的方法而言,無法訪問泛型類的類型參數,所以,如果static方法需要使用泛型能力,就必須使其成爲泛型方法。

​ 要定義泛型方法,只需將泛型參數列表置於返回值之前,就像下面這樣:

public class GenericMethods {

    // 泛型方法
    // <T> 叫做類型參數
    public <T> void f(T t) {
        System.out.println(t.getClass().getSimpleName());
    }

    public static void main(String[] args) {
        GenericMethods gen = new GenericMethods();
        gen.f("");
        gen.f(1);
        gen.f(1.0);
        gen.f(1.0F);
        gen.f('c');
        gen.f(gen);
    }

}

可變參數與泛型方法

泛型方法與可變參數列表能夠很好地共存

public class GenericVarargs {
    public static <T> List<T> makeList(T... args) {
        List<T> result = new ArrayList<>();
        for (T arg : args) {
            result.add(arg);
        }
        return result;
    }

    public static void main(String[] args) {
        List<String> list = makeList("A");
        System.out.println(list);
        list = makeList("A", "B", "C");
        System.out.println(list);
        list = makeList("ABCDEFGHIGKLMNOPQRSTUVWXYZ".split(""));
        System.out.println(list);
    }
}

// makeList()展示了與標準類型中java.util.Arrays.asList()方法相同的功能

一個Set實用工具

public class Sets {
    /**
     * 將兩個參數合併在一起,即並集
     */
    public static <T> Set<T> union(Set<T> a, Set<T> b) {
        Set<T> result = new HashSet<T>(a);
        result.addAll(b);
        return result;
    }

    /**
     * @return 返回兩個參數共有的部分,即取交集
     */
    public static <T> Set<T> intersection(Set<T> a, Set<T> b) {
        Set<T> result = new HashSet<T>(a);
        result.retainAll(b);
        return result;
    }

    /**
     * 從superset中移除subset包含的元素,既取差集
     * @return
     */
    public static <T> Set<T> difference(Set<T> superset, Set<T> subset) {
        Set<T> result = new HashSet<T>(superset);
        result.removeAll(subset);
        return result;
    }

    /**
     *
     * @return 返回除了交集之外的所有元素
     */
    public static <T> Set<T> complement(Set<T> a, Set<T> b) {
        return difference(union(a, b), intersection(a, b));
    }

}


​ 在前三個方法中,都將第一個參數Set複製了一份,將Set中的所有引用都存入一個新的 HeshSet對象中,因此,我們並未直接修改參數中的Set。返回的值是一個全新的Set對象。這四個方法表達瞭如下的數學集合操:union()返回一個Set,它將兩個參數合併在一起, intersection()返回的Set只包含兩個參數共有的分;difference()方法從superset中移除subset包的元素;complement()返回的Set包含除了交集之外的所有元素。

擦除的神祕之處

由於Java泛型是通過擦除來實現的,這意味着當你在使用泛型時,任何具體類型都被擦除了,你唯一知道的就是你在使用一個對象。因此,List和List在運行時事實上都是相同的類型。這兩種形式都被擦除成它們的“原生”類型,即List。殘酷的現實是,在泛型代碼內部,無法獲得任何有關泛型參數類型的信息。

邊界:

邊界使得你可以在用於泛型的參數類型上設置限制條件,儘管這使得你可以強制規定泛型可以應用的類型,但是其潛在的一個更重要的效果是你可以按照自己的邊界類型來調用方法。

因爲擦除移除了類型信息,所以可以用無界泛型參數調用的方只是那些可以用Object調用的方法。但是,如果能夠將這個參數限制爲某個類型子集,那麼你就可以用這些類型子集來調用方法。爲了執行這種限制,Java泛型 重用了extends關鍵字。如<T extends HasColor>,我們就說泛型類型參數將它擦除到它的第一個邊界(它可能y有多個邊界),編譯器實際上會把類型參數替換爲它的擦除,就好像用HasColor替換掉T一樣。

擦除的問題

擦出的代價是顯著的,泛型不能用於顯示地引用運行時類型的操作之中,例如轉型、instanceof操作和new表達式。由於擦除丟失了在泛型代碼中執行某些操作的能力,任何在運行時需要知道確認類型信息的操作都將無法工作。

public class Erased<T> {
    private final int SIZE = 100;
    public static void f(Object arg) {
        if (arg instanceof T) {} // ERROR
        T var = new T(); // EROOR
        T[] array = new T[SIZE]; // Error
    }
}

在上述代碼中,new T()無法實現,部分原因是因爲擦除,而另一部分原因是因爲編譯器不能驗證T具有無參構造器。

關於泛型問題就寫到這裏了,書中用了大量的篇幅來講述泛型擦除的問題,有興趣的小夥伴建議閱讀原書,由於這塊的內容寫起來也晦澀難懂,需要多寫測試代碼去運行看效果,這裏就不再用文字贅述了。

第十六章:數組

Java中已經有了容器,爲什麼還需要數組呢,是因爲數組可以持有基本類型嗎?但是在泛型出來之後,通過自動包裝機制,其實通過容器也能夠持有基本類型。在Jav中,數組是一種效率最高的存儲和隨機訪問對象引用序列的方式。數組就是一個簡單的線性用,這使得元素訪問非常快速。但是爲這種速度所付出的代價是數組對象的大小被固定,並其生命週期中不可改變。

在java.util類庫中可以找到Arrays類,它有一套用於數組的static實用方法,其中有六個基本方法,equals()用於比較兩個數組是否相等(deepEquals()用於多位數組),fill()爲數組填充數據,sort()用於對數組的排序;binarySearch()用於在已排序的數組中查找元素。

Java標準類庫提供有static方法System.arrayCopy(),用它賦值數組比for循環賦值要快很多,System.arrayCopy()針對所有類型做了重載。arrayCopy()需要的參數有:源數組,表示從源數組中的什麼位置開始賦值的偏移量,表示從目標數組的什麼位置開始複製的偏移量,以及需要賦值的元素的個數。如果是複製對象數組,這裏做得是淺複製。而且System.arrayCopy()不會執行自動拆包和自動裝包,所以兩個數組必須具有相同的確切類型。

總結

  目前進度也只看到了第十六章,後面會持續更新,目前的打算是想把整本書通讀完後,再對書中使用到設計模式的代碼都再敲一次,並上傳到我的碼雲。本書後面的內容都比較重要,分別是容器的深入研究、Java I/O系統,註解、併發。最後一章是圖形化用戶界面,這一章我也不打算看,主要是講的Swing組件,因爲這章的內容對我來說幾乎沒什麼接觸,所以就暫時略過吧。

第一章到第十章的內容:《Java編程思想》讀書筆記一

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章