Hot questions for Transforming the Data in Guava

Top 10 Java Open Source / Guava / Transforming the Data

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

Question: 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.

Answer: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);
}

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+"}"));
}

How to transform List to Map with Google collections?

Question: I have a list of strings and I have a function to generate a value for each key in the list.

I want to create a map using this function. Can I do this with Google collections?

Answer: Use Maps.uniqueIndex(Iterable, Function): Returns an immutable map for which the Map.values() are the given elements in the given order, and each key is the product of invoking a supplied function on its corresponding value.

Example:

Map<String,String> mappedRoles = Maps.uniqueIndex(yourList, new Function<String,String>() {
  public String apply(String from) {
    // do stuff here
    return result;
  }});

Transform and convert a List to Set with Guava

Question: Is there a simple way to convert and transform a List to Set with Guava?

I'd like to use method:

Set<To> result = Sets.transformToSet(myList, new Function<From, To>() {
            public To apply(From item) {
                return convert(item);
            }
        });

this is my code, with "tempCollection"

Collection<To> tempCollection = Collections2.transform(myList, new Function<From, To>() {
            public To apply(From item) {
                return convert(item);
            }
        });
Set<To> result = newHashSet(tempCollection );

Answer: This creates an ImmutableSet, which does not accept null. So if you want your Set to contain null, you'll have to use another solution, like the one you're currently using.

Set<To> result = FluentIterable.from(myList)
                               .transform(new Function<From, To>() {
                                   @Override
                                   public To apply(From input) {
                                       return convert(input);
                                   }
                               })
                               .toSet();

Note that, if it's the creation of the temporary collection that bothers you, you shouldn't be bothered. No copy is made. The collection is simply a view over the original list.


Java Serialization Problems, while using guava Lists.transform

Question: I had to serialize a complex object, but one of its component was non-serializable ( a third party graph object), so I created a custom serializable version of this Graph class and used Guava List transform to convert the non-serializable object to the custom objects. The serialization writeObject still failed. I will be interested to know why? My assumption is that the Lists.transform performs its operation Lazily (Holding an hidden reference to the orginal object.)

Also is there an workaround for this problem?

Answer: Lists.transform() does perform lazily as you suspected. You could do one of

Lists.newArrayList(Lists.transform(...))

or, if you want an immutable version,

ImmutableList.copyOf(Lists.transform(...))

and then serialize the resulting list.


how to transform Map<String,String> to List<String> using google collections

Question: I have a map with strings, I want to transform it to a list of strings with " " as a key value separator. Is it possible using google collections?

Code example that I want to do using google collections:

public static List<String> addLstOfSetEnvVariables(Map<String, String> env)
{
    ArrayList<String> result = Lists.newArrayList();
    for (Entry<String, String> entry : env.entrySet())
    {
        result.add(entry.getKey() + " " + entry.getValue());
    }
    return result;
}

Answer: Here you go (using Guava):

private static final Joiner JOINER = Joiner.on(' ');
public List<String> mapToList(final Map<String, String> input){
    return Lists.newArrayList(
        Iterables.transform(
            input.entrySet(), new Function<Map.Entry<String, String>, String>(){
                @Override
                public String apply(final Map.Entry<String, String> input){
                    return JOINER.join(input.getKey(), input.getValue());
                }
            }));
}

with Java 8 streams. No external lib needed.

public List<String> mapToList(final Map<String, String> input) {
    return input.entrySet()
                .stream()
                .map(e -> new StringBuilder(
                             e.getKey().length()
                                     + e.getValue().length()
                                     + 1
                     ).append(e.getKey())
                      .append(' ')
                      .append(e.getValue())
                      .toString()
                )
                .collect(Collectors.toList());
}

guava: Transform a list to a map with the index as the key

Question: In my app I have a List names. I wish to convert this to a map based on the index value. For example

List<String> names = new ArrayList<String>();
names.add("Pratik");
names.add("Pratik");
names.add("Ram");
names.add("Varun");

Can you help me with some guava/java api method which can help me get a map where the key is the index and value is name even if there are duplicate values? So if there are two "Pratik" String in names. The map should be like this

0 -> "Pratik", 1->"Pratik",2->"Ram",3->"Varun"

Answer: I think that you don't need Guava here:

Map<Integer, String> map = new HashMap<Integer, String>();
int i = 0;
for (String name : names) {
    map.put(i++, name);
}

System.out.println(map);

prints:

{0=Pratik, 1=Pratik, 2=Ram, 3=Varun}

If you absolutely want a way to do it with Guava, you could do this (but I would recommend the JDK way to avoid unnecessary complexity):

Maps.asMap(
     ContiguousSet.create(Range.closedOpen(0, names.size()), DiscreteDomain.integers()),
          new Function<Integer, String>() {
              @Override
              public String apply(Integer input) {
                  return names.get(input);
              }
          });