如何使用 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 一次性通过给定的数据集实现这一点,而不会增加内存消耗(即结果将仅包含 id
s 具有所需属性).
为此,您可以创建一个自定义 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
。
我有以下代码
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 一次性通过给定的数据集实现这一点,而不会增加内存消耗(即结果将仅包含 id
s 具有所需属性).
为此,您可以创建一个自定义 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
。