guava Google 公司开源的 Java 核心库
😋 Guava 项目是 Google 公司开源的 Java 核心库,它主要是包含一些在 Java 开发中经常使用到的功能
2022/5/25 10:25:37
➡️

guava

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

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.0-jre</version>
</dependency>

集合

不可变集合

不可变对象有很多优点,包括:

  • 不受信任的库可以安全使用。
  • 线程安全:可以被许多线程使用,没有竞争条件的风险。
  • 不需要支持突变,并且可以通过该假设节省时间和空间。所有不可变集合实现都比它们的可变兄弟更节省内存。
  • 可以用作常数,期望它保持不变。
  • 不支持 null 值
  • 中途也是不可变的

制作对象的不可变副本是一种很好的防御性编程技术。Guava 为每种标准类型提供简单易用的不可变版本 Collection,包括 Guava 自己的Collection变体。

创建不可变集合的三种方法

// jdk 提供的不可变集合,在语法层面没有提供类似 c# 的 readonly 这样的关键字,而是通过新增加集合类型来实现
// JDK 提供了Collections.unmodifiableXXX方法,但在我们看来,这些可以是
// 笨拙且冗长;在你想制作防御性副本的任何地方使用都不愉快
// 不安全:只有在没有人持有对原始集合的引用时,返回的集合才是真正不可变的
// 低效:数据结构仍然具有可变集合的所有开销,包括并发修改检查、哈希表中的额外空间等。
{
    Set<String> set = new HashSet<>();
    set.add("a");
    set.add("a");
    set.add("b");
    // jdk 的不可变集合支持 null
    set.add(null);
    var  immutableSet=   Collections.unmodifiableSet(set);
    //不能被修改,修改就报错 UnsupportedOperationException
    //immutableSet.add("c");
    //对原集合的修改,不可变集合会受影响,是引用
    set.add("c");
    immutableSet.forEach(out::println); // a b c
}

// 方法1 of
//不可变集合里存在重复元素也不会报错
{
    Set<String> immutableSet = ImmutableSet.of("a", "a", "b");
    //不能被修改,修改就报错 UnsupportedOperationException
    //immutableSet.add("a");
    immutableSet.forEach(out::println); // a b
}

//方法2 builder 构造器模式
{
    var build=ImmutableSet.<String>builder();
    build.add("a");
    build.add("a");
    build.add("b");
    build.build().forEach(out::println);// a b
}

// 方法3 拷贝来自其他集合
{
    var list  =new ArrayList<String>();
    list.add("a");
    list.add("a");
    list.add("b");
    //深拷贝
    var immutableSet=ImmutableSet.copyOf(list);
    list.add("c");
    //对原集合的修改,不可变集合不会受影响
    immutableSet.forEach(out::println);
}

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

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

guava 扩展了许多独有的集合便利性非常好

集合分组

Multimap的特点其实就是可以包含有几个重复Key的value,可以put进入多个不同value但是相同的key,但是又不会覆盖前面的内容

map

可变集合的分组,非常方便可以进行分组后获取数据

{
  ListMultimap<String,Integer> mapLIst= ArrayListMultimap.create();
  mapLIst.put("a",1);
  mapLIst.put("a",2);
  mapLIst.put("b",3);
  mapLIst.put("b",4);
  mapLIst.forEach((key,value)->{
      out.println("key:"+key+" , value:"+value);
  });
  // key:a , value:1
  // key:a , value:2
  // key:b , value:3
  // key:b , value:4
  
  var list= mapLIst.get("a");
  
  list.forEach(out::println);
  // 1
  // 2
}

不可变集合的分组,非常方便可以进行分组后获取数据

{
  var multimapList=ImmutableListMultimap.<String,Integer>of(
          "a",1,
          "a",2,
          "b",3,
          "b",4);

  multimapList.forEach((key,value)->{
      out.println("key:"+key+" , value:"+value);
  });
  // key:a , value:1
  // key:a , value:2
  // key:b , value:3
  // key:b , value:4
  var list= multimapList.get("a");
  list.forEach(out::println); // 1 2
}

table

Table<R,C,V> table = HashBasedTable.create();,由泛型可以看出,table由双主键R(行),C(列)共同决定,V是存储值 新增数据:table.put(R,C,V) 获取数据:V v = table.get(R,C) 遍历数据: Set set = table.rowKeySet(); Set set = table.columnKeySet();   

用 原生 steam 更方便

