2024/04/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/04/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/04/08

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

2024/04/01

Guava Collection: BiMap, Table, ClassToInstanceMap, RangeSet, RangeMap

BiMap

JDK 的方式,只能用兩個 Maps 分別儲存,並自己維護同步資料

BiMap 就是 Map<K,V>

  • 提供 inverse(),可取得 Map<V, K>

  • 為了提供 inverse(),values 必須要唯一,values() 可得到 Set

    @Test
    public void biMap() {
        // JDK 的方式,兩個 Map 獨立
        Map<String, Integer> nameToId = Maps.newHashMap();
        Map<Integer, String> idToName = Maps.newHashMap();
        nameToId.put("Bob", 42);
        idToName.put(42, "Bob");

        BiMap<String, Integer> userIdNameBiMap = HashBiMap.create();
        userIdNameBiMap.put("Bob", 42);
        userIdNameBiMap.put("Alice", 43);
        System.out.println("");
        System.out.println("userIdNameBiMap: "+userIdNameBiMap);
        String userForId = userIdNameBiMap.inverse().get(42);
        System.out.println("user 42 ForId: "+userForId);

//        userIdNameBiMap: {Bob=42, Alice=43}
//        user 42 ForId: Bob
    }

implementations

Key-Value Map Impl Value-Key Map Impl Corresponding BiMap
HashMap HashMap HashBiMap
ImmutableMap ImmutableMap ImmutableBiMap
EnumMap EnumMap EnumBiMap
EnumMap HashMap EnumHashBiMap

Table

ref: Guide to Guava Table | Baeldung

    @Test
    public void table_create() {
        // HashBasedTable.create()
        // 內部使用 LinkedHashMap
        Table<String, String, Integer> universityCourseSeatTable
                = HashBasedTable.create();
        // 內部使用 TreeMap,natural ordering
        Table<String, String, Integer> universityCourseSeatTableOrdered
                = TreeBasedTable.create();

        // table size 固定時,可使用 ArrayTable
        List<String> universityRowTable
                = Lists.newArrayList("Mumbai", "Harvard");
        List<String> courseColumnTables
                = Lists.newArrayList("Chemical", "IT", "Electrical");
        Table<String, String, Integer> universityCourseSeatTableArrayTable
                = ArrayTable.create(universityRowTable, courseColumnTables);

        // ImmutableTable: immutable table
        Table<String, String, Integer> universityCourseSeatTableImmutable
                = ImmutableTable.<String, String, Integer> builder()
                .put("Mumbai", "Chemical", 120).build();
    }

    @Test
    public void table_using() {
        Table<String, String, Integer> universityCourseSeatTable
                = HashBasedTable.create();
        universityCourseSeatTable.put("Mumbai", "Chemical", 120);
        universityCourseSeatTable.put("Mumbai", "IT", 60);
        universityCourseSeatTable.put("Harvard", "Electrical", 60);
        universityCourseSeatTable.put("Harvard", "IT", 120);

        // get 可取得 row, col  對應的資料
        int seatCount = universityCourseSeatTable.get("Mumbai", "IT");
        Integer seatCountForNoEntry = universityCourseSeatTable.get("Oxford", "IT");
        assertEquals(seatCount, 60);
        assertNull(seatCountForNoEntry);

        ////////////
        // containsXXX  可判斷是否存在
        // 1. row key
        // 2. col key
        // 3. row, col
        // 4 value
        boolean entryIsPresent
                = universityCourseSeatTable.contains("Mumbai", "IT");
        boolean courseIsPresent
                = universityCourseSeatTable.containsColumn("IT");
        boolean universityIsPresent
                = universityCourseSeatTable.containsRow("Mumbai");
        boolean seatCountIsPresent
                = universityCourseSeatTable.containsValue(60);

        assertTrue(entryIsPresent);
        assertTrue(courseIsPresent);
        assertTrue(universityIsPresent);
        assertTrue(seatCountIsPresent);


        ///////
        // 由 col 取得 row, value 的 Map
        Map<String, Integer> universitySeatMap
                = universityCourseSeatTable.column("IT");

        assertEquals(universitySeatMap.size(), 2);
        assertEquals(universitySeatMap.get("Mumbai").intValue(), 60);
        assertEquals(universitySeatMap.get("Harvard").intValue(), 120);

        /////
        // columnMap  取得  Map<UniversityName, Map<CoursesOffered, SeatAvailable>>
        Map<String, Map<String, Integer>> courseKeyUniversitySeatMap
                = universityCourseSeatTable.columnMap();

        assertEquals(courseKeyUniversitySeatMap.size(), 3);
        assertEquals(courseKeyUniversitySeatMap.get("IT").size(), 2);
        assertEquals(courseKeyUniversitySeatMap.get("Electrical").size(), 1);
        assertEquals(courseKeyUniversitySeatMap.get("Chemical").size(), 1);

        ///////
        // 由 row 取得 col, value 的 Map
        Map<String, Integer> courseSeatMap
                = universityCourseSeatTable.row("Mumbai");

        assertEquals(courseSeatMap.size(), 2);
        assertEquals(courseSeatMap.get("IT").intValue(), 60);
        assertEquals(courseSeatMap.get("Chemical").intValue(), 120);

        //////
        // rowKeySet:  row keys
        // columnKeySet:  col keys
        Set<String> universitySet = universityCourseSeatTable.rowKeySet();
        assertEquals(universitySet.size(), 2);
        Set<String> courseSet = universityCourseSeatTable.columnKeySet();
        assertEquals(courseSet.size(), 3);

        /////////
        // remove 會回傳既有的 value 後,移除該 row, col 的 value
        Integer seatCount2 = universityCourseSeatTable.remove("Mumbai", "IT");
        Integer seatCount3 = universityCourseSeatTable.remove("Mumbai", "IT");

        assertEquals(seatCount2.intValue(), 60);
        assertNull(seatCount3);
    }

ClassToInstanceMap

ClassToInstanceMap 是一種特殊的 Map,可確保 keys, values 都是 B 的子類別

ClassToInstanceMap extends Map 介面,並增加兩個 methods: T getInstance(Class) and T putInstance(Class, T) ,這兩個 method 有做型別檢查,並避免 casting

    @Test
    public void create() {
        // 產生 ImmutableClassToInstanceMap

        // 1. using the of() method to create an empty map
        ImmutableClassToInstanceMap map1 = ImmutableClassToInstanceMap.of();

        // 2. using the of(Class<T> type, T value) method to create a single entry map
        ImmutableClassToInstanceMap map2 = ImmutableClassToInstanceMap.of(Save.class, new Save());

        // 3. copyOf()  複製另一個 ImmutableClassToInstanceMap
        ImmutableClassToInstanceMap map3 = ImmutableClassToInstanceMap.copyOf(map2);

        // 4. builder
        ImmutableClassToInstanceMap map4 = ImmutableClassToInstanceMap
                .<Action>builder()
                .put(Save.class, new Save())
                .put(Delete.class, new Delete())
                .build();

        ////////
        // MutableClassToInstanceMap
        // 1. create()
        MutableClassToInstanceMap mmap1 = MutableClassToInstanceMap.create();

        // 2. create(Map<Class<? extends B>, B> backingMap)
        MutableClassToInstanceMap mmap2 = MutableClassToInstanceMap.create(new HashMap());
    }
    interface Action {

    }
    class Save implements Action {
    }

    class Delete implements Action {
    }

    @Test
    public void using() {
        // 增加兩個 method 到 Map interface
        MutableClassToInstanceMap map = MutableClassToInstanceMap
                .create();
        map.put(Save.class, new Save());
        map.put(Delete.class, new Delete());

        // 1. <T extends B> T getInstance(Class<T> type):
        Action saveAction = (Action) map.get(Save.class);
        Delete deleteAction = (Delete) map.getInstance(Delete.class);

        // 2. <T extends B> T putInstance(Class<T> type, @Nullable T value):
        Action newOpen = (Action) map.put(Save.class, new Save());
        Delete newDelete = (Delete) map.putInstance(Delete.class, new Delete());
    }

