2024/5/6

Guava String Utilities 2

CharMatcher

CharMatcher 用在字元的 trimming, collapsing, removing, retaining

    @Test
    public void char_matchers() {
        String string = "12 ab 34 CD\r\n 56 ef789GH";
        String noControl = CharMatcher.javaIsoControl().removeFrom(string); // remove control characters
        String theDigits = CharMatcher.digit().retainFrom(string); // only the digits
        String spaced = CharMatcher.whitespace().trimAndCollapseFrom(string, ' ');
        // trim whitespace at ends, and replace/collapse whitespace into single spaces
        String noDigits = CharMatcher.javaDigit().replaceFrom(string, "*"); // star out all digits
        String lowerAndDigit = CharMatcher.javaDigit().or(CharMatcher.javaLowerCase()).retainFrom(string);
        // eliminate all characters that aren't digits or lowercase

        assertEquals("12 ab 34 CD 56 ef789GH", noControl);
        assertEquals("123456789", theDigits);
        assertEquals("12 ab 34 CD 56 ef789GH", spaced);
        assertEquals("** ab ** CD\r\n ** ef***GH", noDigits);
        assertEquals("12ab3456ef789", lowerAndDigit);
    }

除了一些已經封裝的methods 以外,通用的 method 有三個

Method Description example
anyOf(CharSequence) 要符合的字元 CharMatcher.anyOf("aeiou")
is(char) 特定的 char
inRange(char, char) 一連串的字元 CharMatcher.inRange('a', 'z')
    @Test
    public void char_matchers2() {
        String string = "12 ab 34 CD\r\n 56";
        String anyOfResult = CharMatcher.anyOf("cdef\r\n").removeFrom(string);
        String isResult = CharMatcher.is('C').removeFrom(string);
        String inRangeResult = CharMatcher.inRange('A', 'Z').removeFrom(string);

        assertEquals("12 ab 34 CD 56", anyOfResult);
        assertEquals("12 ab 34 D\r\n 56", isResult);
        assertEquals("12 ab 34 \r\n 56", inRangeResult);
    }

使用 CharMatcher 的 methods

Method Description
collapseFrom(CharSequence, char) 將一連串的字元,縮小變成一個  ex: WHITESPACE.collapseFrom(string, ' ') 會減少為只有一個空白字元
matchesAllOf(CharSequence) 符合所有字元
removeFrom(CharSequence) 移除符合字元
retainFrom(CharSequence) 保留符合的字元
trimFrom(CharSequence) 去掉 leading, trailing 符合的字元
replaceFrom(CharSequence, CharSequence) 取代字元

Charsets

注意 "不要" 這樣寫

try {
  bytes = string.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
  // how can this possibly happen?
  throw new AssertionError(e);
}

要改用 Charsets

bytes = string.getBytes(Charsets.UTF_8);

CaseFormat

Format Example
LOWER_CAMEL lowerCamel
LOWER_HYPHEN lower-hyphen
LOWER_UNDERSCORE lower_underscore
UPPER_CAMEL UpperCamel
UPPER_UNDERSCORE UPPER_UNDERSCORE
CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, "CONSTANT_NAME"));
// returns "constantName"

References

StringsExplained · google/guava Wiki · GitHub

2024/4/29

Guava String Utilities 1

Joiner

可連接多個 string,同時避免遇到 null 會發生問題

joiner configuration methods 永遠會回傳新的 Joiner 物件,因此 Joiner 是 thread safe 的

    @Test
    public void joiner() {
        Joiner joiner = Joiner.on("; ").skipNulls();
        String joinresult = joiner.join("Harry", null, "Ron", "Hermione");

        assertEquals("Harry; Ron; Hermione", joinresult);

        Joiner joiner2 = Joiner.on("; ").useForNull("?");
        String joinresult2 = joiner2.join("Harry", null, "Ron", "Hermione");
        assertEquals("Harry; ?; Ron; Hermione", joinresult2);

        String joinresult3 = Joiner.on(",").join(Arrays.asList(1, 5, 7));
        assertEquals("1,5,7", joinresult3);
    }

Splitter

原本的 String.split 有些奇怪的回傳結果,Splitter提供多個 method,可以自己決定回傳的結果內容。另外 Splitter 可以套用 Pattern, char, Stirng 或 CharMatcher 作為分割標準

Factory method

