Java 8 extract all keys from matching values in a Map - java

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();

Related

Merge two collections in Java

I have two maps:
Map<String, Student> students1 = new HashMap<>();
students1.put("New York", new Student("John"));
students1.put("Canada", new Student("Robert"));
Map<String, Student> students2 = new HashMap<>();
students2.put("Chicago", new Student("Nick"));
students2.put("New York", new Student("Ann"));
As a result, I want to get this:
{Canada=Robert, New York=[John, Ann], Chicago=Nick}
I can easily do it like this:
Map<City, List<Student>> allStudents = new HashMap<>();
students1.forEach((currentCity, currentStudent) -> {
allStudents.computeIfPresent(currentCity, (city, studentsInCity) -> {
studentsInCity.add(currentStudent);
return studentsInCity;
});
allStudents.putIfAbsent(currentCity, new ArrayList<Student>() {
{
add(currentStudent);
}
});
});
// then again for the second list
But is there any other way to merge many collections (two in this case)? Is there something like short lambda expression, or method from some of the integrated java libraries, etc...?
You could create a stream over any number of maps, then flat map over their entries. It is then as simple as grouping by the key of the entry, with the value of the entry mapped to a List as value:
Map<String, List<Student>> collect = Stream.of(students1, students2)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
With static import for readability:
Map<String, List<Student>> collect = Stream.of(students1, students2)
.flatMap(map -> map.entrySet().stream())
.collect(groupingBy(Entry::getKey, mapping(Entry::getValue, toList())));
Replace toList() with toSet() if a Set is more appropriate as value of the map.
I think the Stream version given by Magnilex is the most elegant way to do this. But I still want to give another choice.
static final Function<...> NEW_LIST = __ -> new ArrayList<>();
Map<City, List<Student>> allStudents = new HashMap<>();
students1.forEach((city, student) -> {
allStudents.computeIfAbsent(city, NEW_LIST).add(student);
});
https://www.baeldung.com/java-merge-maps this is a link, it should help

Filter data in a list(List1) using a inputMap(key and values may vary for each request)

