1

I'm starting with Java streams and during the lecture of effective programming book i found this piece of code:

    public enum Phase {
  SOLID, LIQUID, GAS;

  public enum Transition {
    MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
    BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
    SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);

    private final Phase from;
    private final Phase to;

    Transition(Phase from, Phase to) {
        this.from = from;
        this.to = to;
    }
    private static final Map<Phase, Map<Phase, Transition>>
        m = Stream.of(values()).collect(groupingBy(t -> t.from,
            () -> new EnumMap<>(Phase.class),
            toMap(t -> t.to, t -> t,
                (x, y) -> y, () -> new EnumMap<>(Phase.class))));

    public static Transition from(Phase from, Phase to) {
        return m.get(from).get(to);
    }
  }
}

And I cannot understand how this EnumMap is created. The most difficult to understand for me is :

toMap(t -> t.to, t -> t,
                (x, y) -> y, () -> new EnumMap<>(Phase.class))));

Does anybody could explain it to me ?

1
  • 3
    This is a great example how you can use streams to turn your trivial code into unreadable mess. Although the functional programmers in the crowd might feel a slight tingle in their pants (not that they would care about Java's limited tools in that aspect). Commented Apr 9, 2020 at 17:57

2 Answers 2

1

What the code do: You have a stream of elements, and want to group them by initial phase. Here the author chose to use a complex group by operation, which will also perform post-transformation of groupped data. This is where toMap function comes in. Once groupingBy collector starts buffering elements, it delegates to mapTo collector to build custom records for each group. Here, the author uses toMap to index phases by their target state: to.

So, let's decompose it :

Stream.of(values())
   // Queries a result
   .collect(
       // Elements must be grouped using a key, extracted from each streamed value
       groupingBy(
           // Specify how to extract the grouping key
           t -> t.from,
            // Here, the author specifies himself how to create the group map,
            // to be sure it will use a custom enum-optimized key management.
            () -> new EnumMap<>(Phase.class),
            // Nest another collector, to transform the list of values 
            // of each group into something else. Note that toMap does not 
            // buffer a collection of values for a key. It is designed to 
            // map a key to each value. But, it allows user to merge  
            // merge conflicting values if needed.
            toMap(
                // Same than groupby, we specify how to extract key from value. 
                t -> t.to, 
                // Here, specify that the value associated to the key is the 
                // object we've extracted key from. Here, wwe could have changed
                // it to something else (a text representation, for example).
                t -> t,
                // toMap will bind one key to one value. We have to solve 
                // conflict ourself. Here, author decided to drop arbitrarily 
                // one of the values.
                (x, y) -> y,
                // As for groupby operator, author forces a map implementation 
                // dedicated to enum keys.
                () -> new EnumMap<>(Phase.class))));

Finally, official stream collection documentation provide quite some explanation.

I'm not sure if my explanation is enough, you can give remarks in comments.

Sign up to request clarification or add additional context in comments.

2 Comments

So example: FREEZE(LIQUID, SOLID). t describe this object. Outer key map will be in this case LIQUID and inner key will be SOLID. t -> t means that we don't transform our object and leave as it is. (x,y) -> y means that we have values for outer and inner key and we have to choose one value, so we decided to choose value from inner map. Am I right or I understand it wrong ?
You're almost all right. The only slight difference is about (x, y) -> y. This lambda will be used only if toMap detects a conflict. That means that two Transitions in the same group (therefore with the same from phase), also have the same target phase to. In this particular example, I think it will never be used. However, the collection API ask user to give nonetheless, because it does not know dataset beforehand, so the author must provide a conflict resolution strategy, just in case.
0

What is being create here is a Map of Maps that provide the Transition method when going from one state, to another. If it were done without lambdas or streams it might look like the following:

    public static Map<Phase, Map<Phase, Transition>> init() {
        Map<Phase, Map<Phase,Transition>> outerMap = new EnumMap<>(Phase.class);
        for (Transition v : Transition.values()) {
            Map<Phase, Transition> innerMap = outerMap.get(v.from);
            if (innerMap == null) {
                innerMap = new EnumMap<>(Phase.class);
                outerMap.put(v.from, innerMap);
            }
            innerMap.put(v.to, v);
        }
        return outerMap;
    }

What follows is the Stream explanation.

Start with a stream of the Transition enum values. In all cases here, t is a Transition type.

        public static Map<Phase, Map<Phase, Transition>> map = Stream
                .of(values())

Then they are collected into a EnumMap using the Collectors.groupingBy() method. All the keys here will be taken from the Transition.from field. The value for this outer map will be an EnumMap

                .collect(Collectors.groupingBy(t -> t.from,
                        () -> new EnumMap<>(Phase.class),

Next, the downstream collector, now populates that newly created map with another map. The inner map will use the following.

  • Transition.to field for keys - t.to
  • The resulting Transition type for values which is just t
                        Collectors.toMap(t -> t.to, t -> t,

This next statement is simply a merge function and it isn't used in this particular application so a just chose a value.

                                (x, y) -> x,

Finally, specify a Supplier to for the type of innerMap to create.

                               () -> new EnumMap<>(Phase.class))));

When Transition.from() is invoked using arguments from the Phase enum the map is referenced to display the Transition process.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.