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 对象集合定义如下:
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定义中,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,让集合操作更优雅简洁。