Java之工具集

Google Guava工具集简介

​ Guava工程包含了若干被Google的Java项目广泛依赖的核心库,例如:集合、缓存、原生类型支持、并发库、通用注解、字符串处理、I/O等等。

​ 所有这些工具每天都在被Google的工程师应用在产品服务中。

使用和避免null

大多数情况下,使用null表明的是某种缺失情况。

Guava引入Optional<T>表明可能为null的T类型引用。Optional实例可能包含非null的引用(引用存在),也可能什么也不包括(引用缺失)。

正是收到Guava的启发,Java8将Optional类作为一个新特性引入进Java8 的类库。

Optional的存在就是为了防止使用对象的过程中出现空值的情况,它是一种强制让我们关注空值处理的一种机制。

实战案例:Java8新特性Optional如何使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.java.example.guava;

import org.testng.annotations.Test;

import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

/**
* 学习Java8中Optional使用方法
*
* @author jingLv
* @date 2020/10/30
*/
public class OptionalTest {

@Test
public void test() {
// 三种创建Optional对象方法
// 1.创建空的Optional对象,里面是没有任何引用
Optional.empty();

// 2.使用非null值创建Optional对象,不接受null值的,如果传入null,会立即抛出空指针的异常
Optional.of("xiaohong");

// 3. 使用任意值创建Optional对象,可以接受null值
Optional optional = Optional.ofNullable("xiaohong");

// 判断是否引用缺失的方法(建议不直接使用),返回boolean类型判断是否为空
optional.isPresent();

// 当Optional引用非空时执行
// 类似的方法:map filter flatMap
optional.ifPresent(System.out::println);

// 当Optional引用缺失时执行
optional.orElse("引用缺失");

optional.orElseGet(() -> {
// 自定义引用缺失时的返回值
return "自定义引用缺失";
});

// optional.orElseThrow(() -> {
// // 这里返回一个异常
// throw new RuntimeException("引用缺失异常");
// });

// 以上使用方法错误,如果Optional引用缺失,orElseThrow的正确方式如下:
Optional.ofNullable(null).orElseThrow(() -> {
// 这里返回一个异常
return new RuntimeException("引用缺失异常");
});
}

public static void stream(List<String> list) {
// list.stream().forEach(System.out::println); 如果list为null会报空指针异常
Optional.ofNullable(list).map(List::stream).orElseGet(Stream::empty).forEach(System.out::println);
}

public static void main(String[] args) {
stream(null);
}
}

不可变集合

创建对象不可拷贝是一项很好的防御性编程技巧。

Guava为所有JDK标准集合类型和Guava新集合类型都提供了简单易用的不可变版本。

不可变对象的优点

  • 当对象被不可信的库调用是,不可变形式时安全的
  • 不可变对象被多个线程调用时,不存在竞态条件问题
  • 不可变集合不需要考虑变化,因此可以节省时间和空间
  • 不可变对象因为有固定不变,可以作为常量来安全使用

JDK也提供的unmodifiableXXX方法使集合变为不可变对象,但是呢,这种方式有如下的缺点:

  • 笨重而且累赘

  • 不安全

  • 低效

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

  • copyOf方法:ImmutableSet.copyOf(set)
  • of方法:ImmutableSet.of("a", "b", "c")
  • Builder工具:ImmutableSet.builder().build()

