淺談Java 8 API增強(qiáng)
這里針對(duì)Java 8在API方面的增強(qiáng)及相關(guān)使用方式做一些簡(jiǎn)單的介紹

Collection
removeIf方法
眾所周知,對(duì)于List、Set等集合而言,如果期望刪除某些元素,其實(shí)是一件非常麻煩的事情。例如下面的示例,刪除列表中長(zhǎng)度大于2的元素
@Test
public void test1() {
List<String> list = new LinkedList<>();
list.add("乒乓球");
list.add("足球");
list.add("羽毛球");
list.add("籃球");
list.forEach( e- > {
// 移除長(zhǎng)度大于2的元素
if( e.length()>2 ) {
list.remove(e);
}
});
System.out.println("list: " + list);
}
結(jié)果顯然易見(jiàn),如下所示,刪除失敗。原因自然是不言自明。在迭代器遍歷過(guò)程中,通過(guò)list.remove方法進(jìn)行刪除是不允許的。除非我們顯式使用迭代器進(jìn)行遍歷、刪除,顯然這樣非常繁瑣

為此Java8在Collection中提供removeIf默認(rèn)方法,其接收一個(gè)謂詞參數(shù)。大大方便了我們對(duì)集合進(jìn)行刪除的操作,示例如下所示
@Test
public void test2() {
List<String> list = new LinkedList<>();
list.add("乒乓球");
list.add("足球");
list.add("羽毛球");
list.add("籃球");
// 移除長(zhǎng)度大于2的元素
list.removeIf( e -> e.length()>2 );
System.out.println("list: " + list);
}
測(cè)試結(jié)果如下,符合預(yù)期

Map
計(jì)算模式
在Map中可以根據(jù)Key存在與否的情況,按條件執(zhí)行相關(guān)操作并將計(jì)算結(jié)果作為Value存儲(chǔ)到Map中。具體有以下方法:
compute:使用指定的Key計(jì)算新值,并將計(jì)算結(jié)果作為Value存儲(chǔ)到Map中 computeIfAbsent:如果指定Key在Map中沒(méi)有對(duì)應(yīng)的值(Key不存在 或 其值為null), 則計(jì)算該Key的新值并存儲(chǔ)到Map中 computeIfPresent:如果指定Key在Map中有對(duì)應(yīng)的值(Key存在 且 其值不為null),則計(jì)算該Key的新值并存儲(chǔ)到Map中
compute方法
比如下面的代碼,是一個(gè)統(tǒng)計(jì)單詞次數(shù)的方法
public void test1() {
// 每個(gè)單詞出現(xiàn)的次數(shù), key: 單詞; value: 次數(shù)
Map<String, Integer> map = new HashMap<>();
String doc = "I am Aaron I like Aaron";
String[] words = doc.split(" ");
for(String word : words) {
Integer count = map.get(word);
if( count==null ) {
count = 0;
}
count++;
map.put(word, count);
}
System.out.println("map : " + map);
}
如果采用compute方法則可以大大簡(jiǎn)化代碼,如下所示。利用Key及其Value進(jìn)行計(jì)算,并將計(jì)算結(jié)果作為該Key的新值存儲(chǔ)到Map中
@Test
public void test2() {
// 每個(gè)單詞出現(xiàn)的次數(shù), key: 單詞; value: 次數(shù)
Map<String, Integer> map = new HashMap<>();
String doc = "I am Aaron I like Aaron";
String[] words = doc.split(" ");
for(String word : words) {
map.compute( word, (key, oldValue) ->{
if( oldValue==null ) {
oldValue = 0;
}
oldValue++;
return oldValue;
});
}
System.out.println("<Test 2> map : " + map);
}
兩個(gè)方法測(cè)試結(jié)果如下,符合預(yù)期

