tdm
tdm

Reputation: 115

Conditional mapping to new objects with a Java Stream

I have a stream of objects (a List) and want to create new objects from that stream, to be inserted into a Set. However, two or more objects in the incoming List may hash to the same Key in the Set, in which case I want to append a String from the nth List object to the one already in the Set instead of creating a new one.

Something like this, but in functional form:

HashSet<ClassB> mySet = new HashSet<>();
for (ClassA instanceA : classAList) {
    if (mySet.contains(ClassB.key(instanceA))) { //static method call to find the key
        mySet.get(instanceA).appendFieldA(instanceA.getFieldA());
    } else {
        mySet.add(new ClassB(instanceA));
    }
}
return mySet;

In functional form I though of creating something like this:

List classAList = new ArrayList<>();
classAList.stream()
.map(instanceA -> new ClassB(instanceA))
.collect(Collectors.toSet());

But then of course that ignores the hashmap and I don't get to combine fields my multiple instances of ClassA that would all resolve to the same ClassB. I'm not sure how to put that in there. Do I need ignore the map() call and create a custom collector instead to do this job? There seems to be more than one way to do this, but I'm new to Streams.

Upvotes: 1

Views: 6465

Answers (2)

Holger
Holger

Reputation: 298519

It’s hard to understand what you actually want as your code example does not work at all. The problem is that a Set does not work like a Map, you can’t ask it for the contained equivalent object. Besides that, you are using different objects for your contains(…) and get(…) call. Also, it’s not clear what the difference between ClassB.key(instanceA) and new ClassB(instanceA) is.

Let’s try to redefine it:

Suppose we have a key type Key and a method Key.key(instanceA) to define the group candidates. Then we have a ClassB which is the resulting type, created via new ClassB(instanceA) for a single (or primary ClassA instance), having an .appendFieldA(…) method to receive a value of another ClassA instance when merging two group members. Then, the original (pre Java 8) code will look as follows:

HashMap<Key, ClassB> myMap = new HashMap<>();
for(ClassA instanceA: classAList) {
    Key key=Key.key(instanceA);
    if(myMap.containsKey(key)) {
        myMap.get(key).appendFieldA(instanceA.getFieldA());
    } else {
        myMap.put(key, new ClassB(instanceA));
    }
}

Then, myMap.values() provides you a collection of the ClassB instances. If it has to be a Set, you may create it via

Set<ClassB> result=new HashSet<>(myMap.values());

Note that this also works, when Key and ClassB are identical as it seems to be in your code, but you may ask youself, whether you really need both, the instance created via .key(instanceA) and the one created via new ClassB(instanceA)


This can be simplified via the Java 8 API as:

for(ClassA instanceA: classAList) {
    myMap.compute(Key.key(instanceA), (k,b)-> {
        if(b==null) b=new ClassB(instanceA);
        else b.appendFieldA(instanceA.getFieldA());
        return b;
    });
}

or, if you want it look even more function-stylish:

classAList.forEach(instanceA ->
    myMap.compute(Key.key(instanceA), (k,b)-> {
        if(b==null) b=new ClassB(instanceA);
        else b.appendFieldA(instanceA.getFieldA());
        return b;
    })
);

For a stream solution, there is the problem, that a merge function will get two instances of the same type, here ClassB, and can’t access the ClassA instance via the surrounding context like we did with the compute solution above. For a stream solution, we need a method in ClassB which returns that first ClassA instance, which we passed to its constructor, say getFirstInstanceA(). Then we can use:

Map<Key, ClassB> myMap = classAList.stream()
    .collect(Collectors.toMap(Key::key, ClassB::new, (b1,b2)->{
        b1.appendFieldA(b2.getFirstInstanceA().getFieldA());
        return b1;
    }));

Upvotes: 3

matt.early
matt.early

Reputation: 225

You can group the entries into a map that maps the hashed key to the list of elements and then call map again to convert that map into the set you are after. Something like this:

List classAList = new ArrayList<>();
classAList.stream()
.collect(Collectors.groupingBy(instanceA -> ClassB.key(instanceB)))
.entrySet()
.map(entry -> entry.getValue().stream()
   .map(instanceA -> new ClassB(instanceA))
   .reduce(null, (a,b) -> a.appendFieldA(b)))
.collect(Collectors.toSet());

Upvotes: 1

Related Questions