0

I've a List of Foo, where on each Foo I apply a processor method to get ValidItem.
If there is an error in processing, then I returned ErrorItem.

Now How to process this by Java 8 streams to get the result in form of 2 different lists

List<Foo> FooList = someList....;

class ValidItem extend Item{......}
class ErrorItem extend Item{......}


Item processItem(Foo  foo){
   return either an object of ValidItem or ErrorItem;
}

I believe I can do this

 Map<Class,List<Item>> itemsMap =
    FooList
    .stream()
    .map(processItem)
    .collect(Collectors.groupingBy(Object::getClass));

But as List<Parent> IS NOT a List<Child> so I can't typecast the map result into List<ValidItem> In reality ErrorItem and ValidItem are two completely different class not related at all, just for the sake of this steam processing and processItem method I kept them in same hierarchy by extending a marker Item class,.

and in many other Places in code, I cant/shouldn't refer ValidItem as Item , as It give an idea that it can be an ErroItem too.

Is there a proper way of doing it with streams, where at the end I get 2 lists. and ErrorItem and ValidItem are not extending same Item class ?

############## Update ##############
As I said ValidItem and ErrorItem shouldn't be same, so I changed the signature of process method and passed it a list. I know this is not how Stream shold be used. Let me know if you have better way

    List<Foo> FooList = someList....;

    class ValidItem {......}
    class InvalidFoo{......}


    ValidItem processFoo(Foo  foo, List<InvalidFoo> foolist){
      Do some processing on foo.
       either return  new ValidItem ();
         OR 
         fooList.add(new InvalidFoo()) , and then return null;
    }

List<InvalidFoo> invalidFooList = new ArrayList();
     List<ValidItem> validItem =
        fooList
        .stream()
        .map(e->processItem(e,invalidFooList))
        .filter(Objects::notNull)
        .collect(Collectors.toList());

now I have both invalid and valid list, but this doesn't look like a clean stream code.

ThrowableException
  • 1,168
  • 1
  • 8
  • 29
  • 3
    you shouldn't base your entity design around stream processing, rather use streams on elements as designed. that said, with a type attribute in both the classes to distinguish them, you can then group by that attribute and later cast to specific instance. – Naman Jun 05 '20 at 03:39
  • Agree with your design comment. But these aren't business entities. Just for the sake of question, I posted this.... basically, I'm doing file processing. These are processing result items for each line.....based upon resulting `List` I will create entity to persist and will send `List` back to UI....... I also felt that I shouldn't keep them in same object hierarchy, thus asked question. Suggestion you provided is not solving the problem I asked, I don't have problem in grouping them. My problem is How to have list of 2 different classes returned by streams collect – ThrowableException Jun 05 '20 at 04:09
  • You're reading a file and from each line of the file you create either an `ErrorItem` or a `ValidItem` and you want to create two separate lists, namely `List` and `LIst`. Is that correct? Is the file a text file? – Abra Jun 05 '20 at 04:15
  • @Arba Its CSV , but Yeah, It can be considered text too... Thing is, `ErrorItem` is not at all similar to `ValidItem`... In `ErrorItem` I'm just putting the `errorMessage`, `errorReason`, `fieldname` (if its a field error) and complete `originalline`, so user can refer, Whereas `ValidItem` is not exactly but very close to my Business Entity. – ThrowableException Jun 05 '20 at 04:19
  • Can you [edit] your question and post some sample lines from your CSV including valid lines and error lines and indicate what results you require from such sample data? – Abra Jun 05 '20 at 04:20
  • @Abra, thats actually not the problem statement, You can consider the problem in as its defined, I'm only looking for stream based solution where I need to return list of 2 different Classes by one processing(map) method. – ThrowableException Jun 05 '20 at 04:24

3 Answers3

5

With recent Java versions, you can use

  • for the Item processItem(Foo foo) method returning either ValidItem or ErrorItem:

    Map.Entry<List<ValidItem>, List<ErrorItem>> collected = fooList.stream()
      .map(this::processItem)
      .collect(teeing(
        flatMapping(x -> x instanceof ValidItem? Stream.of((ValidItem)x):null, toList()),
        flatMapping(x -> x instanceof ErrorItem? Stream.of((ErrorItem)x):null, toList()),
        AbstractMap.SimpleImmutableEntry::new
      ));
    
    List<ValidItem> valid = collected.getKey();
    List<ErrorItem> invalid = collected.getValue();
    
  • for the ValidItem processFoo(Foo foo, List<InvalidFoo> foolist):

    Map.Entry<List<ValidItem>, List<InvalidFoo>> collected = fooList.stream()
        .map(foo -> {
            List<InvalidFoo> invalid = new ArrayList<>(1);
            ValidItem vi = processFoo(foo, invalid);
            return new AbstractMap.SimpleImmutableEntry<>(
                vi == null? Collections.<ValidItem>emptyList():
                            Collections.singletonList(vi),
                invalid);
        })
        .collect(teeing(
            flatMapping(e -> e.getKey().stream(), toList()),
            flatMapping(e -> e.getValue().stream(), toList()),
            AbstractMap.SimpleImmutableEntry::new
        ));
    
    List<ValidItem> valid = collected.getKey();
    List<InvalidFoo> invalid = collected.getValue();
    

The flatMapping collector has been introduced in Java 9.
In this specific case, instead of

flatMapping(x -> x instanceof ValidItem? Stream.of((ValidItem)x): null, toList())

you can also use

filtering(x -> x instanceof ValidItem, mapping(x -> (ValidItem)x, toList()))