computeIfAbsent方法
下面是一個(gè)在Map中保存各人所喜歡的書(shū)單的方法。每次在向value中添加書(shū)時(shí),都需要判斷該value是否為空,非常繁瑣
@Test
public void test1() {
// 每個(gè)人喜歡的書(shū)單, key: 人名; value: 書(shū)名列表
Map<String, List<String>> map = new HashMap<>();
// 場(chǎng)景: 給Amy喜歡的書(shū)單添加《老人與?!?/span>
String name = "Amy";
String bookName = "老人與海";
List list = map.get(name);
if( list==null ) {
list = new LinkedList<>();
map.put(name, list);
}
list.add(bookName);
System.out.println("<Test 1> map : " + map);
}
幸運(yùn)的是,在有了computeIfAbsent方法后,我們實(shí)現(xiàn)起來(lái)就簡(jiǎn)潔很多
@Test
public void test2() {
// 每個(gè)人喜歡的書(shū)單, key: 人名; value: 書(shū)名列表
Map<String, List<String>> map = new HashMap<>();
// 場(chǎng)景: 給Amy喜歡的書(shū)單添加《老人與?!?/span>
String name = "Amy";
String bookName = "老人與海";
// 如果map中 key不存在 或 key所對(duì)應(yīng)的值為null
// 則會(huì)通過(guò)第二個(gè)參數(shù) function 計(jì)算新value,并put到map中
// 該方法最終會(huì)返回該key所對(duì)應(yīng)的value值
List<String> list = map.computeIfAbsent( name, key -> new LinkedList<>() );
list.add(bookName);
System.out.println("<Test 2> map : " + map);
}
可以看到,computeIfAbsent方法非常適用于value為集合類(lèi)型、且需要向該集合中添加元素的場(chǎng)景。測(cè)試結(jié)果如下,符合預(yù)期

與此同時(shí),computeIfAbsent方法也適用于緩存信息的場(chǎng)景。在下面的示例中,對(duì)于已有簽名數(shù)據(jù)作為Value的Key,顯然沒(méi)有必要重復(fù)計(jì)算、浪費(fèi)資源
@Test
public void test3() {
Map<String, Integer> map = new HashMap<>();
System.out.println("\n-------------------- Test 1: Start --------------------");
map.computeIfAbsent("China", this::calcSign);
System.out.println("-------------------- Test 1: End ----------------------");
System.out.println("\n-------------------- Test 2: Start --------------------");
map.computeIfAbsent("USA", this::calcSign);
System.out.println("-------------------- Test 2: End ----------------------");
System.out.println("\n-------------------- Test 3: Start --------------------");
map.computeIfAbsent("China", this::calcSign);
System.out.println("-------------------- Test 3: End ----------------------");
System.out.println("\nmap: " + map);
}
/**
* 計(jì)算簽名
* @param str
* @return
*/
private Integer calcSign(String str) {
System.out.println( "Start Calc Sign, str: " + str);
Integer result = str.hashCode();
return result;
}
測(cè)試結(jié)果如下,符合預(yù)期

computeIfPresent方法
下面是一個(gè)在Map中保存各人所喜歡的書(shū)單的方法。每次從value中移除書(shū)時(shí),都需要判斷該value是否為空,非常繁瑣
@Test
public void test1() {
// 每個(gè)人喜歡的書(shū)單, key: 人名; value: 書(shū)名列表
Map<String, List<String>> map = new HashMap();
// map數(shù)據(jù)初始化
map.put("Amy", new ArrayList(Arrays.asList("資治通鑒", "金瓶梅", "山海經(jīng)")) );
// 場(chǎng)景: Amy喜歡的書(shū)單中不能有《金瓶梅》
String name = "Amy";
String bookName = "金瓶梅";
List<String> list = map.get(name);
if( list!=null ) {
list.remove( bookName );
}
System.out.println("<Test 1> map : " + map);
}
結(jié)果符合預(yù)期,如下所示

