Добрый день, пишет потенциальный 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; } }
В результате мы получаем возможность генерировать компараторы на основе данных о классе и порядке сортировки, при этом и класс, и порядок могут изменяться.
Заранее прошу прощения за возможные огрехи в коде и отсутствие более глубокого анализа скорости работы такого метода, возможных ошибках и недостатках, которые не смог отразить в статье в силу отсутствия опыта в коммерческом программировании.
Буду рад комментариям под моей первой статьей.
