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, но кортежи просто используются быстрее.