幸運(yùn)的是,在有了computeIfPresent方法后,我們實(shí)現(xiàn)起來(lái)就簡(jiǎn)潔很多
@Test
public void test2() {
// 每個(gè)人喜歡的書(shū)單, key: 人名; value: 書(shū)名列表
Map<String, List<String>> map = new HashMap<>();
// map數(shù)據(jù)初始化
map.put("Amy", new ArrayList(Arrays.asList("資治通鑒", "金瓶梅", "山海經(jīng)")) );
map.put("Bob", new ArrayList(Arrays.asList("三年高考五年模擬")) );
map.put("Tony", new ArrayList(Arrays.asList("21天入門(mén)理發(fā)")) );
// 場(chǎng)景: Amy喜歡的書(shū)單中不能有《金瓶梅》
String name = "Amy";
String bookName1 = "金瓶梅";
// 如果map中key所對(duì)應(yīng)的value不為null
// 則會(huì)通過(guò)第二個(gè)參數(shù), 利用key、oldValue 計(jì)算newValue,并put到map中
// 該方法最終會(huì)返回該key所對(duì)應(yīng)的value值
map.computeIfPresent(name, (key, value) -> {
value.remove( bookName1 );
return value;
} );
// 場(chǎng)景: Bob喜歡的書(shū)單中不能有《三年高考五年模擬》
name = "Bob";
String bookName2 = "三年高考五年模擬";
map.computeIfPresent(name, (key, value) -> {
value.remove( bookName2 );
return value;
} );
// 場(chǎng)景: 用戶(hù)Tony注銷(xiāo)了,不需要再保存其喜歡的書(shū)單
name = "Tony";
// 在computeIfPresent方法中, 如果該key計(jì)算的newValue為null, 則該映射會(huì)被移除
map.computeIfPresent(name, (key, value) -> {
List newValue = null;
return newValue;
} );
System.out.println("<Test 2> map : " + map);
}
可以看到,computeIfPresent方法非常適用于value為集合類(lèi)型、且需要從集合中移除元素的場(chǎng)景。測(cè)試結(jié)果如下,符合預(yù)期。值得一提的是在computeIfPresent方法中,如果該key計(jì)算的新值為null, 則該映射會(huì)被移除

merge方法
Map接口提供的默認(rèn)方法merge,簽名如下。其第一、二個(gè)參數(shù)就是我們期望存儲(chǔ)到Map的key、newValue。但如果該key在map已經(jīng)存在且其值(這里記為oldValue)不為空,則就需要通過(guò) 第三個(gè)參數(shù)(BiFunction類(lèi)型),其定義了合并oldValue、newValue這兩個(gè)值的計(jì)算規(guī)則
merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
通過(guò)下面的例子,可以更好的幫助大家理解
@Test
public void test1() {
Map<String, String> map = new HashMap<>();
map.put("Aaron", "籃球");
map.merge("Bob", "足球", (oldValue, newValue) -> oldValue+"&"+newValue );
System.out.println("map 1: " + map);
map.merge("Aaron", "乒乓球", (oldValue, newValue) -> oldValue+"&"+newValue );
System.out.println("map 2: " + map);
}
測(cè)試結(jié)果如下,符合預(yù)期
figure 8.jpeg
可以看到,由于merge方法對(duì)于欲插入的key是否在map中存在不為null的value,實(shí)際上是提供了兩種不同的計(jì)算路徑。故對(duì)于上文通過(guò)compute方法實(shí)現(xiàn)次數(shù)統(tǒng)計(jì)的例子而言,我們?nèi)绻褂胢erge方法實(shí)現(xiàn)會(huì)更加簡(jiǎn)單,示例代碼如下所示
@Test
public void test2() {
// 每個(gè)單詞出現(xiàn)的次數(shù), key: 單詞; value: 次數(shù)
Map<String, Integer> map = new HashMap<>();
String doc = "can Aaron can Aaron like I can like Aaron can";
String[] words = doc.split(" ");
for(String word : words) {
// 該單詞在map中value為null,說(shuō)明該單詞首次被統(tǒng)計(jì), 故直接記為1
// 該單詞在map中value不為null,說(shuō)明該單詞非首次被統(tǒng)計(jì), 故使用原有次數(shù)自增1
map.merge(word, 1, (oldValue, newValue)->oldValue+1 );
}
System.out.println("<Test 2> map : " + map);
}
測(cè)試結(jié)果如下,符合預(yù)期

Optional
orElse與orElseGet
二者都是用于在option實(shí)例中value值不存在時(shí),提供一個(gè)默認(rèn)值。但orElse方法是每次均會(huì)計(jì)算默認(rèn)值,無(wú)論option實(shí)例中value值是否存在;而orElseGet方法則是延遲計(jì)算,即只有在option實(shí)例中value值不存在時(shí),才會(huì)去計(jì)算默認(rèn)值。故如果對(duì)于默認(rèn)值的計(jì)算、獲取過(guò)程比較昂貴,推薦使用orElseGet方法
@Test
public void test1() {
Optional<String> optional1 = Optional.ofNullable( "Aaron" );
Optional<String> optional2 = Optional.ofNullable( null );
System.out.println("\n-------------------- Test 1: Start --------------------");
String s1 = optional1.orElse( getDefault() );
System.out.println("s1: " + s1);
System.out.println("-------------------- Test 1: End ----------------------");
System.out.println("\n-------------------- Test 2: Start --------------------");
String s2 = optional2.orElse( getDefault() );
System.out.println("s2: " + s2);
System.out.println("-------------------- Test 2: End ----------------------");
System.out.println("\n-------------------- Test 3: Start --------------------");
s1 = optional1.orElseGet( () -> getDefault() );
System.out.println("s1: " + s1);
System.out.println("-------------------- Test 3: End ----------------------");
System.out.println("\n-------------------- Test 4: Start --------------------");
s2 = optional2.orElseGet( () -> getDefault() );
System.out.println("s2: " + s2);
System.out.println("-------------------- Test 4: End ----------------------");
}
public String getDefault() {
System.out.println("call getDefault method");
return "Tony";
}
測(cè)試結(jié)果如下,符合預(yù)期

