Guava 随记 —— 基本工具

126 阅读6分钟

避免空指针

不小心的使用 null 会导致各种各样的问题。在 Guava 中,会发现 95% 的集合,都不支持接收 null 值,让程序快速失败,而不是默默地接受 null,对开发人员来说是有帮助的

此外,很难清晰地解释 null 返回值意味着什么。例如,Map.get(key) 可以返回 null,有两种含义,要么这个值在 map 中就是 null,要么这个值在 map 中不存在。null 可以意味着失败,也可以意味着成功。

虽然,有的时候,使用 null 也是正确的,首先,null 从内存和速度来说,是消耗很小的,而且,在集合中,也是不可避免的。但是在应用程序代码中,与库相反,它是混淆、困难和奇怪 bug 的主要来源。

由于这些原因,Guava 的许多实用工具都被设计为在出现 null 时,快速失败,而不是允许使用 null。此外,Guava 还提供了许多工具,可以在必须使用 null 时,使其更容易使用,并帮助我们避免使用 null。例如 MoreObjectsStrings

Optional

许多时候, 开发人员使用 null 表示某种不存在的场景。比如,这里应该有值,或者可能有值,实际上没有值,又或者没有找到。

Optional<T> 是一种可以替代 nullable 引用的方式,Optional 表示可能存在,也可能不存在值。

Optional<Integer> possible = Optional.of(5);
possible.isPresent();	// return true
possible.get();	// return 5

乍一看,可能与直接返回 null 没有区别,都需要判断值是否存在。

首先,Optional 就是一强制性的约定,表示可能会有不存在的场景。相对于方法直接返回一个 null 引用,时间长后,调用者可能会忘记该方法返回值可能存在空值,Optional 最起码可以提醒开发人员记得处理值缺失的场景。

当直接判断值是否存在,其实和之前差距不大, 但 Optional 还提供了 mapfilterflatMap 等更方便获取值的方法。

例如:

@Data
class User {
    Address address;
}

@Data
class Address {
    String province;
}

public String getProvince(String userId) {
    User user = getUser(userId);
    if (user != null) {
        Address address = user.getAddress();
        if (address != null) {
            return address.getProvince();
        }
    }
    // ...
    return null;
}

使用 Optional

@Data
class User {
    // 可以为空
    Optional<Address> addressOptional;
}

@Data
class Address {
    // 不能为空
    String province;
}

public Optional<String> getProvince(User user) {
    return Optional.ofNullable(user)
        .flatMap(User::getAddress)
        .map(Address::getProvince);
}

先决条件校验

Guava 提供了一些条件校验工具 —— Preconditions

实例:

checkArgument(i >= 0, "Argument was %s bug expected nonegative", i);
checkArgument(i < j, "Expected i < j, but %s >= %s", i, j);
String userId = checkNotNull(userId);

这里的 checkArgument 是 静态导入的 Preconditions 的方法

其内部实现也很简单,以 checkNotNull 为例:

public static <T> T checkNotNull(T reference) {
  if (reference == null) {
    throw new NullPointerException();
  }
  return reference;
}

实践

实际的开发中,可以根据参考该写法,封装相应的校验类,抛出项目中的业务异常,再通过拦截全局异常的方式,接口返回相应的异常信息。

  • 业务异常

    @Data
    public class BusinessException extends RuntimeException {
    }
    
  • Preconditions

    public class Preconditions {
        
      //....
      
      @CanIgnoreReturnValue
      public static <T> T checkNotNull(
          T reference, @Nullable String errorMessageTemplate, @Nullable Object... errorMessageArgs) {
        if (reference == null) {
          throw new BusinessException(format(errorMessageTemplate, errorMessageArgs));
        }
        return reference;
      }
    
      @CanIgnoreReturnValue
      public static <T> T checkNotNull(T obj, @Nullable String errorMessage) {
        if (obj == null) {
          throw new BusinessException(errorMessage);
        }
        return obj;
      }
        
      //....
        
    }
    

Object command methods

equals

当你的对象属性可以为 null 时,实现或者使用 equals 方法可能是一个痛苦的过程,因为必须单独检查 null

使用 Objects.equal 可以避免空指针异常。

Objects.equal("a", "a");	// true
Objects.equal(null, "a");	// false
Objects.equal("a", null);	// false
Objects.equal(null, null);	// true

注意:JDK 7 中新引入的 Objects 类提供了等效的 Object.equal 方法

hashCode

Guava 的 Objects.hashCode(Object... objs) 为指定的字段序列创建了合理的、顺序敏感的散列。可以使用 Objects.hashCode(field1, field2, ..., fieldn) 代替手工构建散列。

toString

一个好的 toString 方法,对于排查问题来说是非常有用的,但是需要重写 toString 方法。Guava 提供了 MoreObjects.toStringHelper() 来简单的创建一个有用的 toString。示例如下:

// Returns "ClassName{x=1}"
MoreObjects.toStringHelper(this)
    .add("x", 1)
    .toString();

// Returns "MyObject{x=1}"
MoreObjects.toStringHelper("MyObject")
    .add("x", 1)
    .toString();