// 神奇的表格
{
@Data
@AllArgsConstructor
class TestScore {
  private String studentName;
  private String subject;
  /**
    * 年级
    */
  private String classGrade;
  private Double score;

  /**
    * 学期
    */
  private String semester;
}
TestScore t1 = new TestScore("张三", "语文", "三年二班", 80D, "2021上");
TestScore t2 = new TestScore("张三", "数学", "三年二班", 80D, "2021上");
TestScore t3 = new TestScore("李四", "语文", "三年二班", 100D, "2021上");
TestScore t4 = new TestScore("李四", "数学", "三年二班", 90D, "2021上");

var testScoreList = Lists.newArrayList(t1, t2, t3, t4);


// 统计每个学生的总分
{
  //模拟 excel 表格 通过行索引和列索引定位唯一的单元格
  var table = HashBasedTable.<String, String, Double>create();
  testScoreList.forEach(p -> {
    // 行 列 单个格的值
    table.put(p.getStudentName(), p.getSubject(), p.getScore());
  });

  //按行统计
  var rows = table.rowMap();
  var newMap = Maps.<String, Double>newHashMap();
  rows.forEach((p, q) -> {
    var count = q.values().stream().mapToDouble(l -> l).sum();
    newMap.put(p, count);
  });
  newMap.forEach((p, q) -> {
    out.println(p + ":" + q);
    //李四:190.0
    //张三:160.0
  });
}

// 统计每个学生的总分
//不使用框架实现,通过 stream 实现,代码量差不多
{
  var groups = testScoreList.stream().collect(Collectors.groupingBy(p -> p.getStudentName()));
  var newMap = new HashMap<String, Double>();
  groups.forEach((p, q) -> {
    var count = q.stream().mapToDouble(l -> l.score).sum();
    newMap.put(p, count);
  });
  newMap.forEach((p, q) -> {
    out.println(p + ":" + q);
    //李四:190.0
    //张三:160.0
  });
}

//计算科目总分
{
  //模拟 excel 表格 通过行索引和列索引定位唯一的单元格
  var table = HashBasedTable.<String, String, Double>create();
  testScoreList.forEach(p -> {
    // 行 列 单个格的值
    table.put(p.getStudentName(), p.getSubject(), p.getScore());
  });

  //按列统计
  var cols = table.columnMap();
  var newMap = Maps.<String, Double>newHashMap();
  cols.forEach((p, q) -> {
    var count = q.values().stream().mapToDouble(l -> l).sum();
    newMap.put(p, count);
  });
  newMap.forEach((p, q) -> {
    out.println(p + ":" + q);
  });
}

// 统计每个学科的总分
//不使用框架实现,通过 stream 实现,代码量差不多
{
  var groups = testScoreList.stream().collect(Collectors.groupingBy(p -> p.getSubject()));
  var newMap = new HashMap<String, Double>();
  groups.forEach((p, q) -> {
    var count = q.stream().mapToDouble(l -> l.score).sum();
    newMap.put(p, count);
  });
  newMap.forEach((p, q) -> {
    out.println(p + ":" + q);
  });
}


// 不使用框架实现,通过 stream 实现,代码量可以更少
{
  var groups = testScoreList.stream().collect(Collectors.groupingBy(
      p -> p.getSubject(),Collectors.summingDouble(p->p.score)));
  groups.forEach((p, q) -> {
    out.println(p + ":" + q);
  });
}

BiMap

用处不大

// BiMap提供双向关系维护,但是业务上需要保证值唯一,如果不唯一,需使用forcePut
// key 和 value 都是唯一的,可以通过 key 找 value,也可以通过 value 找 key
{
  var biMap = HashBiMap.<String, Integer>create();
  biMap.put("a", 1);
  biMap.forcePut("b", 1);
  out.println(biMap.get('a'));
  out.println(biMap.inverse().get(1));
}

工厂方法

快速的初始化集合

// JDK
{
  var unModifyList =  List.of(1,2,3);

  //要做初始化,只能是这种封锁的方式
  var list = new ArrayList<Integer>() {
    {
      add(1);
      add(2);
      add(3);
    }
  };
}


//guava工厂模式
{
  var list = Lists.newArrayList(1,2,3);
  list.add(4);
  list.forEach(System.out::println);
}

集合交集,并集,差集

guava 缓存

缓存分为本地缓存与分布式缓存。本地缓存为了保证线程安全问题,一般使用ConcurrentMap的方式保存在内存之中,而常见的分布式缓存则有Redis,MongoDB等。 缓存在各种用例中都非常有用。例如,当一个值的计算或检索成本很高时,您应该考虑使用缓存,并且您将多次在某个输入上需要它的值

Guava缓存适用于以下情况:

  • 愿意花费一些内存来提高速度。
  • 使用场景有时会多次查询key。
  • 缓存将不需要存储超出RAM容量的数据
  • 线程安全的缓存,与ConcurrentMap相似,但前者增加了更多的元素失效策略,后者只能显示的移除元素。
  • Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。定时回收有两种:按照写入时间,最早写入的最先回收;按照访问时间,最早访问的最早回收;
  • 监控缓存加载/命中情况

Guava提供了设置并发级别的API,使得缓存支持并发的写入和读取。与ConcurrentHashMap类似,Guava cache的并发也是通过分离锁实现。在通常情况下,推荐将并发级别设置为服务器cpu核心数, Cache有区别于ConcurrentHashMap的使用,就是因为其自带有自动刷新和自动失效的功能,避免我们去自己编写刷新和失效的后台线程程,内存缓存模块,用于将数据缓存到JVM内存中

失效时间,存活时间策略可以单独设置或组合配置

  • expireAfterWrite 自条目创建后 经过指定的持续时间或值的最新替换后,使条目过期。--常用,多个线程是线程之间会互相等待
  • refreshAfterWrite 是指在创建缓存后,如果经过一定时间没有更新或覆盖,则会在下一次获取该值的时候,会在后台异步去刷新缓存, 如果新的缓存值还没有load到时,则会先返回旧值 --常用 比 expireAfterWrite 性能好,线程之间不会等待。
  • expireAfterAccess 仅在自上次通过读取或写入访问条目 后经过指定持续时间后才使条目过期,只要对条目读取或写入过就进行重新计时,多个线程是线程之间会互相等待

添加,插入key

  • get,要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值;
  • getUnchecked CacheLoader 会抛异常,定义的CacheLoader没有声明任何检查型异常,则可以 getUnchecked 查找缓存;反之不能;
  • getAll,方法用来执行批量查询;
  • put,向缓存显式插入值,Cache.asMap()也能修改值,但不具原子性;
  • getIfPresent,从现有的缓存中获取,如果缓存中有key,则返回value,如果没有则返回null

清除key,guava cache 自带清除机制,但仍旧可以手动清除:

  • 个别清除:Cache.invalidate(key)
  • 批量清除:Cache.invalidateAll(keys)
  • 清除所有缓存项:Cache.invalidateAll()

可以通过 asMao() 获取底层的 map

防止缓存击穿

load过程

从图上容易看出,每次只有找不到值都会进入load方法,换句话说,只有发生“取值”操作,才会执行load,然而为了防止“缓存穿透”,在多线程的环境下,任何时刻>只允许一个线程操作执行load操作

但在执行load操作这个步骤,expire 与 refresh 的线程机制不同:

  • expire 在load 阶段——同步机制:当前线程load未完成,其他线程呈阻塞状态,待当前线程load完成,其他线程均需进行”获得锁--获得值--释放锁“的过程。这种 方法会让性能有一定的损耗。但是所有线程获取的都是最新的值
  • refresh 在load阶段——异步机制 :当前线程load未完成,其他线程仍可以取原来的值,等当前线程load完成后,下次某线程再取值时,会判断系统时间间隔是否 超过设定refresh时间,来决定是否设定新值。所以,refresh机制的特点是,设定30分钟刷新,30min后并不一定就是立马就能保证取到新值。 能够想象,只要refresh得时间小于expire时间,就能保证多线程在load取值时不阻塞,也能保证refresh时间到期后,取旧值向新值得平滑过渡,当然,仍旧不能解决取到旧值得问题。

基于过期时间的最简单的缓存

put 和 getIfPresent 配合使用

  • put 更新缓存
  • getIfPresent 从现有的缓存中获取,如果缓存中有key,则返回value,如果没有则返回null

expireAfterAccess

var cache=  CacheBuilder.newBuilder()
      // 设置并发级别为cpu核心数,默认为4,并发级别是指可以同时写缓存的线程数
      .concurrencyLevel(Runtime.getRuntime().availableProcessors())
      // 初始记录数,最大记录数
    .initialCapacity(10).maximumSize(20)
    .expireAfterAccess(Duration.ofSeconds(1))
    //.expireAfterWrite(Duration.ofSeconds(1))
    .build();

//写入后重新计算时间
cache.put("a",1);
Thread.sleep(900);
//读取后重新计算时间
System.out.println(cache.getIfPresent("a"));
//1

Thread.sleep(900);
//读取后重新计算时间
System.out.println(cache.getIfPresent("a"));
//1

Thread.sleep(1000);
//过期了
System.out.println(cache.getIfPresent("a"));
//null
//写入后重新计算时间
cache.put("a",1);
Thread.sleep(900);

//写入后重新计算时间
cache.put("a",2);
Thread.sleep(900);
//读取后重新计算时间
System.out.println(cache.getIfPresent("a"));
//2

Thread.sleep(1000);
System.out.println(cache.getIfPresent("a"));
//null

expireAfterWrite

 var cache=  CacheBuilder.newBuilder()
           // 设置并发级别为cpu核心数,默认为4
           .concurrencyLevel(Runtime.getRuntime().availableProcessors())
           // 初始记录数,最大记录数
          .initialCapacity(10).maximumSize(20)
          //.expireAfterAccess(Duration.ofSeconds(1))
          .expireAfterWrite(Duration.ofSeconds(1))
          .build();

  // 自条目创建后,开始计时,后面不会在更新时间
  cache.put("a",1);
  Thread.sleep(900);
  System.out.println(cache.getIfPresent("a"));
  // 1
  Thread.sleep(900);
  System.out.println(cache.getIfPresent("a"));
  // null

  Thread.sleep(1000);
  System.out.println(cache.getIfPresent("a"));
      //null

加载 cache

全局缓存 CacheLoader

先取缓存——娶不到再执行load

CacheLoader 和 get 配合使用

  • 使用 get 要么返回已经缓存的值,要么使用 CacheLoader 向缓存原子地加载新值
  • 任然可以使用 put 更新缓存值
  • 在这种情况下使用 getIfPresent 没有意义
{
    var cache = CacheBuilder.newBuilder()
        // 设置并发级别为cpu核心数,默认为4
        .concurrencyLevel(Runtime.getRuntime().availableProcessors())
        // 初始记录数,最大记录数
        .initialCapacity(10).maximumSize(20)
        .expireAfterAccess(Duration.ofSeconds(1))
        //.expireAfterWrite(Duration.ofSeconds(1))
        .build(new CacheLoader<String, Optional<String>>() {
                  @Override
                  public Optional<String> load(String s)  {
                    String tmp = null;
                    switch (s) {
                      case "a":
                        tmp = "a:"+LocalDateTime.now().toLocalTime().toString();
                        break;
                      default:
                        break;
                    }
                    return Optional.ofNullable(tmp);
                  }
                }
        );
    //没有缓存 load
    System.out.println(cache.get("a").isPresent()?  cache.get("a").get():null);
    //a:10:28:19.755542900
    Thread.sleep(900);
    //缓存有,用缓存
    System.out.println(cache.get("a").isPresent()?  cache.get("a").get():null);
     //a:10:28:19.755542900
    Thread.sleep(1000);
    //缓存过期,再次 load
    System.out.println(cache.get("a").isPresent()?  cache.get("a").get():null);
    a:10:28:21.699160500
    //缓存不存在,load 
    System.out.println( cache.get("b").isPresent()?  cache.get("a").get():null);
  }
}