RangeSet

a set comprising of zero or more non-empty, disconnected ranges

最基本實作 RangeSet 的類別為 TreeRangeSet

    @Test
    public void create() {
        // 1. 直接用 create 產生一個空的 RangeSet
        RangeSet<Integer> numberRangeSet = TreeRangeSet.create();

        // 2. create 時,加上一個 List of Range 參數
        List<Range<Integer>> numberList = Arrays.asList(Range.closed(0, 2));
        RangeSet<Integer> numberRangeSet2 = TreeRangeSet.create(numberList);

        // ImmutableRangeSet 的 builder 產生 ImmutableRangeSet
        //        ImmutableRangeSet.Builder<Integer> builder = ImmutableRangeSet.builder();
        RangeSet<Integer> numberRangeSet3
                = new ImmutableRangeSet.Builder<Integer>().add(Range.closed(0, 2)).build();
    }

    @Test
    public void add_remove_range() {
        // add/remove range
        RangeSet<Integer> numberRangeSet = TreeRangeSet.create();

        numberRangeSet.add(Range.closed(0, 2));
        numberRangeSet.add(Range.closed(3, 5));
        numberRangeSet.add(Range.closed(6, 8));
        numberRangeSet.add(Range.closed(9, 15));
        numberRangeSet.remove(Range.closed(3, 5));
        numberRangeSet.remove(Range.closed(7, 10));

        assertTrue(numberRangeSet.contains(1));
        assertFalse(numberRangeSet.contains(9));
        assertTrue(numberRangeSet.contains(12));
    }

    @Test
    public void range_span() {
        RangeSet<Integer> numberRangeSet = TreeRangeSet.create();

        numberRangeSet.add(Range.closed(0, 2));
        numberRangeSet.add(Range.closed(3, 5));
        numberRangeSet.add(Range.closed(6, 8));
        Range<Integer> experienceSpan = numberRangeSet.span();

        assertEquals(0, experienceSpan.lowerEndpoint().intValue());
        assertEquals(8, experienceSpan.upperEndpoint().intValue());
    }

    @Test
    public void subrange() {
        RangeSet<Integer> numberRangeSet = TreeRangeSet.create();

        numberRangeSet.add(Range.closed(0, 2));
        numberRangeSet.add(Range.closed(3, 5));
        numberRangeSet.add(Range.closed(6, 8));
        RangeSet<Integer> numberSubRangeSet
                = numberRangeSet.subRangeSet(Range.closed(4, 14));

        assertFalse(numberSubRangeSet.contains(3));
        assertFalse(numberSubRangeSet.contains(14));
        assertTrue(numberSubRangeSet.contains(7));
    }

    @Test
    public void complement() {
        RangeSet<Integer> numberRangeSet = TreeRangeSet.create();

        numberRangeSet.add(Range.closed(0, 2));
        numberRangeSet.add(Range.closed(3, 5));
        numberRangeSet.add(Range.closed(6, 8));
        RangeSet<Integer> numberRangeComplementSet
                = numberRangeSet.complement();

        assertTrue(numberRangeComplementSet.contains(-1000));
        assertFalse(numberRangeComplementSet.contains(2));
        assertFalse(numberRangeComplementSet.contains(3));
        assertTrue(numberRangeComplementSet.contains(1000));
    }

    @Test
    public void intersect() {
        RangeSet<Integer> numberRangeSet = TreeRangeSet.create();

        numberRangeSet.add(Range.closed(0, 2));
        numberRangeSet.add(Range.closed(3, 10));
        numberRangeSet.add(Range.closed(15, 18));

        assertTrue(numberRangeSet.intersects(Range.closed(4, 17)));
        assertFalse(numberRangeSet.intersects(Range.closed(19, 200)));
    }

RangeMap

mapping 不連續非空的 ranges 到非 null 的 values

基本實作為TreeRangeMap

    @Test
    public void create() {
        // 用 TreeRangeMap 的 create 產生 mutable RangeMap
        RangeMap<Integer, String> experienceRangeDesignationMap = TreeRangeMap.create();

        // ImmutableRangeMap.Builder 產生 ImmutableRangeMap
        RangeMap<Integer, String> experienceRangeDesignationMap2 =
                new ImmutableRangeMap.Builder<Integer, String>()
                        .put(Range.closed(0, 2), "Junior")
                        .build();
    }

    @Test
    public void query_within_range() {
        RangeMap<Integer, String> experienceRangeDesignationMap
                = TreeRangeMap.create();

        experienceRangeDesignationMap.put(
                Range.closed(0, 2), "Junior");
        experienceRangeDesignationMap.put(
                Range.closed(3, 5), "Senior");
        experienceRangeDesignationMap.put(
                Range.closed(6, 8),  "College");
        experienceRangeDesignationMap.put(
                Range.closed(9, 15), "Research");

        assertEquals("College",
                experienceRangeDesignationMap.get(6));
        assertEquals("Research",
                experienceRangeDesignationMap.get(15));

        assertNull(experienceRangeDesignationMap.get(30));
    }

    @Test
    public void remove_rage() {
        RangeMap<Integer, String> experienceRangeDesignationMap
                = TreeRangeMap.create();

        experienceRangeDesignationMap.put(
                Range.closed(0, 2), "Junior");
        experienceRangeDesignationMap.put(
                Range.closed(3, 5), "Senior");
        experienceRangeDesignationMap.put(
                Range.closed(6, 8),  "College");
        experienceRangeDesignationMap.put(
                Range.closed(9, 15), "Research");

        experienceRangeDesignationMap.remove(Range.closed(9, 15));
        experienceRangeDesignationMap.remove(Range.closed(1, 4));

        assertNull(experienceRangeDesignationMap.get(9));
        assertEquals("Junior",
                experienceRangeDesignationMap.get(0));
        assertEquals("Senior",
                experienceRangeDesignationMap.get(5));
        assertNull(experienceRangeDesignationMap.get(1));
    }

    @Test
    public void span() {
        RangeMap<Integer, String> experienceRangeDesignationMap
                = TreeRangeMap.create();

        experienceRangeDesignationMap.put(
                Range.closed(0, 2), "Junior");
        experienceRangeDesignationMap.put(
                Range.closed(3, 5), "Senior");
        experienceRangeDesignationMap.put(
                Range.closed(6, 8),  "College");
        experienceRangeDesignationMap.put(
                Range.closed(9, 15), "Research");

        Range<Integer> experienceSpan = experienceRangeDesignationMap.span();

        assertEquals(0, experienceSpan.lowerEndpoint().intValue());
        assertEquals(15, experienceSpan.upperEndpoint().intValue());
    }

    @Test
    public void subRageMap() {
        RangeMap<Integer, String> experienceRangeDesignationMap
                = TreeRangeMap.create();

        experienceRangeDesignationMap.put(
                Range.closed(0, 2), "Junior");
        experienceRangeDesignationMap.put(
                Range.closed(3, 5), "Senior");
        experienceRangeDesignationMap.put(
                Range.closed(6, 8),  "College");
        experienceRangeDesignationMap.put(
                Range.closed(9, 15), "Research");

        RangeMap<Integer, String> experiencedSubRangeDesignationMap
                = experienceRangeDesignationMap.subRangeMap(Range.closed(4, 14));

        assertNull(experiencedSubRangeDesignationMap.get(3));
        assertTrue(experiencedSubRangeDesignationMap.asMapOfRanges().values()
                .containsAll(Arrays.asList("Senior", "College", "Research")));
    }

