英文:
Java streams (forEach) to take attributes from one list and apply to another
问题
我有一个简单的类:
public class Thing {
Long id;
String color;
String size;
}
我有两个 `List<Thing>` 对象,看起来像这样:
List<Thing> colors = [{2, 红色}, {3, 蓝色}];
List<Thing> sizes = [{2, 小号}, {3, 大号}];
使用 Java streams / forEach,如何将一个流式传输到另一个,以得到:
List<Thing> things = [{2, 红色, 小号}, {3, 蓝色, 大号}]
英文:
I have a simple class:
public class Thing {
Long id;
String color;
String size;
}
I have two List<Thing>
objects that looks like this:
List<Thing> colors = [{2, red}, {3, blue}];
List<Thing> sizes = [{2, small}, {3, large}];
Using Java streams / forEach, how would I "stream" one into the other to arrive at:
List<Thing> things = [{2, red, small}, {3, blue, large}]
答案1
得分: 5
这个概念(将两个等长的流混合在一起,以便从每个流中获取一个元素,然后将这两个元素传递给一个单独的函数,然后将组合映射到其他内容)被称为 zipping。
Java的流类并不包括对这种操作的支持。除非你能够以某种方式保证两者在完全相同的顺序中发生这个“id”概念(即如果你的sizes
是[{3, large}, {2, small}]
,我们不再讨论zip操作)。
在前一种情况下(不是关于“组合红色和小号,因为它们都具有id 2
”,而是关于将colors
中第一个元素中的2
和red
与sizes
中第一个元素中的small
组合在一起,而不考虑id),你可以从另一个库中找到zip支持,或者像这样操作:
if (colors.size() != sizes.size()) throw new IllegalStateException();
IntStream.range(0, colors.size())
.mapToObj(i -> new Thing(colors.get(i).getId(), colors.get(i).getColor(), sizes.get(i).getSize()))
.collect(Collectors.toList());
这看起来很丑陋,如果列表不是随机访问的话性能非常糟糕,从我能想到的各种方式来看,与仅创建2个迭代器并进行并行迭代相比,这真是令人失望:
if (colors.size() != sizes.size()) throw new IllegalStateException();
var itColors = colors.iterator();
var itSizes = sizes.iterator();
var out = new ArrayList<Thing>();
while (itColors.hasNext()) {
Thing a = itColors.next();
out.add(new Thing(a.getId(), a.getColor(), itSizes.next().getSize());
}
如果这是您的处理方式,那么您肯定不应该首先使用流。
不,关键在于ID。
在这种情况下,再次使用流就好像你在为你的三明治抹果酱时找到了一个非常华丽的锤子:流在这里根本没有意义。您可以尝试一些奇怪的 hacky 方法,以实现这一点并使用流,但它将是不稳定的代码:它看起来很奇怪,不符合惯例,包含大量附加条件(假设如果不成立,意味着代码无法正常工作,或者在某些情况下性能极差),总体而言会更差。
解决方案是使用映射:
var out = new HashMap<Long, Thing>();
for (Thing c : colors) out.put(c.getId(), c);
for (Thing s : sizes) out.merge(c.getId(), s,
(a, b) -> new Thing(a.getId(), a.getColor(), b.getSize()));
看看这是多么干净,它还可以处理输入之间的任何不匹配(sizes包含id 87,但colors没有)。
英文:
This concept (mixing 2 streams of equal length together such that you end up getting one element from each stream both passed to a single function which then maps the combination to something else) is called zipping.
Java's stream classes doesn't include any support for this operation. What you have here also isn't something you can meaningfully zip together unless you can somehow guarantee that both will have this 'id' concept happening in the exact same order (i.e. if your sizes
is [{3, large}, {2, small}]
instead, we're no longer talking about a zip operation).
In the former case (it's not about 'combine red and small because they both have id 2
, it's about combine the 2
and red
from the first element in colors
with the small
from the first element in sizes
, regardless of ids), you can either find zip support from another library, or hack it:
if (colors.size() != sizes.size()) throw new IllegalStateException();
IntStream.range(0, colors.size())
.mapToObj(i -> new Thing(colors.get(i).getId(), colors.get(i).getColor(), sizes.get(i).getSize()))
.collect(Collectors.toList());
It looks ugly, it has ridiculously bad performance if the lists aren't random access, in in pretty much all ways I can fathom this is such a disappointment compared to just making 2 iterators and parallel-iterating them:
if (colors.size() != sizes.size()) throw new IllegalStateException();
var itColors = colors.iterator();
var itSizes = sizes.iterator();
var out = new ArrayList<Thing>();
while (itColors.hasNext()) {
Thing a = itColors.next();
out.add(new Thing(a.getId(), a.getColor(), itSizes.next().getSize());
}
that you should definitely not use streams in the first place if this is how you want to approach it.
No, it's about the IDs.
In that case, again streams seem very much like you using a very shiny fancy hammer you found to smear your jam on your sandwich: Streams just... make no sense here. At all. You can try some hacky weirdo stuff that, well, makes this happen and uses streams, but it'll be flaky code: It'll look weird, be non-idiomatic, include a ton of caveats (assumptions that, if not true, mean the code fails to do the job right, or has extremely bad performance characteristics in certain cases), and in general just be strictly worse.
The solution is maps:
var out = new HashMap<Long, Thing>();
for (Thing c : colors) out.put(c.getId(), c);
for (Thing s : sizes) out.merge(c.getId(), s,
(a, b) -> new Thing(a.getId(), a.getColor(), b.getSize());
look at how clean that is, and it also deals with any mismatches between your inputs (sizes contains id 87, but colors doesn't).
答案2
得分: 0
我会从id
映射到Thing
创建一个Map<Long, Thing>
,首先放入具有颜色的物品,然后我会将大小设置给映射中的物品,仅当它们存在时:
Map<Long, Thing> map = new LinkedHashMap<>();
colors.forEach(t -> map.put(
t.getId(),
new Thing(t.getId(), t.getColor(), null))); // 假设有基于属性的构造函数
sizes.forEach(t -> {
Thing thing = map.get(t.getId());
if (thing != null) thing.setSize(t.getSize());
});
这样会保留原始的Thing
对象不变。
英文:
I'd create a Map<Long, Thing>
from id
to Thing
and put i.e. things with colors first, and then I'd set the sizes to the things of the map, only if they are present:
Map<Long, Thing> map = new LinkedHashMap<>();
colors.forEach(t -> map.put(
t.getId(),
new Thing(t.getId(), t.getColor(), null))); // assumes constructor from attributes
sizes.forEach(t -> {
Thing thing = map.get(t.getId());
if (thing != null) thing.setSize(t.getSize());
});
This lefts the original Thing
s untouched.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论