在前面的章节,我们已经讨论了什么是GUAVA和怎样去安装GUAVA,在 这一章我们将开始使用guava,我们将展示guava提供的基本功能,并且了解一下这些基本功能是怎样帮助我们简化日常工作遇到的的问题
在这一章节中,我们将覆盖一下几个方面的内容:
- 使用Joiner Class 将字符串以指定的分隔符连接起来。 我们也会涉及到使用MapJoiner
- Splitter Class,和Joiner的作用相反,将一个字符串以给定的分隔符分隔开
- 使用CharMatcher 和 Strings 类 完成一些基本的操作,比如 将String中的一部分移出,匹配String中的某一部分等等
- Preconditions Class 提供了一些方法去判断参数,方法,变量是否和你的预期一样
- 可以和任何一个java对象协作的一些基本的工具类,还有以比较方便的方式实现 Comparable 接口
将一些普通的字符串以固定的分隔符组装在一起是我们在平常工作遇到的,比如将一个Array,List,iterable包含的内容组转成字符串,最常见的就是使用循环将字符串拼接在StringBuilder上,一般的代码如下:
这段代码要注意的就是要将最后一个分隔符去掉。这段代码不是很复杂,但是没有什么价值,完全可以使用Joiner类实现。 下面是一个简单的例子,使用Joiner类以 “|” 将字符串分隔:
Joiner.on(“|”).skipNulls().join(StringList);
这段代码以简洁明明了的方式实现字符串分隔的功能,降低了代码出错的概率。 如果你需要将List中的null值以一个特殊的值代替,那么可以用下面的代码实现:
Joiner.on("|").userForNull(“no value”).join(stringList).
关于Joiner有如下几点我们需要强调:
- Joiner Class不仅仅和String[]一起工作,还可以和array iterable,以及数组对象,Joiner返回是循环Object.toString()方法。
- 如果没有使用skipNulls或者userForNull 那么出现Null时,会出现NullPointException
- Joiner Class是一个不变类,就像String一样
Joiner stringJoiner = Joiner.on("|").skipNulls();
//the useForNull() method returns a new instance
of the Joiner!
stringJoiner.useForNull("missing");
stringJoiner.join("foo","bar",null);
在这个例子中,useForNull这个方法返回了一个新的Joiner对象,对原来的stringJoiner没有任何影响,因此当运行这个例子的时候,最后一个null对象还是会被忽略,不会变成 missing.
StringBuilder stringBuilder = new StringBuilder();
Joiner joiner = Joiner.on("|").skipNulls();
//returns the StringBuilder instance with the values foo,bar,baz
appeneded with "|" delimiters
joiner.appendTo(stringBuilder,"foo","bar","baz")
在这个例子中,我们传递了个一个StringBuilder的实例给Joiner Class,joiner.appedTo 会将 foo,bar,baz 以 “|”分隔,并将值给stringBuilder.
- Joiner 类可以和实现了Appendable的实例合作
FileWriter fileWriter = new FileWriter(new File("path")):
List<Date> dateList = getDates();
Joiner joiner = Joiner.on("#").useForNulls(" ");
//returns the FileWriter instance with the values
appended into it
joiner.appendTo(fileWriter,dateList);
这里我们看到了一个和上面类似的例子,我们传递一个FileWriter的实例和一个包含了Date的list给Joiner 类,Joiner 类会将包含日期的list追加到FileWriter实例上。
从上面的例子中我们可以看到,Joiner类在处理一些基本任务是非常容易的,并且非常容易的。 在我们将目光离开Joiner类之前,我们来看一个特殊的类 MapJoiner,MapJoiner的使用方式和Joiner差不多,但是有一点不同的是: 他是将key/value以特地的分隔符分隔,创建一个MapJoiner类可以采用如下方式:
mapJoiner = Joiner.on("#").withKeyValueSeparator("=");
让我们快速的看一下这段代码做了什么事:
- Joiner.on(“#”) 创建了一个Joiner 对象
- withKeyValueSeparator方法创建了一个MapJoiner对象
下面是一个例子展示MapJoiner的使用:
@Test
public void testMapJoiner() {
//Using LinkedHashMap so that the original
order is preserved
String expectedString = "Washington D.C=Redskins#New York
City=Giants#Philadelphia=Eagles#Dallas=Cowboys";
Map<String,String> testMap = Maps.newLinkedHashMap();
testMap.put("Washington D.C","Redskins");
testMap.put("New York City","Giants");
testMap.put("Philadelphia","Eagles");
testMap.put("Dallas","Cowboys");
String returnedString = Joiner.on("#").
withKeyValueSeparator("=").join(testMap);
assertThat(returnedString,is(expectedString));
}
前面的单元测试的例子中,我们使用Maps的一个静态工厂类的newLinkedHashMap方法创建了一个LinkedHashMap. 这个方法可以在com.google.common.collect包中找到。 Joiner 类将map中的每个对象以”#”分隔. withKeyValueSeparator 返回MapJoiner类,将map里面的key/value对象以”=”分隔。 最后我们使用Assert进行断言。
另一个常见的操作就是解析一个以固定分隔符分隔的字符串,并返回一个包含这个String的数组,如果你需要去阅读一个text文件,你会经常要处理这种情况。但是String.split方法还有一些可以改进的地方,就像下面的例子所展示的:
String testString = "Monday,Tuesday,,Thursday,Friday,,";
//parts is [Monday, Tuesday, , Thursday,Friday]
String[] parts = testString.split(",");
从上面的例子中你可以看到String.split方法默认将最后连个空的字符串去除了,在有些情况下你是希望这么做的。但是这样并不是特别好,这样的选择应该留给程序员去处理。Splitter类可以协助去处理这样的情况。 Splitter 类的作用刚好和Joiner相反,Spliter 类可以使用在 字符,固定的字符串,正则表达式,CharMatcher类(下面我们会介绍这个类). 通过调用Splitter类的on方法,可以获取Splitter实例,一旦创建了Splitter类,就可以调用split方法,spliter方法会返回一个包含了结果的可迭代对象.
Splitter.on('|').split("foo|bar|baz");
Splitter splitter = Splitter.on("\\d+");
上面的例子,我们看到一个Splitter实例使用’|’分隔,另外一个splitter使用了一个正则表达式
Splitter 类还有一个选项可以将字符串中的空白字符去除。 效果就想String.split。只要使用 trimResults()
//Splits on '|' and removes any leading or trailing whitespace
Splitter splitter = Splitter.on('|').trimResults();
Splitter splitter = Splitter.on('|');
//Next call returns a new instance, does not
modify the original!
splitter.trimResults();
//Result would still contain empty elements
Iterable parts = splitter.split("1|2|3|||");
//MapSplitter is defined as an inner class of Splitter
Splitter.MapSplitter mapSplitter = Splitter.on("#").
withKeyValueSeparator("=");
就像我们所看到的一样,MapSplitter的创建和MapJoiner类一样,先创建一个Splitter类,然后通过一个指定一个特定的分隔符得到一个MapSplitter对象。
@Test
public void testSplitter() {
String startString = "Washington D.C=Redskins#New York
City=Giants#Philadelphia=Eagles#Dallas=Cowboys";
Map<String,String> testMap = Maps.newLinkedHashMap();
testMap.put("Washington D.C","Redskins");
testMap.put("New York City","Giants");
testMap.put("Philadelphia","Eagles");
testMap.put("Dallas","Cowboys");
Splitter.MapSplitter mapSplitter =
Splitter.on("#").withKeyValueSeparator("=");
Map<String,String> splitMap =
mapSplitter.split(startSring);
assertThat(testMap,is(splitMap));
}
上面的单元测试类,使用一个String对象,并且使用MapSplitter类创建一个LinkedHashMap实例。
这里我们覆盖了Joiner类和Splitter类的使用,这两个类应该要放到开发人员的工具箱中。
不管你更喜欢使用哪种语言,程序员都必须和字符串打交道,有时候这是无趣的并且容易出错的。 有的时候,我们需要从文件或者数据库的表中读取数据,并格式化这些数据。 或者为了满足业务需求我们需要排序等操作,幸运的是,Guava给我们提供一些非常有用的类,可以帮助我们处理Strings类更加容易.这些类是:
- CharMatcher
- Charsets
- Strings
下面让我们看一下在我们的代码中怎么样是使用这些类。
在Java平台下有个6个标准的字符集,下面是和字符集相关的非常常见的例子:
byte[] bytes = someString.getBytes();
但是上面的这个代码片段是有问题的,在获取byte的时候没有指定字符集编码,那么默认就会去系统的默认编码,但是有可能系统默认的编码不是你想要的编码。因此一般的最佳实践是按照如下的方式获取byte数据:
try{
bytes = "foobarbaz".getBytes("UTF-8");
}catch (UnsupportedEncodingException e){
//This really can't happen UTF-8 must be supported
}
上面的例子中依然有两个错误的地方:
- UTF-8 在Java平台上是肯定会被支持的,所以基本上是不可能会抛出UnsupportedEncodingException异常的。
- 我们是用’UTF-8’这种方式指定编码的,有可能会出现输入错误的情况,例如不小心输入成’UF-8’.
上面的这两个问题正是CharSets可以解决。 Charsets类 对于这6个字符集提供了一个 final的静态的引用。 使用Charsets,上面的那段代码可以简化为如下:
byte[] bytes2 = "foobarbaz".getBytes(Charsets.UTF_8);
不过在Java7中已经提供了类似的功能,在Java 7中的类名是: StandardCharsets.
Strings提供了一些工具方法区处理字符串,你是否写过下面的代码:
StringBuilder builder = new StringBuilder("foo");
char c = 'x';
for(int i=0; i<3; i++){
builder.append(c);
}
return builder.toString();
在上面的这段代码中,我们使用了6行代码完成了这么一个简单的功能,现在利用Strings 我们可以简单的用一行代码就可以完成:
Strings.padEnd("foo",6,'x');
这里面有个重要的提示,这里面的6不是指将x循环的增加6遍,而是指将’foo’补充成6位长度。 如果这时候 ‘fooxxxx’已经是6位长度了,那么这段代码将不起任何作用. 同样 有一个’padStart’方法,它的作用和’padEnd’是相似的,只不过是在字符串前面补充指定的字符。补充到指定的长度。
在处理null值方面,Strings有三个比较好用的方法:
- nullToEmpty 这个方法获取一个字符串参数,如果这个参数不为null或长度大于0,那么直接返回该参数,否则返回”” 空字符串
- emptyToNull 这个方法和nulltoEmpty差不多,只不过如果参数是null或则长度不大于0 那么会返回 null
- isNullOrEmpty 当传入的参数书null或则长度为0 那么这个方法的返回值就是 true。
最佳实践: 在讲String作为参数传递时,最好先使用nullToEmpty对字符串做一下处理。
Charmatcher 类提供了处理各种characters的方法,能够非常容易的格式化处理文本。 下面的例子展示的是,使用CharMatcher将一个多行的字符串转换成以’ ‘分隔的单行字符串
CharMatcher.BREAKING_WHITESPACE.replaceFrom(stringWithLinebreaks,' ');
replaceFrom方法不仅可以接受一个char类型参数还可以接受一个charsequence参数。
为了将连续的多个 tab或者空格 压缩成一个空格,我们可以使用下面的代码:
@Test
public void testRemoveWhiteSpace(){
String tabsAndSpaces = "String with spaces and
tabs";
String expected = "String with spaces and tabs";
String scrubbed = CharMatcher.WHITESPACE.
collapseFrom(tabsAndSpaces,' ');
assertThat(scrubbed,is(expected));
}
上面的test中,我们看到我用一行代码就将多个tab及多个空变成一个空格。 但是上面的这个例子只是在特殊的情况下有效,如果我们想将字符串前面的空格都去掉,只保留字符中间的一个空格呢? 尽管需求这么变态,使用 trimAndCollapseFrom方法就可以实现,示例如下:
@Test
public void testTrimRemoveWhiteSpace(){
String tabsAndSpaces = " String with spaces and
tabs";
String expected = "String with spaces and tabs";
String scrubbed = CharMatcher.WHITESPACE.
trimAndCollapseFrom(tabsAndSpaces,' ');
assertThat(scrubbed,is(expected));
}
上面的例子我们依然讨论了如何将多个tab或空格变成一个空格,现在让我们来看一下其他的使用方式,
@Test
public void testRetainFrom(){
String lettersAndNumbers = ""foo989yxbar234"";
String expected = ""989234"";
String retained = CharMatcher.JAVA_DIGIT.
retainFrom(lettersAndNumbers);
assertThat(expected,is(retained));
}
上面的例子中,我们将所有的数字都提取了出来,仅仅是使用了一行代码哦!
String lettersAndNumbers = "fo o989yxbar234";
String expected = " 989234";
CharMatcher.JAVA_DIGIT.or(CharMatcher.WHITESPACE).retainFrom(lettersAndNumbers);
上面的例子中既可以提取数字也可以提取空白字符
由此可见,CharMatcher在处理Java字符串方面是非常好用并且非常好用的方法。
Preconditions类里面包含了一系列的静态方法,用来验证我们期望的值。 Preconfitions 是非常重要,因为他可以保证我们的预期和实际发生的是一致的。 一旦预期和实际运行的记过不一致,我们立马就可以得到反馈,因此使用preconditions 可以确保我们的代码在我们预期的情况下运行着。 另外对于debug也是非常有好处的。
- 在使用preconfitions 之前我们一定写过下面的代码:
if(someObj == null){
throw new IllegalArgumentException(" someObj must
not be null");
}
使用preconfitions 后,我们可以如下的方法进行检查:
checkNotNull(someObj,"someObj must not be null");
其中preconfitions 是可以静态导入的 static imports
下面我们来看一个例子:
public class PreconditionExample {
private String label;
private int[] values = new int[5];
private int currentIndex;
public PreconditionExample(String label) {
//returns value of object if not null
this.label = checkNotNull(label,"Label can''t be null");
}
public void updateCurrentIndexValue(int index, int valueToSet) {
//Check index valid first
this.currentIndex = checkElementIndex(index, values.length,
"Index out of bounds for values");
//Validate valueToSet
checkArgument(valueToSet <= 100,"Value can't be more than
100");
values[this.currentIndex] = valueToSet;
}
public void doOperation(){
checkState(validateObjectState(),"Can't perform operation");
}
private boolean validateObjectState(){
return this.label.equalsIgnoreCase("open") && values[this.
currentIndex]==10;
}
}
从上面的例子中,我们总结一下这个几个方法的使用:
1. checkNotNull(T object,Object message) : 如果object是null 那么就会抛出一个NullPointException异常,否则返回这个Object.
2. checkElementIndex(int index,int size,Object message): 这个方法中 index 参数是你想要访问的位置,size 是array,list,或者string的长度, 如果 index的范围大于size 那么将会抛出 IndexOutOfBoundexception 否则返回index的值
3. checkArgument(Boolean expression,Object message): 根据expression表达式算出来的值做判断,如果值为false那么就会抛出IllegalArgumentException异常
4. checkState(Boolean expression,Object message): 作用和checkArgument一样,代码实现也是一样,使用场景不一致,checkArgument 一般用来检查输入参数
这一个小章节中,我们将覆盖 空值检查,生成 toString,hashCode 等方法。 我们还将学习一个新的类帮助我们从实现Comparable接口痛苦摆脱出来。
toString 方法在我们debug或者查日志的时候是十分方便的,但是写一个toString方法却是一个比较无趣的事情,但是使用Objects的toStringHelper方法你可以很方便的实现,下面我们看一个例子:
public class Book implements Comparable<Book> {
private Person author;
private String title;
private String publisher;
private String isbn;
private double price;
....
public String toString() {
return Objects.toStringHelper(this)
.omitNullValues()
.add("title", title)
.add("author", author)
.add("publisher", publisher)
.add("price",price)
.add("isbn", isbn).toString();
}
让我们来探究一下这段代码的意思
- 首先我们传递一个Book的引用,创建一个ToStringHelper对象
- 调用omitNullValues 排除属性是空值的
- 分别调用 add 方法将 ‘label’ 和对应的值
不过貌似这个还没有 apache 下面的 ToStringBuilder好用
firstNonNull 方法接受两个参数,如果参数是空,就返回设置的默认值,否则指定的第一次参数,如果传入的两个参数都是null 那么就会抛出NullPointException异常
方法很简单,如下:
Objects.hashCode(Object...);
我们任然使用上面的Book类,下面是一个实现CompareTo接口典型实现:
public int compareTo(Book o) {
int result = this.title.compareTo(o.getTitle());
if (result != 0) {
return result;
}
result = this.author.compareTo(o.getAuthor());
if (result != 0) {
return result;
}
result = this.publisher.compareTo(o.getPublisher());
if(result !=0 ) {
return result;
}
return this.isbn.compareTo(o.getIsbn());
}
下面我们来看一下使用ComparisonChain的方式实现compareTo接口:
public int compareTo(Book o) {
return ComparisonChain.start()
.compare(this.title, o.getTitle())
.compare(this.author, o.getAuthor())
.compare(this.publisher, o.getPublisher())
.compare(this.isbn, o.getIsbn())
.compare(this.price, o.getPrice())
.result();
}
上面的这个例子中,使用ComparisonChain的方式更加简单,也更加易读,只要其中的一个比较不为0,那么就直接返回。
在这一章中我们已经介绍了很多。
- 我们学习了怎样使用Joiner,Splitter,MapJioner,MapSplitter,Charsets,CharMatcher,Strings 让我们在处理格式化的字符串时更加容易
- 我们学习了使用Preconfitions类让我们的代码更加强壮,学习使用了 Objects类的 toString,hashCode 方法让我们的debug更加容易,我们还学习了ComparisonChain类,让我们实现CompareTo方式更加容易
- 在下一章,我们将学习怎样使用guava的函数式变成,让我们的代码更加清晰和强壮。我们将涉及两个接口: Function 和 Predicate 接口