基本Guava工具
使用Joiner类
将任意字符串通过分隔符进行连接到一起是大多程序员经常做的事情。他们经常使用array,list,iterable并且循环变量将每一个临时变量添加到StringBuilder当中去,并且中间添加分隔符。这些笨重的处理方式如下:
public String buildString(List<String> stringList, String delimiter){ StringBuilder builder = new StringBuilder(); for (String s : stringList) { if(s !=null){ builder.append(s).append(delimiter); } } builder.setLength(builder.length() – delimiter.length()); return builder.toString(); }
注意要删除在最后面的分隔符。不是很难懂,但是使用Joiner类可以得到简单的代码模板。同样的例子使用Joiner类代码如下:
public String buildString(List<String> stringList, String delimiter){ return Joiner.on(delimiter).skipNulls().join(stringList); }
这样更加简明并且不会出错。如果你想将null值替换掉,可以使用如下方法:
Joiner.on("|").useForNull("no value").join(stringList);
使用Joiner类有几点需要注意。Joiner类不仅仅可以处理字符串的array、list、iterable,他还可以处理任何对象的array、list、iterable。结果就是调用每一个元素的toString()方法。因此,如果没有使用skipNulls或者useForNull,就会抛出空指针异常。Joiner对象一旦被创建就是不可变的,所以他们是线程安全的,可以被当作常亮来看待。然后看看下面的代码片段:
Joiner stringJoiner = Joiner.on("|").skipNulls(); //使用useForNull方法将会返回一个新的Joiner实例 stringJoiner.useForNull("missing"); stringJoiner.join("foo","bar",null);
在上面的代码实例当中,useForNull方法并没有起作用,null值仍然被忽略了。
Joiner不仅仅能返回字符串,还可以与StringBuilder一起使用:
StringBuilder stringBuilder = new StringBuilder(); Joiner joiner = Joiner.on("|").skipNulls(); //返回的StringBuilder实例当中包含连接完成的字符串 joiner.appendTo(stringBuilder,"foo","bar","baz");
上面的例子,我们传入一个StringBuilder的参数并且返回一个StringBuilder实例。
Joiner类也可以使用实现了Appendable接口的类。
FileWriter fileWriter = new FileWriter(new File("path")): List<Date> dateList = getDates(); Joiner joiner = Joiner.on("#").useForNulls(" "); // 返回由字符串拼接后的FileWriter实例 joiner.appendTo(fileWriter,dateList);
这是一个与上一个相似的例子。我们传入一个FileWriter实例和一组数据,就会将这组数据拼接后附加到FileWriter当中并且返回。
我们可以看到,Joiner使一些公共的操作变的非常简单。有一个特殊的方法会实现MapJoiner方法,MapJoiner方法像Joiner一样使用分割符将每组key与value分开,同时key与value之间也有个分隔符。MapJoiner方法的创建如下:
MapJoiner mapJoiner = Joiner.on("#").withKeyValueSeparator("=");
快速回顾一下上面内容:
- Joiner.on("#")方法会创建一个Joiner的实例。
- 使用返回的Joiner实例调用withKeyValueSeparator方法将会返回MapJoiner对象。
下面是对MapJoiner方法的单元测试代码:
@Test public void testMapJoiner() { 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)); }
回顾时间
上面的单元测试开始时创建一个key和value都是字符串的LinkedHashMap实例,值得注意的是我们使用静态工厂方法newLinkedHashMap()来创建,Maps类是在com.google.common. collect包当中。然后使用Joiner类创建一个使用key与value拼接的字符串。最后我们断言他是否与我们期望返回的字符串相同。
使用Splitter类
程序员另一个经常处理的问题是对字符串以特定分隔符进行分割并且获取一个字符串数组。如果你需要读取文本文件,你总是会做这样的事情。但是
String.split方法不够完美,看下面的例子:
String testString = "Monday,Tuesday,,Thursday,Friday,,"; //parts is [Monday, Tuesday, , Thursday,Friday] String[] parts = testString.split(",");
可以看到,String.split方法省略了最后的2个空串。在有些时候,这个做法是你需要的,但是这些事情是应该由程序员来决定是否省略。Splitter类可以帮助我们实现与Joiner类相反的功能。Splitter可以使用单个字符、固定字符串、正则表达式串、正则表达式对象或者CharMatcher对象(另一个Guava的类,本章会讲到)来分割字符串。可以给定具体分割符来创建Splitter对象然后使用。一旦拥有了Splitter实例后就可以调用split方法,并且会返回包含分割后字符串的迭代器对象。
Splitter.on('|').split("foo|bar|baz");
Splitter splitter = Splitter.on("\\d+");
在上面的例子当中,我们看到一个Splitter 实例使用了'|'字符分割,另外一个实例使用了正则表达式进行分割。
Splitter有一个可以处理前面空格和后面空格的方法是trimResults。
//Splits on '|' and removes any leading or trailing whitespace Splitter splitter = Splitter.on('|').trimResults();
与Joiner类一样Splitter类同样是一个不可变的类,所以在使用的时候应该使用调用trimResults方法后返回的Splitter实例。
Splitter splitter = Splitter.on('|'); //Next call returns a new instance, does not modify the original! splitter.trimResults(); //Result would still contain empty elements Iterable<String> parts = splitter.split("1|2|3|||");
Splitter 类,像Joiner与MapJoiner一样也有MapSplitter类。MapSplitter类可以将字符串转换成Map实例返回,并且元素的顺序与字符串给定的顺序相同。使用下面方法构造一个MapSplitter实例:
//MapSplitter is defined as an inner class of Splitter Splitter.MapSplitter mapSplitter = Splitter.on("#"). withKeyValueSeparator("=");
可以看到MapSplitter的创建方式与MapJoiner一样。首先给Splitter指定一个分隔符,然后指定MapSplitter对象key与value的分隔符。下面是一个关于MapSplitter的例子,实现的是与MapJoiner相反地功能。
@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)); }
回顾时间
上面的单元测试是将一个字符串用MapSplitter分割成一个LinkedHashMap的实例。然后断言这个返回的LinkedHashMap实例是否与我们期望是LinkedHashMap实例相同。
Joiners 和Splitters 这两个类应该是每个java开发人员的工具。
使用Guava操作字符串
无论你喜欢的哪种语言,所有跟字符串相关的事情都是无聊乏味的并且容易出错。有时候,我们需要从文件或者数据当中读取数据并且重新格式化这些数据提交给用户,或者以固定的格式来保存这些数据。幸运的是,Guava提供给我们非常好用的类使我们操作字符串更加轻松。这些类如下:
- CharMatcher
- Charsets
- Strings
现在我们看一看如何在代码中使用它们。
在第一个例子当中,这个单元测试将会展示使用ASCII类方法来确定一个字符是否是小写。第二个例子将展示将小写字符串转换成大写。
使用Charsets类
在java当中,有6个标准字符集在每一个java平台都会被支持。这与经常运行下面代码是相关的:
byte[] bytes = someString.getBytes();
但是有一个问题关于上面的代码。没有指定你想返回字节的字符集,你将会获得系统使用运行时默认的字符集返回的字节,这可能会产生问题有可以默认字符集不是你想要的。所以最佳的做法是像下面这样:
try{ bytes = "foobarbaz".getBytes("UTF-8"); }catch (UnsupportedEncodingException e){ //This really can't happen UTF-8 must be supported }
但是仍然有两个问题在上面的代码当中:
- UTF-8在java平台 一定会被支持,所以UnsupportedEncodingException一定不会被抛出
- 一旦我们使用字符串指定字符集的定义,我们可以产生拼写错误然后导致异常抛出。
Charsets类可以帮助我们,Charsets类提供了static final 的六个java平台支持的字符集。使用Charsets类我们可以使上面的例子更简单些:
byte[] bytes2 = "foobarbaz".getBytes(Charsets.UTF_8);
值得注意的是在JAVA7当中,StandardCharsets也提供了同样的功能。现在我们看看Strings类。
使用Strings类
Strings类提供一些便利实用的方法处理字符串。你是否写过像下面的代码:
StringBuilder builder = new StringBuilder("foo"); char c = 'x'; for(int i=0; i<3; i++){ builder.append(c); } return builder.toString();
上面例子当中的6行代码我们可以使用一行代码来替换。
Strings.padEnd("foo",6,'x');
第二个参数是很重要的,6指定返回的字符串最小长度为6,而不是指定'x'字符在字符串后面追加多少次。如果提供的字符串长度已经大于了6,则不会进行追加。同样也有一个相类似的padStart方法可以在给定字符串的前面追加字符到指定的长度。
在Strings类当中有三个非常有用的方法来处理空值的:
- nullToEmpty:这个方法接受一个字符串参数,如果传入的参数不是null值或者长度大于0则原样返回,否则返回空串("");
- emptyToNull:这个方法类似于nullToEmpty,它将返回null值如果传入的参数是空串或者null。
- isNullOrEmpty:这个方法会检查传入参数是否为null和长度,如果是null和长度为0就返回true。
或许,这将是一个不错的主意总是在任何使用nullToEmpty方法处理传入的字符串参数。
使用CharMatcher类
CharMatcher提供了在一定字符串中匹配是否存在特定字符串的功能。在CharMatcher类当中的方法也可以让格式化更加简单。例如,你可以将多行的字符串格式化成一行,并且换行符将会以空格来代替。
CharMatcher.BREAKING_WHITESPACE.replaceFrom(stringWithLinebreaks,' ');
还有一个版本replaceFrom的,需要一个CharSequence的值作为第2个参数值,而不是一个单一的字符。
移除多个空格和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)); }
在上面的测试代码中,我们把所有多个空格和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替换成一个空格也在一行上。
列出所有在CharMatcher类可用的方法是不切实际的,这里是不是更换一组匹配字符的地方,我们保留所匹配的字符的例子:
@Test public void retainFromTest() { String lettersAndNumbers = "foo989yxbar234"; String expected = "989234"; String actual = CharMatcher.JAVA_DIGIT.retainFrom(lettersAndNumbers); assertEquals(expected, actual); }
在这个例子当中我们找到"foo989yxbar234"字符串当中所以的数字并且保留下来。
往下继续之前,我们应该看看最后一个CharMatcher类中的强大功能。可以联合多个CharMatcher类实例创建一个新的CharMatcher类实例。
假设你需要创一个匹配数字和空格的CharMatcher类实例:
CharMatcher cm = CharMatcher.JAVA_DIGIT.or(CharMatcher.WHITESPACE);
它现在将会匹配任何数字或者空格。CharMatcher类对于操作字符串来说功能强大并且很好用。
使用Preconditions类
Preconditions类是用来验证我们的代码状态的静态方法的集合。 Preconditions非常重要,因为他们保证我们的期望执行成功的代码得到满足。 如果条件与我们期望的不同,我们会及时反馈问题。和以前一样,使用前提条件的重要性是确保我们代码的行为,并在调试中很有用。
你可以写你自己的先决条件,像下面这样:
if(someObj == null){ throw new IllegalArgumentException(" someObj must not be null"); }
使用Preconditions当中的方法(需要静态导入),检查一个空值更简单。
checkNotNull(someObj,"someObj must not be null");
接下来,我们将要展示使用先决条件的例子:
public class PreconditionExample { private String label; private int[] values = new int[5]; private int currentIndex; public PreconditionExample(String label) { //返回label如果不为空 this.label = checkNotNull(label,"Label can''t be null"); } public void updateCurrentIndexValue(int index, int valueToSet) { //检查索引是否有效 this.currentIndex = checkElementIndex(index, values.length, "Index out of bounds for values"); //检查参数值 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; } }
下面是对上面例子当中四个方法的摘要信息:
- checkNotNull(T object, Object message):这个方法如果object不为null直接返回,如果为null会抛出空指针异常。
- checkElementIndex (int index, int size, Object message):在这方法当中,index是你将要访问的元素下标,size是这个要访问的array,list或者字符串的长度。然后校验是否有效,如果无效抛出IndexOutOfBoundsException。
- checkArgument (Boolean expression, Object message):这方法传入布尔表达式。 这个布尔表达式如果为true则继续执行,否则抛出IllegalArgumentException。
- checkState (Boolean expression, Object message):这方法传入 一个布尔表达式涉及对象的状态,而不是参数。 这个布尔表达式如果为true则继续执行,否则抛出IllegalArgumentException。
Object工具
在这个章节当中我们将介绍帮助检查null值和创建toString和hashCode的方法的实用方法。我们接着去看看一个有用的类,它实现Comparable接口。
Getting help with the toString method
当我们要调试的时候,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(); } }
让我们看看toString方法:
- 首先我们传入一个Book对象来创建一个Objects.ToStringHelper实例。
- 第二步,我们调用omitNullValues来排除任何null值的属性。
- 调用add方法来添加每一个属性的标签和属性。
检查null值
firstNonNull方法接受2个参数并且返回第一个参数如果它不为null。
String value = Objects.firstNonNull(someString, "default value");
firstNonNull方法使用时候如果你不确定传入的值是否为null你可以提供一个默认值给它。需要注意:如果传入的2个参数都是null,会抛出空指针异常。
创建hash codes
为类写hashCode方法是基本的但是乏味无趣。Objects类可以帮助我们使用hashCode方法更加简单。考虑下Book类有4个属性:title, author, publisher, 和isbn. 下面的代码将展示使用Object.hashCode方法返回hashCode值。
public int hashCode() { return Objects.hashCode(title, author, publisher, isbn); }
实现CompareTo方法
再次使用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,那就是所有的比较返回的都是0。
概要
在本章中我们介绍了许多基础操作。我们学习了如何使用Guava让分割字符串更简单通过使用Joiner, Splitter,和非常有用的MapJoiner和MapSplitter类。我们也学习了使用Charsets, CharMatcher, 和 Strings 来操作字符串。
我们学习了如何让我们代码更加强壮和便于调试通过使用Preconditions类。在Objects类中,我们学习了一些使用方法来设置默认值和创建toString和hashCode方法。我们也学习了使用ComparisonChain类来实现compareTo方法更简单。
在下一章,我们学习如何使用Guava来进行函数式编程,通过Function和Predicate接口使我们的编程效率更加高。