Method Description Example
Splitter.on(char) 以字元分割 Splitter.on(';')
Splitter.on(CharMatcher) 以一組字元分割 Splitter.on(CharMatcher.BREAKING_WHITESPACE)
Splitter.on(CharMatcher.anyOf(";,."))
Splitter.on(String) 以字串分割 Splitter.on(", ")
Splitter.on(Pattern) 以 regular expression 分割 Splitter.onPattern("\r?\n")
Splitter.onPattern(String)
Splitter.fixedLength(int) 固定長度分割 Splitter.fixedLength(3)

Modifier methods

Method Description Example Result
omitEmptyString() 忽略 empty string Splitter.on(',').omitEmptyStrings().split("a,,c,d") "a", "c", "d"
trimResults() 去掉 whitespace Splitter.on(',').trimResults().split("a, b, c, d") "a", "b", "c", "d"
trimResults(CharMatcher) 去掉一組字元(最前面&最後面) Splitter.on(',').trimResults(CharMatcher.is('')).split("_a ,_b ,c__") "a ", "b_ ", "c"
limit(int) 最多只分割為 int 個字串後,就不再分割 Splitter.on(',').limit(3).split("a,b,c,d") "a", "b", "c,d"

從 split() 改用 splitToList 會回傳 List

@Test
    public void splitter() {
        String[] split1 = ",a,,b,".split(",");
        // 原生 split 會自動去掉最後一個 ""
        // "", "a", "", "b"
        assertEquals("[, a, , b]", Arrays.toString(split1));

        Iterable<String> split2 =
                Splitter.on(',')
                .trimResults()
                .omitEmptyStrings()
                .split("foo,bar,,   qux");
        // "foo", "bar", "qux"
        assertEquals("[foo, bar, qux]", split2.toString());

        Iterable<String> split3 = Splitter.on(',').omitEmptyStrings().split("a,,c,d");
        // "a", "c", "d"
        assertEquals("[a, c, d]", split3.toString());

        Iterable<String> split4 = Splitter.on(',').trimResults().split("a, b, c, d");
        // "a", "b", "c", "d"
        assertEquals("[a, b, c, d]", split4.toString());

        Iterable<String> split5 = Splitter.on(',').trimResults(CharMatcher.is('_')).split("_a ,_b_ ,c__");
        //"a ", "b_ ", "c"
        assertEquals("[a , b_ , c]", split5.toString());

        Iterable<String> split6 = Splitter.on(',').limit(3).split("a,b,c,d");
        //"a", "b", "c,d"
        assertEquals("[a, b, c,d]", split6.toStringa
());


        // 改用 splitToList 會回傳 List<String>
        List<String> split7 = Splitter.on(',').omitEmptyStrings().splitToList("a,,c,d");
    }

How to use Joiner Splitter

Convert List into String Using Joiner

    @Test
    public void whenConvertListToString_thenConverted() {
        List<String> names = Lists.newArrayList("Taipei", "Taichung", "Kaohsiung");
        String result = Joiner.on(",").join(names);

        assertEquals(result, "Taipei,Taichung,Kaohsiung");
    }

Convert Map to String Using Joiner

    @Test
    public void whenConvertMapToString_thenConverted() {
        Map<String, Integer> salary = Maps.newHashMap();
        salary.put("Taipei", 1000);
        salary.put("Taichung", 1500);
        String result = Joiner.on(" , ").withKeyValueSeparator(" = ")
                .join(salary);

        assertTrue(result.contains("Taipei = 1000"));
        assertTrue(result.contains("Taichung = 1500"));
    }

Join Nested Collections

    @Test
    public void whenJoinNestedCollections_thenJoined() {
        List<ArrayList<String>> nested = Lists.newArrayList(
                Lists.newArrayList("apple", "banana", "orange"),
                Lists.newArrayList("cat", "dog", "bird"),
                Lists.newArrayList("John", "Jane", "Adam"));
        String result = Joiner.on(";").join(Iterables.transform(nested,
                new com.google.common.base.Function<List<String>, String>() {
                    @Override
                    public String apply(List<String> input) {
                        return Joiner.on("-").join(input);
                    }
                }));

        assertTrue(result.contains("apple-banana-orange"));
        assertTrue(result.contains("cat-dog-bird"));
        assertTrue(result.contains("John-Jane-Adam"));
    }

Handle Null Values While Using Joiner

    @Test
    public void whenConvertListToStringAndSkipNull_thenConverted() {
        List<String> names = Lists.newArrayList("John", null, "Jane", "Adam", "Tom");
        String result = Joiner.on(",").skipNulls().join(names);

        assertEquals(result, "John,Jane,Adam,Tom");
    }

    @Test
    public void whenUseForNull_thenUsed() {
        List<String> names = Lists.newArrayList("John", null, "Jane", "Adam", "Tom");
        String result = Joiner.on(",").useForNull("nameless").join(names);

        assertEquals(result, "John,nameless,Jane,Adam,Tom");
    }

