4

My basic class is:

public class Student {
  public String name;
  public String className; // In real code I'd have a second object for return to the end user
  public List<String> classes; // Can be zero
}

I'd like to flatten it out so I can return something like

[
  {
    "name":"joe",
    "class":"science"
  },
  {
    "name":"joe",
    "class":"math"
  },
]

Obviously a stupid example for the sake of simplicity.

The only way I've been able to do it is through some long winded code like:

List<Student> students = getStudents();
List<Student> outputStudents = new ArrayList<>();
students.forEach(student -> {
  if(student.getClasses().size() > 0) {
    student.getClasses().forEach(clazz -> {
      outputStudents.add(new Student(student.getName(), clazz));
    });
  } else {
    outputStudents.add(student);
  }
});

Looking if there is a way to simplify this, maybe using flapMap?

canpan14
  • 1,181
  • 1
  • 14
  • 36
  • 1
    Are you asking how to build JSON? – PM 77-1 Jan 17 '19 at 20:34
  • Also `class` is a keyword, so you can't use it in a lambda like that – GBlodgett Jan 17 '19 at 20:35
  • This was sudo code so i got a little lasy. I'm using lombok for getters/setters. And hateoas to assemble resources and the such. – canpan14 Jan 17 '19 at 20:38
  • Oh misread the question. Hateoas/HttpEntity will deal with object -> json for me. So I just need to assemble the object in the correct way. And like all fun things, I have to meet the needs to another system for how I'm sending it. – canpan14 Jan 17 '19 at 20:43
  • You have three properties in this code, but two in the shown output. It's not quite clear what you want. Where did `className` differ from `classes`? – OneCricketeer Jan 17 '19 at 20:43
  • You could use the Jackson library to convert your [JAVA objects to JSON](https://stackoverflow.com/questions/15786129/converting-java-objects-to-json-with-jackson). – Harke Jan 17 '19 at 20:46
  • @cricket_007 For the output the list doesn't matter, whatever I'm returning this to would just look for the name/className combo and build the response object from those. – canpan14 Jan 17 '19 at 20:50

2 Answers2

2

One way to do that could be to partition the current list of Students based on the conditions if the classes within them are empty or not

Map<Boolean, List<Student>> conditionalPartitioning = students.stream()
        .collect(Collectors.partitioningBy(student -> student.getClasses().isEmpty(), Collectors.toList()));

then further use this partitioning to flatMap to a list of new students given those have classes within them as and concatenate them with the other partition to finally collect into the result as :

List<Student> result = Stream.concat(
        conditionalPartitioning.get(Boolean.FALSE).stream() // classes as a list
                .flatMap(student -> student.getClasses() // flatmap based on each class
                        .stream().map(clazz -> new Student(student.getName(), clazz))),
        conditionalPartitioning.get(Boolean.TRUE).stream()) // with classes.size = 0
        .collect(Collectors.toList());
Naman
  • 27,789
  • 26
  • 218
  • 353
  • 1
    That conditional partitioning is one of the cooler things I've seen! I wish I could accept both answers as they both solve the problem in different ways. – canpan14 Jan 18 '19 at 16:18
  • 2
    @canpan14 nevermind, answers are not just for the person asking it upfront, this(answer) would hopefully help others landing to the question link as well. In the end, the consumer can evaluate what they would benefit from if there are multiple approaches handed over to them. :) – Naman Jan 18 '19 at 16:20
1

Yes, you should be able to do something like this:

Student student = ?
List<Student> output = 
    student
        .getClasses()
        .stream()
        .map(clazz -> new Student(student.getName, student.getClassName, clazz))
        .collect(Collectors.toList());

For a single student. For a collection of students it's a little more complicated:

(Updated due to observation in comment from @nullpointer. Thanks!)

List<Student> listOfStudents = getStudents();
List<Student> outputStudents =
    listOfStudents
        .stream()
        .flatMap(student -> {
            List<String> classes = student.getClasses();
            if (classes.isEmpty()) return ImmutableList.of(student).stream();
            return classes.stream().map(clazz -> new Student(student.getName(), student.getClassName(), ImmutableList.of(clazz)));
        })
        .collect(Collectors.toList());
jwismar
  • 12,164
  • 3
  • 32
  • 44
  • Yeah I can handle null checking at least =) Also your code worked for me. Thanks! I'll probably do a performance comparison to see which way comes out on top for fun – canpan14 Jan 17 '19 at 20:58
  • 1
    If a `student` doesn't have `classes` (size =0), this would not map that student in the output list as the code in question does. @jwismar ... Also, the constructor used in the answer doesn't match the actual class attributes, worth sharing if it has some custom logic around it. – Naman Jan 18 '19 at 01:05
  • 1
    That's an excellent point. Thanks for pointing it out! Updated. – jwismar Jan 18 '19 at 14:51