2024/03/25

Guava Collection Multiset Multimap

New Collection Type

ref: NewCollectionTypesExplained · google/guava Wiki · GitHub

ref: 【Guava 教學】(7)Multiset、Multimap 與 BiMap

Multiset

多重集合(Multiset)是集合(Set)概念的推廣(Generalization),Set 裡面相同元素只能出現一次,Multiet 多重集合則允許相同元素出現多次,元素在集合中有重複次數(Occurrence)的概念,多重集合又稱為 Bag。

Guava 提供 Multiset,可方便地計算每一個元素發生的次數,且能 iterate 每一個元素。換句話說,Multiset 有兩個功能

  1. 類似沒有順序的 ArrayList

    • add(E) 新增一個元素

    • iterator() 迭代每一個元素

    • size()

  2. 類似 Map<E, Integer>,儲存元素及數量

    • count(Object) 計算某個元素的數量

    • entrySet() 回傳 Set<Multiset.Entry> ,類似 Map 的 entrySet

    • elementSet() 回傳 Set ,包含所有不同的 elements,類似 Map 的 keySet()

    @Test
    public void jdk_word_count() {
        List<String> words = Arrays.asList("one", "two", "three", "one", "three");
        Map<String, Integer> counts = new HashMap<>();
        for(String word : words) {
            Integer count = counts.get(word);
            if (count == null) {
                counts.put(word, 1);
            } else {
                counts.put(word, count + 1);
            }
        }
        System.out.println(counts); // {two=1, one=2, three=2}

        Map<String, List<String>> wordBag = new HashMap<>();
        for(String word : words) {
            List<String> repeatedWds = wordBag.get(word);
            if(repeatedWds == null) {
                repeatedWds = new ArrayList<>();
                wordBag.put(word, repeatedWds);
            }
            repeatedWds.add(word);
        }
        // {one=[one, one], two=[two], three=[three, three]}
        System.out.println(wordBag);
    }

    @Test
    public void guava_multiset_word_count() {
        List<String> words = Arrays.asList("one", "two", "three", "one", "three");
        Multiset<String> wordBag = HashMultiset.create(words);

        System.out.println("");
        // [two, one x 2, three x 2]
        System.out.println(wordBag);

//        Element: one, Occurrence(s): 2
//        Element: two, Occurrence(s): 1
//        Element: three, Occurrence(s): 2
        for (Multiset.Entry<String> entry : wordBag.entrySet())
        {
            System.out.println("Element: "+entry.getElement() +", Occurrence(s): " + entry.getCount());
        }
        for(String word : wordBag) {
            // 可直接使用 words 裡面的每一個元素
            System.out.print(word);
        }
    }

Guava 提供的 Multiset implementation,可對應到 JDK map implementation

Map Corresponding Multiset Supports null elements
HashMap HashMultiset Yes
TreeMap TreeMultiset Yes
LinkedHashMap LinkedHashMultiset Yes
ConcurrentHashMap ConcurrentHashMultiset No
ImmutableMap ImmutableMultiset No

TreeMultiset 有實作 SortedMultiset 介面,該 interface 可快速取得 sub-multiset,ex:

    @Test
    public void guava_submultiset_word_count() {
        List<String> words = Arrays.asList("one", "two", "three", "one", "three");
        TreeMultiset<String> wordBag = TreeMultiset.create(words);

        SortedMultiset<String> subWordBag = wordBag.subMultiset("one", BoundType.CLOSED, "two", BoundType.OPEN);
        System.out.println("");
        // [two, one x 2, three x 2]
        System.out.println(wordBag);
        // [one x 2, three x 2]
        // 根據 String 的排序,取得比 "two" 比較後還小的那些元素集合
        // TreeMultiset 裡面的元素都有排序過
        System.out.println(subWordBag);

        //Element: one, Occurrence(s): 2
        //Element: three, Occurrence(s): 2
        for (SortedMultiset.Entry<String> entry : subWordBag.entrySet())
        {
            System.out.println("Element: "+entry.getElement() +", Occurrence(s): " + entry.getCount());
        }
        for(String word : subWordBag) {
            // 可直接使用 subWordBag 裡面的每一個元素
            // one one three three
            System.out.print(word+" ");
        }
    }

Multimap

ref: Guava Multimap類 - Guava教學

在 java 只有提供 Map<K, List<V>> or Map<K, Set<V>>

在 Guava 以 Multimap 提供 keys 對應到任意資料結構的一個介面

Multimap 有一個 asMap() ,會回傳 Map<K, Collection<V>>

比較常用的是 ListMultimap / SetMutimap,比較少直接使用 Multimap interface


Construction & Modify

雖然可以直接用 ListMultimap / SetMutimap 的 create() 產生 Multimap,但比較建議使用 MultimapBuilder 產生

    @Test
    public void construction() {
        List<String> tolist = Arrays.asList("to1@domain");
        List<String> cclist = Arrays.asList("cc1@domain", "cc2@domain");
        List<String> bcclist = Arrays.asList("bcc1@domain", "bcc2@domain");

        // 使用 MultimapBuilder
        // creates a ListMultimap with tree keys and array list values
        ListMultimap<String, List<String>> treeListMultimap =
                MultimapBuilder.treeKeys().arrayListValues().build();
        treeListMultimap.put("to", tolist);
        treeListMultimap.put("cc", cclist);
        treeListMultimap.put("bcc", bcclist);
        System.out.println("treeListMultimap:");
        // {bcc=[[bcc1@domain, bcc2@domain]], cc=[[cc1@domain, cc2@domain]], to=[[to1@domain]]}
        System.out.println(treeListMultimap);

        // 直接使用 create()
        Multimap<String,List<String>> treeListMultimap2 = ArrayListMultimap.create();
        treeListMultimap2.put("to", tolist);
        treeListMultimap2.put("cc", cclist);
        treeListMultimap2.put("bcc", bcclist);

        // creates a SetMultimap with hash keys and enum set values
        SetMultimap<String, Grade> hashEnumMultimap =
                MultimapBuilder.hashKeys().enumSetValues(Grade.class).build();
        hashEnumMultimap.put("Alice", Grade.A);
        hashEnumMultimap.put("Bruce", Grade.B);

        System.out.println("hashEnumMultimap:");
        // {Bruce=[B], Alice=[A]}
        System.out.println(hashEnumMultimap);
    }
    public enum Grade {
        A, B, C, D, F, INCOMPLETE
    }

    @Test
    public void modify() {
        System.out.println("");

        List<String> tolist = Arrays.asList("to1@domain");
        List<String> cclist = Arrays.asList("cc1@domain", "cc2@domain");
        List<String> bcclist = Arrays.asList("bcc1@domain", "bcc2@domain");

        // 使用 MultimapBuilder
        // creates a ListMultimap with tree keys and array list values
        ListMultimap<String, List<String>> treeListMultimap =
                MultimapBuilder.treeKeys().arrayListValues().build();
        treeListMultimap.put("to", tolist);
        treeListMultimap.put("cc", cclist);
        treeListMultimap.put("bcc", bcclist);
        // treeListMultimap: {bcc=[[bcc1@domain, bcc2@domain]], cc=[[cc1@domain, cc2@domain]], to=[[to1@domain]]}
        System.out.println("treeListMultimap: "+ treeListMultimap);
        treeListMultimap.removeAll("cc");

        // treeListMultimap2 after removeAll: {bcc=[[bcc1@domain, bcc2@domain]], to=[[to1@domain]]}
        System.out.println("treeListMultimap2 after removeAll: "+ treeListMultimap);

        List<String> tolist2 = Arrays.asList("to2@domain");
        treeListMultimap.put("to", tolist2);
        // treeListMultimap2 put new key/value: {bcc=[[bcc1@domain, bcc2@domain]], to=[[to1@domain], [to2@domain]]}
        System.out.println("treeListMultimap2 put new key/value: "+ treeListMultimap);

        treeListMultimap.clear();
        treeListMultimap.put("to", tolist2);
        // treeListMultimap2: {to=[[to2@domain]]}
        System.out.println("treeListMultimap2: "+ treeListMultimap);
    }