Create Map From String Using Splitter

    @Test
    public void whenCreateMapFromString_thenCreated() {
        String input = "John=first,Adam=second";
        Map<String, String> result = Splitter.on(",")
                .withKeyValueSeparator("=")
                .split(input);

        assertEquals("first", result.get("John"));
        assertEquals("second", result.get("Adam"));
    }

Split String With Multiple Separators

    @Test
    public void whenSplitStringOnMultipleSeparator_thenSplit() {
        String input = "apple.banana,,orange,,.";
        List<String> result = Splitter.onPattern("[.,]")
                .omitEmptyStrings()
                .splitToList(input);

        assertTrue(result.contains("apple"));
        assertTrue(result.contains("banana"));
        assertTrue(result.contains("orange"));
    }

Split a String at Specific Length

    @Test
    public void whenSplitStringOnSpecificLength_thenSplit() {
        String input = "Hello world";
        List<String> result = Splitter.fixedLength(3).splitToList(input);
        //Hel", "lo ", "wor", "ld"
        assertEquals("[Hel, lo , wor, ld]", result.toString());
    }

Limit the Split Result

    @Test
    public void whenLimitSplitting_thenLimited() {
        String input = "a,b,c,d,e";
        List<String> result = Splitter.on(",")
                .limit(4)
                .splitToList(input);

        assertEquals(4, result.size());
        // ["a", "b", "c", "d,e"]
        assertEquals("[a, b, c, d,e]", result.toString());
    }

References

StringsExplained · google/guava Wiki · GitHub

Guava - Join and Split Collections | Baeldung

2024/4/22

Guava Cache

Guava Cache 提供了基本操作,eviction policies,refresh cache 這些功能

How to Use

    @Test
    public void whenCacheMiss_thenValueIsComputed() {
        // caching the uppercase form of String instances
        CacheLoader<String, String> loader;
        loader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return key.toUpperCase();
            }
        };

        LoadingCache<String, String> cache;
        cache = CacheBuilder.newBuilder().build(loader);

        // getUnchecked,會透過 CacheLoader 的 load 載入 cache 裡面,並取得 cache 的結果
        assertEquals(0, cache.size());
        assertEquals("HELLO", cache.getUnchecked("hello"));
        assertEquals(1, cache.size());
        assertEquals("HELLO", cache.getUnchecked("Hello"));
        assertEquals(2, cache.size());
    }

Eviction Policies

  • cache size

  • cache size with weight function

  • idle time

  • total live time

    @Test
    public void whenCacheReachMaxSize_thenEviction() {
        // 設定 cache 的 maximumSize(3)
        CacheLoader<String, String> loader = createLoader();
        LoadingCache<String, String> cache;
        cache = CacheBuilder.newBuilder().maximumSize(3).build(loader);

        cache.getUnchecked("first");
        cache.getUnchecked("second");
        cache.getUnchecked("third");
        // 放入第四個,會將第一個 cached 資料刪除
        cache.getUnchecked("forth");
        assertEquals(3, cache.size());
        assertNull(cache.getIfPresent("first"));
        assertEquals("FORTH", cache.getIfPresent("forth"));
    }

    @Test
    public void whenCacheReachMaxWeight_thenEviction() {
        CacheLoader<String, String> loader = createLoader();

        // 定義 Weigher 的 weight function
        // 以 cached object 的 value 的字串長度,作為 weight
        Weigher<String, String> weighByLength;
        weighByLength = new Weigher<String, String>() {
            @Override
            public int weigh(String key, String value) {
                return value.length();
            }
        };

        // maximumWeight 設定 cache 的 weight 總和限制
        // weigher 指定 weight function class
        LoadingCache<String, String> cache;
        cache = CacheBuilder.newBuilder()
                .maximumWeight(15)
                .weigher(weighByLength)
                .build(loader);

        cache.getUnchecked("first");
        cache.getUnchecked("second");
        cache.getUnchecked("third");
        // 要加入 third 時,會檢查 weight 總和,這邊總和會變成 16 就超過上限 15,因此 first 會被刪除
        assertEquals(2, cache.size());
        cache.getUnchecked("last");
        assertEquals(3, cache.size());
        assertNull(cache.getIfPresent("first"));
        assertEquals("LAST", cache.getIfPresent("last"));
    }

    @Test
    public void time_thenEviction()
            throws InterruptedException {
        CacheLoader<String, String> loader = createLoader();

        // 當 cached object 已經 idle 2ms 後,就被移除
        // expireAfterAccess
        LoadingCache<String, String> cache;
        cache = CacheBuilder.newBuilder()
                .expireAfterAccess(2, TimeUnit.MILLISECONDS)
                .build(loader);

        cache.getUnchecked("hello");
        assertEquals(1, cache.size());

        cache.getUnchecked("hello");
        Thread.sleep(300);

        cache.getUnchecked("test");
        assertEquals(1, cache.size());
        assertNull(cache.getIfPresent("hello"));

        /////
        // 當 cached object 儲存 2ms 後,就被移除
        // expireAfterWrite
        LoadingCache<String, String> cache2;
        cache2 = CacheBuilder.newBuilder()
                .expireAfterWrite(2,TimeUnit.MILLISECONDS)
                .build(loader);

        cache2.getUnchecked("hello");
        assertEquals(1, cache2.size());
        Thread.sleep(300);
        cache2.getUnchecked("test");
        assertEquals(1, cache2.size());
        assertNull(cache2.getIfPresent("hello"));
    }

    CacheLoader<String, String> createLoader() {
        CacheLoader<String, String> loader;
        loader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return key.toUpperCase();
            }
        };
        return loader;
    }

