Java常见问题的简单解法
提示:总结于LeetCode书籍《Java常见问题的简单解法》,全书其实就是用Stream去通过函数式编程,更加简洁,快速,高效的解决实际问题。
文章目录
第四章 比较器与收集器
一、利用比较器实现排序
Stream.sorted方法可以生成一个新的排序流,从而完成排序。先举一个对字符串的排序:
private List<String> sampleStrings =
Arrays.asList("this", "is", "a", "list", "of", "strings");
//这是Java7及以前的版本,这种排序时破坏性的,会修改所提供的的集合即Collections.sort方法不符合Java8所倡导的将不可变性置于首要位置的函数式编程原则。
public List<String> defaultSort() {
Collections.sort(sampleStrings);
return sampleStrings;
}
//这是Java8及以后的版本
public List<String> defaultSortUsingStreams() {
return sampleStrings.stream()
.sorted()
.collect(Collectors.toList());
}
//根据长度对字符串排序,使用lambda:
public List<String> lengthSortUsingSorted() {
return sampleStrings.stream()
.sorted((s1, s2) -> s1.length() - s2.length())
.collect(toList());
}
//使用comparator.comparingInt方法
public List<String> lengthSortUsingComparator() {
return sampleStrings.stream()
.sorted(Comparator.comparingInt(String::length))
.collect(toList());
}
在实际情况中,也会遇到按多个条件进行排序,如:根据长度对字符串排序,长度相同则按字母顺序排序
public List<String> lengthSortThenAlphaSort() {
return sampleStrings.stream()
.sorted(comparing(String::length) ➊
.thenComparing(naturalOrder()))
.collect(toList());
}
再举一个开发中可能遇到的类似情况
//描述高尔夫球手的 Golfer 类
public class Golfer {
private String first;
private String last;
private int score;
// 其他方法
}
//对高尔夫球手排序
private List<Golfer> golfers = Arrays.asList(
new Golfer("Jack", "Nicklaus", 68),
new Golfer("Tiger", "Woods", 70),
new Golfer("Tom", "Watson", 70),
new Golfer("Ty", "Webb", 68),
new Golfer("Bubba", "Watson", 70)
);
public List<Golfer> sortByScoreThenLastThenFirst() {
return golfers.stream()
.sorted(comparingInt(Golfer::getScore)
.thenComparing(Golfer::getLast)
.thenComparing(Golfer::getFirst))
.collect(toList());
}
//输出结果如下:
//Golfer{first='Jack', last='Nicklaus', score=68}
//Golfer{first='Ty', last='Webb', score=68}
//Golfer{first='Bubba', last='Watson', score=70}
//Golfer{first='Tom', last='Watson', score=70}
//Golfer{first='Tiger', last='Woods', score=70}
上述栗子在可以正常排序的前提是需要拍段排序的字段值不能出现Null的情况,一旦出现Null则会报空指针。而且有时候排序会有更加复杂的情况,就需要我们自定义比价器了,如何自定义呢,列举一个我在开发中遇到的实际问题的部分结局代码:需求是给数据设置排名,其中有议价和未税单价俩个字段来判断,规则是没有议价价格就按未税单价比较大小,有议价价格则以议价价格来比较大小,而他们的值都有可能为Null,部分代码如下:
bidDetails = bidDetails.stream().sorted((o1, o2) -> {
BigDecimal bd1 = new BigDecimal("0");
BigDecimal bd2 = new BigDecimal("0");
if (o1.getTaxedQuotation()!= null){
bd1 = o1.getTaxedQuotation();
}
if (o2.getTaxedQuotation()!= null){
bd2 = o2.getTaxedQuotation();
}
//比较大小,没有议价价格就按未税单价比较大小,有议价价格则以议价价格来比较大小
if (o1.getBargainingSupplier() != null) bd1 = new BigDecimal(o1.getBargainingSupplier().toString());
if (o2.getBargainingSupplier() != null) bd2 = new BigDecimal(o2.getBargainingSupplier().toString());
if (bd1.compareTo(bd2)>0){
return 1;
}else {
return -1;
}
}).collect(Collectors.toList());
二、将流转换为集合
Java 8一般通过称为流水线的中间操作来传递流元素,并在达到终止操作后结束。Stream接口定义的collect方法就是一种终止操作,用于将流转换为集合。
collect方法有俩种重载形式:
<R,A> R collect(Collector<? super T,A,R> collector)
<R> R collect(Supplier<R> supplier,
BiConsumer<R,? super T> accumulator,
BiConsumer<R,R> combiner)
接下来列举些创建的各种数据类型的栗子:
//创建List
List<String> superHeroes =
Stream.of("Mr. Furious", "The Blue Raja", "The Shoveler",
"The Bowler", "Invisible Boy", "The Spleen", "The Sphinx")
.collect(Collectors.toList());
//创建Set
Set<String> villains =
Stream.of("Casanova Frankenstein", "The Disco Boys",
"The Not-So-Goodie Mob", "The Suits", "The Suzies",
"The Furriers", "The Furriers")
.collect(Collectors.toSet());
//创建LinkedList
List<String> actors =
Stream.of("Hank Azaria", "Janeane Garofalo", "William H. Macy",
"Paul Reubens", "Ben Stiller", "Kel Mitchell", "Wes Studi")
.collect(Collectors.toCollection(LinkedList::new));
}
//创建Array
String[] wannabes =
Stream.of("The Waffler", "Reverse Psychologist", "PMS Avenger")
.toArray(String[]::new);
//创建Map
Set<Actor> actors = mysteryMen.getActors();
Map<String, String> actorMap = actors.stream()
.collect(Collectors.toMap(Actor::getName, Actor::getRole)); ➊
actorMap.forEach((key,value) ->
System.out.printf("%s played %s%n", key, value));
三、将线性集合添加到映射
很多情况下我们那都一个实例List,我们需要把它转化为Map,key为主键,value为实例对象。
我们来举个栗子:
//Book 类
public class Book {
private int id;
private String name;
private double price;
// 其他方法
}
//图书集合
List<Book> books = Arrays.asList(
new Book(1, "Modern Java Recipes", 49.99),
new Book(2, "Java 8 in Action", 49.99),
new Book(3, "Java SE8 for the Really Impatient", 39.99),
new Book(4, "Functional Programming in Java", 27.64),
new Book(5, "Making Java Groovy", 45.99)
new Book(6, "Gradle Recipes for Android", 23.76)
);
//将图书添加到 Map
Map<Integer, Book> bookMap = books.stream()
.collect(Collectors.toMap(Book::getId, b -> b));
bookMap = books.stream()
.collect(Collectors.toMap(Book::getId, Function.identity()));
四、partitioningBy和groupingBy
partitioningBy的方法包括两种形式:
static <T> Collector<T,?,Map<Boolean,List<T>>> partitioningBy(
Predicate<? super T> predicate)
static <T,D,A> Collector<T,?,Map<Boolean,D>> partitioningBy(
Predicate<? super T> predicate, Collector<? super T,A,D> downstream)
第一种PartitioningBy方法传入单个Predicate作为参数,它将元素分为满足Predicate与不满足Predicate的两类。所以我们总能得到一个包含俩个条目的Map,其中一个值列表满足Predicate,另一个则不满足Predicate
通过PartitioningBy方法将这些字符串按偶数长度和奇数长度进行划分,栗子如下:
List<String> strings = Arrays.asList("this", "is", "a", "long", "list", "of",
"strings", "to", "use", "as", "a", "demo");
Map<Boolean, List<String>> lengthMap = strings.stream()
.collect(Collectors.partitioningBy(s -> s.length() % 2 == 0));
lengthMap.forEach((key,value) -> System.out.printf("%5s: %s%n", key, value));
//
// false: [a, strings, use, a]
// true: [this, is, long, list, of, to, as, demo]
第二种,多了个Collector<? super T,A,D> 类型的参数,他其实是一种归约运算,它将输入元素积累到一个可变结果容器中,在处理完所有输入之后,可以有选择地将积累的结果转换为最终表示形态。还原操作可以顺序或并行执行。
T:归约运算的输入元素的类型
A:归约运算的可变累积类型
R:归约运算的结果类型
Map<布尔值,列表>:包含输入的映射。键是布尔值,相应的值是包含T类型元素的列表。
说再多,不如一个栗子来的实在:
Stream<Integer> s = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Map<Boolean, Long> map = s.collect(Collectors.partitioningBy(
num -> (num > 3), Collectors.counting()));
System.out.println(map);
//输出:{false=3, true=7}
groupingBy 方法的签名如下:
static <T,K> Collector<T,?,Map<K,List<T>>> groupingBy(
Function<? super T,? extends K> classifier)
static <T,K,A,D> Collector<T,?,Map<K,D>> groupingBy(
Function<? super T,? extends K> classifier,
Collector<? super T,A,D> downstream)
groupingBy比较简单列举一个对字符长度分组的栗子:
第一种签名栗子:
List<String> strings = Arrays.asList("this", "is", "a", "long", "list", "of",
"strings", "to", "use", "as", "a", "demo");
Map<Integer, List<String>> lengthMap = strings.stream()
.collect(Collectors.groupingBy(String::length)); ➊
lengthMap.forEach((k,v) -> System.out.printf("%d: %s%n", k, v));
//
// 1: [a, a]
// 2: [is, of, to, as]
// 3: [use]
// 4: [this, long, list, demo]
// 7: [strings]
第二种签名栗子:
List<String> strings = Arrays.asList("this", "is", "a", "long", "list", "of",
"strings", "to", "use", "as", "a", "demo");
Map<Integer, Long> lengthMap = strings.stream()
.collect(Collectors.groupingBy(String::length, Collectors.counting()));
lengthMap.forEach((k,v) -> System.out.printf("%d: %s%n", k, v));```
<hr style=" border:solid; width:100px; height:1px;" color=#000000 size=1">
//输出: 1: 2
// 2: 4
// 3: 1
// 4: 4
// 7: 1
五、BinaryOperator
BinaryOperator 是java.util.function包定义的一种函数式接口,它继承自BiFunction接口,适合在函数和返回值的参数属于同一个类时使用。
BinaryOperator 接口包括两种静态方法:
static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator)
static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator)
两种方法根据所提供的 Comparator,返回一个 BinaryOperator。我们以一个 Employee POJO 为例,讨论如何获取流的最大值:
public class Employee {
private String name;
private Integer salary;
private String department;
// 其他方法
}
List<Employee> employees = Arrays.asList(
new Employee("Cersei", 250_000, "Lannister"),
new Employee("Jamie", 150_000, "Lannister"),
new Employee("Tyrion", 1_000, "Lannister"),
new Employee("Tywin", 1_000_000, "Lannister"),
new Employee("Jon Snow", 75_000, "Stark"),
new Employee("Robb", 120_000, "Stark"),
new Employee("Eddard", 125_000, "Stark"),
new Employee("Sansa", 0, "Stark"),
new Employee("Arya", 1_000, "Stark"));
Employee defaultEmployee =
new Employee("A man (or woman) has no name", 0, "Black and White");
BinaryOperator.maxBy
Optional<Employee> optionalEmp = employees.stream()
.reduce(BinaryOperator.maxBy(Comparator.comparingInt(Employee::getSalary)));
System.out.println("Emp with max salary: " +
optionalEmp.orElse(defaultEmployee));
请注意,reduce 方法需要传入 BinaryOperator 作为参数。静态方法 maxBy 根据所提供的 Comparator 生成该 BinaryOperator,并按工资高低对员工进行比较。上述方案是可行的,不过采用 Stream.max 方法其实更简单,该方法可以直接应用于流:
签名:
Optional<T> max(Comparator<? super T> comparator);
栗子:
optionalEmp = employees.stream()
.max(Comparator.comparingInt(Employee::getSalary));
类似地,Collectors 工具类也定义了一种称为 maxBy 的静态方法,可以直接用于查找最高工资:
optionalEmp = employees.stream()
.collect(Collectors.maxBy(Comparator.comparingInt(Employee::getSalary)));
但是,Collectors.maxBy 方法不便处理,最好采用 Stream.max 方法作为替代.。Collectors.maxBy 方法在用作下游收集器(即对分组或分区操作进行后期处理)时相当有用。如下栗子Collectors.groupingBy 方法创建了一个部门到员工列表的映射,然后计算每个部门中工资最高的员工。
Map<String, Optional<Employee>> map = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))));
map.forEach((house, emp) -> System.out.println(house + ": " + emp.orElse(defaultEmployee)));
/**
输出结果:
Lannister: Tests.Employee(name=Tywin, salary=1000000, department=Lannister)
Stark: Tests.Employee(name=Eddard, salary=125000, department=Stark)
**/