glts
glts

Reputation: 22684

Transform Stream into a Map, when key/value mapping functions have a computational step in common

I have a collection of Employee objects and need to turn it into a map of hyperlink widgets for presentation purposes.

For each employee, an entry is added to the result, with the key being an identifier (here a National Insurance number), and the value being the hyperlink widget. Here's a first attempt:

static Map<String, Hyperlink> toHyperlinksByNIN(Collection<Employee> employees) {
    return employees.stream()
            .collect(Collectors.toMap(
                    Employee::determineUniqueNINumber,
                    employee -> new Hyperlink(
                          employee.getName(), employee.determineUniqueNINumber())));
}

Unfortunately, this solution won't do, because the NI number is actually not part of the employee model, but needs to be fetched from an expensive remote service on every call to Employee.determineUniqueNINumber. This method is simply too costly to call more than once per employee record.

How can I obtain the desired Map

Upvotes: 4

Views: 134

Answers (5)

a better oliver
a better oliver

Reputation: 26828

I would go with caching but you can always create your own collector / use a custom reduction operation:

return employees.stream()
       .collect(HashMap::new,
                (map, e) -> {
                  String number = e.determineUniqueNINumber();
                  map.put(number, new Hyperlink( e.getName(), number));
                },
                Map::putAll);

Upvotes: 1

RealSkeptic
RealSkeptic

Reputation: 34628

As others have suggested, the easiest way is to map the elements of the stream to a container object so that you can then collect the cached NINumber from that container object together with the other details.

If you don't want to write your own custom class for every such use, you can utilize an existing type, such as AbstractMap.SimpleEntry.

You will then be able to write:

return employees.stream()
        .map(emp -> new AbstractMap.SimpleEntry<>(emp.determineUniqueNINumber(),emp.getName()))
        .collect(Collectors.toMap(
                mapEntry -> mapEntry.getKey(),
                mapEntry -> new Hyperlink(mapEntry.getValue(), mapEntry.getKey())));

This saves you writing your own class for a simple case like this. Of course, if you need more than just the getName() from Employee, your second element can be the Employee object itself.

Upvotes: 2

Mrinal
Mrinal

Reputation: 1906

Does Hyperlink class stores UniqueNINumber in instance field and expose getter method? Then you can first create Hyperlink object, and then create the map :

return employees
        .stream()
        .map(employee -> new Hyperlink(employee.getName(), employee
            .determineUniqueNINumber()))
        .collect(Collectors.toMap(Hyperlink::getUniqueNINumber, i -> i));

Here is Hyperlink class :

public class Hyperlink {
    private String name;
    private String uniqueNINumber;

    public Hyperlink(String name, String uniqueNINumber) {
        this.name = name;
        this.uniqueNINumber = uniqueNINumber;
    }

    public String getName() {
        return name;
    }

    public String getUniqueNINumber() {
        return uniqueNINumber;
    }

    // Other stuff

}

Upvotes: 2

Tunaki
Tunaki

Reputation: 137084

I think the easiest solution to your problem is to implement a simple caching procedure inside the method determineUniqueNINumber:

public class Employee {

    private String niNumber;

    public String determineUniqueNINumber() {
        if (niNumber == null) {
            niNumber = resultOfLongAndCostlyMethod();
        }
        return niNumber;
    }

}

This way, on the second call, the costly method is not called and you simply return the already calculated value.

Another solution is to store the insurance number inside a custom typed Tuple class. It would store the employee along with its insurance number.

static Map<String, Hyperlink> toHyperlinksByNIN(Collection<Employee> employees) {
    return employees.stream()
            .map(e -> new Tuple<>(e, e.determineUniqueNINumber()))
            .collect(Collectors.toMap(
                    t -> t.getValue2(),
                    t -> new Hyperlink(t.getValue1().getName(), t.getValue2())));
}

class Tuple<T1, T2> {

    private final T1 value1;
    private final T2 value2;

    public Tuple(T1 value1, T2 value2) {
        this.value1 = value1;
        this.value2 = value2;
    }

    public T1 getValue1() {
        return value1;
    }

    public T2 getValue2() {
        return value2;
    }

}

Upvotes: 1

Sam Lindsay-Levine
Sam Lindsay-Levine

Reputation: 196

You could transform to a helper data class to ensure that you only get the number once:

private static class EmployeeAndNINumber {
  private Employee employee;
  private String niNumber;

  public EmployeeAndNINumber(Employee employee) { 
    this.employee = employee;
    this.niNumber = employee.determineUniqueNINumber();
  }

  public Employee getEmployee() { return this.employee; }
  public String getNINumber() { return this.niNumber; }

  public Hyperlink toHyperlink() {
    return new Hyperlink(employee.getName(), this.getNINumber());
  }
}

Then you could transform to this data class, getting the NI number once and only once, then use that information to build up your map:

employees.stream()
  .map(EmployeeAndNINumber::new) 
  .collect(toMap(EmployeeAndNINumber::getNINumber,
                 EmployeeAndNINumber::toHyperlink));

Upvotes: 0

Related Questions