Weak Keys

    @Test
    public void whenWeakKeyHasNoRef_thenRemoveFromCache() {
        CacheLoader<String, String> loader;
        loader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return key.toUpperCase();
            }
        };

        // weakKeys 定義 cache 的 key 是用 weakReference
        // garbage collector 可在 key 不被 referenced 的時候,將 key 回收
        LoadingCache<String, String> cache;
        cache = CacheBuilder.newBuilder().weakKeys().build(loader);
    }

Soft Values

    @Test
    public void whenSoftValue_thenRemoveFromCache() {
        CacheLoader<String, String> loader;
        loader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return key.toUpperCase();
            }
        };

        // softValues
        // 可讓 garbage collector 回收 cached values
        // 但如果使用太多 soft references 會影響效能,故還是建議使用 maxmimumSize
        LoadingCache<String, String> cache;
        cache = CacheBuilder.newBuilder().softValues().build(loader);
    }

null Values

    @Test
    public void whenNullValue_thenOptional() {
        // 如果要 laod null value,預設會 throw exception
        // 但如果一定要使用 null value,可利用 Optional
        CacheLoader<String, Optional<String>> loader;
        loader = new CacheLoader<String, Optional<String>>() {
            @Override
            public Optional<String> load(String key) {
                return Optional.fromNullable(getSuffix(key));
            }
        };

        LoadingCache<String, Optional<String>> cache;
        cache = CacheBuilder.newBuilder().build(loader);

        assertEquals("txt", cache.getUnchecked("text.txt").get());
        assertFalse(cache.getUnchecked("hello").isPresent());
    }
    private String getSuffix(final String str) {
        int lastIndex = str.lastIndexOf('.');
        if (lastIndex == -1) {
            return null;
        }
        return str.substring(lastIndex + 1);
    }

refresh Cache

更新 cache value 的方法,分為 manual/auto 兩種

    @Test
    public void whenLiveTimeEnd_thenRefresh() {
        CacheLoader<String, String> loader;
        loader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return key.toUpperCase();
            }
        };

        LoadingCache<String, String> cache0;
        cache0 = CacheBuilder.newBuilder().build(loader);
        cache0.getUnchecked("hello");
        try {
            // get 會取得舊的 cachec value
            String value = cache0.get("hello");
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }
        // 呼叫 refresh,(非同步) 更新 cached value
        cache0.refresh("hello");

        //////////////
        // CacheBuilder.refreshAfterWrite(duration) 可自動 refresh cached value
        // 如果 1min 後沒有 get 這個 key,當 cached value 有舊的值,其他 threads 會回傳舊值
        // 如果 cached value 沒有值,就更新 value,其他 threads 會等待
        LoadingCache<String, String> cache;
        cache = CacheBuilder.newBuilder()
                .refreshAfterWrite( 1,TimeUnit.MINUTES)
                .build(loader);
    }

Preload the Cache

    @Test
    public void whenPreloadCache_thenUsePutAll() {
        CacheLoader<String, String> loader;
        loader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return key.toUpperCase();
            }
        };

        LoadingCache<String, String> cache;
        cache = CacheBuilder.newBuilder().build(loader);

        // 透過 Map 的 putAll,一次加入多個 cache values
        Map<String, String> map = new HashMap<String, String>();
        map.put("first", "FIRST");
        map.put("second", "SECOND");
        cache.putAll(map);

        assertEquals(2, cache.size());
    }

