如何使用 Java 8 避免多个流

How to avoid multiple Streams with Java 8

我有以下代码

trainResponse.getIds().stream()
        .filter(id -> id.getType().equalsIgnoreCase("Company"))
        .findFirst()
        .ifPresent(id -> {
            domainResp.setId(id.getId());
        });

trainResponse.getIds().stream()
        .filter(id -> id.getType().equalsIgnoreCase("Private"))
        .findFirst()
        .ifPresent(id ->
            domainResp.setPrivateId(id.getId())
        );

这里是 iterating/streaming Id 个对象的列表 2 次。

两个流之间的唯一区别在于 filter() 操作。

如何在单次迭代中实现,最佳方法是什么(时间space复杂度)要做到这一点?

IMO,两个流的解决方案是最可读的。它甚至可能是使用流的最有效解决方案。

IMO,避免多个流的最佳方法是使用 classical 循环。例如:

// There may be bugs ...

boolean seenCompany = false;
boolean seenPrivate = false;
for (Id id: getIds()) {
   if (!seenCompany && id.getType().equalsIgnoreCase("Company")) {
      domainResp.setId(id.getId());
      seenCompany = true;
   } else if (!seenPrivate && id.getType().equalsIgnoreCase("Private")) {
      domainResp.setPrivateId(id.getId());
      seenPrivate = true;
   }
   if (seenCompany && seenPrivate) {
      break;
   }
}

不清楚执行一次迭代还是两次迭代效率更高。这将取决于 getIds() 返回的 class 和迭代代码。

具有两个标志的复杂内容是如何在 2 流解决方案中复制 findFirst() 的短路行为。我不知道是否可以 完全 使用一个流。如果可以的话,它会涉及一些非常狡猾的代码。

但是正如您所见,您使用 2 个流的原始解决方案显然比上面的更容易理解。


使用流的主要目的是让你的代码更简单。这与效率无关。当您尝试做一些复杂的事情来提高流的效率时,您可能首先违背了使用流的(真实)目的。

您可以按类型分组并查看生成的地图。 我想 ids 的类型是 IdType.

Map<String, List<IdType>> map = trainResponse.getIds()
                                .stream()
                                .collect(Collectors.groupingBy(
                                                     id -> id.getType().toLowerCase()));

Optional.ofNullable(map.get("company")).ifPresent(ids -> domainResp.setId(ids.get(0).getId()));
Optional.ofNullable(map.get("private")).ifPresent(ids -> domainResp.setPrivateId(ids.get(0).getId()));

我建议使用传统的 for 循环。除了易于扩展之外,这还可以防止您多次遍历集合。 您的代码看起来像是将来会被推广的东西,因此是我的通用方法。

这是一些伪代码(有错误,只是为了说明)

Set<String> matches = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
for(id : trainResponse.getIds()) {

    if (! matches.add(id.getType())) {
        continue;
    }

    switch (id.getType().toLowerCase()) {

        case "company":
            domainResp.setId(id.getId());
            break;

        case "private":
            ...
    }
}

按照这些思路可能会奏效,但它会贯穿整个流,并且不会在第一次出现时停止。 但是假设一个小流并且每种类型只有一个 Id,为什么不呢?

Map<String, Consumer<String>> setters = new HashMap<>();
setters.put("Company", domainResp::setId);
setters.put("Private", domainResp::setPrivateId);

trainResponse.getIds().forEach(id -> {
    if (setters.containsKey(id.getType())) {
        setters.get(id.getType()).accept(id.getId());
    }
});

您可以使用 Stream IPA 一次性通过给定的数据集实现这一点,而不会增加内存消耗(即结果将仅包含 ids 具有所需属性).

为此,您可以创建一个自定义 Collector,它将期望作为其参数的 Collection 属性要查找,并且 Function 负责从流元素中提取属性.

这就是这个通用收集器的实现方式。

/** *
 * @param <T> - the type of stream elements
 * @param <F> - the type of the key (a field of the stream element)
 */
class CollectByKey<T, F> implements Collector<T, Map<F, T>, Map<F, T>> {
    private final Set<F> keys;
    private final Function<T, F> keyExtractor;
    
