How to exclude non-single items from two lists? - java

How can I use a stream from two lists to get a list of unique entities?
Match only by username
public class Entity {
private String username;
private String password;
}
var first = Arrays.asList(
new Entity("user1", ""),
new Entity("user2", "")
new Entity("user3", "pass3"),
new Entity("user5", "pass5")
);
var second = Arrays.asList(
new Entity("user1", "pass1"),
new Entity("user2", "pass2"),
);
public static void foo(List<Entity> first, List<Entity> second) {
List<Entity>result = Stream.of(first, second)
.flatMap(List::stream)
?
?
.collect(Collectors.toList());
}
result must be list with Entity("user3", "pass3") and Entity("user5", "pass5")

you can make grouping by username:
var groupedData = Stream.concat(list1.stream(), list2.stream())
.collect(Collectors.groupingBy(Entity::getUsername));
and then filtered entity which size > 1:
groupedData.values().stream()
.filter(s -> s.size() == 1)
.flatMap(Collection::stream)
.collect(Collectors.toList());
or only one a big stream:
Stream.concat(list1.stream(), list2.stream())
.collect(Collectors.groupingBy(Entity::getUsername)).values().stream()
.filter(s -> s.size() == 1)
.flatMap(Collection::stream)
.collect(Collectors.toList());

Along with using groupingBy you can also use Collectors.toMap with merging (val1, val2) -> null to exclude elements getting to merge thus leaving only single elements:
List<Entity> result = Stream.concat(first.stream(), second.stream())
.collect(Collectors.toMap(Entity::getUsername,
val -> val, (val1, val2) -> null))
.values().stream()
.collect(Collectors.toList());

Probably it's not the best way
public static List<Entity> foo(List<Entity> first, List<Entity> second) {
List<Entity> arr = new ArrayList<>();
arr.addAll(first);
arr.addAll(second);
return arr
.stream()
.filter(entity -> (first.stream().map(Entity::getUsername).anyMatch(username -> username.equals(entity.getUsername())) &&
second.stream().map(Entity::getUsername).noneMatch(username -> username.equals(entity.getUsername()))) ||
(second.stream().map(Entity::getUsername).anyMatch(username -> username.equals(entity.getUsername())) &&
first.stream().map(Entity::getUsername).noneMatch(username -> username.equals(entity.getUsername()))))
.collect(Collectors.toList());
}
The logic in the filter is "Exclusive OR", as we don't have a straight way of doing that, we need to make the logic of (Condition1 and not Condition2) or (Condition2 and not Condition1).

lambda as a straight forward solution
concat the streams of first-list-entities not contained in second-list and vice versa
List<Entity> unique = Stream.concat(
first.stream().filter(e -> ! second.contains( e )),
second.stream().filter(e -> ! first.contains( e )) ).collect( toList() );

Related

Add Object to List after orElse

inscTipoTrabAval.getInscricaoTipoTrabalhoAvaliadorQuestoes()
.stream()
.filter(aq -> aq.getEventoQuestao().equals(eventoQuestao))
.findFirst()
.orElse(new InscricaoTipoTrabalhoAvaliadorQuestao(eventoQuestao, inscTipoTrabAval))
.setJustificativa(justificativa);
I'm trying to write an object into a list if it doesn't exist with orElse, but it isn't adding to the list. Is there any way to do this?
The easiest is to store the list in a variable, and check the content of your optional
//assuming the list is not immutable
List<InscricaoTipoTrabalhoAvaliadorQuestao> list = inscTipoTrabAval.getInscricaoTipoTrabalhoAvaliadorQuestoes();
list.stream()
.filter(aq -> aq.getEventoQuestao().equals(eventoQuestao))
.findFirst()
.ifPresentOrElse(
existing -> existing.setJustificativa(justificativa),
() -> {
var value = new InscricaoTipoTrabalhoAvaliadorQuestao(eventoQuestao, inscTipoTrabAval));
value.setJustificativa(justificativa);
list.add(value);
}
);
If you're on Java 8, you can use an if block
Optional<InscricaoTipoTrabalhoAvaliadorQuestao> value = list.stream()
.filter(aq -> aq.getEventoQuestao().equals(eventoQuestao))
.findFirst()
if(value.isPresent()) {
value.get().setJustificativa(justificativa);
} else {
InscricaoTipoTrabalhoAvaliadorQuestao newValue = new InscricaoTipoTrabalhoAvaliadorQuestao(eventoQuestao, inscTipoTrabAval));
newValue.setJustificativa(justificativa);
list.add(newValue);
}

