使用Joiner類
將任意字符串通過分隔符進行連接到一起是大多程序員經常做的事情。他們經常使用array,list,iterable並且循環變量將每一個臨時變量添加到StringBuilder當中去,並且中間添加分隔符。這些笨重的處理方式如下:
public String buildString(List<String> stringList, String delimiter){ StringBuilder builder = new StringBuilder(); for (String s : stringList) { if(s !=null){ builder.append(s).append(delimiter); } } builder.setLength(builder.length() – delimiter.length()); return builder.toString(); }
注意要刪除在最后面的分隔符。不是很難懂,但是使用Joiner類可以得到簡單的代碼模板。同樣的例子使用Joiner類代碼如下:
public String buildString(List<String> stringList, String delimiter){ return Joiner.on(delimiter).skipNulls().join(stringList); }
這樣更加簡明並且不會出錯。如果你想將null值替換掉,可以使用如下方法:
Joiner.on("|").useForNull("no value").join(stringList);
使用Joiner類有幾點需要注意。Joiner類不僅僅可以處理字符串的array、list、iterable,他還可以處理任何對象的array、list、iterable。結果就是調用每一個元素的toString()方法。因此,如果沒有使用skipNulls或者useForNull,就會拋出空指針異常。Joiner對象一旦被創建就是不可變的,所以他們是線程安全的,可以被當作常亮來看待。然后看看下面的代碼片段:
Joiner stringJoiner = Joiner.on("|").skipNulls(); //使用useForNull方法將會返回一個新的Joiner實例 stringJoiner.useForNull("missing"); stringJoiner.join("foo","bar",null);
在上面的代碼實例當中,useForNull方法並沒有起作用,null值仍然被忽略了。
Joiner不僅僅能返回字符串,還可以與StringBuilder一起使用:
StringBuilder stringBuilder = new StringBuilder(); Joiner joiner = Joiner.on("|").skipNulls(); //返回的StringBuilder實例當中包含連接完成的字符串 joiner.appendTo(stringBuilder,"foo","bar","baz");
上面的例子,我們傳入一個StringBuilder的參數並且返回一個StringBuilder實例。
Joiner類也可以使用實現了Appendable接口的類。
FileWriter fileWriter = new FileWriter(new File("path")): List<Date> dateList = getDates(); Joiner joiner = Joiner.on("#").useForNulls(" "); // 返回由字符串拼接后的FileWriter實例 joiner.appendTo(fileWriter,dateList);
這是一個與上一個相似的例子。我們傳入一個FileWriter實例和一組數據,就會將這組數據拼接后附加到FileWriter當中並且返回。
我們可以看到,Joiner使一些公共的操作變的非常簡單。有一個特殊的方法會實現MapJoiner方法,MapJoiner方法像Joiner一樣使用分割符將每組key與value分開,同時key與value之間也有個分隔符。MapJoiner方法的創建如下:
MapJoiner mapJoiner = Joiner.on("#").withKeyValueSeparator("=");
快速回顧一下上面內容:
- Joiner.on("#")方法會創建一個Joiner的實例。
- 使用返回的Joiner實例調用withKeyValueSeparator方法將會返回MapJoiner對象。
下面是對MapJoiner方法的單元測試代碼:
@Test public void testMapJoiner() { String expectedString = "Washington D.C=Redskins#New York City=Giants#Philadelphia=Eagles#Dallas=Cowboys"; Map<String,String> testMap = Maps.newLinkedHashMap(); testMap.put("Washington D.C","Redskins"); testMap.put("New York City","Giants"); testMap.put("Philadelphia","Eagles"); testMap.put("Dallas","Cowboys"); String returnedString = Joiner.on("#"). withKeyValueSeparator("=").join(testMap); assertThat(returnedString,is(expectedString)); }
回顧時間
上面的單元測試開始時創建一個key和value都是字符串的LinkedHashMap實例,值得注意的是我們使用靜態工廠方法newLinkedHashMap()來創建,Maps類是在com.google.common. collect包當中。然后使用Joiner類創建一個使用key與value拼接的字符串。最后我們斷言他是否與我們期望返回的字符串相同。
使用Splitter類
程序員另一個經常處理的問題是對字符串以特定分隔符進行分割並且獲取一個字符串數組。如果你需要讀取文本文件,你總是會做這樣的事情。但是
String.split方法不夠完美,看下面的例子:
String testString = "Monday,Tuesday,,Thursday,Friday,,"; //parts is [Monday, Tuesday, , Thursday,Friday] String[] parts = testString.split(",");
可以看到,String.split方法省略了最后的2個空串。在有些時候,這個做法是你需要的,但是這些事情是應該由程序員來決定是否省略。Splitter類可以幫助我們實現與Joiner類相反的功能。Splitter可以使用單個字符、固定字符串、正則表達式串、正則表達式對象或者CharMatcher對象(另一個Guava的類,本章會講到)來分割字符串。可以給定具體分割符來創建Splitter對象然后使用。一旦擁有了Splitter實例后就可以調用split方法,並且會返回包含分割后字符串的迭代器對象。
Splitter.on('|').split("foo|bar|baz");
Splitter splitter = Splitter.on("\\d+");
在上面的例子當中,我們看到一個Splitter 實例使用了'|'字符分割,另外一個實例使用了正則表達式進行分割。
Splitter有一個可以處理前面空格和后面空格的方法是trimResults。
//Splits on '|' and removes any leading or trailing whitespace Splitter splitter = Splitter.on('|').trimResults();
與Joiner類一樣Splitter類同樣是一個不可變的類,所以在使用的時候應該使用調用trimResults方法后返回的Splitter實例。
Splitter splitter = Splitter.on('|'); //Next call returns a new instance, does not modify the original! splitter.trimResults(); //Result would still contain empty elements Iterable<String> parts = splitter.split("1|2|3|||");
Splitter 類,像Joiner與MapJoiner一樣也有MapSplitter類。MapSplitter類可以將字符串轉換成Map實例返回,並且元素的順序與字符串給定的順序相同。使用下面方法構造一個MapSplitter實例:
//MapSplitter is defined as an inner class of Splitter Splitter.MapSplitter mapSplitter = Splitter.on("#"). withKeyValueSeparator("=");
可以看到MapSplitter的創建方式與MapJoiner一樣。首先給Splitter指定一個分隔符,然后指定MapSplitter對象key與value的分隔符。下面是一個關於MapSplitter的例子,實現的是與MapJoiner相反地功能。
@Test public void testSplitter() { String startString = "Washington D.C=Redskins#New York City=Giants#Philadelphia=Eagles#Dallas=Cowboys"; Map<String,String> testMap = Maps.newLinkedHashMap(); testMap.put("Washington D.C","Redskins"); testMap.put("New York City","Giants"); testMap.put("Philadelphia","Eagles"); testMap.put("Dallas","Cowboys"); Splitter.MapSplitter mapSplitter = Splitter.on("#").withKeyValueSeparator("="); Map<String,String> splitMap = mapSplitter.split(startSring); assertThat(testMap,is(splitMap)); }
回顧時間
上面的單元測試是將一個字符串用MapSplitter分割成一個LinkedHashMap的實例。然后斷言這個返回的LinkedHashMap實例是否與我們期望是LinkedHashMap實例相同。
Joiners 和Splitters 這兩個類應該是每個java開發人員的工具。
使用Guava操作字符串
無論你喜歡的哪種語言,所有跟字符串相關的事情都是無聊乏味的並且容易出錯。有時候,我們需要從文件或者數據當中讀取數據並且重新格式化這些數據提交給用戶,或者以固定的格式來保存這些數據。幸運的是,Guava提供給我們非常好用的類使我們操作字符串更加輕松。這些類如下:
- CharMatcher
- Charsets
- Strings
現在我們看一看如何在代碼中使用它們。
在第一個例子當中,這個單元測試將會展示使用ASCII類方法來確定一個字符是否是小寫。第二個例子將展示將小寫字符串轉換成大寫。
使用Charsets類
在java當中,有6個標准字符集在每一個java平台都會被支持。這與經常運行下面代碼是相關的:
byte[] bytes = someString.getBytes();
但是有一個問題關於上面的代碼。沒有指定你想返回字節的字符集,你將會獲得系統使用運行時默認的字符集返回的字節,這可能會產生問題有可以默認字符集不是你想要的。所以最佳的做法是像下面這樣:
try{ bytes = "foobarbaz".getBytes("UTF-8"); }catch (UnsupportedEncodingException e){ //This really can't happen UTF-8 must be supported }
但是仍然有兩個問題在上面的代碼當中:
- UTF-8在java平台 一定會被支持,所以UnsupportedEncodingException一定不會被拋出
- 一旦我們使用字符串指定字符集的定義,我們可以產生拼寫錯誤然后導致異常拋出。
Charsets類可以幫助我們,Charsets類提供了static final 的六個java平台支持的字符集。使用Charsets類我們可以使上面的例子更簡單些:
byte[] bytes2 = "foobarbaz".getBytes(Charsets.UTF_8);
值得注意的是在JAVA7當中,StandardCharsets也提供了同樣的功能。現在我們看看Strings類。
使用Strings類
Strings類提供一些便利實用的方法處理字符串。你是否寫過像下面的代碼:
StringBuilder builder = new StringBuilder("foo"); char c = 'x'; for(int i=0; i<3; i++){ builder.append(c); } return builder.toString();
上面例子當中的6行代碼我們可以使用一行代碼來替換。
Strings.padEnd("foo",6,'x');
第二個參數是很重要的,6指定返回的字符串最小長度為6,而不是指定'x'字符在字符串后面追加多少次。如果提供的字符串長度已經大於了6,則不會進行追加。同樣也有一個相類似的padStart方法可以在給定字符串的前面追加字符到指定的長度。
在Strings類當中有三個非常有用的方法來處理空值的:
- nullToEmpty:這個方法接受一個字符串參數,如果傳入的參數不是null值或者長度大於0則原樣返回,否則返回空串("");
- emptyToNull:這個方法類似於nullToEmpty,它將返回null值如果傳入的參數是空串或者null。
- isNullOrEmpty:這個方法會檢查傳入參數是否為null和長度,如果是null和長度為0就返回true。
或許,這將是一個不錯的主意總是在任何使用nullToEmpty方法處理傳入的字符串參數。
使用CharMatcher類
CharMatcher提供了在一定字符串中匹配是否存在特定字符串的功能。在CharMatcher類當中的方法也可以讓格式化更加簡單。例如,你可以將多行的字符串格式化成一行,並且換行符將會以空格來代替。
CharMatcher.BREAKING_WHITESPACE.replaceFrom(stringWithLinebreaks,' ');
還有一個版本replaceFrom的,需要一個CharSequence的值作為第2個參數值,而不是一個單一的字符。
移除多個空格和tab以單個空格來代替,代碼如下:
@Test public void testRemoveWhiteSpace(){ String tabsAndSpaces = "String with spaces and tabs"; String expected = "String with spaces and tabs"; String scrubbed = CharMatcher.WHITESPACE.collapseFrom(tabsAndSpaces,' '); assertThat(scrubbed,is(expected)); }
在上面的測試代碼中,我們把所有多個空格和tab都替換成了一個空格,所有都在一行上。
上面例子在某些情況下可行,但是如果字符串在開頭就有空格我們想移除怎么辦?返回的字符串當中前面依然會包含空格,這時可以使用trimAndCollapseFrom方法:
@Test public void testTrimRemoveWhiteSpace(){ String tabsAndSpaces = " String with spaces and tabs"; String expected = "String with spaces and tabs"; String scrubbed = CharMatcher.WHITESPACE. trimAndCollapseFrom(tabsAndSpaces,' '); assertThat(scrubbed,is(expected)); }
在這個測試當中,我們再一次將把多個空格和tab替換成一個空格也在一行上。
列出所有在CharMatcher類可用的方法是不切實際的,這里是不是更換一組匹配字符的地方,我們保留所匹配的字符的例子:
@Test public void retainFromTest() { String lettersAndNumbers = "foo989yxbar234"; String expected = "989234"; String actual = CharMatcher.JAVA_DIGIT.retainFrom(lettersAndNumbers); assertEquals(expected, actual); }
在這個例子當中我們找到"foo989yxbar234"字符串當中所以的數字並且保留下來。
往下繼續之前,我們應該看看最后一個CharMatcher類中的強大功能。可以聯合多個CharMatcher類實例創建一個新的CharMatcher類實例。
假設你需要創一個匹配數字和空格的CharMatcher類實例:
CharMatcher cm = CharMatcher.JAVA_DIGIT.or(CharMatcher.WHITESPACE);
它現在將會匹配任何數字或者空格。CharMatcher類對於操作字符串來說功能強大並且很好用。
使用Preconditions類
Preconditions類是用來驗證我們的代碼狀態的靜態方法的集合。 Preconditions非常重要,因為他們保證我們的期望執行成功的代碼得到滿足。 如果條件與我們期望的不同,我們會及時反饋問題。和以前一樣,使用前提條件的重要性是確保我們代碼的行為,並在調試中很有用。
你可以寫你自己的先決條件,像下面這樣:
if(someObj == null){ throw new IllegalArgumentException(" someObj must not be null"); }
使用Preconditions當中的方法(需要靜態導入),檢查一個空值更簡單。
checkNotNull(someObj,"someObj must not be null");
接下來,我們將要展示使用先決條件的例子:
public class PreconditionExample { private String label; private int[] values = new int[5]; private int currentIndex; public PreconditionExample(String label) { //返回label如果不為空 this.label = checkNotNull(label,"Label can''t be null"); } public void updateCurrentIndexValue(int index, int valueToSet) { //檢查索引是否有效 this.currentIndex = checkElementIndex(index, values.length, "Index out of bounds for values"); //檢查參數值 checkArgument(valueToSet <= 100,"Value can't be more than 100"); values[this.currentIndex] = valueToSet; } public void doOperation(){ checkState(validateObjectState(),"Can't perform operation"); } private boolean validateObjectState(){ return this.label.equalsIgnoreCase("open") && values[this. currentIndex]==10; } }
下面是對上面例子當中四個方法的摘要信息:
- checkNotNull(T object, Object message):這個方法如果object不為null直接返回,如果為null會拋出空指針異常。
- checkElementIndex (int index, int size, Object message):在這方法當中,index是你將要訪問的元素下標,size是這個要訪問的array,list或者字符串的長度。然后校驗是否有效,如果無效拋出IndexOutOfBoundsException。
- checkArgument (Boolean expression, Object message):這方法傳入布爾表達式。 這個布爾表達式如果為true則繼續執行,否則拋出IllegalArgumentException。
- checkState (Boolean expression, Object message):這方法傳入 一個布爾表達式涉及對象的狀態,而不是參數。 這個布爾表達式如果為true則繼續執行,否則拋出IllegalArgumentException。
Object工具
在這個章節當中我們將介紹幫助檢查null值和創建toString和hashCode的方法的實用方法。我們接着去看看一個有用的類,它實現Comparable接口。
Getting help with the toString method
當我們要調試的時候,toString方法是必須重寫的,重寫它的乏味無趣的。然而,Objects類可以使用toStringHelper方法讓重寫更簡單。看下面的例子:
public class Book implements Comparable<Book> { private Person author; private String title; private String publisher; private String isbn; private double price; public String toString() { return Objects.toStringHelper(this).omitNullValues().add("title", title).add("author", author).add("publisher", publisher) .add("price",price).add("isbn", isbn).toString(); } }
讓我們看看toString方法:
- 首先我們傳入一個Book對象來創建一個Objects.ToStringHelper實例。
- 第二步,我們調用omitNullValues來排除任何null值的屬性。
- 調用add方法來添加每一個屬性的標簽和屬性。
檢查null
值
firstNonNull方法接受2個參數並且返回第一個參數如果它不為null。
String value = Objects.firstNonNull(someString, "default value");
firstNonNull方法使用時候如果你不確定傳入的值是否為null你可以提供一個默認值給它。需要注意:如果傳入的2個參數都是null,會拋出空指針異常。
創建hash codes
為類寫hashCode方法是基本的但是乏味無趣。Objects類可以幫助我們使用hashCode方法更加簡單。考慮下Book類有4個屬性:title, author, publisher, 和isbn. 下面的代碼將展示使用Object.hashCode方法返回hashCode值。
public int hashCode() { return Objects.hashCode(title, author, publisher, isbn); }
實現CompareTo方法
再次使用Book類,下面是典型的實現compareTo方法。
public int compareTo(Book o) { int result = this.title.compareTo(o.getTitle()); if (result != 0) { return result; } result = this.author.compareTo(o.getAuthor()); if (result != 0) { return result; } result = this.publisher.compareTo(o.getPublisher()); if(result !=0 ) { return result; } return this.isbn.compareTo(o.getIsbn()); }
現在讓我們看一看使用ComparisonChain類來實現compareTo方法:
public int compareTo(Book o) { return ComparisonChain.start() .compare(this.title, o.getTitle()) .compare(this.author, o.getAuthor()) .compare(this.publisher, o.getPublisher()) .compare(this.isbn, o.getIsbn()) .compare(this.price, o.getPrice()) .result(); }
第二個例子顯得更緊湊和更好閱讀。而且,ComparisonChain類會在第一個比較當中如果返回非零時停止比較,只有一種情況返回0,那就是所有的比較返回的都是0。
概要
在本章中我們介紹了許多基礎操作。我們學習了如何使用Guava讓分割字符串更簡單通過使用Joiner, Splitter,和非常有用的MapJoiner和MapSplitter類。我們也學習了使用Charsets, CharMatcher, 和 Strings 來操作字符串。
我們學習了如何讓我們代碼更加強壯和便於調試通過使用Preconditions類。在Objects類中,我們學習了一些使用方法來設置默認值和創建toString和hashCode方法。我們也學習了使用ComparisonChain類來實現compareTo方法更簡單。
在下一章,我們學習如何使用Guava來進行函數式編程,通過Function和Predicate接口使我們的編程效率更加高。