《Java常见问题解法》第四章 比较器与收集器

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)
**/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值