Search code examples
javaforeachjava-8stream

Java replace enhanced for loop with stream accessing elements from each stream


first time question so bare with me..

I have some objects which include nested lists. I'd typically use nested for loops to carry out any transformation on these, but I'm keen to explore Java8 Streams. Essentially I'm trying to create an output object which will be composed of fields accessed within each nested list.

I've shared a really simple example below, and how I would typically carry this out with enhanced for loops. Could anyone share with me how I should be doing this will streams? Also, if we were to assume that these some of these lists will have a cardinality of [0:M], i.e. they are optional lists, how would I make the stream null-safe? Thanks

    // Objects below
    class Qualification {
        String qualificationName;
        String qualificationValue;
    }

    class Person {
        String name;
        List<Qualification> qualifications;
    }

class Group {
        String groupId;
        List<Person> people;
    }

    class Output {
        String groupId;
        String name;
        String qualificationName;
        String qualificationValue;
    }

Below is how I would typically carry out this transformation.

// Create empty list to hold the output objects    
List<Output> outputList = new ArrayList<Output>();
        for(Group g : groups) {

            for (Person p : g.getPeople()) {

                for (Qualification q : p.getQualifications()) {
                   // Compose new object and add to list

                    Output output = new Output();
                    output.setId(g.getId());
                    output.setName(p.getName());
                    output.setQualificationValue(q.getQualificationValue());
                    output.setQualificationName(q.getQualificationName());
                            
                    outputList.add(output);
                }
            }
        }

Solution

  • You will have to use

    • filter() to take care of the null lists, AND
    • flatMap() to create substreams from internal lists

    (I have used Lombok annotations; you may use regular getters/setters and constructors.)

    public class NestedForLoopsIntoStreams{
        @NoArgsConstructor @AllArgsConstructor( staticName = "of" )
        @Getter @Setter
        private static class Qualification {
            String qualificationName;
            String qualificationValue;
        }
    
        @NoArgsConstructor @AllArgsConstructor( staticName = "of" )
        @Getter @Setter
        private static class Person {
            String name;
            List<Qualification> qualifications;
        }
    
        @NoArgsConstructor @AllArgsConstructor( staticName = "of" )
        @Getter @Setter
        private static class Group {
            String groupId;
            List<Person> people;
        }
    
        @NoArgsConstructor @AllArgsConstructor( staticName = "of" )
        @Getter @Setter @ToString
        private static class Output {
            String groupId;
            String name;
            String qualificationName;
            String qualificationValue;
        }
        
        public static void main( String[] args ){
            List<Group> groups = testData();
            List<Output> outputs = 
            groups.stream().filter( g -> g.getPeople() != null )
                .flatMap( g -> {
                    return g.getPeople().stream().filter( p -> p.getQualifications() != null )
                        .flatMap( p -> {
                            return p.getQualifications().stream().map( q ->
                                Output.of( g.groupId, p.name, q.qualificationName, q.qualificationValue )
                            );
                        }
                );
            } ).collect( Collectors.toList() );
            
            outputs.forEach( System.out::println );
        }
    
        private static List<Group> testData(){
            return Arrays.asList(
                Group.of( "1", Arrays.asList( 
                    Person.of( "Person1", Arrays.asList( Qualification.of( "Engg", "BTech" ) ) ),
                    Person.of( "Person2", Arrays.asList( Qualification.of( "Engg", "MTech" ) ) )
                ) ),
                Group.of( "2", Arrays.asList( 
                    Person.of( "Person3", Arrays.asList( Qualification.of( "Engg", "BTech" ) ) )
                ) ),
                /* Person with no qualication */
                Group.of( "3", Arrays.asList(
                    Person.of( "Person4", Arrays.asList( Qualification.of( "Engg", "BTech" ) ) ),
                    Person.of( "Person5", null )
                ) ),
                /* No people */
                Group.of( "4", null ) 
            );
        }
    }
    

    Running this will give this output:

    NestedForLoopsIntoStreams.Output(groupId=1, name=Person1, qualificationName=Engg, qualificationValue=BTech)
    NestedForLoopsIntoStreams.Output(groupId=1, name=Person2, qualificationName=Engg, qualificationValue=MTech)
    NestedForLoopsIntoStreams.Output(groupId=2, name=Person3, qualificationName=Engg, qualificationValue=BTech)
    NestedForLoopsIntoStreams.Output(groupId=3, name=Person4, qualificationName=Engg, qualificationValue=BTech)