views

    @Test
    public void views() {
        System.out.println("");

        List<String> tolist = Arrays.asList("to1@domain");
        List<String> cclist = Arrays.asList("cc1@domain", "cc2@domain");
        List<String> bcclist = Arrays.asList("bcc1@domain", "bcc2@domain");

        // 使用 MultimapBuilder
        // creates a ListMultimap with tree keys and array list values
        ListMultimap<String, List<String>> treeListMultimap =
                MultimapBuilder.treeKeys().arrayListValues().build();
        treeListMultimap.put("to", tolist);
        treeListMultimap.put("cc", cclist);
        treeListMultimap.put("bcc", bcclist);
        // treeListMultimap: {bcc=[[bcc1@domain, bcc2@domain]], cc=[[cc1@domain, cc2@domain]], to=[[to1@domain]]}
        System.out.println("treeListMultimap: "+ treeListMultimap);

        // asMap:  views any Multimap<K, V> as a Map<K, Collection<V>>
        Map<String, Collection<List<String>>> tomap = treeListMultimap.asMap();
        // asMap: {bcc=[[bcc1@domain, bcc2@domain]], cc=[[cc1@domain, cc2@domain]], to=[[to1@domain]]}
        System.out.println("asMap: "+ tomap);

        // entries():  views the Collection<Map.Entry<K, V>>
        // entries: [bcc=[bcc1@domain, bcc2@domain], cc=[cc1@domain, cc2@domain], to=[to1@domain]]
        Collection<Map.Entry<String, List<String>>> entries = treeListMultimap.entries();
        System.out.println("entries: "+ entries);

        // keySet: views the distinct keys in the Multimap as a Set
        // keySet: [bcc, cc, to]
        Set<String> keySet = treeListMultimap.keySet();
        System.out.println("keySet: "+ keySet);

        // keys: views the keys of the Multimap as a Multiset
        // keysMultiset: [bcc, cc, to]
        Multiset<String> keysMultiset = treeListMultimap.keys();
        System.out.println("keysMultiset: "+ keysMultiset);

        // values: views all the values in the Multimap as a "flattened" Collection<V>
        // values: [[bcc1@domain, bcc2@domain], [cc1@domain, cc2@domain], [to1@domain]]
        Collection<List<String>> values = treeListMultimap.values();
        System.out.println("values: "+ values);
    }

implementations

建議使用  MultimapBuilder 不是直接 create()

Implementation Keys behave like... Values behave like..
ArrayListMultimap HashMap ArrayList
HashMultimap HashMap HashSet
LinkedListMultimap * LinkedHashMap``* LinkedList``*
LinkedHashMultimap** LinkedHashMap LinkedHashSet
TreeMultimap TreeMap TreeSet
ImmutableListMultimap ImmutableMap ImmutableList
ImmutableSetMultimap ImmutableMap ImmutableSet

2024/03/18

Guava Collection 1 Immutable Collections

Immutable Collections

ref: ImmutableCollectionsExplained · google/guava Wiki · GitHub

collection 的資料不一定需要隨時可以修改,如果可以任意修改,反而在某些時候,會造成問題。

  1. 不能修改的 collection,將資料傳給 untrusted libraries 使用,也不會被 library 任意修改資料

  2. thread-safe,再多執行緒環境共用資料時,可避免同時修改發生問題

  3. 可節省 time, space

JDK 也有提供 Collections.unmodifiableXXX methods,但還是有可能有以下問題

  1. unwieldy and verbose: 使用時必須要在每一個地方,都先產生一份

  2. unsafe: 只有在沒有其他地方,有儲存原本 collection 的 reference 時,這個 collection 才會是 immutable

  3. inefficient: 資料結構還是跟原本 mutable collection 一樣,所以還是會有 concurrent modification check, 額外耗費的 space ...

    @Test
    public void jdk_unmodifiableMethod() {
        List list = new ArrayList();
        list.add("item1");
        list.add("item2");

        List unmodifiableList = Collections.unmodifiableList(list);

        Exception exception = assertThrows(UnsupportedOperationException.class, () -> {
            unmodifiableList.add("item3");;
        });

        assertEquals(exception.getClass().getName(), "java.lang.UnsupportedOperationException");
        String actualMessage = exception.getMessage();
        assertNull(actualMessage);

        // 如果修改原本的 list,還是會影響到 unmodifiableList
        list.add("item3");
        assertEquals(unmodifiableList.size(), 3);
    }

    @Test
    public void guava_immutable() {
        List<String> stringArrayList = Lists.newArrayList("item1","item2");
        ImmutableList<String> immutableList = ImmutableList.copyOf(stringArrayList);

        Exception exception = assertThrows(UnsupportedOperationException.class, () -> {
            immutableList.add("item3");
        });

        assertEquals(exception.getClass().getName(), "java.lang.UnsupportedOperationException");
        String actualMessage = exception.getMessage();
        assertNull(actualMessage);

        // 如果修改原本的 list,不會影響到 immutableList
        stringArrayList.add("item3");
        assertEquals(stringArrayList.size(), 3);
        assertEquals(immutableList.size(), 2);
    }

Guava immutable collection 不能使用 null values,因為大部分的 code,都是必須要有值

產生 ImmutableXXX collection 的方法:

  1. 使用 copyOf method ex: ImmutableSet.copyOf(set)

  2. 使用 of method ex: ImmutableSet.of("a", "b", "c") or ImmutableMap.of("a", 1, "b", 2)

  3. 使用 Builder

    @Test
    public void immutableCollection() {
        // copyOf
        List<String> stringArrayList = Lists.newArrayList("item1","item2");
        ImmutableList<String> immutableList = ImmutableList.copyOf(stringArrayList);

        // of
        ImmutableSet.of("a", "b", "c", "a", "d", "b");

        // Builder
        Color color1 = new Color(0, 0, 255);
        Color color2 = new Color(0, 255, 0);
        ImmutableSet<Color> colors = ImmutableSet.of(color1, color2);

        ImmutableSet<Color> newcolors =
                ImmutableSet.<Color>builder()
                        .addAll(colors)
                        .add(new Color(0, 191, 255))
                        .build();
    }

所有 immutable collections 都有透過 asList() 產生的 ImmutableList 的 view。

    @Test
    public void asList() {
        ImmutableSet<String> set = ImmutableSet.of("a", "b", "c", "a", "d", "b");

        String item0 = set.asList().get(0);
        assertEquals(item0, "a");
    }
Interface JDK or Guava? Immutable Version
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

2024/03/11

Guava in Java - ObjectUtilities

基本工具

CommonObjectUtilities

equals

當使用 Object.equals 時,如果遇到某個物件為 null,就會發生問題

com.google.common.base.Objects.equal 可做 null Object 的比較

JDK 提供類似的 java.util.Objects.equals()

    @Test
    public void equals() {
        com.google.common.base.Objects.equal("a", "a"); // returns true
        com.google.common.base.Objects.equal(null, "a"); // returns false
        com.google.common.base.Objects.equal("a", null); // returns false
        com.google.common.base.Objects.equal(null, null); // returns true
        // JDK 提供類似的 java.util.Objects.equals()
        java.util.Objects.equals("a", "a"); // returns true
        java.util.Objects.equals(null, "a"); // returns false
        java.util.Objects.equals("a", null); // returns false
        java.util.Objects.equals(null, null); // returns true

        String a = null;
        String b = "b";
        Exception exception = assertThrows(NullPointerException.class, () -> {
            a.equals(b);
        });
        Exception exception2 = assertThrows(NullPointerException.class, () -> {
            b.equals(a);
        });
    }