实战案例:代码演示

  • 可变集合与不可变集合

    • 可变集合演示

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      package com.java.example.guava;

      import java.util.ArrayList;
      import java.util.List;

      /**
      * 不可变集合用法
      *
      * @author jingLv
      * @date 2020/10/30
      */
      public class ImmutableTest {

      public static void test(List<Integer> list) {
      // 移除list第一个元素
      list.remove(0);
      }

      public static void main(String[] args) {
      List<Integer> list = new ArrayList<>();

      list.add(1);
      list.add(2);
      list.add(3);
      // 调用test方法
      test(list);

      System.out.println(list);
      }
      }

      执行结果

      image-20201102115438212

      从上面的执行结果,看到结合被改变了

    • 不可变集合演示

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      package com.java.example.guava;

      import java.util.ArrayList;
      import java.util.Collections;
      import java.util.List;

      /**
      * 不可变集合用法
      *
      * @author jingLv
      * @date 2020/10/30
      */
      public class ImmutableTest {

      public static void test(List<Integer> list) {
      // 移除list第一个元素
      list.remove(0);
      }

      public static void main(String[] args) {
      List<Integer> list = new ArrayList<>();

      list.add(1);
      list.add(2);
      list.add(3);

      // 设置不可变的list
      List<Integer> newList = Collections.unmodifiableList(list);

      test(newList);

      System.out.println(newList);
      }
      }

      执行结果

      image-20201102115639093

      将list设置为不可变集合后,在进行集合元素的删除操作是,抛出异常:UnsupportedOperationException

  • 创建不可变集合的三种方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    package com.java.example.guava;

    import com.google.common.collect.ImmutableSet;
    import com.google.common.collect.Sets;

    import java.util.ArrayList;
    import java.util.List;

    /**
    * 不可变集合用法
    *
    * @author jingLv
    * @date 2020/10/30
    */
    public class ImmutableTest {

    public void immutable() {
    List<Integer> list = new ArrayList<>();

    list.add(1);
    list.add(2);
    list.add(3);

    // 构造不可变集合对象三种方式
    // 1. 通过已存在的集合创建
    ImmutableSet<Integer> integers = ImmutableSet.copyOf(list);

    // 2. 通过初始值,直接创建不可变集合
    ImmutableSet<Integer> of = ImmutableSet.of(1, 2, 3);

    // 3. 以builder方式创建
    ImmutableSet<Object> build = ImmutableSet.builder()
    .add(1)
    .addAll(Sets.newHashSet(2, 3))
    .add(4).build();
    }
    }

    新集合类型

Guava引入了很多JDK没有的,但明显有用新集合类型。这些新类型是为了和JDK集合框架共存,而没有往JDK集合抽象中硬塞其他概念。

Multiset

  • Multiset两门性
    • 没有元素顺序限制的ArrayList(E)
      • add(E):添加单个给定元素
      • iterator():返回一个迭代器,包含Multiset所有元素(包括重复元素)
      • size(): 返回所有元素的总个数(包括重复元素)
    • Map<E, Integer>,键为元素,值为计数
      • count(Object):返回给定元素的计数
      • entrySet():返回Set<Multiset.Entry<E>>,和Map的entrySet类似
      • elementSet():返回所有不重复元素的Set<E>,和Map的keySet类似
  • Multiset与Map的区别
    • 元素计数只能是正数
    • multiset.size()返回集合大小
    • multiset.iterator()会迭代重复元素
    • multiset支持直接设置元素的计数
    • 没有的元素,multiset.count(E)为0
  • 多种Multiset(是一个接口)的实现
    • HashMultiset
    • TreeMultiset
    • LinkedHashMultiset
    • ConcurrentHashMultiset
    • ImmutableMultiset

实战案例:Multiset使用

使用Multiset集合类,实现统计一篇文章中文字出现次数功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.java.example.guava;

import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
import com.google.common.primitives.Chars;
import org.testng.annotations.Test;

/**
* 实现:使用Multiset统计一首古诗的文字出现频率
*
* @author jingLv
* @date 2020/11/02
*/
public class MultisetTest {

private static final String text =
"《南陵别儿童入京》" +
"白酒新熟山中归,黄鸡啄黍秋正肥。" +
"呼童烹鸡酌白酒,儿女嬉笑牵人衣。" +
"高歌取醉欲自慰,起舞落日争光辉。" +
"游说万乘苦不早,著鞭跨马涉远道。" +
"会稽愚妇轻买臣,余亦辞家西入秦。" +
"仰天大笑出门去,我辈岂是蓬蒿人。";

@Test
public void handle() {
// 创建multiset实例
Multiset<Character> multiset = HashMultiset.create();

// string转换为char数组
char[] chars = text.toCharArray();

// 遍历数组,添加到multiset中
Chars.asList(chars).stream()
.forEach(charItem -> {
multiset.add(charItem);
});

System.out.println("size:" + multiset.size());
System.out.println("count:" + multiset.count('人'));
}
}