RemovalNotification

    @Test
    public void whenEntryRemovedFromCache_thenNotify() {
        CacheLoader<String, String> loader;
        loader = new CacheLoader<String, String>() {
            @Override
            public String load(final String key) {
                return key.toUpperCase();
            }
        };

        // RemovalListener 可在 cachec value 被移除時,收到通知
        RemovalListener<String, String> listener;
        listener = new RemovalListener<String, String>() {
            @Override
            public void onRemoval(RemovalNotification<String, String> n){
                if (n.wasEvicted()) {
                    String cause = n.getCause().name();
                    assertEquals(RemovalCause.SIZE.toString(),cause);
                }
            }
        };

        LoadingCache<String, String> cache;
        cache = CacheBuilder.newBuilder()
                .maximumSize(3)
                .removalListener(listener)
                .build(loader);

        cache.getUnchecked("first");
        cache.getUnchecked("second");
        cache.getUnchecked("third");
        cache.getUnchecked("last");
        assertEquals(3, cache.size());
    }

Others

  • Cache 是 thread-safe

  • 可透過 put(key,value) ,不透過 CacheLoader 直接寫入 cache

    cache.put("key", "value");
    assertEquals("value", cache.getUnchecked("key"));
  • 可用 CacheStats ( hitRate()missRate(), ..) 量測 performance

References

Guava Cache | Baeldung

CachesExplained · google/guava Wiki · GitHub

2024/4/15

Guava Collection Utilities: Maps, Multisets, Multimaps, Tables

Maps

    @Test
    public void maps() {
        Map<String, Integer> left = ImmutableMap.of("a", 1, "b", 2, "c", 3);
        Map<String, Integer> right = ImmutableMap.of("b", 2, "c", 4, "d", 5);
        MapDifference<String, Integer> diff = Maps.difference(left, right);

        // 兩個 map 都有出現的 key-value pair
        Map<String, Integer> entriesInCommon = diff.entriesInCommon(); // {"b" => 2}
        assertEquals("{b=2}", entriesInCommon.toString());

        // 有相同的 key,不同的 values
        Map<String, MapDifference.ValueDifference<Integer>> entriesDiffering = diff.entriesDiffering(); // {"c" => (3, 4)}
        assertEquals("{c=(3, 4)}", entriesDiffering.toString());

        // 出現在 left,沒有出現在 right 的 key-value pair
        Map<String, Integer> entriesOnlyOnLeft = diff.entriesOnlyOnLeft(); // {"a" => 1}
        assertEquals("{a=1}", entriesOnlyOnLeft.toString());
        // 出現在 right,沒有出現在 left
        Map<String, Integer> entriesOnlyOnRight = diff.entriesOnlyOnRight(); // {"d" => 5}
        assertEquals("{d=5}", entriesOnlyOnRight.toString());
    }

uniqueIndex

    @Test
    public void maps_uniqueIndex() {
        // nickname屬性能唯一確定一個WebUser
        ArrayList<User> users = Lists.newArrayList(new User(1,"one"),new User(2,"two"),new User(3,"three"),new User(4,"four"));
        // 以 name 為 key,User為值的map
        ImmutableMap<String, User> map = Maps.uniqueIndex(users,new com.google.common.base.Function<User, String>() {
            @Override
            public String apply(User user) {
                return user.getName();
            }
        });
        System.out.println("map:" + map);
        System.out.println("name:" + map.get("two").getName());

        assertEquals("{one=User(id=1,name=one), two=User(id=2,name=two), three=User(id=3,name=three), four=User(id=4,name=four)}", map.toString());
    }
    class User {
        private int id;
        private String name;

        public User(int id, String name) {
            this.id = id;
            this.name = name;
        }

        public String getName() {
            return this.name;
        }

        public String toString() {
            return "User(id="+id+",name="+name+")";
        }
    }

