Guava - 拯救垃圾代碼,寫出優雅高效,效率提高N倍

Google Guava

最近在看一個同窗代碼的時候,發現代碼中大量使用了 Google 開源的 Guava 核心庫中的內容,讓代碼簡單清晰了很多,故學習分享出 Guava 中我認爲最實用的功能。

Guava 項目是 Google 公司開源的 Java 核心庫,它主要是包含一些在 Java 開發中常用到的功能,如數據校驗不可變集合、計數集合,集合加強操做、I/O、緩存、字符串操做等。而且 Guava 普遍用於 Google 內部的 Java 項目中,也被其餘公司普遍使用,甚至在新版 JDK 中直接引入了 Guava 中的優秀類庫,因此質量毋庸置疑。html

使用方式直接 mavan 依賴引入。java

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.0-jre</version>
</dependency>

<!-- more -->git

數據校驗

數據校驗說來十分簡單,一是非空判斷,二是預期值判斷。非空判斷我想每個 Java 開發者都很熟悉,一開始都常常和 NullPointException 打交道。處理的方式咱們天然是一個 if( xx == null) 就能輕鬆解決。預期值判斷也是相似,檢查數據值是否是本身想要的結果便可。程序員

即便這麼簡單的操做,咱們是否是還常常出錯呢?並且寫起來的代碼老是一行判斷一行異常拋出,怎麼看都以爲那麼優雅。還好,如今就來嘗試第一次使用 Guava 吧。github

非空判斷

String param = "未讀代碼";
String name = Preconditions.checkNotNull(param);
System.out.println(name); // 未讀代碼
String param2 = null;
String name2 = Preconditions.checkNotNull(param2); // NullPointerException
System.out.println(name2);

引入了 Guava 後能夠直接使用 Preconditions.checkNotNull 進行非空判斷,好處爲以爲有兩個,一是語義清晰代碼優雅;二是你也能夠自定義報錯信息,這樣若是參數爲空,報錯的信息清晰,能夠直接定位到具體參數。面試

String param2 = null;
String name2 = Preconditions.checkNotNull(param2,"param2 is null");
// java.lang.NullPointerException: param2 is null

預期值判斷

和非空判斷相似,能夠比較當前值和預期值,若是不相等能夠自定義報錯信息拋出。數組

String param = "www.wdbyte.com2";
String wdbyte = "www.wdbyte.com";
Preconditions.checkArgument(wdbyte.equals(param), "[%s] 404 NOT FOUND", param);
// java.lang.IllegalArgumentException: [www.wdbyte.com2] 404 NOT FOUND

是否越界

Preconditions 類還能夠用來檢查數組和集合的元素獲取是否越界。緩存

// Guava 中快速建立ArrayList
List<String> list = Lists.newArrayList("a", "b", "c", "d");
// 開始校驗
int index = Preconditions.checkElementIndex(5, list.size());
// java.lang.IndexOutOfBoundsException: index (5) must be less than size (4)

代碼中快速建立 List 的方式也是 Guava 提供的,後面會詳細介紹 Guava 中集合建立的超多姿式。安全

不可變的集合

建立不可變集合是我我的最喜歡 Guava 的一個緣由,由於建立一個不能刪除、不能修改、不能增長元素的集合實在是太實用了。這樣的集合你徹底不用擔憂發生什麼問題,總的來講有下面幾個優勢:多線程

  1. 線程安全,由於不能修改任何元素,能夠隨意多線程使用且沒有併發問題。
  2. 能夠無憂的提供給第三方使用,反正修改不了。
  3. 減小內存佔用,由於不能改變,因此內部實現能夠最大程度節約內存佔用。
  4. 能夠用做常量集合。

建立方式

說了那麼多,那麼到底怎麼使用呢?趕忙擼起代碼來。

// 建立方式1:of
ImmutableSet<String> immutableSet = ImmutableSet.of("a", "b", "c");
immutableSet.forEach(System.out::println);
// a
// b
// c

// 建立方式2:builder
ImmutableSet<String> immutableSet2 = ImmutableSet.<String>builder()
    .add("hello")
    .add(new String("未讀代碼"))
    .build();
immutableSet2.forEach(System.out::println);
// hello
// 未讀代碼

// 建立方式3:從其餘集合中拷貝建立
ArrayList<String> arrayList = new ArrayList();
arrayList.add("www.wdbyte.com");
arrayList.add("https");
ImmutableSet<String> immutableSet3 = ImmutableSet.copyOf(arrayList);
immutableSet3.forEach(System.out::println);
// www.wdbyte.com
// https

均可以正常打印遍歷結果,可是若是進行增刪改,會直接報 UnsupportedOperationException .