局部 item Callable

Callable 和 get 配合使用

  • 使用 get 要么返回已经缓存的值,要么使用 Callable 向缓存原子地加载新值
  • 任然可以使用 put 更新缓存值
  • 在这种情况下使用 getIfPresent 没有意义
{
  var cache = CacheBuilder.newBuilder()
      // 设置并发级别为cpu核心数,默认为4
      .concurrencyLevel(Runtime.getRuntime().availableProcessors())
      // 初始记录数,最大记录数
      .initialCapacity(10).maximumSize(20)
      .expireAfterAccess(Duration.ofSeconds(1))
      //.expireAfterWrite(Duration.ofSeconds(1))
      .build();
  
  //缓存没有值,使用回调生成值
  System.out.println(cache.get("a", (Callable<String>) () -> 
         "a:" + LocalDateTime.now().toLocalTime().toString()));
  // a:11:14:19.617927100
  Thread.sleep(900);
  //这次获取的是缓存值
  System.out.println(cache.get("a", (Callable<String>) () -> 
         "a:" + LocalDateTime.now().toLocalTime().toString()));
  //a:11:14:19.617927100
}

refreshAfterWrite

多个线程都去load 的时候,refreshAfterWrite 可以只让一个线程去load,其他线程用旧值代替

{
   var cache = CacheBuilder.newBuilder()
       // 设置并发级别为cpu核心数,默认为4
       .concurrencyLevel(Runtime.getRuntime().availableProcessors())
       // 初始记录数,最大记录数
       .initialCapacity(10).maximumSize(20)
       //.expireAfterAccess(Duration.ofSeconds(1))
       //.expireAfterWrite(Duration.ofSeconds(1))
       .refreshAfterWrite(Duration.ofSeconds(1))
       .build(new CacheLoader<String, String>() {
         @Override
         public String load(String s) throws Exception {
           String tmp = "";
           switch (s) {
             case "a":
               Thread.sleep(5000);
               tmp = "a:" + LocalDateTime.now().toLocalTime().toString();
               break;
             default:
               break;
           }
           return tmp;
         }
       });

   cache.put("a", "1");
   //获取缓存的值
   System.out.println(cache.get("a"));
   //1

   //缓存失效,load
   var thread1 = new Thread(new Runnable() {
     @SneakyThrows
     @Override
     public void run() {
       Thread.sleep(1000);
       System.out.println(Thread.currentThread().getName() +","+ cache.get("a"));
       //thread1,a:13:45:53.725228700
     }
   });
   thread1.setName("thread1");

   //缓存失效,用旧值替代,因为已经有一个线程已经在load 处于阻塞状态,这个线程直接返回旧值
   var thread2 = new Thread(new Runnable() {
     @SneakyThrows
     @Override
     public void run() {
       Thread.sleep(1000);
       System.out.println(Thread.currentThread().getName()  +","+ cache.get("a"));
       //thread2,1
     }
   });
   thread2.setName("thread2");

   thread1.start();thread2.start();
 }
}

