Considering that I have a list of Person objects like this :
Class Person {
String fullName;
String occupation;
String hobby;
int salary;
}
Using java8 streams, how can I get list of duplicated objects only by fullName and occupation property?
By using java-8 Stream() and Collectors.groupingBy() on firstname and occupation
List<Person> duplicates = list.stream()
.collect(Collectors.groupingBy(p -> p.getFullName() + "-" + p.getOccupation(), Collectors.toList()))
.values()
.stream()
.filter(i -> i.size() > 1)
.flatMap(j -> j.stream())
.collect(Collectors.toList());
I need to find if they were any duplicates in fullName - occupation pair, which has to be unique
Based on this comment it seems that you don't really care about which Person objects were duplicated, just that there were any.
In that case you can use a stateful anyMatch:
Collection<Person> input = new ArrayList<>();
Set<List<String>> seen = new HashSet<>();
boolean hasDupes = input.stream()
.anyMatch(p -> !seen.add(List.of(p.fullName, p.occupation)));
You can use a List as a 'key' for a set which contains the fullName + occupation combinations that you've already seen. If this combination is seen again you immediately return true, otherwise you finish iterating the elements and return false.
I offer solution with O(n) complexity. I offer to use Map to group given list by key (fullName + occupation) and then retrieve duplicates.
public static List<Person> getDuplicates(List<Person> persons, Function<Person, String> classifier) {
Map<String, List<Person>> map = persons.stream()
.collect(Collectors.groupingBy(classifier, Collectors.mapping(Function.identity(), Collectors.toList())));
return map.values().stream()
.filter(personList -> personList.size() > 1)
.flatMap(List::stream)
.collect(Collectors.toList());
}
Client code:
List<Person> persons = Collections.emptyList();
List<Person> duplicates = getDuplicates(persons, person -> person.fullName + ':' + person.occupation);
First implement equals and hashCode in your person class and then use.
List<Person> personList = new ArrayList<>();
Set<Person> duplicates=personList.stream().filter(p -> Collections.frequency(personList, p) ==2)
.collect(Collectors.toSet());
If objects are more than 2 then you use Collections.frequency(personList, p) >1 in filter predicate.
Related
I'm doing the below two operations
Iterating through a list of Objects and creating a map of String, Boolean based on a condition.
Map<String,Boolean> myMap = new HashMap<>();
Iterator<Person> iterator = personList.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
if (isValidperson(person)) {
if (person.getName() != null) {
myMap.put(person.getName(), true);
} else {
myMap.put(person.getName(), false);
}
}
}
Now Im checking a list of Names against that map that I created above and if the value is true then adding to a final list
List<String> refinedList = new ArrayList<>();
for (String name : nameList) {
if (myMap.get(name) != null && myMap.get(name)) {
refinedList.add(name);
}
}
I need to simplify the logic using Java streams. The above works fine otherwise.
In the first operation you are filtering out all the non-valid persons, and collecting the valid persons to a map, so:
Map<String,Boolean> myMap = personList.stream()
.filter(YourClass::isValidPerson)
.collect(Collectors.toMap(x -> x.getName(), x -> x.getName() != null))
But really though, the map is going to have at most one false entry, since you can't add multiple nulls into a HashMap, so there isn't much point in using a HashMap at all.
I suggest using a HashSet:
Set<String> mySet = personList.stream()
.filter(YourClass::isValidPerson)
.map(Person::getName)
.filter(Objects::nonNull)
.collect(Collectors.toSet())
And then you can easily check contains with O(1) time:
List<String> refinedList = nameList.stream().filter(mySet::contains).collect(Collectors.toList());
You can directly filter the list by checking contains in nameList and collect the names in list
List<String> refinedList =
personList.stream()
.filter(e -> isValidperson(e))
.map(e -> e.getName())
.filter(Objects::nonNull)
.distinct()
.filter(e -> nameList.contains(e))
.collect(Collectors.toList());
And it better to create a set from nameList to make the contains() operation faster in O(1)
Set<String> nameSet = new HashSet<String>(nameList);
Note: This will works if nameList doesn't contains duplicate.
This should work.
First, create a list of People.
List<Person> personList = List.of(new Person("Joe"),
new Person(null), new Person("Barb"), new Person("Anne"), new Person("Gary"));
Then the nameList. Note it is best to put this in a set to
avoid duplicates, and
make the lookup process more efficient.
Set<String> nameSet = Set.of("Joe", "Anne", "Ralph");
Now this works by
filtering on a valid vs invalid person.
mapping those people to a name
filtering on whether null and then if the name is in the set of names
and placing in a list.
Note: In some cases, lambdas could be replaced by Method References depending on method types and calling contexts.
List<String> names = personList.stream()
.filter(person -> isValidperson(person))
.map(Person::getName)
.filter(name -> name != null && nameSet.contains(name))
.collect(Collectors.toList());
System.out.println(names);
Prints
[Joe, Anne]
Dummy method since criteria not provided
public static boolean isValidperson(Person person) {
return true;
}
Simple person class
class Person {
String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
I have looked on other topics but I cant find the solutions for my problem. I have a map that contains a key = cartID for example C01 and a ArrayList of products. I want to filter this map on another ArrayList that contains productIDs so I can find all the carts that contain these products. I have following code
Map<String, List<OrderLineDTO>> allCarts //contains data these are fetched
List<String> cart; //also contains fetched data for example ["Product01", "Product02"]
allCarts.values().stream().filter(list -> list.stream().allMatch(value -> cart.contains(value.getProductId()))).collect(Collectors.toList()); //filter I'm using but not working
Any ideas?
Thanks!
If you want to find IDs of the carts containing products only in the list, this should work:
List<String> cartIds = allCarts
.entrySet().stream()
.filter(e ->
e.getValue().stream().allMatch(p -> productIds.contains(p.getProductId())))
.map(e -> e.getKey())
.collect(Collectors.toList());
So the only difference is that I used entrySet() instead of values().
To find carts, containing at least one product in the list, .anyMatch predicate needs to be used.
Try to use entrySet() and anyMatch:
allCarts.entrySet().stream().filter(p -> cart.stream().anyMatch(prod -> p.getValue().contains(prod.get))).collect(Collectors.toList());
The following returns a list of all carts int the list that contain the specified product ids. Included is an OrderLineDTO class to fulfill the demo.
Map<String, List<OrderLineDTO>> allCarts = new HashMap<>();
List<String> cart = new ArrayList<>(); // also contains fetched data for example ["Product01", "Product02"]
cart.add("ID7");
cart.add("ID1");
allCarts.put("cart1", List.of(new OrderLineDTO("ID1"),
new OrderLineDTO("ID2")));
allCarts.put("cart2", List.of(new OrderLineDTO("ID3"),
new OrderLineDTO("ID5")));
allCarts.put("cart3", List.of(new OrderLineDTO("ID3"),
new OrderLineDTO("ID4")));
allCarts.put("cart4", List.of(new OrderLineDTO("ID6"),
new OrderLineDTO("ID7")));
List<String> carts = allCarts.entrySet().stream()
.filter(e -> e.getValue().stream().anyMatch(
prod -> cart.contains(prod.getProductId())))
.map(e -> e.getKey()).collect(Collectors.toList());
System.out.println(carts);
Prints
[cart1, cart4]
class OrderLineDTO {
String id;
public OrderLineDTO(String id) {
this.id = id;
}
public String getProductId() {
return id;
}
public String toString() {
return id;
}
}
}
I had to use containsAll to find my answer. Thanks everyone for the help!
List<String> carts = allCarts.entrySet().stream()
.filter(e -> (e.getValue().stream().map(f -> f.getProductId()).collect(Collectors.toList()).containsAll(cart)))
.map(e -> e.getKey()).collect(Collectors.toList());
I think this one is more simple, filters if value equal x then returns a list of keys.
List<String> result = map.entrySet().stream()
.filter(e -> e.getValue().equals(x))
.map(e -> e.getKey()).collect(Collectors.toList());
Starting point:
public class Employee {
private String id;
private String name;
private String age;
}
I have a list of Employee: List<Employee> employee;
Employee examples from the list:
{id="1", name="John", age=10}
{id="2", name="Ana", age=12}
{id="3", name="John", age=23}
{id="4", name="John", age=14}
Let's assume that age is unique.
How can I remove all duplicates from the list based on the name property and to keep in the output the entry with the largest age?
The output should look like:
{id="2", name="Ana", age=12}
{id="3", name="John", age=23}
The way I tried:
HashSet<Object> temp = new HashSet<>();
employee.removeIf(e->!temp.add(e.getName()));
..but the this way the first match will be kept in employee
{id="1", name="John", age=10}
{id="2", name="Ana", age=12}
...and I have no idea how to put an another condition here to keep the one with the largest age.
Here's a way that groups elements by name and reduces groups by selecting the one with max age:
List<Employee> uniqueEmployees = employees.stream()
.collect(Collectors.groupingBy(Employee::getName,
Collectors.maxBy(Comparator.comparing(Employee::getAge))))
.values()
.stream()
.map(Optional::get)
.collect(Collectors.toList());
Which returns [[id=2, name=Ana, age=12], [id=3, name=John, age=23]] with your test data.
Apart from the accepted answer, here are two variants:
Collection<Employee> employeesWithMaxAge = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(Employee::getAge))))
.values();
This one uses Collectors.toMap to group employees by name, letting Employee instances as the values. If there are employees with the same name, the 3rd argument (which is a binary operator), selects the employee that has max age.
The other variant does the same, but doesn't use streams:
Map<String, Employee> map = new LinkedHashMap<>(); // preserves insertion order
employees.forEach(e -> map.merge(
e.getName(),
e,
(e1, e2) -> e1.getAge() > e2.getAge() ? e1 : e2));
Or, with BinaryOperator.maxBy:
Map<String, Employee> map = new LinkedHashMap<>(); // preserves insertion order
employees.forEach(e -> map.merge(
e.getName(),
e,
BinaryOperator.maxBy(Comparator.comparing(Employee::getAge))));
ernest_k answer is great but if you maybe want to avoid adding duplicates you can use this:
public void addToEmployees(Employee e) {
Optional<Employee> alreadyAdded = employees.stream().filter(employee -> employee.getName().equals(e.getName())).findFirst();
if(alreadyAdded.isPresent()) {
updateAgeIfNeeded(alreadyAdded.get(), e);
}else {
employees.add(e);
}
}
public void updateAgeIfNeeded(Employee alreadyAdded, Employee newlyRequested) {
if(Integer.valueOf(newlyRequested.getAge()) > Integer.valueOf(alreadyAdded.getAge())) {
alreadyAdded.setAge(newlyRequested.getAge());
}
}
Just use addToEmployees method to add Employee to your list.
You can also create class extending ArrayList and override add method like so and then use your own list :)
you can add values in a Map<String, Employee> (where string is name) only if age is greater of equals than the one in the map.
I'm relatively new to Java8 and I have a scenario where I need to retrieve all the keys from the Map which matched with the objects.
Wanted to know if there is a way to get all keys without iterating them from the list again.
Person.java
private String firstName;
private String lastName;
//setters and getters & constructor
MAIN Class.
String inputCriteriaFirstName = "john";
Map<String, Person> inputMap = new HashMap<>();
Collection<Person> personCollection = inputMap.values();
List<Person> personList = new ArrayList<>(personCollection);
List<Person> personOutputList = personList.stream()
.filter(p -> p.getFirstName().contains(inputCriteriaFirstName ))
.collect(Collectors.toList());
//IS There a BETTER way to DO Below ??
Set<String> keys = new HashSet<>();
for(Person person : personOutputList) {
keys.addAll(inputMap.entrySet().stream().filter(entry -> Objects.equals(entry.getValue(), person))
.map(Map.Entry::getKey).collect(Collectors.toSet()));
}
inputMap.entrySet()
.stream()
.filter(entry -> personOutputList.contains(entry.getValue()))
.map(Entry::getKey)
.collect(Collectors.toCollection(HashSet::new))
Instead of iterating over all the entries of the Map for each Person, I suggest iterating over the Map once:
Set<String> keys =
inputMap.entrySet()
.stream()
.filter(e -> personOutputList.contains(e.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toCollection(HashSet::new));
This would still result in quadratic running time (since List.contains() has linear running time). You can improve that to overall linear running time if you create a HashSet containing the elements of personOutputList, since contains for HashSet takes constant time.
You can achieve that by changing
List<Person> personOutputList =
personList.stream()
.filter(p -> p.getFirstName().contains(inputCriteriaFirstName))
.collect(Collectors.toList());
to
Set<Person> personOutputSet =
personList.stream()
.filter(p -> p.getFirstName().contains(inputCriteriaFirstName))
.collect(Collectors.toCollection(HashSet::new));
You can also use foreach api provided in java8 under lambda's
Below is code for your main method :
public static void main() {
String inputCriteriaFirstName = "john";
Map<String, Person> inputMap = new HashMap<>();
Set<String> keys = new HashSet<>();
inputMap.forEach((key,value) -> {
if(value.getFirstName().contains(inputCriteriaFirstName)){
keys.add(key);
}
});
}
So, you want a personOutputList with all the selected persons, and a keys set with the keys for those selected persons?
Best (for performance) option is to not discard the keys during search, then split the result into separate person list and key set.
Like this:
String inputCriteriaFirstName = "john";
Map<String, Person> inputMap = new HashMap<>();
Map<String, Person> tempMap = inputMap.entrySet()
.stream()
.filter(e -> e.getValue().getFirstName().contains(inputCriteriaFirstName))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
List<Person> personOutputList = new ArrayList<>(tempMap.values());
Set<String> keys = new HashSet<>(tempMap.keySet());
The keys set is explicitly made an updatable copy. If you don't need that, drop the copying of the key values:
Set<String> keys = tempMap.keySet();
I have a Person object which has a name attribute and some other attributes. I have two HashSet with Person objects. Note that name is not an unique attribute meaning that two Persons with same name can have different height so using HashSet does not guarantee that two Persons with same name are not in the same set.
I need to add one set to another so there are no Persons in the result with the same name. So something like this:
public void combine(HashSet<Person> set1, HashSet<Person> set2){
for (String item2 : set2) {
boolean exists = false;
for (String item1 : set1) {
if(item2.name.equals(item1.name)){
exists = true;
}
}
if(!exists){
set1.add(item2);
}
}
}
Is there a cleaner way of doing this in java8?
set1.addAll(set2.stream().filter(e -> set1.stream()
.noneMatch(p -> p.getName().equals(e.getName())))
.collect(Collectors.toSet()));
If it makes sense for you to override equals and hashCode you can use something like this:
Set<Parent> result = Stream.concat(set1.stream(), set2.stream())
.collect(Collectors.toSet());
Without the Java 8 streams you can easily just do this:
Set<Parent> result = new HashSet<>();
result.addAll(set1);
result.addAll(set2);
But remember this solution is only feasible when it makes sense to have equals and hashCode overridden.
`
You can use a HashMap with name as key, then you avoid the O(n²) runtime complexity of your method. If you need HashSet, then there is no faster way. Even if you use Java 8 Streams. They add just more overhead.
public Map<String, Person> combine(Set<Person> set1, Set<Person> set2) {
Map<String, Person> persons = new HashMap<>();
set1.forEach(pers -> persons.computeIfAbsent(pers.getName(), key -> pers));
set2.forEach(pers -> persons.computeIfAbsent(pers.getName(), key -> pers));
return persons;
}
Alternatively, you could create your own collector. Assuming that you're certain that two persons with the same name are in fact the same person:
First you can define a collector:
static Collector<Person, ?, Map<String, Person>> groupByName() {
return Collector.of(
HashMap::new,
(a,b) -> a.putIfAbsent(b.name, b),
(a,b) -> { a.putAll(b); return a;}
);
}
Then you can use it to group persons by name:
Stream.concat(s1.stream(), s2.stream())
.collect(groupByName());
However, this would give you a Map<String, Person> and you just want the whole set of Persons found, right?
So, you could just do:
Set<Person> p = Stream.concat(s1.stream(), s2.stream())
.collect(collectingAndThen(groupByName(), p -> new HashSet<>(p.values())));