Extract duplicate objects from a List in Java 8 - java

This code removes duplicates from the original list, but I want to extract the duplicates from the original list -> not removing them (this package name is just part of another project):
Given:
a Person pojo:
package at.mavila.learn.kafka.kafkaexercises;
import org.apache.commons.lang3.builder.ToStringBuilder;
public class Person {
private final Long id;
private final String firstName;
private final String secondName;
private Person(final Builder builder) {
this.id = builder.id;
this.firstName = builder.firstName;
this.secondName = builder.secondName;
}
public Long getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public String getSecondName() {
return secondName;
}
public static class Builder {
private Long id;
private String firstName;
private String secondName;
public Builder id(final Long builder) {
this.id = builder;
return this;
}
public Builder firstName(final String first) {
this.firstName = first;
return this;
}
public Builder secondName(final String second) {
this.secondName = second;
return this;
}
public Person build() {
return new Person(this);
}
}
#Override
public String toString() {
return new ToStringBuilder(this)
.append("id", id)
.append("firstName", firstName)
.append("secondName", secondName)
.toString();
}
}
Duplication extraction code.
Notice here we filter the id and the first name to retrieve a new list, I saw this code someplace else, not mine:
package at.mavila.learn.kafka.kafkaexercises;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static java.util.Objects.isNull;
public final class DuplicatePersonFilter {
private DuplicatePersonFilter() {
//No instances of this class
}
public static List<Person> getDuplicates(final List<Person> personList) {
return personList
.stream()
.filter(duplicateByKey(Person::getId))
.filter(duplicateByKey(Person::getFirstName))
.collect(Collectors.toList());
}
private static <T> Predicate<T> duplicateByKey(final Function<? super T, Object> keyExtractor) {
Map<Object,Boolean> seen = new ConcurrentHashMap<>();
return t -> isNull(seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE));
}
}
The test code.
If you run this test case you will get [alex, lolita, elpidio, romualdo].
I would expect to get instead [romualdo, otroRomualdo] as the extracted duplicates given the id and the firstName:
package at.mavila.learn.kafka.kafkaexercises;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.*;
public class DuplicatePersonFilterTest {
private static final Logger LOGGER = LoggerFactory.getLogger(DuplicatePersonFilterTest.class);
#Test
public void testList(){
Person alex = new Person.Builder().id(1L).firstName("alex").secondName("salgado").build();
Person lolita = new Person.Builder().id(2L).firstName("lolita").secondName("llanero").build();
Person elpidio = new Person.Builder().id(3L).firstName("elpidio").secondName("ramirez").build();
Person romualdo = new Person.Builder().id(4L).firstName("romualdo").secondName("gomez").build();
Person otroRomualdo = new Person.Builder().id(4L).firstName("romualdo").secondName("perez").build();
List<Person> personList = new ArrayList<>();
personList.add(alex);
personList.add(lolita);
personList.add(elpidio);
personList.add(romualdo);
personList.add(otroRomualdo);
final List<Person> duplicates = DuplicatePersonFilter.getDuplicates(personList);
LOGGER.info("Duplicates: {}",duplicates);
}
}
In my job I was able to get the desired result it by using Comparator using TreeMap and ArrayList, but this was creating a list then filtering it, passing the filter again to a newly created list, this looks bloated code, (and probably inefficient)
Does someone has a better idea how to extract duplicates?, not remove them.
Thanks in advance.
Update
Thanks everyone for your answers
To remove the duplicate using same approach with the uniqueAttributes:
public static List<Person> removeDuplicates(List<Person> personList) {
return getDuplicatesMap(personList).values().stream()
.filter(duplicates -> duplicates.size() > 1)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
private static Map<String, List<Person>> getDuplicatesMap(List<Person> personList) {
return personList.stream().collect(groupingBy(DuplicatePersonFilter::uniqueAttributes));
}
private static String uniqueAttributes(Person person){
if(Objects.isNull(person)){
return StringUtils.EMPTY;
}
return (person.getId()) + (person.getFirstName()) ;
}
Update 2
But also the answer provided by #brett-ryan is correct:
public static List<Person> extractDuplicatesWithIdentityCountingV2(final List<Person> personList){
List<Person> duplicates = personList.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream()
.filter(n -> n.getValue() > 1)
.flatMap(n -> nCopies(n.getValue().intValue(), n.getKey()).stream())
.collect(toList());
return duplicates;
}
EDIT
Above code can be found under:
https://gitlab.com/totopoloco/marco_utilities/-/tree/master/duplicates_exercises
Please see:
Usage:
https://gitlab.com/totopoloco/marco_utilities/-/blob/master/duplicates_exercises/src/test/java/at/mavila/exercises/duplicates/lists/DuplicatePersonFilterTest.java
Implementation:
https://gitlab.com/totopoloco/marco_utilities/-/blob/master/duplicates_exercises/src/main/java/at/mavila/exercises/duplicates/lists/DuplicatePersonFilter.java

To indentify duplicates, no method I know of is better suited than Collectors.groupingBy(). This allows you to group the list into a map based on a condition of your choice.
Your condition is a combination of id and firstName. Let's extract this part into an own method in Person:
String uniqueAttributes() {
return id + firstName;
}
The getDuplicates() method is now quite straightforward:
public static List<Person> getDuplicates(final List<Person> personList) {
return getDuplicatesMap(personList).values().stream()
.filter(duplicates -> duplicates.size() > 1)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
private static Map<String, List<Person>> getDuplicatesMap(List<Person> personList) {
return personList.stream().collect(groupingBy(Person::uniqueAttributes));
}
The first line calls another method getDuplicatesMap() to create the map as explained above.
It then streams over the values of the map, which are lists of persons.
It filters out everything except lists with a size greater than 1, i.e. it finds the duplicates.
Finally, flatMap() is used to flatten the stream of lists into one single stream of persons, and collects the stream to a list.
An alternative, if you truly identify persons as equal if the have the same id and firstName is to go with the solution by Jonathan Johx and implement an equals() method.

If you could implement equals and hashCode on Person you could then use a counting down-stream collector of the groupingBy to get distinct elements that have been duplicated.
List<Person> duplicates = personList.stream()
.collect(groupingBy(identity(), counting()))
.entrySet().stream()
.filter(n -> n.getValue() > 1)
.map(n -> n.getKey())
.collect(toList());
If you would like to keep a list of sequential repeated elements you can then expand this out using Collections.nCopies to expand it back out. This method will ensure repeated elements are ordered together.
List<Person> duplicates = personList.stream()
.collect(groupingBy(identity(), counting()))
.entrySet().stream()
.filter(n -> n.getValue() > 1)
.flatMap(n -> nCopies(n.getValue().intValue(), n.getKey()).stream())
.collect(toList());

List<Person> duplicates = personList.stream()
.collect(Collectors.groupingBy(Person::getId))
.entrySet().stream()
.filter(e->e.getValue().size() > 1)
.flatMap(e->e.getValue().stream())
.collect(Collectors.toList());
That should give you a List of Person where the id has been duplicated.

In this scenario you need to write your custom logic to extract the duplicates from the list, you will get all the duplicates in the Person list
public static List<Person> extractDuplicates(final List<Person> personList) {
return personList.stream().flatMap(i -> {
final AtomicInteger count = new AtomicInteger();
final List<Person> duplicatedPersons = new ArrayList<>();
personList.forEach(p -> {
if (p.getId().equals(i.getId()) && p.getFirstName().equals(i.getFirstName())) {
count.getAndIncrement();
}
if (count.get() == 2) {
duplicatedPersons.add(i);
}
});
return duplicatedPersons.stream();
}).collect(Collectors.toList());
}
Applied to:
List<Person> l = new ArrayList<>();
Person alex = new
Person.Builder().id(1L).firstName("alex").secondName("salgado").build();
Person lolita = new
Person.Builder().id(2L).firstName("lolita").secondName("llanero").build();
Person elpidio = new
Person.Builder().id(3L).firstName("elpidio").secondName("ramirez").build();
Person romualdo = new
Person.Builder().id(4L).firstName("romualdo").secondName("gomez").build();
Person otroRomualdo = new
Person.Builder().id(4L).firstName("romualdo").secondName("perez").build();
l.add(alex);
l.add(lolita);
l.add(elpidio);
l.add(romualdo);
l.add(otroRomualdo);
Output:
[Person [id=4, firstName=romualdo, secondName=gomez], Person [id=4, firstName=romualdo, secondName=perez]]

I think first you should overwrite equals method of Person class and focus on id and name. And after you can update it adding a filter for that.
#Override
public int hashCode() {
return Objects.hash(id, name);
}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Person other = (Person) obj;
if (!Objects.equals(name, other.name)) {
return false;
}
if (!Objects.equals(id, other.id)) {
return false;
}
return true;
}
personList
.stream()
.filter(p -> personList.contains(p))
.collect(Collectors.toList());

Solution based on generic key:
public static <T> List<T> findDuplicates(List<T> list, Function<T, ?> uniqueKey) {
if (list == null) {
return emptyList();
}
Function<T, ?> notNullUniqueKey = el -> uniqueKey.apply(el) == null ? "" : uniqueKey.apply(el);
return list.stream()
.collect(groupingBy(notNullUniqueKey))
.values()
.stream()
.filter(matches -> matches.size() > 1)
.map(matches -> matches.get(0))
.collect(toList());
}
// Example of usage:
List<Person> duplicates = findDuplicates(list, el -> el.getFirstName());

List<Person> arr = new ArrayList<>();
arr.add(alex);
arr.add(lolita);
arr.add(elpidio);
arr.add(romualdo);
arr.add(otroRomualdo);
Set<String> set = new HashSet<>();
List<Person> result = arr.stream()
.filter(data -> (set.add(data.name +";"+ Long.toString(data.id)) == false))
.collect(Collectors.toList());
arr.removeAll(result);
Set<String> set2 = new HashSet<>();
result.stream().forEach(data -> set2.add(data.name +";"+ Long.toString(data.id)));
List<Person> resultTwo = arr.stream()
.filter(data -> (set2.add(data.name +";"+ Long.toString(data.id)) == false))
.collect(Collectors.toList());
result.addAll(resultTwo);
The above code will filter based on name and id. The result List will have all the duplicated Person Object

Related

How to return a HashMap having enum parameters of the method?

I have method returnSpecificOrder(Type type) (like in code below) where Type is enum class , this method should return only Orders containing item.type = "Clothes" where Item is another class witch have variable public Type type;I try to return orders from hashMap but java says that can't resolve symbol , how I should change my method ?
public class Order {
public long id;
public LocalDate dateTime;
public User user;
public List<Item> items;
//set seters and geters
public HashMap<Long, Order> createOrder() {
Order myFirstOrder = new Order();
myFirstOrder.setId(1);
myFirstOrder.setDateTime(LocalDate.now());
myFirstOrder.setUser(user);
myFirstOrder.setItems(items);
Order mySecondOrder = new Order();
mySecondOrder.setId(2);
mySecondOrder.setDateTime(LocalDate.now());
mySecondOrder.setUser(user);
mySecondOrder.setItems(item2);
//hash map of orders
HashMap<Long, Order> orders = new HashMap<>();
orders.put(myFirstOrder.getId(), myFirstOrder);
orders.put(mySecondOrder.getId(), mySecondOrder);
return orders;
}
//method that will return only Orders containing item.type = "Clothes"
public static Map<Long, Order> returnSpecificOrder(Type type) {
return orders.entrySet()
.stream()
.filter(o -> o.getValue().getItems().stream().anyMatch(item -> item.getType() == Type.clothes))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
} }
your filter should work.
The following is the working codes.
I refactored your Entity classes, and added comments where needed.
class Order {
public long id;
public List<Item> items;
//convenient constructor.
public Order(long id, Type... items) {
this.id = id;
this.items = Arrays.stream(items).map(Item::new).collect(Collectors.toList());
}
}
class Item {
Type type;
public Item(Type type) {
this.type = type;
}
}
enum Type {
Clothes, NotClothes, Car, Bicycle, Shoes
}
Then added your filter method into main
public class Main {
public static void main(String[] args) {
//pre-populate with sample data
Order o1 = new Order(1, Type.Bicycle, Type.Car, Type.Clothes);
Order o2 = new Order(2, Type.NotClothes, Type.Bicycle);
HashMap<Long, Order> orders = new HashMap<>();
orders.put(1L, o1);
orders.put(2L, o2);
//method that will return only Orders containing item.type = "Clothes"
Map<Long, Order> result = orders.entrySet().stream()
.filter(o -> {
//I Split into two lines to make it less cluttered.
List<Item> list = o.getValue().items;
return list.stream().anyMatch(item -> item.type == Type.Clothes);
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
//The answer shows that only 1 item is returned.
System.out.println("result = " + result.size());
}
}

Java Stream Grouping by multiple fields individually in declarative way in single loop

I googled for it but I mostly found cases for grouping by aggregated fields or on to alter response of stream but not the scenario below:
I have a class User with fields category and marketingChannel.
I have to write a method in the declarative style that accepts a list of users and counts users based on
category and also based on marketingChannel individually (i.e not groupingBy(... ,groupingBy(..)) ).
I am unable to do it in a single loop. This is what I have to achieve.
I coded few methods as follows:
import java.util.*;
import java.util.stream.*;
public class Main
{
public static void main(String[] args) {
List<User> users = User.createDemoList();
imperative(users);
declerativeMultipleLoop(users);
declerativeMultipleColumn(users);
}
public static void imperative(List<User> users){
Map<String, Integer> categoryMap = new HashMap<>();
Map<String, Integer> channelMap = new HashMap<>();
for(User user : users){
Integer value = categoryMap.getOrDefault(user.getCategory(), 0);
categoryMap.put(user.getCategory(), value+1);
value = channelMap.getOrDefault(user.getMarketingChannel(), 0);
channelMap.put(user.getMarketingChannel(), value+1);
}
System.out.println("imperative");
System.out.println(categoryMap);
System.out.println(channelMap);
}
public static void declerativeMultipleLoop(List<User> users){
Map<String, Long> categoryMap = users.stream()
.collect(Collectors.groupingBy(
User::getCategory, Collectors.counting()));
Map<String, Long> channelMap = users.stream()
.collect(Collectors.groupingBy(
User::getMarketingChannel, Collectors.counting()));
System.out.println("declerativeMultipleLoop");
System.out.println(categoryMap);
System.out.println(channelMap);
}
public static void declerativeMultipleColumn(List<User> users){
Map<String, Map<String, Long>> map = users.stream()
.collect(Collectors.groupingBy(
User::getCategory,
Collectors.groupingBy(User::getMarketingChannel,
Collectors.counting())));
System.out.println("declerativeMultipleColumn");
System.out.println("groupingBy category and marketChannel");
System.out.println(map);
Map<String, Long> categoryMap = new HashMap<>();
Map<String, Long> channelMap = new HashMap<>();
for (Map.Entry<String, Map<String, Long>> entry: map.entrySet()) {
String category = entry.getKey();
Integer count = entry.getValue().size();
Long value = categoryMap.getOrDefault(category,0L);
categoryMap.put(category, value+count);
for (Map.Entry<String, Long> channelEntry : entry.getValue().entrySet()){
String channel = channelEntry.getKey();
Long channelCount = channelEntry.getValue();
Long channelValue = channelMap.getOrDefault(channel,0L);
channelMap.put(channel, channelValue+channelCount);
}
}
System.out.println("After Implerative Loop on above.");
System.out.println(categoryMap);
System.out.println(channelMap);
}
}
class User{
private String name;
private String category;
private String marketChannel;
public User(String name, String category, String marketChannel){
this.name = name;
this.category = category;
this.marketChannel = marketChannel;
}
public String getName(){
return this.name;
}
public String getCategory(){
return this.category;
}
public String getMarketingChannel(){
return this.marketChannel;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(name, user.name) &&
Objects.equals(category, user.category) &&
Objects.equals(marketChannel, user.marketChannel);
}
#Override
public int hashCode() {
return Objects.hash(name, category, marketChannel);
}
public static List<User> createDemoList(){
return Arrays.asList(
new User("a", "student","google"),
new User("b", "student","bing"),
new User("c", "business","google"),
new User("d", "business", "direct")
);
}
The method declerativeMultipleLoop is declarative but it has a separate loop for each field. Complexity : O(noOfFields * No of users)
The problem is in declerativeMultipleColumn Method as I end up writing imperative code and multiple loops.
I want to write the above method in completely declarative and as efficient as possible. i.e Complexity : O(No of users)
Sample output:
imperative
{business=2, student=2}
{direct=1, google=2, bing=1}
declerativeMultipleLoop
{business=2, student=2}
{direct=1, google=2, bing=1}
declerativeMultipleColumn
groupingBy category and marketChannel
{business={direct=1, google=1}, student={google=1, bing=1}}
After Implerative Loop on above.
{business=2, student=2}
{direct=1, google=2, bing=1}
If I understand your requirement it is to use a single stream operation that results in 2 separate maps. That is going to require a structure to hold the maps and a collector to build the structure. Something like the following:
class Counts {
public final Map<String, Integer> categoryCounts = new HashMap<>();
public final Map<String, Integer> channelCounts = new HashMap<>();
public static Collector<User,Counts,Counts> countsCollector() {
return Collector.of(Counts::new, Counts::accept, Counts::combine, CONCURRENT, UNORDERED);
}
private Counts() { }
private void accept(User user) {
categoryCounts.merge(user.getCategory(), 1, Integer::sum);
channelCounts.merge(user.getChannel(), 1, Integer::sum);
}
private Counts combine(Counts other) {
other.categoryCounts.forEach((c, v) -> categoryCounts.merge(c, v, Integer::sum));
other.channelCounts.forEach((c, v) -> channelCounts.merge(c, v, Integer::sum));
return this;
}
}
That can then be used as a collector:
Counts counts = users.stream().collect(Counts.countsCollector());
counts.categoryCounts.get("student")...
(Opinion only: the distinction between imperative and declarative is pretty arbitrary in this case. Defining stream operations feels pretty procedural to me (as opposed to the equivalent in, say, Haskell)).
You can compute two maps in a single forEach method:
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User("a", "student", "google"),
new User("b", "student", "bing"),
new User("c", "business", "google"),
new User("d", "business", "direct"));
Map<String, Integer> categoryMap = new HashMap<>();
Map<String, Integer> channelMap = new HashMap<>();
// group users into maps
users.forEach(user -> {
categoryMap.compute(user.getCategory(),
(key, value) -> value == null ? 1 : value + 1);
channelMap.compute(user.getChannel(),
(key, value) -> value == null ? 1 : value + 1);
});
// output
System.out.println(categoryMap); // {business=2, student=2}
System.out.println(channelMap); // {direct=1, google=2, bing=1}
}
static class User {
private final String name, category, channel;
public User(String name, String category, String channel) {
this.name = name;
this.category = category;
this.channel = channel;
}
public String getName() { return this.name; }
public String getCategory() { return this.category; }
public String getChannel() { return this.channel; }
}

Person objects contain EnumSet. How to convert List<Person> to EnumMap<EnumSet.values, List<Person.name>> using aggregate functions / lambdas

I'm trying to learn aggregate functions and lambdas in Java. I have a class:
public class Person {
public enum Privilege{
PRIV1, PRIV2, PRIV3, PRIV4, PRIV4
}
private String name;
private Set<Privilege> privileges;
...
}
and a list of objects of this class.
I want to convert it to EnumMap<Privilege, List<String>>
where List contains names of all people having certain privilege. I created a method to do this:
public static Map<Privilege,List<String>> personsByPrivilege(List<Person> personList){
Map<Privilege, List<String>> resultMap = new EnumMap(Privilege.class);
Arrays.asList(Privilege.values())
.stream()
.forEach(p->resultMap.put(p,new ArrayList<String>()));
for(Person p :personList){
Set<Privilege> personsPrivileges = p.getPrivileges();
for(Privilege pr : personsPrivileges){
resultMap.get(pr).add(p.getName());
}
}
return resultMap;
}
How do I do it using aggregate functions?
I mean e.g. personlist.stream().collect style
You could flatten the list of person -> list of privileges into pairs, then groupby by the privilege, and map with the name
public static Map<Person.Privilege, List<String>> personsByPrivilegeB(List<Person> personList) {
return personList.stream()
.flatMap(pers -> pers.getPrivileges().stream().map(priv -> new Object[]{priv, pers.getName()}))
.collect(groupingBy(o -> (Person.Privilege) o[0], mapping(e -> (String) e[0], toList())));
}
You can add a Pair class and use the below code for achieving your goal
return personList.stream().flatMap(p -> {
String name = p.getName();
return p.getPrivileges().stream()
.flatMap(priv -> Stream.of(new NamePriviledge(priv, name)));
}).collect(Collectors.groupingBy(NamePriviledge::getPrivilege, Collectors.mapping(NamePriviledge::getName, Collectors.toList())));
}
class NamePriviledge {
private final Person.Privilege privilege;
private final String name;
public NamePriviledge(Person.Privilege privilege, String name) {
this.privilege = privilege;
this.name = name;
}
public String getName() {
return name;
}
public Person.Privilege getPrivilege() {
return privilege;
}
#Override
public String toString() {
return "NamePriviledge{" +
"privilege=" + privilege +
", name='" + name + '\'' +
'}';
}
}

How to filter map collection with helper method usage which prevent nullPointerException

I have a method like:
static List<Employee> getEmployeesByFactoryAndTask(
ProtectedMap<String, Factory> factoryMap, String factoryName, Task task) {
return factoryMap.values().stream()
.filter(factory -> factory.getName().equals(factoryName))
.map(Factory::getEmployees)
.flatMap(Optional::stream)
.flatMap(Collection::stream)
.filter(employee -> employee.getTask().equals(task))
.collect(Collectors.toList());
}
which assume filter Employees collection by factoryName and Task enum parameters.
Above code works fine but I want to use helper method find in my ProtectedMap class like:
public class ProtectedMap<T, U> extends HashMap<T, U> {
public Optional<U> find(T key) {
return Optional.ofNullable(super.get(key));
}
}
to get the same result.
So far my method looks like:
return factoryMap
.find(factoryName)
.stream()
.map(Factory::getEmployees)
.flatMap(Optional::stream)
.flatMap(Collection::stream)
.collect(Collectors.toMap(Employee::getTask, Function.identity()));
but I don't know where I suppose to put the second method call find which will filter by Task to get desribale effect. As a side note, Factory contains two fields: Optional<List<Employees>> and String which is a name of factory.
I will be grateful for a suggestion on how to solve a problem.
Below is the code which handles your requirement but getEmployeesByFactoryAndTask also returns Optional<List<Employee>> and as I can understand this return value can be handled by you.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.*;
enum Task {
QA, DEVELOPEMENT, DESIGN
}
class Employee {
long id;
String name;
Task task;
Employee(long id, String name, Task task) {
this.id = id;
this.name = name;
this.task = task;
}
Task getTask() {
return task;
}
}
class Factory {
List<Employee> employees;
Factory() {}
Factory(List<Employee> employees) {
this.employees = employees;
}
public Optional<List<Employee>> getEmployees() {
return Optional.ofNullable(employees);
}
}
class ProtectedMap<T, U> extends HashMap<T, U> {
public Optional<U> find(T key) {
return Optional.ofNullable(super.get(key));
}
}
public class Main57989215 {
public static void main(String[] args) {
List<Employee> employees1 = new ArrayList<>();
employees1.add(new Employee(21, "Alen", Task.DESIGN));
employees1.add(new Employee(31, "Herry", Task.QA));
employees1.add(new Employee(41, "John", Task.DEVELOPEMENT));
employees1.add(new Employee(51, "Martin", Task.QA));
employees1.add(new Employee(61, "Jack", Task.DESIGN));
List<Employee> employees2 = new ArrayList<>();
employees2.add(new Employee(121, "John", Task.DESIGN));
employees2.add(new Employee(131, "Porter", Task.QA));
employees2.add(new Employee(141, "Cena", Task.DEVELOPEMENT));
employees2.add(new Employee(151, "Luther", Task.QA));
employees2.add(new Employee(161, "Dinga", Task.DESIGN));
ProtectedMap<String, Factory> factoryMap = new ProtectedMap<>();
factoryMap.put("factory1", new Factory(employees1));
factoryMap.put("factory2", new Factory(employees2));
factoryMap.put("factory3", new Factory());
System.out.println(getEmployeesByFactoryAndTask(factoryMap, "factory1", Task.QA));
System.out.println(getEmployeesByFactoryAndTask(factoryMap, "factory1", Task.QA));
System.out.println(getEmployeesByFactoryAndTask(factoryMap, "factory2", Task.DEVELOPEMENT));
System.out.println(getEmployeesByFactoryAndTask(factoryMap, "factory3", Task.DESIGN));
System.out.println(getEmployeesByFactoryAndTask(factoryMap, "factory4", Task.QA));
}
static Optional<List<Employee>> getEmployeesByFactoryAndTask(ProtectedMap<String, Factory> factoryMap, String factoryName, Task task) {
return Optional.ofNullable(factoryMap.find(factoryName)).orElse(Optional.empty())
.flatMap(factory -> Optional.ofNullable(factory.getEmployees())
.orElse(Optional.empty()).map(employees -> employees.stream()
.filter(employee -> employee.getTask().equals(task)).collect(Collectors.toList())));
}
}

Java-8: stream or simpler solution?

I have two models, a List<ModelA> and I want to convert it to a List<ModelB>.
Here are my models:
class ModelA {
private Long id;
private String name;
private Integer value;
public ModelA(Long id, String name, Integer value) {
this.id = id;
this.name = name;
this.value = value;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public Integer getValue() {
return value;
}
}
class ModelB {
private Long id;
private Map<String, Integer> valuesByName;
public ModelB(Long id, Map<String, Integer> valuesByName) {
this.id = id;
this.valuesByName = valuesByName;
}
public Long getId() {
return id;
}
public Map<String, Integer> getValuesByName() {
return valuesByName;
}
}
Actual solution:
public static List<ModelB> convert(List<ModelA> models) {
List<ModelB> toReturn = new ArrayList<>();
Map<Long, Map<String, Integer>> helper = new HashMap<>();
models.forEach(modelA -> {
helper.computeIfAbsent(modelA.getId(), value -> new HashMap<>())
.computeIfAbsent(modelA.getName(), value -> modelA.getValue());
});
helper.forEach((id, valuesByName) -> toReturn.add(new ModelB(id,valuesByName)));
return toReturn;
}
But I think there is a simpler solution, do you have any idea how can I do it in a single stream, or simplify it somehow?
EDIT: I want to clarify that I cannot use java9 and I need to group them by Id-s then by Name. If in ModelB I have 4 elements with the same id I don't want new instances of ModelA.
I have combined both operations, but still constructs the intermediate map as you need to group all name, value pairs for a given id.
models.stream()
.collect(Collectors.groupingBy(model -> model.getId(), //ModelA::getId - Using method reference
Collectors.toMap(model -> model.getName(), model -> model.getValue(), (map1, map2) -> map1)))
.entrySet()
.stream()
.map(entry -> new ModelB(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
EDIT:
I missed (map1, map2) -> map1 in the initial answer. It is needed to avoid overwriting the already existing value for a id, name(the equivalent of your second computeIfAbsent in your code)
You need to choose one of them (or mege them), as by default it throws IllegalStateException when it finds a duplicate key.
This is easily achieved using the map function from Stream:
public static List<MobelB> convert(List<ModelA> models) {
Map<Long, Map<String, Integer>> modelAMap = models.stream()
.collect(Collectors.toMap(ModelA::getId, modelA -> computeMap(modelA)));
return models.stream()
.map(modelA -> new ModelB(modelA.getId(), modelAMap.get(modelA.getId())))
.collect(Collectors.toList());
}
private static Map<String, Integer> computeMap(ModelA model) {
Map<String, Integer> map = new HashMap<>();
map.put(model.getId(), model.getName());
return map;
}

Categories