Фильтрация списка по изменяющемуся количеству условий - PullRequest
0 голосов
/ 30 января 2019

Я хочу написать код, который сможет использовать ряд условий для фильтрации списка объектов, но я хочу, чтобы количество условий колебалось (иногда фильтрация выполняется с 1 полем, а иногда со всеми 3 полями).

В моей ситуации я хочу, чтобы у пользователя была возможность выбора 'any' для определенного поля, но затем все еще применяю фильтр для других полей.В приведенной ниже ситуации пользователь может фильтровать список по полям x, y & z, но затем он может также изменить один из фильтров на «любой», что делает этот фильтр ненужным при фактической фильтрации списка.Метод getFilteredList берет значение String из каждого поля со списком и использует каждый String для фильтрации списка, если String не равен 'any', тогда он будет принимать любые String из этого поля.

combo boxes uses to input a filter option for each field

Ниже я написал код, который будет работать для такой ситуации, но будет становиться все более неэффективным при добавлении большего количества фильтров в будущем.(2 фильтра приводят к 4 операторам возврата, 3 фильтра - к 8 и т. Д.).

private static ArrayList<Model> getFilteredList(ArrayList<Model> originalList, String x, String y, String z ){

    if(x.equals("any") && y.equals("any") && z.equals("any")) {
        return originalList;
    }
    if(x.equals("any") && y.equals("any") && !z.equals("any")) {
        return originalList.stream().filter(item -> item.getZ().equals(z)).collect(Collectors.toCollection(ArrayList::new));
    }
    if(x.equals("any") && !y.equals("any") && z.equals("any")) {
        return originalList.stream().filter(item -> item.getY().equals(y)).collect(Collectors.toCollection(ArrayList::new));
    }
    if(x.equals("any") && !y.equals("any") && !z.equals("any")) {
        return originalList.stream().filter(item -> item.getY().equals(y)&& item.getZ().equals(z)).collect(Collectors.toCollection(ArrayList::new));
    }
    if(!x.equals("any") && y.equals("any") && z.equals("any")) {
        return originalList.stream().filter(item -> item.getX().equals(x)).collect(Collectors.toCollection(ArrayList::new));
    }
    if(!x.equals("any") && y.equals("any") && !z.equals("any")) {
        return originalList.stream().filter(item -> item.getX().equals(x)&& item.getZ().equals(z)).collect(Collectors.toCollection(ArrayList::new));
    }
    if(!x.equals("any") && !y.equals("any") && z.equals("any")) {
        return originalList.stream().filter(item -> item.getX().equals(x)&& item.getY().equals(y)).collect(Collectors.toCollection(ArrayList::new));
    }
    if(!x.equals("any") && !y.equals("any") && !z.equals("any")) {
        return originalList.stream().filter(item -> item.getX().equals(x)&& item.getY().equals(y)&& item.getZ().equals(z)).collect(Collectors.toCollection(ArrayList::new));
    }
    return originalList;
}

Вот как будет выглядеть Model:

public class Model {
    private String x;
    private String y;
    private String z;

    public String getX() {
        return x;
    }
    public String getY() {
        return y;
    }
    public String getZ() {
        return z;
    }
}

Приведенный ниже код вернет список, где поля x равны 'красным', где поля y равны 'средним' и где поля z могут быть любыми.

private static ArrayList<Model> items = new ArrayList<>();

public static void main(String[] args) {
    ArrayList<Model> filteredList = getFilteredList(items, "red", "medium", "any");
}
* 1028Я не уверен, есть ли более простой способ сделать мой метод getFilteredList, я много искал, но не могу найти более простой / более эффективной реализации.В будущем я надеюсь разрешить использование более 10 фильтров, где вариант «любой» всегда является опцией, но мое текущее решение слишком длинное и не будет элегантным.Если бы кто-нибудь мог дать ответы на вопросы о том, как элегантно отфильтровать список с различным количеством фильтров, это было бы очень полезно.

Спасибо.

Ответы [ 4 ]

0 голосов
/ 30 января 2019

Вы можете использовать

private static List<Model> getFilteredList(
    ArrayList<Model> originalList, String x, String y, String z ){

    if(Stream.of(x, y, z).allMatch("any"::equals)) return originalList;

    Stream<Model> s = originalList.stream();
    if(!"any".equals(x)) s = s.filter(m -> m.getX().equals(x));
    if(!"any".equals(y)) s = s.filter(m -> m.getY().equals(y));
    if(!"any".equals(z)) s = s.filter(m -> m.getZ().equals(z));
    return s.collect(Collectors.toList());
}

