| 改進JAVA字串分解的方法 一、概述 大多數Java程式員都曾經使用過java.util.StringTokenizer類。它是一個很方便的字串分解器,主要用來根據分隔字元把字串分割成標記(Token),然後按照請求返回各個標記。這個過程稱為Tokenization,實際上就是把字元序列轉換成應用程式能夠理解的多個標記。 雖然StringTokenizer用起來很方便,但它的功能卻很有限。這個類只是簡單地在輸入字串中尋找分隔字元,一旦找到了分隔字元就分割字串。它不會檢查分隔字元是否在子串之中這類條件,當輸入字串中出現兩個連續的分隔字元時,它也不會返回""(字串長度為0)形式的標記。 為了突破這些局限,Java 2平台提供了BreakIterator類,它是在StringTokenizer之上改進的字串分解器。由於JDK 1.1.x沒有提供這個類,為了滿足自己的需要,開發人員經常花費很多時間從頭開始編寫分解器。在涉及到資料格式化處理的大型工程中,這類定製的字串分解器有時隨處可見,而且這種情況並不罕見。 本文的目標是協助你利用現有的StringTokenizer類,編寫一個進階字串分解器。 二、StringTokenizer的局限 你可以用以下三種建構函式中的任意一種建立StringTokenizer分解器: StringTokenizer(String sInput):以空白字元(“ ”,“\t”,“\n”)為分隔字元分割字串。 StringTokenizer(String sInput, String sDelimiter):以sDelimiter為分隔字元分割字串。 StringTokenizer(String sInput, String sDelimiter, boolean bReturnTokens):以sDelimiter為分隔字元分割字串,但如果bReturnTokens為true,則分隔字元也作為標記返回。 第一個建構函式不檢查輸入字串是否包含子串。例如,如果以空白字元為分隔字元分割“hello. Today \"I am \" going to my home town”,則字串分解結果是hello.、Today、"I、am、"、going等,而不是hello.、Today、"I am "、going等。 第二個建構函式不檢查兩個分隔字元連續出現的情況。例如,如果以“,”為分隔字元分割“book, author, publication,,,date published”這個字串,則StringTokenizer返回book、author、publication和date published這四個標記,而不是book、author、publication、""、""和date published這6個標記(其中""表示0長度字串)。要得到6個標記的答案,你必須把StringTokenizer的bReturnTokens參數設定為true。 允許設定值為true的bReturnTokens參數是一個重要的功能,因為它考慮到了分隔字元連續出現的情況。例如,使用第二個建構函式時,如果資料是動態收集得到而且要用來更新資料庫中的表,輸入字串中的標記對應著表裡面列的值,那麼當我們不能確定哪一個列應該設定為""時,我們就無法把輸入串中的標記映射到資料庫列。假設我們要把記錄插入到一個有6個列的表,而輸入資料中包含兩個連續的分隔字元。此時,StringTokenizer的分解結果是5個標記(兩個連續的分隔字元代表""標記,它將被StringTokenizer忽略),而我們卻有6個欄位需要設定。同時,我們也不知道連續分隔字元在哪裡出現,所以也就不知道哪一個列應該設定成""。 當標記本身等同於分隔字元(無論是長度還是值)且位於子串之內時,第三個建構函式無效。例如,如果我們要以“,”為分隔字元分解字串“book, author, publication,\",\",date published”(這個字串包含一個“,”標記,它與分隔字元一樣),結果是book、author、publication、"、"、date published這六個標記,而不是book、author、publication、,(逗號字元)、date published這五個標記。再提醒一下,即使我們把StringTokenizer的bReturnTokens參數設定設定成了true,在這種情況下也沒有什麼協助。 三、進階字串分解器 在編寫代碼之前,你必須搞清楚一個好的分解器有哪些基本要求。因為Java開發人員已經習慣於使用StringTokenizer類,所以一個好的分解器應該提供StringTokenizer類提供的所有實用方法,比如hasMoreTokens()、nextToken()、countTokens()。 本文提供的代碼很簡單,而且大部分代碼足以自我解釋。在這裡,我主要利用了StringTokenizer類(建立類執行個體時bReturnTokens參數設定為true),並提供了上面提到的幾個方法。大多數時候標記與分隔字元不同,有些時候分隔字元卻要作為標記輸出(儘管非常罕見),此時如果出現了對標記的請求,分解器要把分隔字元作為標記輸出。建立PowerfulTokenizer對象時,你只需要提供輸入字串和分隔字元這兩個參數,PowerfulTokenizer將在內部使用bReturnTokens設定成true的StringTokenizer。(這麼做的原因在於,如果不是用bReturnTokens設定成true的方式建立StringTokenizer,那麼它將在解決先前提出的問題時受到限制)。為了正確地控制分解器,代碼在幾個地方(計算標記的總數量以及nextToken())檢查bReturnTokens是否設定成了true。 你可能已經發現,PowerfulTokenizer實現了Enumeration介面,從而也就實現了hasMoreElements()和nextElement()這兩個方法,而這兩個方法又分別把調用直接委託給hasMoreTokens()和nextToken()。(由於實現了Enumeration介面,PowerfulTokenizer實現了與StringTokenizer的向後相容。) 我們來看一個例子,假設輸入字串是“hello, Today,,, \"I, am \", going to,,, \"buy, a, book\"”,分隔字元是“,”。用分解器分割這個字串時返回結果如表1所示: 表1:字串分解結果 輸入字串包含11個逗號(,)字元,其中3個在子串裡面、4個連續出現(“Today,,,”中包含兩個連續逗號,第一個逗號是Today的分隔字元)。下面是PowerfulTokenizer計算標記總數的演算法: 如果bReturnTokens=true,把子串中的分隔字元數量乘以2,再從實際總數量減去該數字,就得到了標記的總數。理由是,對於子串“buy, a, book”,StringTokenizer將返回5個標記(即“buy:,:a:,:book”),而PowerfulTokenizer將返回一個標記(即“buy, a, book”),兩者的差值是4(即,2乘以子串中的分隔字元數量)。這個公式對於所有包含分隔字元的子串都有效。 類似地,對於bReturnTokens=false的情形,我們從實際總數(19)減去運算式[分隔字元總數(11)- 連續分隔字元數量(4) + 子串中的分隔字元數量(3)]。由於這時我們不返回分隔字元,它們(非連續出現或在子串內部)對我們來說沒有用,上面的公式為我們返回了標記的總數量(9)。 請記住這兩個公式,它們是PowerfulTokenizer的核心。這兩個公式適用於幾乎所有它們各自條件下的情形。但是,如果你有更複雜的要求,不能使用這兩個公式,那麼你應該在編寫代碼之前分析各種可能出現的情況,並設計出自己的公式。 // 檢查分隔字元是否位於子串之內 for (int i=1; i<aiIndex.length; i++)>/td> { iIndex = sInput.indexOf(sDelim, iIndex+1); if (iIndex == -1) break; // 如果分隔字元位於子串之內,則向前分析直至子串結束 while (sInput.substring(iIndex-iLen, iIndex).equals(sDelim)) { iNextIndex = sInput.indexOf(sDelim, iIndex+1); if (iNextIndex == -1) break; iIndex = iNextIndex; } aiIndex[i] = iIndex; //System.out.println("aiIndex[" + i + "] = " + iIndex); if (isWithinQuotes(iIndex)) { if (bIncludeDelim) iTokens -= 2; else iTokens -= 1; } } countTokens()方法檢查子串是否包含雙引號。如果包含,那麼它減少總數並把索引值修改為字串中下一個雙引號出現的位置(如上面的代碼片斷所示)。如果bReturnTokens是false,那麼它從總數減去輸入字串中出現的非連續分隔字元的數量。 // 如發現多個連續的分隔字元,則返回""作為標記 if ( (sPrevToken.equals(sDelim)) && (sToken.equals(sDelim)) ) { sPrevToken = sToken; iTokenNo++; return ""; } // 檢查標記本身是否等於分隔字元 if ( (sToken.trim().startsWith("\"")) && (sToken.length() == 1) ) { // 標記本身等於分隔字元的特殊情況 String sNextToken = oTokenizer.nextToken(); while (!sNextToken sToken += sNextToken; sPrevToken = sToken; iTokenNo++; return sToken.substring(1, sToken.length()-1); } // 檢查字串中是否包含子串 else if ( (sToken.trim().startsWith("\"")) && (!((sToken.trim().endsWith("\"")) && (!sToken.trim().endsWith("\"\"")))) ) { if (oTokenizer.hasMoreTokens()) { String sNextToken = oTokenizer.nextToken(); // 檢查"\"\"" while (!((sNextToken.trim().endsWith("\"")) && (!sNextToken.trim().endsWith("\"\""))) ) { sToken += sNextToken; if (!oTokenizer.hasMoreTokens()) { sNextToken = ""; break; } sNextToken = oTokenizer.nextToken(); } sToken += sNextToken; } } nextToken()方法通過StringTokenizer.nextToken方法擷取標記,並檢查標記中的雙引號字元。如果發現了這些字元,它繼續擷取標記直至不能再找到帶有雙引號的標記。另外,它還把標記儲存到一個變數(sPrevToken,參見本文後面完整的原始碼)以檢查連續出現的分隔字元。如果nextToken()發現等同於分隔字元的連續多個標記,那麼它返回""(長度為0的字串)作為標記。 按照類似的方法,hasMoreTokens()方法檢查已經返回的標記數量是否小於標記的總數量。 【結束語】本文為你介紹了如何輕鬆地編寫一個強大的字串分解器。根據本文介紹的原理,你能夠迅速編寫出複雜的字串分解器,節省大量的開發時間 |