Java函数式编程(九):Comparator

实现Comparator接口

Comparator接口的身影在JDK库中随处可见,从查找到排序,再到反转操作,等等。Java 8里它变成了一个函数式接口,这样的好处就是我们可以使用流式语法来实现比较器了。

我们用几种不同的方式来实现一下Comparator,看看新式语法的价值所在。你的手指头会感谢你的,不用实现匿名内部类少敲了多少键盘啊。

使用Comparator进行排序

下面这个例子将使用不同的比较方法,来将一组人进行排序。我们先来创建一个Person的JavaBean。

代码如下:

public class Person {
private final String name;
private final int age;
public Person(final String theName, final int theAge) {
name = theName;
age = theAge;
}
public String getName() { return name; }
public int getAge() { return age; }
public int ageDifference(final Person other) {
return age - other.age;
}
public String toString() {
return String.format("%s - %d", name, age);
}
}

我们可以通过Person类来实现Comparator接口,不过这样我们只能使用一种比较方式。我们希望能比较不同的属性——比如名字,年龄,或者这些的组合。为了可以灵活的进行比较,我们可以使用Comparator,当我们需要进行比较的时候,再去生成相关的代码。

我们先来创建一个Person的列表,每个人都有不同的名字和年龄。

代码如下:

final List<Person> people = Arrays.asList(
new Person("John", 20),
new Person("Sara", 21),
new Person("Jane", 21),
new Person("Greg", 35));

我们可以通过人的名字或者年龄来对他们进行升序或者降序的排序。一般的方法就是使用匿名内部类来实现Comparator接口。这样写的话只有比较相关的代码是有意义的,其它的都只是走走形式而已。而使用lambda表达式则可以聚焦到比较的本质上来。

我们先按年龄从小到大对他们进行排序。

既然我们已经有了一个List对象,我们可以用它的sort()方法来进行排序。不过这个方法也有它的问题。这是一个void方法,也就是说当我们调用这个方法的时候,这个列表会发生改动。要保留原始列表的话,我们得先拷贝出一份来,然后再调用sort()方法。这简直太费劲了。这个时候我们得求助下Stream类了。

我们可以从List那获取一个Stream对象,然后调用它的sorted()方法。它会返回一个排好序的集合,而不是在原来的集合上做修改。使用这个方法的话可以很方便的配置Comparator的参数。

代码如下:

List<Person> ascendingAge =
people.stream()
.sorted((person1, person2) -> person1.ageDifference(person2))
.collect(toList());
printPeople("Sorted in ascending order by age: ", ascendingAge);

我们先通过stream()方法将列表转化成一个Stream对象。然后调用它的sorted()方法。这个方法接受一个Comparator参数。由于Comparator是一个函数式接口,我们可以传入一个lambda表达式。最后我们调用collect方法,让它把结果存储到一个列表里。collect方法是一个归约器,它能把迭代过程中的对象输出成某种特定的格式或者类型。toList()方法是Collectors类的一个静态方法。

Comparator的抽象方法compareTo()接收两个参数,也就是要比较的对象,并返回一个int类型的结果。为了兼容这个,我们的lambda表达式也接收两个参数,两个Person对象,它们的类型是由编译器自动推导的。我们返回一个int类型,表明比较的对象是否相等。

因为要按年龄进行排序,所以我们会比较两个对象的年龄,然后返回比较的结果。如果他们一样大,则返回0。否则如果第一个人更年轻的话就返回一个负数,更年长的话就返回正数。

sorted()方法会遍历目标集合的每个元素并调用指定的Comparator,来确定出元素的排序顺序。sorted()方法的执行方式有点类似前面说到的reduce()方法。reduce()方法把列表逐步归约出一个结果。而sorted()方法则通过比较的结果来进行排序。

一旦我们排好序后我们想要把结果打印出来,因此我们调用了一个printPeople()方法;下面来实现下这个方法。

代码如下:

public static void printPeople(
final String message, final List<Person> people) {
System.out.println(message);
people.forEach(System.out::println);
}

这个方法中,我们先打印了一个消息,然后遍历列表,打印出里面的每个元素。

我们来调用下sorted()方法看看,它会将列表中的人按年龄从小到大进行排列。

代码如下:

Sorted in ascending order by age:
John - 20
Sara - 21
Jane - 21
Greg - 35

我们再看一下sorted()方法,来做一个改进。