弱引用

通过weakKeys和weakValues方法,可以指定Cache只保存“对缓存记录key和value的弱引用”。这样,当没有其他强引用指向key和value时,key和value对象就会被垃圾回收器回收。

{
  var cache = CacheBuilder.newBuilder()
      // 设置并发级别为cpu核心数,默认为4
      .concurrencyLevel(Runtime.getRuntime().availableProcessors())
      // 初始记录数,最大记录数
      .initialCapacity(10).maximumSize(20)
      //.expireAfterAccess(Duration.ofSeconds(1))
      .expireAfterWrite(Duration.ofSeconds(1))
      //对 key 进行弱引用
      .weakValues()
      .build();
  var value =new Object();
  //这里相当于一种绑定关系,把堆里的对象绑定给 key,但是这种绑定是弱引用的关系
  cache.put("a", value);
  
  //原来的栈指向新的对象
  value=new Object();
  //回收弱引用对象
  System.gc();
  
  System.out.println(cache.getIfPresent("a"));
  //null
}

缓存移除监听器

  var cache = CacheBuilder.newBuilder()
  //同步移除
  .removalListener((notice)->{
              var  name=Thread.currentThread().getName();
              System.out.println(name+",监听到移除的 key:"+notice.getKey());
              System.out.println(name+",监听到移除的 value:"+notice.getValue());
          })

  //异步移除
  .removalListener(RemovalListeners.asynchronous(notice->{
    var  name=Thread.currentThread().getName();
      System.out.println(name+",监听到移除的 key:"+notice.getKey());
      System.out.println(name+",监听到移除的 value:"+notice.getValue());

  }, Executors.newSingleThreadExecutor()))
  .build();
  cache.invalidate("a");
   //缓存移除后,不会显式地立即删除,需要自己清除
  cache.cleanUp(); 

缓存统计

缓存统计功能,需要使用 recordStats() 打开缓存统计功能,没有发现好用 返回如下统计信息:

  • hitRate():缓存命中率;
  • hitMiss(): 缓存失误率;
  • loadcount() ; 加载次数;
  • averageLoadPenalty():加载新值的平均时间,单位为纳秒;
  • evictionCount():缓存项被回收的总数,不包括显式清除。

计算中间代码的运行时间

{
  var stopWatch = Stopwatch.createStarted();
  for (int i = 0; i < 10; i++)
    Thread.sleep(1000);
  var times= stopWatch.elapsed(TimeUnit.SECONDS);
  System.out.println(times);
  //10
}
👍🎉🎊