Java中对集合排序的实现演变:从Comparable、Comparator到lambda,从啰嗦到简洁

今年初学Java,是个新人。若文中有错误纰漏,希望能指出,见谅。

 

目标:对 User 对象集合进行排序,要求使用简单并且代码可读性强。

User 类定义如下:

public class User {

    /**
     * id
     */
    private String id;
    /**
     * 姓名
     */
    private String Name;
    /**
     * 年龄
     */
    private int age;
    /**
     * 身高
     */
    private Integer height;
    /**
     * 体重
     */
    private Double weight;
    /**
     * 性别
     */
    private boolean sex;
    /**
     * 出生年月
     */
    private Date birthday;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return Name;
    }

    public void setName(String name) {
        Name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Integer getHeight() {
        return height;
    }

    public void setHeight(Integer height) {
        this.height = height;
    }

    public Double getWeight() {
        return weight;
    }

    public void setWeight(Double weight) {
        this.weight = weight;
    }

    public boolean isSex() {
        return sex;
    }

    public void setSex(boolean sex) {
        this.sex = sex;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }
}
User类

User 对象集合定义如下:

private List<User> getUsers() throws Exception{
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");

    User u1 = new User();
    u1.setId(UUID.randomUUID().toString());
    u1.setName("隔壁老王");
    u1.setSex(false);
    u1.setAge(33);
    u1.setHeight(168);
    u1.setWeight(72.5);
    u1.setBirthday(format.parse("1970-05-13"));

    User u2 = new User();
    u2.setId(UUID.randomUUID().toString());
    u2.setName("小头爸爸");
    u2.setSex(false);
    u2.setAge(30);
    u2.setHeight(172);
    u2.setWeight(59.8);
    u2.setBirthday(format.parse("1980-10-08"));

    User u3 = new User();
    u3.setId(UUID.randomUUID().toString());
    u3.setName("大头儿子的妈妈");
    u3.setSex(true);
    u3.setAge(27);
    u3.setHeight(165);
    u3.setWeight(47.3);
    u3.setBirthday(format.parse("1983-11-15"));

    User u4 = new User();
    u4.setId(UUID.randomUUID().toString());
    u4.setName("大头儿子");
    u4.setSex(false);
    u4.setAge(9);
    u4.setHeight(108);
    u4.setWeight(37D);
    u4.setBirthday(format.parse("2001-04-01"));

    List<User> data = new ArrayList<>();
    data.add(u1);
    data.add(u2);
    data.add(u3);
    data.add(u4);

    return data;
}
User集合定义

 
User定义中,age(年龄)的数据类型为 int,height(身高)的数据类型为 Integer,以基本类型、包类型为比较,分别对它们实现排序。

先实现对 height(身高)的排序。

实现按 height(身高)排序的方式有多种,按照“jdk版本从低到高”、“写法从复杂到简单”的次序逐一演示。

方法1:定义比较器类

List<User> data = this.getUsers();
System.out.println("原始数据:");
this.prints(data);

class HeightComparator implements Comparator {
    public int compare(Object object1, Object object2) {
            User p1 = (User) object1;
            User p2 = (User) object2;
            return p1.getHeight().compareTo(p2.getHeight());
        }
    }

Collections.sort(data, new HeightComparator());
System.out.println("按身高排序后:");
this.prints(data);

 

方法2:定义比较器对象

List<User> data = this.getUsers();
System.out.println("原始数据:");
this.prints(data);

    Comparator<User> c1 = new Comparator<User>() {
        @Override
        public int compare(User o1, User o2) {
            return o1.getHeight() - o2.getHeight();
        }
    };

Collections.sort(data, c1);
System.out.println("按身高排序后:");
this.prints(data);

 

方法3:以lambda方式定义比较器对象

List<User> data = this.getUsers();
System.out.println("原始数据:");
this.prints(data);

Function<User, Integer> f1 = u -> u.getHeight();
Comparator<User> c1 = Comparator.comparing(f1);
//上述2句代码,也可以简化成一句:
//Comparator<User> c1 = Comparator.comparing(u -> u.getHeight());

Collections.sort(data, c1);
System.out.println("按身高排序后:");
this.prints(data);

 

jrk1.8之后实现了lambda表达式,于是有了Comsumer、Function、Predicate这些基于lambda的实现(在java里不知道概念叫什么),相应的Comparator也实现了对Function的支持,因此方法3需要jdk1.8的支持。从上述3种方式来看,代码量越来越少,也越来越优雅。
方法3已经能够一句代码实现按 height(身高)排序的功能:

Collections.sort(data, Comparator.comparing(u -> u.getHeight()));

但这仍旧不足,因为排序操作应该只需要关心“要排序的集合”和“按什么排序”,上面这句代码还多了“比较器”。不能忍,必须消灭它。