代码如下:

.sorted((person1, person2) -> person1.ageDifference(person2))

在传入的这个lambda表达式里,我们只是简单的路由了下这两个参数——第一个参数作为ageDifference()方法的调用目标,而第二个作为它的参数。但是我们可以不这么写,而是用一个office-space模式——也就是使用方法引用,让Java编译器去做路由。

这里用到的参数路由和前面看到的有点不同。我们之前看到的,要么参数是作为调用目标,要么是作为调用参数。而现在,我们有两个参数,我们希望能分成两个部分,一个是作为方法调用的目标,第二个则作为参数。别担心,Java编译器会告诉你,“这个我来搞定”。

我们可以把前面的sorted()方法里面的lambda表达式替换成一个短小精悍的ageDifference方法。

代码如下:

people.stream()
.sorted(Person::ageDifference)

这段代码非常简洁,这多亏了Java编译器提供的方法引用。编译器接收到两个person实例的参数,把第一个用作ageDifference()方法的调用目标,而第二个作为方法参数。我们让编译器去做这个工作,而不是自己直接去写代码。当使用这种方式的时候,我们必须确定第一个参数就是引用的方法的调用目标,而剩下那个就是方法的入参。

重用Comparator

我们很容易就将列表中的人按年龄从小到大排好序了,当然从大到小进行排序也很容易。我们来试一下。

代码如下:

printPeople("Sorted in descending order by age: ",
people.stream()
.sorted((person1, person2) -> person2.ageDifference(person1))
.collect(toList()));

我们调用了sorted()方法并传入一个lambda表达式,它正好能适配成Comparator接口,就像前面的例子那样。唯一不同的就是这个lambda表达式的实现——我们把要比较的人调了下顺序。结果应该是按他们的年龄由从大到小排列的。我们来看一下。

代码如下:

Sorted in descending order by age:
Greg - 35
Sara - 21
Jane - 21
John - 20

只是改一下比较的逻辑费不了太多劲。但我们没法把这个版本重构成方法引用的,因为参数的顺序不符合方法引用的参数路由的规则;第一个参数并不是用作方法的调用目标,而是作为方法参数。有一个方法能解决这个问题,同时它还能减少重复的工作。我们来看下如何实现。

前面我们已经创建了两个lambda表达式:一个是按年龄从小到大排序,一个是从大到小排序。这么做的话,会出现代码冗余和重复,并破坏了DRY原则。如果我们只是想要调整下排序顺序的话,JDK提供了一个reverse方法,它有一个特殊的方法修饰符,default。我们会在77页中的default方法来讨论它,这里我们先用下这个reversed()方法来去除冗余性。

代码如下:

Comparator<Person> compareAscending =
(person1, person2) -> person1.ageDifference(person2);
Comparator<Person> compareDescending = compareAscending.reversed();

我们先创建了一个Comparator,compareAscending,来将人按年龄从小到大进行排序。为了反转比较顺序,而不是再写一次这个代码,我们只需要调用一下这个第一个Comparator的reversed()方法就可以获取第二个Comparator对象。在reversed()方法底层,它创建了一个比较器,来交换了比较的参数的顺序。这说明reversed也是一个高阶方法——它创建并返回了一个无副作用的函数。我们把这个两个比较器用到代码里。

代码如下:

printPeople("Sorted in ascending order by age: ",
      people.stream()
    
    
.sorted(compareAscending)
    
    
.collect(toList())
);
printPeople("Sorted in descending order by age: ",
people.stream()
.sorted(compareDescending)
.collect(toList())
);

从代码中明显可以看到,Java8的这些新特性极大的减少了代码的冗余及复杂度,不过好处远不止这些,JDK里还有无限可能等着你去探索。

我们已经可以按年龄进行排序了,想按名字来排序的话也很简单。我们来按名字进行字典序排列,同样的,只需要改下lambda表达式里的逻辑就好了。

代码如下:

printPeople("Sorted in ascending order by name: ",
people.stream()
.sorted((person1, person2) ->
person1.getName().compareTo(person2.getName()))
.collect(toList()));

输出的结果里会按名字的字典序进行排列。

代码如下:

Sorted in ascending order by name:
Greg - 35
Jane - 21
John - 20
Sara - 21

现在为止,我们要么就按年龄排序,要么就按名字排序。我们可以让lambda表达式的逻辑更智能一些。比如我们可以同时按年龄和名字排序。

