Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java streams: Add to map but avoid mutation

I often find myself in a situation where I need to create a Map of objects from a Set or List. The key is usually some String or Enum or the like, and the value is some new object with data lumped together. The usual way of doing this, for my part, is by first creating the Map<String, SomeKeyValueObject> and then iterating over the Set or List I get in and mutate my newly created map.

Like the following example:

class Example {
   Map<String, GroupedDataObject> groupData(final List<SomeData> list){
      final Map<String, GroupedDataObject> map = new HashMap<>();
      for(final SomeData data : list){
         final String key = data.valueToGroupBy();
         map.put(key, GroupedDataObject.of(map.get(key), data.displayName(), data.data()));
      }
      return map;
   }

}

class SomeData {
   private final String valueToGroupBy;
   private final Object data;
   private final String displayName;

   public SomeData(final String valueToGroupBy, final String displayName, final Object data) {
      this.valueToGroupBy = valueToGroupBy;
      this.data = data;
      this.displayName = displayName;
   }

   public String valueToGroupBy() {
      return valueToGroupBy;
   }

   public Object data() {
      return data;
   }

   public String displayName() {
      return displayName;
   }
}

class GroupedDataObject{

   private final String key;
   private final List<Object> datas;

   private GroupedDataObject(final String key, final List<Object> list) {
      this.key = key;
      this.datas = list;
   }

   public static GroupedDataObject of(final GroupedDataObject groupedDataObject, final String key, final Object data) {
      final List<Object> list = new ArrayList<>();
      if(groupedDataObject != null){
         list.addAll(groupedDataObject.datas());
      }
      list.add(data);
      return new GroupedDataObject(key, list);
   }

   public String key() {
      return key;
   }

   public List<Object> datas() {
      return datas;
   }
}

This feels very unclean. We create a map, and then mutate it over and over.

I've taken a liking to java 8s use of Streams and creating non-mutating data structures (or rather, you don't see the mutation). So is there a way to turn this grouping of data into something that uses a declarative approach rather than the imperative way?

I tried to implement the suggestion in https://stackoverflow.com/a/34453814/3478016 but I seem to be stumbling. Using the approach in the answer (the suggestion of using Collectors.groupingBy and Collectors.mapping) I'm able to get the data sorted into a map. But I can't group the "datas" into one and the same object.

Is there some way to do it in a declarative way, or am I stuck with the imperative?

like image 531
qwelyt Avatar asked Dec 07 '17 08:12

qwelyt


People also ask

Does Stream mutate Java?

Java streams are not a data structure and cannot mutate data; they can only transform data. You can often create a stream from collections to apply a number of functions on a data structure, but a stream itself is not a data structure.

Can we use Stream with map in Java?

Converting only the Value of the Map<Key, Value> into Stream: This can be done with the help of Map. values() method which returns a Set view of the values contained in this map. In Java 8, this returned set can be easily converted into a Stream of key-value pairs using Set. stream() method.

Does Java Stream map preserve order?

If our Stream is ordered, it doesn't matter whether our data is being processed sequentially or in parallel; the implementation will maintain the encounter order of the Stream.

What is the purpose of the map () method in the Stream T interface?

The map() function is a method in the Stream class that represents a functional programming concept. In simple words, the map() is used to transform one object into another by applying a function. That's the reason the Stream.

What is stream map () in Java?

Introduction Stream map () is an intermediate operation used to apply one given function to the elements of a stream. It takes one function as its argument and applies it to each value of the stream and returns one fresh stream. In this tutorial, we will learn how to use the Java Streams map function with different examples.

What is the use of mutation in Java?

Mutation is changing an object and is one common side effect in programming languages. A method that has a functional contract will always return the same value to the same arguments and have no other side effects (like storing file, printing, reading).

How to create a map from a list in Java?

A Map is created, when you collect a stream of elements using either Collectors.toMap () or Collectors.groupingBy (). Let’s stream the List and collect it to a Map using Collectors.toMap (keyMapper, valueMapper) where key is unique id of user and value is name of the user which may duplicate:-

How to handle duplicate keys in Java 8 streams?

Java 8 Streams provide Collectors.toMap (keyMapper, valueMapper, mergeFunction) overloaded method where you can specify which value to consider when duplicate key issue occur. Let’s collect a Map having user name as a key, merge function indicate that keep the old value for the same key:-


Video Answer


3 Answers

You can use Collectors.toMap with a merge function instead of Collectors.groupingBy.

Map<String, GroupedDataObject> map =
    list.stream()
        .collect(Collectors.toMap(SomeData::valueToGroupBy,
                                  d -> {
                                     List<Object> l = new ArrayList<>();
                                     l.add(d.data());
                                     return new GroupedDataObject(d.valueToGroupBy(), l);
                                  },
                                  (g1,g2) -> {
                                      g1.datas().addAll(g2.datas());
                                      return g1;
                                  }));

The GroupedDataObject constructor must be made accessible in order for this to work.

like image 126
Eran Avatar answered Oct 09 '22 03:10

Eran


If you avoid the GroupedDataObject and simply want a map with a key and a list you can use Collectors.groupingBy that you have been looking into.

Collectors.groupingBy will allow you to do this:

List<SomeObject> list = getSomeList();

Map<SomeKey, List<SomeObject>> = list.stream().collect(Collectors.groupingBy(SomeObject::getKeyMethod));

This will require SomeKey to have proper implementations of equals and hashValue

like image 25
Mathjoh Avatar answered Oct 09 '22 04:10

Mathjoh


Sometimes streams are not the way to go. I believe this is one of those times.

A little refactoring using merge() gives you:

Map<String, MyTuple> groupData(final List<SomeData> list) {
    Map<String, MyTuple> map = new HashMap<>();
    list.forEach(d -> map.merge(d.valueToGroupBy(), new MyTuple(data.displayName(), data.data()), 
      (a, b) -> {a.addAll(b.getDatas()); return a;});

Assuming a reasonable class to hold your stuff:

class MyTuple {
    String displayName;
    List<Object> datas = new ArrayList<>();
    // getters plus constructor that takes 1 data and adds it to list
}
like image 2
Bohemian Avatar answered Oct 09 '22 04:10

Bohemian