I have a list of documents objects that need to be mapped based on certain criteria. There is a utility function that takes any 2 document types and determines if they match on a number of criteria, like genre of document, whether they share any authors etc. The code works but I';d like to use Java Streams to solve it if possible.
I currently solve this by using the following code:
class Document{
private String genre;
private List<Author> authors;
private String isbn;
private boolean paperBack;
...
}
I also use a library utility that has a function that returns true given a series of matching criteria and a pair of documents. It simply returns a boolean.
boolean matchesOnCriteria(Document doc1, Document doc2, Criteria criteria){
...
}
Here is the matching code for finding the books that match on the provided criteria
DocumentUtils utils = DocumentUitls.instance();
Criteria userCriteria = ...
List<Pair> matches = new ArrayList<>();
List<Document> documents = entityManager.findBy(.....);
for(Document doc1 : documents){
for(Documents doc2 : documents){
if(!doc1.equals(doc2){
if (utils.matchesOnCriteria(doc1,doc2, userCriteria)) {
Pair<Document> p = new Pair(doc1,doc2);
if(!matches.contains(p)){
matches.add(p);
}
}
}
}
}
}
How can I do this using Streams?
The idea of the following solution using Steam::reduce is simple:
Group the qualified pairs of documents to Map<Document, List<Document>> having all possible acceptable combinations. Let's say odd and even documents are in pairs:
D1=[D3, D5], D2=[D4], D3=[D1, D5], D4=[D2], D5[D1, D3] // dont mind the duplicates
Using Stream::reduce you can achieve the following steps:
Transform entries to Pair<>,
D1-D3, D1-D5, D2-D4, D3-D1, D1-D5, D4-D2, D5-D1, D5-D3
Save these items to Set guaranteeing the equal pairs occur once (D1-D3 = D3-D1). The condition the Pair must override both Object::equals and Object:hashCode and implements equality based on the both documents present.
D1-D3, D1-D5, D3-D5, D2-D4
Reducing (merging) the particular sets into a single collection Set<Pair<Document>>.
Map<Document, List<Document>> map = documents.stream()
.collect(Collectors.toMap( // Collected to Map<Document, List<Document>>
Function.identity(), // Document is the key
d1 -> documents.stream() // Value are the qualified documents
.filter(d2 -> !d1.equals(d2) &&
utils.matchesOnCriteria(d1,d2, userCriteria)
.collect(Collectors.toList()))); // ... as List<Document>
Set<Pair<Document>> matches = map.entrySet().stream().reduce( // Reduce the Entry<Dokument, List<Document>>
new HashSet<>(), // ... to Set<Pair<>>
(set, e) -> {
set.addAll(e.getValue().stream() // ... where is
.map(v -> new Pair<Document>(e.getKey(), v)) // ... the Pair of qualified documents
.collect(Collectors.toSet()));
return set;
},
(left, right) -> { left.addAll(right); return left; }); // Merge operation
The condition !matches.contains(p) is redundant, there are better ways to assure distinct values. Either use Stream::distinct or collect the stream to Set which is an unordered distinct collection.
Read more at Baeldung's: remove all duplicates.
Related
OrderList - Id, Status, Product List.
ProductList - Id, Name, category.
Obtain a list of order with List of products belong to category “Baby” Java 8
I have tried
orderList.stream().flatMap(prodCat -> prodCat.getProduct().stream())
.filter(cat -> cat.getCategary()=="Baby").forEach(System.out::println);
It return list of product type only. I am looking for return list of order of list product.
If you're looking for orders that have some product with a certain category, you can use anyMatch() in a filter(), like this:
orderList.stream()
.filter(o -> o.getProducts()
.stream()
.anyMatch(p -> p.getCategory().equals("Baby")))
.forEach(System.out::println);
Your code is right except for equals between strings.
Pay attention:
This equality is false because it compares the references:
"Baby" == new String("Baby")
This equality is true because it compares the values:
"Baby".equals(new String("Baby"))
This is the final result:
orderList.stream()
.flatMap(prodCat -> prodCat.getProduct().stream())
.filter(cat -> "Baby".equals(cat.getCategary()))
.forEach(System.out::println);
If want to obtain the result of execution of the stream pipeline as a List you should use collect() as terminal operation and Collectors.toList() as its argument.
Or as an alternative available with Java 16 onwards you might use toList() as a terminal operation, which returns an immutable list.
The method below iterates over the list of orders and creates a new instance of Order for every object that contains in its list of products at least one product of a given category otherwise, the order will be discarded. Then orders get collected to a new list.
public static List<Order> getOrdersOfCategory(List<Order> orderList, String category) {
return orderList.stream()
.map(order -> getNewOrderOfCategory(order, category)) // creating a new Order object that contains a filtered List of products
.filter(Optional::isPresent) // removing the orders that have no products of a given category at all
.map(Optional::get)
.collect(Collectors.toList());
}
The responsibility of the method below is to create new instances of Order class. Each of them will be associated with a filtered list of products.
Result returned by this method is of type Optional<Order> in order to represent a situation when a filtered list of products is empty and order is discarded.
public static Optional<Order> getNewOrderOfCategory(Order order, String category) {
List<Product> filteredProducts = getProductsOfCategory(order.getProduct(), category);
return filteredProducts.isEmpty() ? Optional.empty() : // no target products
new Order(order.getId(),
order.getStatus(),
filteredProducts);
}
This method is used to extract the products of a given category from the given list.
public static List<Product> getProductsOfCategory(List<Product> products, String category) {
return products.stream()
.filter(prod -> prod.getCategary().equals(category))
.collect(Collectors.toList());
}
Note
Don't compare objects with a double equals sign, always use method equals() instead;
It'll be better to implement product category as an enum instead of relying on a string. All product categories will be defined in one place and if you'll make a typo it'll be pretty obvious because the code will not compile.
I have Person class which represents worker of company. I am getting all workers of company from database. Admin wants to add some new workers to the company. I want to check if worker exists or not. If exists I will update in db if not I will insert. Check will be based on phoneNumber.
#Data
public class Person {
private int id;
private String name;
private String surname;
private String phoneNumber;
...
}
----------
**ServiceClass**
List<Person> workersOfCompany = ..; // get all users of company
List<Person> requestedWorkers = ... ; // requested contacts to be updated/inserted
// Getting users in our system. But in this case id will be always zero. because requester will not send id.
List<Person> updatable = requestedWorkers.stream()
.filter(workersOfCompany::contains) // equal method of Person override
.collect(Collectors.toList());
I want to change this function. I need to get workers which in our system (in workersOfCompany) and set ids to newly created list by getting ids from workersOfCompany.
I can do this by using 2 fors but I want to do this by java 8 streams.
How can I find objects that are in both list and set ids to the objects in new list.?
It could make sense to use Collectors::partitioningBy to split the input list of requested workers into existing and new workers.
Also, existing code snippet does not show that the Person::equals is overridden to use only the phone number (Lombok #Data provides implementation of equals and hashCode based on all instance fields), so it may be needed to build a map of phones to the ids for the existing workers assuming that the phones are unique per person:
Map<String, Integer> phoneIdMap = workersOfCompany
.stream()
.collect(Collectors.toMap(
Person::getPhoneNumber,
Person::getId,
(p1, p2) -> p1 // resolve possible conflicts
));
Map<Boolean, List<Person>> split = requestedWorkers
.stream()
.collect(Collectors.partitioningBy(
p -> phoneIdMap.containsKey(p.getPhoneNumber())
));
split.get(Boolean.TRUE) // update ids of existing workers
.forEach(w -> w.setId(phoneIdMap.get(w.getPhoneNumber())));
split.get(Boolean.FALSE) // insert into DB and fix ids for new workers
.forEach(w -> w.setId(personRepository.insert(w))); // assuming id is returned
Similar implementation without creating the intermediate split map looks as follows providing that phoneIdMap has been created:
requestedWorkers.forEach(
w -> w.setId(phoneIdMap.getOrDefault(
w.getPhoneNumber(), personRepository.insert(w)
))
);
Or, if the ids should only be updated for the existing workers, a filter may be applied:
requestedWorkers
.stream()
.filter(w -> phoneIdMap.containsKey(w.getPhoneNumber()))
.forEach(w -> w.setId(phoneIdMap.get(w.getPhoneNumber())));
You can use forEach or peek to alter the Person items of requestedWorkers. Beware of peek since it depends on the terminal operation (collect in our example). In our case it works however it would be better to use forEach
List<Person> updatable = requestedWorkers.stream()
.filter(workersOfCompany::contains)
.peek(r -> { // -> better use forEach
final int indexOf = workersOfCompany.indexOf(r); //here matching equality works with phoneNumber only as you stated
if (indexOf != -1) {
final int id = workersOfCompany.get(indexOf).getId();
r.setId(id);
}
}).collect(Collectors.toList());
I'm new to java stream API.
I have 2 lists, and if both their internal object ID matches wants to put some attributes to MAP.
Below is the implementation.
List<LookupMstEntity> examTypeDetails; //This list contains values init.
List<MarksMstEntity> marksDetailList; //This list contains values init.
//FYI above entities have lombok setter, getter, equals & hashcode.
Map<Long, Integer> marksDetailMap = new HashMap<>();
//need below implementation to changed using java 8.
for (LookupMstEntity examType : examTypeDetails) {
for (MarksMstEntity marks : marksDetailList) {
if (examType.getLookupId() == marks.getExamTypeId())
marksDetailMap.put(examType.getLookupId(), marks.getMarks());
}
}
Creating a set of lookupIds Set<Long> ids helps you to throw away duplicate values and to get rid of unnecessary checks.
Then you can filter marksDetailList accordingly with examTypeId values:
filter(m -> ids.contains(m.getExamTypeId()))
HashSet contains() method has constant time complexity O(1).
Try this:
Set<Long> ids = examTypeDetails.stream().map(LookupMstEntity::getLookupId)
.collect(Collectors.toCollection(HashSet::new));
Map<Long, Integer> marksDetailMap = marksDetailList.stream().filter(m -> ids.contains(m.getExamTypeId()))
.collect(Collectors.toMap(MarksMstEntity::getExamTypeId, MarksMstEntity::getMarks));
As long as you are looking for these with equal ID, it doesn't matter which ID you use then. I suggest you to start streaming the marksDetailList first since you need its getMarks(). The filtering method searches if there is a match in IDs. If so, collect the required key-values to the map.
Map<Long, Integer> marksDetailMap = marksDetailList.stream() // List<MarksMstEntity>
.filter(mark -> examTypeDetails.stream() // filtered those where ...
.map(LookupMstEntity::getLookupId) // ... the lookupId
.anyMatch(id -> id == mark.getExamTypeId())) // ... is present in the list
.collect(Collectors.toMap( // collected to Map ...
MarksMstEntity::getExamTypeId, // ... with ID as a key
MarksMstEntity::getMarks)); // ... and marks as a value
The .map(..).anyMatch(..) can be shrink into one:
.anyMatch(exam -> exam.getLookupId() == mark.getExamTypeId())
As stated in the comments, I'd rather go for the for-each iteration as you have already used for sake of brevity.
An observation:
First, your resultant map indicates that there can only be one match for ID types (otherwise you would have duplicate keys and the value would need to be a List or some other way of merging duplicate keys, not an Integer. So when you find the first one and insert it in the map, break out of the inner loop.
for (LookupMstEntity examType : examTypeDetails) {
for (MarksMstEntity marks : marksDetailList) {
if (examType.getLookupId() == marks.getExamTypeId()) {
marksDetailMap.put(examType.getLookupId(),
marks.getMarks());
// no need to keep on searching for this ID
break;
}
}
}
Also if your two classes were related by a parent class or a shared interface that had access to to the id, and the two classes were considered equal based on that id, then you could do something similar to this.
for (LookupMstEntity examType : examTypeDetails) {
int index = marksDetailList.indexOf(examType);
if (index > 0) {
marksDetailMap.put(examType.getLookupId(),
marksDetaiList.get(index).getMarks());
}
}
Of course the burden of locating the index is still there but it is now under the hood and you are relieved of that responsibility.
You can do it with O(N) time complexity using HashMap, first convert two lists into Map<Integer, LookupMstEntity> and Map<Integer, MarksMstEntity> with id as key
Map<Integer, LookupMstEntity> examTypes = examTypeDetails.stream()
.collect(Collectors.toMap(LookupMstEntity::getLookupId,
Function.identity()) //make sure you don't have any duplicate LookupMstEntity objects with same id
Map<Integer, MarksMstEntity> marks = marksDetailList.stream()
.collect(Collectors.toMap(MarksMstEntity::getExamTypeId,
Function.identity()) // make sure there are no duplicates
And then stream the examTypes map and then collect into map if MarksMstEntity exists with same id in marks map
Map<Integer, Integer> result = examTypes.entrySet()
.stream()
.map(entry->new AbstractMap.SimpleEntry<Integer, MarksMstEntity>(entry.getKey(), marks.get(entry.getKey())))
.filter(entry->entry.getValue()!=null)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
There are two entities:
class GiftCertificate {
Long id;
List<Tag> tags;
}
class Tag {
Long id;
String name;
}
There is a list
List<GiftCertificate>
which contains, for example, the following data:
<1, [1, "Tag1"]>, <2, null>, <1, [2, "Tag2"]>. (It does not contain a set of tags, but only one tag or does not have it at all).
I need to do so that in the result it was this:
<1, {[1," Tag1 "], [2," Tag2 "]}>, <2, null>. I mean, add to the set of the first object a tag from the third GiftCertificate and at the same time delete the 3rd one. I would like to get at least some ideas on how to do this. it would be nice to use stream.
Probably not the most effective way, but it might help
private List<GiftCertificate> joinCertificates(List<GiftCertificate> giftCertificates) {
return giftCertificates.stream()
.collect(Collectors.groupingBy(GiftCertificate::getId))
.entrySet().stream()
.map(entry -> new GiftCertificate(entry.getKey(), joinTags(entry.getValue()))).collect(Collectors.toList());
}
private List<Tag> joinTags(List<GiftCertificate> giftCertificates) {
return giftCertificates.stream()
.flatMap(giftCertificate -> Optional.ofNullable(giftCertificate.getTags()).stream().flatMap(Collection::stream))
.collect(Collectors.toList());
}
You can do what you want with streams and with the help of a dedicated custom constructor and a couple of helper methods in GiftCertificate. Here's the constructor:
public GiftCertificate(GiftCertificate another) {
this.id = another.id;
this.tags = new ArrayList<>(another.tags);
}
This just works as a copy constructor. We're creating a new list of tags, so that if the list of tags of either one of the GiftCertificate instances is modified, the other one won't. (This is just basic OO concepts: encapsulation).
Then, in order to add another GiftCertificate's tags to this GiftCertificate's list of tags, you could add the following method to GiftCertificate:
public GiftCertificate addTagsFrom(GiftCertificate another) {
tags.addAll(another.tags);
return this;
}
And also, a helper method that returns whether the list of tags is empty or not will come in very handy:
public boolean hasTags() {
return tags != null && !tags.isEmpty();
}
Finally, with these three simple methods in place, we're ready to use all the power of streams to solve the problem in an elegant way:
Collection<GiftCertificate> result = certificates.stream()
.filter(GiftCertificate::hasTags) // keep only gift certificates with tags
.collect(Collectors.toMap(
GiftCertificate::getId, // group by id
GiftCertificate::new, // use our dedicated constructor
GiftCertificate::addTagsFrom)) // merge the tags here
.values();
This uses Collectors.toMap to create a map that groups gift certificates by id, merging the tags. Then, we keep the values of the map.
Here's the equivalent solution, without streams:
Map<Long, GiftCertificate> map = new LinkedHashMap<>(); // preserves insertion order
certificates.forEach(cert -> {
if (cert.hasTags()) {
map.merge(
cert.getId(),
new GiftCertificate(cert),
GiftCertificate::addTagsFrom);
}
});
Collection<GiftCertificate> result = map.values();
And here's a variant with a slight performance improvement:
Map<Long, GiftCertificate> map = new LinkedHashMap<>(); // preserves insertion order
certificates.forEach(cert -> {
if (cert.hasTags()) {
map.computeIfAbsent(
cert.getId(),
k -> new GiftCertificate(k)) // or GitCertificate::new
.addTagsFrom(cert);
}
});
Collection<GiftCertificate> result = map.values();
This solution requires the following constructor:
public GiftCertificate(Long id) {
this.id = id;
this.tags = new ArrayList<>();
}
The advantage of this approach is that new GiftCertificate instances will be created only if there's no other entry in the map with the same id.
Java 9 introduced flatMapping collector that is particularly well-suited for problems like this. Break the task into two steps. First, build a map of gift certificate IDs to list of tags and then assemble a new list of GiftCertificate objects:
import static java.util.stream.Collectors.flatMapping;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
......
Map<Long, List<Tag>> gcIdToTags = gcs.stream()
.collect(groupingBy(
GiftCertificate::getId,
flatMapping(
gc -> gc.getTags() == null ? Stream.empty() : gc.getTags().stream(),
toList()
)
));
List<GiftCertificate> r = gcIdToTags.entrySet().stream()
.map(e -> new GiftCertificate(e.getKey(), e.getValue()))
.collect(toList());
This assumes that GiftCertificate has a constructor that accepts Long id and List<Tag> tags
Note that this code deviates from your requirements by creating an empty list instead of null in case there are no tags for a gift certificate id. Using null instead of an empty list is just a very lousy design and forces you to pollute your code with null checks everywhere.
The first argument to flatMapping can also be written as gc -> Stream.ofNullable(gc.getTags()).flatMap(List::stream) if you find that more readable.
I have a hashMap that contains "term" as key and "documents list" as values.
Eg:
KEY::VALUE
afternoon::Doc2
activities::Doc1, Doc2, Doc3
admissions::Doc1, Doc2, Doc4, Doc5
alternate::Doc5
I need to pass boolean expressions against the terms and fetch the matching documents. This expression will be passed through another string variable.
Eg:
(afternoon AND activities) OR alternate => Doc2, Doc5
(afternoon AND activities) OR (admissions AND alternate) => Doc2, Doc5
activities AND NOT afternoon = > Doc1, Doc3
Are there any functions in Java for such operations? Any external libraries will work too.
A code snippet will help me a lot since my assignment is due tomorrow & this is the final step of my solution.
Iterate through the map values and find all the keys corresponding to that value, to make a Map<String, Set<String>> where the key is eg. Doc1 and the value is eg. activities,admissions. Represent each expression as a Predicate<Set<String>>, which looks at a set of terms and decides whether it matches the expression.
Now,
map.entrySet().stream().filter(e -> expression.test(e.getValue())).map(Entry::getKey).collect(Collectors.toList());
returns the list of documents matching a term expression
The way I see it, you want to achieve union and intersection of list elements for boolean expressions.
You can write a custom method like
public Set<Doc> andOp(Set<Doc> list1, Set<String> Doc) {
if(list1.isEmpty() || list2.isEmpty())
return new HashSet<>();
Set<Doc> result = new HashSet<>();
Iterator<Doc> it = list1.iterator();
while( it.hasNext()) {
Doc Doc1 = it.next();
if(list2.contains(Doc))
result.add(Doc);
}
return result;
}
public Set<Doc> orOp(Set<Doc> list1, Set<Doc> list2) {
if(list1.isEmpty()) return list2;
if(list2.isEmpty()) return list1;
Set<Doc> result = new HashSet<>();
result.addAll(list1);
result.addAll(list2);
return result;
}
So, the resultant expression for (afternoon AND activities) OR (admissions AND admissions) would be
Set<Doc> doc1 = andOp(map.get("activities"), map.get("afternoon"));
Set<Doc> doc2 = andOp(map.get("admissions"), map.get("admissions"));
doc1 = orOp(doc1, doc2);