根据结果可以看到,打印出来的字符串,是 Json 格式的,但效率比 Json 序列化要高得多。

其内部实现,也比较简单,保存了一个类名和属性链表,依据这两个构建的 toString

public static final class ToStringHelper {
    private final String className;
    private final ValueHolder holderHead = new ValueHolder();
    private ValueHolder holderTail = holderHead;
    private boolean omitNullValues = false;
    private boolean omitEmptyValues = false;
    
    private ToStringHelper(String className) {
      this.className = checkNotNull(className);
    }
      
    public ToStringHelper add(String name, @CheckForNull Object value) {
      return addHolder(name, value);
    }
      
    private ToStringHelper addHolder(String name, @CheckForNull Object value) {
      ValueHolder valueHolder = addHolder();
      valueHolder.value = value;
      valueHolder.name = checkNotNull(name);
      return this;
    }
      
    private ValueHolder addHolder() {
      ValueHolder valueHolder = new ValueHolder();
      holderTail = holderTail.next = valueHolder;
      return valueHolder;
    }
    
    //...省略
      
    @Override
    public String toString() {
      // create a copy to keep it consistent in case value changes
      boolean omitNullValuesSnapshot = omitNullValues;
      boolean omitEmptyValuesSnapshot = omitEmptyValues;
      String nextSeparator = "";
      StringBuilder builder = new StringBuilder(32).append(className).append('{');
      for (ValueHolder valueHolder = holderHead.next;
          valueHolder != null;
          valueHolder = valueHolder.next) {
        Object value = valueHolder.value;
        if (valueHolder instanceof UnconditionalValueHolder
            || (value == null
                ? !omitNullValuesSnapshot
                : (!omitEmptyValuesSnapshot || !isEmpty(value)))) {
          builder.append(nextSeparator);
          nextSeparator = ", ";

          if (valueHolder.name != null) {
            builder.append(valueHolder.name).append('=');
          }
          if (value != null && value.getClass().isArray()) {
            Object[] objectArray = {value};
            String arrayString = Arrays.deepToString(objectArray);
            builder.append(arrayString, 1, arrayString.length() - 1);
          } else {
            builder.append(value);
          }
        }
      }
      return builder.append('}').toString();
    }
}
  • 像这里只是简单的遍历查询场景,用链表比数组效率要高

  • 使用 StringBuilder 构建字符串

compare/compareTo

当想要比较一个对象时,直接实现 ComparatorComparable 接口,重写 compareTo 方法,比较需要的属性,是比较麻烦的。例如:

class Person implements Comparable<Person> {
  private String lastName;
  private String firstName;
  private int zipCode;

  public int compareTo(Person other) {
    int cmp = lastName.compareTo(other.lastName);
    if (cmp != 0) {
      return cmp;
    }
    cmp = firstName.compareTo(other.firstName);
    if (cmp != 0) {
      return cmp;
    }
    return Integer.compare(zipCode, other.zipCode);
  }
}

这段代码很容易搞乱,排查问题也不容易,而且冗长。Guava 提供了更好的方式 —— ComparisonChain

   public int compareTo(Foo that) {
     return ComparisonChain.start()
         .compare(this.aString, that.aString)
         .compare(this.anInt, that.anInt)
         .compare(this.anEnum, that.anEnum, Ordering.natural().nullsLast())
         .result();
   }

ComparisonChain 是一种“延迟”比较,它只执行比较,直到找到一个非零结果,然后忽略进一步的输入。

这个流式比较方式,可读性更高,也不容易出现粗心搞错的情况,不会做过多的工作。

其内部实现很有意思,内部维护了三个 ComparisonChain 单例,分别代表相等、小于、大于三种情况,当小于或大于时,再执行 comparision 方法,会直接返回小于或者大于,当等于时,直接拿入参两个引用进行比较。

处理异常

Guava 提供了 Throwables 工具类,更加简单的处理异常。

传播异常

有时候,当在 catch 捕获了一个异常,想要往上抛到下一个 try/catch 块。Throwables 提供了几个方法,更加便携的传播异常。

try {
  someMethodThatCouldThrowAnything();
} catch (IKnowWhatToDoWithThisException e) {
  handle(e);
} catch (Throwable t) {
  Throwables.throwIfInstanceOf(t, IOException.class);
  Throwables.throwIfInstanceOf(t, SQLException.class);
  Throwables.throwIfUnchecked(t);
  throw new RuntimeException(t);
}

下面是传播异常方法的总结:

  • void propagateIfPossible(Throwable, Class<X extends Throwable>) throw X:只有 RuntimeExceptionErrorX,才会抛出 throw
  • void throwIfInstanceOf(Throwable, Class<X extends Exception> throws X):只有 throwX 型异常时,才会抛出
  • void throwIfUnckecked(Throwable):只有 RuntimeExceptionError 时,才会抛出 throw

异常原因链

  • Throwable getRootCause(Throwable)
  • List<Throwable> getCausalChain(Throwable)
  • String getStackTraceAsString(Throwable)