0

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}]
IVR Avenger
  • 15,090
  • 13
  • 46
  • 57
  • Looks like we can use `zip` concept? https://stackoverflow.com/questions/31963297/how-to-zip-two-java-lists – Sraw Oct 20 '20 at 04:18

2 Answers2

5

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).

rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • "...Streams just... make no sense here." -- Right. My assumption is that Streams solve all such odd data structure ugliness and apparently they do not. This covered what I'm stuck doing nicely. – IVR Avenger Oct 28 '20 at 21:06
0

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 Things untouched.

fps
  • 33,623
  • 8
  • 55
  • 110