map與flatMap
Optional的初衷就是為了解決NPE而設(shè)計(jì)的。比如在傳統(tǒng)的代碼中,為了防止出現(xiàn)NPE,開(kāi)發(fā)者需要層層判空。示例代碼如下所示,這樣顯然非常繁瑣
public class OptionalTest {
@Test
public void test2() {
Person person = getPerson();
String addr = null;
if(person!=null) {
Person.Company company = person.getCompany();
if( company!=null ) {
addr = company.getAddr();
}
}
String tel = null;
if(person!=null) {
Person.Company company = person.getCompany();
if( company!=null ) {
tel = company.getTel();
}
}
System.out.println("addr: " + addr);
System.out.println("tel: " + tel);
}
/**
* 獲取Person實(shí)例
* @return
*/
public Person getPerson() {
Person.Family family = Person.Family.builder()
.mother("Amy")
.build();
Person person = Person.builder()
.name("Tony")
.company( Person.Company.builder()
.type("外貿(mào)")
.addr("廣東省")
.build())
.family( Optional.ofNullable(family) )
.build();
return person;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class Person {
private String name;
private Integer age;
private Company company;
private Optional<Family> family;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class Company {
private String type;
private String addr;
private String tel;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class Family {
private String father;
private String mother;
}
}
}
測(cè)試結(jié)果如下,符合預(yù)期

而自從有了Optional后,情況就大不一樣了。我們可以使用map、flatMap實(shí)現(xiàn)層層轉(zhuǎn)化。在整個(gè)鏈?zhǔn)秸{(diào)用過(guò)程中一旦某個(gè)Optional的value為null,則會(huì)返回一個(gè)空Optional,繼續(xù)執(zhí)行。以免出現(xiàn)NPE
@Test
public void test3() {
Person person = getPerson();
Optional<Person> optionalPerson = Optional.ofNullable(person);
String addr = optionalPerson.map( Person::getCompany )
.map( Person.Company::getAddr )
.orElse(null);
String tel = optionalPerson.map( Person::getCompany )
.map( Person.Company::getTel )
.orElse(null);
String mother = optionalPerson.flatMap( Person::getFamily )
.map( Person.Family::getMother )
.orElse(null);
String father = optionalPerson.flatMap( Person::getFamily )
.map( Person.Family::getFather )
.orElse(null);
System.out.println("addr: " + addr);
System.out.println("tel: " + tel);
System.out.println("mother: " + mother);
System.out.println("father: " + father);
}
測(cè)試結(jié)果如下,符合預(yù)期

Note
對(duì)于Map接口提供的putIfAbsent默認(rèn)方法而言,其與computeIfAbsent方法在功能上雖然類(lèi)似。但有一些不同的地方。首先,computeIfAbsent方法對(duì)于value的計(jì)算是延遲計(jì)算,即只有在key不存在 或 該key在Map中的value為null 時(shí),其才會(huì)計(jì)算value。而putIfAbsent無(wú)論最終是否需要設(shè)置該鍵值對(duì),都會(huì)去計(jì)算value。所以value的計(jì)算如果是一個(gè)昂貴的過(guò)程,推薦使用延遲計(jì)算特性的computeIfAbsent方法;其次,二者返回值不同。computeIfAbsent方法總是會(huì)返回該key在map中相應(yīng)的value值,而putIfAbsent方法,如果 key不存在 或 該key在Map中的value為null 時(shí),會(huì)返回null。否則返回該key在map中相應(yīng)的value值
參考文獻(xiàn)
Java實(shí)戰(zhàn)·第2版 拉烏爾-加布里埃爾·烏爾瑪、馬里奧·富斯科、艾倫·米克羅夫特著
