Guava库学习:Guava 零碎知识

jopen 9年前

    这将是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本质上是位向量。它以如下方式工作:

  1. 添加一个元素到filter中

  2. 将这个元素进行多次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实例,例如:

  1. Optional.absent() ,返回一个空的Optional实例 

  2. Optional.of(T ref) ,返回一个包含Type ref的Optioanal实例 

  3. Optioanal.fromNullable(T ref) ,如果 ref不为null,那么返回一个包含Type ref的Optional实例,否则返回一个空的Optional实例

  4. Optional.or(Supplier<T> 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,它包含一些有用的方法,能够方便的帮助我们处理程序中抛出的异常。