其實 JDK 中也提供了一個不可變集合,能夠像下面這樣建立。

ArrayList<String> arrayList = new ArrayList();
arrayList.add("www.wdbyte.com");
arrayList.add("https");
// JDK Collections 建立不可變 List
List<String> list = Collections.unmodifiableList(arrayList);
list.forEach(System.out::println);// www.wdbyte.com https
list.add("未讀代碼"); // java.lang.UnsupportedOperationException

注意事項

  1. 使用 Guava 建立的不可變集合是拒絕 null 值的,由於在 Google 內部調查中,95% 的狀況下都不須要放入 null 值。
  2. 使用 JDK 提供的不可變集合建立成功後,原集合添加元素會體如今不可變集合中,而 Guava 的不可變集合不會有這個問題。
List<String> arrayList = new ArrayList<>();
   arrayList.add("a");
   arrayList.add("b");
   List<String> jdkList = Collections.unmodifiableList(arrayList);
   ImmutableList<String> immutableList = ImmutableList.copyOf(arrayList);
   arrayList.add("ccc");
   jdkList.forEach(System.out::println);// result: a b ccc
   System.out.println("-------");
   immutableList.forEach(System.out::println);// result: a b
  1. 若是不可變集合的元素是引用對象,那麼引用對象的屬性是能夠更改的。

其餘不可變集合

不可變集合除了上面演示的 set 以外,還有不少不可變集合,下面是 Guava 中不可變集合和其餘集合的對應關係。

可變集合接口 屬於JDK仍是Guava 不可變版本
Collection JDK ImmutableCollection
List JDK ImmutableList
Set JDK ImmutableSet
SortedSet/NavigableSet JDK ImmutableSortedSet
Map JDK ImmutableMap
SortedMap JDK ImmutableSortedMap
Multiset Guava ImmutableMultiset
SortedMultiset Guava ImmutableSortedMultiset
Multimap Guava ImmutableMultimap
ListMultimap Guava ImmutableListMultimap
SetMultimap Guava ImmutableSetMultimap
BiMap Guava ImmutableBiMap
ClassToInstanceMap Guava ImmutableClassToInstanceMap
Table Guava ImmutableTable

集合操做工廠

其實這裏只會介紹一個建立方法,可是爲何仍是單獨拿出來介紹了呢?看下去你就會大呼好用。雖然 JDK 中已經提供了大量的集合相關的操做方法,用起來也是很是的方便,可是 Guava 仍是增長了一些十分好用的方法,保證讓你用上一次就愛不釋手,

建立集合。

// 建立一個 ArrayList 集合
List<String> list1 = Lists.newArrayList();
// 建立一個 ArrayList 集合,同時塞入3個數據
List<String> list2 = Lists.newArrayList("a", "b", "c");
// 建立一個 ArrayList 集合,容量初始化爲10
List<String> list3 = Lists.newArrayListWithCapacity(10);

LinkedList<String> linkedList1 = Lists.newLinkedList();
CopyOnWriteArrayList<String> cowArrayList = Lists.newCopyOnWriteArrayList();

HashMap<Object, Object> hashMap = Maps.newHashMap();
ConcurrentMap<Object, Object> concurrentMap = Maps.newConcurrentMap();
TreeMap<Comparable, Object> treeMap = Maps.newTreeMap();

HashSet<Object> hashSet = Sets.newHashSet();
HashSet<String> newHashSet = Sets.newHashSet("a", "a", "b", "c");

Guava 爲每個集合都添加了工廠方法建立方式,上面已經展現了部分集合的工廠方法建立方式。是否是十分的好用呢。並且能夠在建立時直接扔進去幾個元素,這個簡直太讚了,不再用一個個 add 了。

集合交集並集差集

過於簡單,直接看代碼和輸出結果吧。

Set<String> newHashSet1 = Sets.newHashSet("a", "a", "b", "c");
Set<String> newHashSet2 = Sets.newHashSet("b", "b", "c", "d");

// 交集
SetView<String> intersectionSet = Sets.intersection(newHashSet1, newHashSet2);
System.out.println(intersectionSet); // [b, c]

// 並集
SetView<String> unionSet = Sets.union(newHashSet1, newHashSet2);
System.out.println(unionSet); // [a, b, c, d]

// newHashSet1 中存在,newHashSet2 中不存在
SetView<String> setView = Sets.difference(newHashSet1, newHashSet2);
System.out.println(setView); // [a]

有數量的集合

這個真的太有用了,由於咱們常常會須要設計能夠計數的集合,或者 value 是 ListMap 集合,若是說你不太明白,看下面這段代碼,是否某天夜裏你也這樣寫過。

  1. 統計相同元素出現的次數(下面的代碼我已經儘量精簡寫法了)。

    JDK 原生寫法:

// Java 統計相同元素出現的次數。
   List<String> words = Lists.newArrayList("a", "b", "c", "d", "a", "c");
   Map<String, Integer> countMap = new HashMap<String, Integer>();
   for (String word : words) {
       Integer count = countMap.get(word);
       count = (count == null) ? 1 : ++count;
       countMap.put(word, count);
   }
   countMap.forEach((k, v) -> System.out.println(k + ":" + v));
   /**
    * result:
    * a:2
    * b:1
    * c:2
    * d:1
    */

儘管已經儘可能優化代碼,代碼量仍是很多的,那麼在 Guava 中有什麼不同呢?在 Guava. 中主要是使用 HashMultiset 類,看下面。

ArrayList<String> arrayList = Lists.newArrayList("a", "b", "c", "d", "a", "c");
   HashMultiset<String> multiset = HashMultiset.create(arrayList);
   multiset.elementSet().forEach(s -> System.out.println(s + ":" + multiset.count(s)));
   /**
    * result:
    * a:2
    * b:1
    * c:2
    * d:1
    */

是的,只要把元素添加進去就好了,不用在意是否重複,最後均可以使用 count 方法統計重複元素數量。看着舒服,寫着優雅,HashMultiset 是 Guava 中實現的 Collection 類,能夠輕鬆統計元素數量。

  1. 一對多,value 是 ListMap 集合。

    假設一個場景,須要把不少動物按照種類進行分類,我相信最後你會寫出相似的代碼。

    JDK 原生寫法:

HashMap<String, Set<String>> animalMap = new HashMap<>();
   HashSet<String> dogSet = new HashSet<>();
   dogSet.add("旺財");
   dogSet.add("大黃");
   animalMap.put("狗", dogSet);
   HashSet<String> catSet = new HashSet<>();
   catSet.add("加菲");
   catSet.add("湯姆");
   animalMap.put("貓", catSet);
   System.out.println(animalMap.get("貓")); // [加菲, 湯姆]

最後一行查詢貓獲得了貓類的 "加菲" 和 」湯姆「。這個代碼簡直太煩作了,若是使用 Guava 呢?

// use guava
   HashMultimap<String, String> multimap = HashMultimap.create();
   multimap.put("狗", "大黃");
   multimap.put("狗", "旺財");
   multimap.put("貓", "加菲");
   multimap.put("貓", "湯姆");
   System.out.println(multimap.get("貓")); // [加菲, 湯姆]

HashMultimap 能夠扔進去重複的 key 值,最後獲取時能夠獲得全部的 value 值,能夠看到輸出結果和 JDK 寫法上是同樣的,可是代碼已經無比清爽。

字符串操做

做爲開發中最長使用的數據類型,字符串操做的加強可讓開發更加高效。

字符拼接

JDK 8 中其實已經內置了字符串拼接方法,可是它只是簡單的拼接,沒有額外操做,好比過濾掉 null 元素,去除先後空格等。先看一下 JDK 8 中字符串拼接的幾種方式。

// JDK 方式一
ArrayList<String> list = Lists.newArrayList("a", "b", "c", null);
String join = String.join(",", list);
System.out.println(join); // a,b,c,null
// JDK 方式二
String result = list.stream().collect(Collectors.joining(","));
System.out.println(result); // a,b,c,null
// JDK 方式三
StringJoiner stringJoiner = new StringJoiner(",");
list.forEach(stringJoiner::add);
System.out.println(stringJoiner.toString()); // a,b,c,null

能夠看到 null 值也被拼接到了字符串裏,這有時候不是咱們想要的,那麼使用 Guava 有什麼不同呢?

ArrayList<String> list = Lists.newArrayList("a", "b", "c", null);
String join = Joiner.on(",").skipNulls().join(list);
System.out.println(join); // a,b,c

String join1 = Joiner.on(",").useForNull("空值").join("旺財", "湯姆", "傑瑞", null);
System.out.println(join1); // 旺財,湯姆,傑瑞,空值

能夠看到使用 skipNulls() 能夠跳過空值,使用 useFornull(String) 能夠爲空值自定義顯示文本。

字符串分割

JDK 中是自帶字符串分割的,我想你也必定用過,那就是 String 的 split 方法,可是這個方法有一個問題,就是若是最後一個元素爲空,那麼就會丟棄,奇怪的是第一個元素爲空卻不會丟棄,這就十分迷惑,下面經過一個例子演示這個問題。

String str = ",a,,b,";
String[] splitArr = str.split(",");
Arrays.stream(splitArr).forEach(System.out::println);
System.out.println("------");
/**
 *
 * a
 * 
 * b
 * ------
 */

