?
? ???????
========
Chap11??? 對象的集合
?
?????? 1.數組
?????? 數組與其它容器的區別體現在三個方面:效率,類型識別,以及可以持有primitives。
?????? 數組是Java提供的,能隨機存儲和訪問reference序列的諸多方法中,最高效的一種。速度的代價是,當一個數組創建后,容量就固定了。
?????? 創建數組的時候,同時指明了數組元素的類型。而泛型容器類如List,Set和Map等,所持有的對象均被上傳為Object。
??????
?????? 2.數組是第一流的對象
?????? 數組的標識符實際上是一個“創建在堆(heap)里的實實在在的對象的”reference。這個對象持有其它對象的reference,或直接持有primitive類型的值。
??????
?????? 3.Arrays類
?????? java.util.Arrays類,包括了一組可用于數組的static方法。其中asList()方法,可把數組轉成一個List。
?????? Arrays.fill()方法,把一個值或對象的reference拷貝到數組的各個位置,或指定的范圍。
??????
?????? 4.復制一個數組
?????? 相比for循環,System.arrayCopy()能以更快的速度拷貝數組。如果是對象數組,拷貝的是數組中對象的reference,對象本身不會被拷貝。這被稱為淺拷貝(Shallow copy)。
??????
?????? 5.數組的比較
?????? Arrays提供了equals()方法。數組是否相等是基于其內容的。
?????? 數組要想完全相等,它們必須有相同數量的元素,且數組的每個元素必須與另一個數組的對應位置上的元素相等。
?????? 元素的相等性,用equals()判斷。對于primitive,會使用其wrapper類的equals()。
??????
?????? 6.數組元素的比較
?????? 實現比較功能的一個方法是實現java.lang.Comparable接口。這個接口只有一個compareTo()方法。
?????? Arrays.sort()會把傳給它的數組的元素轉換成Comparable。如果數組元素沒有實現Comparable接口,就會引發一個ClassCastException。
?????? 實現比較功能的另一個方法使用策略模式(strategy design pattern),即實現Comparator接口。
?????? Arrays.sort()可接受一個數組和一個Comparator,根據Comparator的compare()方法對數組元素排序。
?????? Java標準類庫所用的排序算法已經作了優化--對于primitive,它用的是快速排序(Quicksort),對于對象,它用的是穩定合并排序(stable merge sort)。
??????
?????? 7.查詢有序數組
?????? 一旦對數組進行了排序,就能用Arrays.binarySearch()進行快速查詢了。但切忌對一個尚未排序的數組使用binarySearch()。
?????? 如果Arrays.binarySearch()查找到了,就返回一個大于或等于0的值。否則返回負值。這個負值的意思是,如果手動維護這個數組,這個值應該插在哪個位置。這個值是:
?????? -(插入點)-1
?????? “插入點”就是,在所有比要找的值更大的值中,最小的那個值的下標。如果數組中所有值都比要查找的值小,它就是a.size()。
?????? 如果數組里有重復元素,binarySearch()不能保證返回哪一個,但也不報錯。
?????? 如果排序的時候用到了Comparator,那么調用binarySearch()的時候,也必須使用同一個Comparator。
??????
?????? 8.數組部分的總結
?????? 如果要持有一組對象,首選,同時效率最高的,應該是數組。如果是要持有一組primitive,也只能用數組。
??????
?????? 9.容器簡介
?????? Java2的容器類要解決“怎樣持有對象”,它把這個問題分成兩類:
?????? (1).Collection:通常是一組有一定規律的獨立元素。List必須按特定的順序持有這些元素,而Set不能保存重復的元素。
?????? (2).Map:一組以“鍵-值”(key-value)形式出現的pair。Map可以返回鍵(Key)的Set,值的Collection,或者pair的Set。
??????
?????? 10.填充容器
?????? Collection也有一個輔助類Collections,它包含了一些靜態的使用工具方法,其中有fill()。fill()只是把同一個對象的reference負值到整個容器,而且只能為List,不能為Set和Map工作。并且這個fill()只能替換容器中的值,而不是往List加新元素。如:
?????? List list = new ArrayList();
?????? for(int i = 0; i<10; i++)
????????????? list.add("");
?????? Collections.fill(list, "Hello");
??????
?????? 11.容器的缺點:不知道對象的類型
?????? Java的容器只持有Object。容器對“能往里面加什么類型的對象”沒有限制。在使用容器中的對象之前,還必須進行類型轉換
??????
?????? 12.迭代器
?????? 迭代器(iterator),又是一個設計模式。iterator能讓程序員在不知道或不關心他所處理的是什么樣的底層序列結構的情況下,在一個對象序列中前后移動,并選取其中的對象。iterator是“輕量級”的對象,即創建代價很小的對象。
?????? 不經意的遞歸(Unintended recursion)
?????? public class A{
????????????? public String toString(){
???????????????????? return "A address:" + this +"\n";//
????????????? }
????????????? public static void main(String[] args){
???????????????????? System.out.println(new A());
????????????? }
?????? }
?????? 上面的程序會出現無窮無盡的異常。
?????? "A address:" + this???????? ,編譯器會試著將this轉換成String,要用大toString(),于是就變成遞歸調用了。
?????? 如果想打印對象的地址,應該調用Object的toString()方法。而不要用this,應該寫super.toString()。
??????
?????? 13.List的功能
?????? ArrayList,一個用數組實現的List。能進行快速的隨機訪問,但是往列表中插入和刪除元素比較慢。
?????? LinkedList,對順序訪問進行了優化。在List中插入和刪除元素代價也不高。但是隨機訪問的速度相對較慢。可以把它當成棧(Stack),隊列(queue)或雙向隊列(deque)來用。
??????
?????? 14.Set的功能
?????? 加入Set的每個元素必須是唯一的。要想加進Set,Object必須定義equals(),才能標明對象的唯一性。
?????? HashSet,為優化查詢速度而設計的Set。要放進HashSet的Object還要定義hashCode()。
?????? TreeSet,一個有序的Set,能從中提取一個有序序列。用了紅黑樹(red-black tree)數據結構。
?????? LinkedHashSet,使用鏈表的Set,既有HashSet的查詢速度,又能保存元素的插入順序。用Iterator遍歷Set的時候,它是按插入順序進行訪問的。
?????? Set要有一個判斷以什么順序來存儲元素的標準,也就是說必須實現Comparable接口,并且定義compareTo()方法。
??????
?????? 15.SortedSet
?????? SortedSet(只有TreeSet這一個實現可用)中的元素一定是有序的。SortedSet的意思是“根據對象的比較順序”,而不是“插入順序”進行排序。
??????
?????? 16.Map的功能
?????? 如果知道get()是怎么工作的,就會發覺在ArrayList里面找對象是相當慢的。而這正是HashMap的強項。HashMap利用對象的hashCode()來進行快速查找。
?????? Map的keySet()方法返回一個由Map的鍵組成的Set。values()返回的是由Map的值所組成的Collection。由于這些Collection的后臺都是map,因此對這些Collection的任何修改都會反映到Map上。
??????
?????? 17.SortedMap
?????? SortedMap(只有TreeMap這一個實現)的鍵肯定是有序的。
??????
?????? 18.LinkedHashMap
?????? 為提高速度,LinkedHashMap對所有東西都作了hash,而且遍歷的時候,還會按插入順序返回pair。此外,還可通過構造函數進行配置,讓它使用基于訪問的LRU(least-recently-used)算法,這樣沒被訪問過的元素(通常也是要刪除的候選對象)就會出現在隊列的最前面。
??????
?????? 19.散列算法與Hash數
?????? 要想用自己的類作HashMap的鍵,必須覆寫equals()和hashCode()。HashMap用equals()來判斷查詢用的鍵是否與表里其它鍵相等。
?????? Object的hashCode(),在缺省情況下就是返回對象的內存地址。
??????
?????? 一個合適的equals()必須做到以下五點:
?????? (1).反身性:對任何x,x.equals(x)必須是true。
?????? (2).對稱性:對任何x和y,如果y.equals(x)是true的,那么x.equals(y)也必須是true。
?????? (3).傳遞性:對任何x,y和z,如果x.equals(y)是true,且y.equals(z)也是true,那么x.equals(z)也必須是true。
?????? (4).一致性:對任何x和y,如果對象里面用來判斷相等性的信息沒有修改過,那么無論調用多少次x.equals(y),它都必須一致地返回true或false。
?????? (5).對于任何非空的x,x.equals(null)必須返回false。
??????
?????? 默認的Object.equals()只是簡單地比較兩個對象的地址,所以一個Dog("A")會不等于另一個Dog("A")。
?????? 下面是覆寫equals()和hashCode()的例子。
?????? public class Dog{
????????????? public int id;
????????????? public Dog(int x){ id = x; }
????????????? public int hashCode(){ return id; }
????????????? public boolean equals(Object o){
???????????????????? return (o instanceof Dog) && (id == ((Dog)o).id)
????????????? }
?????? }
?????? equals()在利用instanceof檢查參數是不是Dog類型的同時,還檢查了對象是不是null,如果是null,instanceof會返回false。
??????
?????? 20.理解hashCode()
?????? 數組是最快的數據結構,所以很容易想到用數組存儲Map的鍵的信息(而不是鍵本身)。Map要能存儲任意數量的pair,而鍵的數量又被數組的固定大小限制了,所以不能用數組存儲鍵本身。
?????? 要解決定長數組的問題,就得允許多個鍵生成同一個hash數,也就是會有沖突,每個鍵對象都會對應數組的某個位置。
?????? 查找過程從計算hash數開始,算完后用這個數在數組里定位。如果散列函數能確保不產生沖突(如果對象數量是固定的,這是可能的),那么它就被稱為“完全散列函數”,這是特例。通常,沖突是由“外部鏈(external chaining)”處理的:數組并不直接指向對象,而是指向一個對象的列表。然后再用equals()在這個列表中一個個找。如果散列函數定義得好,每個hash數只對應很少的對象,這樣,與搜索整個序列相比,能很快跳到這個子序列,比較少量對象,會快許多。
?????? hash表的“槽位”常被稱為bucket。
??????
?????? 21.影響HashMap性能的因素
?????? Capacity:hash表里bucket的數量。
?????? Initial capacity:創建hash表時,bucket的數量。
?????? Size:當前hash表的記錄的數量。
?????? Load factor:size/capacity。一個負載較輕的表會有較少的沖突,因此插入和查找的速度會比較快,但在用迭代器遍歷的時候會比較慢。
?????? HashMap和HashSet都提供了能指定load factor的構造函數,當load factor達到這個閥值的時候,容器會自動將capacity(bucket的數量)增加大約一倍,然后將現有的對象分配到新的bucket里面(這就是所謂的rehash)。缺省情況下HashMap會使用0.75的load factor。
??????
?????? 22.選擇實現
?????? HashTable,Vector和Stack屬于老版本遺留下來的類,應該避免使用。
?????? 如何挑選List
?????? 數組的隨機訪問和順序訪問比任何容器都快。ArrayList的隨機訪問比LinkedList快,奇怪的時LinkedList的順序訪問居然比ArrayList略快。LinkedList的插入和刪除,特別時刪除,比ArrayList快很多。Vector各方面速度都比ArrayList慢,應避免使用。
?????? 如何挑選Set
?????? HashSet各項性能都比TreeSet好,只有在需要有序的Set時,才應該用TreeSet。
?????? LinkedHashSet的插入比HashSet稍慢一些,因為要承擔維護鏈表和hash容器的雙重代價,但是它的遍歷速度比較快。
?????? 如何挑選Map
?????? 首選HashMap,只有在需要有序map時,才選TreeMap。LinkedHashMap比Hashmap稍慢一些。
??????
?????? 23.把Collection和Map設成不可修改的
?????? Collections.unmodifiableCollection()方法,會把傳給它的容器變成只讀版返回。這個方法有四種變形,unmodifiableCollection(),unmodifiableList(),unmodifiableSet(),unmodifiableMap()。
??????
?????? 24.Collection和Map的同步
?????? Collections里有一個自動對容器做同步的方法,它的語法與“unmodifiable”方法有些相似。synchronizedCollection(),synchronizedList(),synchronizedSet(),synchronizedMap()。
??????
?????? 25.Fail fast
?????? Java容器類庫繼承了fail-fast(及早報告錯誤)機制,它能防止多個進程同時修改容器的內容。當它發現有其它進程在修改容器,就會立即返回一個ConcurrentModificationException。
??????
?????? 26.可以不支持的操作
?????? 可以用Arrays.asList()把數組改造成List,但它只是部分的實現了Collection和List接口。它支持的都是那些不改變數組容量的操作,不支持add(),addAll(),clear(),retainAll(),remove(),removeAll()等。調用不支持的方法會引發一個UnsupportedOperationException異常。
?????? 要想創建普通容器,可以把Arrays.asList()的結果做為構造函數參數傳給List或Set,這樣就能使用它的完整接口了。
??????
??????
??????
??????
=============
Chap12?? Java I/O系統
?
?????? 1.File類
?????? File類有一個極具欺騙性的名字,可以用來表示某個文件的名字,也可以用來表示目錄里一組文件的名字。
?????? File類的功能不僅限于顯示文件或目錄。它能創建新的目錄,甚至是目錄路徑。此外還能檢查文件的屬性,判斷File對象表示的是文件還是目錄,以及刪除文件等。
??????
?????? 2.輸入與輸入
?????? 流(Stream)是一種能生成或接受數據的,代表數據的源和目標的對象。流把I/O設備內部的具體操作給隱藏起來了。
?????? Java的I/O類庫分成輸入和輸出兩大部分。
??????
?????? 3.添加屬性與適用的接口
?????? 使用“分層對象(layered objects)”,為單個對象動態地,透明地添加功能的做法,被稱為Decorator Pattern。Decorator模式要求所有包覆在原始對象之外的對象,都必須具有與之完全相同的接口。無論對象是否被decorate過,傳給它的消息總是相同的。
?????? 為InputStream和OutputStream定義decorator類接口的類,分別是FilterInputStream和FilterOutputStream,它們都繼承自I/O類庫的基類InputStream和OutputStream,這是decorator模式的關鍵(惟有這樣,decorator類的接口才能與它要服務的對象的完全相同)。
?????? 對于I/O類庫來說,比較明智的做法是,普遍都做緩沖,把不緩沖當特例。
??????
?????? Reader和Writer類系
?????? InputStream和OutputStream的某些功能已經淘汰,但仍然提供了很多有價值的,面向byte的I/O功能。而Java 1.1引進的Reader和Writer則提供了Unicode兼容的,面向字符的I/O功能。Java 1.1還提供了兩個適配器(adapter)類,InputStreamReader和OutputStreamWriter負載將InputStream和OutputStream轉化成Reader和Writer。
?????? Reader和Writer要解決的,最主要是國際化。原先的I/O類庫只支持8位的字節流,因此不可能很好地處理16位的Unicode字符流。此外新類庫的性能也比舊的好。
??????
?????? 4.數據源和目的
?????? 幾乎所有的Java I/O流都有與之對應的,專門用來處理Unicode的Reader和Writer。但有時,面向byte的InputStream和OutputStream才是正確的選擇,特別是java.util.zip,它的類都是面向byte的。
?????? 明智的做法是,先用Reader和Writer,等到必須要用面向byte的類庫時,你自然會知道,因為程序編譯不過去了。
??????
?????? 5.常見的I/O流的使用方法
?????? (1).對輸入文件做緩沖
?????? BufferedReader in = new BufferedReader( new FileReader("IOStreamDemo.java"));
?????? String s, s2 = new String();
?????? while((s = in.readLine())!= null)
????????????? s2 += s + "\n";//readLine()會把換行符剝掉,所以在這里加上。
?????? in.close();
?????? //讀取標準輸入
?????? BufferedReader stdin = new BufferedReader( new InputStreamReader(System.in));
?????? System.out.print("Enter a line:");
?????? System.out.println(stdin.readLine());
??????
?????? (2).讀取內存
?????? StringReader in2 = new StringReader(s2);
?????? int c;
?????? while((c = in2.read())!=-1)//read()會把讀出來的byte當做int
????????????? System.out.print((char)c);
??????
?????? (3).讀取格式化內存
?????? try{
????????????? DataInputStream in3 = new DataInputStream(new ByteArrayInputStream(s2.getBytes()));
????????????? while(true)
???????????????????? System.out.print((char)in3.readByte());//無法根據readByte()返回值判斷是否結束
?????? } catch(EOFException e){
????????????? System.err.println("End of stream");
?????? }
?????? //使用available()來判斷還有多少字符
?????? DataInputStream in = new DataInputStream(new BufferedInputStream(new FileInputStream("TestEOF.java")));
?????? while(in.available() != 0)
????????????? System.out.print((char)in.readByte());
??????
?????? (4).讀取文件
?????? try{
????????????? BfferedReader in4 = new BufferedReader(new StringReader(s2));
????????????? PrintWriter out1 = new PrintWriter(new BufferedWriter(new FileWriter(IODemo.out)));
????????????? int lineCount = 1;
????????????? while((s = in4.readLine())!= null)
???????????????????? out1.println(lineCount++ +": "+ s);
????????????? out1.close();
?????? } catch(EOFException e){
????????????? System.err.println(End of stream);
?????? }
?????? 使用PrintWriter去排版,就能得出能夠讀得懂的,普通的文本文件。
??????
?????? 6.標準I/O
?????? 標準I/O是Unix的概念,意思是,一個程序只使用一個信息流。所有輸入都是從“標準輸入”進來的,輸出都從“標準輸出”出去,錯誤消息都送到“標準錯誤”里。
?????? Java遵循標準I/O的模型,提供了Syetem.in,System.out,以及System.err。
??????
?????? 將System.out轉換成PrintWriter
?????? System.out是PrintStream,也就是說它是OutputStream。不過可通過PrintWriter的構造函數把它改造成PrintWriter。
?????? PrintWriter out = new PrintWriter(System.out, true);
?????? out.println("Hello, world");
?????? 為了啟動自動清空緩沖區的功能,一定要使用雙參數版的構造函數,并把第二個參數設成true。這點非常重要,否則就有可能會看不到輸出。
??????
?????? 標準I/O的重定向
?????? Java的System類提供了幾個能重定向標準輸入,標準輸出和標準錯誤的靜態方法:
?????? setIn(InputStream),setOut(PrintStream),setErr(PrintStream)。
?????? I/O重定向處理的不是character流,而是byte流,因此不能用Reader和Writer,要用InputStream和OutputStream。
??????
?????? 7.New I/O
?????? Java 1.4的java.nio.*引入了一個新的I/O類庫,其目的就是提高速度。實際上,舊的I/O類庫已經用nio重寫。
?????? 性能的提高源于它用了更貼近操作系統的結構:channel和buffer。
?????? java.nio.ByteBuffer是唯一一個能直接同channel打交道的buffer。它是一個相當底層的類,存儲和提取數據的時候,可以選擇是以byte形式還是以primitive形式,但它不能存儲對象。這是為了有效地映射到絕大多數操作系統上。
?????? 新I/O修改了舊I/O的三個類,即FileInputStream,FileOutputStream,以及RandomAccessFile,以獲取FileChannel。
?????? // Write a file:
??? FileChannel fc = new FileOutputStream("data.txt").getChannel();
??? fc.write(ByteBuffer.wrap("Some text ".getBytes()));
??? fc.close();
??? // Add to the end of the file:
??? fc = new RandomAccessFile("data.txt", "rw").getChannel();
??? fc.position(fc.size()); // Move to the end
??? fc.write(ByteBuffer.wrap("Some more".getBytes()));
??? fc.close();
??? // Read the file:
??? fc = new FileInputStream("data.txt").getChannel();
??? ByteBuffer buff = ByteBuffer.allocate(4096);
??? fc.read(buff);
??? buff.flip();
??? while(buff.hasRemaining())
????? System.out.print((char)buff.get());?
?????? 用wrap( )方法把一個已經拿到手的byte數組"包"到ByteBuffer。如果是用這種方法,新創建的ByteBuffer是不會去拷貝底層的(byte)數組的,相反它直接用那個byte數組來當自己的存儲空間。所以我們說ByteBuffer的"后臺"是數組。
?????? 從buffer中取數據前,要調用buffer的flip()。往buffer中裝數據前,要調用buffer的clear()。
?????? FileChannel
????? in = new FileInputStream(args[0]).getChannel(),
????? out = new FileOutputStream(args[1]).getChannel();
??? ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
??? while(in.read(buffer) != -1) {
????? buffer.flip(); // Prepare for writing
????? out.write(buffer);
????? buffer.clear();? // Prepare for reading
??? }
?
?????? View Buffers
??????
?????? View Buffer能讓你從特殊的視角,來觀察其底層的ByteBuffer。對view的任何操作都會作用到ByteBuffer上。同一個ByteBuffer,能讀出不同的數據。ByteBuffer以1字節區分數據,CharBuffer是2字節,IntBuffer,FloatBuffer是4字節,LongBuffer和DoubleBuffer是8字節。
?????? ByteBuffer bb = ByteBuffer.wrap(new byte[]{ 0, 0, 0, 0, 0, 0, 0, 'a' });
??? bb.rewind();
??? System.out.println("Byte Buffer");
??? while(bb.hasRemaining())
????? System.out.println(bb.position()+ " -> " + bb.get());
??? CharBuffer cb = ((ByteBuffer)bb.rewind()).asCharBuffer();
??? System.out.println("Char Buffer");
??? while(cb.hasRemaining())
????? System.out.println(cb.position()+ " -> " + cb.get());
??? FloatBuffer fb = ((ByteBuffer)bb.rewind()).asFloatBuffer();
??? System.out.println("Float Buffer");
??? while(fb.hasRemaining())
????? System.out.println(fb.position()+ " -> " + fb.get());
??? IntBuffer ib = ((ByteBuffer)bb.rewind()).asIntBuffer();
??? System.out.println("Int Buffer");
??? while(ib.hasRemaining())
????? System.out.println(ib.position()+ " -> " + ib.get());
?????
??? Buffer的細節
??? 如果使用相對定位的get()和put()方法,buffer的position會跟著變化。也可以用下標參數調用絕對定位的get()和put()方法,這時它不會改動buffer的position。
??? mark()方法會記錄當前position,reset()會把position設置到mark的位置。rewind()把position設置到buffer的開頭,mark被擦掉了。flip()把limit設為position,把position設為零。當你將數據寫入buffer,準備讀取的時候,必須先調用這個方法。
???
?????? 內存映射文件
?????? memory-mapped file能讓你創建和修改那些大到無法讀入內存的文件(最大2GB)。
?????? int length = 0x8FFFFFF; // 128 Mb
?????? MappedByteBuffer out = new RandomAccessFile("test.dat","rw").getChannel().map(FileChannel.MapMode.READ_WRITE,0,length);
?????? for(int i = 0; i < length; i++)
????????????? out.put((byte)'x');
?????? for(int i = length/2;i<length/2+6;i++)
????????????? System.out.print((char)out.get(i));
?????? MappedByteBuffer是ByteBuffer的派生類。例程創建了一個128MB的文件,文件的訪問好像只是一瞬間的事,這是因為,真正調入內存的只是其中的一小部分,其余部分則被放在交換文件上。Java是調用操作系統的"文件映射機制(file-mapping facility)"來提升性能的。只有RandomAccessFile才能寫映射文件。
??????
?????? 文件鎖
?????? Java的文件鎖是直接映射操作系統的鎖機制的,因此其它進程也能看到文件鎖。
?????? FileOutputStream fos= new FileOutputStream("file.txt");
??? FileLock fl = fos.getChannel().tryLock();
??? if(fl != null) {
????? System.out.println("Locked File");
????? Thread.sleep(100);
????? fl.release();
????? System.out.println("Released Lock");
??? }
??? fos.close();
??? tryLock( ) 是非阻塞的。它會試著去獲取這個鎖,但是如果得不到(其它進程已經以獨占方式得到這個鎖了),那它就直接返回。而lock( )是阻塞的。如果得不到鎖,它會在一直處于阻塞狀態,除非它得到了鎖,或者你打斷了調用它(即lock( )方法)的線程,或者關閉了它要lock( )的channel,否則它是不會返回的。最后用FileLock.release( )釋放鎖。
??? 還可以像這樣鎖住文件的某一部分,
tryLock(long position, long size, boolean shared)
?????? 或者
lock(long position, long size, boolean shared)
這個方法能鎖住文件的某個區域(size - position)。其中第三個參數表示鎖能不能共享。
?????? 對于帶參數的lock( )和tryLock( )方法,如果你鎖住了position到position+size這段范圍,而文件的長度又增加了,那么position+size后面是不加鎖的。而無參數的lock方法則會鎖定整個文件,不管它變不變長。
?
?????? 8.壓縮
?????? Java I/O類庫還收錄了一些能讀寫壓縮格式流的類,它們是InputStream和OutputStream的派生類。這是因為壓縮算法是針對byte而不是字符的。
?????? GZIP的接口比較簡單,因此如果你只有一個流要壓縮的話,用它會比較合適。
?????? BufferedReader in = new BufferedReader(new FileReader(args[0]));
??? BufferedOutputStream out = new BufferedOutputStream(
????? new GZIPOutputStream(new FileOutputStream("test.gz")));
??? System.out.println("Writing file");
??? int c;
??? while((c = in.read()) != -1)
????? out.write(c);
??? in.close();
??? out.close();
??? System.out.println("Reading file");
??? BufferedReader in2 = new BufferedReader(
????? new InputStreamReader(new GZIPInputStream(new FileInputStream("test.gz"))));
??? String s;
??? while((s = in2.readLine()) != null)
????? System.out.println(s);
?????? 只要用GZIPOutputStream 或ZipOutputStream把輸出流包起來,再用GZIPInputStream 或ZipInputStream把輸入流包起來就行了。
??????
?????? 用Zip存儲多個文件
?????? FileOutputStream f = new FileOutputStream("test.zip");
??? CheckedOutputStream csum = new CheckedOutputStream(f, new Adler32());
??? ZipOutputStream zos = new ZipOutputStream(csum);
??? BufferedOutputStream out = new BufferedOutputStream(zos);
??? zos.setComment("A test of Java Zipping");
??? // No corresponding getComment(), though.
??? for(int i = 0; i < args.length; i++) {
????? System.out.println("Writing file " + args[i]);
????? BufferedReader in = new BufferedReader(new FileReader(args[i]));
????? zos.putNextEntry(new ZipEntry(args[i]));
????? int c;
????? while((c = in.read()) != -1)
??????? out.write(c);
????? in.close();
??? }
??? out.close();
??? // Checksum valid only after the file has been closed!
??? System.out.println("Checksum: " + csum.getChecksum().getValue());
??? // Now extract the files:
??? System.out.println("Reading file");
??? FileInputStream fi = new FileInputStream("test.zip");
??? CheckedInputStream csumi = new CheckedInputStream(fi, new Adler32());
??? ZipInputStream in2 = new ZipInputStream(csumi);
??? BufferedInputStream bis = new BufferedInputStream(in2);
??? ZipEntry ze;
??? while((ze = in2.getNextEntry()) != null) {
????? System.out.println("Reading file " + ze);
????? int x;
????? while((x = bis.read()) != -1)
??????? System.out.write(x);
??? }
??? System.out.println("Checksum: " + csumi.getChecksum().getValue());
??? bis.close();
??? // Alternative way to open and read zip files:
??? ZipFile zf = new ZipFile("test.zip");
??? Enumeration e = zf.entries();
??? while(e.hasMoreElements()) {
????? ZipEntry ze2 = (ZipEntry)e.nextElement();
????? System.out.println("File: " + ze2);
????? // ... and extract the data as before
??? }
?????? 雖然標準的Zip格式是支持口令的,但是Java的Zip類庫卻不支持。
??????
?????? Java ARchives (JARs)
?????? 一個JAR只有一個文件,包含兩個文件,一個是Zip文件,另一個是描述Zip文件所包含的文件的"manifest(清單)"。
?????? 如果JAR是用0(零)選項創建的,不會進行壓縮,那么它就能被列入CLASSPATH了。
?????? 不能往已經做好的JAR里添加新文件或修改文件。不能在往JAR里移文件的同時把原來的文件給刪了。不過JAR格式是跨平臺的,無論JAR是在哪個平臺上創建的,jar程序都能將它讀出來(zip格式有時就會有問題了)。
?
?????? 9.對象的序列化
?????? Java的"對象序列化"能讓你將一個實現了Serializable接口的對象轉換成一組byte,需要的時候,根據byte數據重新構建那個對象。這一點甚至在跨網絡的環境下也是如此,序列化機制能自動補償操作系統方面的差異。
?????? 對象序列化不僅能保存對象的副本,而且還會跟著對象里面的reference,把它所引用的對象也保存起來,然后再繼續跟蹤那些對象的reference,以此類推。這種情形常被稱為"單個對象所聯結的'對象網'"。這個機制所涵蓋的范圍不僅包括對象的成員數據,而且還包含數組里面的reference。
?????? Worm w = new Worm(6, 'a');
??? ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("worm.out"));
??? out.writeObject("Worm storage\n");
??? out.writeObject(w);
??? out.close(); // Also flushes output
??? ObjectInputStream in = new ObjectInputStream(new FileInputStream("worm.out"));
??? String s = (String)in.readObject();
??? Worm w2 = (Worm)in.readObject();
?
?????? 把對象從序列化狀態中恢復出來的必要條件是,一定要讓JVM找到.class文件。
??????
?????? 控制序列化
?????? 可以讓對象去實現Externalizable而不是Serializable接口,并以此來控制序列化的過程。
?????? 對于Externalizable對象,readExternal( )要在默認的構造行為會發生之后(包括在定義數據成員時進行的初始化)才啟動。
?????? 不但要在writeExternal( )的時候把重要的數據保存起來(默認情況下,Externalizable對象不會保存任何成員對象),還得在readExternal( )的時候把它們恢復出來。為了能正確地存取其父類的組件,你還得調用其父類的writeExternal( )和readExternal( )。
??????
?????? transient關鍵詞
?????? 要想禁止敏感信息的序列化,除了可以實現Externalizable外。還可以使用transient關鍵詞修飾Serializable對象中不想序列化的成員。
?????? 默認情況下,Externalizable對象不保存任何字段,因此transient只能用于Serializable對象。
??????
?????? Externalizable的替代方案
?????? 如果你不喜歡Externalizable,還可以選擇Serializable接口,然后再加入(注意,我沒說"覆寫"或"實現")序列化和恢復的時候會自動調用的writeObject( )和readObject( )方法。也就是說,如果你寫了這兩個方法,Java就會避開默認的序列化機制而去調用這兩個方法了。
?????? 兩個方法的特征簽名如下,(它們都是private的,怪異):
private void writeObject(ObjectOutputStream stream) throws IOException;
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException
?????? 如果你決定用默認的序列化機制來存儲非transient的數據,你就得在writeObject( )里面調用defaultWriteObject( ),不帶參數,而且得第一個做。恢復的時候,也要在readObject( )的開頭部分調用defaultReadObject( )。
?????? 如果你要序列化static數據,就必須親自動手。
??????
?????? Preferences
?????? JDK 1.4所引入的Preferences API能自動存儲和恢復信息。但是,它只能存取很少幾種數據——primitive和String,而且每個String的長度都不能超過8K。
?????? Preferences是一組存儲在"由節點所組成的層次體系(a hierarchy of nodes)"里的鍵值集(很像Map)。Preferences API是借用操作系統的資源來實現功能的。對于Windows,它就放在注冊表里。
?????? //也可以用systemNodeForPackage( )
?????? //"user"指的是單個用戶的preference,而"system"指整個系統的共用配置
?????? //一般用XXX.class做節點的標識符
?????? Preferences prefs = Preferences.userNodeForPackage(PreferencesDemo.class);
??? prefs.put("Location", "Oz");
??? prefs.putInt("Companions", 4);
??? prefs.putBoolean("Are there witches?", true);
???
??? 10.正則表達式
??? 正則表達式是JDK 1.4的新功能。由java.util.regex的Pattern和Matcher類實現的。
??? Pattern p = Pattern.compile(("\\w+");
?????? Matcher m = p.matcher(args[0]);
?????? while(m.find()) {
???????? System.out.println("Match \"" + m.group() +
?????????? "\" at positions " +
?????????? m.start() + "-" + (m.end() - 1));
?????? }
??? 只要字符串里有這個模式,find( )就能把它給找出來,但是matches( )成功的前提是正則表達式與字符串完全匹配,而lookingAt( )成功的前提是,字符串的開始部分與正則表達式相匹配。
??????
?????? split()
?????? 所謂分割是指將以正則表達式為界,將字符串分割成String數組。
?????? String[] split(CharSequence charseq)
?????? String[] split(CharSequence charseq, int limit)//限定分割的次數
?????? String input = "This!!unusual use!!of exclamation!!points";
??? System.out.println(Arrays.asList(Pattern.compile("!!").split(input)));
?
?
===========
Chap13?? 并發編程
?
?????? 1.基本線程
?????? 要想創建線程,最簡單的辦法就是繼承java.lang.Thread。run( )是Thread最重要的方法,什么時候run( )返回了,線程也就中止了。
?????? Thread的start( )方法會先對線程做一些初始化,再調用run( )。
?????? 整個步驟應該是:調用構造函數創建一個Thread對象,并且在構造函數里面調用start( )來配置這個線程,然后讓線程的執行機制去調用run( )。如果你不調用start( ),那么線程永遠也不會啟動。
?????? 有時我們創建了Thread,但是卻沒去拿它的reference。如果是普通對象,這一點就足以讓它成為垃圾,但Thread不會。Thread都會為它自己"注冊",所以實際上reference還保留在某個地方。除非run( )退出,線程中止,否則垃圾回收器不能動它。
?????? 線程的調度機制是非決定性,即多個線程的執行順序是不確定的。
??????
?????? yielding
?????? 如果知道run()已經告一段落了,你就可以用yield( )形式給線程調度機制作一個暗示。Java的線程調度機制是搶占式的(preemptive),只要它認為有必要,它會隨時中斷當前線程(運行到yield之前),并且切換到其它線程。總之,yield( )只會在很少的情況下起作用。
??????
?????? Sleeping
?????? sleep( )一定要放在try域里,這是因為有可能會出現時間沒到sleep( )就被中斷的情況。如果有人拿到了線程的reference,并且調用了它的interrupt( ),這種事就發生了。(interrupt( )也會影響處于wait( )或join( )狀態的線程,所以這兩個方法也要放在try域里。)如果你準備用interrupt( )喚醒線程,那最好是用wait( )而不是sleep( ),因為這兩者的catch語句是不一樣的。
??????
?????? 優先級
?????? 線程往控制臺打印的時候是不會被中斷的,否則控制臺的顯示就亂了。
??????
?????? 守護線程
?????? 所謂"守護線程(daemon thread)"是指,只要程序還在運行,它就應該在后臺提供某種公共服務的線程,但是守護線程不屬于程序的核心部分。因此,當所有非守護線程都運行結束的時候,程序也結束了。
?????? 要想創建守護線程,必須在它啟動之前就setDaemon(true)。守護線程所創建的線程也自動是守護線程。
??????
?????? 連接線程
?????? 線程還能調用另一個線程的join( ),等那個線程結束之后再繼續運行。如果線程調用了另一個線程t的t.join( ),那么在線程t結束之前(判斷標準是,t.isAlive( )等于false),主叫線程會被掛起。
??????
?????? 另一種方式:Runable
?????? 類可能已經繼承了別的類,這時就需要實現Runable接口了。
?????? 如果要在這個實現了Runable的類里做Thread對象才有的操作,必須用Thread.currentThread()獲取其reference。
?????? 除非迫不得已只能用Runnable,否則選Thread。
??????
?????? 2.共享有限的資源
?????? 多線程環境的最本質的問題:永遠也不會知道線程會在什么時候啟動。
?????? 我們不能從線程內部往外面拋異常,因為這只會中止線程而不是程序。
??????
?????? 資源訪問的沖突
?????? Semaphore是一種用于線程間通信的標志對象。如果semaphore的值是零,則線程可以獲得它所監視的資源,如果不是零,那么線程就必須等待。如果申請到了資源,線程會先對semaphore作遞增,再使用這個資源。遞增和遞減是原子操作(atomic operation,也就是說不會被打斷的操作),由此semaphore就防止兩個線程同時使用同一項資源。
??????
?????? 解決共享資源的沖突
?????? 一個特定的對象中的所有的synchronized方法都會共享一個鎖,而這個鎖能防止兩個或兩個以上線程同時讀寫一塊共用內存。當你調用synchronized方法時,這個對象就被鎖住了。在方法返回并且解鎖之前,誰也不能調用同一個對象的其它synchronized方法。
?????? 一定要記住:所有訪問共享資源的方法都必須是synchronized的,否則程序肯定會出錯。
?????? 一個線程能多次獲得對象的鎖。比如,一個synchronized方法調用了另一個synchronized方法,而后者又調用了另一synchronized方法。線程每獲一次對象的鎖,計數器就加一。當然,只有第一次獲得對象鎖的線程才能多次獲得鎖。線程每退出一個synchronized方法,計數器就減一。等減到零了,對象也就解鎖了。
?????? 此外每個類還有一個鎖(它屬于類的Class對象),這樣當類的synchronized static方法讀取static數據的時候,就不會相互干擾了。
??????
?????? 原子操作
?????? 通常所說的原子操作包括對非long和double型的primitive進行賦值,以及返回這兩者之外的primitive。不過如果你在long或double前面加了volatile,那么它就肯定是原子操作了。最安全的原子操作只有讀取和對primitive賦值這兩種。
?????? 如果你要用synchronized修飾類的一個方法,索性把所有的方法全都synchronize了。要判斷,哪個方法該不該synchronize,通常是很難的,而且也沒什么把握。
?????? 并發編程的最高法則:絕對不能想當然。
??????
?????? 關鍵段
?????? 有時你只需要防止多個線程同時訪問方法中的某一部分,而不是整個方法。這種需要隔離的代碼就被稱為關鍵段(critical section)。創建關鍵段需要用到synchronized關鍵詞,指明執行下列代碼需獲得哪個對象的鎖。
synchronized(syncObject) {
? // This code can be accessed by only one thread at a time
}
?????? 關鍵段又被稱為"同步塊(synchronized block)"。相比同步整個方法,同步一段代碼能顯著增加其它線程獲得這個對象的機會。
??????
?????? 3.線程的狀態
?????? 線程的狀態可歸納為以下四種:
?????? (1).new: 線程對象已經創建完畢,但尚未啟動(start),因此還不能運行。
?????? (2).Runnable: 處在這種狀態下的線程,只要分時機制分配給它CPU周期,它就能運行。
?????? (3).Dead: 要想中止線程,正常的做法是退出run( )。
?????? (4).Blocked: 就線程本身而言,它是可以運行的,但是有什么別的原因在阻止它運行。線程調度機制會直接跳過blocked的線程,根本不給它分配CPU的時間。除非它重新進入runnable狀態,否則什么都干不了。
?????? 如果線程被阻塞了,那肯定是出了什么問題。問題可能有以下幾種:
?????? (1).你用sleep(milliseconds)方法叫線程休眠。在此期間,線程是不能運行的。
?????? (2).你用wait( )方法把線程掛了起來。除非收到notify( )或notifyAll( )消息,否則線程無法重新進入runnable狀態。
?????? (3).線程在等I/O結束。
?????? (4).線程要調用另一個對象的synchronized方法,但是還沒有得到對象的鎖。
??????
?????? 4.線程間的協作
?????? wait與notify
?????? 線程sleep( )的時候并不釋放對象的鎖,但是wait( )的時候卻會釋放對象的鎖。也就是說在線程wait( )期間,別的線程可以調用它的synchronized方法。??? 此外,sleep( )屬于Thread。wait( ), notify( ), 和notifyAll( )是根Object的方法。
?????? 只能在synchronized方法里或synchronized段里調用wait( ),notify( )或notifyAll( )。
?????? wait( )能讓你在等待條件改變的同時讓線程休眠,當其他線程調用了對象的notify( )或notifyAll( )的時候,線程自會醒來,然后檢查條件是不是改變了。
?????? 安全的做法就是套用下面這個wait( )定式:
?????? while(conditionIsNotMet)
????????????? wait( );
?????????????
?????? 用管道進行線程間的I/O操作
?????? 在很多情況下,線程也可以利用I/O來進行通信。對Java I/O類庫而言,就是PipedWriter(可以讓線程往管道里寫數據)和PipedReader(讓另一個線程從這個管道里讀數據)。
??????
?????? 5.死鎖
?????? Dijkstra發現的經典的死鎖場景:哲學家吃飯問題。
?????? 只有在下述四個條件同時滿足的情況下,死鎖才會發生:
?????? (1).互斥:也許線程會用到很多資源,但其中至少要有一項是不能共享的(同一時刻只能被一個線程訪問)。
?????? (2).至少要有一個進程會在占用一項資源的同時還在等另一項正被其它進程所占用的資源。也就是說,要想讓死鎖發生,哲學家必須攥著一根筷子等另一根。
?????? (3).(調度系統或其他進程)不能從進程里搶資源。所有進程都必須正常的釋放資源。我們的哲學家都彬彬有禮,不會從他的鄰座手里搶筷子。
?????? (4).需要有等待的環。一個進程在等一個已經被另一進程搶占了的資源,而那個進程又在等另一個被第三個進程搶占了的資源,以此類推,直到有個進程正在等被第一個進程搶占了的資源,這樣就形成了癱瘓性的阻塞了。這里,由于每個哲學家都是先左后右的拿筷子,所以有可能會造成等待的環。在例程中,我們修改了最后一位哲學家的構造函數,讓他先右后左地拿筷子,從而破解了死鎖。
?????? Java語言沒有提供任何能預防死鎖的機制。
??????
?????? 6.停止線程的正確的方法
?????? 為了降低死鎖的發生幾率,Java 2放棄了Thread類stop( ),suspend( )和resume( )方法。
?????? 應該設置一個旗標(flag)來告訴線程什么時候該停止。
?
?????? 7.打斷受阻的線程
?????? 有時線程受阻之后就不能再做輪詢了,比如在等輸入,這時你就不能像前面那樣去查詢旗標了。碰到這種情況,你可以用Thread.interrupt( )方法打斷受阻的線程。最后要把受阻線程的 reference設成null。
??????
?????? 8.總結
?????? 諾貝爾經濟學獎得主Joseph Stiglitz有一條人生哲學,就是所謂的承諾升級理論:
"延續錯誤的代價是別人付的,但是承認錯誤的代價是由你付的。"
?????? 多線程的主要缺點包括:
?????? (1).等待共享資源的時候,運行速度會慢下來。
?????? (2).線程管理需要額外的CPU開銷。
?????? (3).如果設計得不不合理,程序會變得異常復雜。
?????? (4).會引發一些不正常的狀態,像饑餓(starving),競爭(racing),死鎖(deadlock),活鎖(livelock)。
?????? (5).不同平臺上會有一些不一致。
?????? 通常你可以在run( )的主循環里插上yield( ),然后讓線程調度機制幫你加快程序的運行。
?
?
==============
Chap14 創建Windows與Applet程序
??????
?????? 設計中一條基本原則:讓簡單的事情變得容易,讓困難的事情變得可行。
?????? 軟件工業界的“三次修訂”規則:產品在修訂三次后才會成熟。
??????
?????? 1.控制布局
?????? 在Java中,組件放置在窗體上的方式可能與其他GUI系統都不相同。首先,它完全基于代碼,沒有用來控制組件布局的“資源”。第二,組件的位置不是通過絕對坐標控制,二十由“布局管理器”(layout manager)根據組件加入的順序決定其位置。使用不同的布局管理器,組件的大小、形狀和位置將大不相同。此外,布局管理器還可以適應applet或視窗的大小,調整組件的布局。
?????? JApplet,JFrame,JWindow和JDialog都可以通過getContentPane()得到一個容器(Container),用來包含和顯示組件。容器有setLayout()方法,用來設置布局管理器。
??????
?????? 2.Swing事件模型
?????? 在Swing的事件模型中,組件可以觸發一個事件。每種事件的類型由單獨的類表示。當事件被觸發時,它將被一個或多個監聽器接收,監聽器負責處理事件。
?????? 所謂事件監聽器,就是一個“實現了某種類型的監聽器接口的”類的對象。程序員要做的就是,先創建一個監聽器對象,然后把它注冊給觸發事件的組件。注冊動作是通過該組件的addXXXListener()方法完成的。
?????? 所有Swing組件都具有addXXXListener()和removeXXXListener()方法。
?
?????? 3.Swing組件一覽
??????
?????? 工具提示ToolTip
?????? 任何JComponent子類對象都可以調用setToolTipText(String)。
??????
?????? Swing組件上的HTML
?????? 任何能接受文本的組件都可以接受HTML文本,且能根據HTML格式化文本。例如,
?????? JButton b = new JButton("<html><b><font size=+2>Hello<br>Press me");
?????? 必須以"<html>"標記開始,但不會強制添加結束標記。
?????? 對于JApplet,在除init()之外的地方添加新組件后,必須調用容器的validate()來強制對組件進行重新布局,才能顯示新添加的組件。
??????
?????? 4.選擇外觀(Look & Feel)
?????? “可插拔外觀”(Pluggable Look & Feel)使你的程序能夠模仿不同操作系統的外觀。
?????? 設置外觀的代碼要在創建任何可視組件之前調用。Swing的跨平臺的金屬外觀是默認外觀。
?????? try{
????????????? UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
?????? } catch(Exception e){
?????? }
?????? catch子句中什么也不用做,因為缺省情況下,如果設置外觀失敗,UIManager將設置成跨平臺的外觀。
??????
?????? 動態綁定事件
?????? 不能保證事件監聽器被調用的順序與它們被添加的順序相同。
??????
?????? 5.Swing與并發
?????? 始終存在著一個Swing事件調度線程,它用來依次對Swing的所有事件進行調度。
??????
?????? 管理并發
?????? 當你在類的main()方法中,或在一個獨立線程中,準備修改任何Swing組件屬性的時候,要注意,Swing的事件調度線程可能會與你競爭同一資源。
?????? 要解決這個問題,必須確保在任何情況下,只能在事件調度線程里修改Swing組件的屬性。Swing提供了兩種機制:SwingUtilities.invokeLater(Runnable)和SwingUtilities.invokeAndWait(Runnable)。它們都接受runnable對象作參數,并且在Swing的事件處理線程中,只有當事件隊列中的任何未處理的事件都被處理完畢之后,它們才會調用runnable對象的run()方法。
?????? SwingUtilities.invokeLater(new Runnable(){
????????????? public void run(){
???????????????????? txt.setText("ready");
????????????? }
?????? });
?????? invokeLater()是異步方法,會立即返回。invokeAndWait()是同步方法,會一直阻塞,直到事件處理完畢才會放回。
??????
?????? 6.JavaBean與同步
?????? 當你創建Bean的時候,你必須要假設它可能會在多線程環境下運行。也就是說:
?????? (1).盡可能讓Beand中的所有公共方法都是synchronized。這將導致synchronized的運行時開銷。
?????? (2).當一個多路事件觸發了一組對該事件感興趣的監聽器時,必須假定,在遍歷列表進行通知的同時,監聽器可能會被添加或移除。
?????? public void notifyListeners(){
????????????? ActionEvent a = new ActionEvent(BangBean2.this, ActionEvent.ACTION_PERFORMED, null);
????????????? ArrayList lv = null;
????????????? //Make a shallow copy of the List in case someone adds a listener while we're
????????????? //calling listeners
????????????? synchronized(this){
???????????????????? lv = (ArrayList)actinListeners.clone();
????????????? }
????????????? for(int i = 0; i < lv.size(); i++){
???????????????????? ((ActionListener)lv.get(i)).actionPerformed(a);
????????????? }
?????? }
??????
??????
??????
??????
?
==============
Chap15 發現問題
?
?????? 1.單元測試
?????? //Discover the name of the class this object was created within:
?????? className = new Throwable().getStackTrace()[1].getClassName();
??????
?????? JUnit
?????? JUnit在輸出消息中使用"."表示每個測試的開始。
?????? JUnit為每個測試創建一個測試對象(繼承自TestCase),以確保在測試運行之間沒有不利的影響。所有的測試對象都是同時被創建的,而不是正好在測試方法執行之前才創建。
?????? setUp是在每個測試方法運行之前被調用的。
??????
?????? 2.利用斷言提高可靠性
?????? 斷言語法
?????? assert boolean-expression;
?????? assert boolean-expression: information-expression;
??????
?????? 在JDK 1.4中,缺省情況下斷言是關閉的。為了防止編譯時的錯誤,必須帶下面的標志進行編譯:
?????? -source 1.4
?????? 如:javac -source 1.4 Assert1.java
??????
?????? 運行程序也必須加上標志-ea,全拼是-enableassertions。這樣才會執行所有的斷言語句。
??????
?????? 我們也可以基于類名或包名來決定打開或關閉斷言。
?????? 還有另一種動態控制斷言的方法:通過ClassLoader對象的方法setDefaultAssertionStatus(),它為所有隨后載入的類設置斷言的狀態。
?????? public static void main(String[] args){
????????????? ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true);
????????????? //other statements
?????? }
?????? 這樣可以在運行時,不必使用-ea標志,但是仍然必須使用-source 1.4標志編譯。
??????
?????? 為DBC使用斷言
??????
?????? DBC(Design by Contract)是由Bertrand Meyer(Eiffel編程語言的創建者)所闡明的一個概念,它通過確保對象遵循特定的、不能被編譯時的類型檢查所驗證的規則,來幫助建立健壯的程序。
??????
?????? 3.剖析和優化
?????? “我們應該忽略較小的效率,在97%的時間里我們都應該說:不成熟的優化是萬惡之源。”--Donald Knuth
??????
?????? 最優化指南
?????? 避免為性能而犧牲代碼的可讀性。
?????? 不能孤立地考慮性能。要權衡所需付出的努力與能得到的利益之間的關系。
?????? 性能是大型工程要關心的問題,但通常不是小型工程要考慮的。
?????? 使程序可運轉比鉆研程序的性能有更高的優先權。僅當性能被確定是一個關鍵因素的時候,在初始設計開發過程期間才應該予以考慮。
?????? 不要假設瓶頸在什么地方,而應該運行剖析器來獲得數據。
?????? 在任何可能的情況下,盡量通過將對象設置為null,從而顯式地將其銷毀。有時這可能是對垃圾回收器的一種很有幫助的提示。
?????? 程序大小的問題。僅當程序是大型的、運行時間長而且速度也是一個問題時,性能優化才有價值。
?????? static final變量可以通過JVM進行優化以提高程序的速度。
??????
?????? 做可以運轉的最簡單的事物。(極限編程)
??????
?
?
================
附錄A:對象的傳遞與返回
?
?????? 確切地說,Java有指針。Java中(除了基本類型)每個對象的標識符就是一個指針。但是它們受到了限制,有編譯器和運行期系統監視著它們。Java有指針,但沒有指針的相關算法。可以將它們看作“安全的指針”。
?????? “別名效應”是指,多個引用指向同一個對象。將引用作為方法的參數傳遞時,它會自動被別名化。
??????
?????? 制作局部拷貝
?????? Java中所有的參數傳遞,執行的都是引用傳遞。當你傳遞對象時,真正傳遞的只是一個引用,指向存活于方法外的“對象”。對此引用做的任何修改,都是在修改方法外的對象。此外:
?????? (1).別名效應在參數傳遞時自動發生。
?????? (2).方法內沒有局部對象,只有局部引用。
?????? (3).引用有作用域,對象則沒有。
?????? (4).在Java中,不需要為對象的生命周期操心。
?????? (5).沒有提供語言級別的支持(例如“常量”)以阻止對象被修改,或者消除別名效應的負面影響。不能簡單地使用final關鍵字來修飾參數,它只能阻止你將當前引用指向其他對象。
??????
?????? 克隆對象
?????? 如果確實要在方法調用中修改參數,但又不希望修改外部參數,那么就應該在方法內部制作一份參數的副本,以保護原參數。
?????? Object類提供了protected方法clone(),要使用它,必須在子類中以public方式重載此方法。例如,ArrayList就重載了clone()。ArrayList的clone()方法,并不自動克隆容器中包含的每個對象,只是將原ArrayList中的對象別名化,即只復制了ArrayList中對象的引用。這稱為淺拷貝(shallow copy)。
??????
?????? 使類具有克隆能力
?????? 雖然在所有類的基類Object中定義了克隆方法,但也不是每個類都自動具有克隆能力。
?????? 克隆對象時有兩個關鍵問題:
?????? (1).調用super.clone()
?????? (2).將子類的clone()方法聲明為public
?????? 基類的clone()方法,能“逐位復制(bitwise copy)”對象。
??????
?????? 實現Cloneable接口
?????? interface Cloneable{}
?????? 這樣的空接口稱為“標記接口(tagging interface)”。
?????? Cloneable接口的存在有兩個理由。第一,如果某個引用上傳為基類后,就不知道它是否能克隆。此時,可以用instanceof檢查該引用是否指向一個可克隆的對象。
?????? if(myref instanceof Cloneable)//...
?????? 第二,與克隆能力的設計有關,考慮到也許你不愿意所有類型的對象都是可克隆的。所以Object.clone()會檢查當前類是否實現了Cloneable接口,如果沒有,就拋出CloneNotSupportedException異常。所以,作為實現克隆能力的一部分,通常必須實現Cloneable接口。
??????
?????? ==與!=
?????? Java比較對象相等的等價測試并未深入對象的內部。==和!=只是簡單地比較引用。如果引用代表的內存地址相同,則它們指向同一個對象,因此視為相等。所以,該操作符測試的是:不同的引用是否是同一個對象的別名。
??????
?????? Object.clone()的效果
?????? 克隆過程的第一步通常都是調用super.clone()。它制作出完全相同的副本,為克隆操作建立了基礎。在此基礎上,你可以執行對完成克隆必要的其他操作。
?????? 這里的其他操作是指,對對象中的每個引用,都明確地調用clone()。否則,那些引用會被別名化,仍指向原本的對象。
?????? 只要沒有向子類中添加需要克隆的引用,那么無論clone()定義于繼承層次中多深的位置,只需要調用Object.clone()一次,就能完成所有必要的復制。
?????? 對ArrayList深層拷貝而言,以下操作是必須的:克隆ArrayList之后,必須遍歷ArrayList中的每個對象,逐一克隆。對HashMap做深層拷貝,也必須做類似的操作。
??????
?????? 向繼承體系的更下層增加克隆能力
?????? 可以向任意層次的子類添加克隆能力,從那層以下的子類,也就都具備了克隆能力。
??????
?????? 克隆小結
?????? 如果希望一個類可以被克隆:
?????? (1).實現Cloneable接口。
?????? (2).重載clone(),聲明為public。
?????? (3).在clone()中調用Super.clone()。
?????? (4).在clone()中捕獲異常。
??????
?????? 只讀類
?????? 在只讀類中所有數據都是private的,并且沒有定義會修改對象內部狀態的方法。只讀類的對象可以有很多別名,也不會造成傷害。例如,Java標準類庫中所有基本類型的包裝類。
??????
?????? 恒常性(immutability)的缺點
?????? 當你需要一個被修改過的此類的對象的時候,必須承受創建新對象的開銷,也會更頻繁地引發垃圾回收。對于有些類(如String),其代價讓人不得不禁止這么做。
?????? 解決之道是創建一個可被修改的伴隨類(companion class)。
??????
?
=============
附錄B:Java編程指南
?
?????? 設計
?????? 1.優雅設計終將得到回報。精心設計程序的時候生產率不會很高,但欲速則不達。
?????? 2.先能運行,再求快速。
?????? 3.分而治之。
?????? 4.盡量讓所有東西自動化。(如測試和構建,先寫測試,再編寫類)
?????? 5.盡可能使類原子化。
?????? 建議重新設計類的線索有:
?????? (1).復雜的switch語句,請考慮使用多態。
?????? (2).有許多方法,處理類型極為不同的操作:請考慮劃分成不同的類。
?????? (3).有許多成員變量,表示類型極為不同的屬性:請考慮劃分成不同的類。
?????? (4).參考《Refactoring:Improving the Design of Existing Code》,Martin Fowler著,(Addison-Wesley 1999)。
?????? 6.將變動的和不變的因素分離。
?????? 7.在判斷應該使用繼承還是組合的時候,考慮是否需要上傳為基類。
??????
?????? 實現
?????? 1.編寫通用性的類時,請遵守標準形式。包括定義equals()、hashCode()、toString()、clone()(實現Cloneable接口,或者選擇其它對象復制策略),并實現Comparable和Serialiable接口。
?????? 2.在構造器中只做必要的動作:將對象設定為正確的狀態。避免在構造器內調用其它方法(final方法除外),因為這些方法可能會被其他人重載,這就可能在構造期間得到意外的結果。
?????? 3.優先選擇接口而不是抽象類。只有在必須放進方法定義或成員變量時,才把它改為抽象類。接口只和客戶希望的動作有關,而類則傾向于實現細節。
?