Java 8 Стрим-фильтрация и группировка по тому же дорогому методу вызова

Я ищу способ оптимизировать обработку Stream чистым способом.

У меня есть что-то вроде этого:

try (Stream<Path> stream = Files.list(targetDir)) {
    Map<String, List<Path>> targetDirFilteredAndMapped = stream.parallel()                                                                                                
        .filter(path -> sd.containsKey(md5(path)))                                                                                                                    
        .collect(Collectors.groupingBy(path -> md5(path)));
} catch (IOException ioe) { // manage exception }

и поскольку функция md5 довольно дорога, мне было интересно, есть ли способ вызвать ее только один раз для каждого файла.

Любые предложения?

Ответы

Ответ 1

Вы можете создать некоторый объект PathWrapper, содержащий экземпляр Path и соответствующий ему md5(path).

public class PathWrapper
{
    Path path;
    String md5; // not sure if it a String
    public PathWrapper(Path path) {
        this.path = path;
        this.md5 = md5(path);
    }
    public Path getPath() {return path;}
    public String getMD5() {return md5;}
}

Затем сопоставьте свой поток с Stream<PathWrapper>:

try (Stream<Path> stream = Files.list(targetDir)) {
    Map<String, List<Path>> targetDirFilteredAndMapped =
        stream.parallel() 
              .map(PathWrapper::new)
              .filter(path -> sd.containsKey(path.getMD5()))                                                                                                                    
              .collect(Collectors.groupingBy(PathWrapper::getMD5,
                                             Collectors.mapping(PathWrapper::getPath,
                                                                Collectors.toList())));
} catch (IOException ioe) { /* manage exception */ }

Ответ 2

Если операция md5 действительно доминирует над производительностью, вы можете рассмотреть возможность оставить здесь фильтрацию и просто удалить группы, не связанные с этим:

try(Stream<Path> stream = Files.list(targetDir)) {
    Map<String, List<Path>> targetDirFilteredAndMapped = stream.parallel()
        .collect(Collectors.groupingBy(p -> md5(p), HashMap::new, Collectors.toList()));
    targetDirFilteredAndMapped.keySet().retainAll(sd.keySet());
} catch (IOException ioe) { 
    // manage exception
}

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

Ответ 3

Другой альтернативой создания выделенного класса является непосредственный метод collect, где вы позаботитесь о вычислении md5 в аккумуляторе и где объединитель позаботится о слиянии записей.

try (Stream<Path> stream = Files.list(targetDir)) {
    Map<String, List<Path>> targetDirFilteredAndMapped =
        stream.parallel()
              .collect(HashMap::new,
                       (m, p) -> {
                           String res = md5(p);
                           if(sd.containsKey(res)) {
                               m.computeIfAbsent(res, k -> new ArrayList<>()).add(p);
                           }
                        },
                        (m1, m2) -> m2.forEach((k, v) -> m1.computeIfAbsent(k, k2 -> new ArrayList<>()).addAll(v)));
} catch (IOException ioe) { 
    // manage exception
}

Как отметил @Holger, вы можете оптимизировать это, избегая создания нового списка с использованием лучшей функции слияния:

(m1, m2) -> m2.forEach((k,v) -> m1.merge(k, v, (l1,l2) -> { l1.addAll(l2); return l1; })) 

Ответ 4

Я использую кортежи для таких случаев.

public static void main(String [] args) {
    Map<String, String> sd = Maps.newHashMap();
    Stream<Path> stream = Stream.empty();
    Map<String, List<Path>> targetDirFilteredAndMapped = stream.parallel()
        .map(path -> Tuple.tuple(path, md5(path)))
        .filter(tuple -> sd.containsKey(tuple.right()))
        .collect(groupingBy(Tuple::right,
                 mapping(Tuple::left,
                 toList())));
}

private static String md5(final Path path) {
        return "md5";
}

Несчастливо нет кортежа в java (вроде() в scala), поэтому я создал такой класс:

@ToString
@EqualsAndHashCode
public class Tuple<L, R> {
    public static <L, R> Tuple<L, R> tuple(L left, R right) {
        return new Tuple<>(left, right);
    }

    private final L left;
    private final R right;

    private Tuple(L left, R right) {
        this.left = left;
        this.right = right;
    }

    public L left() {
        return left;
    }

    public R right() {
        return right;
    }
}

Вы также можете создать какой-то частный класс, в котором хранятся как Path, так и md5, но кортежи просто используются быстрее.