How to filter based on list returned by map param using Java 8 streams

I'm trying to use Java stream to filter some values based on certain conditions. I am able to achieve the same using traditional for loops and a little bit of streams, but I want to rewrite the same logic fully in streams.
Original code:
public List <String> getProductNames(Hub hub, String requestedGroup) {
List <SupportedProduct> configuredProducts = repo.getSupportedProducts(hub);
List <String> productNames = new ArrayList <> ();
for (SupportedProduct supportedProduct: configuredProducts) {
List < String > categoryNameList = new ArrayList <> ();
String activeCategoryName = supportedProduct.getCategoryDetails().getActiveCategoryName();
if (activeCategoryName == null) {
Optional.ofNullable(supportedProduct.getCategoryDetails().getCategories())
.orElse(Collections.emptyList())
.forEach(category - > categoryNameList.add(category.getName()));
} else {
categoryNameList.add(activeCategoryName);
}
for (String catName: categoryNameList) {
Division division = divisionRepo.getDivisionByCatName(catName);
if (division != null && division.getGroup() == requestedGroup) {
productNames.add(supportedProduct.getProductName());
}
}
}
return productNames;
}
My try:
return Optional.ofNullable(configuredProducts).orElse(Collections.emptyList()).stream()
.map(supportedProduct -> {
List<String> categoryNameList = new ArrayList<>();
String activeCategoryName = supportedProduct.getCategoryDetails().getActiveCategoryName();
if (activeCategoryName == null) {
Optional.ofNullable(supportedProduct.getCategoryDetails().getCategories())
.orElse(Collections.emptyList())
.forEach(category -> categoryNameList.add(category.getName()));
} else {
categoryNameList.add(activeCategoryName);
}
return categoryNameList;
})
.filter(catName ->{
Division division = divisionRepo.getDivisionByCatName(catName);
return division != null && division.getGroup() == requestedGroup;
})........
But I'm lost beyond this.
Please help me to write the same using streams.
EDIT: Added IDEOne for testing - Link
The logic inside is quite complicated, however, try this out:
public List <String> getProductNames(Hub hub, String requestedGroup) {
List<SupportedProduct> configuredProducts = repo.getSupportedProducts(hub);
// extract pairs:
// key=SupportedProduct::getProductName
// values=List with one activeCategoryName OR names of all the categories
Map<String, List<String>> namedActiveCategoryNamesMap = configuredProducts.stream()
.collect(Collectors.toMap(
SupportedProduct::getProductName,
p -> Optional.ofNullable(p.getCategoryDetails().getActiveCategoryName())
.map(Collections::singletonList)
.orElse(Optional.ofNullable(p.getCategoryDetails().getCategories())
.stream()
.flatMap(Collection::stream)
.map(Category::getName)
.collect(Collectors.toList()))));
// look-up based on the categories' names, group equality comparison and returning a List
return namedActiveCategoryNamesMap.entrySet().stream()
.filter(entry -> entry.getValue().stream()
.map(catName -> divisionRepo.getDivisionByCatName(catName))
.filter(Objects::nonNull)
.map(Division::getGroup)
.anyMatch(requestedGroup::equals))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
I recommend splitting into separate methods for sake of readability (the best way to go).
The verbose logics of Optional chains including two orElse calls can be surely simplified, however, it gives you the idea.
You can perform within one Stream using Collectors.collectingAndThen. In that case, I'd extract the Function finisher elsewhere, example:
public List<String> getProductNames(Hub hub, String requestedGroup) {
return repo.getSupportedProducts(hub).stream()
.collect(Collectors.collectingAndThen(
Collectors.toMap(
SupportedProduct::getProductName,
categoryNamesFunction()),
productNamesFunction(requestedGroup)));
}
private Function<Map<String, List<String>>, List<String>> productNamesFunction(String requestedGroup) {
return map -> map.entrySet().stream()
.filter(entry -> entry.getValue().stream()
.map(divisionRepo::getDivisionByCatName)
.filter(Objects::nonNull)
.map(Division::getGroup)
.anyMatch(requestedGroup::equals))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
private Function<SupportedProduct, List<String>> categoryNamesFunction() {
return p -> Optional.ofNullable(p.getCategoryDetails().getActiveCategoryName())
.map(Collections::singletonList)
.orElse(Optional.ofNullable(p.getCategoryDetails().getCategories())
.stream()
.flatMap(Collection::stream)
.map(Category::getName)
.collect(Collectors.toList()));
}

Filter in Java when meeting conditions

There is a condition that I need values in the following Set
Set<String> name = studentResponse
.stream()
.map(StudentResponse::getDetails)
.flatMap(List::stream)
.map(StudentDetail::getName())
.filter(s -> s.contains("A"))
.collect(Collectors.toSet());
I want to filter student names containing "A" if List<StudentDetail> details in StudentResponse contains more than 5 elements. If not, I want to take all names in StudentDetail. Is there any way to handle this condition?
You can use
Set<String> name = studentResponse
.stream()
.map(StudentResponse::getDetails)
.flatMap(l -> l.stream()
.map(StudentDetail::getName)
.filter(s -> l.size() <= 5 || s.contains("A")))
.collect(Collectors.toSet());
but it has the disadvantage of re-checking a list condition for every element, despite it shouldn't change during the entire traversal. A better solution is not to perform a filter operation when it is not necessary, like
Set<String> name = studentResponse
.stream()
.map(StudentResponse::getDetails)
.flatMap(l -> l.size() <= 5?
l.stream().map(StudentDetail::getName):
l.stream().map(StudentDetail::getName).filter(s -> s.contains("A")))
.collect(Collectors.toSet());
or, to avoid the code duplication:
Set<String> name = studentResponse
.stream()
.map(StudentResponse::getDetails)
.flatMap(l -> {
Stream<String> names = l.stream().map(StudentDetail::getName);
return l.size() <= 5? names: names.filter(s -> s.contains("A"));
})
.collect(Collectors.toSet());
Other way is using Supplier<T>
Supplier<Stream<List<StudentDetail>>> stuSup = ()-> studentResponse
.stream()
.map(StudentResponse::getDetails);
then perform a filter on it.
Stream<String> gtFive = stuSup.get()
.filter(d->d.size()>5)
.flatMap(List::stream)
.map(StudentDetail::getName())
.filter(s -> s.contains("A"));
and for less than five:
Stream<String> lteFive = stuSup.get()
.filter(d->d.size()<=5)
.flatMap(List::stream)
.map(StudentDetail::getName());
and finally, combine both of them.
Stream.concat(gtFive,lteFive).collect(toSet());
You can try out processing the two halves based on the conditions using partitioningBy.
Map<Boolean, List<StudentResponse>> greaterThanFive = studentResponse.stream()
.collect(Collectors.partitioningBy(sr -> sr.getDetails().size() > 5));
Set<String> names = Stream.concat(
greaterThanFive.get(Boolean.FALSE).stream()
.flatMap(sr -> sr.getDetails().stream())
.map(StudentDetail::getName),
greaterThanFive.get(Boolean.TRUE).stream()
.flatMap(sr -> sr.getDetails().stream())
.map(StudentDetail::getName)
.filter(name -> name.contains("A"))) // for details size more than 5
.collect(Collectors.toSet());
But there is no reason to choose it over a solution that can perform the partitioning on-the-fly.
You can use Map.Entry as a bag to collect all informations that are needed in the last filtering.
Set<String> name = studentResponse
.stream()
.flatMap(sr -> sr.getDetails().stream().map(
d -> Map.entry(sr.getDetails().size(), d.getName())))
.filter(e -> (e.getKey() <= 5) || (e.getKey() > 5 && e.getValue().contains("A")))
.map(Map.Entry::getValue)
.collect(Collectors.toSet());

Method to calculate the most frequent last name from list of given users with Java Stream API

Function should return optional of most frequent last name (if it encountered at least two times) or optional empty if number of last names is the same or list of users is empty
This is what i came up with, but it doesnt return Optional.empty
#Override
public Optional<String> getMostFrequentLastName(final List<User> users) {
return users.stream()
.map(User::getLastName)
.distinct()
.collect
(Collectors.groupingBy(
Function.identity(),
Collectors.summingInt(w -> 1)
))
.entrySet()
.stream()
.filter(stringIntegerEntry -> stringIntegerEntry.getValue() >= 2)
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.map(Map.Entry::getKey)
.findFirst();
}
This is my test class
public static void main(String[] args) {
Optional<String> optionalS = Stream.of(new User("name1"),
new User("name1"), new User("name2"), new User("name2"))
.map(User::getLastName)
.collect
(Collectors.groupingBy(
Function.identity(),
Collectors.counting()
))
.entrySet()
.stream()
.filter(stringIntegerEntry -> stringIntegerEntry.getValue() >= 2)
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.map(Map.Entry::getKey)
.findFirst();
System.out.println(optionalS.toString());
}
Here is the awnser
Optional[name2]
But should be
Optional[empty]
You may use
Optional<String> optionalS =
Stream.of(new User("name1"), new User("name1"), new User("name2"), new User("name2"))
.collect(Collectors.groupingBy(User::getLastName, Collectors.counting()))
.entrySet()
.stream()
.filter(entry -> entry.getValue() >= 2)
.reduce((e1, e2) -> e1.getValue() < e2.getValue()? e2:
e1.getValue() > e2.getValue()? e1:
new AbstractMap.SimpleImmutableEntry<>(null, e1.getValue()))
.map(Map.Entry::getKey);
System.out.println(optionalS.toString());
Getting the maximum value is a form of Reduction. Since you want to get an empty optional in case of a tie, the simplest solution is to write the reduction function explicitly, use the Map.Entry with the bigger value if there is one, otherwise construct a new Map.Entry with a null key.
The result of the reduction is already an Optional, which will be empty if there were no elements (with a count >=2). So the last map step is applied on an Optional. If already empty, the map function won’t be evaluated and the resulting Optional stays empty. If the optional is not empty, but Map.Entry::getKey evaluates to null, the resulting optional will be empty.
It seems to me that if you have the same number of maximum of some different lastNames you want to return an Optional::empty, as such:
Map<String, Long> map =
Stream.of(new User("name1"),
new User("name1"),
new User("name2"),
new User("name2"))
.collect(Collectors.groupingBy(User::getLastName, Collectors.counting()));
map.entrySet()
.stream()
.max(Entry.comparingByValue())
.flatMap(en -> {
boolean b = map.entrySet()
.stream()
.filter(x -> !x.getKey().equals(en.getKey()))
.mapToLong(Entry::getValue)
.noneMatch(x -> x == en.getValue());
return b ? Optional.of(en.getKey()) : Optional.empty();
})
.ifPresent(System.out::println);
}
Here my monster for you:
Optional<String> optionalS = Stream.of(
new User("name1"),
new User("name1"),
new User("name2"),
new User("name2"))
.map(User::getLastName)
.collect(
Collectors.groupingBy(
Function.identity(),
Collectors.counting()
))
.entrySet()
.stream()
.filter(stringIntegerEntry -> stringIntegerEntry.getValue() >= 2)
.collect(
Collectors.groupingBy(
Map.Entry::getValue,
Collectors.toList()
))
.entrySet()
.stream()
.sorted(Comparator.comparing(
Map.Entry::getKey,
Comparator.reverseOrder()))
.map(Map.Entry::getValue)
.findFirst()
.filter(x -> x.size() == 1)
.map(x -> x.get(0).getKey());
System.out.println(optionalS);
As far as I undestand your solution in stream you code creates
Map<String(lastname),Integer(number of occurence)>
and then filter that map where number of occurence >=2 and in your test case you have map with entries:
<"name1",2>
<"name2",2>
So ordering by value will still return two values.
You should try create
Map<Integer,List<String>>
which will store number of occurence -> names, then filter map keys, sort them descending and (in map value) you will get most frequently lastname (or lastnames if there were more than once in input).
//edit
Below short snippet with my solution:
Map<Integer, List<String>> map = new HashMap<>();
map.put(2,Arrays.asList("name1","name2"));
Optional<String> optionalS = map
.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey(Comparator.reverseOrder()))
.findFirst() //get max{map's keys}
.filter(x->x.getValue().size() == 1) //get lastname that occured only once
.map(x->x.getValue().get(0)); //get that lastname (above filter check that list has only one element) or Optional.empty if stream didn't find any
System.out.println(optionalS.toString());
I skipped the part of creating map.
P.S. You can replace HashMap with TreeMap with custom comparator to avoid sorting in stream.

