深入探究java中的 xxxable和xxxator

前言

相信有一定工作经验的朋友,都见过或者用过xxxable和xxxator ,比如常见的Comparable和Comparator, 还有还有常见并且容易迷糊的Iterable和Iterator, 看这名字,前两个是和比较相关的, 后两个是和迭代相关. 但是命名如此相似的接口, 又有何区别呢?各自的用途又是什么呢? 今天阿亮带大家一起揭开这神秘的面纱.

揭秘

首先我们要明确的是,xxxable和xxxator都是接口, 都是用来描述其子类具有某种能力的. 因为在java中,接口就是用来定义能力的. 接口中的方法,就代表着其实现类有什么能力. 因为实现类必须要实现接口中的所有方法.

从名称上感受

首先,直观的从名称上直观的感受,复习一下英文

–able: 表形容词, 可…的,能…

able单词
所以,今天我们要研究的其中两个主角,

  • Comparable: 可比较的
  • Iterable: 可迭代的

-ator: 表名词,通常由ate结尾的动词而来, 做事的人或物
ator结尾单词
所以,我们今天要研究的另外两个主角

  • Comparator: 比较器,
  • Iterator: 迭代器,

一番咬文嚼字, 从字面上理解:

xxxable 就是具有xxx能力,是形容词,带入java接口,就这么认为: 实现了xxxable接口,代表着具有了xxx能力,可以进行xxx. 着重描述的是: 实现类具有xxx能力.

xxxator 就是xxx器,是名词, 带入java中的接口,就可以这么认为: 实现了xxxator接口,就代表着可以干xx. 着重描述的是: 实现类可以对某个对象进行xxx.

看到这儿,可能还是有点模糊,不用着急,我们从代码,亲自来感受一下,感受完了再回过头来看这段.

从代码带入

上面我们从名称上理解了,下面我们从实操代码入手,直观感受.

Comparable接口

位于java.lang 包下,只有一个抽象方法

public int compareTo(T o);

实现这个方法,必须实现compareTo这个方法,实现类就具有可比较的能力了.比较肯定要区分大小,那这个方法要怎么区分大小呢? 方法的doc注释上解释了

a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object.

大意就是 返回一个负整数,零或正整数,表示此对象小于,等于或大于指定的对象。

现在我们假设一个场景,我们有一个<书本>对象,有一个序号属性,通过序号的大小,来对书进行比较,一遍排序整理.指定的规则是: 编号小的更大,编号大的更小,

书籍类的声明如下:

public class Book implements Comparable<Book>{

    private int order;
    private String name;

    public Book(int order, String name) {
        this.order = order;
        this.name = name;
    }

    public int getOrder() {
        return order;
    }

    @Override
    public int compareTo(Book o) {
        //  相等
        if (this == o || this.getOrder() == o.getOrder()) return 0;
        // 如果 此类的order大于被比较的order,返回-1,表示此书"更小"
        if (this.order > o.getOrder()) {
            return -1;
        }
        return 1;
    }

    @Override
    public String toString() {
        return "Book{" +
                "order=" + order +
                ", name='" + name + '\'' +
                '}';
    }

怎么使用呢, 最简单的是比较两本书

        // 比较两本书
        Book songs = new Book(1, "诗经");
        Book threeKingdoms = new Book(2, "三国演义");

        int i = songs.compareTo(threeKingdoms);
        // 返回1, 正数, 代表 诗经 "大于" 三国演义
        System.out.println(i);

感觉这样好像没有什么用. 下面展示一个比较可能会在开发中用到的: 对书籍数组进行排序

