У меня есть поверхностное представление о возможностях потоковой передачи Java 8 Collection, поэтому я не уверен, возможно ли даже следующее: я хотел бы filter коллекцию на основе целочисленного сравнения и повторно использовать это значение для отображения.

Конкретно, у меня есть Collection<String> strings и я хотел бы отобразить каждое из его значений на расстояние Левенштейна с фиксированным String x, если расстояние Левенштейна меньше значения levenshteinLimit.

String x = "some string";
Collection<String> strings = new LinkedList<>(Arrays.asList("not some string",
        "some other string"));
int levenshteinLimit = 10;
Map<Integer, String> stringsLevenshteinMap = strings.stream()
        .filter(string -> LevenshteinDistance.getDefaultInstance().apply(x, string) < levenshteinLimit)
        .collect(Collectors.toMap(string -> LevenshteinDistance.getDefaultInstance().apply(x, string), Function.identity()));
System.out.println(stringsLevenshteinMap);

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

Я предполагаю, что более эффективно сначала фильтровать, а затем отображать, потому что число объектов потенциально меньше, что означает меньше работы.

Приведенный выше код использует Apache commons-text 1.1. Пример проекта можно найти по адресу https://github.com/krichter722. / Java - фильтр и карта -без резервирования.

2
Karl Richter 4 Сен 2017 в 18:31

3 ответа

Лучший ответ

Если вы хотите избежать временного объекта, содержащего ключ и значение, вам нужен собственный коллектор, отслеживающий то, что делает встроенный коллектор, но включающий фильтрацию напрямую. Кстати, я не думаю, что Collectors.toMap уместно здесь, так как нет гарантии, что будет только одна строка для каждого расстояния. Поэтому я использовал Collectors.groupingBy вместо шаблона:

public static <T> Collector<T,?,Map<Integer,List<T>>>
                  grouping(ToIntFunction<T> f, int limit) {
    return Collector.of(HashMap::new,
        (m,t) -> {
            int v = f.applyAsInt(t);
            if(v < limit) m.computeIfAbsent(v, x -> new ArrayList<>()).add(t);
        },
        (m1,m2) -> {
            m2.forEach((k,v) -> m1.merge(k, v, (l1,l2)->{ l1.addAll(l2); return l1; }));
            return m1;
        });
}

Это в основном делает то, что делает Collectors.groupingBy, но ограничивает ее использование ключевой функцией, вычисляющей число int, и обрабатывает только элементы, отображающие числа ниже указанного предела. Также было бы возможно обобщить это, чтобы использовать Function и Predicate вместо этого.

Это можно использовать как

Map<Integer, List<String>> stringsLevenshteinMap
    = Stream.of("not some string", "some other string")
            .collect(grouping(
                string -> LevenshteinDistance.getDefaultInstance().apply(x, string),
                levenshteinLimit));

Но следует подчеркнуть, что нет гарантии, что это работает лучше, чем просто создание временного объекта, содержащего два значения; это зависит от многих факторов окружающей среды. Создание временных объектов не дорого в целом. Подход с использованием объекта-держателя является гораздо более гибким, особенно когда вы хотите позже изменить операцию потока.

1
Holger 4 Сен 2017 в 19:07

Во-первых, вы могли бы сделать код быстрее, избегая создания бесполезного LinkedList.

Теперь, что касается вашего вопроса, если вы хотите продолжать использовать потоки, чтобы сделать это, то решение будет состоять в том, чтобы сопоставить каждую строку с объектом, содержащим строку и ее расстояние, затем отфильтровать эти объекты, а затем собрать на карту:

String x = "some string";
int levenshteinLimit = 10;

List<String> strings = Arrays.asList("not some string", "some other string"));
Map<Integer, String> stringsLevenshteinMap = 
    strings.stream()
           .map(string -> new StringWithDistance(string, LevenshteinDistance.getDefaultInstance().apply(x, string))
           .filter(o -> o.getDistance() < levenshteinLimit)
           .collect(Collectors.toMap(StringWithDistance::getDistance, StringWithDistance.getString));

System.out.println(stringsLevenshteinMap);
0
JB Nizet 4 Сен 2017 в 15:47

Что-то с промежуточным объектом Tuple должно работать:

Map<Integer, String> stringsLevenshteinMap = strings.stream()
    .map(s -> new Tuple<>(LD.getInstance().apply(x, s), s)
    .filter(t -> t.getFirst() < maxDistance)
    .collect(Collectors.toMap(Tuple::getFirst, Tuple::getSecond));
5
daniu 4 Сен 2017 в 15:43