Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java: how to transform from List<T> to Map<f1(T), List(f2(T))> without iterating

I have a list of objects that I need to transform to a map where the keys are a function of each element, and the values are lists of another function of each element. Effectively this is grouping the elements by a function of them.

For example, suppose a simple element class:

class Element {
    int f1() { ... }
    String f2() { ... }
}

and a list of these:

[
    { f1=100, f2="Alice" },
    { f1=200, f2="Bob" },
    { f1=100, f2="Charles" },
    { f1=300, f2="Dave" }
]

then I would like a map as follows:

{
    {key=100, value=[ "Alice", "Charles" ]},
    {key=200, value=[ "Bob" ]},
    {key=300, value=[ "Dave" ]}
}

Can anyone suggest a succinct way of doing this in Java without iterating? A combination of LambdaJ's group method with Guava's Maps.transform nearly gets there, but group doesn't generate a map.

like image 967
Kkkev Avatar asked Dec 02 '11 09:12

Kkkev


People also ask

Can we convert list to map in Java?

With Java 8, you can convert a List to Map in one line using the stream() and Collectors. toMap() utility methods. The Collectors. toMap() method collects a stream as a Map and uses its arguments to decide what key/value to use.


2 Answers

Guava has Maps.uniqueIndex(Iterable values, Function keyFunction) and Multimaps.index(Iterable values, Function keyFunction), but they don't transform the values. There are some requests to add utility methods that do what you want, but for now, you'll have to roll it yourself using Multimaps.index() and Multimaps.transformValues():

static class Person {
    private final Integer age;
    private final String name;

    public Person(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

private enum GetAgeFunction implements Function<Person, Integer> {
    INSTANCE;

    @Override
    public Integer apply(Person person) {
        return person.getAge();
    }
}

private enum GetNameFunction implements Function<Person, String> {
    INSTANCE;

    @Override
    public String apply(Person person) {
        return person.getName();
    }
}

public void example() {
    List<Person> persons = ImmutableList.of(
            new Person(100, "Alice"),
            new Person(200, "Bob"),
            new Person(100, "Charles"),
            new Person(300, "Dave")
    );

    ListMultimap<Integer, String> ageToNames = getAgeToNamesMultimap(persons);

    System.out.println(ageToNames);

    // prints {100=[Alice, Charles], 200=[Bob], 300=[Dave]}
}

private ListMultimap<Integer, String> getAgeToNamesMultimap(List<Person> persons) {
    ImmutableListMultimap<Integer, Person> ageToPersons = Multimaps.index(persons, GetAgeFunction.INSTANCE);
    ListMultimap<Integer, String> ageToNames = Multimaps.transformValues(ageToPersons, GetNameFunction.INSTANCE);

    // Multimaps.transformValues() returns a *lazily* transformed view of "ageToPersons"
    // If we want to iterate multiple times over it, it's better to create a copy
    return ImmutableListMultimap.copyOf(ageToNames);
}

A re-usable utility method could be:

public static <E, K, V> ImmutableListMultimap<K, V> keyToValuesMultimap(Iterable<E> elements, Function<E, K> keyFunction, Function<E, V> valueFunction) {
    ImmutableListMultimap<K, E> keysToElements = Multimaps.index(elements, keyFunction);
    ListMultimap<K, V> keysToValuesLazy = Multimaps.transformValues(keysToElements, valueFunction);
    return ImmutableListMultimap.copyOf(keysToValuesLazy);
}

I guess we could improve the generics in the signature by using Function<? extends E, K> or something, but I don't have the time to delve further...

like image 144
Etienne Neveu Avatar answered Sep 23 '22 00:09

Etienne Neveu


Now with Java8 you can do it like:

static class Element {
    final int f1;
    final String f2;

    Element(int f1, String f2) {
        this.f1 = f1;
        this.f2 = f2;
    }

    int f1() { return f1;}
    String f2() { return f2; }
}

public static void main(String[] args) {
    List<Element> elements = new ArrayList<>();
    elements.add(new Element(100, "Alice"));
    elements.add(new Element(200, "Bob"));
    elements.add(new Element(100, "Charles"));
    elements.add(new Element(300, "Dave"));

    elements.stream()
            .collect(Collectors.groupingBy(
                    Element::f1,
                    Collectors.mapping(Element::f2, Collectors.toList())
                    ))
            .forEach((f1, f2) -> System.out.println("{"+f1.toString() + ", value="+f2+"}"));
}
like image 27
enterbios Avatar answered Sep 23 '22 00:09

enterbios