        // 对书进行排序
        Book[] books = new Book[3];
        Book songs = new Book(1, "诗经");
        Book threeKingdoms = new Book(2, "三国演义");
        Book soulLand = new Book(3, "斗罗大陆");
        books[0] = soulLand;
        books[1] = songs;
        books[2] = threeKingdoms;
        // 排序之前
        System.out.println(Arrays.toString(books));
        // 利用Arrays.sort进行排序 (这个方法是升序)
        Arrays.sort(books);
        // 排序之后
        System.out.println(Arrays.toString(books));

输出结果为:

[Book{order=3, name='斗罗大陆'}, Book{order=1, name='诗经'}, Book{order=2, name='三国演义'}]
[Book{order=3, name='斗罗大陆'}, Book{order=2, name='三国演义'}, Book{order=1, name='诗经'}]

因为我们定义是: 编号越大,书籍 “越小”,所以这个结果是没问题. Arrays.sort方法就是利用Comparable,进行比较然后排序的

如果Book这个类不实现java.lang.Comparable,然后调用Arrays.sort(books);猜猜会怎样?

同理, java集合Stream流中的sorted()方法,也是同样的道理. 此处不展开, 感兴趣的朋友请移步: java stream使用指南-------sorted使用及进阶

另外,我们知道 java.util.TreeSet 这个集合,添加进去的元素自动就排序好了, 比如我 new TreeSet<Integer> ,然后往里面添加几个数字,打印出来就是有序的, 或者new TreeSet<String>.

    @Test
    public void test4(){
        // 数字TreeSet
        TreeSet<Integer> integers = new TreeSet<>();
        integers.add(20);
        integers.add(11);
        integers.add(34);
        integers.add(49);
        System.out.println(integers);

        // 字符串TreeSet
        TreeSet<String> strings = new TreeSet<>();
        strings.add("B");
        strings.add("E");
        strings.add("G");
        strings.add("A");
        System.out.println(strings);
    }

输出:

[11, 20, 34, 49]
[A, B, E, G]

我添加顺序是随意的, 最终打印出来的结果是有序的, 大家有没有想过这个排序规则是怎么定义的能?在哪里定义的呢? 相信大家已经猜到了, 其实就是Integer String已经实现了java.lang.Comparable接口, TreeSet才知道他们的比较规则,然后由此来进行排序. 如果你往TreeSet中添加一个没有实现Comparable接口的元素, 看看会出现什么情况.

当然,TreeSet还有另一种指定规则的方式,我们下面讨论java.util.Comparator的时候再说

Comparator 接口

说完了Comparable,然后我们来说说Comparator

Comparator 位于java.util包下, 可以翻译为比较器, 是一个函数式接口,其中只有一个抽象方法

 int compare(T o1, T o2);

实现这个方法之后, 就可以对两个对象进行比较了. 因为Comparator是比较器,是工具, 所以可以用这个工具来对两个对象进行比较

比较的规则也是类似, 如果compare方法返回了 了一个正数 0 负数 ,则说明 o1 大于 等于 小于 o2

上代码 ,还是以上面的书籍为例, 但是不再实现Comparable接口

书籍类

public class Book {

    private int order;
    private String name;

    public Book(int order, String name) {
        this.order = order;
        this.name = name;
    }

    public int getOrder() {
        return order;
    }


    @Override
    public String toString() {
        return "Book{" +
                "order=" + order +
                ", name='" + name + '\'' +
                '}';
    }

}

然后我们再为书籍建一个比较器, 书籍比较器

public class BookComparator implements Comparator<Book> {   
    
    @Override
    public int compare(Book o1, Book o2) {
        if (o1 == o2 || o1.getOrder() == o2.getOrder()) return 0;
        
        if (o1.getOrder() > o2.getOrder()) {
            return -1;
        }
        return 1;
    }
    
}

我们还是先比较两本书,但是此时Book已经没有实现Comparable接口, 没有compareTo方法,所以不能直接比较,需要使用 书籍比较器 进行比较

        // 比较两本书
        Book songs = new Book(1, "诗经");
        Book threeKingdoms = new Book(2, "三国演义");

        // 使用书籍比较器进行比较
        BookComparator bookComparator = new BookComparator();
        int i = bookComparator.compare(songs, threeKingdoms);
        // 返回1, 正数, 代表 诗经 "大于" 三国演义
        System.out.println(i);

再来排序,