but each variant requires Java 9, as filtering also does not exist in Java 8. The teeing collector even requires Java 12.

However, these collectors are not hard to implement.

This answer contains a Java 8 compatible version of the flatMapping collector at the end. If you want to use the alternative with filtering and mapping, you can find a Java 8 compatible version of filtering in this answer. Finally, this answer contains a Java 8 compatible variant of the teeing collector.

When you add these collectors to your codebase, the solutions at the beginning of this answer work under Java 8 and will being easily adaptable to future versions. Assuming an import static java.util.stream.Collectors.*; in your source file, you only have to remove the backports of these methods, to switch to the standard JDK versions.


It would be better if processItem returned an Either or Pair type instead of the two variants addressed above. If you don’t want to use 3rd party libraries, you can use a Map.Entry instance as a “poor man’s pair type”.

Having a method signature like

/** Returns an entry with either, key or value, being {@code null} */
Map.Entry<ValidItem,InvalidFoo> processItem(Foo foo){

which could be implemented by returning an instance of AbstractMap.SimpleImmutableEntry,

a JDK 9+ solution could look like

Map.Entry<List<ValidItem>, List<InvalidFoo>> collected = fooList.stream()
    .map(this::processItem)
    .collect(teeing(
        flatMapping(e -> Stream.ofNullable(e.getKey()), toList()),
        flatMapping(e -> Stream.ofNullable(e.getValue()), toList()),
        AbstractMap.SimpleImmutableEntry::new
    ));

and Java 8 compatible (when using the linked collector backports) versions:

Map.Entry<List<ValidItem>, List<InvalidFoo>> collected = fooList.stream()
    .map(this::processItem)
    .collect(teeing(
        flatMapping(e -> Stream.of(e.getKey()).filter(Objects::nonNull), toList()),
        flatMapping(e -> Stream.of(e.getValue()).filter(Objects::nonNull), toList()),
        AbstractMap.SimpleImmutableEntry::new
    ));

or

Map.Entry<List<ValidItem>, List<InvalidFoo>> collected = fooList.stream()
    .map(this::processItem)
    .collect(teeing(
        mapping(Map.Entry::getKey, filtering(Objects::nonNull, toList())),
        mapping(Map.Entry::getValue, filtering(Objects::nonNull, toList())),
        AbstractMap.SimpleImmutableEntry::new
    ));
Holger
  • 285,553
  • 42
  • 434
  • 765
  • Just an opinion, for someone on Java-12+, `Map.Entry, List> collected` could not be as important and they can make use of `var collected`. – Naman May 20 '21 at 17:42
  • 1
    @Naman I’m not sure whether the result type of the `collect` operation is so obvious to the reader that writing `var` instead would be an improvement in this specific case. I’m not even sure whether type inference would do its job then. Maybe a recent version using a `record` would improve this, but I’d wait for the deconstruction language feature, to directly get two variables of type `List`, before making a change… – Holger May 21 '21 at 07:19
2
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<Foo> FooList = new ArrayList<>();
        for(int i = 0 ; i < 100; i++){
            FooList.add(new Foo(i+""));
        }
        Map<Class,List<Item>> itemsMap =
                FooList
                        .stream()
                        .map(Main::processItem)
                        .collect(Collectors.groupingBy(Object::getClass));
        List<ValidItem> validItems = itemsMap.get(ValidItem.class).stream().map((o -> (ValidItem)o)).collect(Collectors.toList());



    }
    public static Item processItem(Foo  foo){
        Random random = new Random(System.currentTimeMillis());
        if(Integer.parseInt(foo.name) % 2== 0){
            return new ValidItem(foo.name);
        }else{
            return new ErrorItem(foo.name);
        }
    }
    static class ValidItem extends Item{
        public ValidItem(String name) {
            super("valid: " + name);
        }
    }
    static class ErrorItem extends Item{
        public ErrorItem(String name) {
            super("error: "+name);
        }
    }
    public static class Item {
        private String name;

        public Item(String name) {
            this.name = name;
        }
    }

}

I suggest this solution.

  • 1
    Wont work, compilation error, `Cannot cast from Item to ValidItem` error at second line – ThrowableException Jun 05 '20 at 04:16
  • Please see the updated answer. I uploaded my whole application. If you cannot cast, u initiate Item class not ValidItem or ErrorItem – user3562932 Jun 05 '20 at 04:54
  • You are missing Foo definition. but i checked. your code is compiles..I upvoted but not accepted as answer, as if you read my last 2 lines (now those are lines before update) i asked something else, i said that i did this hierarchy, but this was not right... and i was looking for that solution....but great effort. i upvoted your answer...marked as useful.. – ThrowableException Jun 05 '20 at 05:16
0

You can use Vavr library.

final List<String> list = Arrays.asList("1", ",", "1", "0");
final List<Either<ErrorItem, ValidItem>> eithers = list.stream()
                .map(MainClass::processData)
                .collect(Collectors.toList());
final List<ValidItem> validItems = eithers.stream()
                .filter(Either::isRight)
                .map(Either::get)
                .collect(Collectors.toList());
final List<ErrorItem> errorItems = eithers.stream()
                .filter(Either::isLeft)
                .map(Either::getLeft)
                .collect(Collectors.toList());

...

private static Either<ErrorItem,ValidItem> processData(String data){
        if(data.equals("1")){
            return Either.right(new ValidItem());
        }
        return Either.left(new ErrorItem());
}
Alex
  • 706
  • 7
  • 16