Добрый день, пишет потенциальный Java разработчик. Недавно столкнулся со следующей стандартной задачей: написать многоступенчатый компаратор для сортировки коллекции простых объектов.
После изучения данного вопроса возникло желание написать компаратор для класса, который бы мог динамически изменяться при изменении полей класса и (или) параметров сортировки, которые задаются извне.
Итак обо всем по порядку. Есть некоторый класс Product:
import java.util.Formatter;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class Product {
private String name;
private Integer rate;
private Double price;
public Product(String name, Integer rate, Double price) {
this.name = name;
this.rate = rate;
this.price = price;
}
@Override
public String toString() {
String shortName = name.substring(0, Math.min(name.length(), 18));
return new Formatter().format("name: %-20s rate: %8d price: %8.2f", shortName, rate, price)
.toString();
}
}
Если заранее известно направление (ASC, DESC) и очередность сортировки, то решение этой задачи может легко осуществиться следующим методом:
public static List<Product> sortMethod(List<Product> products){
return products.stream()
.sorted(Comparator.comparing(Product::getName)
.thenComparing(Comparator.comparing(Product::getRate).reversed())
.thenComparing(Product::getPrice))
.toList();
}
Аналогичные действия можно выполнить с помощью CompareToBuilder.class или других мне пока не известных библиотек.
Но как быть, если состав полей класса не известен заранее, а параметры сортировки хранятся в отдельном файле, и тоже могут быть заданы произвольно? Как написать динамический компаратор, способный работать без изменения кода, при изменение полей самой сущности и параметров сортировки.
Имеем класс Product, представленный выше, и параметры компаратора, которые содержатся в XML файле вида:
<sort>
<name>asc</name>
<price>asc</price>
<rate>desc</rate>
</sort>
Первое, что необходимо сделать - это распарсить данный файл и записать параметры нашего будущего компаратора в некую структуру, которая позволит хранить порядок записанных в нее данных. Я использовал для этого LinkedHashMap<Field, SortType>, где SortType это Enum вида:
package org.example.sort;
import lombok.Getter;
public enum SortType {
ASC(1),
DESC(-1);
@Getter
private int value;
SortType(int i) {
this.value = i;
}
}
Мои XMLParser.class выглядит следующим образом, возможно не самым лучшим и оптимальным, но не это является главным в данной статье. Обращаю внимание, что в качестве параметров метода getSortTypeMap() используется наш Product.class и некоторый путь к файлу с параметрами сортировки, что позволяет говорить о динамическом построении аргументов метода getComparatorBySortMap() будущего ComparatorFactory.class.
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import lombok.SneakyThrows;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class XMLParser {
@SneakyThrows
public static LinkedHashMap<Field, SortType>
getSortTypeMap (Class<?> clazz, String path) {
LinkedHashMap<Field, SortType> sortTypeMap = new LinkedHashMap<>();
List<Field> fields = List.of(clazz.getDeclaredFields());
DocumentBuilder documentBuilder;
Document document = null;
try {
documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
document = documentBuilder.parse(path);
} catch (SAXException | ParserConfigurationException | IOException e) {
e.printStackTrace();
}
XPathFactory pathFactory = XPathFactory.newInstance();
XPath xpath = pathFactory.newXPath();
XPathExpression expr = xpath.compile("//sort/child::*");
NodeList nodes = (NodeList) expr.evaluate(document, XPathConstants.NODESET);
for (int i = 0; i < nodes.getLength(); i++) {
Node n = nodes.item(i);
Field field =
fields.stream().filter(t -> t.getName().equals(n.getNodeName()))
.findFirst()
.orElse(null);
if (field != null) {
if (n.getTextContent().toUpperCase().equals(SortType.ASC.toString())) {
sortTypeMap.put(field, SortType.ASC);
}
if (n.getTextContent().toUpperCase().equals(SortType.DESC.toString())) {
sortTypeMap.put(field, SortType.DESC);
}
}
}
return sortTypeMap;
}
}
И теперь непосредственно сам класс, позволяющий сгенерировать необходимый компаратор и основанный на использовании ComparatorChain.class и BeanComparator.class:
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections4.comparators.ComparatorChain;
public class ComparatorFactory <T> {
public ComparatorChain<T> getProductComparatorBySortMap(
LinkedHashMap<Field, SortType> sortTypeMap) {
if (sortTypeMap.isEmpty()) {
return null;
}
ComparatorChain<T> chain = new ComparatorChain<>();
String parameterName;
boolean direction;
for (Map.Entry<Field, SortType> sortParametr : sortTypeMap.entrySet()) {
parameterName = sortParametr.getKey().getName();
direction = sortParametr.getValue().getValue() <= 0;
chain.addComparator(new BeanComparator<>(parameterName), direction);
}
return chain;
}
}
В результате мы получаем возможность генерировать компараторы на основе данных о классе и порядке сортировки, при этом и класс, и порядок могут изменяться.
Заранее прошу прощения за возможные огрехи в коде и отсутствие более глубокого анализа скорости работы такого метода, возможных ошибках и недостатках, которые не смог отразить в статье в силу отсутствия опыта в коммерческом программировании.
Буду рад комментариям под моей первой статьей.
myachin97
Статья хорошая, другой вопрос, насколько это применимо в реальный жизни ? =)
GapSerg Автор
Интересный вопрос)) Посмотрим, надеюсь, что те кто используют этот подход на практике или в обучении (это уверен произойдет) , напишут об этом.