        // 对书进行排序
        Book[] books = new Book[3];
        Book songs = new Book(1, "诗经");
        Book threeKingdoms = new Book(2, "三国演义");
        Book soulLand = new Book(3, "斗罗大陆");
        books[0] = soulLand;
        books[1] = songs;
        books[2] = threeKingdoms;
        // 排序之前
        System.out.println(Arrays.toString(books));
        // 利用Arrays.sort进行排序,因为此时Book没有实现Comparable接口,直接使用下面的方法是没办法进行排序的
        // Arrays.sort(books);
        // 利用 书籍比较器进行排序
        BookComparator bookComparator = new BookComparator();
        Arrays.sort(books,bookComparator);
        // 排序之后
        System.out.println(Arrays.toString(books));

结果:

[Book{order=3, name='斗罗大陆'}, Book{order=1, name='诗经'}, Book{order=2, name='三国演义'}]
[Book{order=3, name='斗罗大陆'}, Book{order=2, name='三国演义'}, Book{order=1, name='诗经'}]

比较器毕竟是工具, 所以工具还提供了一些额外的方法比如reversed

上面的排序, 我看着不太舒服, 我想倒序一下, 当然比较器已经固定了,就不能再改了

在排序时,可以这样

Arrays.sort(books,bookComparator.reversed());

排序后的结果就是:

[Book{order=1, name='诗经'}, Book{order=2, name='三国演义'}, Book{order=3, name='斗罗大陆'}]

同理, java集合Stream流中除了sorted()方法,还提供了一个重载方法 sorted(Comparator<? super T> comparator) 也是同样的道理.传入一个比较器进行比较, 此处不展开, 感兴趣的朋友请移步: java stream使用指南-------sorted使用及进阶

上面在讲TreeSet时,我们还留了一个悬念, TreeSet 可以通过另外一种方式指定排序规则, 那肯定是用 比较器来指定规则啦

首先我们使用前面的方式(注意:此时Book未实现Comparable接口)

        // 我们先来试试之前我用的方法
        TreeSet<Book> books = new TreeSet<>();
        books.add(new Book(1, "诗经"));
        books.add(new Book(3, "斗罗大陆"));
        books.add(new Book(2, "三国演义"));
        System.out.println(books);

肯定会出问题的,因为此时TreeSet已经不知道用什么规则对添加进来的元素排序了,此时应该使用另一种方式

        // new 书籍比较器
        BookComparator bookComparator = new BookComparator();
        // 构造TreeSet时将比较器传入
        TreeSet<Book> books = new TreeSet<>(bookComparator);
        books.add(new Book(1, "诗经"));
        books.add(new Book(3, "斗罗大陆"));
        books.add(new Book(2, "三国演义"));
        System.out.println(books);

结果也是一样正确的 被排序好的.

这里多提一嘴, 因为Comparator是个函数式接口, 更多时候我们使用的匿名内部类或者lambda来实现一个构造器,而不是单独的创建一个书籍比较器, 除非这个比较器需要在不止一个地方使用,此处不展开描述.

好了到此为此, 我们已经搞清楚了Comparable和Comparator的用法

总结

我们已经知道Comparable和Comparator是怎么使用的了,回头再去看 最初的字面描述,肯定有更深的理解.

我现在做个总结: xxxable和xxxator 都是为做xxx事情而存在
xxxable是个形容词, 着重描述的是: 其实现类本身就具有xxx能力,
xxxator是个名词,意思是其实现类本身是xxx器, 一些不具备xxx能力的类可以借助xxx器有用xxx能力

面向对象, 也是对现实世界的抽象, 我来举个例子,以飞行(fly)举例,
Flyable: 可飞行的
Flyator: 飞行器

一般的鸟类(Bird)都应该实现Flyable接口,因为鸟类本身就具有飞行的能力

人类(Human)不能实现Flyable接口,因为人类不具有飞行的能力, 但是应该存在 人类飞行器(HumanFlyator)这个类,这个类实现Flyator接口, 人类本身不具有飞行的能力,但是人类可以借助飞行器飞行.

还有Iterable和Iterator,也是一样的,但是和 <比较> 不同的是, Iterable的迭代能力是借助Iterator完成的, 此处不在展开描述,感兴趣的朋友可以自行研究.

不得不说,人家设计java语言的人,也是对现实世界有充分认识的,这些命名是如此的深刻,内涵. 我们在自己的工作中,也可以参考,进行规范的接口设计

深入探究java中的 xxxable和xxxator_java 以able结尾的接口-CSDN博客

java stream使用指南-------sorted使用及进阶_java stream sorted-CSDN博客

引入

用了一段时间的jdk8的新特性,lambda表达式、方法引用、stream流,用起来是真的顺手啊,最近碰到了一个排序的问题,引发了一些思考,然后就写了这篇博客,归纳总结sorted的用法,在做笔记的同时也让自己有更深的理解。

数据准备

1. 依赖

我喜欢用google的集合工具类,让我构造测试数据更简便。然后也用lombok,依赖:

