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.
Related
I have code :
#GetMapping("/goal/{id}")
public String goalInfo(#PathVariable(value = "id") long id, Model model) {
if (!goalRepository.existsById(id)) {
return "redirect:/goal";
}
Iterable<SubGoal> subGoal = subGoalRepository.findAll();
ArrayList<SubGoal> subGoals = new ArrayList<>();
//How refactor this?
for(SubGoal sub : subGoal){
if(sub.getParentGoal().getId().equals(id)){
subGoals.add(sub);
}
}
if(subGoals.size() > 0) {
goalPercent(id, subGoal);
}
Optional<Goal> goal = goalRepository.findById(id);
ArrayList<Goal> result = new ArrayList<>();
goal.ifPresent(result::add);
model.addAttribute("goal", result);
model.addAttribute("subGoal",subGoals);
return "goal/goal-info";
}
Here I get sub-goals from the repository and filter these values.
How I can do it without foreach? I want to use Streams or something else.
You don't need to declare an iterable on your code to filter your ArrayList. The filter method already provides one for you. You can use:
subGoals = subGoals.stream().filter(subGoal ->
/*Here goes your filter condition*/ ).collect(Collectors.toList());
To convert Iterable to Stream use StreamSupport.stream(iter.spliterator(), par).
Iterable<SubGoal> subGoal = subGoalRepository.findAll();
List<SubGoal> subGoals = StreamSupport
.stream(subGoal.spliterator(), false)
.filter(sub -> sub.getParentGoal().getId().equals(id))
.collect(toList()) // static import `Collectors.toList()`
...
Additionally, this part can be also single statement.
before (three statement)
Optional<Goal> goal = goalRepository.findById(id);
ArrayList<Goal> result = new ArrayList<>();
goal.ifPresent(result::add);
after (single statement)
List<Goal> result = goalRepository.findById(id)
.map(goal -> singletonList(goal)) // Collections.singletonList()
.orElse(emptyList()); // Collections.emptyList()
Updates
1. singletonList(), emptyList()
These are just factory methods used when creating single entity list and empty list.
you can change this part any kind of function that has Goal as input and List as output and any empty list.
For example,
.map(goal -> Arrays.asList(goal))
.orElse(new ArrayList<>());
or
.map(goal -> {
ArrayList<Goal> l = new ArrayList<>();
l.add(goal);
return l;
})
...
2. I changed the List Type to List<Goal>, not ArrayList<Goal>
Sorry, I missed explanation about that.
In OOP, using Interface will be better practices than using Concrete Class in many situation.
If you have to use ArrayList<> Type explicitly or want to specify actual list instance in some reason, you can also use toCollection() like below.
.collect(toCollection(ArrayList::new)) // you can specify the actual list instance
Thanks to #John Bollinger #hfontanez for pointing this out.
This is client-side filtering and is extremely inefficient. Instead, simply declare this method on your repository interface:
Collection<SubGoal> findByParentId(Long id); // or Stream, Iterable
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.
I have 2 List
List<Obligation> and List<ObligationStatus>
Structure is as follows:
public class Obligation {
private String topic;
private String status;
private String comment;
private String text;
}
and
public class ObligationStatus {
private String topic;
private String status;
private String comment;
}
status and comment inside List<Obligation> is null for all the elements,
topic is populated in both the list
I want to set the status and comment from each element of List<ObligationStatus> into each element of List<Obligation> based on topic.
// this is what i have tried, and is working fine
obList.stream().forEach(ob -> {
osList.stream().forEach(os -> {
if (ob.getTopic().equalsIgnoreCase(os.getTopic())) {
ob.setStatus(os.getStatus());
ob.setComment(os.getComment());
}
});
});
// also tried to do this without using forEach, but compilation error here
List<Obligation> result = obList.stream()
.map(ob -> osList.stream().map(os -> os.getTopic().equals(ob.getTopic())).collect(Function.identity()))
.collect(Collectors.toList());
Can we do this without suing forEach ?
any info would be helpful.
Why use stream for this? Stream is not the right tool for mutating objects.
Use standard for loops. Makes code easier to understand too.
List<Obligation> obligationList = ...;
List<ObligationStatus> statusList = ...;
// For better performance, make a map
Map<String, ObligationStatus> statusMap = new HashMap<>(statusList.size());
for (ObligationStatus status : statusList)
statusMap.put(status.getTopic(), status);
// Assign status values
for (Obligation obligation : obligationList) {
ObligationStatus status = statusMap.get(obligation.getTopic());
if (status != null) {
ob.setStatus(status.getStatus());
ob.setComment(status.getComment());
}
}
If you want to do some stream logic, the first part is a good candidate:
// For better performance, make a map
Map<String, ObligationStatus> statusMap = statusList.stream()
.collect(Collectors.toMap(ObligationStatus::getTopic, Function.identity()));
UPDATE
Noticed that question code did equalsIgnoreCase(...) when comparing topic values. If that is really needed, change the HashMap to a case-insensitive TreeMap:
Map<String, ObligationStatus> statusMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
The stream version of that gets convoluted, so better keep it old-style.
Since you are modifying an existing List, there's no need to use collect.
You could, however, use a single forEach. Locating the ObligationStatus instance matching the Obligation instance can be done with filter and findFirst.
obList.stream().forEach(ob -> {
osList.stream()
.filter(os -> ob.getTopic().equalsIgnoreCase(os.getTopic()))
.findFirst()
.ifPresent (os -> {
ob.setStatus(os.getStatus());
ob.setComment(os.getComment());
});
});
The model:
public class MyModel{
private int id;
private String name;
....
....
//getters and setters
}
I have a list of MyModel object:
//First Object to be added to list
MyModel myModelObject1 = new MyModel();
myModelObject1.setId(1);
myModelObject1.setName("abc");
//Second Object to be added to list
MyModel myModelObject2 = new MyModel();
myModelObject1.setId(2);
myModelObject1.setName("pqr");
List<MyModel> myModelList = new ArrayList<MyModel>();
myModelList.add(myModelObject1);
myModelList.add(myModelObject2);
I want to get a list of names present in the MyModel List i.e. I want to create a list of names (List<String> in this case) from myModelList. So, I want my list to have:
{"abc", "pqr"}
There is always a way to iterate and create another list but is there any better way to do that? (Not necessarily to be efficient but if it can be done in a line using streams, foreach e.t.c.).
EDIT:
The answers worked for me but I have some modifications in my use case: If I want to add a condition that only name which contains character 'a' should be added to the list and I want to add a logger message to debug for each element then how should I approach this?
I tried doing the following using a method (charAPresent()) which checks that if the passed String contains character 'a' but it didn't work:
List<String> modelNameList = myModelList.stream()
.map(model -> {
if (charAPresent(model.getName)) {
model.getName()
}
})
.collect(Collectors.toList());
Let me know if I am missing something.
Using Java 8 Stream:
List<String> modelNameList = myModelList.stream()
.map(Model::getName)
.collect(Collectors.toList());
Model::getName is called as method reference. It equivalents to model -> model.getName()
You can use streams and map your object to its name and collect to a list as:
List<String> names = myModelList.stream()
.map(MyModel::getName)
.collect(Collectors.toList());
There is always a way to iterate and create another list but is there
any better way to do that
Even with the use of streams, you would have to iterate the complete collection. There is no better way of doing it than iterating the complete collection.
You can use Stream.map() to get only the names of your model. Use Stream.filter() to get only the names matching your charAPresent() method. To log the entries before collecting you can use Stream.peek():
List<String> modelNameList = myModelList.stream()
.map(Model::getName) // map only the name
.filter(YourClass::charAPresent) // filter the items
.peek(System.out::println) // print all items which are in the filter
.collect(Collectors.toList());
You can also use foreach like this:
public static List<String> modelNames(List<MyModel> myModelList)
List<String> list = new ArrayList<String>();
for(MyModel mm : myModelList) {
if(mm.getName().contains("a") {
list.add(mm.getName());
}
}
return list;
}
I have a list of elements, let's call it "keywords", like this:
public class Keyword {
Long id;
String name;
String owner;
Date createdTime;
Double price;
Date metricDay;
Long position;
}
The thing is that there is a keyword for every single day. For example:
Keyword{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", price="0.1", metricDay="11/11/1999", position=109}
Keyword{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", price="0.3", metricDay="12/11/1999", position=108}
Keyword{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", price="0.2", metricDay="13/11/1999", position=99}
Keyword{id=2, name="kw2", owner="Josh", createdTime="13/12/1992", price="0.6", metricDay="13/11/1999", position=5}
Keyword{id=2, name="kw2", owner="Josh", createdTime="13/12/1992", price="0.1", metricDay="14/11/1999", position=4}
Keyword{id=3, name="kw3", owner="Josh", createdTime="13/12/1992", price="0.1", metricDay="13/11/1999", position=8}
Then, from this list I would like to create a new list with all the metrics from all those different days on one single list. First, I created a class like this:
public class KeywordMetric {
Double price;
Date metricDay;
Long position;
}
And what I would like to archive is go from the first list, to a structure like this:
public class KeywordMeged {
Long id;
String name;
String owner;
List<KeywordMetric> metricList;
}
Example of what I expect:
KeywordMerged{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", metricList=[KeywordMetric{price=0.1,metricDay="11/11/1999",position=109},KeywordMetric{price=0.3,metricDay="12/11/1999",position=108},KeywordMetric{price=0.2,metricDay="13/11/1999",position=99}]
KeywordMerged{id=2, name="kw2", owner="Josh", createdTime="13/12/1992", metricList=[KeywordMetric{price=0.6,metricDay="13/11/1999",position=5},KeywordMetric{price=0.1,metricDay="14/11/1999",position=4}]
KeywordMerged{id=3, name="kw3", owner="Josh", createdTime="13/12/1992", metricList=[KeywordMetric{price=0.1,metricDay="13/11/1999",position=8}]
I know how to do this with a lot of loops and mutable varibles, but I can't figure out how to do this with streams and lambda operations. I was able to group all related keywords by Id with this:
Map<Long, List<Keyword>> kwL = kwList.stream()
.collect(groupingBy(Keyword::getId))
And I know that with .forEach() I could iterate over that Map, but can't figure out how to make the collect() method of streams pass from List to KeywordMerged.
You can try to use the Collectors.toMap(...) instead. Where:
Keyword::getId is a key mapper function.
KeywordMerged.from(...) performs a transformation: Keyword => KeywordMerged
(left, right) -> { .. } combines metrics for entities with identical ids.
Collection<KeywordMerged> result = keywords.stream()
.collect(Collectors.toMap(
Keyword::getId,
k -> KeywordMerged.from(k), // you can replace this lambda with a method reference
(left, right) -> {
left.getMetricList().addAll(right.getMetricList());
return left;
}))
.values();
A transformation method might look something like this:
public class KeywordMerged {
public static KeywordMerged from(Keyword k) {
KeywordMetric metric = new KeywordMetric();
metric.setPrice(k.getPrice());
metric.setMetricDay(k.getMetricDay());
metric.setPosition(k.getPosition());
KeywordMerged merged = new KeywordMerged();
merged.setId(k.getId());
merged.setName(k.getName());
merged.setOwner(k.getOwner());
merged.setMetricList(new ArrayList<>(Arrays.asList(metric)));
return merged;
}
}
I think you've got the basic idea. So, refactor according to your needs...
A slightly different approach. First you collect the Map of keywords grouped by id:
Map<Integer, List<Keyword>> groupedData = keywords.stream()
.collect(Collectors.groupingBy(k -> k.getId()));
Further you convert your map to the list of desired format:
List<KeywordMerged> finalData = groupedData.entrySet().stream()
.map(k -> new KeywordMerged(k.getValue().get(0).getId(),
k.getValue().stream()
.map(v -> new KeywordMetric(v.getMetricDay(), v.getPrice(), getPosition()))
.collect(Collectors.toList())))
.collect(Collectors.toList());
This will work on the grouped data, but transforming the map it will create KeywordMerged object, which as argument will receive id (you can extent it further yourself) and converted to List<KeywordMetric> previously grouped by ID data.
EDIT: I believe with some extraction to methods you can make it look much nicer :)