Добрый день, пишет потенциальный 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;
    }
}

В результате мы получаем возможность генерировать компараторы на основе данных о классе и порядке сортировки, при этом и класс, и порядок могут изменяться.

Заранее прошу прощения за возможные огрехи в коде и отсутствие более глубокого анализа скорости работы такого метода, возможных ошибках и недостатках, которые не смог отразить в статье в силу отсутствия опыта в коммерческом программировании.

Буду рад комментариям под моей первой статьей.

Комментарии (2)


  1. myachin97
    10.10.2022 19:17

    Статья хорошая, другой вопрос, насколько это применимо в реальный жизни ? =)


    1. GapSerg Автор
      10.10.2022 19:21

      Интересный вопрос)) Посмотрим, надеюсь, что те кто используют этот подход на практике или в обучении (это уверен произойдет) , напишут об этом.