Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use the streaming api to loop over fields instead of over objects

I want to use Java 8's stream api for the following exercise:

  • Given a List<Person> persons
  • Person is a simple POJO with a name (string) and age (int) property
  • Construct the following String Names=[joe,diana,thomas]&Ages=[34,52,19]

What I can easily do with streams is the following:

String result = persons.stream()
    .map(p -> "(" + p.getName() + "," + p.getAge() + ")")
    .collectors(Collectors.joining("&","persons=[","]");

Which will give persons=[(joe,34)&(diana,52)&(thomas,19)], but I don't know how I can concatenate on the fields across mutiple person objects rather then on the properties of each person individually. It's like I need to flatmap the person to one property, and afterwards flatmap the list of persons a second time for the next property, and then combine these outputs. What would the syntax be for this?

The Person class is very simple. If possible, I would like a solution that is written in such a way that reflection is used to loop over ALL fields of a class instead of checking hardcoded only the "name" and "age" fields. Assume that all fields are simple properties like String or int (there are no nested classes or collections).

like image 543
user1884155 Avatar asked Sep 13 '25 01:09

user1884155


2 Answers

I guess having a class Person (as you describe), you would need a custom collector for this:

 String result = List.of(new Person(1, "david"), new Person(33, "eugene"))
            .stream()
            .parallel()
            .collect(Collector.of(
                    () -> new StringJoiner[]{new StringJoiner(",", "[", "]"), new StringJoiner(",", "[", "]")},
                    (sj, per) -> {
                        sj[0] = sj[0].add("" + per.getAge());
                        sj[1] = sj[1].add(per.getName());
                    },
                    (left, right) -> {
                        left[0] = left[0].merge(right[0]);
                        left[1] = left[1].merge(right[1]);
                        return left;
                    },

                    x -> "Names = " + x[1].toString() + "&Ages = " + x[0].toString()
            ));
like image 57
Eugene Avatar answered Sep 14 '25 15:09

Eugene


You could use

String result = persons.stream()
    .flatMap(p -> Stream.of(new AbstractMap.SimpleEntry<>(true, p.getName()),
                            new AbstractMap.SimpleEntry<>(false, ""+p.getAge())))
    .collect(Collectors.collectingAndThen(
        Collectors.partitioningBy(Map.Entry::getKey,
            Collectors.mapping(Map.Entry::getValue, Collectors.joining(",", "[", "]"))),
        m -> "Names="+m.get(true)+"&Ages="+m.get(false)));

AbstractMap.SimpleEntry is just a stand-in for the missing standard Pair type. If you have a Pair type in your project, you can use it instead; you just have to replace the Map.Entry::getKey and Map.Entry::getValue method references with the right ones to retrieve the first resp. second value of the pair.

The solution above is tailored to the case of two properties. An alternative which can be easily adapted to more than two properties would be

String result = persons.stream()
    .flatMap(p -> Stream.of(new AbstractMap.SimpleEntry<>("Names", p.getName()),
                            new AbstractMap.SimpleEntry<>("Ages", ""+p.getAge())))
    .collect(Collectors.collectingAndThen(
        Collectors.groupingBy(Map.Entry::getKey,
            Collectors.mapping(Map.Entry::getValue, Collectors.joining(",", "[", "]"))),
        m -> m.entrySet().stream()
              .map(e -> e.getKey()+"="+e.getValue()).collect(Collectors.joining("&"))));
like image 43
Holger Avatar answered Sep 14 '25 14:09

Holger