hashCode

簡化 hashCode 的做法,可直接根據多個 fields 產生 hash

    @Test
    public void hashCodeTest() {
        InnerClass innerClass = new InnerClass();
        int hash1 = innerClass.hashCode();
        int hash2 = innerClass.hashCode2();
        assertEquals(hash1, hash2);;
    }

    public static class InnerClass {
        private String a="a";
        private String b="b";

        @Override
        public int hashCode() {
            return com.google.common.base.Objects.hashCode(a, b);
        }
        // JDK 有對應類似的 java.util.Objects.hash
        public int hashCode2() {
            return java.util.Objects.hash(a, b);
        }
    }

toString

利用 MoreObjects.toStringHelper() 簡化 toString

    @Test
    public void toStringTest() {
        InnerClass2 cls = new InnerClass2();
        String clsString = cls.toString();
//        System.out.printf("clsString=%s%n", clsString);
        assertEquals(clsString, "InnerClass2{a=a, b=2, x=1}");;
    }

    public static class InnerClass2 {
        private String a="a";
        private int b=2;

        @Override
        public String toString() {
            return com.google.common.base.MoreObjects.toStringHelper(this)
                    .add("a", a)
                    .add("b", b)
                    .add("x", 1)
                    .toString();
        }
    }

compare/compareTo

guava 提供 ComparisonChain,他是 fluent Comparator 可改善 compareTo 的寫法

    class Person implements Comparable<Person> {
        private String lastName;
        private String firstName;
        private int zipCode;

        public int compareTo(Person that) {
            return ComparisonChain.start()
                    .compare(this.firstName, that.firstName)
                    .compare(this.lastName, that.lastName)
                    .compare(this.zipCode, that.zipCode, Ordering.natural().nullsLast())
                    .result();
        }

        public int compareTo2(Person other) {
            int cmp = lastName.compareTo(other.lastName);
            if (cmp != 0) {
                return cmp;
            }
            cmp = firstName.compareTo(other.firstName);
            if (cmp != 0) {
                return cmp;
            }
            return Integer.compare(zipCode, other.zipCode);
        }
    }

Throwable

package guava.basic;

import java.util.List;

import com.google.common.base.Throwables;

public class ThrowableTest {
    public static void main(String[] args) {

        ThrowableTest tester = new ThrowableTest();

        try {
            System.out.println("invalidInputExceptionTest");
            tester.invalidInputExceptionTest();
        } catch (InvalidInputException e) {
            //get the root cause
            System.out.println("invalidInputExceptionTest getRootCause");
            System.out.println(Throwables.getRootCause(e));

        } catch (Exception e) {
            //get the stack trace in string format
            System.out.println("invalidInputExceptionTest getStackTraceAsString");
            System.out.println(Throwables.getStackTraceAsString(e));
        }

        System.out.println("");
        try {
            System.out.println("indexOutOfBoundsExceptionTest");
            tester.indexOutOfBoundsExceptionTest();
        } catch (Exception e) {
            System.out.println("indexOutOfBoundsExceptionTest getStackTraceAsString");
            List<Throwable> elist = Throwables.getCausalChain(e);
            for( Throwable t1: elist ) {
                System.out.println(Throwables.getStackTraceAsString(t1));
            }
//            System.out.println(Throwables.getStackTraceAsString(e));
        }
    }

    public void invalidInputExceptionTest() throws InvalidInputException {
        try {
            sqrt(-1.0);
        } catch (Throwable e) {
            //check the type of exception and throw it
            Throwables.propagateIfInstanceOf(e, InvalidInputException.class);
            // Throws throwable as-is only if it is a RuntimeException or an Error.
            Throwables.throwIfUnchecked(e);
            throw new RuntimeException(e);
        }
    }

    public void indexOutOfBoundsExceptionTest() {
        try {
            int[] data = {1, 2, 3};
            getValue(data, 4);
        } catch (Throwable e) {
            Throwables.propagateIfInstanceOf(e, IndexOutOfBoundsException.class);
            Throwables.throwIfUnchecked(e);
            throw new RuntimeException(e);
        }
    }

    public double sqrt(double input) throws InvalidInputException {
        if (input < 0) throw new InvalidInputException();
        return Math.sqrt(input);
    }

    public double getValue(int[] list, int index) throws IndexOutOfBoundsException {
        return list[index];
    }
}

class InvalidInputException extends Exception {
}

執行結果

invalidInputExceptionTest
invalidInputExceptionTest getRootCause
com.maxkit.guava.InvalidInputException

indexOutOfBoundsExceptionTest
indexOutOfBoundsExceptionTest getStackTraceAsString
java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 3
    at com.maxkit.guava.ThrowableTest.getValue(ThrowableTest.java:71)
    at com.maxkit.guava.ThrowableTest.indexOutOfBoundsExceptionTest(ThrowableTest.java:57)
    at com.maxkit.guava.ThrowableTest.main(ThrowableTest.java:31)

References

Home · google/guava Wiki · GitHub

Guava Guide | Baeldung

Guava:Google开源的Java工具库,太强大了 | Java程序员进阶之路

Google Guava 工具類 的介紹和使用 - HackMD

# Google Guava官方教程

專欄文章:Guava 教學

2024/03/04

Guava Basic Preconditions

Guava 是 Google 開發的 Java 開源工具 Library。內容主要有兩個部分:擴充 Java Collection Framework,提供更好用的 function,例如cache, range,以及 hash function

使用 Guava 只需要在 maven pom.xml 直接加入 dependency 即可

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>32.1.2-jre</version>
  <!-- or, for Android: -->
  <!-- <version>32.1.2-android</version> -->
</dependency>

基本工具

Null Check

JDK 跟 Guava 都有針對 null check 問題提供了 Optional 類別來解決

Optional,Gauva工具及和Java8中实现的区别_Tonels的博客-CSDN博客

java.util.Optional 是 final 的類別,無法被繼承

com.google.common.base.Optional 是 abstract class,有實作 Serializable

兩種 Optional 基本的使用方式差不多

        com.google.common.base.Optional<Integer> possible = com.google.common.base.Optional.of(5);
        boolean isPresent = possible.isPresent(); // returns true
//        int val = possible.get(); // returns 5
        int val = possible.or(-1); // returns 5
        System.out.printf("isPresent=%b, val=%d%n", isPresent, val);

        com.google.common.base.Optional<Integer> possible2 = com.google.common.base.Optional.fromNullable(null);
        isPresent = possible2.isPresent();
        val = possible2.or(-1);
        System.out.printf("isPresent=%b, val=%d%n", isPresent, val);

        java.util.Optional<Integer> opt = java.util.Optional.of(5);
        boolean isPresent2 = opt.isPresent();
//        int val2 = opt.get();
        int val2 = opt.orElse(-1);
        System.out.printf("isPresent2=%b, val2=%d%n", isPresent2, val2);

        java.util.Optional<Integer> opt2 = java.util.Optional.ofNullable(null);
        isPresent2 = opt2.isPresent();
        val2 = opt2.orElse(-1);
        System.out.printf("isPresent2=%b, val2=%d%n", isPresent2, val2);

Preconditions

有很多 static method 可檢查 method 或 constructor 是否有正確的參數數值,如果檢查結果失敗,就會 throw exception

每一種 Preconditions 的 static method 都有三種變化

  1. No arguments,exception 裡面沒有 error message

  2. 有一個 Object argument,作為 error message,丟出的 exception 裡面會有 error message

  3. 有一個 String argument,搭配任意數量的 Object arguments,作為 error message 的 placeholder,類似 printf