我们来选出列表中最年轻的人来。我们可以先按年龄从小到大排序然后选中结果中的第一个。不过其实用不着那样,Stream有一个min()方法可以实现这个。这个方法同样也接受一个Comparator,不过返回的是集合中最小的对象。我们来用下它。

代码如下:

people.stream()
.min(Person::ageDifference)
.ifPresent(youngest -> System.out.println("Youngest: " + youngest));

调用min()方法的时候我们用了ageDifference这个方法引用。min()方法返回的是一个Optinal对象,因为列表可能为空并且里面可能不止一个年纪最小的人。接着我们通过Optinal的ifPrsend()方法获取到年纪最小的那个人,并打印出他的详细信息。来看下输出结果。

代码如下:

Youngest: John - 20

输出年纪最大的同样也很简单。只要把这个方法引用传给一个max()方法就好了。

代码如下:

people.stream()
.max(Person::ageDifference)
.ifPresent(eldest -> System.out.println("Eldest: " + eldest));

我们来看下最年长那位的名字和年龄。

代码如下:

Eldest: Greg - 35

有了lambda表达式和方法引用之后,比较器的实现变得更简洁也更方便了。JDK也给Compararor类引入了不少便利的方法,使得我们可以更流畅的进行比较,下面我们将会看到。

多重比较和流式比较

我们来看下Comparator接口提供了哪些方便的新方法,并用它们来进行多个属性的比较。

我们还是继续使用上节中的那个例子。按名字排序的话,我们上面是这么写的:

代码如下:

people.stream()
.sorted((person1, person2) ->
person1.getName().compareTo(person2.getName()));

和上个世纪的内部类的写法比起来,这种写法简直太简洁了。不过如果用了Comparator类里面的一些函数能让它变得更简单,使用这些函数能够让我们更流畅的表述自己的目的。比如说,要按名字排序的话,我们可以这么写:

代码如下:

final Function<Person, String> byName = person -> person.getName();
people.stream()
.sorted(comparing(byName));

这段代码中我们导入了Comparator类的静态方法comparing()。comparing()方法使用传入的lambda表达式来生成一个Comparator对象。也就是说,它也是一个高阶函数,接受一个函数入参并返回另一个函数。除了能让语法变得更简洁外,这样的代码读起来也能更好的表述我们想要解决的实际问题。

有了它,进行多重比较的时候也能变得更加流畅。比如,下面这段按名字和年龄比较的代码就能说明一切:

代码如下:

final Function<Person, Integer> byAge = person -> person.getAge();
final Function<Person, String> byTheirName = person -> person.getName();
printPeople("Sorted in ascending order by age and name: ",
people.stream()
.sorted(comparing(byAge).thenComparing(byTheirName))
.collect(toList()));

我们先是创建了两个lambda表达式,一个返回指定人的年龄,一个返回的是他的名字。在调用sorted()方法的时候我们把这两个表达式组合 到了一起,这样就能进行多个属性的比较了。comparing()方法创建并返回了一个按年龄比较的Comparator ,我们再调用这个返回的Comparator上面的thenComparing()方法来创建一个组合的比较器,它会对年龄和名字两项进行比较。下面的输出是先按年龄再按名字进行排序后的结果。

代码如下:

Sorted in ascending order by age and name:
John - 20
Jane - 21
Sara - 21
Greg - 35

可以看到,使用lambda表达式和JDK提供的新的工具类,可以很容易的将Comparator的实现进行组合。下面我们来介绍下Collectors。

(0)