Multisets

    @Test
    public void multisets() {
        Multiset<String> multiset1 = HashMultiset.create();
        multiset1.add("a", 2);

        Multiset<String> multiset2 = HashMultiset.create();
        multiset2.add("a", 5);

        // returns true: all unique elements are contained,
        boolean containsAll = multiset1.containsAll(multiset2);
        assertTrue(containsAll);

        // containsOccurrences(Multiset sup, Multiset sub)
        // if sub.count(o) <= sup.count(o) for all o  -> return true
        // even though multiset1.count("a") == 2 < multiset2.count("a") == 5
        // supCountA=2, subCountA=5
        // System.out.println("multiset1 CountA="+multiset1.count("a")+", multiset2 CountA="+multiset2.count("a"));
        boolean containsOccurrences = Multisets.containsOccurrences(multiset1, multiset2); // returns false
        assertFalse(containsOccurrences);

        // retainOccurrences(Multiset removeFrom, Multiset toRetain)
        // 確保 removeFrom.count(o) <= toRetain.count(o) for all o
        boolean retainOccurrences = Multisets.retainOccurrences(multiset1, multiset2);
        assertFalse(retainOccurrences);
        System.out.println("multiset1 CountA="+multiset1.count("a")+", multiset2 CountA="+multiset2.count("a"));

        // intersection of two multisets
        Multiset<String> intersection = Multisets.intersection(multiset1, multiset2);
        assertEquals(2, intersection.count("a"));

        // removeOccurrences(Multiset removeFrom, Multiset toRemove)
        // 自 removeFrom 中,移除 toRemove 的 element 出現的次數
        Multisets.removeOccurrences(multiset2, multiset1); // multiset2 now contains 3 occurrences of "a"
        assertEquals(3, multiset2.count("a"));

        // 移除 multiset2 中所有的元素
        multiset2.removeAll(multiset1); // removes all occurrences of "a" from multiset2, even though multiset1.count("a") == 2
        assertTrue(multiset2.isEmpty()); // returns true


        Multiset<String> multiset = HashMultiset.create();
        multiset.add("a", 3);
        multiset.add("b", 5);
        multiset.add("c", 1);

        // 次數從大到小的順序,排列 multiset
        ImmutableMultiset<String> highestCountFirst = Multisets.copyHighestCountFirst(multiset);
        assertEquals("[b x 5, a x 3, c]", highestCountFirst.toString());
    }

Multimaps

    @Test
    public void mutlimaps() {
        ImmutableSet<String> digits = ImmutableSet.of(
                "zero", "one", "two", "three", "four",
                "five", "six", "seven", "eight", "nine");
        Function<String, Integer> lengthFunction = new Function<String, Integer>() {
            public Integer apply(String string) {
                return string.length();
            }
        };
        //  Multimaps.index(Iterable, Function)
        // 以 Function apply 的結果分類,將相同字串長度的元素,放在一起
        ImmutableListMultimap<Integer, String> digitsByLength = Multimaps.index(digits, lengthFunction);
        /*
         * digitsByLength maps:
         *  3 => {"one", "two", "six"}
         *  4 => {"zero", "four", "five", "nine"}
         *  5 => {"three", "seven", "eight"}
         */
        assertEquals("{4=[zero, four, five, nine], 3=[one, two, six], 5=[three, seven, eight]}", digitsByLength.toString());

        ////////
        //  Multimaps.invertFrom(Multimap toInvert, Multimap dest)
        //  如果要得到 Immutablemiltimap 就呼叫 ImmutableMultimap.inverse()
        ArrayListMultimap<String, Integer> multimap = ArrayListMultimap.create();
        multimap.putAll("b", Ints.asList(2, 4, 6));
        multimap.putAll("a", Ints.asList(4, 2, 1));
        multimap.putAll("c", Ints.asList(2, 5, 3));

        TreeMultimap<Integer, String> inverse = Multimaps.invertFrom(multimap, TreeMultimap.<Integer, String>create());
        // note that we choose the implementation, so if we use a TreeMultimap, we get results in order
        /*
         * inverse maps:
         *  1 => {"a"}
         *  2 => {"a", "b", "c"}
         *  3 => {"c"}
         *  4 => {"a", "b"}
         *  5 => {"c"}
         *  6 => {"b"}
         */
        assertEquals("{1=[a], 2=[a, b, c], 3=[c], 4=[a, b], 5=[c], 6=[b]}", inverse.toString());

        /////
        // forMap(Map)  將 Map 轉換為 SetMultimap
        // 特別適合用在 Multimaps.invertFrom
        Map<String, Integer> map = ImmutableMap.of("a", 1, "b", 1, "c", 2);
        SetMultimap<String, Integer> multimap2 = Multimaps.forMap(map);
        // multimap maps ["a" => {1}, "b" => {1}, "c" => {2}]
        Multimap<Integer, String> inverse2 = Multimaps.invertFrom(multimap2, HashMultimap.<Integer, String> create());
        // inverse maps [1 => {"a", "b"}, 2 => {"c"}]
        assertEquals("{1=[a, b], 2=[c]}", inverse2.toString());
    }

