handle duplicate key in Collectors.toMap() function - java

I am creating a map which its (key,value) will be (name, address) in my Person object:
Map<String, String> myMap = persons.stream.collect(Collector.toMap(person.getName(), person.getAddress(), (address1, address2) -> address1));
In the duplicate key situation, I would like to skip to add the second address to the map and would like to log the name also. Skipping the duplicate address I can do already using mergeFunction, but in oder to log the name I need in this mergeFunction the person object, something like:
(address1, address2) -> {
System.out.println("duplicate "+person.name() + " is found!");
return address1;
}
I am getting stuck by passing person object to this merge function.

I believe the forEach approach along with Map.merge would be much simpler and appropriate for the current use case :
Map<String, String> myMap = new HashMap<>();
persons.forEach(person -> myMap.merge(person.getName(), person.getAddress(), (adrs1, adrs2) -> {
System.out.println("duplicate " + person.getName() + " is found!");
return adrs1;
}));
Note: Map.merge also uses BiFunction (parent of BinaryOperator as used in toMap), hence you could correlate the merge function here to your existing desired functionality easily.

#Aomine: solution looks good and works for me too. Just wanted to confirm that with this it iterates twice right ?? Cause with simple solution like below it iterates only once but achieve what is required.
Map<String, String> myMap = new HashMap<>();
persons.forEach(item -> {
if(myMap.containsKey(item.getName()))
{/*do something*/}
else
myMap.put(item.getName(), item.getAddress());
});

if you want to access the whole person object in the merge function then pass Function.identity() for the valueMapper:
Map<String, Person> myMap =
persons.stream()
.collect(toMap(p -> p.getName(),
Function.identity(), // or p -> p
(p1, p2) -> { /* do logic */ }));
But as you can see the resulting map values are Person objects, if you still want a Map<String, String> as a result and still access the whole Person object in the mergeFunction then you can do the following:
persons.stream()
.collect(toMap(p -> p.getName(), Function.identity(),(p1, p2) -> { /* do logic */ }))
.entrySet()
.stream()
.collect(toMap(Map.Entry::getKey, p -> p.getValue().getAddress()));

