这将是Guava库学习系列的最后一篇,但是仍然包含许多零零碎碎的知识。虽然不可能覆盖所有Guava涉及的知识,但我们会竭尽所能。本篇将会介绍一些Guava中有用的工具,并不需要再开一个系列。本篇学习的一些工具可能并不会经常使用,但当你需要时,它是必不可少的。接下来,开始本篇的学习。 本篇,我们将主要学习以下内容:Hashing、BloomFilter、Optional、Throwable。
Hashing散列类包含静态实用方法获取HashFunction实例
BloomFilter数据结构,用于判断一个元素是否存在于一个Set集合中,BloomFilter数据结构有独特的属性,它可以明确返回一个元素不存在,不能确保一个元素肯定存在。
Optional类为我们提供了一种使用null引用的方式
Throwable类提供了一些静态实用方法处理Throwable的实例
创建合适的Hash函数
散列Hash函数在编程时很基础,它用于确定身份和检查重复。另外,它对于正确使用Java集合至关重要。散列函数工作原理是提供各种长度的数据并将它们映射为数字。因为我们打算将任意数据映射到数字,所以保证Hash函数的耐碰撞性是至关重要的。换句话说,就是我们要避免为不同的数据产生同样的编号。当然了要写一个优秀的哈希函数,最好是留给专家来做。幸运的是,通过Guava,我们不需要编写自己的Hash函数。Hashing类提供了静态的方法来创建HashFunction实例和一些需要注意的类型。
校验总和Hash函数
Guava提供了两种HashFunction类,实现众所周知的校验总和算法:Adler-32和CRC-32。通过如下代码,可以为HashFunction创建一个实例:
HashFunction adler32 = Hashing.adler32();
HashFunction crc32 = Hashing.crc32();
上面,我们简单的做了一个Hashing类静态方法的调用,来说明所需的HashFuction的实现。
一般Hash函数
接下来我们将调用一般的Hash函数,一般的散列函数非加密,适合用于基于散列的查询任务。第一个是murmur Hash算法,由Austin Appleby创建于2008年,其他一般的散列函数称为goodFastHash。下面来看怎样创建这些一般性Hash函数:
HashFunction gfh = Hashing.goodFastHash(128);
HashFunction murmur3_32 = Hashing.murmur3_32();
HashFunction murmur3_128 = Hashing.murmur3_128();
goodFastHash方法返回一个最小包含128长度bit位,一个字节有8个bit,因此调用goodFastHash 至少返回16个字节(128 / 8)。接下来,我们创建了两个murmur Hash实例,第一个murmur Hash实例是一个32位murmur3_32算法的实现,第二个murmur Hash实例是一个128位murmur3_128算法的实现。
加密Hash函数
加密哈希函数用于信息安全,对加密Hash函数进行完整描述超出了本文的范围,这里只做简单的学习。一般来说,加密哈希函数有以下属性:
数据的任何小变化,产生的Hash码都会发生大的变化
通过反向工程,根据Hash code值推算出原始的数据是不可能的
Guava提供了如下的方式创建加密Hash函数:
HashFunction sha1 = Hashing.sha1();
HashFunction sha256 = Hashing.sha256();
HashFunction sha512 = Hashing.sha512();
上面的三种Hash算法实现了sha1,sha256,sha512三种加密算法。
Bloom Filter
Bloom Filter是一个独特的数据结构,用来确定一个元素是否存在于一个集合中。有意思的一点是,它能准确的判断一个元素不存在,不能准确的判断元素存在。这种特性可以用在比较耗时的操作中,如磁盘检索。
BloomFilter简述
BloomFilter本质上是位向量。它以如下方式工作:
添加一个元素到filter中
将这个元素进行多次Hash运算,将得到的hash值的bit位设置为1
当判断一个元素是否存在在set中时,按照同样的方法进行多次hash,并判断bit位设置的是1还是0。这就是BloomFilter确保一个元素不存在的过程。 如果bit位不为1,就说明这个元素不存在于集合中,相反,即使元素位都是为1,也不能确定元素存在于集合中,因为可能在之前已经发生了hash碰撞。在 学习Guava BloomFilter的创建和使用之前,我们需要了解如何得到对象的字节并读入BloomFilter进行hash运算。
Funnels 和 PrimitiveSinks
Funnel接口接收一个确定类型的对象,并将数据发送给PrimitiveSinks实例。PrimitiveSinks对象用来接收原始类型的数据。PrimitiveSinks实例将抽取hash运算所需的字节数。BloomFilter中使用Funnel接口来抽取那些在BloomFilter数据结构中等待hash的元素的字节。来看下面的例子:
public enum BookFunnel implements Funnel<Book> {
//This is the single enum value
FUNNEL;
public void funnel(Book from, PrimitiveSink into) {
into.putBytes(from.getIsbn().getBytes(Charsets.UTF_8))
.putDouble(from.getPrice());
}
}
上面的例子中,我们创建了一个简单的Funnel实例来接收Book实例。需要注意的是,我们通过枚举实现了Funnel接口,这有助于保持BloomFilter的序列化,也需要Funnel实例是可序列化的。ISBN和Price被放入到PrimitiveSink实例作为Hash函数的入参。
创建BloomFilter实例
上面我们了解了如何创建Funnel实例,下面介绍如何创建BloomFilter实例:
BloomFilter<Book> bloomFilter = BloomFilter.create(BookFunnel.FUNNEL, 5);
上面的例子中,我们通过调用BloomFilter静态的create方法,传入Funnel实例和一个表示int值,这个值表示BloomFilter中使用hash函数的个数。如果使用的hash函数的个数大大超过了,假阳性的数量将大幅上升。我们来看一个创建BloomFilter实例的例子:
@Test
public void testBloomFilter() throws IOException {
File booksPipeDelimited = new File("src/books.data");
List<Book> books = Files.readLines(booksPipeDelimited,
Charsets.UTF_8, new LineProcessor<List<Book>>() {
Splitter splitter = Splitter.on('|');
List<Book> books = Lists.newArrayList();
Book.Builder builder = new Book.Builder();
public boolean processLine(String line) throws IOException {
List<String> parts = Lists.newArrayList(splitter.split(line));
builder.author(parts.get(0))
.title(parts.get(1))
.publisher(parts.get(2))
.isbn(parts.get(3))
.price(Double.parseDouble(parts.get(4)));
books.add(builder.build());
return true;
}
@Override
public List<Book> getResult() {
return books;
}
});
BloomFilter<Book> bloomFilter = BloomFilter.create(BookFunnel.FUNNEL, 5);
for (Book book : books) {
bloomFilter.put(book);
}
Book newBook = new Book.Builder().title("Test Cook Book 2").build();
Book book1 = books.get(0);
System.out.println("book [" + book1.getTitle() + "] contained " + bloomFilter.mightContain(book1));
System.out.println("book [" + newBook.getTitle() + "] contained " + bloomFilter.mightContain(newBook));
}
测试结果如下:
book [Test Cook Book] contained true
book [Test Cook Book 2] contained false
在上面的例子中,我们通过Files.readLines方法以 | 为分隔符读取和使用文件,结合LineProcessor回调将每行的文本转换为Book对象。每一个Book对象都添加到List里面并返回。之后我们通过BookFunnel枚举和期望的hash次数 5,创建了一个BloomFilter实例。之后将所有的Book对象从list中添加到BloomFilter,最后,通过调用mightContain方法测试添加和未添加到BloomFilter的Book。
虽然我们可能不需要经常使用BloomFilter,但在工作中这是一个非常有用的工具。
Optional
空对象的处理比较麻烦,有很大一部分问题,都是由于我们认为一个方法返回的值可能不是null,但我们惊讶的发现对象居然为null,为了解决这个问题,Guava提供了一个Optional类,它是一个不可变对象,可能包含或不包含另一个对象的引用。如果Optional包含实例,它被认为是存在present,如果不包含实例,它被认为是缺席absent。Optional类比较好的使用方式是使用Optional作为方法的返回值。这样我们迫使客户端考虑返回值可能不存在,我们应该采取相应的措施防止此情况。
创建Optional实例
Optional类是抽象类,我们可以直接继承,我们可以使用它提供的一些静态方法来创建Optional实例,例如:
Optional.absent() ,返回一个空的Optional实例
Optional.of(T ref) ,返回一个包含Type ref的Optioanal实例
Optioanal.fromNullable(T ref) ,如果 ref不为null,那么返回一个包含Type ref的Optional实例,否则返回一个空的Optional实例
Optional.or(Supplier
supplier),如果引用存在,返回此引用,否则,返回Supplier.get。
我们来看一些简单的例子:
@Test
public void testOptionalOfInstance() {
Book book = new Book.Builder().build();
Optional<Book> bookOptional = Optional.of(book);
assertThat(bookOptional.isPresent(), is(true));
}
在上面的单元测试中,我们使用了静态的Optional.of方法对传入的对象装饰后,返回一个Optional实例。我们通过调用isPresent方 法来断言包含的对象存在(为true)。更有趣的是通过如下方式使用Optional.fromNullable方法:
@Test(expected = IllegalStateException.class)
public void testOptionalNull() {
Optional<Book> bookOptional = Optional.fromNullable(null);
assertThat(bookOptional.isPresent(), is(false));
bookOptional.get();
}
在上面的单元测试中,我们通过fromNullable静态方法创建了Optional实例,同样我们也返回了Optional实例,这里我们断言调用 isPresent方法返回的是false。之后,由于没有实例存在,我们断言调用get方法会抛出IllegalStateExeption异常。 Optional.fromNullable是很好的方法,用来在调用返回结果之前装饰对象。Optional的真正重要性是,它对于返回值是否存在是没 有保证的,它迫使我们必须去处理Null值的情况。
Throwables
Throwables类包含一些实用的静 态方法,用来处理在java中经常遇到的java.lang.Throwable、Errors 和 Exceptions错误。有的时候,有一个工具类去处理异常堆栈是很方便的,Throwables类给我们提供了方便。下面我们将介绍两个比较特别的方 法:Throwables.getCausalChain 和 Throwables.getRootCause。
获取Throwables异常链
Throwables.getCausalChain 方法返回一个Throwable对象集合,从堆栈的最顶层依次到最底层,来看下面的例子:
@Test
public void testGetCausalChain() {
ExecutorService executor = Executors.newSingleThreadExecutor();
List<Throwable> throwAbles = null;
Callable<FileInputStream> fileCallable = new Callable<FileInputStream>() {
@Override
public FileInputStream call() throws Exception {
return new FileInputStream("Bogus file");
}
};
Future<FileInputStream> fisFuture = executor.submit(fileCallable);
try {
fisFuture.get();
} catch (Exception e) {
throwAbles = Throwables.getCausalChain(e);
}
assertThat(throwAbles.get(0).getClass().isAssignableFrom(ExecutionException.class), is(true));
assertThat(throwAbles.get(1).getClass().isAssignableFrom(FileNotFoundException.class), is(true));
executor.shutdownNow();
}
在这个例子中,我们创建了一个Callable实例期望返回一个FileInputStream对象,我们故意制造了一个 FileNotFoundException。之后,我们将Callable实例提交给ExecutorService,并返回了Future引用。当我 们调用Future.get方法,抛出了一个异常,我们调用Throwables.getCausalChain方法获取到具体的异常链。最后,我们断言 异常链中的第一个Throwable实例是ExecutionException,第二个是FileNotFoundException。通过这个 Throwable的异常链,我们可以选择性的过滤我们想要检查的异常。
获取根异常
Throwables.getRootCause方法接收一个Throwable实例,并返回根异常信息。下面是一个例子:
@Test
public void testGetRootCause() throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
Throwable cause = null;
final String nullString = null;
Callable<String> stringCallable = new Callable<String>() {
@Override
public String call() throws Exception {
return nullString.substring(0, 2);
}
};
Future<String> stringFuture = executor.submit(stringCallable);
try {
stringFuture.get();
} catch (Exception e) {
cause = Throwables.getRootCause(e);
}
assertThat(cause.getClass().isAssignableFrom(NullPointerException.class), is(true));
executor.shutdownNow();
}
我们同样使用一个Callable 实例,并故意抛出一个异常,这次是NullPointerException。当我们通过返回的Future对象stringFuture调用get方法 捕获到相应的异常后,我们调用了Throwables.getRootCause方法,并将返回的Throwable对象赋值给cause变量,然后我们 断言根异常确实是NullPointerException。虽然这些方法不会取代日志文件中对异常堆栈信息的追踪,但我们可以使用这些方法在以后处理那些有价值的信息。
Summary
在本文中,我们介绍了一些非常有用的类,这些类可能并不经常被用到,但是在需要的时候,会变得很方便。首先我们学习了Hash函数和Hashing提供的 一些工具,之后我们利用hash函数构造了一个有用的数据结构BloomFilter。我们也学习了Optional类,使用它可以避免那些null值引 起的异常,让我们的代码变得更健壮。最后,我们介绍了Throwables,它包含一些有用的方法,能够方便的帮助我们处理程序中抛出的异常。