    public CollectByKey(Collection<F> keys, Function<T, F> keyExtractor) {
        this.keys = new HashSet<>(keys);
        this.keyExtractor = keyExtractor;
    }
    
    @Override
    public Supplier<Map<F, T>> supplier() {
        return HashMap::new;
    }
    
    @Override
    public BiConsumer<Map<F, T>, T> accumulator() {
        return this::tryAdd;
    }
    
    private void tryAdd(Map<F, T> map, T item) {
        F key = keyExtractor.apply(item);
        if (keys.remove(key)) {
            map.put(key, item);
        }
    }
    
    @Override
    public BinaryOperator<Map<F, T>> combiner() {
        return this::tryCombine;
    }
    
    private Map<F, T> tryCombine(Map<F, T> left, Map<F, T> right) {
        right.forEach(left::putIfAbsent);
        return left;
    }
    
    @Override
    public Function<Map<F, T>, Map<F, T>> finisher() {
        return Function.identity();
    }
    
    @Override
    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }
}

main() - 演示(虚拟 Id class 未显示)

public class CustomCollectorByGivenAttributes {
    public static void main(String[] args) {
        List<Id> ids = List.of(new Id(1, "Company"), new Id(2, "Fizz"),
                               new Id(3, "Private"), new Id(4, "Buzz"));
        
        Map<String, Id> idByType = ids.stream()
                .collect(new CollectByKey<>(List.of("Company", "Private"), Id::getType));
        
        idByType.forEach((k, v) -> {
            if (k.equalsIgnoreCase("Company")) domainResp.setId(v);
            if (k.equalsIgnoreCase("Private")) domainResp.setPrivateId(v);
        });
    
        System.out.println(idByType.keySet()); // printing keys - added for demo purposes
    }
}

输出

[Company, Private]

注意,键集变空后(即所有结果数据都已获取),流的其他元素将被忽略,但仍然需要所有剩余数据待处理。

对于您的 ID 列表,您可以只使用地图,然后在检索后分配它们(如果存在)。

Map<String, Integer> seen = new HashMap<>();

for (Id id : ids) {
    if (seen.size() == 2) {
        break;
    }
    seen.computeIfAbsent(id.getType().toLowerCase(), v->id.getId());
}

如果你想测试它,你可以使用以下方法:

record Id(String getType, int getId) {
    @Override
    public String toString() {
        return String.format("[%s,%s]", getType, getId);
    }
}

Random r = new Random();
List<Id> ids = r.ints(20, 1, 100)
        .mapToObj(id -> new Id(
                r.nextBoolean() ? "Company" : "Private", id))
        .toList();

编辑为仅允许检查某些类型

如果你有两种以上的类型,但只想检查某些类型,你可以按如下方式进行。

  • 除了您有 Set 种允许的类型外,过程是相同的。
  • 您只需使用 contains.
  • 检查您是否正在处理其中一种类型
Map<String, Integer> seen = new HashMap<>();

Set<String> allowedTypes = Set.of("company", "private");
for (Id id : ids) {
    String type = id.getType();

    if (allowedTypes.contains(type.toLowerCase())) {
        if (seen.size() == allowedTypes.size()) {
            break;
        }
        seen.computeIfAbsent(type,
                v -> id.getId());
    }
}

测试类似,只是需要包含其他类型。

  • 创建可能存在的一些类型的列表。
  • 并像以前一样建立一个列表。
  • 请注意,允许类型的大小替换了值 2,以允许在退出循环之前检查两种以上的类型。
List<String> possibleTypes = 
      List.of("Company", "Type1", "Private", "Type2");
Random r = new Random();
List<Id> ids =
        r.ints(30, 1, 100)
                .mapToObj(id -> new Id(possibleTypes.get(
                        r.nextInt((possibleTypes.size()))),
                        id))
                .toList();

我们可以使用 Java 9 之后的 Collectors.filtering 来根据条件收集值。

对于这种情况,我更改了如下代码

final Map<String, String> results = trainResponse.getIds()
            .stream()
            .collect(Collectors.filtering(
                id -> id.getType().equals("Company") || id.getIdContext().equals("Private"),
                Collectors.toMap(Id::getType, Id::getId, (first, second) -> first)));

并从 results 地图获取 id