Preconditions 的 checkArgument 可檢查參數的正確性,失敗時會丟出 IllegalArgumentException

沒有 error messge

    @Test
    public void checkArgument_without_error_message() {
        int age = -18;

        Exception exception = assertThrows(IllegalArgumentException.class, () -> {
            Preconditions.checkArgument(age > 0);
        });

//        String expectedMessage = null;
        String actualMessage = exception.getMessage();
        assertNull(actualMessage);
    }

有 error message

    @Test
    public void checkArgument_with_error_message() {
        int age = -18;
        String message = "Age can't be zero or less than zero.";

        Exception exception = assertThrows(IllegalArgumentException.class, () -> {
            Preconditions.checkArgument(age > 0, message);
        });

        String expectedMessage = message;
        String actualMessage = exception.getMessage();
        assertEquals(expectedMessage, actualMessage);
    }

有 error message template

    @Test
    public void checkArgument_with_template_error_message() {
        int age = -18;
        String message = "Age should be positive number, you supplied %s.";

        Exception exception = assertThrows(IllegalArgumentException.class, () -> {
            Preconditions.checkArgument(age > 0, message, age);
        });

        String expectedMessage = String.format(message, age);
        String actualMessage = exception.getMessage();
        assertEquals(expectedMessage, actualMessage);;
    }

checkElementIndex

    @Test
    public void checkElementIndex() {
        int[] numbers = { 1, 2, 3, 4, 5 };
        String message = "Please check the bound of an array and retry";

        Exception exception = assertThrows(IndexOutOfBoundsException.class, () -> {
            Preconditions.checkElementIndex(6, numbers.length - 1, message);
        });

//        expectedMessage: Please check the bound of an array and retry (6) must be less than size (4)
        String expectedMessage = String.format(message+" (%d) must be less than size (%d)", 6, numbers.length - 1);
//        System.out.printf("expectedMessage=%s%n", expectedMessage);
        String actualMessage = exception.getMessage();
        assertEquals(expectedMessage, actualMessage);;
    }

checkNotNull

    @Test
    public void checkNotNull () {
        String nullObject = null;
        String message = "Please check the Object supplied, its null!";

        Exception exception = assertThrows(NullPointerException.class, () -> {
            Preconditions.checkNotNull(nullObject, message);
        });

        String expectedMessage = message;
//        System.out.printf("expectedMessage=%s%n", expectedMessage);
        String actualMessage = exception.getMessage();
        assertEquals(expectedMessage, actualMessage);;
    }

checkPositionIndex

    @Test
    public void checkPositionIndex() {
        int[] numbers = { 1, 2, 3, 4, 5 };
        String message = "Please check the bound of an array and retry";

        Exception exception = assertThrows(IndexOutOfBoundsException.class, () -> {
            Preconditions.checkPositionIndex(6, numbers.length - 1, message);
        });

//        expectedMessage: Please check the bound of an array and retry (6) must not be greater than size (4)
        String expectedMessage = String.format(message+" (%d) must not be greater than size (%d)", 6, numbers.length - 1);
//        System.out.printf("expectedMessage=%s%n", expectedMessage);
        String actualMessage = exception.getMessage();
        assertEquals(expectedMessage, actualMessage);;
    }

checkState

    @Test
    public void checkState() {
        int[] validStates = { -1, 0, 1 };
        int givenState = 10;
        String message = "You have entered an invalid state";

        Exception exception = assertThrows(IllegalStateException.class, () -> {
            Preconditions.checkState(
                    Arrays.binarySearch(validStates, givenState) > 0, message);
        });

        String expectedMessage = message;
        String actualMessage = exception.getMessage();
        assertEquals(expectedMessage, actualMessage);;
    }

References

Home · google/guava Wiki · GitHub

Guava Guide | Baeldung

Guava:Google开源的Java工具库,太强大了 | Java程序员进阶之路

Google Guava 工具類 的介紹和使用 - HackMD

# Google Guava官方教程

專欄文章:Guava 教學

2024/02/26

Well-known URI

well-known URI 是網站上以 /.well-known/ 開始的網頁路徑,例如 https://www.example.com/.well-known/ 這樣的路徑。Well-known URI 是在 RFC 8615 定義,目的是在不同 http server 之間,提供該 http server 相關的一些資訊。

目前最常見的使用方式有兩種,第一種是在申請 http server SSL 憑證的時候,憑證中心會發送一個檔案給申請者,請申請者將該檔案放到 .well-known 的某個路徑下,讓憑證中心可以檢查確認憑證申請者確實有服務該網域的 http server 的所有權。

例如 Let's Encrypt 就是將驗證的檔案放在這個路徑下面 /.well-known/acme-challenge/,這是給 ACME (Automatic Certificate Management Environment) 使用的

第二種是給 Android iOS 手機使用的

  • /.well-known/assetlinks.json 是 Digital Asset Links,可告訴Android browser,要使用什麼 APP 打開

  • /.well-known/apple-app-site-association Universal Links,是 iOS 使用的

GitHub - moul/awesome-well-known: A curated list of well-known URIs, resources, guides and tools (RFC 5785) 這個網頁提供了 .well-known 網址的使用列表

References

Well-known URI - Wikipedia

2024/02/19

Java Date Time API

以往處理日期是用 java.util.Date,以及 java.util.Calendar,但 Date 裡面不存在時區概念,只是 Timestamp 的一個 wrapper,Calendar 也很奇怪的將一月份的數值設定為 0,Java 8 以後可以改用 java.time.* API

java.time 這個 package 裡面有這些 class

  • Instant – represents a point in time (timestamp)
  • LocalDate – represents a date (year, month, day)
  • LocalDateTime – same as LocalDate, but includes time with nanosecond precision
  • OffsetDateTime – same as LocalDateTime, but with time zone offset
  • LocalTime – time with nanosecond precision and without date information
  • ZonedDateTime – same as OffsetDateTime, but includes a time zone ID
  • OffsetLocalTime – same as LocalTime, but with time zone offset
  • MonthDay – month and day, without year or time
  • YearMonth – month and year, without day or time
  • Duration – amount of time represented in seconds, minutes and hours. Has nanosecond precision
  • Period – amount of time represented in days, months and years

有 Timezone 的時間

Date 跟 Instant 的概念不同,處理不同時區的做法不同,但 Instant 的做法比較直覺

        Instant nowInstant = Instant.now();
        ZoneId zoneIdTaipei = ZoneId.of("Asia/Taipei");
        ZoneId zoneIdTokyo = ZoneId.of("Asia/Tokyo");
        ZonedDateTime nowZonedDateTimeTaipei = ZonedDateTime.ofInstant(nowInstant, zoneIdTaipei);
        ZonedDateTime nowZonedDateTimeTokyo = ZonedDateTime.ofInstant(nowInstant, zoneIdTokyo);
        System.out.println("nowZonedDateTimeTaipei="+nowZonedDateTimeTaipei);
        System.out.println("nowZonedDateTimeTokyo="+nowZonedDateTimeTokyo);
        // 跟想像的一樣,將現在的時間放到不同時區,可得到不同時區的正確時間
//        nowZonedDateTimeTaipei=2023-06-14T14:42:44.873547+08:00[Asia/Taipei]
//        nowZonedDateTimeTokyo=2023-06-14T15:42:44.873547+09:00[Asia/Tokyo]

        Date nowDate = new Date();
        System.out.println("nowDate="+nowDate);
        TimeZone tz = TimeZone.getTimeZone("Asia/Tokyo");
        Calendar calendar = Calendar.getInstance(tz);
        calendar.setTime(nowDate);
        Date nowDate2 = calendar.getTime();
        System.out.println("nowDate2="+nowDate2);
        // 因為 Date 沒有時區的概念,是儲存 GMT 1970/1/1 00:00:00 以後所經過的 ms 數值
        // 但列印 Date 時,會自動根據執行程式的時區,轉換為該時區的時間
        // 把現在時間 Date 放到 Tokyo 時區,取回新的 Date 以後,結果兩個 Date 很奇怪的結果是一樣的
