Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Converting List<MyObject> to Map<String, List<String>> in Java 8 when we have duplicate elements and custom filter criteria

I have an instances of Student class.

class Student {
    String name;
    String addr;
    String type;

    public Student(String name, String addr, String type) {
        super();
        this.name = name;
        this.addr = addr;
        this.type = type;
    }

    @Override
    public String toString() {
        return "Student [name=" + name + ", addr=" + addr + "]";
    }

    public String getName() {
        return name;
    }

    public String getAddr() {
        return addr;
    }
}

And I have a code to create a map , where it store the student name as the key and some processed addr values (a List since we have multiple addr values for the same student) as the value.

public class FilterId {

public static String getNum(String s) {
    // should do some complex stuff, just for testing
    return s.split(" ")[1];
}

public static void main(String[] args) {
    List<Student> list = new ArrayList<Student>();
    list.add(new Student("a", "test 1", "type 1"));
    list.add(new Student("a", "test 1", "type 2"));
    list.add(new Student("b", "test 1", "type 1"));
    list.add(new Student("c", "test 1", "type 1"));
    list.add(new Student("b", "test 1", "type 1"));
    list.add(new Student("a", "test 1", "type 1"));
    list.add(new Student("c", "test 3", "type 2"));
    list.add(new Student("a", "test 2", "type 1"));
    list.add(new Student("b", "test 2", "type 1"));
    list.add(new Student("a", "test 3", "type 1"));
    Map<String, List<String>> map = new HashMap<>();

    // This will create a Map with Student names (distinct) and the test numbers (distinct List of tests numbers) associated with them.
    for (Student student : list) {
        if (map.containsKey(student.getName())) {
            List<String> numList = map.get(student.getName());
            String value = getNum(student.getAddr());

            if (!numList.contains(value)) {
                numList.add(value);
                map.put(student.getName(), numList);
            }
        } else {
            map.put(student.getName(), new ArrayList<>(Arrays.asList(getNum(student.getAddr()))));
        }
    }

    System.out.println(map.toString());

}
}

Output would be : {a=[1, 2, 3], b=[1, 2], c=[1, 3]}

How can I just do the same in java8 in a much more elegant way, may be using the streams ?

Found this Collectors.toMap in java 8 but could't find a way to actually do the same with this.

I was trying to map the elements as CSVs but that it didn't work since I couldn't figure out a way to remove the duplicates easily and the output is not what I need at the moment.

Map<String, String> map2 = new HashMap<>();
map2 = list.stream().collect(Collectors.toMap(Student::getName, Student::getAddr, (a, b) -> a + " , " + b));
System.out.println(map2.toString());
// {a=test 1 , test 1 , test 1 , test 2 , test 3, b=test 1 , test 1 , test 2, c=test 1 , test 3}
like image 529
prime Avatar asked Feb 02 '18 17:02

prime


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.

How do I convert a List of strings to a List of objects?

Pass the List<String> as a parameter to the constructor of a new ArrayList<Object> . List<Object> objectList = new ArrayList<Object>(stringList); Any Collection can be passed as an argument to the constructor as long as its type extends the type of the ArrayList , as String extends Object .

Can we convert ArrayList to Map in Java?

Array List can be converted into HashMap, but the HashMap does not maintain the order of ArrayList. To maintain the order, we can use LinkedHashMap which is the implementation of HashMap.


3 Answers

With streams, you could use Collectors.groupingBy along with Collectors.mapping:

Map<String, Set<String>> map = list.stream()
    .collect(Collectors.groupingBy(
        Student::getName,
        Collectors.mapping(student -> getNum(student.getAddr()),
            Collectors.toSet())));

I've chosen to create a map of sets instead of a map of lists, as it seems that you don't want duplicates in the lists.


If you do need lists instead of sets, it's more efficient to first collect to sets and then convert the sets to lists:

Map<String, List<String>> map = list.stream()
    .collect(Collectors.groupingBy(
        Student::getName,
        Collectors.mapping(s -> getNum(s.getAddr()),
            Collectors.collectingAndThen(Collectors.toSet(), ArrayList::new))));

