Guava – 拯救垃圾代码,写出优雅高效,效率提升N倍

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> Guava – 拯救垃圾代码,写出优雅高效,效率提升N倍

最近在看一个同学代码的时候,发现代码中大量使用了 Google 开源的 Guava 核心库中的内容,让代码简单清晰了不少,故学习分享出 Guava 中我认为最实用的功能。

Guava 项目是 Google 公司开源的 Java 核心库,它主要是包含一些在 Java 开发中经常使用到的功能,如数据校验不可变集合、计数集合,集合增强操作、I/O、缓存、字符串操作等。并且 Guava 广泛用于 Google 内部的 Java 项目中,也被其他公司广泛使用,甚至在新版 JDK 中直接引入了 Guava 中的优秀类库,所以质量毋庸置疑。

使用方式直接 mavan 依赖引入。


!-- https://mvnrepository.com/artifact/com.google.guava/guava --
dependency
    groupIdcom.google.guava/groupId
    artifactIdguava/artifactId
    version30.0-jre/version
/dependency

数据校验

数据校验说来十分简单,一是非空判断,二是预期值判断。非空判断我想每一个 Java 开发者都很熟悉,一开始都经常和 NullPointException 打交道。处理的方式我们自然是一个 if( xx == null) 就能轻松解决。预期值判断也是类似,检查数据值是不是自己想要的结果即可。

即使这么简单的操作,我们是不是还经常出错呢?而且写起来的代码总是一行判断一行异常抛出,怎么看都觉得那么优雅。还好,现在就来尝试第一次使用 Guava 吧。

非空判断


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

引入了 Guava 后可以直接使用 Preconditions.checkNotNull 进行非空判断,好处为觉得有两个,一是语义清晰代码优雅;二是你也可以自定义报错信息,这样如果参数为空,报错的信息清晰,可以直接定位到具体参数。


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

预期值判断

和非空判断类似,可以比较当前值和预期值,如果不相等可以自定义报错信息抛出。


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

是否越界

Preconditions 类还可以用来检查数组和集合的元素获取是否越界。


// Guava 中快速创建ArrayList
ListString list = Lists.newArrayList("a", "b", "c", "d");
// 开始校验
int index = Preconditions.checkElementIndex(5, list.size());
// java.lang.IndexOutOfBoundsException: index (5) must be less than size (4)

代码中快速创建 List 的方式也是 Guava 提供的,后面会详细介绍 Guava 中集合创建的超多姿势。

不可变的集合