Но вы должны определиться с типом возврата.Явный тип возврата ArrayList предполагает, что вызывающая сторона получает изменяемый список, но когда список иногда является исходным списком, а иногда нет, в зависимости от условия фильтра, модификации имеют неопределенную семантику, поскольку они могут повлиять на исходный список илинет, что было бы катастрофическим.

Таким образом, вы должны решить, не должен ли

  • возвращенный список быть измененным (не объявляйте его ArrayList и не форсируйте создание ArrayList), как это делает пример выше, или

  • возвращаемый список должен быть изменяемым списком без влияния на источник,затем удалите оптимизацию возврата исходного списка для всех - "any", или

  • , после чего исходный список, как предполагается, не будет использоваться, или

  • модификации возвращенного списка должны всегда влиять на исходный список

Для c) контракт не может быть принудительно применен в методе, а для d) онбыло бы невозможно сделать с Stream API.Поэтому для c) или d) было бы лучше вместо этого изменить исходный список, например

private static void filterList(ArrayList<Model> list, String x, String y, String z) {
    Predicate<Model> any = m -> false, effective = any;
    if(!"any".equals(x)) effective = m -> !m.getX().equals(x);
    if(!"any".equals(y)) effective = effective.or(m -> !m.getY().equals(y));
    if(!"any".equals(z)) effective = effective.or(m -> !m.getZ().equals(z));
    if(effective != any) list.removeIf(effective);
}

, что не оставляет никаких сомнений относительно семантики.

0 голосов
/ 30 января 2019

Вот способ, который будет расти линейно, а не экспоненциально, при увеличении количества условий фильтрации:

private static ArrayList<Model> getFilteredList(ArrayList<Model> originalList, String x, String y, String z){
    Predicate<Model> valueCheck, modelCheck = null;
    if ((valueCheck = (x.equals("any") ? null : item -> x.equals(item.getX()))) != null)
        modelCheck = valueCheck;
    if ((valueCheck = (y.equals("any") ? null : item -> y.equals(item.getY()))) != null)
        modelCheck = (modelCheck != null ? modelCheck.and(valueCheck) : valueCheck);
    if ((valueCheck = (z.equals("any") ? null : item -> z.equals(item.getZ()))) != null)
        modelCheck = (modelCheck != null ? modelCheck.and(valueCheck) : valueCheck);
    if (modelCheck == null)
        return originalList; // No filtering needed
    return originalList.stream().filter(modelCheck).collect(Collectors.toCollection(ArrayList::new));
}

Он основан на построении составного фильтра, он же Predicate, используя вспомогательный метод and(...).

0 голосов
/ 30 января 2019

Вы можете просто чередовать вызовы фильтра в потоке для каждого String, по которому производится фильтрация, и соответствующего ему свойства Model.

BiFunction stringFilter гарантирует, что модель будет включена в результирующийпоток, если либо для этого свойства установлен фильтр "any", либо фильтр равен значению в соответствующем поле Model.
Поскольку каждый фильтр работает на выходе последнего, мы можем убедиться, что нет Modelкоторый не соответствует всем желаемым критериям, будет включен в возвращенный список.

public List<Model> getFilteredList(List<Model> originalList, String x, String y, String z) {

    final BiPredicate<String, Supplier<String>> stringFilter = (filter, stringSupplier) ->
            filter.equals("any") || filter.equals(stringSupplier.get());

    return originalList.stream()
            .filter(model -> stringFilter.test(x, model::getX))
            .filter(model -> stringFilter.test(y, model::getY))
            .filter(model -> stringFilter.test(z, model::getZ))
            .collect(Collectors.toList());
}

Тип Supplier<T> - это функциональный интерфейс (интерфейс только с одним методом), определенный в java.util.function, который будет возвращатьэкземпляр типа T, когда вызывается его метод get ().

Когда мы фильтруем поток, мы передаем одну из строк для фильтрации и соответствующий метод получения модели в наш BiPredicate.Получатель передается по ссылке и будет действовать как источник строки для нашего Supplier.

0 голосов
/ 30 января 2019

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

private static ArrayList<Model> getFilteredList(ArrayList<Model> originalList, String x, String y, String z ){
    Predicate<Model> filter = Stream.of(compare(Model::getX, x), compare(Model::getY, y), compare(Model::getZ, z))
            .filter(Objects::nonNull)
            .reduce(Predicate::and)
            .orElse($ -> true);

    return originalList.stream()
            .filter(filter)
            .collect(Collectors.toCollection(ArrayList::new));
}

private static Predicate<Model> compare(Function<Model, String> field, String value) {
    return value.equals("any") ? null : m -> field.apply(m).equals(value);
}
...