        <!--google集合工具类-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>30.0-jre</version>
        </dependency>

			<!--lombok,需要插件配合使用-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>

2. 相关类

User类

/*
*链式调用,我也有写相关博客,不过不是介绍基础用法的,是这个链式调用一个不太完美的地方,
*感兴趣的可以去看一看:https://blog.csdn.net/ql_7256/article/details/120274432 
*/
@Data
@Accessors(chain = true) 
public class User  {

    private String username;
    private String password;
    private Integer age;
    private Integer height;

    private Address address;

    private Map others;

}

Address类,注意:这个类在后续的测试中要改动

@Data
@Accessors(chain = true)
public class Address {

    private String province;
    private String city;
    private String county;
    
}

数据准备,


    private List<User> users = new ArrayList<User>() {{
        add(new User().setUsername("张三").setPassword("123456").setAge(20).setHeight(170)
                .setAddress(new Address().setProvince("四川省").setCity("成都市").setCounty("武侯区"))
                .setOthers(ImmutableMap.builder().put("sorted","AAA").put("bbb","BBB").put("ccc","CCC").build()));

        add(new User().setUsername("李四").setPassword("123456").setAge(16).setHeight(175)
                .setAddress(new Address().setProvince("四川省").setCity("成都市").setCounty("锦江区"))
                .setOthers(ImmutableMap.builder().put("sorted","DDD").put("eee","EEE").put("fff","FFF").build()));

        add(new User().setUsername("王五").setPassword("123456").setAge(20).setHeight(180)
                .setAddress(new Address().setProvince("四川省").setCity("成都市").setCounty("青羊区"))
                .setOthers(ImmutableMap.builder().put("sorted","GGG").put("hhh","HHH").put("iii","III").build()));

        add(new User().setUsername("赵六").setPassword("123456").setAge(17).setHeight(168)
                .setAddress(new Address().setProvince("四川省").setCity("成都市").setCounty("高新区"))
                .setOthers(ImmutableMap.builder().put("sorted","JJJ").put("kkk","KKK").put("lll","LLL").build()));
    }};


    private List<String> strings = new ArrayList<String>() {{
        add("222");add("666");add("444");add("111");add("333");add("555");
    }};

    private List<Integer> integers = new ArrayList<Integer>() {{
        add(222);add(555);add(666);;add(333);add(444);add(111);
    }};

    private List others = new ArrayList() {{
        add(444);add(555);add(666);add(111);add(222);add(333);
    }};

初体验

stream流、方法引用、lambda那些前置知识咱们就不说了哈,直接上手,先体直观的验一下排序

        List<String> sortedStrings = strings.stream().sorted().collect(Collectors.toList());
        
        // [111, 222, 333, 444, 555, 666]
        System.out.println(sortedStrings);

        
        
        List<User> sortedUsers = users.stream().sorted(Comparator.comparing(e -> e.getAge())).collect(Collectors.toList());
        // 等效写法如下
        // List<User> sortedUsers = users.stream().sorted(Comparator.comparing(User::getAge)).collect(Collectors.toList());
        
        // [User(username=李四, password=123456, age=16, height=175, address=Address(province=四川省, city=成都市, county=锦江区), others={sorted=DDD, eee=EEE, fff=FFF}), User(username=赵六, password=123456, age=17, height=168, address=Address(province=四川省, city=成都市, county=高新区), others={sorted=JJJ, kkk=KKK, lll=LLL}), User(username=张三, password=123456, age=20, height=170, address=Address(province=四川省, city=成都市, county=武侯区), others={sorted=AAA, bbb=BBB, ccc=CCC}), User(username=王五, password=123456, age=20, height=180, address=Address(province=四川省, city=成都市, county=青羊区), others={sorted=GGG, hhh=HHH, iii=III})]
        System.out.println(sortedUsers);

很简单,也没啥难理解的,就是排序

基础用法

排序的初体验之后,我们来看看几种正式场景下的使用

1. 降序几种方式

在上面的体验排序中,排序的结果默认都是升序的,那如果我要降序呢?那怎么办?有三种方式,或者三种写法

1. 使用reversed

根据user中的age降序