//        nowDate=Wed Jun 14 14:42:44 CST 2023
//        nowDate2=Wed Jun 14 14:42:44 CST 2023

        // 要搭配 SimpleDateFormat
        SimpleDateFormat sdfTaipei = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdfTaipei.setTimeZone(TimeZone.getTimeZone("Asia/Taipei"));
        SimpleDateFormat sdfTokyo = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdfTokyo.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
        System.out.println();
        System.out.println("nowDate="+nowDate);
        System.out.println("nowDate Taipei="+sdfTaipei.format(nowDate));
        System.out.println("nowDate Tokyo="+sdfTokyo.format(nowDate));
//        nowDate=Wed Jun 14 14:53:19 CST 2023
//        nowDate Taipei=2023-06-14 14:53:19
//        nowDate Tokyo=2023-06-14 15:53:19

java.time 的優點

immutable 且為 thread-safe,所有的 method 回傳的物件都是產生一個新的物件,物件本身的狀態是永久不變的,因此為 thread-safe。java.util.Date 並不是 thread-safe

因為 method 會回傳新的物件,因此可以做 method chaining

ZonedDateTime nextFriday = LocalDateTime.now()
                .plusHours(1)
                .with(TemporalAdjusters.next(DayOfWeek.FRIDAY))
                .atZone(ZoneId.of("Asia/Taipei"));
System.out.println("plus 1hr nextFriday="+nextFriday);

新舊寫法

以下是一些特定工作,新舊 API 的不同寫法

        /* get current timestamp */
        Date now = new Date();
        Instant instant = Instant.now();
        // 帶有時區的現在時間
        ZonedDateTime zonedDateTime = ZonedDateTime.now();

        /* 特定時間 */
        Date sDate = new GregorianCalendar(1990, Calendar.JANUARY, 15).getTime();
// New
        LocalDate sLocalDate = LocalDate.of(1990, Month.JANUARY, 15);

        /* 時間的加減 */
        GregorianCalendar calendar = new GregorianCalendar();
        calendar.add(Calendar.HOUR_OF_DAY, -5);
        Date fiveHoursBeforeOld = calendar.getTime();

        // New
        LocalDateTime fiveHoursBeforeNew = LocalDateTime.now().minusHours(5);

        /* 修改特定欄位 */
        // Old
        GregorianCalendar calc = new GregorianCalendar();
        calc.set(Calendar.MONTH, Calendar.JUNE);
        Date inJuneOld = calc.getTime();

        // New
        LocalDateTime inJuneNew = LocalDateTime.now().withMonth(Month.JUNE.getValue());

        /* Date Time truncating */
        // Old
        Calendar nowCalc = Calendar.getInstance();
        nowCalc.set(Calendar.MINUTE, 0);
        nowCalc.set(Calendar.SECOND, 0);
        nowCalc.set(Calendar.MILLISECOND, 0);
        Date truncatedOld = nowCalc.getTime();

        // New
        LocalTime truncatedNew = LocalTime.now().truncatedTo(ChronoUnit.HOURS);

        /* TimeZone 轉換 */
        // Old
        GregorianCalendar cal2 = new GregorianCalendar();
        cal2.setTimeZone(TimeZone.getTimeZone("Asia/Taipei"));
        Date centralEasternOld = calendar.getTime();

        // New
        ZonedDateTime centralEasternNew = LocalDateTime.now().atZone(ZoneId.of("Asia/Taipei"));

        /* Time difference */
        // Old
        GregorianCalendar calc3 = new GregorianCalendar();
        Date nowdate = new Date();
        calc3.add(Calendar.HOUR, 1);
        Date hourLater = calc3.getTime();
        long elapsed = hourLater.getTime() - nowdate.getTime();

        // New
        LocalDateTime nowLocalDateTime = LocalDateTime.now();
        LocalDateTime hourLaterLocalDateTime = LocalDateTime.now().plusHours(1);
        Duration span = Duration.between(nowLocalDateTime, hourLaterLocalDateTime);

        /* Date formatter, parsing */
        // Old
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        Date nowdate2 = new Date();
        String formattedDateOld = dateFormat.format(nowdate2);
        try {
            Date parsedDateOld = dateFormat.parse(formattedDateOld);
        } catch (ParseException e) {
            throw new RuntimeException(e);
        }

        // New
        LocalDate nowLocalDate = LocalDate.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        String formattedDateNew = nowLocalDate.format(formatter);
        LocalDate parsedDateNew = LocalDate.parse(formattedDateNew, formatter);

新舊 classes 之間能夠互相轉換

        // GregorianCalendar 轉為 Instant
        Instant instantFromCalendar = GregorianCalendar.getInstance().toInstant();
        // GregorianCalendar 轉為 ZonedDateTime
        ZonedDateTime zonedDateTimeFromCalendar = new GregorianCalendar().toZonedDateTime();

        // Instant 轉為 Date
        Date dateFromInstant = Date.from(Instant.now());
        // ZonedDateTime 轉為 GregorianCalendar
        GregorianCalendar calendarFromZonedDateTime = GregorianCalendar.from(ZonedDateTime.now());
        // Date 轉為 Instant
        Instant instantFromDate = new Date().toInstant();
        // TimeZone 轉為 ZoneId
        ZoneId zoneIdFromTimeZone = TimeZone.getTimeZone("Asia/Taipei").toZoneId();

References

Set the Time Zone of a Date in Java | Baeldung

Convert Date to LocalDate or LocalDateTime and Back | Baeldung

Migrating to the New Java 8 Date Time API | Baeldung

How to set time zone of a java.util.Date? - Stack Overflow

2024/02/05

在網頁使用 sqlite

SQLite compiled to JavaScript 透過 WASM,可在網頁直接載入 sqlite db,使用 SQL 指令操作資料庫。WebAssembly或稱wasm是一個低階程式語言,讓開發者能運用自己熟悉的程式語言(最初以C/C++作為實作目標)編譯,再藉虛擬機器引擎在瀏覽器內執行。透過 WebAssembly 可以讓一些 C/C++ 開發的函示庫,移動到網頁裡面運作。sql.js 就是用這種方式,讓網頁可以直接使用 sqlite 資料庫。

使用sql.js要先初始化資料庫物件

引用 javascript

<script src='https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.min.js'></script>

接下來有兩種方式初始化資料庫