集合工具类

Guava为集合类提供了许多工具方法。这也是Guava最流行和成熟的部分之一。

常见的集合工具类如:Lists,Sets,Maps等。

实战案例:集合工具类

  • 使用Lists工具类操作List集合
  • 使用Sets工具类操作Set集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package com.java.example.guava;


import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.testng.annotations.Test;

import java.util.List;
import java.util.Set;


/**
* 测试集合的工具类
* Lists/Sets使用
*
* @author jingLv
* @date 2020/11/02
*/
public class SetsTest {
/**
* Sets工具类的常用方法
* 对两个集合可以进行:并集/交集/差集
* 分解集合中的所有子集
* 求两个集合的笛卡尔积
* <p>
* Lists工具类的常用方法
* 反转 / 拆分
*/

private static final Set set1 = Sets.newHashSet(1, 2, 3, 4);
private static final Set set2 = Sets.newHashSet(4, 5, 6);

/**
* 并集,集合合并
*/
@Test
public void union() {
Set<Integer> set = Sets.union(set1, set2);
System.out.println(set);
}

/**
* 交集
*/
@Test
public void intersection() {
Set<Integer> set = Sets.intersection(set1, set2);
System.out.println(set);
}

/**
* 差集:如果元素属于A而且不属于B
*/
@Test
public void difference() {
Set<Integer> set = Sets.difference(set1, set2);
System.out.println(set);
// 相对差集:属于A而且不属于B或者属于B而且不属于A
set = Sets.symmetricDifference(set1, set2);
System.out.println(set);
}

/**
* 分解集合中的所有子集
*/
@Test
public void powerSet() {
Set<Set<Integer>> powerSet = Sets.powerSet(set1);
System.out.println(JSON.toJSONString(powerSet, true));
}

/**
* 计算两个集合的笛卡尔积
*/
@Test
public void cartesianProduct() {
Set<List<Integer>> set = Sets.cartesianProduct(set1, set2);
System.out.println(JSON.toJSONString(set));
}

/**
* 拆分
*/
@Test
public void partition() {
List<Integer> list = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7);
// 以三个元素为一组进行拆分
List<List<Integer>> partition = Lists.partition(list, 3);
System.out.println(JSON.toJSONString(partition));
}

/**
* 反转
*/
@Test
public void reverse() {
List<Integer> list = Lists.newLinkedList();
list.add(1);
list.add(2);
list.add(3);

List<Integer> newList = Lists.reverse(list);
System.out.println(newList);
}
}

IO流

对字节流/字符流提供的工具方法

  • ByteStreams:提供对InputStream/OutputStream的操作
  • CharStreams:提供多Reader/Writer的操作

对源(Sourece)与汇(Sink)的抽象

  • 源是可读的:ByteSource/CharSource
  • 汇是可写的:ByteSink/CharSink

实战案例:文件操作

利用Guava提供的相关工具类,实现对文件的常见操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.java.example.guava;

import com.google.common.base.Charsets;
import com.google.common.io.CharSink;
import com.google.common.io.CharSource;
import com.google.common.io.Files;
import org.testng.annotations.Test;

import java.io.File;
import java.io.IOException;

/**
* 演示如何使用流(Source)与汇(Sink)来对文件进行常用操作
*
* @author jingLv
* @date 2020/11/02
*/
public class IOTest {

@Test
public void copyFile() {
// 1. 创建对应的source和sink
CharSource charSource = Files.asCharSource(new File("原文件路径"), Charsets.UTF_8);
CharSink charSink = Files.asCharSink(new File("拷贝文件路径"), Charsets.UTF_8);

// 2. 拷贝
try {
charSource.copyTo(charSink);
} catch (IOException e) {
e.printStackTrace();
}
}
}

布隆过滤器

在软件开发时,经常要判断一个元素是否在一个集合中。

如何实现检查一个单词是否拼写正确?

​ – 摘自《数学之美》

常见的实现方式:

  • 方法一:使用一个集合对象,缓存字典中所有单词。判断被检查的单词是否存在于集合之中。
  • 方法二:使用数据库,将字典中所有单词做成数据记录。使用被检查的单词来索引数据。

