10

Say, I've a class called Project,

class Project {
    private String projectId;
    private String projectName;
}

and a class called Employee, which has a list of projects

class Employee {
    private String name;
    private List<Project> projects
}

I also have a list of Employee objects. Now, I need to create a Map with the list of projects as the key and a set of employee objects as the value from this list. I can get it working by

Map<List<Project>, Set<Employee>> x =
        employees
        .stream
        .collect(Collectors.groupingBy(Employee::getProjects, Collectors.toSet()));

However, since I'm using List as the key, I want to be extra careful and make sure that the list is immutable. Is there a way to achieve this?

Thanks.

cdoe
  • 447
  • 5
  • 13
  • 4
    @tobias_k that doesn't really solve the issue since one could call `employee.getProjects().add(newProject);`. I would recommend instead: `e -> Collections.unmodifiableList(new ArrayList<> (e.getProjects()))`, i.e. a separate copy. – assylias Oct 13 '17 at 11:15
  • I think you'll just have to make a defensive copy. – Jorn Vernee Oct 13 '17 at 11:16
  • There may be a way to achieve your goal without using a `Map, ...>`. – assylias Oct 13 '17 at 11:17
  • Actually, you should elaborate why you want the lists to be immutable. Do you want to prevent users of the map from accidentally modifying the lists inside the employee objects, or do you want to ensure that the hashes do not change and you can actually retrieve the employee sets from the map? Depending on what's your focus, the best solution may be different. – tobias_k Oct 13 '17 at 12:34
  • @tobias_k - it's the later. I don't want someone to change the hash resulting in unexpected behaviour when I do a get() on map. – cdoe Oct 13 '17 at 12:53
  • @cdoe In this case, the list in the `Employee` class itself should probably be immutable. – tobias_k Oct 13 '17 at 12:59
  • From where are you going to get the list that you will pass to get? Can it be changed after you created your map? If it can you're also not going to get anything. – Oleg Oct 13 '17 at 12:59
  • Alterantively, you could use a `Map>` and then use the intersection of all the employee sets you get for a given employees list of projects. The result would be a bit different though: All the employees that work in those _or more_ projects. But maybe that's actually closer to what you want. Also, no problem with list hashing. – tobias_k Oct 13 '17 at 13:02

5 Answers5

2

List immutability is supported in Java 9. You can simply change Employee#getProjects to the following:

public List<Project> getProjects() {
    return List.of(projects.toArray(new Project[projects.size()]));
}

If you don't want this method to return an immutable List, then you can change the Collector:

employees.stream()
         .collect(Collectors.groupingBy(e -> List.of(e.getProjects().toArray(new Project[0])), Collectors.toSet()));
Jacob G.
  • 28,856
  • 5
  • 62
  • 116
2

This is how you would do it with Guava (I tried it with version 24.1, which is the latest one as of today)

List<Employee> employees = new ArrayList<>();

//  ... let's assume someone fills in the employees

// Everything mutable
Map<List<Project>, Set<Employee>> x =
    employees
        .stream()
        .collect(Collectors.groupingBy(Employee::getProjects, Collectors.toSet()));

// Everything immutable
ImmutableMap<ImmutableList<Project>, ImmutableSet<Employee>> immutableX =
    employees
        .stream()
        .collect(
            Collectors.collectingAndThen(
                Collectors.groupingBy(
                    (employee) -> ImmutableList.copyOf(employee.getProjects()),
                    ImmutableSet.<Employee>toImmutableSet()),
                ImmutableMap::copyOf));

// Only the List<Project> immutable
Map<ImmutableList<Project>, Set<Employee>> immutableListX =
    employees
        .stream()
        .collect(
            Collectors.groupingBy(
                (employee) -> ImmutableList.copyOf(employee.getProjects()),
                Collectors.toSet()));

This assumes your classes definitions are these (I needed to add the method getProjects for the original example to compile):

class Project {
  public String projectId;
  public String projectName;
}

class Employee {
  public String name;
  public List<Project> projects;

  public List<Project> getProjects() {
    return projects;
  }
}
epere4
  • 2,892
  • 2
  • 12
  • 8
1

If I am not missing something obvious here, can't you simply wrap that into Collections.unmodifiableList:

 collect(Collectors.groupingBy(
          emps -> Collections.unmodifiableList(emps.getProjects()), 
          Collectors.toSet());

If you have guava on the class path on the other hand, you could use ImmutableList

Eugene
  • 117,005
  • 15
  • 201
  • 306
-1

You can use

private List<? extends Project> projects = new LinkedList<>();

when populating employees. It sets up a logically immutable list.

Evgeny Mamaev
  • 1,237
  • 1
  • 14
  • 31
-1

If you don't mind your map to be initially stored in a mutable Map and then copied to an immutable collection, you could do something like the following:

ImmutableMap<List<Project>, List<Employee>> collect = employees.stream()
            .collect(Collectors.collectingAndThen(Collectors.groupingBy(Employee::getProjects),
                    ImmutableMap::copyOf));

The ImmutableMap is a Google Common's structure (com.google.common.collect.ImmutableMap). Credits to this as well.

Niko
  • 616
  • 4
  • 20
  • 1
    Why do you think this makes the list key immutable? – shmosel Oct 15 '17 at 21:21
  • I'm wrong. I was under the impression that the immutability of the map would infer that the key/value pairs it contains would be immutable themselves. Testing this I can clearly see that although there are restrictions in adding new entries in the map, there is no restriction whatsoever in adding a new Employee in the Employee's list. @shmosel Thank you for your attention! – Niko Oct 16 '17 at 10:08