        List<User> collect = users.stream().sorted(Comparator.comparing(User::getAge).reversed()).collect(Collectors.toList());
        // [User(username=张三, password=123456, age=20, height=170, address=Address(province=四川省, city=成都市, county=武侯区), others={sorted=AAA, bbb=BBB, ccc=CCC}), User(username=王五, password=123456, age=20, height=180, address=Address(province=四川省, city=成都市, county=青羊区), others={sorted=GGG, hhh=HHH, iii=III}), User(username=赵六, password=123456, age=17, height=168, address=Address(province=四川省, city=成都市, county=高新区), others={sorted=JJJ, kkk=KKK, lll=LLL}), User(username=李四, password=123456, age=16, height=175, address=Address(province=四川省, city=成都市, county=锦江区), others={sorted=DDD, eee=EEE, fff=FFF})]
        System.out.println(collect);

根据User中age先排序,默认情况下是升序,然后再逆序一下,就变成了降序。但是这样有点不好,因为是先排序然后在逆序的,要两步操作。

2.使用Comparator.reverseOrder

根据user中的age降序

        List<User> collect1 = users.stream().sorted(Comparator.comparing(User::getAge, Comparator.reverseOrder())).collect(Collectors.toList());
        // [User(username=张三, password=123456, age=20, height=170, address=Address(province=四川省, city=成都市, county=武侯区), others={sorted=AAA, bbb=BBB, ccc=CCC}), User(username=王五, password=123456, age=20, height=180, address=Address(province=四川省, city=成都市, county=青羊区), others={sorted=GGG, hhh=HHH, iii=III}), User(username=赵六, password=123456, age=17, height=168, address=Address(province=四川省, city=成都市, county=高新区), others={sorted=JJJ, kkk=KKK, lll=LLL}), User(username=李四, password=123456, age=16, height=175, address=Address(province=四川省, city=成都市, county=锦江区), others={sorted=DDD, eee=EEE, fff=FFF})]
        System.out.println(collect1);

这种方式原理和和方法1差不多,只是写法不一样

3. 在sorted中使用compareTo

方式1和方式2都是利用sorted默认序,然后再逆序来实现排序的,这样会有两个步骤,先升序,然后再逆序。难道就没有直接按照降序来排序的方法?肯定是有的。既然是排序,肯定是可以指定规则的

按照年龄降序

        List<User> collect2 = users.stream().sorted((x, y) -> y.getAge().compareTo(x.getAge())).collect(Collectors.toList());
        // [User(username=张三, password=123456, age=20, height=170, address=Address(province=四川省, city=成都市, county=武侯区), others={sorted=AAA, bbb=BBB, ccc=CCC}), User(username=王五, password=123456, age=20, height=180, address=Address(province=四川省, city=成都市, county=青羊区), others={sorted=GGG, hhh=HHH, iii=III}), User(username=赵六, password=123456, age=17, height=168, address=Address(province=四川省, city=成都市, county=高新区), others={sorted=JJJ, kkk=KKK, lll=LLL}), User(username=李四, password=123456, age=16, height=175, address=Address(province=四川省, city=成都市, county=锦江区), others={sorted=DDD, eee=EEE, fff=FFF})]
        System.out.println(collect2);

这种方式是通过Integer中的compareTo方法来实现降序的,当然也可以自己实现,只不过age是Integer类型,既然Integer中已经实现两个Integer比较的方法,就可以偷个懒

4. 在sorted自定义规则

