# Guava
# 1.1-使用和避免null
Doug Lea 说,“Null 真糟糕。”
当Sir C. A. R. Hoare 使用了null引用后说,”使用它导致了十亿美金的错误。”
轻率地使用null可能会导致很多令人惊愕的问题。通过学习Google底层代码库,我们发现95%的集合类不接受null值作为元素。我们认为, 相比默默地接受null,使用快速失败操作拒绝null值对开发者更有帮助。
此外,Null的含糊语义让人很不舒服。Null很少可以明确地表示某种语义,例如,Map.get(key)返回Null时,可能表示map中的值是null,亦或map中没有key对应的值。Null可以表示失败、成功或几乎任何情况。使用Null以外的特定值,会让你的逻辑描述变得更清晰。
Null确实也有合适和正确的使用场景,如在性能和速度方面Null是廉价的,而且在对象数组中,出现Null也是无法避免的。但相对于底层库来说,在应用级别的代码中,Null往往是导致混乱,疑难问题和模糊语义的元凶,就如同我们举过的Map.get(key)的例子。最关键的是,Null本身没有定义它表达的意思。
鉴于这些原因,很多Guava工具类对Null值都采用快速失败操作,除非工具类本身提供了针对Null值的因变措施。此外,Guava还提供了很多工具类,让你更方便地用特定值替换Null值。
具体案例
不要在Set中使用null,或者把null作为map的键值。使用特殊值代表null会让查找操作的语义更清晰。
如果你想把null作为map中某条目的值,更好的办法是 不把这一条目放到map中,而是单独维护一个”值为null的键集合” (null keys)。Map 中对应某个键的值是null,和map中没有对应某个键的值,是非常容易混淆的两种情况。因此,最好把值为null的键分离开,并且仔细想想,null值的键在你的项目中到底表达了什么语义。
如果你需要在列表中使用null——并且这个列表的数据是稀疏的,使用Map<Integer, E>可能会更高效,并且更准确地符合你的潜在需求。
此外,考虑一下使用自然的null对象——特殊值。举例来说,为某个enum类型增加特殊的枚举值表示null,比如java.math.RoundingMode就定义了一个枚举值UNNECESSARY,它表示一种不做任何舍入操作的模式,用这种模式做舍入操作会直接抛出异常。
如果你真的需要使用null值,但是null值不能和Guava中的集合实现一起工作,你只能选择其他实现。比如,用JDK中的Collections.unmodifiableList替代Guava的ImmutableList
Optional
大多数情况下,开发人员使用null表明的是某种缺失情形:可能是已经有一个默认值,或没有值,或找不到值。例如,Map.get返回null就表示找不到给定键对应的值。
Guava用Optional表示可能为null的T类型引用。一个Optional实例可能包含非null的引用(我们称之为引用存在),也可能什么也不包括(称之为引用缺失)。它从不说包含的是null值,而是用存在或缺失来表示。但Optional从不会包含null值引用。
Optional<Integer> possible = Optional.of(5);
possible.isPresent(); // returns true
possible.get(); // returns 5
Optional无意直接模拟其他编程环境中的”可选” or “可能”语义,但它们的确有相似之处。
Optional最常用的一些操作被罗列如下:
创建Optional实例(以下都是静态方法):
Optional.of(T) | 创建指定引用的Optional实例,若引用为null则快速失败 |
---|---|
Optional.absent() | 创建引用缺失的Optional实例 |
Optional.fromNullable(T) | 创建指定引用的Optional实例,若引用为null则表示缺失 |
用Optional实例查询引用(以下都是非静态方法):
boolean isPresent() | 如果Optional包含非null的引用(引用存在),返回true |
---|---|
T get() | 返回Optional所包含的引用,若引用缺失,则抛出java.lang.IllegalStateException |
T or(T) | 返回Optional所包含的引用,若引用缺失,返回指定的值 |
T orNull() | 返回Optional所包含的引用,若引用缺失,返回null |
Set asSet() | 返回Optional所包含引用的单例不可变集,如果引用存在,返回一个只有单一元素的集合,如果引用缺失,返回一个空集合。 |
使用Optional的意义在哪儿?
使用Optional除了赋予null语义,增加了可读性,最大的优点在于它是一种傻瓜式的防护。Optional迫使你积极思考引用缺失的情况,因为你必须显式地从Optional获取引用。直接使用null很容易让人忘掉某些情形,尽管FindBugs可以帮助查找null相关的问题,但是我们还是认为它并不能准确地定位问题根源。
如同输入参数,方法的返回值也可能是null。和其他人一样,你绝对很可能会忘记别人写的方法method(a,b)会返回一个null,就好像当你实现method(a,b)时,也很可能忘记输入参数a可以为null。将方法的返回类型指定为Optional,也可以迫使调用者思考返回的引用缺失的情形。
其他处理null的便利方法
当你需要用一个默认值来替换可能的null,请使用[Objects.firstNonNull(T, T)
](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/base/Objects.html#firstNonNull(T, T)) 方法。如果两个值都是null,该方法会抛出NullPointerException。Optional也是一个比较好的替代方案,例如:Optional.of(first).or(second).
还有其它一些方法专门处理null或空字符串:emptyToNull(String),nullToEmpty(String)
,
isNullOrEmpty(String)
。我们想要强调的是,这些方法主要用来与混淆null/空的API进行交互。当每次你写下混淆null/空的代码时,Guava团队都泪流满面。(好的做法是积极地把null和空区分开,以表示不同的含义,在代码中把null和空同等对待是一种令人不安的坏味道。
# 1.2-前置条件
前置条件:让方法调用的前置条件判断更简单。
Guava在Preconditions类中提供了若干前置条件判断的实用方法,我们强烈建议在Eclipse中静态导入这些方法。每个方法都有三个变种:
- 没有额外参数:抛出的异常中没有错误消息;
- 有一个Object对象作为额外参数:抛出的异常使用Object.toString() 作为错误消息;
- 有一个String对象作为额外参数,并且有一组任意数量的附加Object对象:这个变种处理异常消息的方式有点类似printf,但考虑GWT的兼容性和效率,只支持%s指示符。例如:
checkArgument(i >= 0, "Argument was %s but expected nonnegative", i);
checkArgument(i < j, "Expected i < j, but %s > %s", i, j);
方法声明(不包括额外参数) | 描述 | 检查失败时抛出的异常 |
---|---|---|
checkArgument(boolean) | 检查boolean是否为true,用来检查传递给方法的参数。 | IllegalArgumentException |
checkNotNull(T) | 检查value是否为null,该方法直接返回value,因此可以内嵌使用checkNotNull。 | NullPointerException |
checkState(boolean) | 用来检查对象的某些状态。 | IllegalStateException |
checkElementIndex(int index, int size) | 检查index作为索引值对某个列表、字符串或数组是否有效。index>=0 && index<size * | IndexOutOfBoundsException |
checkPositionIndex(int index, int size) | 检查index作为位置值对某个列表、字符串或数组是否有效。index>=0 && index<=size * | IndexOutOfBoundsException |
checkPositionIndexes(int start, int end, int size) | 检查[start, end]表示的位置范围对某个列表、字符串或数组是否有效* | IndexOutOfBoundsException |
译者注:
***索引值常用来查找列表、字符串或数组中的元素,如**List.get(int), String.charAt(int)
*位置值和位置范围常用来截取列表、字符串或数组,如List.subList(int,int), String.substring(int)
相比Apache Commons提供的类似方法,我们把Guava中的Preconditions作为首选。Piotr Jagielski在他的博客中简要地列举了一些理由:
- 在静态导入后,Guava方法非常清楚明晰。checkNotNull清楚地描述做了什么,会抛出什么异常;
- checkNotNull直接返回检查的参数,让你可以在构造函数中保持字段的单行赋值风格:this.field = checkNotNull(field)
- 简单的、参数可变的printf风格异常信息。鉴于这个优点,在JDK7已经引入Objects.requireNonNull的情况下,我们仍然建议你使用checkNotNull。
在编码时,如果某个值有多重的前置条件,我们建议你把它们放到不同的行,这样有助于在调试时定位。此外,把每个前置条件放到不同的行,也可以帮助你编写清晰和有用的错误消息。
# 1.3-常见Object方法
原文链接 译者: 沈义扬
equals
当一个对象中的字段可以为null时,实现Object.equals方法会很痛苦,因为不得不分别对它们进行null检查。使用[Objects.equal
](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/base/Objects.html#equal(java.lang.Object, java.lang.Object))帮助你执行null敏感的equals判断,从而避免抛出NullPointerException。例如:
Objects.equal("a", "a"); // returns true
Objects.equal(null, "a"); // returns false
Objects.equal("a", null); // returns false
Objects.equal(null, null); // returns true
注意:JDK7引入的Objects类提供了一样的方法[*Objects.equals*
](http://docs.oracle.com/javase/7/docs/api/java/util/Objects.html#equals(java.lang.Object, java.lang.Object))。
hashCode
用对象的所有字段作散列[hash]运算应当更简单。Guava的Objects.hashCode(Object...)
会对传入的字段序列计算出合理的、顺序敏感的散列值。你可以使用Objects.hashCode(field1, field2, …, fieldn)来代替手动计算散列值。
注意:JDK7引入的Objects类提供了一样的方法*Objects.hash(Object...)*
toString
好的toString方法在调试时是无价之宝,但是编写toString方法有时候却很痛苦。使用 Objects.toStringHelper可以轻松编写有用的toString方法。例如:
// Returns "ClassName{x=1}"
Objects.toStringHelper(this).add("x", 1).toString();
// Returns "MyObject{x=1}"
Objects.toStringHelper("MyObject").add("x", 1).toString();
compare/compareTo
实现一个比较器[Comparator],或者直接实现Comparable接口有时也伤不起。考虑一下这种情况:
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
。
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();
}
这种Fluent接口风格的可读性更高,发生错误编码的几率更小,并且能避免做不必要的工作。更多Guava排序器工具可以在下一节里找到。
# 1.4-排序: Guava强大的”流畅风格比较器”
原文链接 译者: 沈义扬
[排序器Ordering]是Guava流畅风格比较器[Comparator]的实现,它可以用来为构建复杂的比较器,以完成集合排序的功能。
从实现上说,Ordering实例就是一个特殊的Comparator实例。Ordering把很多基于Comparator的静态方法(如Collections.max)包装为自己的实例方法(非静态方法),并且提供了链式调用方法,来定制和增强现有的比较器。
创建排序器:常见的排序器可以由下面的静态方法创建
方法 | 描述 |
---|---|
natural() | 对可排序类型做自然排序,如数字按大小,日期按先后排序 |
usingToString() | 按对象的字符串形式做字典排序[lexicographical ordering] |
from(Comparator) | 把给定的Comparator转化为排序器 |
实现自定义的排序器时,除了用上面的from方法,也可以跳过实现Comparator,而直接继承Ordering:
Ordering<String> byLengthOrdering = new Ordering<String>() {
public int compare(String left, String right) {
return Ints.compare(left.length(), right.length());
}
};
链式调用方法:通过链式调用,可以由给定的排序器衍生出其它排序器
方法 | 描述 |
---|---|
reverse() | 获取语义相反的排序器 |
nullsFirst() | 使用当前排序器,但额外把null值排到最前面。 |
nullsLast() | 使用当前排序器,但额外把null值排到最后面。 |
compound(Comparator) | 合成另一个比较器,以处理当前排序器中的相等情况。 |
lexicographical() | 基于处理类型T的排序器,返回该类型的可迭代对象Iterable |
onResultOf(Function) | 对集合中元素调用Function,再按返回值用当前排序器排序。 |
例如,你需要下面这个类的排序器。
class Foo {
@Nullable String sortedBy;
int notSortedBy;
}
考虑到排序器应该能处理sortedBy为null的情况,我们可以使用下面的链式调用来合成排序器:
Ordering<Foo> ordering = Ordering.natural()
.nullsFirst()
.onResultOf(
new Function<Foo, String>() {
public String apply(Foo foo) {
return foo.sortedBy;
}
});
当阅读链式调用产生的排序器时,应该从后往前读。上面的例子中,排序器首先调用apply方法获取sortedBy值,并把sortedBy为null的元素都放到最前面,然后把剩下的元素按sortedBy进行自然排序。之所以要从后往前读,是因为每次链式调用都是用后面的方法包装了前面的排序器。
注:用compound方法包装排序器时,就不应遵循从后往前读的原则。为了避免理解上的混乱,请不要把compound写在一长串链式调用的中间,你可以另起一行,在链中最先或最后调用compound。
超过一定长度的链式调用,也可能会带来阅读和理解上的难度。我们建议按下面的代码这样,在一个链中最多使用三个方法。此外,你也可以把Function分离成中间对象,让链式调用更简洁紧凑。
Ordering<Foo> ordering = Ordering.natural().nullsFirst().onResultOf(sortKeyFunction)
运用排序器: Guava的排序器实现有若干操纵集合或元素值的方法
方法 | 描述 | 另请参见 |
---|---|---|
greatestOf(Iterable iterable, int k) | 获取可迭代对象中最大的k个元素。 | leastOf |
isOrdered(Iterable) | 判断可迭代对象是否已按排序器排序:允许有排序值相等的元素。 | isStrictlyOrdered |
sortedCopy(Iterable) | 判断可迭代对象是否已严格按排序器排序:不允许排序值相等的元素。 | immutableSortedCopy |
min(E, E) | 返回两个参数中最小的那个。如果相等,则返回第一个参数。 | max(E, E) |
min(E, E, E, E...) | 返回多个参数中最小的那个。如果有超过一个参数都最小,则返回第一个最小的参数。 | max(E, E, E, E...) |
min(Iterable) | 返回迭代器中最小的元素。如果可迭代对象中没有元素,则抛出NoSuchElementException。 | max(Iterable) |
# Example-1
/**
* descrption: 排序的简单使用(Ordering是Guava流畅风格比较器Comparator的实现,它可以用来为构建复杂的比较器,以完成集合排序的功能。)
*/
public class OrderExample {
@Test
public void testJDKOrder() {
List<Integer> list = Arrays.asList(1, 5, 3, 8, 2);
System.out.println("排序前" + list.toString());
Collections.sort(list);
System.out.println("排序后" + list.toString());
}
/**
* 排序中出现,空指针会异常的!
*/
@Test
public void testJDKOrderIssue() {
List<Integer> list = Arrays.asList(1, 5, null, 3, 8, 2);
System.out.println("排序前" + list.toString());
Collections.sort(list); // 出现异常...
}
/**
* 将空值放置在最前面的情况
* 结果:空在最前面-排序后[null, 1, 2, 3, 5, 8]
*/
@Test
public void testOrderNaturalByNullFirst() {
List<Integer> list = Arrays.asList(1, 5, null, 3, 8, 2);
Collections.sort(list, Ordering.natural().nullsFirst());
System.out.println("空在最前面-排序后" + list.toString());
}
/**
* 将空值放置在最后面的情况
* 结果:空在最后面-排序后[1, 2, 3, 5, 8, null]
*/
@Test
public void testOrderNaturalByNullLast() {
List<Integer> list = Arrays.asList(1, 5, null, 3, 8, 2);
Collections.sort(list, Ordering.natural().nullsLast());
System.out.println("空在最后面-排序后" + list.toString());
}
/**
* isOrdered(Iterable) 判断可迭代对象是否已按排序器排序:允许有排序值相等的元素。
* 结果:排好序的:是的
*/
@Test
public void testOrderNatural() {
List<Integer> list = Arrays.asList(1, 5, 3, 8, 2);
Collections.sort(list);
boolean order = Ordering.natural().isOrdered(list); //是否为按照这样的顺序排好序的!自然的排序
System.out.println("排好序的:" + (order == true ? "是的" : "不是"));
}
/**
* reverse() 获取语义相反的排序器
* 结果:获取最大的元素8
* 结果:排序后[8, 5, 3, 2, 1]
*/
@Test
public void testOrderReverse() {
List<Integer> list = Arrays.asList(1, 5, 3, 8, 2);
Collections.sort(list, Ordering.natural().reverse());
System.out.println("获取最大的元素" + Ordering.natural().max(list).toString());
System.out.println("排序后" + list.toString());
}
/**
* 返回元素中的最大的值,同理最小的
* 结果:获取最大的元素8
* 结果:获取最大的元素1
* 结果:获取最大的元素1
*/
@Test
public void testMax() {
List<Integer> list = Arrays.asList(1, 5, 3, 8, 2);
System.out.println("获取最大的元素" + Ordering.natural().max(list).toString()); //获取最大的元素8
System.out.println("获取最大的元素" + Ordering.natural().reverse().max(list).toString());//获取反向最大的元素1
System.out.println("获取最大的元素" + Ordering.natural().min(list).toString());//获取最小的元素1
}
/**
* 返回一个排序对象中的最大的多少个元素-是不稳定的。
* 结果:获取最大的k个元素:[8, 5, 3]
* 结果:获取最大的Minik个元素:[1, 2, 3]
*/
@Test
public void testGreaTestOf() {
List<Integer> list = Arrays.asList(1, 5, 3, 8, 2);
List<Integer> listMaxtOfK = Ordering.natural().greatestOf(list, 3);//获取可迭代对象中最大的k个元素。
System.out.println("获取最大的k个元素:" + listMaxtOfK.toString());
List<Integer> listMaxtOfMinik = Ordering.natural().reverse().greatestOf(list, 3);
// listMaxtOfK.add(1); UnmodifiableCollection 返回的是不可变对象,不可以进行操作
System.out.println("获取最大的Minik个元素:" + listMaxtOfMinik.toString());
}
/**
* natural():使用Comparable类型的自然顺序, 例如:整数从小到大,字符串是按字典顺序;
* usingToString() :使用toString()返回的字符串按字典顺序进行排序;
* 结果:排序后[1, 2, 3, 5, 8]
* 结果:排序后[a, test, wang]
*/
@Test
public void testUsingToString() {
List<Integer> list = Arrays.asList(1, 5, 3, 8, 2);
Collections.sort(list, Ordering.usingToString());
System.out.println("排序后" + list.toString());
List<String> listString = Lists.newArrayList("wang", "test", "a");
Collections.sort(listString, Ordering.usingToString());
System.out.println("排序后" + listString.toString());
}
/**
* 复制一个副本,这里没有进行集合元素的深复制,需要保证集合对象中为不可变对象
* 这里可以通过sortedCopy,返回一个可变的对象,immutableSortedCopy返回一个不可变的对象
* <p>
* 结果:
* 返回的一个副本[1, 2, 3, 5, 8]
* 返回的一个可变的副本[1, 2, 3, 5, 8, 10]
* 返回的一个副本[1, 2, 3, 5, 8]
* 返回的一个可变的副本不能添加
*/
@Test
public void testSortedCopy() {
List<Integer> list = Arrays.asList(1, 5, 3, 8, 2);
List<Integer> duplicateList = Ordering.natural().sortedCopy(list);// 返回一个副本
if (!Objects.equals(list, duplicateList)) {
System.out.println("返回的一个副本" + duplicateList.toString());
duplicateList.add(10);
System.out.println("返回的一个可变的副本" + duplicateList.toString());
}
List<Integer> duplicateImmutableList = Ordering.natural().immutableSortedCopy(list); // 返回一个不可变的副本
if (!Objects.equals(list, duplicateImmutableList)) {
System.out.println("返回的一个副本" + duplicateImmutableList.toString());
try {
duplicateImmutableList.add(10);
} catch (Exception e) {
System.out.println("返回的一个可变的副本不能添加");
}
}
}
}
# Example-2
/**
* descrption: 使用自定义排序规则、多字段排序
*/
public class OrderExample2 {
public class WorkerIdComparator implements Comparator<Worker> {
@Override
public int compare(Worker worker1, Worker worker2) {
return Ints.compare(worker1.getWorkId(), worker2.getWorkId());
}
}
public class WorkerNameComparator implements Comparator<Worker> {
@Override
public int compare(Worker worker1, Worker worker2) {
return worker1.getName().compareTo(worker2.getName());
}
}
/**
* http://blog.csdn.net/wenniuwuren/article/details/46361119
* guava 多参数排序
* http://ifeve.com/google-guava-ordering/
* Ordering.from 的使用
*/
@Test
public void testFrom() {
Worker worker = new Worker(1, "wangji");
Worker worker2 = new Worker(11, "guava");
Worker worker22 = new Worker(11, "aaa");
Worker worker3 = new Worker(111, "wangji3");
List<Worker> workers = Lists.newArrayList(worker, worker2, worker22, worker3);
Ordering<Worker> orderWorker = Ordering.from(new WorkerIdComparator());
Collections.sort(workers, orderWorker);
System.out.println("单参数:" + workers.toString());
// compound 相等的情况下,考虑第二个参数进行比较
Ordering<Worker> orderManyWorker = Ordering.from(new WorkerIdComparator()).compound(new WorkerNameComparator()).reverse();
Collections.sort(workers, orderManyWorker);
System.out.println("多参数:" + workers.toString());
}
}
# Example-3
class Person implements Comparable<Person> {
private String name;
private Integer age;
private Gender gender;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Gender getGender() {
return gender;
}
public void setGender(Gender gender) {
this.gender = gender;
}
@Override
public int compareTo(Person that) {
//ComparisonChain执行一种懒比较: 它执行比较操作直至发现非零的结果, 在那之后的比较输入将被忽略,相对于下面的比较简单明了多了
return ComparisonChain.start()
.compare(this.name, that.name, Ordering.natural().nullsFirst())
.compare(this.age, that.age)
.compare(this.gender, that.gender)
.result();
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", gender=" + gender +
'}';
}
/**
* 性别枚举
*/
public enum Gender {
MAN,
WOMAN
}
}
# 1.5-Throwables:简化异常和错误的传播与检查
原文链接 译者: 沈义扬
异常传播
有时候,你会想把捕获到的异常再次抛出。这种情况通常发生在Error或RuntimeException被捕获的时候,你没想捕获它们,但是声明捕获Throwable和Exception的时候,也包括了了Error或RuntimeException。Guava提供了若干方法,来判断异常类型并且重新传播异常。例如:
try {
someMethodThatCouldThrowAnything();
} catch (IKnowWhatToDoWithThisException e) {
handle(e);
} catch (Throwable t) {
Throwables.propagateIfInstanceOf(t, IOException.class);
Throwables.propagateIfInstanceOf(t, SQLException.class);
throw Throwables.propagate(t);
}
所有这些方法都会自己决定是否要抛出异常,但也能直接抛出方法返回的结果——例如,throw Throwables.propagate(t);—— 这样可以向编译器声明这里一定会抛出异常。
Guava中的异常传播方法简要列举如下:
RuntimeException propagate(Throwable) | 如果Throwable是Error或RuntimeException,直接抛出;否则把Throwable包装成RuntimeException抛出。返回类型是RuntimeException,所以你可以像上面说的那样写成throw Throwables.propagate(t) ,Java编译器会意识到这行代码保证抛出异常。 |
---|---|
void propagateIfInstanceOf( Throwable, Class) throws X | Throwable类型为X才抛出 |
void propagateIfPossible( Throwable) | Throwable类型为Error或RuntimeException才抛出 |
void propagateIfPossible( Throwable, Class) throws X | Throwable类型为X, Error或RuntimeException才抛出 |
Throwables.propagate的用法
- 模仿Java7的多重异常捕获和再抛出
通常来说,如果调用者想让异常传播到栈顶,他不需要写任何catch代码块。因为他不打算从异常中恢复,他可能就不应该记录异常,或者有其他的动作。他可能是想做一些清理工作,但通常来说,无论操作是否成功,清理工作都要进行,所以清理工作可能会放在finallly代码块中。但有时候,捕获异常然后再抛出也是有用的:也许调用者想要在异常传播之前统计失败的次数,或者有条件地传播异常。
当只对一种异常进行捕获和再抛出时,代码可能还是简单明了的。但当多种异常需要处理时,却可能变得一团糟:
@Override public void run() {
try {
delegate.run();
} catch (RuntimeException e) {
failures.increment();
throw e;
}catch (Error e) {
failures.increment();
throw e;
}
}} catch (RuntimeException | Error e) {
failures.increment();
throw e;
}
Java7用多重捕获解决了这个问题:
} catch (RuntimeException | Error e) {
failures.increment();
throw e;
}
非Java7用户却受困于这个问题。他们想要写如下代码来统计所有异常,但是编译器不允许他们抛出Throwable(译者注:这种写法把原本是Error或RuntimeException类型的异常修改成了Throwable,因此调用者不得不修改方法签名):
} catch (Throwable t) {
failures.increment();
throw t;
}
解决办法是用throw Throwables.propagate(t)替换throw t。在限定情况下(捕获Error和RuntimeException),Throwables.propagate和原始代码有相同行为。然而,用Throwables.propagate也很容易写出有其他隐藏行为的代码。尤其要注意的是,这个方案只适用于处理RuntimeException 或Error。如果catch块捕获了受检异常,你需要调用propagateIfInstanceOf来保留原始代码的行为,因为Throwables.propagate不能直接传播受检异常。
总之,Throwables.propagate的这种用法也就马马虎虎,在Java7中就没必要这样做了。在其他Java版本中,它可以减少少量的代码重复,但简单地提取方法进行重构也能做到这一点。此外,使用propagate会意外地包装受检异常。
- 非必要用法:把抛出的Throwable转为Exception
有少数API,尤其是Java反射API和(以此为基础的)Junit,把方法声明成抛出Throwable。和这样的API交互太痛苦了,因为即使是最通用的API通常也只是声明抛出Exception。当确定代码会抛出Throwable,而不是Exception或Error时,调用者可能会用Throwables.propagate转化Throwable。这里有个用Callable执行Junit测试的范例:
public Void call() throws Exception {
try {
FooTest.super.runTest();
} catch (Throwable t) {
Throwables.propagateIfPossible(t, Exception.class);
Throwables.propagate(t);
}
return null;
}
在这儿没必要调用propagate()方法,因为propagateIfPossible传播了Throwable之外的所有异常类型,第二行的propagate就变得完全等价于throw new RuntimeException(t)。(题外话:这个例子也提醒我们,propagateIfPossible可能也会引起混乱,因为它不但会传播参数中给定的异常类型,还抛出Error和RuntimeException)
这种模式(或类似于throw new RuntimeException(t)的模式)在Google代码库中出现了超过30次。(搜索’propagateIfPossible[^;]* Exception.class[)];’)绝大多数情况下都明确用了”throw new RuntimeException(t)”。我们也曾想过有个”throwWrappingWeirdThrowable”方法处理Throwable到Exception的转化。但考虑到我们用两行代码实现了这个模式,除非我们也丢弃propagateIfPossible方法,不然定义这个throwWrappingWeirdThrowable方法也并没有太大必要。
Throwables.propagate的有争议用法
- 争议一:把受检异常转化为非受检异常
原则上,非受检异常代表bug,而受检异常表示不可控的问题。但在实际运用中,即使JDK也有所误用——如Object.clone()、Integer. parseInt(String)、URI(String)——或者至少对某些方法来说,没有让每个人都信服的答案,如URI.create(String)的异常声明。
因此,调用者有时不得不把受检异常和非受检异常做相互转化:
try {
return Integer.parseInt(userInput);
} catch (NumberFormatException e) {
throw new InvalidInputException(e);
}
try {
return publicInterfaceMethod.invoke();
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
有时候,调用者会使用Throwables.propagate转化异常。这样做有没有什么缺点?最主要的恐怕是代码的含义不太明显。throw Throwables.propagate(ioException)做了什么?throw new RuntimeException(ioException)做了什么?这两者做了同样的事情,但后者的意思更简单直接。前者却引起了疑问:”它做了什么?它并不只是把异常包装进RuntimeException吧?如果它真的只做了包装,为什么还非得要写个方法?”。应该承认,这些问题部分是因为”propagate”的语义太模糊了(用来抛出未声明的异常吗?)。也许”wrapIfChecked”更能清楚地表达含义。但即使方法叫做”wrapIfChecked”,用它来包装一个已知类型的受检异常也没什么优点。甚至会有其他缺点:也许比起RuntimeException,还有更合适的类型——如IllegalArgumentException。 我们有时也会看到propagate被用于传播可能为受检的异常,结果是代码相比以前会稍微简短点,但也稍微有点不清晰:
} catch (RuntimeException e) {
throw e;
}catch (Exception e) {
throw new RuntimeException(e);
}
} catch (Exception e) {
throw Throwables.propagate(e);
}
然而,我们似乎故意忽略了把检查型异常转化为非检查型异常的合理性。在某些场景中,这无疑是正确的做法,但更多时候它被用于避免处理受检异常。这让我们的话题变成了争论受检异常是不是坏主意了,我不想对此多做叙述。但可以这样说,Throwables.propagate不是为了鼓励开发者忽略IOException这样的异常。
- 争议二:异常穿隧
但是,如果你要实现不允许抛出异常的方法呢?有时候你需要把异常包装在非受检异常内。这种做法挺好,但我们再次强调,没必要用propagate方法做这种简单的包装。实际上,手动包装可能更好:如果你手动包装了所有异常(而不仅仅是受检异常),那你就可以在另一端解包所有异常,并处理极少数特殊场景。此外,你可能还想把异常包装成特定的类型,而不是像propagate这样统一包装成RuntimeException。
- 争议三:重新抛出其他线程产生的异常
try {
return future.get();
} catch (ExecutionException e) {
throw Throwables.propagate(e.getCause());
}
对这样的代码要考虑很多方面:
- ExecutionException的cause可能是受检异常,见上文”争议一:把检查型异常转化为非检查型异常”。但如果我们确定future对应的任务不会抛出受检异常呢?(可能future表示runnable任务的结果——译者注:如ExecutorService中的submit(Runnable task, Tresult)方法)如上所述,你可以捕获异常并抛出AssertionError。尤其对于Future,请考虑 Futures.get方法。(TODO:对future.get()抛出的另一个异常InterruptedException作一些说明)
- ExecutionException的cause可能直接是Throwable类型,而不是Exception或Error。(实际上这不大可能,但你想直接重新抛出cause的话,编译器会强迫你考虑这种可能性)见上文”用法二:把抛出Throwable改为抛出Exception”。
- ExecutionException的cause可能是非受检异常。如果是这样的话,cause会直接被Throwables.propagate抛出。不幸的是,cause的堆栈信息反映的是异常最初产生的线程,而不是传播异常的线程。通常来说,最好在异常链中同时包含这两个线程的堆栈信息,就像ExecutionException所做的那样。(这个问题并不单单和propagate方法相关;所有在其他线程中重新抛出异常的代码都需要考虑这点)
异常原因链
Guava提供了如下三个有用的方法,让研究异常的原因链变得稍微简便了,这三个方法的签名是不言自明的:
Throwable getRootCause(Throwable) |
---|
List getCausalChain(Throwable) |
String getStackTraceAsString(Throwable) |
# 2.1-不可变集合Immutable
概述
guava是google的一个库,弥补了java语言的很多方面的不足,很多在java8中已有实现,暂时不展开。Collections是jdk提供的一个工具类。
Guava中不可变对象和Collections工具类的unmodifiableSet/List/Map/etc的区别:
当Collections创建的不可变集合的wrapper类改变的时候,不可变集合也会改变,而Guava的Immutable集合保证确实是不可变的。
# 1、JDK中实现immutable集合
在JDK中提供了Collections.unmodifiableXXX系列方法来实现不可变集合, 但是存在一些问题,下面我们先看一个具体实例:
public class ImmutableTest {
@Test
public void testJDKImmutable(){
List<String> list=new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
//通过list创建一个不可变的unmodifiableList集合
List<String> unmodifiableList=Collections.unmodifiableList(list);
System.out.println(unmodifiableList);
//通过list添加元素
list.add("ddd");
System.out.println("往list添加一个元素:"+list);
System.out.println("通过list添加元素之后的unmodifiableList:"+unmodifiableList);
//通过unmodifiableList添加元素
unmodifiableList.add("eee");
System.out.println("往unmodifiableList添加一个元素:"+unmodifiableList);
}
}
运行结果:
通过运行结果我们可以看出:虽然unmodifiableList不可以直接添加元素,但是我的list是可以添加元素的,而list的改变也会使unmodifiableList改变。
所以说Collections.unmodifiableList实现的不是真正的不可变集合。
# 2、Guava的immutable集合
Guava提供了对JDK里标准集合类里的immutable版本的简单方便的实现,以及Guava自己的一些专门集合类的immutable实现。当你不希望修改一个集合类,
或者想做一个常量集合类的时候,使用immutable集合类就是一个最佳的编程实践。
注意:每个Guava immutable集合类的实现都拒绝null值。我们做过对Google内部代码的全面的调查,并且发现只有5%的情况下集合类允许null值,而95%的情况下
都拒绝null值。万一你真的需要能接受null值的集合类,你可以考虑用Collections.unmodifiableXXX。
immutable集合可以有以下几种方式来创建:
1、用copyOf方法, 譬如, ImmutableSet.copyOf(set)
2、使用of方法,譬如,ImmutableSet.of("a", "b", "c")或者ImmutableMap.of("a", 1, "b", 2)
3、使用Builder类
举例:
@Test
public void testGuavaImmutable(){
List<String> list=new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
ImmutableList<String> imlist=ImmutableList.copyOf(list);
System.out.println("imlist:"+imlist);
ImmutableList<String> imOflist=ImmutableList.of("peida","jerry","harry");
System.out.println("imOflist:"+imOflist);
ImmutableSortedSet<String> imSortList=ImmutableSortedSet.of("a", "b", "c", "a", "d", "b");
System.out.println("imSortList:"+imSortList);
list.add("baby");
//关键看这里是否imlist也添加新元素了
System.out.println("list添加新元素之后看imlist:"+imlist);
ImmutableSet<Color> imColorSet =
ImmutableSet.<Color>builder()
.add(new Color(0, 255, 255))
.add(new Color(0, 191, 255))
.build();
System.out.println("imColorSet:"+imColorSet);
}
运行结果:发现imlist并未改变。
对于排序的集合来说有例外,因为元素的顺序在构建集合的时候就被固定下来了。譬如,ImmutableSet.of("a", "b", "c", "a", "d", "b"),对于这个集合的遍历顺序来说就是"a", "b", "c", "d"。
# 3、更智能的copyOf
copyOf方法比你想象的要智能,ImmutableXXX.copyOf会在合适的情况下避免拷贝元素的操作-先忽略具体的细节,但是它的实现一般都是很“智能”的。譬如:
@Test
public void testCotyOf(){
ImmutableSet<String> imSet=ImmutableSet.of("peida","jerry","harry","lisa");
System.out.println("imSet:"+imSet);
//set直接转list
ImmutableList<String> imlist=ImmutableList.copyOf(imSet);
System.out.println("imlist:"+imlist);
//list直接转SortedSet
ImmutableSortedSet<String> imSortSet=ImmutableSortedSet.copyOf(imSet);
System.out.println("imSortSet:"+imSortSet);
List<String> list=new ArrayList<String>();
for(int i=0;i<=10;i++){
list.add(i+"x");
}
System.out.println("list:"+list);
//截取集合部分元素
ImmutableList<String> imInfolist=ImmutableList.copyOf(list.subList(2, 8));
System.out.println("imInfolist:"+imInfolist);
}
运行结果
# 4、Guava集合和不可变对应关系
可变集合类型 | 可变集合源:JDK or Guava? | 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 |
# 2.2-新集合类型
Guava引入了很多JDK没有的、但我们发现明显有用的新集合类型。这些新类型是为了和JDK集合框架共存,而没有往JDK集合抽象中硬塞其他概念。作为一般规则,Guava集合非常精准地遵循了JDK接口契约。
# Multiset
Guava提供了一个新集合类型Multiset,它可以多次添加相等的元素,且和元素顺序无关。Multiset继承于JDK的Cllection接口,而不是Set接口。它和set最大的区别就是
它可以对相同元素做一个计数的功能,普通的 Set 就像这样 :[car, ship, bike],而 Multiset 会是这样 : [car x 2, ship x 6, bike x 3]Multiset有一个有用的功能,
就是跟踪每种对象的数量,所以你可以用来进行数字统计。每存放一个相同元素,那么该元素的count就加1。
譬如一个 List 里面有各种字符串,然后你要统计每个字符串在 List 里面出现的次数,这个用Multiset就能够快速实现。
# 1、举例说明
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
public class MultisetTest {
public static void main(String[] args) {
String str = "张三 李四 李四 王五 王五 王五";
String[] strArr = str.split(" ");
List<String> words = new ArrayList<String>(Arrays.asList(strArr));
//创建一个HashMultiset集合,并将words集合数据放入
Multiset<String> wordMultiset = HashMultiset.create();
wordMultiset.addAll(words);
//将不同的元素放在一个集合set中
for (String key : wordMultiset.elementSet()) {
//查看指定元素的个数
System.out.println(key + "-->" + wordMultiset.count(key));
}
System.out.println("---------向集合中添加元素----------");
//向集合中添加元素
wordMultiset.add("李四");
for (String key : wordMultiset.elementSet()) {
System.out.println(key + "-->" + wordMultiset.count(key));
}
System.out.println("-------向集合中添加若干个元素------");
//向集合中添加若干个元素
wordMultiset.add("赵六", 10);
for (String key : wordMultiset.elementSet()) {
System.out.println(key + "-->" + wordMultiset.count(key));
}
System.out.println("--------向集合中移出一个元素------");
//向集合中移出一个元素
wordMultiset.remove("张三");
for (String key : wordMultiset.elementSet()) {
System.out.println(key + "-->" + wordMultiset.count(key));
}
System.out.println("------向集合中移出若干个元素------");
//向集合中移出若干个元素,如果元素的个数小于要移除的个数,则会把该元素移除光
wordMultiset.remove("赵六",5);
for (String key : wordMultiset.elementSet()) {
System.out.println(key + "-->" + wordMultiset.count(key));
}
System.out.println("-----设定某一个元素的重复次数-----");
//设定某一个元素的重复次数
wordMultiset.setCount("张三", 10);
for (String key : wordMultiset.elementSet()) {
System.out.println(key + "-->" + wordMultiset.count(key));
}
System.out.println("-----设置复合元素的个数设为新的重复次数-----");
//设置复合元素的个数设为新的重复次数(条件是第二个参数的数量要是实际数量一致,否则无效)
wordMultiset.setCount("and", 1,0);
for (String key : wordMultiset.elementSet()) {
System.out.println(key + "-->" + wordMultiset.count(key));
}
System.out.println("-------删除给定集合中的元素------");
//删除给定集合中的元素
wordMultiset.removeAll(words);
for (String key : wordMultiset.elementSet()) {
System.out.println(key + "-->" + wordMultiset.count(key));
}
}
}
根据运行结果我们得到:
(1)它把把list放入HashMultiset中,就成了key还是list的属性,value就是重复数的一个计数。
(2)每添加一个相同元素,计数+1。
(3)可以添加和移除计数的值。
# 2、Multiset主要方法介绍
- add(E element) :向其中添加单个元素
- add(E element,int occurrences) : 向其中添加指定个数的元素
- count(Object element) : 返回给定参数元素的个数
- remove(E element) : 移除一个元素,其count值 会响应减少
- remove(E element,int occurrences): 移除相应个数的元素
- elementSet() : 将不同的元素放入一个Set中
- entrySet(): 类似与Map.entrySet 返回Set<Multiset.Entry>。包含的Entry支持使用getElement()和getCount()
- setCount(E element ,int count): 设定某一个元素的重复次数
- setCount(E element,int oldCount,int newCount): 将符合原有重复个数的元素修改为新的重复次数
- retainAll(Collection c) : 保留出现在给定集合参数的所有的元素
- removeAll(Collectionc) : 去除出现给给定集合参数的所有的元素
# 3、常用的实现了Multiset 接口的类
HashMultiset: 元素存放于 HashMap
LinkedHashMultiset: 元素存放于 LinkedHashMap,即元素的排列顺序由第一次放入的顺序决定
TreeMultiset:元素被排序存放于TreeMap
EnumMultiset: 元素必须是 enum 类型
ImmutableMultiset: 不可修改的 Mutiset
# 4、Multiset与Map<E, Integer>区别
实际开发中我们也可以利用Map<E, Integer>来实现计数功能,但它和Multiset还是有很大区别的,首先Multiset也不是Map<E, Integer>类型的结构,区别如下:
Multiset中的元素出现的次数只能为正数,前面说了原因。如果E的出现次数为0,那么E将不出现在multiset中,是不能在elementSet()和entrySet()的视图中;
multiset.size()返回这个集合的大小,相当于在multiset中元素的出现的总数。如果想得到multiset中不同元素出现的总数,可以利用elementSet().size()来实现;
multiset.iterator()可以遍历multiset中的所有元素,所以iteration遍历的次数就等于multiset.size();
Multiset支持添加、删除元素,设置元素出现的次数;setCount(elem, 0)相当于移除elem的所有元素;
- multiset.count(elem)方法中的elem如果没有出现在Multiset中,那么它的返回值永远都是0。
SortedMultiset
SortedMultiset是Multiset 接口的变种,它支持高效地获取指定范围的子集。比方说,你可以用 latencies.subMultiset(0,BoundType.CLOSED, 100, BoundType.OPEN).size()来统计你的站点中延迟在100毫秒以内的访问,然后把这个值和latencies.size()相比,以获取这个延迟水平在总体访问中的比例。
TreeMultiset实现SortedMultiset接口。在撰写本文档时,ImmutableSortedMultiset还在测试和GWT的兼容性。
# Multimap
每个有经验的Java程序员都在某处实现过Map<K, List
可以用两种方式思考Multimap的概念:”键-单个值映射”的集合:
a -> 1 a -> 2 a ->4 b -> 3 c -> 5
或者”键-值集合映射”的映射:
a -> [1, 2, 4] b -> 3 c -> 5
一般来说,Multimap接口应该用第一种方式看待,但asMap()视图返回Map<K, Collection
很少会直接使用Multimap接口,更多时候你会用ListMultimap或SetMultimap接口,它们分别把键映射到List或Set。
# Example
public class MultimapTest {
public static void main(String args[]){
Multimap<String,String> multimap = ArrayListMultimap.create();
multimap.put("lower", "a");
multimap.put("lower", "b");
multimap.put("lower", "c");
multimap.put("upper", "A");
multimap.put("upper", "B");
List<String> lowerList = (List<String>)multimap.get("lower");
//输出key为lower的list集合
System.out.println("输出key为lower的list集合=========");
System.out.println(lowerList.toString());
lowerList.add("f");
System.out.println(lowerList.toString());
Map<String, Collection<String>> map = multimap.asMap();
System.out.println("把Multimap转为一个map============");
for (Map.Entry<String, Collection<String>> entry : map.entrySet()) {
String key = entry.getKey();
Collection<String> value = multimap.get(key);
System.out.println(key + ":" + value);
}
System.out.println("获得所有Multimap的key值==========");
Set<String> keys = multimap.keySet();
for(String key:keys){
System.out.println(key);
}
System.out.println("输出Multimap所有的value值========");
Collection<String> values = multimap.values();
System.out.println(values);
}
}
/**输出结果:
*输出key为lower的list集合=========
* [a, b, c]
* [a, b, c, f]
* 把Multimap转为一个map============
* lower:[a, b, c, f]
* upper:[A, B]
* 获得所有Multimap的key值==========
* lower
* upper
* 输出Multimap所有的value值========
* [a, b, c, f, A, B]
*/
# 修改Multimap
Multimap.get(key)以集合形式返回键所对应的值视图,即使没有任何对应的值,也会返回空集合。ListMultimap.get(key)返回List,SetMultimap.get(key)返回Set。
对值视图集合进行的修改最终都会反映到底层的Multimap。例如:
Set<Person> aliceChildren = childrenMultimap.get(alice);
aliceChildren.clear();
aliceChildren.add(bob);
aliceChildren.add(carol);
其他(更直接地)修改Multimap的方法有:
方法签名 | 描述 | 等价于 |
---|---|---|
put(K, V) | 添加键到单个值的映射 | multimap.get(key).add(value) |
putAll(K, Iterable) | 依次添加键到多个值的映射 | Iterables.addAll(multimap.get(key), values) |
remove(K, V) | 移除键到值的映射;如果有这样的键值并成功移除,返回true。 | multimap.get(key).remove(value) |
removeAll(K) | 清除键对应的所有值,返回的集合包含所有之前映射到K的值,但修改这个集合就不会影响Multimap了。 | multimap.get(key).clear() |
replaceValues(K, Iterable) | 清除键对应的所有值,并重新把key关联到Iterable中的每个元素。返回的集合包含所有之前映射到K的值。 | multimap.get(key).clear(); Iterables.addAll(multimap.get(key), values) |
# Multimap的视图
Multimap还支持若干强大的视图:
asMap
为Multimap<K, V>提供Map<K,Collection>形式的视图。返回的Map支持remove操作,并且会反映到底层的Multimap,但它不支持put或putAll操作。更重要的是,如果你想为Multimap中没有的键返回null,而不是一个新的、可写的空集合,你就可以使用asMap().get(key)。(你可以并且应当把asMap.get(key)返回的结果转化为适当的集合类型——如SetMultimap.asMap.get(key)的结果转为Set,ListMultimap.asMap.get(key)的结果转为List——Java类型系统不允许ListMultimap直接为asMap.get(key)返回List——译者注:也可以用**Multimaps中的asMap静态方法帮你完成类型转换) entries
用Collection<Map.Entry<K, V>>返回Multimap中所有”键-单个值映射”——包括重复键。(对SetMultimap,返回的是Set)keySet
用Set表示Multimap中所有不同的键。keys
用Multiset表示Multimap中的所有键,每个键重复出现的次数等于它映射的值的个数。可以从这个Multiset中移除元素,但不能做添加操作;移除操作会反映到底层的Multimap。values()
用一个”扁平”的Collection包含Multimap中的所有值。这有一点类似于Iterables.concat(multimap.asMap().values()),但它直接返回了单个Collection,而不像multimap.asMap().values()那样是按键区分开的Collection。
# Multimap不是Map
Multimap<K, V>不是Map<K,Collection
- Multimap.get(key)总是返回非null、但是可能空的集合。这并不意味着Multimap为相应的键花费内存创建了集合,而只是提供一个集合视图方便你为键增加映射值——译者注:如果有这样的键,返回的集合只是包装了Multimap中已有的集合;如果没有这样的键,返回的空集合也只是持有Multimap引用的栈对象,让你可以用来操作底层的Multimap。因此,返回的集合不会占据太多内存,数据实际上还是存放在Multimap中。
- 如果你更喜欢像Map那样,为Multimap中没有的键返回null,请使用asMap()视图获取一个Map<K, Collection
>。(或者用静态方法Multimaps.asMap()为ListMultimap返回一个Map<K, List >。对于SetMultimap和SortedSetMultimap,也有类似的静态方法存在) - 当且仅当有值映射到键时,Multimap.containsKey(key)才会返回true。尤其需要注意的是,如果键k之前映射过一个或多个值,但它们都被移除后,Multimap.containsKey(key)会返回false。
- Multimap.entries()返回Multimap中所有”键-单个值映射”——包括重复键。如果你想要得到所有”键-值集合映射”,请使用asMap().entrySet()。
- Multimap.size()返回所有”键-单个值映射”的个数,而非不同键的个数。要得到不同键的个数,请改用Multimap.keySet().size()。
# Multimap的各种实现
Multimap提供了多种形式的实现。在大多数要使用Map<K, Collection
实现 | 键行为类似 | 值行为类似 |
---|---|---|
ArrayListMultimap | HashMap | ArrayList |
HashMultimap | HashMap | HashSet |
LinkedListMultimap* | LinkedHashMap* | LinkedList* |
LinkedHashMultimap** | LinkedHashMap | LinkedHashMap |
TreeMultimap | TreeMap | TreeSet |
ImmutableListMultimap | ImmutableMap | ImmutableList |
ImmutableSetMultimap | ImmutableMap | ImmutableSet |
除了两个不可变形式的实现,其他所有实现都支持null键和null值
*LinkedListMultimap.entries()保留了所有键和值的迭代顺序。详情见doc链接。
**LinkedHashMultimap保留了映射项的插入顺序,包括键插入的顺序,以及键映射的所有值的插入顺序。
请注意,并非所有的Multimap都和上面列出的一样,使用Map<K, Collection
如果你想要更大的定制化,请用[Multimaps.newMultimap(Map, Supplier)](http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/collect/Multimaps.html#newMultimap(java.util.Map, com.google.common.base.Supplier))或[list](http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/collect/Multimaps.html#newListMultimap(java.util.Map, com.google.common.base.Supplier))和 [set](http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/collect/Multimaps.html#newSetMultimap(java.util.Map, com.google.common.base.Supplier))版本,使用自定义的Collection、List或Set实现Multimap。
# BiMap
bimap的作用很清晰:它是一个一一映射,可以通过key得到value,也可以通过value得到key。
# 1、bimap和普通HashMap区别
(1)在Java集合类库中的Map,它的特点是存放的键(Key)是唯一的,而值(Value)可以不唯一,而
bimap要求key和value都唯一,如果key不唯一则覆盖key,如果value不唯一则直接报错。
# 2、案例展示
public class bimapTest {
public static void main(String args[]){
//双向map
BiMap<Integer,String> biMap=HashBiMap.create();
biMap.put(1,"张三");
biMap.put(2,"李四");
biMap.put(3,"王五");
biMap.put(4,"赵六");
biMap.put(5,"李七");
biMap.put(4,"小小");
//通过key值得到value值(注意key里面的类型根据泛行
String value= biMap.get(1);
System.out.println("id为1的value值 --"+value);
//通过value值得到key值
int key= biMap.inverse().get("张三");
System.out.println("张三key值 --"+key);
//通过key值重复,那么vakue值会被覆盖。
String valuename= biMap.get(4);
System.out.println("id为4的value值 --"+valuename);
}
}
/*运行结果:
*id为1的value值 --张三
*张三key值 --1
*id为4的value值 --小小
*/
如果value值重复,则运行直接报错如下:
biMap.put(6,"小小");
/**后台直接报错
* Exception in thread "main" java.lang.IllegalArgumentException: value already present: 小小
* at com.google.common.collect.HashBiMap.put(HashBiMap.java:285)
* at com.google.common.collect.HashBiMap.put(HashBiMap.java:260)
* at com.jincou.stream.study.bimapTest.main(bimapTest.java:40)
*/
如果你想value也发生覆盖key值,那么可以:
// inverse方法会返回一个反转的BiMap,但是注意这个反转的map不是新的map对象,它实现了一种视图关联,这样你对于反转后的map的所有操作都会影响原先的map对象。
biMap.forcePut(6,"小小");
int key6= biMap.inverse().get("小小");
System.out.println("小小key值 --"+key6);
/**
* 后台输出(已经把:biMap.put(4,"小小"覆盖)
* 小小key值 --6
*/
# 3、BiMap的各种实现
键**–**值实现 | 值**–**键实现 | 对应的BiMap实现 |
---|---|---|
HashMap | HashMap | HashBiMap |
ImmutableMap | ImmutableMap | ImmutableBiMap |
EnumMap | EnumMap | EnumBiMap |
EnumMap | HashMap | EnumHashBiMap |
注:Maps类中还有一些诸如synchronizedBiMap的BiMap工具方法.
# Table
看到table的使用时候真的是眼前一亮,之前的代码中写过很多的Map> 这种格式的代码,这种阅读起来非常的不友好,甚至都不知道map中的key到底是什么还要联系上下文联想才可以,而table类型的出现彻底解决掉了这个麻烦。
Table支持 row、column、value 我们把上面定义的map结构想象成一张数据表就可以了:
Table<R,C,V> == Map<R,Map<C,V>>
下面先让我们来看一张数据表吧,结合数据表来编写我们的代码:
# 方法 & 描述
S.N. | 方法 & 描述 |
---|---|
1 | Set> cellSet() 返回集合中的所有行键/列键/值三元组。 |
2 | void clear() 从表中删除所有映射。 |
3 | Map column(C columnKey) 返回在给定列键的所有映射的视图。 |
4 | Set columnKeySet() 返回一组具有表中的一个或多个值的列键。 |
5 | Map> columnMap() 返回关联的每一列键与行键对应的映射值的视图。 |
6 | boolean contains(Object rowKey, Object columnKey) 返回true,如果表中包含与指定的行和列键的映射。 |
7 | boolean containsColumn(Object columnKey) 返回true,如果表中包含与指定列的映射。 |
8 | boolean containsRow(Object rowKey) 返回true,如果表中包含与指定的行键的映射关系。 |
9 | boolean containsValue(Object value) 返回true,如果表中包含具有指定值的映射。 |
10 | boolean equals(Object obj) 比较指定对象与此表是否相等。 |
11 | V get(Object rowKey, Object columnKey) 返回对应于给定的行和列键,如果没有这样的映射存在值,返回null。 |
12 | int hashCode() 返回此表中的哈希码。 |
13 | boolean isEmpty() 返回true,如果表中没有映射。 |
14 | V put(R rowKey, C columnKey, V value) 关联指定值与指定键。 |
15 | void putAll(Table table) 复制从指定的表中的所有映射到这个表。 |
16 | V remove(Object rowKey, Object columnKey) 如果有的话,使用给定键相关联删除的映射。 |
17 | Map row(R rowKey) 返回包含给定行键的所有映射的视图。 |
18 | Set rowKeySet() 返回一组行键具有在表中的一个或多个值。 |
19 | Map> rowMap() 返回关联的每一行按键与键列对应的映射值的视图。 |
20 | int size() 返回行键/列键/表中的值映射关系的数量。 |
21 | Collection values() 返回所有值,其中可能包含重复的集合。 |
# Demo
下面是根据上面的表格写的
/*
* Company: IBM, Microsoft, TCS
* IBM -> {101:Mahesh, 102:Ramesh, 103:Suresh}
* Microsoft -> {101:Sohan, 102:Mohan, 103:Rohan }
* TCS -> {101:Ram, 102: Shyam, 103: Sunil }
*
* */
//create a table
Table<String, String, String> employeeTable = HashBasedTable.create();
//initialize the table with employee details
employeeTable.put("IBM", "101","Mahesh");
employeeTable.put("IBM", "102","Ramesh");
employeeTable.put("IBM", "103","Suresh");
employeeTable.put("Microsoft", "111","Sohan");
employeeTable.put("Microsoft", "112","Mohan");
employeeTable.put("Microsoft", "113","Rohan");
employeeTable.put("TCS", "121","Ram");
employeeTable.put("TCS", "102","Shyam");
employeeTable.put("TCS", "123","Sunil");
//所有行数据
System.out.println(employeeTable.cellSet());
//所有公司
System.out.println(employeeTable.rowKeySet());
//所有员工编号
System.out.println(employeeTable.columnKeySet());
//所有员工名称
System.out.println(employeeTable.values());
//公司中的所有员工和员工编号
System.out.println(employeeTable.rowMap());
//员工编号对应的公司和员工名称
System.out.println(employeeTable.columnMap());
//row+column对应的value
System.out.println(employeeTable.get("IBM","101"));
//IBM公司中所有信息
Map<String,String> ibmEmployees = employeeTable.row("IBM");
System.out.println("List of IBM Employees");
for(Map.Entry<String, String> entry : ibmEmployees.entrySet()){
System.out.println("Emp Id: " + entry.getKey() + ", Name: " + entry.getValue());
}
//table中所有的不重复的key
Set<String> employers = employeeTable.rowKeySet();
System.out.print("Employers: ");
for(String employer: employers){
System.out.print(employer + " ");
}
System.out.println();
//得到员工编号为102的所有公司和姓名
Map<String,String> EmployerMap = employeeTable.column("102");
for(Map.Entry<String, String> entry : EmployerMap.entrySet()){
System.out.println("Employer: " + entry.getKey() + ", Name: " + entry.getValue());
}
运行结果
[(IBM,101)=Mahesh, (IBM,102)=Ramesh, (IBM,103)=Suresh, (Microsoft,111)=Sohan, (Microsoft,112)=Mohan, (Microsoft,113)=Rohan, (TCS,121)=Ram, (TCS,102)=Shyam, (TCS,123)=Sunil]
[IBM, Microsoft, TCS]
[101, 102, 103, 111, 112, 113, 121, 123]
[Mahesh, Ramesh, Suresh, Sohan, Mohan, Rohan, Ram, Shyam, Sunil]
{IBM={101=Mahesh, 102=Ramesh, 103=Suresh}, Microsoft={111=Sohan, 112=Mohan, 113=Rohan}, TCS={121=Ram, 102=Shyam, 123=Sunil}}
{101={IBM=Mahesh}, 102={IBM=Ramesh, TCS=Shyam}, 103={IBM=Suresh}, 111={Microsoft=Sohan}, 112={Microsoft=Mohan}, 113={Microsoft=Rohan}, 121={TCS=Ram}, 123={TCS=Sunil}}
Mahesh
List of IBM Employees
Emp Id: 101, Name: Mahesh
Emp Id: 102, Name: Ramesh
Emp Id: 103, Name: Suresh
Employers: IBM Microsoft TCS
Employer: IBM, Name: Ramesh
Employer: TCS, Name: Shyam
Table的几种实现:
- HashBasedTable:本质上用HashMap<R, HashMap<C, V>>实现;
- TreeBasedTable:本质上用TreeMap<R, TreeMap<C,V>>实现;
- ImmutableTable:本质上用ImmutableMap<R, ImmutableMap<C, V>>实现;注:ImmutableTable对稀疏或密集的数据集都有优化。
- ArrayTable:要求在构造时就指定行和列的大小,本质上由一个二维数组实现,以提升访问速度和密集Table的内存利用率。ArrayTable与其他Table的工作原理有点不同,请参见Javadoc了解详情。
# ClassToInstanceMap
ClassToInstanceMap是一种特殊的Map:它的键是类型,而值是符合键所指类型的对象。
为了扩展Map接口,ClassToInstanceMap额外声明了两个方法:T getInstance(Class) 和T putInstance(Class, T),从而避免强制类型转换,同时保证了类型安全。
ClassToInstanceMap有唯一的泛型参数,通常称为B,代表Map支持的所有类型的上界。例如:
ClassToInstanceMap<Number> numberDefaults=MutableClassToInstanceMap.create();
numberDefaults.putInstance(Integer.class, Integer.valueOf(0));
从技术上讲,ClassToInstanceMap实现了Map<Class<? extends B>, B>——或者换句话说,是一个映射B的子类型到对应实例的Map。这让ClassToInstanceMap包含的泛型声明有点令人困惑,但请记住B始终是Map所支持类型的上界——通常B就是Object。
对于ClassToInstanceMap,Guava提供了两种有用的实现:MutableClassToInstanceMap和 ImmutableClassToInstanceMap。
# RangeSet
RangeSet描述了一组不相连的、非空的区间。当把一个区间添加到可变的RangeSet时,所有相连的区间会被合并,空区间会被忽略。例如:
RangeSet<Integer> rangeSet = TreeRangeSet.create();
rangeSet.add(Range.closed(1, 10)); // {[1,10]}
rangeSet.add(Range.closedOpen(11, 15));//不相连区间:{[1,10], [11,15)}
rangeSet.add(Range.closedOpen(15, 20)); //相连区间; {[1,10], [11,20)}
rangeSet.add(Range.openClosed(0, 0)); //空区间; {[1,10], [11,20)}
rangeSet.remove(Range.open(5, 10)); //分割[1, 10]; {[1,5], [10,10], [11,20)}
请注意,要合并Range.closed(1, 10)和Range.closedOpen(11, 15)这样的区间,你需要首先用Range.canonical(DiscreteDomain)对区间进行预处理,例如DiscreteDomain.integers()。
注:RangeSet不支持GWT,也不支持JDK5和更早版本;因为,RangeSet需要充分利用JDK6中NavigableMap的特性。
# RangeSet的视图
RangeSet的实现支持非常广泛的视图:
- complement():返回RangeSet的补集视图。complement也是RangeSet类型,包含了不相连的、非空的区间。
- subRangeSet(Range
):返回RangeSet与给定Range的交集视图。这扩展了传统排序集合中的headSet、subSet和tailSet操作。 - asRanges():用Set<Range
>表现RangeSet,这样可以遍历其中的Range。 - asSet(DiscreteDomain
)(仅ImmutableRangeSet支持):用ImmutableSortedSet 表现RangeSet,以区间中所有元素的形式而不是区间本身的形式查看。(这个操作不支持DiscreteDomain 和RangeSet都没有上边界,或都没有下边界的情况)
# RangeSet的查询方法
为了方便操作,RangeSet直接提供了若干查询方法,其中最突出的有:
- contains(C):RangeSet最基本的操作,判断RangeSet中是否有任何区间包含给定元素。
- rangeContaining(C):返回包含给定元素的区间;若没有这样的区间,则返回null。
- encloses(Range
):简单明了,判断RangeSet中是否有任何区间包括给定区间。 - span():返回包括RangeSet中所有区间的最小区间。
# RangeMap
RangeMap描述了”不相交的、非空的区间”到特定值的映射。和RangeSet不同,RangeMap不会合并相邻的映射,即便相邻的区间映射到相同的值。例如:
RangeMap<Integer, String> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closed(1, 10), "foo"); //{[1,10] => "foo"}
rangeMap.put(Range.open(3, 6), "bar"); //{[1,3] => "foo", (3,6) => "bar", [6,10] => "foo"}
rangeMap.put(Range.open(10, 20), "foo"); //{[1,3] => "foo", (3,6) => "bar", [6,10] => "foo", (10,20) => "foo"}
rangeMap.remove(Range.closed(5, 11)); //{[1,3] => "foo", (3,5) => "bar", (11,20) => "foo"}
RangeMap的视图
RangeMap提供两个视图:
- asMapOfRanges():用Map<Range
, V>表现RangeMap。这可以用来遍历RangeMap。 - subRangeMap(Range
):用RangeMap类型返回RangeMap与给定Range的交集视图。这扩展了传统的headMap、subMap和tailMap操作。
# 2.3-强大的集合工具类:java.util.Collections中未包含的集合工具
任何对JDK集合框架有经验的程序员都熟悉和喜欢java.util.Collections
包含的工具方法。Guava沿着这些路线提供了更多的工具方法:适用于所有集合的静态方法。这是Guava最流行和成熟的部分之一。
我们用相对直观的方式把工具类与特定集合接口的对应关系归纳如下:
集合接口 | 属于JDK还是Guava | 对应的Guava工具类 |
---|---|---|
Collection | JDK | Collections2 :不要和java.util.Collections混淆 |
List | JDK | Lists |
Set | JDK | Sets |
SortedSet | JDK | Sets |
Map | JDK | Maps |
SortedMap | JDK | Maps |
Queue | JDK | Queues |
Multiset | Guava | Multisets |
Multimap | Guava | Multimaps |
BiMap | Guava | Maps |
Table | Guava | Tables |
在找类似转化、过滤的方法?请看第四章,函数式风格。
# 静态工厂方法
在JDK 7之前,构造新的范型集合时要讨厌地重复声明范型:
List<TypeThatsTooLongForItsOwnGood> list = new ArrayList<TypeThatsTooLongForItsOwnGood>();
我想我们都认为这很讨厌。因此Guava提供了能够推断范型的静态工厂方法:
List<TypeThatsTooLongForItsOwnGood> list = Lists.newArrayList();
Map<KeyType, LongishValueType> map = Maps.newLinkedHashMap();
可以肯定的是,JDK7版本的钻石操作符(<>)没有这样的麻烦:
List<TypeThatsTooLongForItsOwnGood> list = new ArrayList<>();
但Guava的静态工厂方法远不止这么简单。用工厂方法模式,我们可以方便地在初始化时就指定起始元素。
Set<Type> copySet = Sets.newHashSet(elements);
List<String> theseElements = Lists.newArrayList("alpha", "beta", "gamma");
此外,通过为工厂方法命名(Effective Java第一条),我们可以提高集合初始化大小的可读性:
List<Type> exactly100 = Lists.newArrayListWithCapacity(100);
List<Type> approx100 = Lists.newArrayListWithExpectedSize(100);
Set<Type> approx100Set = Sets.newHashSetWithExpectedSize(100);
确切的静态工厂方法和相应的工具类一起罗列在下面的章节。
注意:Guava引入的新集合类型没有暴露原始构造器,也没有在工具类中提供初始化方法。而是直接在集合类中提供了静态工厂方法,例如:
Multiset<String> multiset = HashMultiset.create();
# Iterables
在可能的情况下,Guava提供的工具方法更偏向于接受Iterable而不是Collection类型。在Google,对于不存放在主存的集合——比如从数据库或其他数据中心收集的结果集,因为实际上还没有攫取全部数据,这类结果集都不能支持类似size()的操作 ——通常都不会用Collection类型来表示。
因此,很多你期望的支持所有集合的操作都在Iterables
类中。大多数Iterables方法有一个在Iterators类中的对应版本,用来处理Iterator。
截至Guava 1.2版本,Iterables使用FluentIterable类
进行了补充,它包装了一个Iterable实例,并对许多操作提供了”fluent”(链式调用)语法。
下面列出了一些最常用的工具方法,但更多Iterables的函数式方法将在第四章讨论。
# 常规方法
concat(Iterable) | 串联多个iterables的懒视图* | concat(Iterable...) |
---|---|---|
frequency(Iterable, Object) | 返回对象在iterable中出现的次数 | 与Collections.frequency (Collection, Object)比较;Multiset |
partition(Iterable, int) | 把iterable按指定大小分割,得到的子集都不能进行修改操作 | Lists.partition(List, int);paddedPartition(Iterable, int) |
getFirst(Iterable, T default) | 返回iterable的第一个元素,若iterable为空则返回默认值 | 与Iterable.iterator(). next()比较;FluentIterable.first() |
getLast(Iterable) | 返回iterable的最后一个元素,若iterable为空则抛出NoSuchElementException | getLast(Iterable, T default); FluentIterable.last() |
elementsEqual(Iterable, Iterable) | 如果两个iterable中的所有元素相等且顺序一致,返回true | 与List.equals(Object)比较 |
unmodifiableIterable(Iterable) | 返回iterable的不可变视图 | 与Collections. unmodifiableCollection(Collection)比较 |
limit(Iterable, int) | 限制iterable的元素个数限制给定值 | FluentIterable.limit(int) |
getOnlyElement(Iterable) | 获取iterable中唯一的元素,如果iterable为空或有多个元素,则快速失败 | getOnlyElement(Iterable, T default) |
boolean removeIf(Iterable removeFrom,Predicate predicate) | 移除集合中使得apply()方法返回为true的元素 | public boolean apply(String input) |
skip(final Iterable | 返回跳过前numberToSkip的集合 |
*译者注:懒视图意味着如果还没访问到某个iterable中的元素,则不会对它进行串联操作。
Iterable<Integer> concatenated = Iterables.concat(
Ints.asList(1, 2, 3),
Ints.asList(4, 5, 6)); // concatenated包括元素 1, 2, 3, 4, 5, 6
String lastAdded = Iterables.getLast(myLinkedHashSet);
String theElement = Iterables.getOnlyElement(thisSetIsDefinitelyASingleton);
//如果set不是单元素集,就会出错了!
# 与Collection方法相似的工具方法
通常来说,Collection的实现天然支持操作其他Collection,但却不能操作Iterable。
下面的方法中,如果传入的Iterable是一个Collection实例,则实际操作将会委托给相应的Collection接口方法。例如,往Iterables.size方法传入是一个Collection实例,它不会真的遍历iterator获取大小,而是直接调用Collection.size。
方法 | 类似的Collection方法 | 等价的FluentIterable方法 |
---|---|---|
addAll(Collection addTo, Iterable toAdd) | Collection.addAll(Collection) | |
contains(Iterable, Object) | Collection.contains(Object) | FluentIterable.contains(Object) |
removeAll(Iterable removeFrom, Collection toRemove) | Collection.removeAll(Collection) | |
retainAll(Iterable removeFrom, Collection toRetain) | Collection.retainAll(Collection) | |
size(Iterable) | Collection.size() | FluentIterable.size() |
toArray(Iterable, Class) | Collection.toArray(T[]) | FluentIterable.toArray(Class) |
isEmpty(Iterable) | Collection.isEmpty() | FluentIterable.isEmpty() |
get(Iterable, int) | List.get(int) | FluentIterable.get(int) |
toString(Iterable) | Collection.toString() | FluentIterable.toString() |
# FluentIterable
除了上面和第四章提到的方法,FluentIterable还有一些便利方法用来把自己拷贝到不可变集合
ImmutableList | |
---|---|
ImmutableSet | toImmutableSet() |
ImmutableSortedSet | toImmutableSortedSet(Comparator) |
# Lists
除了静态工厂方法和函数式编程方法,Lists
为List类型的对象提供了若干工具方法。
方法 | 描述 |
---|---|
partition(List, int) | 把List按指定大小分割 |
reverse(List) | 返回给定List的反转视图。注: 如果List是不可变的,考虑改用ImmutableList.reverse() |
transform(List<F> fromList, Function<? super F, ? extends T> function) | 转换 |
List countUp = Ints.asList(1, 2, 3, 4, 5);
List countDown = Lists.reverse(theList); // {5, 4, 3, 2, 1}
List<List> parts = Lists.partition(countUp, 2);//{{1,2}, {3,4}, {5}}
# 静态工厂方法
Lists提供如下静态工厂方法:
具体实现类型 | 工厂方法 |
---|---|
ArrayList | basic, with elements, from Iterable , with exact capacity, with expected size, from Iterator |
LinkedList | basic, from Iterable |
# Sets
Sets
工具类包含了若干好用的方法。
# 集合理论方法
我们提供了很多标准的集合运算(Set-Theoretic)方法,这些方法接受Set参数并返回SetView
,可用于:
- 直接当作Set使用,因为SetView也实现了Set接口;
- 用
copyInto(Set)
拷贝进另一个可变集合; - 用
immutableCopy()
对自己做不可变拷贝。
方法 |
---|
[union(Set, Set) ](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Sets.html#union(java.util.Set, java.util.Set)) |
[intersection(Set, Set) ](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Sets.html#intersection(java.util.Set, java.util.Set)) |
[difference(Set, Set) ](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Sets.html#difference(java.util.Set, java.util.Set)) |
[symmetricDifference(Set, Set) ](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Sets.html#symmetricDifference(java.util.Set, java.util.Set)) |
使用范例:
Set<String> wordsWithPrimeLength = ImmutableSet.of("one", "two", "three", "six", "seven", "eight");
Set<String> primes = ImmutableSet.of("two", "three", "five", "seven");
SetView<String> intersection = Sets.intersection(primes,wordsWithPrimeLength);
// intersection包含"two", "three", "seven"
return intersection.immutableCopy();//可以使用交集,但不可变拷贝的读取效率更高
# 其他Set工具方法
方法 | 描述 | 另请参见 |
---|---|---|
cartesianProduct(List) | 返回所有集合的笛卡儿积 | cartesianProduct(Set...) |
powerSet(Set) | 返回给定集合的所有子集 |
Set<String> animals = ImmutableSet.of("gerbil", "hamster");
Set<String> fruits = ImmutableSet.of("apple", "orange", "banana");
Set<List<String>> product = Sets.cartesianProduct(animals, fruits);
// {{"gerbil", "apple"}, {"gerbil", "orange"}, {"gerbil", "banana"},
// {"hamster", "apple"}, {"hamster", "orange"}, {"hamster", "banana"}}
Set<Set<String>> animalSets = Sets.powerSet(animals);
// {{}, {"gerbil"}, {"hamster"}, {"gerbil", "hamster"}}
# 静态工厂方法
Sets提供如下静态工厂方法:
具体实现类型 | 工厂方法 |
---|---|
HashSet | basic, with elements, from Iterable , with expected size, from Iterator |
LinkedHashSet | basic, from Iterable , with expected size |
TreeSet | basic, with Comparator , from Iterable |
# Maps
Maps
类有若干值得单独说明的、很酷的方法。
# uniqueIndex
Maps.uniqueIndex(Iterable,Function)
通常针对的场景是:有一组对象,它们在某个属性上分别有独一无二的值,而我们希望能够按照这个属性值查找对象——译者注:这个方法返回一个Map,键为Function返回的属性值,值为Iterable中相应的元素,因此我们可以反复用这个Map进行查找操作。
比方说,我们有一堆字符串,这些字符串的长度都是独一无二的,而我们希望能够按照特定长度查找字符串:
ImmutableMap<Integer, String> stringsByIndex = Maps.uniqueIndex(
strings,
new Function<String, Integer> () {
public Integer apply(String string) {
return string.length();
}
});
如果索引值不是独一无二的,请参见下面的Multimaps.index方法。
# difference
[Maps.difference(Map, Map)
](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Maps.html#difference(java.util.Map, java.util.Map))用来比较两个Map以获取所有不同点。该方法返回MapDifference对象,把不同点的维恩图分解为:
entriesInCommon() | 两个Map中都有的映射项,包括匹配的键与值 |
---|---|
entriesDiffering() | 键相同但是值不同值映射项。返回的Map的值类型为MapDifference.ValueDifference ,以表示左右两个不同的值 |
entriesOnlyOnLeft() | 键只存在于左边Map的映射项 |
entriesOnlyOnRight() | 键只存在于右边Map的映射项 |
Map<String, Integer> left = ImmutableMap.of("a", 1, "b", 2, "c", 3);
Map<String, Integer> left = ImmutableMap.of("a", 1, "b", 2, "c", 3);
MapDifference<String, Integer> diff = Maps.difference(left, right);
diff.entriesInCommon(); // {"b" => 2}
diff.entriesInCommon(); // {"b" => 2}
diff.entriesOnlyOnLeft(); // {"a" => 1}
diff.entriesOnlyOnRight(); // {"d" => 5}
# 处理BiMap的工具方法
Guava中处理BiMap的工具方法在Maps类中,因为BiMap也是一种Map实现。
BiMap工具方法 | 相应的Map工具方法 |
---|---|
synchronizedBiMap(BiMap) | Collections.synchronizedMap(Map) |
unmodifiableBiMap(BiMap) | Collections.unmodifiableMap(Map) |
# 静态工厂方法
Maps提供如下静态工厂方法:
具体实现类型 | 工厂方法 |
---|---|
HashMap | basic, from Map , with expected size |
LinkedHashMap | basic, from Map |
TreeMap | basic, from Comparator , from SortedMap |
EnumMap | from Class , from Map |
ConcurrentMap:支持所有操作 | basic |
IdentityHashMap | basic |
# Multisets
标准的Collection操作会忽略Multiset重复元素的个数,而只关心元素是否存在于Multiset中,如containsAll方法。为此,Multisets
提供了若干方法,以顾及Multiset元素的重复性:
方法 | 说明 | 和Collection方法的区别 |
---|---|---|
containsOccurrences(Multiset sup, Multiset sub) | 对任意o,如果sub.count(o)<=super.count(o),返回true | Collection.containsAll忽略个数,而只关心sub的元素是否都在super中 |
removeOccurrences(Multiset removeFrom, Multiset toRemove) | 对toRemove中的重复元素,仅在removeFrom中删除相同个数。 | Collection.removeAll移除所有出现在toRemove的元素 |
retainOccurrences(Multiset removeFrom, Multiset toRetain) | 修改removeFrom,以保证任意o都符合removeFrom.count(o)<=toRetain.count(o) | Collection.retainAll保留所有出现在toRetain的元素 |
intersection(Multiset, Multiset) | 返回两个multiset的交集; | 没有类似方法 |
Multiset<String> multiset1 = HashMultiset.create();
multiset1.add("a", 2);
Multiset<String> multiset2 = HashMultiset.create();
multiset2.add("a", 5);
multiset1.containsAll(multiset2); //返回true;因为包含了所有不重复元素,
//虽然multiset1实际上包含2个"a",而multiset2包含5个"a"
Multisets.containsOccurrences(multiset1, multiset2); // returns false
multiset2.removeOccurrences(multiset1); // multiset2 现在包含3个"a"
multiset2.removeAll(multiset1);//multiset2移除所有"a",虽然multiset1只有2个"a"
multiset2.isEmpty(); // returns true
Multisets中的其他工具方法还包括:
copyHighestCountFirst(Multiset) | 返回Multiset的不可变拷贝,并将元素按重复出现的次数做降序排列 |
---|---|
unmodifiableMultiset(Multiset) | 返回Multiset的只读视图 |
unmodifiableSortedMultiset(SortedMultiset) | 返回SortedMultiset的只读视图 |
Multiset<String> multiset = HashMultiset.create();
multiset.add("a", 3);
multiset.add("b", 5);
multiset.add("c", 1);
ImmutableMultiset highestCountFirst = Multisets.copyHighestCountFirst(multiset);
//highestCountFirst,包括它的entrySet和elementSet,按{"b", "a", "c"}排列元素
# Multimaps
Multimaps
提供了若干值得单独说明的通用工具方法
# index
作为Maps.uniqueIndex的兄弟方法,[Multimaps.index(Iterable, Function)
](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Multimaps.html#index(java.lang.Iterable, com.google.common.base.Function))通常针对的场景是:有一组对象,它们有共同的特定属性,我们希望按照这个属性的值查询对象,但属性值不一定是独一无二的。
比方说,我们想把字符串按长度分组。
ImmutableSet digits = ImmutableSet.of("zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine");
Function<String, Integer> lengthFunction = new Function<String, Integer>() {
public Integer apply(String string) {
return string.length();
}
};
ImmutableListMultimap<Integer, String> digitsByLength= Multimaps.index(digits, lengthFunction);
/*
* digitsByLength maps:
* 3 => {"one", "two", "six"}
* 4 => {"zero", "four", "five", "nine"}
* 5 => {"three", "seven", "eight"}
*/
# invertFrom
鉴于Multimap可以把多个键映射到同一个值(译者注:实际上这是任何**map都有的特性),也可以把一个键映射到多个值,反转Multimap也会很有用。Guava 提供了[invertFrom(Multimap toInvert,Multimap dest)
](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Multimaps.html#invertFrom(com.google.common.collect.Multimap, M))做这个操作,并且你可以自由选择反转后的Multimap实现。
注:如果你使用的是ImmutableMultimap,考虑改用ImmutableMultimap.inverse()
做反转。
ArrayListMultimap<String, Integer> multimap = ArrayListMultimap.create();
multimap.putAll("b", Ints.asList(2, 4, 6));
multimap.putAll("a", Ints.asList(4, 2, 1));
multimap.putAll("c", Ints.asList(2, 5, 3));
TreeMultimap<Integer, String> inverse = Multimaps.invertFrom(multimap, TreeMultimap<String, Integer>.create());
//注意我们选择的实现,因为选了TreeMultimap,得到的反转结果是有序的
/*
* inverse maps:
* 1 => {"a"}
* 2 => {"a", "b", "c"}
* 3 => {"c"}
* 4 => {"a", "b"}
* 5 => {"c"}
* 6 => {"b"}
*/
# forMap
想在Map对象上使用Multimap的方法吗?forMap(Map)
把Map包装成SetMultimap。这个方法特别有用,例如,与Multimaps.invertFrom结合使用,可以把多对一的Map反转为一对多的Multimap。
Map<String, Integer> map = ImmutableMap.of("a", 1, "b", 1, "c", 2);
SetMultimap<String, Integer> multimap = Multimaps.forMap(map);
// multimap:["a" => {1}, "b" => {1}, "c" => {2}]
Multimap<Integer, String> inverse = Multimaps.invertFrom(multimap, HashMultimap<Integer, String>.create());
// inverse:[1 => {"a","b"}, 2 => {"c"}]
# 包装器
Multimaps提供了传统的包装方法,以及让你选择Map和Collection类型以自定义Multimap实现的工具方法。
只读包装 | Multimap | ListMultimap | SetMultimap | SortedSetMultimap |
---|---|---|---|---|
同步包装 | Multimap | ListMultimap | SetMultimap | SortedSetMultimap |
自定义实现 | [Multimap ](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Multimaps.html#newMultimap(java.util.Map, com.google.common.base.Supplier)) | [ListMultimap ](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Multimaps.html#newListMultimap(java.util.Map, com.google.common.base.Supplier)) | [SetMultimap ](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Multimaps.html#newSetMultimap(java.util.Map, com.google.common.base.Supplier)) | [SortedSetMultimap ](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Multimaps.html#newSortedSetMultimap(java.util.Map, com.google.common.base.Supplier)) |
自定义Multimap的方法允许你指定Multimap中的特定实现。但要注意的是:
- Multimap假设对Map和Supplier产生的集合对象有完全所有权。这些自定义对象应避免手动更新,并且在提供给Multimap时应该是空的,此外还不应该使用软引用、弱引用或虚引用。
- 无法保证修改了Multimap以后,底层Map的内容是什么样的。
- 即使Map和Supplier产生的集合都是线程安全的,它们组成的Multimap也不能保证并发操作的线程安全性。并发读操作是工作正常的,但需要保证并发读写的话,请考虑用同步包装器解决。
- 只有当Map、Supplier、Supplier产生的集合对象、以及Multimap存放的键值类型都是可序列化的,Multimap才是可序列化的。
- Multimap.get(key)返回的集合对象和Supplier返回的集合对象并不是同一类型。但如果Supplier返回的是随机访问集合,那么Multimap.get(key)返回的集合也是可随机访问的。
请注意,用来自定义Multimap的方法需要一个Supplier参数,以创建崭新的集合。下面有个实现ListMultimap的例子——用TreeMap做映射,而每个键对应的多个值用LinkedList存储。
ListMultimap<String, Integer> myMultimap = Multimaps.newListMultimap(
Maps.<String, Collection>newTreeMap(),
new Supplier<LinkedList>() {
public LinkedList get() {
return Lists.newLinkedList();
}
});
# Tables
Tables
类提供了若干称手的工具方法。
# 自定义Table
堪比Multimaps.newXXXMultimap(Map, Supplier)工具方法,[Tables.newCustomTable(Map, Supplier)
](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Tables.html#newCustomTable(java.util.Map, com.google.common.base.Supplier))允许你指定Table用什么样的map实现行和列。
// 使用LinkedHashMaps替代HashMaps
Table<String, Character, Integer> table = Tables.newCustomTable(
Maps.<String, Map<Character, Integer>>newLinkedHashMap(),
new Supplier<Map<Character, Integer>> () {
public Map<Character, Integer> get() {
return Maps.newLinkedHashMap();
}
});
# transpose
transpose(Table)
方法允许你把Table<C, R, V>转置成Table<R, C, V>。例如,如果你在用Table构建加权有向图,这个方法就可以把有向图反转。
# 包装器
还有很多你熟悉和喜欢的Table包装类。然而,在大多数情况下还请使用ImmutableTable
Unmodifiable | Table | RowSortedTable |
---|---|---|
# 2.4-集合扩展工具类
# 简介
有时候你需要实现自己的集合扩展。也许你想要在元素被添加到列表时增加特定的行为,或者你想实现一个Iterable,其底层实际上是遍历数据库查询的结果集。Guava为你,也为我们自己提供了若干工具方法,以便让类似的工作变得更简单。(毕竟,我们自己也要用这些工具扩展集合框架。)
# Forwarding装饰器
针对所有类型的集合接口,Guava都提供了Forwarding抽象类以简化装饰者模式的使用。
Forwarding抽象类定义了一个抽象方法:delegate(),你可以覆盖这个方法来返回被装饰对象。所有其他方法都会直接委托给delegate()。例如说:ForwardingList.get(int)实际上执行了delegate().get(int)。
通过创建ForwardingXXX的子类并实现delegate()方法,可以选择性地覆盖子类的方法来增加装饰功能,而不需要自己委托每个方法——译者注:因为所有方法都默认委托给**delegate()返回的对象,你可以只覆盖需要装饰的方法。
此外,很多集合方法都对应一个”标准方法[standardxxx]”实现,可以用来恢复被装饰对象的默认行为,以提供相同的优点。比如在扩展AbstractList或JDK中的其他骨架类时,可以使用类似standardAddAll这样的方法。
让我们看看这个例子。假定你想装饰一个List,让其记录所有添加进来的元素。当然,无论元素是用什么方法——add(int, E), add(E), 或addAll(Collection)——添加进来的,我们都希望进行记录,因此我们需要覆盖所有这些方法。
class AddLoggingList<E> extends ForwardingList<E> {
final List<E> delegate; // backing list
@Override protected List<E> delegate() {
return delegate;
}
@Override public void add(int index, E elem) {
log(index, elem);
super.add(index, elem);
}
@Override public boolean add(E elem) {
return standardAdd(elem); // 用add(int, E)实现
}
@Override public boolean addAll(Collection<? extends E> c) {
return standardAddAll(c); // 用add实现
}
}
记住,默认情况下,所有方法都直接转发到被代理对象,因此覆盖ForwardingMap.put并不会改变ForwardingMap.putAll的行为。小心覆盖所有需要改变行为的方法,并且确保装饰后的集合满足接口契约。
通常来说,类似于AbstractList的抽象集合骨架类,其大多数方法在Forwarding装饰器中都有对应的”标准方法”实现。
对提供特定视图的接口,Forwarding装饰器也为这些视图提供了相应的”标准方法”实现。例如,ForwardingMap提供StandardKeySet、StandardValues和StandardEntrySet类,它们在可以的情况下都会把自己的方法委托给被装饰的Map,把不能委托的声明为抽象方法。
# PeekingIterator
有时候,普通的Iterator接口还不够。
Iterators提供一个Iterators.peekingIterator(Iterator)
方法,来把Iterator包装为PeekingIterator
,这是Iterator的子类,它能让你事先窥视[peek()
]到下一次调用next()返回的元素。
注意:Iterators.peekingIterator返回的PeekingIterator不支持在peek()操作之后调用remove()方法。
举个例子:复制一个List,并去除连续的重复元素。
List<E> result = Lists.newArrayList();
PeekingIterator<E> iter = Iterators.peekingIterator(source.iterator());
while (iter.hasNext()) {
E current = iter.next();
while (iter.hasNext() && iter.peek().equals(current)) {
//跳过重复的元素
iter.next();
}
result.add(current);
}
传统的实现方式需要记录上一个元素,并在特定情况下后退,但这很难处理且容易出错。相较而言,PeekingIterator在理解和使用上就比较直接了。
# AbstractIterator
实现你自己的Iterator?AbstractIterator
让生活更轻松。
用一个例子来解释AbstractIterator最简单。比方说,我们要包装一个iterator以跳过空值。
public static Iterator<String> skipNulls(final Iterator<String> in) {
return new AbstractIterator<String>() {
protected String computeNext() {
while (in.hasNext()) {
String s = in.next();
if (s != null) {
return s;
}
}
return endOfData();
}
};
}
你实现了computeNext()
方法,来计算下一个值。如果循环结束了也没有找到下一个值,请返回endOfData()表明已经到达迭代的末尾。
注意:AbstractIterator继承了UnmodifiableIterator,所以禁止实现remove()方法。如果你需要支持remove()的迭代器,就不应该继承AbstractIterator。
# AbstractSequentialIterator
有一些迭代器用其他方式表示会更简单。AbstractSequentialIterator
就提供了表示迭代的另一种方式。
Iterator<Integer> powersOfTwo = new AbstractSequentialIterator<Integer>(1) { // 注意初始值1!
protected Integer computeNext(Integer previous) {
return (previous == 1 << 30) ? null : previous * 2;
}
};
我们在这儿实现了computeNext(T)
方法,它能接受前一个值作为参数。
注意,你必须额外传入一个初始值,或者传入null让迭代立即结束。因为computeNext(T)假定null值意味着迭代的末尾——AbstractSequentialIterator不能用来实现可能返回null的迭代器。
# 6-字符串处理:分割,连接,填充
# 连接器[Joiner]
用分隔符把字符串序列连接起来也可能会遇上不必要的麻烦。如果字符串序列中含有null,那连接操作会更难。Fluent风格的Joiner
让连接字符串更简单。
Joiner用于帮助我们把多个字符串通过连接符连接起来。Joiner里面也提供和很多很有用的方法,比如null提花,跳过null值等等。而且还派生了MapJoiner类用于连接多个Map,可以同时制定Map之间的连接符和key value之间的连接符。
Joiner joiner = Joiner.on("; ").skipNulls();
return joiner.join("Harry", null, "Ron", "Hermione");
上述代码返回”Harry; Ron; Hermione”。另外,useForNull(String)方法可以给定某个字符串来替换null,而不像skipNulls()方法是直接忽略null。 Joiner也可以用来连接对象类型,在这种情况下,它会把对象的toString()值连接起来。
Joiner.on(",").join(Arrays.asList(1, 5, 7)); // returns "1,5,7"
警告:joiner实例总是不可变的。用来定义joiner目标语义的配置方法总会返回一个新的joiner实例。这使得joiner实例都是线程安全的,你可以将其定义为static final常量。
# Joiner方法介绍
S.N. | 方法及说明 |
---|---|
1 | static Joiner on(char separator) static Joiner on(String separator) 初始化Joiner连接器,separator为Joiner连接器的连接符 |
2 | <A extends Appendable> A appendTo(A appendable, Iterable<?> parts) throws IOException <A extends Appendable> A appendTo(A appendable, Iterator<?> parts) throws IOException <A extends Appendable> A appendTo(A appendable, Object[] parts) throws IOException <A extends Appendable> A appendTo(A appendable, @Nullable Object first, @Nullable Object second, Object… rest) throws IOException 将parts通过连接器的连接符连接成字符串,并拼接到appendable后 |
3 | StringBuilder appendTo(StringBuilder builder, Iterable<?> parts) StringBuilder appendTo(StringBuilder builder, Iterator<?> parts) StringBuilder appendTo(StringBuilder builder, @Nullable Object first, @Nullable Object second, Object… rest) StringBuilder appendTo(StringBuilder builder, Object[] parts) 将parts通过连接器的连接符连接成字符串,并拼接到builder后,返回StringBuilder |
4 | String join(Iterable<?> parts) String join(Iterator<?> parts) String join(@Nullable Object first, @Nullable Object second, Object… rest) String join(Object[] parts) 将parts通过连接器的连接符连接成字符串 |
5 | Joiner skipNulls() 连接器做join连接操作时跳过null元素 |
6 | Joiner useForNull(final String nullText) 连接器做join连接操作时用nullText替换null元素值 |
7 | Joiner.MapJoiner withKeyValueSeparator(char keyValueSeparator) Joiner.MapJoiner withKeyValueSeparator(String keyValueSeparator) 初始化一个Map连接器,连接器连接Map对象时,keyValueSeparator为key和value之间的分隔符 |
# Joiner使用实例
public class JoinerTest {
@Test
public void joinTest(){
List<String> list = Lists.newArrayList("aaa", "bbb", null, "ccc");
String joinStr = Joiner.on("-").skipNulls().join(list);
assertEquals("aaa-bbb-ccc", joinStr);
}
@Test
public void useForNullTest(){
List<String> list = Lists.newArrayList("aaa", "bbb", null, "ccc");
String joinStr = Joiner.on("-").useForNull("null").join(list);
assertEquals("aaa-bbb-null-ccc", joinStr);
}
@Test
public void appendToTest(){
List<String> list = Lists.newArrayList("aaa", "bbb", null, "ccc");
StringBuilder sb = new StringBuilder("this is: ");
StringBuilder result = Joiner.on("-").skipNulls().appendTo(sb, list);
assertEquals("this is: aaa-bbb-ccc", result.toString());
}
@Test
public void withKeyValueSeparatorTest(){
Map<Integer, String> idNameMap = Maps.newHashMap();
idNameMap.put(1, "Michael");
idNameMap.put(2, "Mary");
idNameMap.put(3, "Jane");
String result = Joiner.on("\n").withKeyValueSeparator(":").join(idNameMap);
System.out.println(result);
}
}
# 拆分器[Splitter]
Splitter可以帮助我们制定拆分符对字符串进行拆分。里面也提供了很多使用的方法,比如去掉空格,限制拆分出来的字符串的个数等等。同时还提供了MapSplitter派生类用来把指定格式的字符串拆分到Map里面去。
JDK内建的字符串拆分工具有一些古怪的特性。比如,String.split悄悄丢弃了尾部的分隔符。 问题:”,a,,b,”.split(“,”)返回?
- “”, “a”, “”, “b”, “”
- null, “a”, null, “b”, null
- “a”, null, “b”
- “a”, “b”
- 以上都不对
正确答案是5:””, “a”, “”, “b”。只有尾部的空字符串被忽略了。 Splitter
使用令人放心的、直白的流畅API模式对这些混乱的特性作了完全的掌控。
Splitter.on(',')
.trimResults()
.omitEmptyStrings()
.split("foo,bar,, qux");
上述代码返回Iterable
# 拆分器工厂
方法 | 描述 | 范例 |
---|---|---|
Splitter.on(char) | 按单个字符拆分 | Splitter.on(‘;’) |
Splitter.on(CharMatcher) | 按字符匹配器拆分 | Splitter.on(CharMatcher.BREAKING_WHITESPACE) |
Splitter.on(String) | 按字符串拆分 | Splitter.on(“, “) |
Splitter.on(Pattern) Splitter.onPattern(String) | 按正则表达式拆分 | Splitter.onPattern(“\r?\n”) |
Splitter.fixedLength(int) | 按固定长度拆分;最后一段可能比给定长度短,但不会为空。 | Splitter.fixedLength(3) |
# 拆分器修饰符
方法 | 描述 |
---|---|
omitEmptyStrings() | 从结果中自动忽略空字符串 |
trimResults() | 移除结果字符串的前导空白和尾部空白 |
trimResults(CharMatcher) | 给定匹配器,移除结果字符串的前导匹配字符和尾部匹配字符 |
limit(int) | 限制拆分出的字符串数量 |
如果你想要拆分器返回List,只要使用Lists.newArrayList(splitter.split(string))或类似方法。 警告:splitter实例总是不可变的。用来定义splitter目标语义的配置方法总会返回一个新的splitter实例。这使得splitter实例都是线程安全的,你可以将其定义为static final常量。
# 2.1 Splitter方法介绍
S.N. | 方法及说明 |
---|---|
1 | static Splitter on(char separator) static Splitter on(final CharMatcher separatorMatcher) static Splitter on(Pattern separatorPattern) static Splitter on(final String separator) static Splitter onPattern(String separatorPattern) 初始化拆分器,参数为分隔符 |
2 | static Splitter fixedLength(final int length) 初始化拆分器,拆分器会将字符串分割为元素长度固定的List,最后一个元素长度不足可以直接返回 |
3 | Splitter omitEmptyStrings() 修饰拆分器,拆分器做拆分操作时,会忽略产生的空元素 |
4 | Splitter trimResults() 修饰拆分器,拆分器做拆分操作时,会对拆分的元素做trim操作(删除元素头和尾的空格) |
5 | Splitter trimResults(CharMatcher trimmer) 修饰拆分器,拆分器做拆分操作时,会删除元素头尾charMatcher匹配到的字符 |
6 | Iterable<String> split(final CharSequence sequence) 对Stirng通过拆分器进行拆分,返回一个Iterable<String> |
7 | List<String> splitToList(CharSequence sequence) 对Stirng通过拆分器进行拆分,返回一个List |
8 | Splitter.MapSplitter withKeyValueSeparator(char separator) Splitter.MapSplitter withKeyValueSeparator(Splitter keyValueSplitter) Splitter.MapSplitter withKeyValueSeparator(String separator) 初始化一个Map拆分器,拆分器对String拆分时,separator为key和value之间的分隔符 |
# 2.2 Splitter使用实例
public class SplitterTest {
@Test
public void splitStringToIterableWithDelimiter() {
/*通过Char初始化拆分器,将String分隔为Iterable*/
String str = "this, is , , random , text,";
List<String> result = Lists.newArrayList(Splitter.on(',').omitEmptyStrings().trimResults().split(str));
assertThat(result, contains("this", "is", "random", "text"));
String str1 = "~?~this, is~~ , , random , text,";
result = Splitter.on(',').omitEmptyStrings().trimResults(CharMatcher.anyOf("~? ")).splitToList(str1);
System.out.println(result);
assertThat(result, contains("this", "is", "random", "text"));
}
@Test
public void splitStringToListWithDelimiter() {
/*通过Char初始化拆分器,将String直接分隔为List*/
String str = "this, is , , random , text,";
List<String> result = Splitter.on(',').omitEmptyStrings().trimResults().splitToList(str);
assertThat(result, contains("this", "is", "random", "text"));
/*生成的list不支持add、remove操作*/
assertThatThrownBy(() -> result.add("haha"))
.isInstanceOf(UnsupportedOperationException.class)
.hasNoCause();
}
@Test
public void splitStringToListWithCharMatcher() {
/*通过CharMatcher初始化拆分器*/
String str = "a,b;c.d,e.f),g,h.i;j.1,2.3;";
List<String> result = Splitter.on(CharMatcher.anyOf(";,.)")).omitEmptyStrings().trimResults().splitToList(str);
assertEquals(13, result.size());
}
@Test
public void splitStringToListWithRegularExpression() {
/*通过正则表达式初始化拆分器*/
String str = "apple.banana,,orange,,.";
List<String> result = Splitter.onPattern("[.|,]").omitEmptyStrings().trimResults().splitToList(str);
assertEquals(3, result.size());
}
@Test
public void splitStringToListWithFixedLength() {
/*将字符串分割为元素长度固定的List,最后一个元素长度不足可以直接返回*/
String str = "Hello world";
List<String> result = Splitter.fixedLength(3).splitToList(str);
assertThat(result, contains("Hel", "lo", "wor", "ld"));
}
@Test
public void splitStringToMap() {
/*String转Map*/
String str = "John=first,Adam=second";
Map<String, String> result = Splitter.on(",")
.withKeyValueSeparator("=")
.split(str);
assertEquals("first", result.get("John"));
assertEquals("second", result.get("Adam"));
}
}
# 字符匹配器[CharMatcher]
在以前的Guava版本中,StringUtil类疯狂地膨胀,其拥有很多处理字符串的方法:allAscii、collapse、collapseControlChars、collapseWhitespace、indexOfChars、lastIndexNotOf、numSharedChars、removeChars、removeCrLf、replaceChars、retainAllChars、strip、stripAndCollapse、stripNonDigits。 所有这些方法指向两个概念上的问题:
- 怎么才算匹配字符?
- 如何处理这些匹配字符?
为了收拾这个泥潭,我们开发了CharMatcher。
直观上,你可以认为一个CharMatcher实例代表着某一类字符,如数字或空白字符。事实上来说,CharMatcher实例就是对字符的布尔判断——CharMatcher确实也实现了Predicate——但类似”所有空白字符”或”所有小写字母”的需求太普遍了,Guava因此创建了这一API。
然而使用CharMatcher的好处更在于它提供了一系列方法,让你对字符作特定类型的操作:修剪[trim]、折叠[collapse]、移除[remove]、保留[retain]等等。CharMatcher实例首先代表概念1:怎么才算匹配字符?然后它还提供了很多操作概念2:如何处理这些匹配字符?这样的设计使得API复杂度的线性增加可以带来灵活性和功能两方面的增长。
String noControl = CharMatcher.JAVA_ISO_CONTROL.removeFrom(string); //移除control字符
String theDigits = CharMatcher.DIGIT.retainFrom(string); //只保留数字字符
String spaced = CharMatcher.WHITESPACE.trimAndCollapseFrom(string, ' ');
//去除两端的空格,并把中间的连续空格替换成单个空格
String noDigits = CharMatcher.JAVA_DIGIT.replaceFrom(string, "*"); //用*号替换所有数字
String lowerAndDigit = CharMatcher.JAVA_DIGIT.or(CharMatcher.JAVA_LOWER_CASE).retainFrom(string);
// 只保留数字和小写字母
注:CharMatcher只处理char类型代表的字符;它不能理解0x10000到0x10FFFF的Unicode 增补字符。这些逻辑字符以代理对[surrogate pairs]的形式编码进字符串,而CharMatcher只能将这种逻辑字符看待成两个独立的字符。
# 获取字符匹配器
CharMatcher中的常量可以满足大多数字符匹配需求:
ANY | NONE | WHITESPACE | BREAKING_WHITESPACE |
---|---|---|---|
INVISIBLE | DIGIT | JAVA_LETTER | JAVA_DIGIT |
JAVA_LETTER_OR_DIGIT | JAVA_ISO_CONTROL | JAVA_LOWER_CASE | JAVA_UPPER_CASE |
ASCII | SINGLE_WIDTH |
其他获取字符匹配器的常见方法包括:
方法 | 描述 |
---|---|
anyOf(CharSequence) | 枚举匹配字符。如CharMatcher.anyOf(“aeiou”)匹配小写英语元音 |
is(char) | 给定单一字符匹配。 |
[inRange(char, char) ](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/base/CharMatcher.html#inRange(char, char)) | 给定字符范围匹配,如CharMatcher.inRange(‘a’, ‘z’) |
此外,CharMatcher还有negate()
、and(CharMatcher)
和or(CharMatcher)
方法。
# 使用字符匹配器
CharMatcher提供了多种多样的方法操作CharSequence中的特定字符。其中最常用的罗列如下:
方法 | 描述 |
---|---|
[collapseFrom(CharSequence, char) ](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/base/CharMatcher.html#collapseFrom(java.lang.CharSequence, char)) | 把每组连续的匹配字符替换为特定字符。如WHITESPACE.collapseFrom(string, ‘ ‘)把字符串中的连续空白字符替换为单个空格。 |
matchesAllOf(CharSequence) | 测试是否字符序列中的所有字符都匹配。 |
removeFrom(CharSequence) | 从字符序列中移除所有匹配字符。 |
retainFrom(CharSequence) | 在字符序列中保留匹配字符,移除其他字符。 |
trimFrom(CharSequence) | 移除字符序列的前导匹配字符和尾部匹配字符。 |
[replaceFrom(CharSequence, CharSequence) ](http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/base/CharMatcher.html#replaceFrom(java.lang.CharSequence, java.lang.CharSequence)) | 用特定字符序列替代匹配字符。 |
所有这些方法返回String,除了matchesAllOf返回的是boolean。
# 字符集[Charsets]
不要这样做字符集处理:
try {
bytes = string.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
// how can this possibly happen?
throw new AssertionError(e);
}
试试这样写:
bytes = string.getBytes(Charsets.UTF_8);
Charsets
针对所有Java平台都要保证支持的六种字符集提供了常量引用。尝试使用这些常量,而不是通过名称获取字符集实例。
# 大小写格式[CaseFormat]
CaseFormat被用来方便地在各种ASCII大小写规范间转换字符串——比如,编程语言的命名规范。CaseFormat支持的格式如下:
格式 | 范例 |
---|---|
LOWER_CAMEL | lowerCamel |
LOWER_HYPHEN | lower-hyphen |
LOWER_UNDERSCORE | lower_underscore |
UPPER_CAMEL | UpperCamel |
UPPER_UNDERSCORE | UPPER_UNDERSCORE |
CaseFormat的用法很直接:
CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, "CONSTANT_NAME")); // returns "constantName"
我们CaseFormat在某些时候尤其有用,比如编写代码生成器的时候。