常见实现的缺点:

  • 字典中单词量大时,消耗很多系统资源
  • 检索性能不好,容易出现瓶颈
  • 需要存储大量无用数据,容易造成数据泄露风险

布隆过滤器

布隆过滤器(Bloom Filter)是由伯顿·布隆(Burton Bloom)与1970年提出的。

它实际上是由一个很长的二进制向量和一系列随机映射函数构成。

组成及原理

image-20201102184703838

  1. 一个输入信息进来后,会分别进入到下面的随机函数(f1,f2,f3)中
  2. 由随机函数处理后,得到一个数值,f1得到的数值g1,假设g1找到数组中下标为2的位置,将数组下标为2的值从0置为1
  3. 同样的,f2得到的g2的值,将该位置的值从0置为1,同理f3和g3是一样的

添加元素过程

image-20201105142043248

  1. 假设添加spring这个词到布隆过滤器中,spring通过随机函数f1会生成一个地址,这个地址找到数组的位置,将这个数组的0置为1

  2. spring在经过随机函数f2再 会生成一个地址,将这个地址找到数组位置的值从0置为1

  3. 随机函数f3,同上的过程

    image-20201105142257115

  4. 又加一个单词boot,通过三个随机函数,生成三个位置,恰好spring的g3位置与boot的g3位置一样,先不用管这种情况,我们只关心将位置的数组的值从0置为1

  5. 这时数组中的有五个位置的值为1

检测元素过程

image-20201105142351743

  1. 使用springboot这个词到数组中检测是否存在
  2. springboot这个词经过三个随机函数,生成三个位置
  3. 当随机函数f1处理位置的数为0时,这时说明springboot没有添加到布隆过滤器中
  4. 同理f2,f3处理的位置的数都为0,就说明springboot不是一个正确的单词,因为正确的单词已经添加进来了,处理位置的值应为1

检测元素假阳性

image-20201105142629568

  1. 使用boting这个词进行检测,首先这是一个错误的词,但是在随机函数处理后,都指向了数组位置为1的地方 ,这说明这个词是在布隆过滤器中,因而判断为正确的值
  2. 这就是布隆过滤器的缺陷,就是检测的假阳性

实战案例:Guava单机版布隆过滤器

Guava工具集中提供的单机版布隆过滤器的使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.java.example.guava;

import com.google.common.hash.BloomFilter;
import com.google.common.hash.PrimitiveSink;
import org.testng.annotations.Test;

/**
* 布隆过滤器使用
*
* @author jingLv
* @date 2020/11/02
*/
public class BloomFilterTest {

@Test
public void bloomFilter() {
BloomFilter<Integer> bloomFilter = BloomFilter.create(
// 处理的数据,将任意类型数据转换为Java基础类型,默认转换为byte数组
(Integer from, PrimitiveSink primitiveSink) -> primitiveSink.putInt(from),
// 预计插入的元素总数
10000L,
// 期望误判率(0.0~1.0)
0.1
);

// 向布隆过滤器中添加元素
for (int i = 0; i < 10000; i++) {
bloomFilter.put(i);
}
// 检查给定元素是否可能存在在布隆过滤器中
boolean mightContain = bloomFilter.mightContain(66666);
System.out.println("是否存在?" + mightContain);
}
}

注意:66666不在10000以内,但是误判率很高时例如:0.9,就会返回true

布隆过滤器优点

  • 不需要存储数据本身,节省资源,并且可以保证信息安全
  • 时间效率高,插入和查找的时间复杂度均为O(k)。k是函数个数
  • Hash函数之间相互独立,可以在硬件指令层面并行计算

布隆过滤器缺点

  • 存在假阳性,不适用于任何要求100%准确率的场景
  • 只能插入和查询元素,不能删除元素。主要是不能判断要删除的元素是否一定在集合中

布隆过滤器的应用场景

  • 字处理软件中,检查一个单词是否拼写正确
  • 网络爬虫里,一个网址是否被访问过
  • 垃圾邮件过滤功能
  • Hbase中根据RowKey查询数据时