方法 1: fetch

    async function initdb() {
        let config = {
            locateFile: filename => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.wasm`
        };
        const sqlPromise = initSqlJs(config);
        const dataPromise = fetch("csv/townsnote.db").then(res => res.arrayBuffer());
        const [SQL, buf] = await Promise.all([sqlPromise, dataPromise])
        const sqlitedb = new SQL.Database(new Uint8Array(buf));
        window.sqlitedb = sqlitedb;
    };

    initdb();

方法 2: XMLHttpRequest

    let config = {
        locateFile: filename => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.wasm`
    };
    initSqlJs(config).then(function(SQL){
        const xhr = new XMLHttpRequest();

        xhr.open('GET', 'csv/townsnote.db', true);
        xhr.responseType = 'arraybuffer';

        xhr.onload = e => {
            const uInt8Array = new Uint8Array(xhr.response);
            const db = new SQL.Database(uInt8Array);

            window.sqlitedb = db;

            // const contents = db.exec("SELECT * FROM towns");
            // contents is now [{columns:['col1','col2',...], values:[[first row], [second row], ...]}]
            // console.log("contents=",contents);
        };
        xhr.send();
    });

初始化資料庫後,就可以直接使用資料庫,執行 SQL 查詢指令

let contents = window.sqlitedb.exec("SELECT * FROM towns where id="+id);

以下是載入 sqlite db,執行一個 SQL 查詢的範例

<!DOCTYPE html>
<html lang="zh-tw">
<head>
    <meta charset="utf-8">
    <title>test</title>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.min.js'></script>
    <script>
    async function initdb() {
        let config = {
            locateFile: filename => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.wasm`
        };
        const sqlPromise = initSqlJs(config);
        const dataPromise = fetch("csv/townsnote.db").then(res => res.arrayBuffer());
        const [SQL, buf] = await Promise.all([sqlPromise, dataPromise])
        const sqlitedb = new SQL.Database(new Uint8Array(buf));
        window.sqlitedb = sqlitedb;
    };

    initdb();

    function get_town_by_id() {
        if(!window.sqlitedb) return;
        let id = document.getElementById('id').value;
        let contents = window.sqlitedb.exec("SELECT * FROM towns where id="+id);
        console.log("contents=", contents);

        var jsonArray = JSON.parse(JSON.stringify(contents))
        console.log("jsonArray=", jsonArray);
        document.getElementById('result').value = JSON.stringify(contents);
    };
    </script>

</head>
<body>
    <input type="text" value="9007010" id="id"></input>
    <button onclick="get_town_by_id();">query</button>
    <br/><br/>
    <textarea id="result" rows="20" cols="50"></textarea>
</body>
</html>

2024/01/22

Leaflet 基本使用方法

大部分想到地圖 API 直覺都是 Google Map,但在商業用途上,Google Map API 有使用量要收費的問題。目前可以透過 Leaflet 使用 OpenStreepMap,這部分在商業用途是可以免費使用的。

建立地圖

使用 leaflet 要先 include css 及 js,一開始要先指定地圖的中心點,以下範例定位在台中市政府。過程是用 L.map 建立地圖物件,然後加入 OpenStreetMap 這個 tile layer

建立地圖的範例

<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
    integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
    crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
    integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
    crossorigin=""></script>

<style>
    #map { height: 450px; };
</style>

</head>
<body>
<div id="map"></div>
</body>

<script>
var map = null;
function create_map() {

    let zoom = 16; // 0 - 18
    let taichung_cityhall = [24.1635657,120.6486657]; // 中心點座標
    let maptemp = L.map('map',{renderer: L.canvas()}).setView(taichung_cityhall, zoom);
    map = maptemp;
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '© OpenStreetMap', // 商用時必須要有版權出處
        zoomControl: true, // 是否顯示 - + 按鈕
        zoomAnimation:false,
        markerZoomAnimation: false,
        fadeAnimation: false,
    }).addTo(map);

    // 強制用 resize window 將所有 tiles 載入
    setTimeout(function () {
        window.dispatchEvent(new Event('resize'));
    }, 1000);

};

create_map();

</script>

</html>

當我們把程式整合到比較複雜的網頁中,會發現地圖的區塊在一開始載入後,會出現灰色的區塊,這一段程式碼是修正這個問題,用意是強制用 js resize window,讓 leaflet 能夠載入所有的地圖區塊。

// 強制用 resize window 將所有 tiles 載入
    setTimeout(function () {
        window.dispatchEvent(new Event('resize'));
    }, 1000);

建立 Marker,放上 Tooltip

因為要使用 bootstrap icon 測試,要先引用 bootstrap-icons,然後加上兩個 css class: icon1, icon2

<link href="
https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css
" rel="stylesheet">

<style>
#map { height: 450px; }

.icon1 {
  color: #e41a1c;
}
.icon2 {
  color: #377eb8;
}
</style>

這邊加上兩個 Marker 地圖標記,分別在台中市政府及火車站。

我們使用 L.divIcon ,這裡要注意,不能直接在 html 裡面寫上 style 調整 icon 顏色,只能指定 className 套用 css class。css class 很簡單,就只是修改 icon 的顏色

過程是用 L.marker 產生 marker,然後 bindTooltip,再將 marker 加入地圖中

function test_marker() {
    console.log("test_marker")
    // ref: https://gis.stackexchange.com/questions/291901/leaflet-divicon-how-to-hide-the-default-square-shadow
    // 不要寫 style   要用 className
    const icon1 = L.divIcon({
        html: '<i class="bi bi-geo-alt-fill"></i>',
        iconSize: [20, 20],
        className: 'icon1'
    });
    let taichung_cityhall = [24.1635657,120.6486657];
    let taichung_station = [24.136941,120.685056];
    let taichung_station2 = [24.1360,120.685056];
    let marker1 = L.marker(taichung_cityhall, {
        icon: icon1
    });

    marker1.bindTooltip("test1", {
        direction: 'bottom', // right、left、top、bottom、center。default: auto
        sticky: false, // true 跟著滑鼠移動。default: false
        permanent: false, // 是滑鼠移過才出現,還是一直出現
        opacity: 1.0
    }).openTooltip();
    marker1.addTo(map);
    // remove marker
    // marker1.remove();

    const icon2= L.divIcon({
        html: '<i class="bi bi-geo-alt-fill"></i>',
        iconSize: [20, 20],
        className: 'icon2'
    });
    let marker2 =  L.marker(taichung_station2, {
        icon: icon2
    });
    marker2.bindTooltip("test2", {
        direction: 'bottom',
        sticky: false,
        permanent: false,
        opacity: 1.0
    }).openTooltip();
    marker2.addTo(map);
};

test_marker();

Layer Group, Layer Control

多個 Marker 可以組合成一個 Layer,放到 Layer Group 裡面。再透過右上角 Layer Control,可將該 layer 的標記切換 on/off

function test_polyline() {
    console.log("test_polyline");
    const icon1 = L.divIcon({
        html: '<i class="bi bi-geo-alt-fill"></i>',
        iconSize: [20, 20],
        className: 'icon1'
    });

    const layerControl = L.control.layers(null).addTo(map);

    var test1_geos = [
        [24.136941,120.68],
        [24.136941,120.685056],
        [24.1360,120.6850]
    ];
    var test1_markers = [];
    for (let i = 0; i < test1_geos.length; i++) {
        let marker = L.marker([test1_geos[i][0], test1_geos[i][1]], {
            icon: icon1
        });
        marker.bindTooltip("test1", {
            direction: 'bottom',
            sticky: false,
            permanent: false,
            opacity: 1.0
        }).openTooltip();
        test1_markers.push(marker);
    }
    // marker 座標的連線
    var polyline1 = L.polyline(test1_geos, {color: '#e41a1c'});
    var test1LayerGroup = L.layerGroup(test1_markers).addLayer(polyline1);

    map.addLayer(test1LayerGroup);
    layerControl.addOverlay(test1LayerGroup, "test1");

    map.panTo(test1_geos[1]);
};
test_polyline();

搭配 vue3 使用的問題

ref: https://www.cnblogs.com/hjyjack9563-bk/p/16856014.html

在測試過程中,常會在 console 發生類似這樣的錯誤

Cannot read properties of null (reading '_latLngToNewLayerPoint')

這是因為 vue3 搭配 leaflet 才會遇到的問題,解決方式是在使用到 map 時,都要用 toRaw() 轉回原本的物件,所有用到 addlayer,removeLayer,clearLayers的方法 都應該用 toRaw(this.map)

ex:

// add layer control
this.layerControl = L.control.layers(null).addTo(toRaw(this.map));

References

OSM + Leaflet 學習筆記 1:建地圖、marker、事件、換圖層 - Front-End - Let's Write

Quick Start Guide - Leaflet - a JavaScript library for interactive maps