Tables

    @Test
    public void tables() {
        // Tables.newCustomTable(Map, Supplier<Map>)
        Table<String, Character, Integer> table = Tables.newCustomTable(
                Maps.<String, Map<Character, Integer>>newLinkedHashMap(),
                new Supplier<Map<Character, Integer>>() {
                    public Map<Character, Integer> get() {
                        return Maps.newLinkedHashMap();
                    }
                });
        table.put("a", 'x', 1);
        table.put("a", 'y', 2);
        table.put("a", 'z', 3);
        table.put("b", 'x', 4);
        table.put("b", 'y', 5);
        table.put("b", 'z', 6);
        assertEquals("{a={x=1, y=2, z=3}, b={x=4, y=5, z=6}}", table.toString());

        // transpose 轉置矩陣
        assertEquals("{x={a=1, b=4}, y={a=2, b=5}, z={a=3, b=6}}", Tables.transpose(table).toString());
    }

References

CollectionUtilitiesExplained · google/guava Wiki · GitHub

2024/4/8

Guava Collection Utilities: Lists, Sets

Guava 在 Collection interfaces 有對應的 utility class,這些 utilities 是 static method,可直接使用。

Interface JDK or Guava? Corresponding Guava utility class
Collection JDK Collections2
List JDK Lists
Set JDK Sets
SortedSet JDK Sets
Map JDK Maps
SortedMap JDK Maps
Queue JDK Queues
Multiset Guava Multisets
Multimap Guava Multimaps
BiMap Guava Maps
Table Guava Tables

Static Contructor

    @Test
    public void static_constructor() {
        //JDK 的 list 必須建立後再加入元素
        List<String> list = new ArrayList<>();
        list.add("item1");
        list.add("item2");

        // Guava 在建立物件時,可直接填寫 init data
        Set<String> sets = Sets.newHashSet("alpha", "beta", "gamma");
        List<String> list2 = Lists.newArrayList("alpha", "beta", "gamma");

        // Guava 在 init 時,可直接設定 size
        List<String> exactly100 = Lists.newArrayListWithCapacity(100);
        List<String> approx100 = Lists.newArrayListWithExpectedSize(100);
        Set<String> approx100Set = Sets.newHashSetWithExpectedSize(100);

        // Guava 新的 Collection 類別,不提供 constructor,而是提供一個 factory method: create
        Multiset<String> multiset = HashMultiset.create();
    }

Iteratables

Iterables 封裝了 Iterable,提供 fluent 語法

    @Test
    public void iterables() {
        //// concat
        //// concat 可將兩個 collections 連接在一起
        Iterable<Integer> concatenated = Iterables.concat(
                Ints.asList(1, 2, 3),
                Ints.asList(5, 5, 6));
        // concatenated has elements 1, 2, 3, 4, 5, 6
        assertEquals("[1, 2, 3, 5, 5, 6]", concatenated.toString());

        //// concat 在不同類別的 list,不能連接在一起
//        Iterable<Integer> concatenated2 = Iterables.concat(
//                Ints.asList(1, 2, 3),
//                Lists.newArrayList("alpha", "beta", "gamma"));

        //// getFirst: first element, 第二個參數是 預設值
        //// getLast: last element, 失敗時會 throw NoSuchElementException
        Integer firstElement = Iterables.getFirst(concatenated, null);
        Integer lastAdded = Iterables.getLast(concatenated);
        assertEquals(firstElement.intValue(), 1);
        assertEquals(lastAdded.intValue(), 6);

        /// getOnlyElement: the only element in Iterable
        Iterable<Integer> iterable2 = () -> Arrays.asList(100).iterator();
        Iterable<Integer> iterable3 = Collections.singleton(200);
        Exception exception = assertThrows(IllegalArgumentException.class, () -> {
            Integer theElement = Iterables.getOnlyElement(concatenated);
        });
        assertEquals(Iterables.getOnlyElement(iterable2).intValue(), 100);

        //// frequency:  查詢某個 element 出現的次數
        assertEquals(2, Iterables.frequency(concatenated, 5));

        //// partition:  分割成多個 Iterable
        Iterable<List<Integer>> list = Iterables.partition(concatenated, 2);
//        System.out.println("list="+ list);
        assertEquals("[[1, 2], [3, 5], [5, 6]]", list.toString());
        assertEquals("[1, 2]", Iterables.getFirst(list, null).toString());
    }

