4

We are using 3 lists ListA,ListB,ListC to keep the marks for 10 students in 3 subjects (A,B,C).

Subject B and C are optional, so only few students out of 10 have marks in those subjects

Class Student{
String studentName;
int marks;
}

ListA has records for 10 students, ListB for 5 and ListC for 3 (which is also the size of the lists)

Want to know how we can sum up the marks of the students for their subjects using java 8 steam.

I tried the following

List<Integer> list = IntStream.range(0,listA.size() -1).mapToObj(i -> listA.get(i).getMarks() + 
listB.get(i).getMarks() + 
listC.get(i).getMarks()).collect(Collectors.toList());;

There are 2 issues with this

a) It will give IndexOutOfBoundsException as listB and listC don't have 10 elements

b) The returned list if of type Integer and I want it to be of type Student.

Any inputs will be very helpful

cooldev
  • 497
  • 1
  • 3
  • 17
  • So, a Subject is, in fact, a Student, with its mark in one subject, right? The name is the name of the student, right? – JB Nizet May 06 '17 at 07:45
  • I think you need zipping for that, frankly zipping isn't in the standard library !! see here for how to do it : http://stackoverflow.com/questions/17640754/zipping-streams-using-jdk8-with-lambda-java-util-stream-streams-zip – niceman May 06 '17 at 08:02
  • Yes, it is a Student..i have corrected the details. Thanks – cooldev May 06 '17 at 08:04

2 Answers2

6

You can make a stream of the 3 lists and then call flatMap to put all the lists' elements into a single stream. That stream will contain one element per student per mark, so you will have to aggregate the result by student name. Something along the lines of:

Map<String, Integer> studentMap = Stream.of(listA, listB, listC)
        .flatMap(Collection::stream)
        .collect(groupingBy(student -> student.name, summingInt(student -> student.mark)));

Alternatively, if your Student class has getters for its fields, you can change the last line to make it more readable:

Map<String, Integer> studentMap = Stream.of(listA, listB, listC)
        .flatMap(Collection::stream)
        .collect(groupingBy(Student::getName, summingInt(Student::getMark)));

Then check the result by printing out the studentMap:

studentMap.forEach((key, value) -> System.out.println(key + " - " + value));

If you want to create a list of Student objects instead, you can use the result of the first map and create a new stream from its entries (this particular example assumes your Student class has an all-args constructor so you can one-line it):

List<Student> studentList = Stream.of(listA, listB, listC)
        .flatMap(Collection::stream)
        .collect(groupingBy(Student::getName, summingInt(Student::getMark)))
        .entrySet().stream()
            .map(mapEntry -> new Student(mapEntry.getKey(), mapEntry.getValue()))
            .collect(toList());
Thomas Kåsene
  • 5,301
  • 3
  • 18
  • 30
  • Good answer. The list of lists approach is flexible in that it can accommodate any number of mandatory and optional subjects. If `listA`, `listB` and `listC` are hardcoded, one may alternatively use two invocations of `Stream.concat()` for concatenating three streams into one. – Ole V.V. May 06 '17 at 09:26
  • 4
    @OleV.V. Stream.of(T... values) is any other option for 3 or more lists. `Stream.of(listA, listb, listc).flatMap(Collection::stream). ...` – Ömer Erden May 06 '17 at 09:39
  • Instead of returning Map how can i return Map where Student object . mark will have the summed mark? – cooldev May 06 '17 at 10:00
  • As far as I know, once you call `collect` on a stream, the stream is finished, so you can't do this all in one stream. You can, however, chain the calls together. I'll update my answer. – Thomas Kåsene May 06 '17 at 10:07
  • 1
    I updated the answer to incorporate the suggestion from @ÖmerErden and also to include a possible solution for returning a `List`. You can easily change this to a `Map` by replacing the last line with `collect(toMap(Student::getName, student -> student))` if that's more useful to you. – Thomas Kåsene May 06 '17 at 12:00
1

I would do it as follows:

Map<String, Student> result = Stream.of(listA, listB, listC)
    .flatMap(List::stream)
    .collect(Collectors.toMap(
        Student::getName, // key: student's name
        s -> new Student(s.getName(), s.getMarks()), // value: new Student
        (s1, s2) -> { // merge students with same name: sum marks
            s1.setMarks(s1.getMarks() + s2.getMarks());
            return s1;
        }));

Here I've used Collectors.toMap to create the map (I've also assumed you have a constructor for Student that receives a name and marks).

This version of Collectors.toMap expects three arguments:

  1. A function that returns the key for each element (here it's Student::getName)
  2. A function that returns the value for each element (I've created a new Student instance that is a copy of the original element, this is to not modify instances from the original stream)
  3. A merge function that is to be used when there are elements that have the same key, i.e. for students with the same name (I've summed the marks here).

If you could add the following copy constructor and method to your Student class:

public Student(Student another) {
    this.name = another.name;
    this.marks = another.marks;
}

public Student merge(Student another) {
    this.marks += another.marks;
    return this;
}

Then you could rewrite the code above in this way:

Map<String, Student> result = Stream.of(listA, listB, listC)
    .flatMap(List::stream)
    .collect(Collectors.toMap(
        Student::getName,
        Student::new,
        Student::merge));
fps
  • 33,623
  • 8
  • 55
  • 110
  • 1
    I agree, this is a more efficient way to do it than my solution. I recommend marking this as accepted answer, @girish . – Thomas Kåsene May 07 '17 at 18:19
  • There are pros and cons. I don’t like you modifying the objects from the original lists. The asker will have to weigh this against the advantages. – Ole V.V. May 08 '17 at 05:29
  • @OleV.V. In the first snippet the original objects are not modified – fps May 08 '17 at 10:19
  • 1
    @FedericoPeraltaSchaffner, I believe the call `s1.setMarks(s1.getMarks() + s2.getMarks())` modifies the `Student` object `s1`, and that this object comes from the original stream. Please correct me if I’m wrong. – Ole V.V. May 08 '17 at 10:22
  • @OleV.V. Now I get what you mean. I will update the answer to fix this, thank you very much for pointing this out. – fps May 08 '17 at 10:27
  • 1
    @OleV.V. Updated the answer as per your comment, thanks again – fps May 08 '17 at 12:40