创建不可变集合是我个人最喜欢 Guava 的一个原因,因为创建一个不能删除、不能修改、不能增加元素的集合实在是太实用了。这样的集合你完全不用担心发生什么问题,总的来说有下面几个优点:

  • 线程安全,因为不能修改任何元素,可以随意多线程使用且没有并发问题。
  • 可以无忧的提供给第三方使用,反正修改不了。
  • 减少内存占用,因为不能改变,所以内部实现可以最大程度节约内存占用。
  • 可以用作常量集合。
  • 创建方式

    说了那么多,那么到底怎么使用呢?赶紧撸起代码来。

    
    // 创建方式1:of
    ImmutableSetString immutableSet = ImmutableSet.of("a", "b", "c");
    immutableSet.forEach(System.out::println);
    // a
    // b
    // c
    
    // 创建方式2:builder
    ImmutableSetString immutableSet2 = ImmutableSet.Stringbuilder()
        .add("hello")
        .add(new String("未读代码"))
        .build();
    immutableSet2.forEach(System.out::println);
    // hello
    // 未读代码
    
    // 创建方式3:从其他集合中拷贝创建
    ArrayListString arrayList = new ArrayList();
    arrayList.add("www.wdbyte.com");
    arrayList.add("https");
    ImmutableSetString immutableSet3 = ImmutableSet.copyOf(arrayList);
    immutableSet3.forEach(System.out::println);
    // www.wdbyte.com
    // https
    

    都可以正常打印遍历结果,但是如果进行增删改,会直接报 UnsupportedOperationException .

    其实 JDK 中也提供了一个不可变集合,可以像下面这样创建。

    
    ArrayListString arrayList = new ArrayList();
    arrayList.add("www.wdbyte.com");
    arrayList.add("https");
    // JDK Collections 创建不可变 List
    ListString list = Collections.unmodifiableList(arrayList);
    list.forEach(System.out::println);// www.wdbyte.com https
    list.add("未读代码"); // java.lang.UnsupportedOperationException
    

    注意事项

  • 使用 Guava 创建的不可变集合是拒绝 `null` 值的,因为在 Google 内部调查中,95% 的情况下都不需要放入 `null` 值。
  • 使用 JDK 提供的不可变集合创建成功后,原集合添加元素会体现在不可变集合中,而 Guava 的不可变集合不会有这个问题。
  • 使用 JDK 提供的不可变集合创建成功后,原集合添加元素会体现在不可变集合中,而 Guava 的不可变集合不会有这个问题。

    
       ListString arrayList = new ArrayList();
       arrayList.add("a");
       arrayList.add("b");
       ListString jdkList = Collections.unmodifiableList(arrayList);
       ImmutableListString immutableList = ImmutableList.copyOf(arrayList);
       arrayList.add("ccc");
       jdkList.forEach(System.out::println);// result: a b ccc
       System.out.println("-------");
       immutableList.forEach(System.out::println);// result: a b
    
  • 如果不可变集合的元素是引用对象,那么引用对象的属性是可以更改的。
  • 其他不可变集合

    不可变集合除了上面演示的 set 之外,还有很多不可变集合,下面是 Guava 中不可变集合和其他集合的对应关系。

    **可变集合接口**属于JDK还是Guava**不可变版本**|------

    集合操作工厂

    其实这里只会介绍一个创建方法,但是为什么还是单独拿出来介绍了呢?看下去你就会大呼好用。虽然 JDK 中已经提供了大量的集合相关的操作方法,用起来也是非常的方便,但是 Guava 还是增加了一些十分好用的方法,保证让你用上一次就爱不释手,

    创建集合。

    
    // 创建一个 ArrayList 集合
    ListString list1 = Lists.newArrayList();
    // 创建一个 ArrayList 集合,同时塞入3个数据
    ListString list2 = Lists.newArrayList("a", "b", "c");
    // 创建一个 ArrayList 集合,容量初始化为10
    ListString list3 = Lists.newArrayListWithCapacity(10);
    
    LinkedListString linkedList1 = Lists.newLinkedList();
    CopyOnWriteArrayListString cowArrayList = Lists.newCopyOnWriteArrayList();
    
    HashMapObject, Object hashMap = Maps.newHashMap();
    ConcurrentMapObject, Object concurrentMap = Maps.newConcurrentMap();
    TreeMapComparable, Object treeMap = Maps.newTreeMap();
    
    HashSetObject hashSet = Sets.newHashSet();
    HashSetString newHashSet = Sets.newHashSet("a", "a", "b", "c");
    

    Guava 为每一个集合都添加了工厂方法创建方式,上面已经展示了部分集合的工厂方法创建方式。是不是十分的好用呢。而且可以在创建时直接扔进去几个元素,这个简直太赞了,再也不用一个个 add 了。

    集合交集并集差集

    过于简单,直接看代码和输出结果吧。

    
    SetString newHashSet1 = Sets.newHashSet("a", "a", "b", "c");
    SetString newHashSet2 = Sets.newHashSet("b", "b", "c", "d");
    
    // 交集
    SetViewString intersectionSet = Sets.intersection(newHashSet1, newHashSet2);
    System.out.println(intersectionSet); // [b, c]
    
    // 并集
    SetViewString unionSet = Sets.union(newHashSet1, newHashSet2);
    System.out.println(unionSet); // [a, b, c, d]
    
    // newHashSet1 中存在,newHashSet2 中不存在
    SetViewString setView = Sets.difference(newHashSet1, newHashSet2);
    System.out.println(setView); // [a]
    

    有数量的集合

    这个真的太有用了,因为我们经常会需要设计可以计数的集合,或者 value 是 List Map 集合,如果说你不太明白,看下面这段代码,是否某天夜里你也这样写过。

  • 统计相同元素出现的次数(下面的代码我已经尽可能精简写法了)。 JDK 原生写法:
  • JDK 原生写法:

    
       // Java 统计相同元素出现的次数。
       ListString words = Lists.newArrayList("a", "b", "c", "d", "a", "c");
       MapString, Integer countMap = new HashMapString, Integer();
       for (String word : words) {
           Integer count = countMap.get(word);
           count = (count == null) ? 1 : ++count;
           countMap.put(word, count);
       }
       countMap.forEach((k, v) - System.out.println(k + ":" + v));
       /**
        * result:
        * a:2
        * b:1
        * c:2
        * d:1
        */
    

    尽管已经尽量优化代码,代码量还是不少的,那么在 Guava 中有什么不一样呢?在 Guava. 中主要是使用 HashMultiset 类,看下面。

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

    是的,只要把元素添加进去就行了,不用在乎是否重复,最后都可以使用 count 方法统计重复元素数量。看着舒服,写着优雅, HashMultiset 是 Guava 中实现的 Collection 类,可以轻松统计元素数量。

  • 一对多,value 是 `List` 的 `Map` 集合。 假设一个场景,需要把很多动物按照种类进行分类,我相信最后你会写出类似的代码。 JDK 原生写法:
  • 假设一个场景,需要把很多动物按照种类进行分类,我相信最后你会写出类似的代码。

    
       HashMapString, SetString animalMap = new HashMap();
       HashSetString dogSet = new HashSet();
       dogSet.add("旺财");
       dogSet.add("大黄");
       animalMap.put("狗", dogSet);
       HashSetString catSet = new HashSet();
       catSet.add("加菲");
       catSet.add("汤姆");
       animalMap.put("猫", catSet);
       System.out.println(animalMap.get("猫")); // [加菲, 汤姆]
    

    最后一行查询猫得到了猫类的 “加菲” 和 ”汤姆“。这个代码简直太烦做了,如果使用 Guava 呢?

    
       // use guava
       HashMultimapString, String multimap = HashMultimap.create();
       multimap.put("狗", "大黄");
       multimap.put("狗", "旺财");
       multimap.put("猫", "加菲");
       multimap.put("猫", "汤姆");
       System.out.println(multimap.get("猫")); // [加菲, 汤姆]
    

    HashMultimap 可以扔进去重复的 key 值,最后获取时可以得到所有的 value 值,可以看到输出结果和 JDK 写法上是一样的,但是代码已经无比清爽。

    字符串操作

    作为开发中最长使用的数据类型,字符串操作的增强可以让开发更加高效。

    字符拼接

    JDK 8 中其实已经内置了字符串拼接方法,但是它只是简单的拼接,没有额外操作,比如过滤掉 null 元素,去除前后空格等。先看一下 JDK 8 中字符串拼接的几种方式。

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

    可以看到 null 值也被拼接到了字符串里,这有时候不是我们想要的,那么使用 Guava 有什么不一样呢?

    
    ArrayListString list = Lists.newArrayList("a", "b", "c", null);
    String join = Joiner.on(",").skipNulls().join(list);
    System.out.println(join); // a,b,c
    
    String join1 = Joiner.on(",").useForNull("空值").join("旺财", "汤姆", "杰瑞", null);
    System.out.println(join1); // 旺财,汤姆,杰瑞,空值
    

    可以看到使用 skipNulls() 可以跳过空值,使用 useFornull(String) 可以为空值自定义显示文本。

    字符串分割

    JDK 中是自带字符串分割的,我想你也一定用过,那就是 String 的 split 方法,但是这个方法有一个问题,就是如果最后一个元素为空,那么就会丢弃,奇怪的是第一个元素为空却不会丢弃,这就十分迷惑,下面通过一个例子演示这个问题。

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

    你也可以自己测试下,最后一个元素不是空,直接消失了。

    如果使用 Guava 是怎样的操作方式呢?Guava 提供了 Splitter 类,并且有一系列的操作方式可以直观的控制分割逻辑。

    
    String str = ",a ,,b ,";
    IterableString split = Splitter.on(",")
        .omitEmptyStrings() // 忽略空值
        .trimResults() // 过滤结果中的空白
        .split(str);
    split.forEach(System.out::println);
    /**
     * a
     * b
     */
    

    缓存

    在开发中我们可能需要使用小规模的缓存,来提高访问速度。这时引入专业的缓存中间件可能又觉得浪费。现在可以了, Guava 中提供了简单的缓存类,且可以根据预计容量、过期时间等自动过期已经添加的元素。即使这样我们也要预估好可能占用的内存空间,以防内存占用过多。

    现在看一下在 Guava 中缓存该怎么用。

    
    @Test
    public void testCache() throws ExecutionException, InterruptedException {
    
        CacheLoader cacheLoader = new CacheLoaderString, Animal() {
            // 如果找不到元素,会调用这里
            @Override
            public Animal load(String s) {
                return null;
            }
        };
        LoadingCacheString, Animal loadingCache = CacheBuilder.newBuilder()
            .maximumSize(1000) // 容量
            .expireAfterWrite(3, TimeUnit.SECONDS) // 过期时间
            .removalListener(new MyRemovalListener()) // 失效监听器
            .build(cacheLoader); //
        loadingCache.put("狗", new Animal("旺财", 1));
        loadingCache.put("猫", new Animal("汤姆", 3));
        loadingCache.put("狼", new Animal("灰太狼", 4));
    
        loadingCache.invalidate("猫"); // 手动失效
    
        Animal animal = loadingCache.get("狼");
        System.out.println(animal);
        Thread.sleep(4 * 1000);
        // 狼已经自动过去,获取为 null 值报错
        System.out.println(loadingCache.get("狼"));
        /**
         * key=猫,value=Animal{name='汤姆', age=3},reason=EXPLICIT
         * Animal{name='灰太狼', age=4}
         * key=狗,value=Animal{name='旺财', age=1},reason=EXPIRED
         * key=狼,value=Animal{name='灰太狼', age=4},reason=EXPIRED
         *
         * com.google.common.cache.CacheLoader$InvalidCacheLoadException: CacheLoader returned null for key 狼.
         */
    }
    
    /**
     * 缓存移除监听器
     */
    class MyRemovalListener implements RemovalListenerString, Animal {
    
        @Override
        public void onRemoval(RemovalNotificationString, Animal notification) {
            String reason = String.format("key=%s,value=%s,reason=%s", notification.getKey(), notification.getValue(), notification.getCause());
            System.out.println(reason);
        }
    }
    
    class Animal {
        private String name;
        private Integer age;
    
        @Override
        public String toString() {
            return "Animal{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
        }
    
        public Animal(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
    }
    

    这个例子中主要分为 CacheLoader、MyRemovalListener、LoadingCache。

    CacheLoader 中重写了 load 方法,这个方法会在查询缓存没有命中时被调用,我这里直接返回了 null,其实这样会在没有命中时抛出 CacheLoader returned null for key 异常信息。

    MyRemovalListener 作为缓存元素失效时的监听类,在有元素缓存失效时会自动调用 onRemoval 方法,这里需要注意的是这个方法是同步方法,如果这里耗时较长,会阻塞直到处理完成。

    LoadingCache 就是缓存的主要操作对象了,常用的就是其中的 put get 方法了。

    总结

    上面介绍了我认为最常用的 Guava 功能,Guava 作为 Google 公司开源的 Java 开发核心库,个人觉得实用性还是很高的。引入后不仅能快速的实现一些开发中常用的功能,而且还可以让代码更加的优雅简洁。我觉得适用于每一个 Java 项目。Guava 的其他的功能你也可以自己去发现。它的 Github 地址是:https://github.com/google/guava.

    参考

  • https://github.com/google/guava/wiki
  • END

    推荐好文

    强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

    为什么MySQL不推荐使用uuid或者雪花id作为主键?

    为什么建议大家使用 Linux 开发?爽(外加七个感叹号)

    IntelliJ IDEA 15款 神级超级牛逼插件推荐(自用,真的超级牛逼)

    炫酷,SpringBoot+Echarts实现用户访问地图可视化(附源码)

    记一次由Redis分布式锁造成的重大事故,避免以后踩坑!

    十分钟学会使用 Elasticsearch 优雅搭建自己的搜索系统(附源码)

    Guava - 拯救垃圾代码,写出优雅高效,效率提升N倍

    原文始发于微信公众号(Java知音):Guava - 拯救垃圾代码,写出优雅高效,效率提升N倍

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> Guava – 拯救垃圾代码,写出优雅高效,效率提升N倍


     上一篇
    New Object()到底占用几个字节,看完这篇就彻底明白了 New Object()到底占用几个字节,看完这篇就彻底明白了
    前言Java虚拟机栈是线程私有的,没有数据安全问题,而堆相比较于Java虚拟机栈而言更为复杂,因为堆是所有线程共享的一块内存空间,会出现线程安全性问题,而垃圾回收也主要是回收堆内空间,所以堆内的布局我们非常有必要深入去了解一下。现在就让
    下一篇 
    五分钟,带你彻底掌握 MyBatis缓存 工作原理 五分钟,带你彻底掌握 MyBatis缓存 工作原理
    作者:双子孤狼 blog.csdn.net/zwx900102/article/details/108696005 前言在计算机的世界中,缓存无处不在,操作系统有操作系统的缓存,数据库也会有数据库