        List<User> collect3 = users.stream().sorted((x, y) -> y.getAge() - x.getAge()).collect(Collectors.toList());
        // [User(username=张三, password=123456, age=20, height=170, address=Address(province=四川省, city=成都市, county=武侯区), others={sorted=AAA, bbb=BBB, ccc=CCC}), User(username=王五, password=123456, age=20, height=180, address=Address(province=四川省, city=成都市, county=青羊区), others={sorted=GGG, hhh=HHH, iii=III}), User(username=赵六, password=123456, age=17, height=168, address=Address(province=四川省, city=成都市, county=高新区), others={sorted=JJJ, kkk=KKK, lll=LLL}), User(username=李四, password=123456, age=16, height=175, address=Address(province=四川省, city=成都市, county=锦江区), others={sorted=DDD, eee=EEE, fff=FFF})]
        System.out.println(collect3);

比较实际的最终结果就是返回一个数字,大于0、小于0、等于0,分别就代表大于、小于、等于,所以在sorted方法中返回一个做减法的数字即可。

2. 多级排序

比如我现在的需求是先按照年龄降序,年龄相同的再按照名字降序

        List<User> collect = users.stream().sorted(Comparator.comparing(User::getAge, Comparator.reverseOrder()).thenComparing(User::getHeight, Comparator.reverseOrder())).collect(Collectors.toList());
        // [User(username=王五, password=123456, age=20, height=180, address=Address(province=四川省, city=成都市, county=青羊区), others={sorted=GGG, hhh=HHH, iii=III}), User(username=张三, password=123456, age=20, height=170, address=Address(province=四川省, city=成都市, county=武侯区), others={sorted=AAA, bbb=BBB, ccc=CCC}), User(username=赵六, password=123456, age=17, height=168, address=Address(province=四川省, city=成都市, county=高新区), others={sorted=JJJ, kkk=KKK, lll=LLL}), User(username=李四, password=123456, age=16, height=175, address=Address(province=四川省, city=成都市, county=锦江区), others={sorted=DDD, eee=EEE, fff=FFF})]
        System.out.println(collect);

thenComparing顾名思义,再比较

进阶

放在以前,我肯定会从stream 排序的api说起,看看有哪些方法,再看怎么调用,但是这流式太过抽象,所以我先讲了怎么用,再回头来看看有哪些api,本质是什么

1. 本质

在java.util.stream.Stream中,sorted方法有两个重载形式
sorted一个是无参,一个是需要一个参数java.util.Comparator。

1. Stream sorted();

其实这两个方法我们都用过,在初体验中,第一个就是无参的,这样会根据默认规则排序,至于默认规则是什么,就是排序对象实现的java.lang.Comparable接口中的compareTo方法,不然你试试跑一下这个

List<User> collect = users.stream().sorted().collect(Collectors.toList());

直接报错,报错的原因就是,你要排序一堆User,但是sorted这个无参的方法不知道排序的规则是什么。所以,在使用这个无参的方法时,被排序的元素必须得实现java.lang.Comparable接口,来指定排序规则。

2. Stream sorted(Comparator<? super T> comparator);

除了初体验中的第一个排序,其他的全都是使用的这个方法,很神奇是吧?我好像传的参数不止这样。

但事实上就是这样子,只传了这个一个参数,无非有两种传参形式:一种是确确实实的传了一个java.util.Comparator进去,另外一种是自己实现了java.util.Comparator中的抽象方法compare,这个方法用来进行元素间的比较。因为java.util.Comparator是一个函数式接口,接口中只有compare这一个抽象方法,所以可以结合lambda表达式使用。

我们使用的.sorted(Comparator.comparing(……))其实就是直接传了一个Comparator进去,因为Comparator.comparing这个方法:java.util.Comparator#comparing(java.util.function.Function<? super T,? extends U>, java.util.Comparator<? super U>) 返回的就是一个Comparator。

而类似这种使用:sorted((x, y) -> y.getAge().compareTo(x.getAge()))或者sorted((x, y) -> y.getAge() - x.getAge()),其实就是我们自己在实现java.util.Comparator中的抽象方法compare,这其实就是匿名内部类---->简化---->lambda表达式的这么一个过程,其实还可以简化成 方法引用。

2. 拓展及思考

        List<User> sortedUsers = users.stream().sorted(Comparator.comparing(x -> x.getOthers().get("aaa").toString())).collect(Collectors.toList());
        List<User> sortedUsers2 = users.stream().sorted(Comparator.comparing(x -> x.getOthers().get("aaa").toString()).reversed()).collect(Collectors.toList());

为什么第二个排序会有问题?

 

posted @ 2025-01-24 09:55  CharyGao  阅读(22)  评论(0)    收藏  举报