Group objects by multiple attributes with Java 8 stream API

Given we have a list of Bank, each Bank have multiple offices,
public class Bank {
private String name;
private List<String> branches;
public String getName(){
return name;
}
public List<String> getBranches(){
return branches;
}
}
For example:
Bank "Mizuho": branches=["London", "New York"]
Bank "Goldman": branches = ["London", "Toronto"]
Given a list of banks, I would have a map of bank representation for each city. In the example above, I need a result of
Map["London"] == ["Mizuho", "Goldman"]
Map["New York"] == ["Mizuho"]
Map["Toronto"] == ["Goldman"]
How can I achieve that result using Java 8 API? Using pre-Java8 is easy, but verbose.
Thank you.
Map<String, Set<Bank>> result = new HashMap<>();
for (Bank bank : banks) {
for (String branch : bank.getBranches()) {
result.computeIfAbsent(branch, b -> new HashSet<Bank>()).add(bank);
}
}
banks.flatMap(bank -> bank.getBranches()
.stream()
.map(branch -> new AbstractMap.SimpleEntry<>(branch, bank)))
.collect(Collectors.groupingBy(
Entry::getKey,
Collectors.mapping(Entry::getValue, Collectors.toList())));
Result would be:
{London=[Mizuho, Goldman], NewYork=[Mizuho], Toronto=[Goldman]}
You could do it using the version of Stream.collect that accepts a supplier, an accumulator function and a combiner function, as follows:
Map<String, List<Bank>> result = banks.stream()
.collect(
HashMap::new,
(map, bank) -> bank.getBranches().forEach(branch ->
map.computeIfAbsent(branch, k -> new ArrayList<>()).add(bank)),
(map1, map2) -> map2.forEach((k, v) -> map1.merge(k, v, (l1, l2) -> {
l1.addAll(l2);
return l1;
})));
I think solution provided by #JB Nizet is one of the most simple/efficient solutions. it can also be rewritten by forEach
banks.forEach(b -> b.getBranches().forEach(ch -> result.computeIfAbsent(ch, k -> new ArrayList<>()).add(b)));
Another short solution by Stream with abacus-common
Map<String, List<Bank>> res = Stream.of(banks)
.flatMap(e -> Stream.of(e.getBranches()).map(b -> Pair.of(b, e)))
.collect(Collectors.toMap2());

Categories

Resources