你也能夠本身測試下,最後一個元素不是空,直接消失了。

若是使用 Guava 是怎樣的操做方式呢?Guava 提供了 Splitter 類,而且有一系列的操做方式能夠直觀的控制分割邏輯。

String str = ",a ,,b ,";
Iterable<String> split = Splitter.on(",")
    .omitEmptyStrings() // 忽略空值
    .trimResults() // 過濾結果中的空白
    .split(str);
split.forEach(System.out::println);
/**
 * a
 * b
 */

緩存

在開發中咱們可能須要使用小規模的緩存,來提升訪問速度。這時引入專業的緩存中間件可能又以爲浪費。如今能夠了, Guava 中提供了簡單的緩存類,且能夠根據預計容量、過時時間等自動過時已經添加的元素。即便這樣咱們也要預估好可能佔用的內存空間,以防內存佔用過多。

如今看一下在 Guava 中緩存該怎麼用。

@Test
public void testCache() throws ExecutionException, InterruptedException {

    CacheLoader cacheLoader = new CacheLoader<String, Animal>() {
        // 若是找不到元素,會調用這裏
        @Override
        public Animal load(String s) {
            return null;
        }
    };
    LoadingCache<String, Animal> loadingCache = CacheBuilder.newBuilder()
        .maximumSize(1000) // 容量
        .expireAfterWrite(3, TimeUnit.SECONDS) // 過時時間
        .removalListener(new MyRemovalListener()) // 失效監聽器
        .build(cacheLoader); //
    loadingCache.put("狗", new Animal("旺財", 1));
    loadingCache.put("貓", new Animal("湯姆", 3));
    loadingCache.put("狼", new Animal("灰太狼", 4));

    loadingCache.invalidate("貓"); // 手動失效

    Animal animal = loadingCache.get("狼");
    System.out.println(animal);
    Thread.sleep(4 * 1000);
    // 狼已經自動過去,獲取爲 null 值報錯
    System.out.println(loadingCache.get("狼"));
    /**
     * key=貓,value=Animal{name='湯姆', age=3},reason=EXPLICIT
     * Animal{name='灰太狼', age=4}
     * key=狗,value=Animal{name='旺財', age=1},reason=EXPIRED
     * key=狼,value=Animal{name='灰太狼', age=4},reason=EXPIRED
     *
     * com.google.common.cache.CacheLoader$InvalidCacheLoadException: CacheLoader returned null for key 狼.
     */
}

/**
 * 緩存移除監聽器
 */
class MyRemovalListener implements RemovalListener<String, Animal> {

    @Override
    public void onRemoval(RemovalNotification<String, Animal> notification) {
        String reason = String.format("key=%s,value=%s,reason=%s", notification.getKey(), notification.getValue(), notification.getCause());
        System.out.println(reason);
    }
}

class Animal {
    private String name;
    private Integer age;

    @Override
    public String toString() {
        return "Animal{" +
            "name='" + name + '\'' +
            ", age=" + age +
            '}';
    }

    public Animal(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

這個例子中主要分爲 CacheLoader、MyRemovalListener、LoadingCache。

CacheLoader 中重寫了 load 方法,這個方法會在查詢緩存沒有命中時被調用,我這裏直接返回了 null,其實這樣會在沒有命中時拋出 CacheLoader returned null for key 異常信息。

MyRemovalListener 做爲緩存元素失效時的監聽類,在有元素緩存失效時會自動調用 onRemoval 方法,這裏須要注意的是這個方法是同步方法,若是這裏耗時較長,會阻塞直處處理完成。

LoadingCache 就是緩存的主要操做對象了,經常使用的就是其中的 putget 方法了。

總結

上面介紹了我認爲最經常使用的 Guava 功能,Guava 做爲 Google 公司開源的 Java 開發核心庫,我的以爲實用性仍是很高的。引入後不只能快速的實現一些開發中經常使用的功能,並且還可讓代碼更加的優雅簡潔。我以爲適用於每個 Java 項目。Guava 的其餘的功能你也能夠本身去發現。它的 Github 地址是:https://github.com/google/guava.

參考

  1. https://github.com/google/guava/wiki

訂閱

文章已經收錄在 Github.com/niumoo/JavaNotes ,歡迎Star和指教。更有一線大廠面試點,Java程序員須要掌握的核心知識等文章,也整理了不少個人文字,歡迎 Star 和完善,但願咱們一塊兒變得優秀。

文章每週持續更新,有幫助能夠點個「」或「分享」,都是支持,我都喜歡!

要實時關注更新的文章以及分享的乾貨,能夠關注 未讀代碼 公衆號(下方二維碼)或者個人網站

公衆號

相關文章
相關標籤/搜索