Combining to operations using java streams - java

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;
}
}

Related

Adding two lists of own type

I have a simple User class with a String and an int property.
I would like to add two Lists of users this way:
if the String equals then the numbers should be added and that would be its new value.
The new list should include all users with proper values.
Like this:
List1: { [a:2], [b:3] }
List2: { [b:4], [c:5] }
ResultList: {[a:2], [b:7], [c:5]}
User definition:
public class User {
private String name;
private int comments;
}
My method:
public List<User> addTwoList(List<User> first, List<User> sec) {
List<User> result = new ArrayList<>();
for (int i=0; i<first.size(); i++) {
Boolean bsin = false;
Boolean isin = false;
for (int j=0; j<sec.size(); j++) {
isin = false;
if (first.get(i).getName().equals(sec.get(j).getName())) {
int value= first.get(i).getComments() + sec.get(j).getComments();
result.add(new User(first.get(i).getName(), value));
isin = true;
bsin = true;
}
if (!isin) {result.add(sec.get(j));}
}
if (!bsin) {result.add(first.get(i));}
}
return result;
}
But it adds a whole lot of things to the list.
This is better done via the toMap collector:
Collection<User> result = Stream
.concat(first.stream(), second.stream())
.collect(Collectors.toMap(
User::getName,
u -> new User(u.getName(), u.getComments()),
(l, r) -> {
l.setComments(l.getComments() + r.getComments());
return l;
}))
.values();
First, concatenate both the lists into a single Stream<User> via Stream.concat.
Second, we use the toMap collector to merge users that happen to have the same Name and get back a result of Collection<User>.
if you strictly want a List<User> then pass the result into the ArrayList constructor i.e. List<User> resultSet = new ArrayList<>(result);
Kudos to #davidxxx, you could collect to a list directly from the pipeline and avoid an intermediate variable creation with:
List<User> result = Stream
.concat(first.stream(), second.stream())
.collect(Collectors.toMap(
User::getName,
u -> new User(u.getName(), u.getComments()),
(l, r) -> {
l.setComments(l.getComments() + r.getComments());
return l;
}))
.values()
.stream()
.collect(Collectors.toList());
You have to use an intermediate map to merge users from both lists by summing their ages.
One way is with streams, as shown in Aomine's answer. Here's another way, without streams:
Map<String, Integer> map = new LinkedHashMap<>();
list1.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
list2.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
Now, you can create a list of users, as follows:
List<User> result = new ArrayList<>();
map.forEach((name, comments) -> result.add(new User(name, comments)));
This assumes User has a constructor that accepts name and comments.
EDIT: As suggested by #davidxxx, we could improve the code by factoring out the first part:
BiConsumer<List<User>, Map<String, Integer>> action = (list, map) ->
list.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
Map<String, Integer> map = new LinkedHashMap<>();
action.accept(list1, map);
action.accept(list2, map);
This refactor would avoid DRY.
There is a pretty direct way using Collectors.groupingBy and Collectors.reducing which doesnt require setters, which is the biggest advantage since you can keep the User immutable:
Collection<Optional<User>> d = Stream
.of(first, second) // start with Stream<List<User>>
.flatMap(List::stream) // flatting to the Stream<User>
.collect(Collectors.groupingBy( // Collecting to Map<String, List<User>>
User::getName, // by name (the key)
// and reducing the list into a single User
Collectors.reducing((l, r) -> new User(l.getName(), l.getComments() + r.getComments()))))
.values(); // return values from Map<String, List<User>>
Unfortunately, the result is Collection<Optional<User>> since the reducing pipeline returns Optional since the result might not be present after all. You can stream the values and use the map() to get rid of the Optional or use Collectors.collectAndThen*:
Collection<User> d = Stream
.of(first, second) // start with Stream<List<User>>
.flatMap(List::stream) // flatting to the Stream<User>
.collect(Collectors.groupingBy( // Collecting to Map<String, List<User>>
User::getName, // by name (the key)
Collectors.collectingAndThen( // reduce the list into a single User
Collectors.reducing((l, r) -> new User(l.getName(), l.getComments() + r.getComments())),
Optional::get))) // and extract from the Optional
.values();
* Thanks to #Aomine
As alternative fairly straight and efficient :
stream the elements
collect them into a Map<String, Integer> to associate each name to the sum of comments (int)
stream the entries of the collected map to create the List of User.
Alternatively for the third step you could apply a finishing transformation to the Map collector with collectingAndThen(groupingBy()..., m -> ...
but I don't find it always very readable and here we could do without.
It would give :
List<User> users =
Stream.concat(first.stream(), second.stream())
.collect(groupingBy(User::getName, summingInt(User::getComments)))
.entrySet()
.stream()
.map(e -> new User(e.getKey(), e.getValue()))
.collect(toList());

Java collect to list but specify pre-defined first two elements order

I have a List<Person> objects. From it I want to get a list of all id's, and I always want the id "abc" and "bob" to come as the 0th and 1st index of the list if available. Is there a way to do this with java streams?
class Person {
private String id;
}
List<Person> allPeople = ...
List<String> allIds = allPeople.stream().map(Person::id).collect(Collectors.toList());
My approach is:
Set<String> allIds = allPeople.stream().map(Person::id).collect(Collectors.Set());
List<String> orderedIds = new ArrayList<>();
if(allIds.contains("abc")) {
orderedIds.add("abc");
}
if(allIds.contains("bob")) {
orderedIds.add("bob");
}
//Iterate through the set and all add all entries which are not bob and abc in the list.
It seems like you need more of a PriorityQueue rather than a List here, so may be something like this:
PriorityQueue<String> pq = list.stream()
.map(Person::getId)
.distinct()
.collect(Collectors.toCollection(() -> new PriorityQueue<>(
Comparator.comparing(x -> !"abc".equals(x))
.thenComparing(x -> !"bob".equals(x)))));
If you still need a List though, just drain that pq into one:
List<String> result = new ArrayList<>();
while (!pq.isEmpty()) {
result.add(pq.poll());
}
I assume that each id occurs only once in the list. With this I would choose a simple straightforward solution:
List<Person> allPeople = ...;
List<String> allIds = allPeople.stream().map(Person::id).collect(toCollection(ArrayList::new));
boolean foundBob = allIds.remove("bob");
if (foundBob) allIds.add(0, "bob");
boolean foundAbc = allIds.remove("abc");
if (foundAbc) allIds.add(0, "abc");
Note that "bob" and "abc" are moved to the head of the list in reverse order. So "abc" is first in the end.
You can make a small utility method for moving an element:
static void moveToHead(List<String> list, String elem) {
boolean found = list.remove(elem);
if (found) list.add(0, elem);
}
With this your code is even simpler and easier to understand:
List<Person> allPeople = ...;
List<String> allIds = allPeople.stream().map(Person::id).collect(toCollection(ArrayList::new));
moveToHead(allIds, "bob");
moveToHead(allIds, "abc");
if you want to perform this in a "fully" stream pipeline you could do:
allPeople.stream()
.map(Person::id)
.distinct()
.collect(collectingAndThen(partitioningBy(s -> "abc".equals(s) || "bob".equals(s)),
map -> Stream.concat(map.get(true).stream(), map.get(false).stream())));
.collect(toList());
if you always want "abc" in front of "bob" then change
map.get(true).stream()
to
map.get(true).stream()
.sorted(Comparator.comparing((String s) -> !s.equals("abc")))
Another solution you could do is:
Set<String> allIds = allPeople.stream().map(Person::id).collect(toSet());
List<String> orderedIds = Stream.concat(allIds.stream()
.filter(s -> "abc".equals(s) || "bob".equals(s))
.sorted(Comparator.comparing((String s) -> !s.equals("abc"))),
allIds.stream().filter(s -> !"abc".equals(s) && !"bob".equals(s)))
.collect(toList());
which is pretty much doing the same thing as the above partitioningBy but just in a different approach.
Finaly, you might be surprised but your approach actually seems good, so you may want to complete it with:
Set<String> allIds = allPeople.stream().map(Person::id).collect(toSet());
List<String> orderedIds = new ArrayList<>();
if(allIds.contains("abc"))
orderedIds.add("abc");
if(allIds.contains("bob"))
orderedIds.add("bob");
orderedIds.addAll(allIds.stream().filter(s -> !"abc".equals(s) && ! "bob".equals(s)).collect(toList()));
Inspired by Stuart Marks there is an even simpler solution:
List<String> allIds = allPeople.stream()
.map(Person::getId)
.distinct()
.sorted(comparing(x -> !"abc".equals(x)).thenComparing(x -> !"bob".equals(x)))
.collect(Collectors.toList());

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.

Java 8 extract all keys from matching values in a 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();

converting java 7 nested for loops to use java 8 streams API

I have an example here which basically returns list based on simple logic
Given an input list and a list of grouping objects, which has a list field, the method should return a list that contains either all the members of grouping.list if the grouping.name matches any of the strings in the input list OR simply add the input string to the returning list.
After I writing this code, I am thinking it could be made simpler in Java 7 and a better example to use Java 8 Streaming API.
public class CollectorExample {
public static void main(String[] args){
List<String> input = new ArrayList<>();
input.add("foo");
input.add("bar");
input.add("foobar");
input.add("java");
List<String> list1 = new ArrayList<>();
list1.add("hello");
list1.add("world");
List<String> list2 = new ArrayList<>();
list2.add("spring");
list2.add("multi-threaded");
Grouping g1 = new Grouping("foobar",list1);
Grouping g2 = new Grouping("java",list2);
List<Grouping> groupingList = new ArrayList<>();
groupingList.add(g1);
groupingList.add(g2);
System.out.println(mapAndMerge(input,groupingList));
}
public static List<String> mapAndMerge(List<String> input, List<Grouping> groupingList){
Set<String> returnDocs = new HashSet<>();
Iterator<String> it = input.iterator();
while(it.hasNext()){
String doc = it.next();
boolean found = false;
for (Grouping lg : groupingList){
if (lg.getName().equals(doc)){
returnDocs.addAll(lg.getList());
found=true;
}
}
if (!found){
returnDocs.add(doc);
}
}
return new ArrayList<>(returnDocs);
}
}
class Grouping {
List<String> list;
String name;
public Grouping(String name, List<String> list){
this.list=list;
this.name=name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getList() {
return list;
}
public void setList(List<String> list) {
this.list = list;
}
}
This outputs [spring, bar, world, foo, hello, multi-threaded] which is correct.
Here is my Java 8 syntax that I tried and did NOT work;
// List<String> mergedDocs =
// input.forEach(doc->
// groupingList.stream().map( g -> g.getName().equals(doc) ? e.getList() : doc ).collect(Collectors.toList()));
// return mergedDocs;
You can make this a lot simpler by not using your Grouping class but using a simple Map<String, List<String>> instead. This map would act as the grouping, holding the list for a given name. This also enables to have a much better performance since looking into the map is constant-time (whereas your solution is in linear time since it traverses the grouping to find a matching one).
If you have to use the List<Grouping>, you can still pre-process it to convert into an intermediate Map:
The mapAndMerge method simply becomes:
public static List<String> mapAndMerge(List<String> input, List<Grouping> groupingList) {
Map<String, List<String>> map = groupingList.stream().collect(Collectors.toMap(Grouping::getName, Grouping::getList));
return input.stream()
.flatMap(s -> map.getOrDefault(s, Arrays.asList(s)).stream())
.collect(Collectors.toList());
}
Each input is flat mapped to the list contained in the map or a default list containing the current element. Then this is collected to a new list. This code prints:
[foo, bar, hello, world, spring, multi-threaded]
You can re-write the mapAndMerge method following way using java 8. But it is not very concise as you like.
public static List<String> mapAndMerge(List<String> input,
List<Grouping> groupingList) {
Set<String> returnDocs = input
.stream()
.map(t -> groupingList
.stream()
.filter(g -> g.getName().equals(t))
.map(v -> v.getList())
.findAny()
.orElse(Arrays.asList(t)))
.flatMap(t -> t.stream())
.collect(Collectors.toSet());
return new ArrayList<>(returnDocs);
}
I think it would be much simple and clearer if you use Map instead of the Grouping class.
So that's what you'll have in the main() method:
Map<String, List<String>> groupingMap = new HashMap<>();
groupingMap.put("foobar", list1);
groupingMap.put("java", list2);
List<String> mergedDocs = new ArrayList<>();
input.stream()
.map(doc -> groupingMap.getOrDefault(doc, Collections.singletonList(doc)))
.forEach(mergedDocs::addAll);
System.out.println(mergedDocs);

Categories