Here is another possibility using peek
import java.util.*;
import java.util.stream.*;
class App {
public static void main(String[] args) {
List<Person> persons = Arrays.asList(new Person("foo", "bar"), new Person("baz", "qux"), new Person("foo", "zuz"));
Set<String> names = new HashSet<>();
Map<String, String> nameAddress = persons.stream().peek(p -> {
if (names.contains(p.getName())) {
System.out.println("Duplicate key being skipped: " + p);
} else {
names.add(p.getName());
}
}).collect(Collectors.toMap(person -> person.getName(), person -> person.getAddress(), (addr1, addr2) -> addr1));
}
}
class Person {
String name;
String address;
public Person(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
#Override
public String toString() {
return name + " " + address;
}
}
Output for me will be as follows for the snippet above:
Duplicate key being skipped: foo zuz

Related

Transform Java Stream and return reduced values both

Assuming I consume a Stream of entities from a source which I do not want to materialize, and I want to both transform the elements, and return some globally reduced value, what is the idiomatic way with java(8)?
This is essentially trying to perform both a reduce() and a collect().
Example:
class Person {
public String firstname,
public String lastname,
public int age;
}
class TeamSummary {
public List<String> fullnames, // firstname and lastname of all
public Person oldest
}
public TeamSummary getSummary(Stream<Person> personStream) {
final TeamSummary summary = new Summary();
summary.fullnames = personStream
.peek(p -> if (summary.getOldest() == null || summary.getOldest.age < p.age) {
summary.oldest = p;
})
.map(p -> p.firstname + ' ' + p.lastname)
.collect(toList());
return summary;
}
It looks ugly to interact with a variable outside the stream inside the peek method, but what good alternatives are there, it seems I need to combine collect() and reduce().
It get's worse if I want to get a reduced value from the whole stream (like average age), and a filtered list (like Persons above 18 years). It also get's worse if TeamSummary is an immutable class, and additional mutable variables are required.
In such cases it is more idiomatic to use a while loop on stream.iterator() to avoid coupling of stream methods and variables? Or is it natural to use reduce to a tuple like (oldest, accumulated).
I am aware this question is a matter of opinion unless there is an obvious way (like a special collector) that solves this elegantly.
So you want to reduce your collection to a single value? That's where Collectors.reducing comes into play (Alternative: You could use Stream.reduce but with other modifications). Furthermore, you want to aggregate your values in some way and also have the perfect accumulator: TeamSummary.
Now, in the below code I made the foollowing adjustments:
Team Summary has the merge/identity function required for reduce, as it serves as the accumulator
I use a Null Object instead of null for a non-existing person, which makes the code much more readable without null checks (NPE during converter being one of the problems). Have you thought about your output if the stream is empty?
I added a Person constructor for my own convenience. But consider using getters and final fields (even if you think getters and the whole fake encapsulation are boilerplate: You can use method references, e.g. to pass to a comparator, but not field references)
Here is the code:
static class Person {
public String firstname;
public String lastname;
public int age;
public Person(String firstname, String lastname, int age) {
this.firstname = firstname;
this.lastname = lastname;
this.age = age;
}
public static Person getNullObjectYoung() {
return new Person("", "", 0);
}
}
static class TeamSummary {
public List<String> fullnames;
public Person oldest;
public static TeamSummary merge(TeamSummary lhs, TeamSummary rhs) {
TeamSummary result = new TeamSummary();
result.fullnames = new ArrayList<>();
result.fullnames.addAll(lhs.fullnames);
result.fullnames.addAll(rhs.fullnames);
result.oldest = Comparator.<Person, Integer>comparing(p -> p.age).reversed()
.compare(lhs.oldest, rhs.oldest) < 0
? lhs.oldest
: rhs.oldest;
return result;
}
public static TeamSummary of(Person person) {
TeamSummary result = new TeamSummary();
result.fullnames = new ArrayList<>();
result.fullnames.add(person.firstname + " " + person.lastname);
result.oldest = person;
return result;
}
public static TeamSummary identity() {
TeamSummary result = new TeamSummary();
result.fullnames = new ArrayList<>();
result.oldest = Person.getNullObjectYoung();
return result;
}
}
public static void main(String[] args) {
Stream<Person> personStream = Arrays.asList(
new Person("Tom", "T", 32),
new Person("Bob", "B", 40))
.stream();
TeamSummary result = personStream.collect(
Collectors.reducing(
TeamSummary.identity(),
TeamSummary::of,
TeamSummary::merge
));
System.out.println(result.fullnames + " " + result.oldest.age);
}
Note: You asked for a java 8 version. Maybe in java 12, you could also use Collectors.teeing, since you basically want to do two different reductions at the same time (for which we can currently leverage the accumulator).
Edit: Also added a solution for Stream.reduce, which requires a BiFunction (summary, person) -> person:
static class TeamSummary {
...
public TeamSummary include(final Person person) {
final TeamSummary result = new TeamSummary();
result.fullnames = new ArrayList<>(fullnames);
result.fullnames.add(person.firstname + " " + person.lastname);
result.oldest = Comparator.<Person, Integer> comparing(p -> p.age).reversed()
.compare(oldest, person) < 0
? oldest
: person;
return result;
}
}
public static void main(final String[] args) {
...
final TeamSummary reduced = personStream.reduce(
TeamSummary.identity(),
TeamSummary::include,
TeamSummary::merge);
}
Based on the requirements such as - Stream as input and inferring the complete list of names in the output of teamSummary. You can perform the operation mapping the person and its name details to an entry and then reduce them further such as :
return personStream
.map(p -> new AbstractMap.SimpleEntry<>(p, Collections.singletonList(p.getFirstname() + ' ' + p.getLastname())))
.reduce((entry1, entry2) -> new AbstractMap.SimpleEntry<>(entry1.getKey().getAge() >= entry2.getKey().getAge() ?
entry1.getKey() : entry2.getKey(), Stream.of(entry1.getValue(), entry2.getValue()).flatMap(List::stream).collect(Collectors.toList())))
.map(entry -> new TeamSummary(entry.getKey(), entry.getValue()))
.orElseThrow(IllegalArgumentException::new);
For a readable and simplified approach though I would rather suggest passing on the collection and working with multiple stream operations here to construct the TeamSummary as :
public TeamSummary getSummary(List<Person> people) {
List<String> fullNames = people.stream()
.map(p -> p.getFirstname() + ' ' + p.getLastname())
.collect(Collectors.toList());
Person oldestPerson = people.stream()
.reduce(BinaryOperator.maxBy(Comparator.comparing(Person::getAge)))
.orElseThrow(IllegalArgumentException::new);
return new TeamSummary(oldestPerson, fullNames);
}
I don't know why you'd use Collectors.reducing() when you can stream.reduce() directly?
BinaryOperator<Player> older = (p1, p2) ->
Comparator.comparing(Player::getAge) > 0
? p1 : p2;
TeamSummary summary = stream.reduce(
TeamSummary::new, // identity
// accumulator
(ts, player) -> {
ts.addFullnames(String.format("%s %s", player.firstName, player.lastName));
ts.setOldest(older.apply(ts.getOldest(), player));
}
// combiner
(ts1, ts2) -> {
// we can safely modify the given summaries, they were all created while reducing
ts1.setOldest(Math.max(ts1.getOldest(), ts2.getOldest()));
ts1.addFullnames(ts2.getFullnames().toArray());
return ts1;
});
TeamSummary would then look like this:
class TeamSummary {
private int oldest;
public Player getOldest() { return oldest; }
public void setOldest(Player newOldest) { oldest = newOldest; }
private List<String> fullnames();
public List<String> getFullnames() { return Collections.unmodifiableList(fullnames); }
public void addFullnames(String... names) {
fullnames.addAll(Arrays.asList(names));
}
}
Alternative
You could also extend TeamSummary with something like addPlayer(Player p) and merge() to allow it to maintain its consistency:
class TeamSummary {
#Getter
private int oldest;
#Getter
private List<String> fullnames = new ArrayList<>();
public void addPlayer(Player p) {
fullnames.add(String.format("%s %s", p.getFirstname(), p.getLastname()));
oldest = olderPlayer(oldest, p);
}
public TeamSummary merge(TeamSummary other) {
older = olderPlayer(oldest, other.oldest)
fullnames.addAll(other.fullnames);
return this;
}
final static Comparator<Player> BY_AGE = Comparator.comparing(Player::getAge);
private static Player olderPlayer(Player p1, Player p2) {
return BY_AGE.compare(p1, p2) > 0 ? p1 : p2;
}
}
which would make the reduction
stream.reduce(
TeamSummary::new,
TeamSummary::addPlayer,
TeamSummary::merge
);

Java 8 List to Map conversion

I have a problem with conversion List Object to Map String, List Object. I'm looking for Map with a keys name of all components in cars, and a value is represented by cars with this component
public class Car {
private String model;
private List<String> components;
// getters and setters
}
I write a solution but looking for a better stream solution.
public Map<String, List<Car>> componentsInCar() {
HashSet<String> components = new HashSet<>();
cars.stream().forEach(x -> x.getComponents().stream().forEachOrdered(components::add));
Map<String, List<Car>> mapCarsComponents = new HashMap<>();
for (String keys : components) {
mapCarsComponents.put(keys,
cars.stream().filter(c -> c.getComponents().contains(keys)).collect(Collectors.toList()));
}
return mapCarsComponents;
}
You could do it with streams too, but I find this a bit more readable:
public static Map<String, List<Car>> componentsInCar(List<Car> cars) {
Map<String, List<Car>> result = new HashMap<>();
cars.forEach(car -> {
car.getComponents().forEach(comp -> {
result.computeIfAbsent(comp, ignoreMe -> new ArrayList<>()).add(car);
});
});
return result;
}
Or using stream:
public static Map<String, List<Car>> componentsInCar(List<Car> cars) {
return cars.stream()
.flatMap(car -> car.getComponents().stream().distinct().map(comp -> new SimpleEntry<>(comp, car)))
.collect(Collectors.groupingBy(
Entry::getKey,
Collectors.mapping(Entry::getValue, Collectors.toList())
));
}
I know this is a Java question, and there is already a Java answer. However, I would like to add that Kotlin, which is a JVM language and perfectly interoperable with Java, you can do things like this very easily and cleanly:
val carsByComponent = cars
.flatMap { it.components }
.distinct()
.map { component -> component to cars.filter { car -> component in car.components } }
.toMap()
or even more concise, allthough less readable:
val carsByComponent = cars
.flatMap { car -> car.components.map { it to car } }
.groupBy { it.first }
.mapValues {it.value.map { it.second }}

How to filter nested objects with Stream?

I want to filter nested objects with using Stream API. Problem is there are too many nested classes and with below method I am writing too many duplicate code.
Is there any way to handle this stream without duplicate codes?
public class Country{
Map<String, City> cities;
}
public class City{
Map<String, School> schools;
}
public class School{
String name;
String address;
Model model;
}
public class Model{
String name;
Teacher teacher;
}
public class Teacher{
String name;
String id;
}
My Stream;
country.getCities().values().stream().foreach(
(City city) ->
city.getSchools()
.entrySet()
.stream()
.filter(schoolEntry -> schoolEntry.getValue().getName().equals("test"))
.filter(schoolEntry -> schoolEntry.getValue().getModel().getName().equals("test2"))
.filter(schoolEntry -> schoolEntry.getValue().getModel().getTeacher().getName().equals("test2"))
.foreach(schoolEntry -> {
String schoolKey = schoolEntry.getKey();
resultList.put(schoolKey, schoolEntry.getValue().getModel().getTeacher().getId());
})
);
You could define a method to use it as Predicate to filter the schools.
public static boolean matches(School school, String schoolName, String modelName, String teacherId) {
return school.getName().equals(schoolName)
&& school.getModel().getName().equals(modelName)
&& school.getModel().getTeacher().getId().equals(teacherId);
}
Applying this to the stream:
public static Map<String, String> getSchoolAndTeacherFrom(Country country, Predicate<School> schoolWithModelAndTeacher) {
return country.getCities().values().stream()
.flatMap(c -> c.getSchools().entrySet().stream())
.filter(s -> schoolWithModelAndTeacher.test(s.getValue()))
.collect(Collectors.toMap(Entry::getKey, schoolEntry -> schoolEntry.getValue().getModel().getTeacher().getId()));
}
Using this like that:
Country country = <county>
Predicate<School> schoolWithModelAndTeacher = school -> matches(school, "test1", "test2", "test2");
getSchoolAndTeacherFrom(country, schoolWithModelAndTeacher);
Some further thoughts:
If the map schools uses School.getName() as keys, then we can write:
public static Map<String, String> getSchoolAndTeacherFrom(Country country, Predicate<School> schoolWithModelAndTeacher) {
return country.getCities().values().stream()
.flatMap(city -> city.getSchools().values().stream())
.filter(schoolWithModelAndTeacher::test)
.collect(Collectors.toMap(School::getName, school -> school.getModel().getTeacher().getId()));
}
Assuming that school names and teacher ids in a country are unique (while model names are common), the filtering would result in a single value if any. But then there is no need for Map as result type. A result of type Entry<String String> would do it.
And if the parameters of the predicate are still known (school, model, teacher), then this whole thing is just a question whether there is a given teacher on a given school for a given model in a certain country. Then we can write it even shorter:
public static boolean isMatchingSchoolInCountryPresent(Country country, Predicate<School> schoolWithModelAndTeacher) {
return country.getCities().values().stream()
.flatMap(c -> c.getSchools().values().stream())
.anyMatch(schoolWithModelAndTeacher::test);
}
You can just use a "bigger lambda":
.filter(schoolEntry -> {
School value = schoolEntry.getValue();
return value.getName().equals("test")
&& value.getModel().getName().equals("test2")
&& value.getModel().getTeacher().getName().equals("test2")
}
Alternatively you can also create a makePredicate method inside the School class like so:
public static Predicate<School> makePredicate(String first, String second) {
return (school) -> school.name.equals(first) && this.model.getName().equals("test2") && this.model.getTeacher().getName().equals("test2");
}
And use it as a filter predicate:
.filter(School.makePredicate("test", "test2"))
replace the name with a more appropriate name if you can find one
First you need to create a Predicate based on the condition you want to make a filter on the stream
Predicate<School> func1 = (school)-> "test".equals(school.name)
&& "test2".equals(school.model.getName())
&& "test2".equals(school.model.getTeacher().getName());
an then you can easily achieve your aim by:
country.cities.
entrySet()
.stream()
.map(Map.Entry::getValue)
.flatMap(x->x.schools.entrySet().stream())
.filter(s->func1.test(s.getValue()))
.collect(toMap(Map.Entry::getKey, schoolEntry -> schoolEntry.getValue().getModel().getTeacher().id));

Java 8 Lambda Collectors.summingLong multiple columns?

I have POJO definition as follows:
class EmployeeDetails{
private String deptName;
private Double salary;
private Double bonus;
...
}
Currently, i have lambda expression for Group By 'deptName' as :
$set.stream().collect(Collectors.groupingBy(EmployeeDetails::getDeptName,
Collectors.summingLong(EmployeeDetails::getSalary));
Question Is it possible to Sum more than one column? I need to compute sum on both fields salary and bonus in one expression instead of multiple times?
SQL representation would be:
SELECT deptName,SUM(salary),SUM(bonus)
FROM TABLE_EMP
GROUP BY deptName;
You need to create an additional class that will hold your 2 summarised numbers (salary and bonus). And a custom collector.
Let's say you have
private static final class Summary {
private double salarySum;
private double bonusSum;
public Summary() {
this.salarySum = 0;
this.bonusSum = 0;
}
#Override
public String toString() {
return "Summary{" +
"salarySum=" + salarySum +
", bonusSum=" + bonusSum +
'}';
}
}
for holding sums. Then you need a collector like this:
private static class EmployeeDetailsSummaryCollector implements Collector<EmployeeDetails, Summary, Summary> {
#Override
public Supplier<Summary> supplier() {
return Summary::new;
}
#Override
public BiConsumer<Summary, EmployeeDetails> accumulator() {
return (summary, employeeDetails) -> {
summary.salarySum += employeeDetails.salary;
summary.bonusSum += employeeDetails.bonus;
};
}
#Override
public BinaryOperator<Summary> combiner() {
return (summary, summary1) -> {
summary.salarySum += summary1.salarySum;
summary.bonusSum += summary1.bonusSum;
return summary;
};
}
#Override
public Function<Summary, Summary> finisher() {
return Function.identity();
}
#Override
public Set<Characteristics> characteristics() {
return EnumSet.of(Collector.Characteristics.IDENTITY_FINISH);
}
}
With these classes you can collect your results like
final List<EmployeeDetails> employees = asList(
new EmployeeDetails(/* deptName */"A", /* salary */ 100d, /* bonus */ 20d),
new EmployeeDetails("A", 150d, 10d),
new EmployeeDetails("B", 80d, 5d),
new EmployeeDetails("C", 100d, 20d)
);
final Collector<EmployeeDetails, Summary, Summary> collector = new EmployeeDetailsSummaryCollector();
final Map<String, Summary> map = employees.stream()
.collect(Collectors.groupingBy(o -> o.deptName, collector));
System.out.println("map = " + map);
Which prints this:
map = {A=[salary=250.0, bonus=30.0], B=[salary=80.0, bonus=5.0], C=[salary=100.0, bonus=20.0]}
I know you've got your answer, but here is my take(I was writing while the other was posted). There is already a Pair in java in the form of AbstractMap.SimpleEntry.
System.out.println(Stream.of(new EmployeeDetails("first", 50d, 7d), new EmployeeDetails("first", 50d, 7d),
new EmployeeDetails("second", 51d, 8d), new EmployeeDetails("second", 51d, 8d))
.collect(Collectors.toMap(EmployeeDetails::getDeptName,
ed -> new AbstractMap.SimpleEntry<>(ed.getSalary(), ed.getBonus()),
(left, right) -> {
double key = left.getKey() + right.getKey();
double value = left.getValue() + right.getValue();
return new AbstractMap.SimpleEntry<>(key, value);
}, HashMap::new)));
Grouping by is a terminal operation that yields a map. The map produced by the groupingBy in the code below is a Map<String, List<EmployeeDetails>>. I create a new stream using the Map entrySet method. I then create a new Map using Collectors.toMap. This approach uses method chaining to avoid creating another class and create more concise code.
details.stream()
.collect(Collectors.groupingBy(EmployeeDetails::getDeptName))
.entrySet()
.stream()
.collect(Collectors.toMap(x->x.getKey(), x->x.getValue()
.stream()
.mapToDouble(y -> y.getSalary() + y.getBonus())
.sum()));

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
doing a single pass through the collection using the Stream API,
while applying the common part in the key/value mapping functions (Employee.determineUniqueNINumber) only once per stream element?
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
}
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.
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);
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;
}
}
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));

Categories