相关推荐

  • c# 实现IComparable、IComparer接口、Comparer类的详解

    在默认情况下,对象的Equals(object o)方法(基类Object提供),是比较两个对象变量是否引用同一对象.我们要必须我自己的对象,必须自己定义对象比较方式.IComparable和ICompare 接口是.net framework 中比较对象的标准方式,这两个接口之间的区别如下:1. IComparable 在要比较的对象的类中实现,可以比较该对象和另一个对象.2.IComparer 在一个单独的类中实现,可以比较任意两个对象.一般情况下,我们使用 IComparable 给出类的

  • 浅析Java中comparator接口与Comparable接口的区别

    Comparable 简介 Comparable 是排序接口. 若一个类实现了Comparable接口,就意味着"该类支持排序".  即然实现Comparable接口的类支持排序,假设现在存在"实现Comparable接口的类的对象的List列表(或数组)",则该List列表(或数组)可以通过 Collections.sort(或 Arrays.sort)进行排序. 此外,"实现Comparable接口的类的对象"可以用作"有序映射(如

  • 对比Java中的Comparable排序接口和Comparator比较器接口

    Comparable Comparable 是排序接口. 若一个类实现了Comparable接口,就意味着"该类支持排序". 即然实现Comparable接口的类支持排序,假设现在存在"实现Comparable接口的类的对象的List列表(或数组)",则该List列表(或数组)可以通过 Collections.sort(或 Arrays.sort)进行排序. 此外,"实现Comparable接口的类的对象"可以用作"有序映射(如Tree

  • java比较器comparator使用示例分享

    复制代码 代码如下: import java.util.ArrayList;import java.util.Collections;import java.util.Comparator;import java.util.List; public class ComparatorTest implements Comparator<stuEntity> { /**     * @param args     */    public static void main(String[] arg

  • java比较器Comparable接口与Comaprator接口的深入分析

    java的比较器有两类,分别是Comparable接口和Comparator接口.在为对象数组进行排序时,比较器的作用非常明显,首先来讲解Comparable接口.让需要进行排序的对象实现Comparable接口,重写其中的compareTo(T o)方法,在其中定义排序规则,那么就可以直接调用java.util.Arrays.sort()来排序对象数组,实例如下: 复制代码 代码如下: class Student implements Comparable<Student>{    priv

  • Java中实现Comparable和Comparator对象比较

    当需要排序的集合或数组不是单纯的数字型时,通常可以使用Comparator或Comparable,以简单的方式实现对象排序或自定义排序. A comparison function, which imposes a total ordering on some collection of objects. Comparators can be passed to a sort method (such as Collections.sort or Arrays.sort) to allow pr

  • Java 中Comparable和Comparator区别比较

    Comparable 简介Comparable 是排序接口.若一个类实现了Comparable接口,就意味着"该类支持排序".  即然实现Comparable接口的类支持排序,假设现在存在"实现Comparable接口的类的对象的List列表(或数组)",则该List列表(或数组)可以通过 Collections.sort(或 Arrays.sort)进行排序.此外,"实现Comparable接口的类的对象"可以用作"有序映射(如Tre

  • Java中实现Comparator接口和用法实例(简明易懂)

    在java中,如果要对集合对象或数组对象进行排序,需要实现Comparator接口以达到我们想要的目标. 接下来我们模拟下在集合对象中对日期属性进行排序 一.实体类Step package com.ljq.entity; /** * 运号单流程 * * @author Administrator * */ public class Step{ /** 处理时间 */ private String acceptTime = ""; /** 快件所在地点 */ private String

  • Java函数式编程(九):Comparator

    实现Comparator接口 Comparator接口的身影在JDK库中随处可见,从查找到排序,再到反转操作,等等.Java 8里它变成了一个函数式接口,这样的好处就是我们可以使用流式语法来实现比较器了. 我们用几种不同的方式来实现一下Comparator,看看新式语法的价值所在.你的手指头会感谢你的,不用实现匿名内部类少敲了多少键盘啊. 使用Comparator进行排序 下面这个例子将使用不同的比较方法,来将一组人进行排序.我们先来创建一个Person的JavaBean. 复制代码 代码如下:

  • 详解JAVA 函数式编程

    1.函数式接口 1.1概念: java中有且只有一个抽象方法的接口. 1.2格式: 修饰符 interface 接口名称 { public abstract 返回值类型 方法名称(可选参数信息); // 其他非抽象方法内容 } //或者 public interface MyFunctionalInterface { void myMethod(); } 1.3@FunctionalInterface注解: 与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解

  • Java 函数式编程梳理

    目录 一.Lambda表达式 1.1 函数式编程思想概述 1.2 体验Lambda表达式 1.3 Lambda表达式的标准格式 1.4 Lambda表达式的练习 1.5 Lambda表达式的省略模式 1.6 Lambda表达式的注意事项 1.7 Lambda表达式和匿名内部类的区别 二.接口组成更新 1.1 接口组成更新概述 1.2 接口中默认方法 1.3 接口中静态方法 1.4 接口中私有方法 三.方法引用 1.1 体验方法引用 1.2 方法引用符 1.3 Lambda表达式支持的方法引用 1

  • 详解Java函数式编程和lambda表达式

    为什么要使用函数式编程 函数式编程更多时候是一种编程的思维方式,是种方法论.函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做.说白了,函数式编程是基于某种语法或调用API去进行编程.例如,我们现在需要从一组数字中,找出最小的那个数字,若使用用命令式编程实现这个需求的话,那么所编写的代码如下: public static void main(String[] args) { int[] nums = new int[]{1, 2, 3, 4, 5,

  • Java 函数式编程要点总结

    目录 一.函数式概念 二.函数与方法 三.JDK函数基础 1.Lambda表达式 2.函数式接口 四.Optional类 1.Null判断 2.Optional应用 五.Stream流 六.源代码地址 一.函数式概念 函数式编程是一种结构化编程的范式,主要思想是把运算过程尽量写成系列嵌套的函数调用.函数编程的概念表述带有很抽象的感觉,可以基于案例看: public class Function01 {     public static void main(String[] args) {   

  • Java函数式编程(八):字符串及方法引用

    第三章 字符串,比较器和过滤器 JDK引入的一些方法对写出函数式风格的代码很有帮助.JDK库里的一些的类和接口我们已经用得非常熟悉了,比如说String,为了摆脱以前习惯的那种老的风格,我们得主动寻找机会来使用这些新的方法.同样,当我们需要用到只有一个方法的匿名内部类时,我们现在可以用lambda表达式来替换它了,不用再像原来那样写的那么繁琐了. 本章我们会使用lambda表达式和方法引用来遍历字符串,实现Comparator接口,查看目录中的文件,监视文件及目录的变更.上一章中介绍的一些方法还

  • Java函数式编程(一):你好,Lambda表达式

    第一章 你好,lambda表达式! 第一节 Java的编码风格正面临着翻天覆地的变化. 我们每天的工作将会变成更简单方便,更富表现力.Java这种新的编程方式早在数十年前就已经出现在别的编程语言里面了.这些新特性引入Java后,我们可以写出更简洁,优雅,表达性更强,错误更少的代码.我们可以用更少的代码来实现各种策略和设计模式. 在本书中我们将通过日常编程中的一些例子来探索函数式风格的编程.在使用这种全新的优雅的方式进行设计编码之前,我们先来看下它到底好在哪里. 改变了你的思考方式 命令式风格--

  • Java函数式编程(三):列表的转化

    列表的转化 将集合转化成一个新的集合就和遍历它一样简单.假设我们要将列表中的名字转化成全大写的.我们看下都有哪些实现方式. Java中的字符串是不可变的,所以它没法改变.我们可以生成新的字符串,用来替换列表中原有的元素.然而这样做的话,原来列表就没了;还有一个问题,原来的列表可能也是不可变的,比如Arrays.asList()生成的,所以修改原来的列表这招不行.还有一个缺点就是这样做很难并行操作. 生成一个新的全大写的列表是个不错的选择. 乍听起来这个建议弱爆了;性能是我们都很关注的一个问题.令

  • Java函数式编程(七):MapReduce

    译注:map(映射)和reduce(归约,化简)是数学上两个很基础的概念,它们很早就出现在各类的函数编程语言里了,直到2003年Google将其发扬光大,运用到分布式系统中进行并行计算后,这个组合的名字才开始在计算机界大放异彩(那些函数式粉可能并不这么认为).本文我们会看到Java 8在摇身一变支持函数式编程后,map和reduce组合的首次亮相(这里只是初步介绍,后续还会有针对它们的专题). 对集合进行归约 现在为止我们已经介绍了几个操作集合的新技巧了:查找匹配元素,查找单个元素,集合转化.这

  • Java函数式编程(二):集合的使用

    第二章:集合的使用 我们经常会用到各种集合,数字的,字符串的还有对象的.它们无处不在,哪怕操作集合的代码要能稍微优化一点,都能让代码清晰很多.在这章中,我们探索下如何使用lambda表达式来操作集合.我们用它来遍历集合,把集合转化成新的集合,从集合中删除元素,把集合进行合并. 遍历列表 遍历列表是最基本的一个集合操作,这么多年来,它的操作也发生了一些变化.我们使用一个遍历名字的小例子,从最古老的版本介绍到现在最优雅的版本. 用下面的代码我们很容易创建一个不可变的名字的列表: 复制代码 代码如下:

随机推荐