This uses Collectors.collectingAndThen, which first collects and then transforms the result.


Another more compact way, without streams:

Map<String, Set<String>> map = new HashMap<>(); // or LinkedHashMap
list.forEach(s -> 
    map.computeIfAbsent(s.getName(), k -> new HashSet<>()) // or LinkedHashSet
        .add(getNum(s.getAddr())));

This variant uses Iterable.forEach to iterate the list and Map.computeIfAbsent to group transformed addresses by student name.

like image 105
fps Avatar answered Oct 20 '22 23:10

fps


First of all, the current solution is not really elegant, regardless of any streaming solution.

The pattern of

if (map.containsKey(k)) {
    Value value = map.get(k);
    ...
} else {
    map.put(k, new Value());
}

can often be simplified with Map#computeIfAbsent. In your example, this would be

// This will create a Map with Student names (distinct) and the test
// numbers (distinct List of tests numbers) associated with them.
for (Student student : list)
{
    List<String> numList = map.computeIfAbsent(
        student.getName(), s -> new ArrayList<String>());
    String value = getNum(student.getAddr());
    if (!numList.contains(value))
    {
        numList.add(value);
    }
}

(This is a Java 8 function, but it is still unrelated to streams).


Next, the data structure that you want to build there does not seem to be the most appropriate one. In general, the pattern of

if (!list.contains(someValue)) {
    list.add(someValue);
}

is a strong sign that you should not use a List, but a Set. The set will contain each element only once, and you will avoid the contains calls on the list, which are O(n) and thus may be expensive for larger lists.

Even if you really need a List in the end, it is often more elegant and efficient to first collect the elements in a Set, and afterwards convert this Set into a List in one dedicated step.

So the first part could be solved like this:

// This will create a Map with Student names (distinct) and the test
// numbers (distinct List of tests numbers) associated with them.
Map<String, Collection<String>> map = new HashMap<>();
for (Student student : list)
{
    String value = getNum(student.getAddr());
    map.computeIfAbsent(student.getName(), s -> new LinkedHashSet<String>())
        .add(value);
}

It will create a Map<String, Collection<String>>. This can then be converted into a Map<String, List<String>> :

// Convert the 'Collection' values of the map into 'List' values 
Map<String, List<String>> result = 
    map.entrySet().stream().collect(Collectors.toMap(
        Entry::getKey, e -> new ArrayList<String>(e.getValue())));

Or, more generically, using a utility method for this:

private static <K, V> Map<K, List<V>> convertValuesToLists(
    Map<K, ? extends Collection<? extends V>> map)
{
    return map.entrySet().stream().collect(Collectors.toMap(
        Entry::getKey, e -> new ArrayList<V>(e.getValue())));
}

I do not recommend this, but you also could convert the for loop into a stream operation:

Map<String, Set<String>> map = 
    list.stream().collect(Collectors.groupingBy(
        Student::getName, LinkedHashMap::new,
        Collectors.mapping(
            s -> getNum(s.getAddr()), Collectors.toSet())));

Alternatively, you could do the "grouping by" and the conversion from Set to List in one step:

Map<String, List<String>> result = 
    list.stream().collect(Collectors.groupingBy(
        Student::getName, LinkedHashMap::new,
        Collectors.mapping(
            s -> getNum(s.getAddr()), 
            Collectors.collectingAndThen(
                Collectors.toSet(), ArrayList<String>::new))));

Or you could introduce an own collector, that does the List#contains call, but all this tends to be far less readable than the other solutions...

like image 5
Marco13 Avatar answered Oct 20 '22 23:10

Marco13


I think you are looking for something like below

   Map<String,Set<String>> map =  list.stream().
           collect(Collectors.groupingBy(
                    Student::getName,
                    Collectors.mapping(e->getNum(e.getAddr()), Collectors.toSet())
                ));

   System.out.println("Map : "+map);
like image 3
Amit Bera Avatar answered Oct 21 '22 01:10

Amit Bera