class Person
{
private String name;
private String birthDate;
private String city;
private String state;
private int zipCode;
}
Map<String, String> inputMap = new HashMap<>();
inputMap.put(“name”, “David”);
Map<String, String> inputMap1 = new HashMap<>();
inputMap1.put(“name”, “David”);
inputMap1.put(“city”, “Auburn”);
I’ll get List of Persons from DB and below map is the input (this inputMap is dynamic. We may get just city or city & zipCode or any combination of the above 5 properties defined in Person object)
I need to filter List of Persons matching with the inputMap using streams. I tried different ways using java stream but no luck, please help.
You can apply a filter for each possible key in the Map (i.e. you'll need 5 filter operations):
List<Person> input = ...
List<Person> filtered = input.stream()
.filter(p -> !inputMap.containsKey("name") || p.getName().equals(inputMap.get("name")))
.filter(p -> !inputMap.containsKey("city") || p.getCity().equals(inputMap.get("city")))
...
.collect(Collectors.toList());
If you want to generalize this for an arbitrary number of Map keys, you'll need another Map that maps the keys to corresponding properties of the Person.
For example, if you had a:
Map<String,Function<Person,Object>> propMap = new HashMap<>();
propMap.put ("name",Person::getName);
propMap.put ("city",Person::getCity);
...
You could write:
List<Person> filtered = input.stream()
.filter(p -> inputMap.entrySet()
.stream()
.allMatch(e -> propMap.get(e.getKey()).apply(p).equals(e.getValue())))
.collect(Collectors.toList());
This means that for each key of inputMap, the corresponding property of a Person instance (which is obtained via propMap.get(key).apply(p) where p is the Person) must be equal to the value of that key.

Finding duplicated objects by two properties

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.

Java8 convert List of Map to List of string

I am using Java8 to achieve the below things,
Map<String, String> m0 = new HashMap<>();
m0.put("x", "123");
m0.put("y", "456");
m0.put("z", "789");
Map<String, String> m1 = new HashMap<>();
m1.put("x", "000");
m1.put("y", "111");
m1.put("z", "222");
List<Map<String, String>> l = new ArrayList<>(Arrays.asList(m0, m1));
List<String> desiredKeys = Lists.newArrayList("x");
List<Map<String, String>> transformed = l.stream().map(map -> map.entrySet().stream()
.filter(e -> desiredKeys.stream().anyMatch(k -> k.equals(e.getKey())))
.collect(Collectors.toMap(e -> e.getKey(), p -> p.getValue())))
.filter(m -> !m.isEmpty())
.collect(Collectors.toList());
System.err.println(l);
System.err.println(transformed);
List<String> values = new ArrayList<>();
for (Map<String,String> map : transformed) {
values.add(map.values().toString());
System.out.println("Values inside map::"+map.values());
}
System.out.println("values::"+values); //values::[[123], [000]]
Here, I would like to fetch only the x-values from the list. I have achieved it but it is not in a proper format.
Expected output:
values::[123, 000]
Actual output:
values::[[123], [000]]
I know how to fix the actual output. But is there any easy way to achieve this issue? Any help would be appreciable.
You do not need to iterate over the entire map to find an entry by its key. That's what Map.get is for. To flatten the list of list of values, use flatMap:
import static java.util.stream.Collectors.toList;
.....
List<String> values = l.stream()
.flatMap(x -> desiredKeys.stream()
.filter(x::containsKey)
.map(x::get)
).collect(toList());
On a side note, avoid using l (lower case L) as a variable name. It looks too much like the number 1.
I’m not sure Streams will help, here. It’s easier to just loop through the Maps:
Collection<String> values = new ArrayList<>();
for (Map<String, String> map : l) {
Map<String, String> copy = new HashMap<>(map);
copy.keySet().retainAll(desiredKeys);
values.addAll(copy.values());
}
Flat map over the stream of maps to get a single stream representing the map entries of all your input maps. From there, you can filter out each entry whose key is not contained in the desired keys. Finally, extract the equivalent value of each entry to collect them into a list.
final List<String> desiredValues = l.stream()
.map(Map::entrySet)
.flatMap(Collection::stream)
.filter(entry -> desiredKeys.contains(entry.getKey()))
.map(Map.Entry::getValue)
.collect(Collectors.toList());
EDIT
This assumes that if a map has the key "x" it must also has the key "y" so to fetch the corredponding value.
final List<String> desiredValues = l.stream()
.filter(map -> map.containsKey("x"))
.map(map -> map.get("y"))
.collect(Collectors.toList());

Java 8 mapping to sub list entries of a collection using streams and collectors

I have a collection of Person objects:.
public class Person {
String name;
ChildrenListHolder childrenListHolder;
}
public class ChildrenListHolder {
List<Children> children;
}
public class Children {
String childrensName;
}
(The entity structure is given by third party.)
Now, I need a Map<String,List<Person>> childrensName -> person-list
For example (simplified):
Person father: {name: "John", childrensListHolder -> {"Lisa", "Jimmy"}}
Person mother: {name: "Clara", childrensListHolder -> {"Lisa", "Paul"}}
Person george: {name: "George", childrensListHold -> "Paul"}}
The map, I need is
Map<String, List<Person>> map: {"Lisa" -> {father, mother},
"Jimmy" -> {father},
"Paul" -> {mother, george}}
I can do that with a bunch of for's and if's. But how can I do this using streams and collectors. I have tried many approaches, but I cannot get the expected result. TIA.
Given a List<Person> persons, you can have the following
Map<String,List<Person>> map =
persons.stream()
.flatMap(p -> p.childrenListHolder.children.stream().map(c -> new AbstractMap.SimpleEntry<>(c, p)))
.collect(Collectors.groupingBy(
e -> e.getKey().childrensName,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())
));
This is creating a Stream over the persons. Then each person is flat mapped by a tuple holding the child and the person for each child. Finally, we group by the child name and collect all the persons into a list.
Sample code assuming there are appropriate constructors:
public static void main(String[] args) {
List<Person> persons = Arrays.asList(
new Person("John", new ChildrenListHolder(Arrays.asList(new Children("Lisa"), new Children("Jimmy")))),
new Person("Clara", new ChildrenListHolder(Arrays.asList(new Children("Lisa"), new Children("Paul")))),
new Person("George", new ChildrenListHolder(Arrays.asList(new Children("Paul"))))
);
Map<String,List<Person>> map =
persons.stream()
.flatMap(p -> p.childrenListHolder.children.stream().map(c -> new AbstractMap.SimpleEntry<>(c, p)))
.collect(Collectors.groupingBy(
e -> e.getKey().childrensName,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())
));
System.out.println(map);
}
I can do that with a bunch of for's and if's.
I know that you asked for a stream/collectors solution, but in any case a nested for loop using Map#computeIfAbsent works fine too:
Map<String, List<Person>> map = new HashMap<>();
for(Person p : persons) {
for(Children c : p.childrenListHolder.children) {
map.computeIfAbsent(c.childrensName, k -> new ArrayList<>()).add(p);
}
}
and this written using the new forEach method introduced on collections:
Map<String, List<Person>> map = new HashMap<>();
persons.forEach(p -> p.childrenListHolder.children.forEach(c -> map.computeIfAbsent(c.childrensName, k -> new ArrayList<>()).add(p)));
Of course it's not a one-liner nor easy parallelizable like in Tunaki's solution (+1), but you don't need a "bunch" of if's to achieve that too (and you avoid also the creation of temporary map entries instances).

Categories