Iterables 提供跟 Collection 類似的一些 methods

    @Test
    public void iterables_collection_like() {
        // Iterables 提供這些類似 Collection 的 methods
        // 1. addAll
        // 2. contains
        // 3. removeAll
        // 4. retainAll
        // 5. size
        // 6. toArray
        // 7. isEmpty
        // 8. get
        // 9. toString
        ArrayList<Integer> list1 = Lists.newArrayList(1, 2);
        ArrayList<Integer> list2 = Lists.newArrayList(100, 101);
        boolean changed = Iterables.addAll( list1 , list2);
        assertTrue( changed );
        assertEquals("[1, 2, 100, 101]", list1.toString());

        assertTrue( list1.contains(2) );
        assertEquals(list1.size(), 4);
        assertFalse( list1.isEmpty() );

        assertEquals(1, list1.get(0).intValue());
    }

FluentIterable 有幾個 method,可產生 immutable collection

Result Type Method
ImmutableList toList()
ImmutableList toSortedList()
ImmutableSet toSet()
ImmutableSortedSet toSortedSet()
ImmutableMultiset toMultiset()
ImmutableMap toMap()
    @Test
    public void FluentIterable_immutable() {
        // FluentIterable
        ImmutableList<String> list1 =
                FluentIterable.from( Arrays.asList(1,2,3) )
                .transform( Functions.toStringFunction() )
                .limit(10)
                .toList();
        assertEquals( 3, list1.size());
    }

Lists

    @Test
    public void lists() {
        List<Integer> countUp = Ints.asList(1, 2, 3);

        // Lists.reverse: reverse a list
        List<Integer> countDown = Lists.reverse(countUp); // {3, 2, 1}
        // 將 list 以 size 分割成多個 sublist
        List<List<Integer>> parts = Lists.partition(countUp, 2); // {{1, 2}, {3}}

        assertEquals("[1, 2, 3]", countUp.toString());
        assertEquals("[3, 2, 1]", countDown.toString());
        assertEquals("[[1, 2], [3]]", parts.toString());

        // Lists 可產生 ArrayList 及 LinkedList
        // Lists.newArrayList()
        // Lists.newLinkedList()
        List<Integer> list1 = Lists.newArrayList(1,3,2,4);
        List<Integer> list2 = Lists.newLinkedList(Ints.asList(1, 3, 2));
        assertEquals("[1, 3, 2, 4]", list1.toString());
        assertEquals("[1, 3, 2]", list2.toString());

        // 透過 Collections.max 找到最大的元素, Collections.min 最小的元素
        int maxElement = Collections.max(Arrays.asList(1,3,2,4));
        int minElement = Collections.min(Arrays.asList(1,3,2,4));
        assertEquals(4, maxElement);
        assertEquals(1, minElement);
    }

Sets

    @Test
    public void sets() {
        Set<String> set1 = ImmutableSet.of("a", "b", "c");
        Set<String> set2 = ImmutableSet.of("b", "c", "d");

        // 聯集 union of two sets
        Set<String> union1 = Sets.union(set1, set2);
        // 回傳 SetView,可再轉換為 immutable set 使用
        Sets.SetView<String> union2 = Sets.union(set1, set2);
        ImmutableSet<String> immutableSet = union2.immutableCopy();
        assertEquals("[a, b, c, d]", union1.toString());
        assertEquals("[a, b, c, d]", immutableSet.toString());

        // 交集 intersection of two sets
        Sets.SetView<String> intersection = Sets.intersection(set1, set2);
        // I can use intersection as a Set directly, but copying it can be more efficient if I use it a lot.
        assertEquals("[b, c]", intersection.toString());

        // 差集 difference
        Set<String> diff = Sets.difference(set1, set2);
        assertEquals("[a]", diff.toString());

        // symmetricDifference
        Set<String> diff2 = Sets.symmetricDifference(set1, set2);
        assertEquals("[a, d]", diff2.toString());

        // cartesianProduct: Cartesian Product 笛卡兒積
        Set<List<String>> product = Sets.cartesianProduct(set1, set2);
        assertEquals("[[a, b], [a, c], [a, d], [b, b], [b, c], [b, d], [c, b], [c, c], [c, d]]", product.toString());

        // powerSet: 找到所有可能的 subsets
        Set<Set<String>> powerset = Sets.powerSet(set1);
        assertTrue(powerset.contains(ImmutableSet.of("a")));
        assertTrue(powerset.contains(ImmutableSet.of("b")));
        assertTrue(powerset.contains(ImmutableSet.of("a", "b")));
        assertTrue(powerset.contains(ImmutableSet.of("a", "b", "c")));
        assertEquals(8, powerset.size());
    }

References

CollectionUtilitiesExplained · google/guava Wiki · GitHub