Guava:好用的java類庫 學習小記
基礎功能
google guava中定義的String操作
在google guava中為字串操作提供了很大的便利,有老牌的判斷字串是否為空字串或者為null,用指定字元填充字串,以及拆分合並字串,字串匹配的判斷等等。
1. 使用com.google.common.base.Strings類的isNullOrEmpty(input)方法判斷字串是否為空
1//Strings.isNullOrEmpty(input) demo 2String input = ""; 3boolean isNullOrEmpty = Strings.isNullOrEmpty(input); 4System.out.println("input " + (isNullOrEmpty?"is":"is not") + " null or empty.");
2. 獲得兩個字串相同的字首或者字尾
1//Strings.commonPrefix(a,b) demo 2String a = "com.jd.coo.Hello"; 3String b = "com.jd.coo.Hi"; 4String ourCommonPrefix = Strings.commonPrefix(a,b); 5System.out.println("a,b common prefix is " + ourCommonPrefix); 6 7//Strings.commonSuffix(a,b) demo 8String c = "com.google.Hello"; 9String d = "com.jd.Hello"; 10String ourSuffix = Strings.commonSuffix(c,d); 11System.out.println("c,d common suffix is " + ourSuffix);
3. Strings的padStart和padEnd方法來補全字串
1int minLength = 4; 2String padEndResult = Strings.padEnd("123", minLength, '0'); 3System.out.println("padEndResult is " + padEndResult); 4 5String padStartResult = Strings.padStart("1", 2, '0'); 6System.out.println("padStartResult is " + padStartResult);
4. 使用Splitter類來拆分字串
Splitter類可以方便的根據正則表示式來拆分字串,可以去掉拆分結果中的空串,可以對拆分後的字串做trim操作,還可以做二次拆分。
我們先看一個基本的拆分例子:
Iterable<String> splitResults = Splitter.onPattern("[,,]{1,}") .trimResults() .omitEmptyStrings() .split("hello,word,,世界,水平"); for (String item : splitResults) { System.out.println(item); }
Splitter的onPattern方法傳入的是一個正則表示式,其後緊跟的trimResults()方法表示要對結果做trim,omitEmptyStrings()表示忽略空字串,split方法會執行拆分操作。
split返回的結果為Iterable<String>,我們可以使用for迴圈語句來逐個列印拆分字串的結果。
Splitter還有更強大的功能,做二次拆分,這裡二次拆分的意思是拆分兩次,例如我們可以將a=b;c=d這樣的字串拆分成一個Map<String,String>。
1String toSplitString = "a=b;c=d,e=f"; 2Map<String,String> kvs = Splitter.onPattern("[,;]{1,}").withKeyValueSeparator('=').split(toSplitString); 3for (Map.Entry<String,String> entry : kvs.entrySet()) { 4System.out.println(String.format("%s=%s", entry.getKey(),entry.getValue())); 5}
二次拆分首先是使用onPattern做第一次的拆分,然後再通過withKeyValueSeperator('')方法做第二次的拆分。
5. 有拆分字串必然就有合併字串,guava為我們提供了Joiner類來做字串的合併
我們先看一個簡單的示例:
1String joinResult = Joiner.on(" ").join(new String[]{"hello","world"}); 2System.out.println(joinResult);
面例子中我們使用Joiner.on(" ").join(xx)來合併字串。很簡單也很有效。
Splitter方法可以對字串做二次的拆分,對應的Joiner也可以逆向操作,將Map<String,String>做合併。我們看下下面的例子:
1Map<String,String> map = new HashMap<String,String>(); 2map.put("a", "b"); 3map.put("c", "d"); 4String mapJoinResult = Joiner.on(",").withKeyValueSeparator("=").join(map); 5System.out.println(mapJoinResult);
使用withKeyValueSeparator方法可以對map做合併。合併的結果是:a=b,c=d
guava庫中還可以對字串做大小寫轉換(CaseFormat列舉),可以對字串做模式匹配。使用起來都很方便。
guava中的物件操作封裝
在開發中經常會需要比較兩個物件是否相等,這時候我們需要考慮比較的兩個物件是否為null,然後再呼叫equals方法來比較是否相等,google guava庫的com.google.common.base.Objects類提供了一個靜態方法equals可以避免我們自己做是否為空的判斷,示例如下:
1Object a = null; 2Object b = new Object(); 3 boolean aEqualsB = Objects.equal(a, b);
Objects.equals的實現是很完美的,其實現程式碼如下:
1 public static boolean equal(@Nullable Object a, @Nullable Object b) { 2return a == b || (a != null && a.equals(b)); 3}
首先判斷a b是否是同一個物件,如果是同一物件,那麼直接返回相等,如果不是同一物件再判斷a不為null並且a.equals(b). 這樣做既考慮了效能也考慮了null空指標的問題。
另外Objects類中還為我們提供了方便的重寫toString()方法的機制,我們通過例子來了解一下吧:
1 import com.google.common.base.Objects; 2 3 public class ObjectsDemo { 4public static void main(String [] args) { 5Student jim = new Student(); 6jim.setId(1); 7jim.setName("Jim"); 8jim.setAge(13); 9System.out.println(jim.toString()); 10} 11 12public static class Student { 13private int id; 14private String name; 15private int age; 16 17public int getId() { 18return id; 19} 20public void setId(int id) { 21this.id = id; 22} 23 24public String getName() { 25return name; 26} 27public void setName(String name) { 28this.name = name; 29} 30 31public int getAge() { 32return age; 33} 34public void setAge(int age) { 35this.age = age; 36} 37 38public String toString() { 39return Objects.toStringHelper(this.getClass()) 40.add("id", id) 41.add("name", name) 42.add("age", age) 43.omitNullValues().toString(); 44} 45} 46 }
我們定義了一個Student類,該類有三個屬性,分別為id,name,age,我們重寫了toString()方法,在這個方法中我們使用了Objects.toStringHelper方法,首先指定toString的類,然後依次add屬性名稱和屬性值,可以使用omitNullValues()方法來指定忽略空值,最後呼叫其toString()方法,就可以得到一個格式很好的toString實現了。
上面程式碼輸出的結果是:
Student{id=1, name=Jim, age=13}
這種方式寫起來很簡單,可讀性也很好,所以用Guava吧。
guava的Preconditions使用
guava的base包中提供的Preconditions類用來方便的做引數的校驗,他主要提供如下方法:
- checkArgument 接受一個boolean型別的引數和一個可選的errorMsg引數,這個方法用來判斷引數是否符合某種條件,符合什麼條件google guava不關心,在不符合條件時會丟擲IllegalArgumentException異常
- checkState 和checkArgument引數和實現基本相同,從字面意思上我們也可以知道這個方法是用來判斷狀態是否正確的,如果狀態不正確會丟擲IllegalStateException異常
- checkNotNull方法用來判斷引數是否不是null,如果為null則會丟擲NullPointerException空指標異常
- checkElementIndex方法用來判斷使用者傳入的陣列下標或者list索引位置,是否是合法的,如果不合法會丟擲IndexOutOfBoundsException
- checkPositionIndexes方法的作用和checkElementIndex方法相似,只是此方法的索引範圍是從0到size包括size,而上面的方法不包括size。
下面我們看一個具體的使用示例:
1 import com.google.common.base.Preconditions; 2 3 public class PreconditionsDemo { 4public static void main(String[] args) { 5PreconditionsDemo demo = new PreconditionsDemo(); 6demo.doSomething("Jim", 19, "hello world, hello java"); 7} 8 9public void doSomething(String name, int age, String desc) { 10Preconditions.checkNotNull(name, "name may not be null"); 11Preconditions.checkArgument(age >= 18 && age < 99, "age must in range (18,99)"); 12Preconditions.checkArgument(desc !=null && desc.length() < 10, "desc too long, max length is ", 10); 13 14//do things 15} 16 }
上面例子中的doSomething()方法呼叫了三次Preconditions的方法,來對引數做校驗。
看似Preconditions實現很簡單,他的意義在於為我們提供了同一的引數校驗,並對不同的異常情況丟擲合適型別的異常,並對異常資訊做格式化。
使用google guava的Optional介面來避免空指標錯誤
null會帶來很多問題,從開始有null開始有無數程式栽在null的手裡,null的含義是不清晰的,檢查null在大多數情況下是不得不做的,而我們又在很多時候忘記了對null做檢查,在我們的產品真正投入使用的時候,空指標異常出現了,這是一種討厭的情況。
鑑於此google的guava庫中提供了Optional介面來使null快速失敗,即在可能為null的物件上做了一層封裝,在使用Optional靜態方法of時,如果傳入的引數為null就丟擲NullPointerException異常。
我們看一個實際的例子:
1 import com.google.common.base.Optional; 2 3 public class OptionalDemo { 4public static void main(String[] args) { 5Optional<Student> possibleNull = Optional.of(null); 6possibleNull.get(); 7} 8public static class Student { } 9 }
上面的程式,我們使用Optional.of(null)方法,這時候程式會第一時間丟擲空指標異常,這可以幫助我們儘早發現問題。
我們再看另外一個例子,我們使用Optional.absent方法來初始化posibleNull例項,然後我們get此物件,看看會是什麼情況。
1 public class OptionalDemo { 2public static void main(String[] args) { 3Optional<Student> possibleNull = Optional.absent(); 4Student jim = possibleNull.get(); 5} 6public static class Student { } 7 }
執行上面的程式,發現出現了:Exception in thread "main" java.lang.IllegalStateException: Optional.get() cannot be called on an absent value。
這樣使用也會有異常出來,那Optional到底有什麼意義呢?
使用Optional除了賦予null語義,增加了可讀性,最大的優點在於它是一種傻瓜式的防護。Optional迫使你積極思考引用缺失的情況 ,因為你必須顯式地從Optional獲取引用。直接使用null很容易讓人忘掉某些情形,儘管FindBugs可以幫助查詢null相關的問題,但是我們還是認為它並不能準確地定位問題根源。
如同輸入引數,方法的返回值也可能是null。和其他人一樣,你絕對很可能會忘記別人寫的方法method(a,b)會返回一個null,就好像當你實現method(a,b)時,也很可能忘記輸入引數a可以為null。將方法的返回型別指定為Optional,也可以迫使呼叫者思考返回的引用缺失的情形。
google guava Throwables幫你丟擲異常,處理異常
guava類庫中的Throwables提供了一些異常處理的靜態方法,這些方法的從功能上分為兩類,一類是幫你丟擲異常,另外一類是幫你處理異常。
也許你會想:為什麼要幫我們處理異常呢?我們自己不會丟擲異常嗎?
假定下面的方法是我們要呼叫的方法。
1public void doSomething() throws Throwable { 2//ignore method body 3} 4 5public void doSomethingElse() throws Exception { 6//ignore method body 7}
這兩個方法的簽名一個throws出了Throwable另外一個throws出了Exception,他們沒有定義具體會丟擲什麼異常,也就是說他們什麼異常都有可能丟擲來,如果我們要呼叫這樣的方法,就需要對他們的異常做一些處理了,我們需要判斷什麼樣的異常需要丟擲去,什麼樣的異常需要封裝成RuntimeException。而這些事情就是Throwables類要幫我們做的事情。
假定我們要實現一個doIt的方法,該方法要呼叫doSomething方法,而doIt的定義中只允許丟擲SQLException,我們可以這樣做:
1public void doIt() throws SQLException { 2try { 3doSomething(); 4} catch (Throwable throwable) { 5Throwables.propagateIfInstanceOf(throwable, SQLException.class); 6Throwables.propagateIfPossible(throwable); 7} 8}
請注意doIt的catch塊,下面這行程式碼的意思是如果異常的型別是SQLException,那麼丟擲這個異常
Throwables.propagateIfInstanceOf(throwable, SQLException.class);
第二行表示如果異常是Error型別,那麼丟擲這個型別,否則將丟擲RuntimeException,我們知道RuntimeException是不需要在throws中宣告的。
Throwables.propagateIfPossible(throwable);
Throwables類還為我們提供了一些方便的異常處理幫助方法:
- 我們可以通過Throwables.getRooCause(Throwable)獲得根異常
- 可以使用getCausalChain方法獲得異常的列表
- 可以通過getStackTraceAsString獲得異常堆疊的字串
集合增強
guava的不可變集合
不可變集合的意義
不可變物件有很多優點,包括:
- 當物件被不可信的庫呼叫時,不可變形式是安全的;
- 不可變物件被多個執行緒呼叫時,不存在競態條件問題
- 不可變集合不需要考慮變化,因此可以節省時間和空間。所有不可變的集合都比它們的可變形式有更好的記憶體利用率(分析和測試細節);
- 不可變物件因為有固定不變,可以作為常量來安全使用。
建立物件的不可變拷貝是一項很好的防禦性程式設計技巧。Guava為所有JDK標準集合型別和Guava新集合型別都提供了簡單易用的不可變版本。
JDK也提供了Collections.unmodifiableXXX方法把集合包裝為不可變形式,但我們認為不夠好:
- 笨重而且累贅:不能舒適地用在所有想做防禦性拷貝的場景;
- 不安全:要保證沒人通過原集合的引用進行修改,返回的集合才是事實上不可變的;
- 低效:包裝過的集合仍然保有可變集合的開銷,比如併發修改的檢查、散列表的額外空間,等等。
如果你沒有修改某個集合的需求,或者希望某個集合保持不變時,把它防禦性地拷貝到不可變集合是個很好的實踐。
重要提示:所有Guava不可變集合的實現都不接受null值。我們對Google內部的程式碼庫做過詳細研究,發現只有5%的情況需要在集合中允許null元素,剩下的95%場景都是遇到null值就快速失敗。如果你需要在不可變集合中使用null,請使用JDK中的Collections.unmodifiableXXX方法。更多細節建議請參考 “使用和避免null” 。
如何使用guava的不可變集合
1. 如何建立不可變集合
第一種方法使用builder建立:
1 public class ImmutableDemo { 2public static void main(String[] args) { 3Set<String> immutableNamedColors = ImmutableSet.<String>builder() 4.add("red", "green","black","white","grey") 5.build(); 6//immutableNamedColors.add("abc"); 7for (String color : immutableNamedColors) { 8System.out.println(color); 9} 10} 11 }
第二種方法使用of靜態方法建立:
ImmutableSet.of("red","green","black","white","grey");
第三種方法使用copyOf靜態方法建立:
ImmutableSet.copyOf(new String[]{"red","green","black","white","grey"});
2. 使用asList()獲得不可變集合的list檢視
asList方法是在ImmutableCollection中定義,而所有的不可變集合都會從ImmutableCollection繼承,所以所有的不可變集合都會有asList()方法返回當前不可變集合的list檢視,這個檢視也是不可變的。
3. 不可變集合的使用
不可變集合的使用和普通集合一樣,只是不能使用他們的add,remove等修改集合的方法。
guava集合之Multiset
Multiset看似是一個Set,但是實質上它不是一個Set,它沒有繼承Set介面,它繼承的是Collection<E>介面,你可以向Multiset中新增重複的元素,Multiset會對新增的元素做一個計數。
它本質上是一個Set加一個元素計數器。
1 import com.google.common.base.Splitter; 2 import com.google.common.collect.HashMultiset; 3 import com.google.common.collect.Multiset; 4 5 public class MultisetDemo { 6public static void main(String[] args) { 7Multiset multiset = HashMultiset.create(); 8String sentences = "this is a story, there is a good girl in the story."; 9Iterable<String> words = Splitter.onPattern("[^a-z]{1,}").omitEmptyStrings().trimResults().split(sentences); 10for (String word : words) { 11multiset.add(word); 12} 13 14for (Object element : multiset.elementSet()) { 15System.out.println((String)element + ":" + multiset.count(element)); 16} 17} 18 }
在上面的示例中我們對一段文字拆分成一個一個的單詞,然後依次放入到multiset中,注意這段文字中有多個重複的單詞,然後我們通過for迴圈遍歷multiset中的每一個元素,並輸出他們的計數。輸出內容如下:
story:2 is:2 girl:1 there:1 a:2 good:1 the:1 in:1 this:1
顯然計數不是問題,Multiset還提供了add和remove的過載方法,可以在add或這remove的同時指定計數的值。
常用實現 Multiset 介面的類有:
元素被排序存放於
看到這裡你可能已經發現 Guava Collections 都是以 create 或是 of 這樣的靜態方法來構造物件。這是因為這些集合類大多有多個引數的私有構造方法,由於引數數目很多,客戶程式碼程式設計師使用起來就很不方便。而且以這種方式可以返回原型別的子型別物件。另外,對於建立範型物件來講,這種方式更加簡潔。
google guava的BiMap:雙向Map
我們知道Map是一種鍵值對對映,這個對映是鍵到值的對映,而BiMap首先也是一種Map,他的特別之處在於,既提供鍵到值的對映,也提供值到鍵的對映,所以它是雙向Map.
想象這麼一個場景,我們需要做一個星期幾的中英文表示的相互對映,例如Monday對應的中文表示是星期一,同樣星期一對應的英文表示是Monday。這是一個絕好的使用BiMap的場景。
1 mport com.google.common.collect.BiMap; 2 import com.google.common.collect.HashBiMap; 3 4 public class BiMapDemo { 5public static void main(String[] args) { 6BiMap<String,String> weekNameMap = HashBiMap.create(); 7weekNameMap.put("星期一","Monday"); 8weekNameMap.put("星期二","Tuesday"); 9weekNameMap.put("星期三","Wednesday"); 10weekNameMap.put("星期四","Thursday"); 11weekNameMap.put("星期五","Friday"); 12weekNameMap.put("星期六","Saturday"); 13weekNameMap.put("星期日","Sunday"); 14 15System.out.println("星期日的英文名是" + weekNameMap.get("星期日")); 16System.out.println("Sunday的中文是" + weekNameMap.inverse().get("Sunday")); 17} 18 }
BiMap的值鍵對的Map可以通過inverse()方法得到。
BiMap的常用實現有:
- HashBiMap: key 集合與 value 集合都有 HashMap 實現
- EnumBiMap: key 與 value 都必須是 enum 型別
- ImmutableBiMap: 不可修改的 BiMap
google guava的Multimaps:一鍵多值的Map
有時候我們需要這樣的資料型別Map<String,Collection<String>>,guava中的Multimap就是為了解決這類問題的。
Multimap的實現
Multimap提供了豐富的實現,所以你可以用它來替代程式裡的Map<K, Collection<V>>,具體的實現如下:
實現 | Key實現 | Value實現 |
ArrayListMultimap | HashMap | ArrayList |
HashMultimap | HashMap | HashSet |
LinkedListMultimap | LinkedHashMap | LinkedList |
LinkedHashMultimap | LinkedHashMap | LinkedHashSet |
TreeMultimap | TreeMap | TreeSet |
ImmutableListMultimap | ImmutableMap | ImmutableList |
ImmutableSetMultimap | ImmutableMap | ImmutableSet |
我們通過一個示例來了解Multimap的使用方法:
1 public class MutliMapTest { 2public static void main(String... args) { 3Multimap<String, String> myMultimap = ArrayListMultimap.create(); 4 5// 新增鍵值對 6myMultimap.put("Fruits", "Bannana"); 7//給Fruits元素新增另一個元素 8myMultimap.put("Fruits", "Apple"); 9myMultimap.put("Fruits", "Pear"); 10myMultimap.put("Vegetables", "Carrot"); 11 12// 獲得multimap的size 13int size = myMultimap.size(); 14System.out.println(size);// 4 15 16// 獲得Fruits對應的所有的值 17Collection<string> fruits = myMultimap.get("Fruits"); 18System.out.println(fruits); // [Bannana, Apple, Pear] 19 20Collection<string> vegetables = myMultimap.get("Vegetables"); 21System.out.println(vegetables); // [Carrot] 22 23//遍歷Mutlimap 24for(String value : myMultimap.values()) { 25System.out.println(value); 26} 27 28// Removing a single value 29myMultimap.remove("Fruits","Pear"); 30System.out.println(myMultimap.get("Fruits")); // [Bannana, Pear] 31 32// Remove all values for a key 33myMultimap.removeAll("Fruits"); 34System.out.println(myMultimap.get("Fruits")); // [] (Empty Collection!) 35 } 36 }
google guava集合之Table
在guava庫中還提供了一種二維表結構:Table。使用Table可以實現二維矩陣的資料結構,可以是稀溜矩陣。
我們看一個使用示例:
1 public class TableDemo { 2public static void main(String[] args) { 3Table<Integer, Integer, String> table = HashBasedTable.create(); 4for (int row = 0; row < 10; row++) { 5for (int column = 0; column < 5; column++) { 6table.put(row, column, "value of cell (" + row + "," + column + ")"); 7} 8} 9for (int row=0;row<table.rowMap().size();row++) { 10Map<Integer,String> rowData = table.row(row); 11for (int column =0;column < rowData.size(); column ++) { 12System.out.println("cell(" + row + "," + column + ") value is:" + rowData.get(column)); 13} 14} 15} 16 }
在上面示例中我們通過HashBasedTable建立了一個行型別為Integer,列型別也為Integer,值為String的Table。然後我們使用put方法向Table中添加了一些值,然後顯示這些值
Guava集合:使用Iterators簡化Iterator操作
Iterators是Guava中對Iterator迭代器操作的幫助類,這個類提供了很多有用的方法來簡化Iterator的操作。
1. 判斷迭代器中的元素是否都滿足某個條件 all 方法
1List<String> list = Lists.newArrayList("Apple","Pear","Peach","Banana"); 2 3Predicate<String> condition = new Predicate<String>() { 4@Override 5public boolean apply(String input) { 6return ((String)input).startsWith("P"); 7} 8}; 9boolean allIsStartsWithP = Iterators.all(list.iterator(), condition); 10System.out.println("all result == " + allIsStartsWithP);
all方法的第一個引數是Iterator,第二個引數是Predicate<String>的實現,這個方法的意義是不需要我們自己去寫while迴圈了,他的內部實現中幫我們做了迴圈,把迴圈體中的條件判斷抽象出來了。
2. 通過any判斷迭代器中是否有一個滿足條件的記錄,any方法的引數和all方法一樣,就不再具體舉例了
3. get方法獲得迭代器中的第x個元素
String secondElement = Iterators.get(list.iterator(), 1);
4. filter方法過濾符合條件的項
1Iterator<String> startPElements = Iterators.filter(list.iterator(), new Predicate<String>() { 2@Override 3public boolean apply(String input) { 4return input.startsWith("P"); 5} 6});
filter方法的第一個引數是源迭代器,第二個引數是Predicate的實現,其apply方法會返回當前元素是否符合條件。
5. find方法返回符合條件的第一個元素
1String length5Element = Iterators.find(list.iterator(), new Predicate<String>() { 2@Override 3public boolean apply(String input) { 4return input.length() == 5; 5} 6});
6. transform方法,對迭代器元素做轉換
1Iterator<Integer> countIterator = Iterators.transform(list.iterator(), new Function<String, Integer>() { 2@Override 3public Integer apply(String input) { 4return input.length(); 5} 6});
上面的例子中我們將字串轉換成了其長度,transform方法輸出的是另外一個Iterator.