实现对List的工具类方法sort和sortDescending,分别表示顺序排序和倒序排序,实现如下:

/**
     * 根据指定属性对集合顺序排序
     * @param data 集合对象
     * @param func 委托
     * @param <T> 数据类型
     * @param <R> 要排序的属性的数据类型
     */
    public static <T, R extends Comparable<? super R>> void sort(List<T> data, Function<T, R> func){
        Comparator<T> comparator = Comparator.comparing(func);
        data.sort(comparator);
    }

    /**
     * 根据指定属性对集合倒序排序
     * @param data 集合对象
     * @param func 委托
     * @param <T> 数据类型
     * @param <R> 要排序的属性的数据类型
     */
    public static <T, R extends Comparable<? super R>> void sortDescending(List<T> data, Function<T, R> func){
        Comparator<T> comparator = Comparator.comparing(func).reversed();
        data.sort(comparator);
    }

由于jdk中只支持对List的排序(List对象内置sort方法,Collections.sort方法虽然封装在Collections中,但其实也只支持List),因此可以将上述工具方法封装在ListUtil中,有了这2个方法,就可以像下面那样“快速”地写出可读性很高的排序了:

List<User> data = this.getUsers();
System.out.println("原始数据:");
this.prints(data);

ListUtil.sort(data, a -> a.getHeight());
System.out.println("按身高顺序排序后:");
this.prints(data);

ListUtil.sortDescending(data, a -> a.getHeight());
System.out.println("按身高倒序排序后:");
this.prints(data);

若想根据其他属性排序,动几下(真的是几下)手指头,就可以实现:

ListUtil.sort(data, a -> a.getAge());

ListUtil.sortDescending(data, a -> a.getWeight());

 

那么实现对 age(年龄)的排序,和上面是不是一样?
并不完全一样,存在较大的差异。
像上面的方法1是无法实现对 age(年龄)的排序的,因为该属性数据类型为 int。
我们来看一下方法1中一句关键代码:

return p1.getHeight().compareTo(p2.getHeight());

Heigth是Integer类型,而这种包装类,都是继承了Comparable接口并实现了compareTo方法的,因此上述代码中可以直接使用compareTo方法。但基本类型int并没有该方法的实现,因此无法以方法1的方式实现排序。

方法2可以实现对 age(年龄)的排序,但当排序属性非Number类型时,这个compare写起来就微微麻烦了,因为compare的返回值的含义是“1表示大于,0表示等于,-1表示小于”。当排序属性是boolean、string等类型时,得这样写:

public int compare(User o1, User o2) {
        //性别属性,boolean类型
        int i1 = o1.isSex() ? 1 : 0;
        int i2 = o2.isSex() ? 1 : 0;
        return i1 - i2;
}


public int compare(UT o1, UT o2) {
        //姓名属性,String类型
        int i1 = (o1.getName() == null ? 0 : (o1.getName().charAt(0)));
        int i2 = (o2.getName() == null ? 0 : (o2.getName().charAt(0)));
        return i1 - i2;
}

方法3可以实现对 age(年龄)的排序,并且也支持其他各种数据类型的属性,Comparaor中会有对各种数据类型(包含非Number类型)的默认排序规则,比如boolean类型按照false->true排序。

List<User> data = this.getUsers();
System.out.println("原始数据:");
this.prints(data);

Function<User, Integer> f1 = u -> u.getAge();
Comparator<User> c1 = Comparator.comparing(f1);
//上述2句代码,也可以简化成一句:
//Comparator<User> c1 = Comparator.comparing(u -> u.getHeight());

Collections.sort(data, c1);
System.out.println("按年龄排序后:");
this.prints(data);

需要说明的是,虽然 age(年龄)的数据类型是int,但Function中的返回类型不能写成int,因为Function的泛型定义并不支持基本类型。 
所以,如果要对 sex(性别,数据类型是 boolean)排序,这句Function的定义得这样写:

Function<UT, Boolean> f1 = u -> u.isSex();


综上所述,无论是对 height(身高)的排序还是对 age(年龄)的排序,方法3都支持得非常棒。而前面贴出的扩展工具方法—— sort 和 sortDescending,都是基于方法3的,所以该扩展方法,适用于各种排序。


结尾。
拥抱变化吧,虽然目前java实现的lambda由于受擦除式泛型的限制,还不是非常灵活,但目前这些新写法,无论是代码数量、简洁程度、优雅性、可读性,在我看来都优于常规的for写法。虽然jdk1.8本身暴露的lambda接口寥寥无几,但我们能够去扩展去完善这些api,让集合操作更优雅简洁。

posted @ 2016-09-21 17:02  落阳  阅读(4535)  评论(0编辑  收藏  举报