JAVA内存分页排序小框架
对于程序员来说,排序和分页是再普通不过的需求,需要在内存中排序分页的情况也时有发生。
而自从有了JAVA8 Stream之后,对一个集合的排序分页貌似也在变的简单。
来看个例子,定义一个学生类:
public class Student {
private String name;
private Integer age;
private String sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
下面是对一个学生类集合的按照年龄字段排序,分页的写法。
public static void main(String[] args) {
ArrayList<Student> students = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
Student student = new Student();
student.setName("小" + i);
student.setAge(i);
student.setSex(i % 2 == 0 ? "男" : "女");
students.add(student);
}
int page = 1;//当前页
int size = 10;//每页记录条数
List<Student> collect = students.stream().sorted(Comparator.comparing(Student::getAge))
.limit(2).skip((page - 1) * size).limit(size)
.collect(Collectors.toList());
}
是不是已经很简单了吧。
最开始我们项目组也是这样写的,但是也有问题,根据什么字段进行排序往往是前端指定的,
Comparator.comparing(Student::getAge)中lamada表达式怎么写还要自己建立对应关系,不够友好。
而且我们项目中大量api用到了排序分页,,总是在写类似的代码,程序员的最大的成就当然就是干掉这些重复或者类似的事情了,一套小框架势在必行。
先来看一下小框架最终的使用情况:
School school = new School();
school.setStudents(students);
school.doOrderOrPaging(1,10,"age","desc");
doOrderOrPaging方法中传入排序分页参数,students的排序分页就做好了。
怎么完成的呢?我们一步一步来看
首先,定义了一个抽象类AbstractMultiLevelSortVO,School类要继承该类,doOrderOrPaging就定义在AbstractMultiLevelSortVO中
public abstract class AbstractMultiLevelSortVO<T extends BaseCompareBean> {
//子类提供实现方法,通常就是返回需要比较的集合
@Transient
public abstract List<T> provideSortList();
//子类提供实现方法,通常就是覆盖子类中的集合
@Transient
public abstract void updateSortList(List<T> list);
@Transient
public List<T> doOrderOrPaging(String orderField, String orderType) {
return doOrderOrPaging(new DefaultBasePageReq(orderField, orderType));
}
@Transient
public List<T> doOrderOrPaging(Integer page, Integer size, String orderField, String orderType) {
return doOrderOrPaging(new DefaultBasePageReq(page, size, orderField, orderType));
}
@Transient
public List<T> doOrderOrPaging(BasePageReq req) {
String orderField = req.getOrderField();
String orderType = req.getOrderType();
//取子类中的集合
Stream<T> tickerStream = provideSortList().stream();
if (req.needOrder()) {
Comparator<T> comparator = (o1, o2) -> doCompare(orderField, o1, o2, ASC.equalsIgnoreCase(orderType));
if (!ASC.equalsIgnoreCase(orderType)) {
comparator = comparator.reversed();
}
tickerStream = tickerStream.sorted(comparator);
}
if (req.needPaging()) {
Integer pageSize = req.checkAndGetDefaultPageSize();
Integer pageNum = req.checkAndGetDefaultPage();
tickerStream = tickerStream.skip(pageSize * (pageNum - 1)).limit(pageSize);
}
List<T> list = tickerStream.collect(Collectors.toList());
//更新子类中的集合
updateSortList(list);
return list;
}
}
中间的逻辑比较简单,通过provideSortList拿到实现类对象的要排序分页的集合, needOrder时通过doCompare来进行order, needPaging就是简单地stream分页
接下主要看doCompare的实现,这个方法也定义到AbstractMultiLevelSortVO中
@Transient
public int doCompare(String orderField, T o1, T o2, boolean asc) {
try {
/*
空边界之一
先解决bean为空的情况,原则:无论升序还是降序,空bean无需比较,直接往后放
*/
if (o1 == null || o1.abnormal()) {
return thisValuePutBehind(asc);
}
if (o2 == null || o2.abnormal()) {
return thisValuePutForward(asc);
}
Comparable v1 = o1.tryBestToFindCompareValue(orderField);
Comparable v2 = o2.tryBestToFindCompareValue(orderField);
return compareThinkAboutNull(v1, v2, asc);
} catch (Exception e) {
logger.error("获取指定比较字段失败,放弃比较。 orderField {} e {}", orderField, ExceptionUtils.getFullStackTrace(e));
//若仍有意外发生,也先往后放
return thisValuePutBehind(asc);
}
}
/**
* 根据排序方式 把当前值往前放
*
* @param asc
* @return
*/
private int thisValuePutForward(boolean asc) {
return asc ? -1 : 1;
}
/**
* 根据排序方式 把当前值往后放
*
* @param asc
* @return
*/
private int thisValuePutBehind(boolean asc) {
return asc ? 1 : -1;
}
private int compareThinkAboutNull(Comparable v1, Comparable v2, boolean asc) {
/*
空边界之二
解决最后真正比较字段的空问题,原则相同
*/
if (v1 == null) {
return thisValuePutBehind(asc);
}
if (v2 == null) {
return thisValuePutForward(asc);
}
return v1.compareTo(v2);
}
这之中有比较多的空值逻辑的判断处理,重要的就是通过具体比较对象的tryBestToFindCompareValue方法拿到具体值之后返回v1.compareTo(v2)就可以了
tryBestToFindCompareValue定义在一个接口中
import com.onepiece.shipelves.common.utils.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;
import java.beans.Transient;
import java.lang.reflect.Field;
import java.math.BigDecimal;
public interface BaseCompareBean {
Logger logger = LoggerFactory.getLogger(BaseCompareBean.class);
@Transient
default Comparable tryBestToFindCompareValue(String fieldName) {
Comparable value = tryBestToFindFieldValue(fieldName, Comparable.class);
if (value instanceof String) {
//有一些数值在响应报文里用的String,为了避免String.compare的影响,尝试转BigDecimal进行比较,若value不是数值,则依旧用原值比较
try {
return new BigDecimal(value.toString().trim().replaceAll(",", "").replaceAll("%", ""));
} catch (Exception e) {
logger.error("排序时String类型转换为BigDecimal类型失败 原始值 {}", value);
}
}
return value;
}
/**
* @param fieldName 支持snake风格,可自动转换为驼峰风格再取值;若失败,会尝试用蛇形风格直接取值
* @return
*/
@Transient
default <T> T tryBestToFindFieldValue(String fieldName, Class<T> c) {
//因为传入的排序字段为snake风格
String humpName = StringUtil.toHumpStr(fieldName);
try {
return simpleFindFieldValue(humpName, c);
} catch (Exception e) {
try {
return simpleFindFieldValue(fieldName, c);
} catch (Exception e2) {
logger.warn("找不到要目标排序字段 {}", fieldName);
return null;
}
}
}
/**
* 简单获取指定属性名的属性值
*
* @param fieldName
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
@Transient
default <T> T simpleFindFieldValue(String fieldName, Class<T> c) throws IllegalAccessException {
Field field = ReflectionUtils.findField(getClass(), fieldName);
field.setAccessible(true);
return c.cast(field.get(this));
}
default boolean abnormal() {
return false;
}
}
利用ReflectionUtils.findField可以拿到一个对象的某个字段的值,达到我们的目的。
所以最终在使用时需要
用来比较的集合中的对象也就是student实现BaseCompareBean对象
包装类School继承AbstractMultiLevelSortVO并实现两个对应方法 provideSortList updateSortList就可以了
public class Student implements BaseCompareBean
public class School extends AbstractMultiLevelSortVO<Student> {
private List<Student> students;
private Integer count;
@Override
public List<Student> provideSortList() {
return students;
}
@Override
public void updateSortList(List<Student> list) {
students = list;
}
public List<Student> getStudents() {
return students;
}
public void setStudents(List<Student> students) {
this.students = students;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
}
最终达到调用一个方法传入简单参数就可以支持分页排序的目的
School school = new School();
school.setStudents(students);
school.doOrderOrPaging(1,10,"age","desc");