Java 8 - Stream - refer composite objects - java

I have List<Entry> entries, with below code structure,
class Entry {
public Product getProduct() {
return product;
}
}
class Product {
public List<CategoriesData> getCategories() {
return categoriesData;
}
}
class CategoriesData {
public String getName() {
return name;
}
}
I'm looking at sorting by Product - CategoriesData - name (from the first element in List<CategoriesData>)
// Not sure how to refer Name within CategoriesData,
// entries.stream().sorted(
// Comparator.comparing(Entry::getProduct::getCategories::getName))
// .collect(Collectors.toList())

Using Mureinik solution you can do:
Comparator<Entry> entryComperator = Comparator
.comparing(e -> e.getProduct().getCategories().get(0).getName());
List<Entry> sorted =
entries.stream()
.sorted(entryComperator)
.collect(Collectors.toList());
You might consider accessing the name from the list better in case the list is empty. you can hide all this logic in the comparator like I did above

If you're trying to sort by the first category, you need to refer to it in the comparator:
List<Entry> sorted =
entries.stream()
.sorted(Comparator.comparing(
e -> e.getProduct().getCategories().get(0).getName())
.collect(Collectors.toList())
EDIT:
To answer the question in the comments, for a secondary sorting, you'll have to specify the type in the Comparator:
List<Entry> sorted =
entries.stream()
.sorted(Comparator.comparing(
e -> e.getProduct().getCategories().get(0).getName())
.thenComparing(
(Entry e) -> e.getProduct().getName())
.collect(Collectors.toList())

With input from #Mureinik and making some modifications, I ended up with below and it is working. My requirement slightly changed, I need result in a map. Category name is the key in the map, value will be a list of 'Entry'.
final Map<String, List<Entry>> sortedMap =
data.getEntries().stream()
.sorted(Comparator.comparing(e -> ((Entry) e).getProduct().getCategories().stream().findFirst().get().getName())
.thenComparing(Comparator.comparing(e -> ((Entry) e).getProduct().getName())) )
.collect(Collectors.groupingBy(e -> e.getProduct().getCategories().stream().findFirst().get().getName(),
LinkedHashMap::new, Collectors.toList()));

Related

Getting data from Map inside a Map with streams

I have a problem with receiving data from Map nested in another Map.
private Map<Customer, Map<Item,Integer>> orders;
I'm generating this map from JSON, its add Customer if he is not on the list with Items and their number.
If Customer is already in the map then key Item in the second map is updated and if a key was there already then Integer which is the number of items is updated.
Classes Customer and Items are not connected I mean Class Customer don't have field Items and class Items don't have a field Customer.
public class Customer {
private String name;
private String surname;
private Integer age;
private BigDecimal money;
}
public class Item {
private String name;
private String category;
private BigDecimal price;
}
Using streams I want to get for example Customer who paid the most for items but I have problem with getting this data from the map, it was not so hard with List but now I can't figure it out.
Ok I did figure out something like this and it seems to be working but I'm sure it can be simplified.
Customer key = customersMap.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
e -> e.getValue()
.entrySet()
.stream()
.map(o -> o.getKey().getPrice().multiply(BigDecimal.valueOf(o.getValue())))
.collect(Collectors.toList())))
.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, t -> t.getValue().stream().reduce(BigDecimal.ZERO, BigDecimal::add)))
.entrySet()
.stream()
.max(Map.Entry.comparingByValue())
.orElseThrow()
.getKey();
Your answer Naman was very helpful so maybe you can give me advice about this.
This is how I'm receiving it from JSON.
JsonConverterCustomer jsonConverterCustomer = new JsonConverterCustomer(FILENAME3);
List<Order> orders = jsonConverterCustomer.fromJson().orElseThrow();
Map<Customer, Map<Item, Integer>> customersMap = new HashMap<>();
for (Order order : orders) {
if (!customersMap.containsKey(order.getCustomer())) {
addNewCustomer(customersMap, order);
} else {
for (Product product : order.getItems()) {
if (!customersMap.get(order.getCustomer()).containsKey(items)) {
addNewCustomerItem(item, customersMap.get(order.getCustomer()));
} else {
updateCustomerItem(customersMap, order, item);
}
}
}
}
private static void updateCustomerProduct(Map<Customer, Map<Item, Integer>> customersMap, Order order, Item item) {
customersMap.get(order.getCustomer())
.replace(item,
customersMap.get(order.getCustomer()).get(item),
customersMap.get(order.getCustomer()).get(item) + 1);
}
private static void addNewCustomerItem(Item item, Map<Item, Integer> itemIntegerMap) {
itemIntegerMap.put(item, 1);
}
private static void addNewCustomer(Map<Customer, Map<Item, Integer>> customersMap, Order order) {
Map<Item, Integer> temp = new HashMap<>();
addNewCustomerItem(order.getItems().get(0), temp);
customersMap.put(order.getCustomer(), temp);
}
Order class is a class which one help me receiving data from JSON
It is a simple class with Customer as a field and List as a field.
As you can see I'm receiving List of Orders and from it, I'm creating this Map.
Can I make it more functional? Using streams? I was trying to do but not sure how;/
There are two possible ways to make it more maintainable/readable as Jason pointed out and at the same time simplify the logic performed.
One, you can get rid of one of the stages in the pipeline and merge map and reduce into a single pipeline.
Another would be to abstract out per customer computation of the total amount paid by them.
So the abstraction would look like the following and work on the inner maps for your input:
private BigDecimal totalPurchaseByCustomer(Map<Item, Integer> customerOrders) {
return customerOrders.entrySet()
.stream()
.map(o -> o.getKey().getPrice().multiply(BigDecimal.valueOf(o.getValue())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
Now to easily fit this in while you iterate for each customer entry, you can do that in a single collect itself:
private Customer maxPayingCustomer(Map<Customer, Map<Item, Integer>> customersMap) {
Map<Customer, BigDecimal> customerPayments = customersMap.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
e -> totalPurchaseByCustomer(e.getValue())));
return customerPayments.entrySet()
.stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElseThrow();
}

How to Filter List<String,Object> Collection with java stream?

I have
List<String, Person> generalList
as a list.
there is Customer object Under Person, 1 more list under Customer named Id
I want to filter this nested IdList under object but it is not working.
I tried to use flatMap but this code is not working
String s = generalList.stream()
.flatMap(a -> a.getCustomer().getIdList().stream())
.filter(b -> b.getValue().equals("1"))
.findFirst()
.orElse(null);
I expect the output as String or Customer object
Edit:
My original container is a map, I am filtering Map to List
Explanation.
Map<String, List<Person> container;
List<Person> list = container.get("A");
String s = list.stream()
.flatMap(a -> a.getCustomer().getIdList().stream())
.filter(b -> b.getValue().equals("1"))
.findFirst()
.orElse(null);
Here is the Person
public class Person
{
private Customer customer;
public Customer getCustomer ()
{
return customer;
}
}
And Customer
public class Customer {
private Id[] idList;
/*getter setter*/
}
And Id
public class Id {
private String value;
/*getter setter*/
}
You're possibly looking for a map operation as:
String s = list.stream()
.flatMap(a -> a.getCustomer().getIdList().stream())
.filter(b -> b.getValue().equals("1"))
.findFirst()
.map(Id::getValue) // map to the value of filtered Id
.orElse(null);
which is equivalent of(just to clarify)
String valueToMatch = "1";
String s = list.stream()
.flatMap(a -> a.getCustomer().getIdList().stream())
.anyMatch(b -> b.getValue().equals(valueToMatch))
? valueToMatch : null;
Update 2 This solution works directly on a list of Person objects:
String key = "1";
List<Person> list = container.get("A");
String filteredValue = list.stream()
.flatMap(person -> Arrays.stream(person.getCustomer().getId())
.filter(id -> id.getValue().equals(key)))
.findFirst().get().getValue();
Old answer working with map
Since you are only interested in the values of your map you should stream on them and in the flatMap I not only got a stream on the getId() list but also filtered on them directly. So if I understood your code structure correctly this should work
String key = "1";
String filteredValue = map.values().stream()
.flatMap(list -> list.stream()
.flatMap(person -> Arrays.stream(person.getCustomer().getId())
.filter(id -> id.getValue().equals("1"))))
.findFirst().get().getValue();
Updated to adjust for edited question

Java 8 Join Map with Collectors.toMap

I'm trying to collect in a Map the results from the process a list of objects and that it returns a map. I think that I should do it with a Collectors.toMap but I haven't found the way.
This is the code:
public class Car {
List<VersionCar> versions;
public List<VersionCar> getVersions() {
return versions;
}
}
public class VersionCar {
private String wheelsKey;
private String engineKey;
public String getWheelsKey() {
return wheelsKey;
}
public String getEngineKey() {
return engineKey;
}
}
process method:
private static Map<String,Set<String>> processObjects(VersionCar version) {
Map<String,Set<String>> mapItems = new HashMap<>();
mapItems.put("engine", new HashSet<>(Arrays.asList(version.getEngineKey())));
mapItems.put("wheels", new HashSet<>(Arrays.asList(version.getWheelsKey())));
return mapItems;
}
My final code is:
Map<String,Set<String>> mapAllItems =
car.getVersions().stream()
.map(versionCar -> processObjects(versionCar))
.collect(Collectors.toMap()); // here I don't know like collect the map.
My idea is to process the list of versions and in the end get a Map with two items: wheels and engine but with a set<> with all different items for all versions. Do you have any ideas as can I do that with Collectors.toMap or another option?
The operator you want to use in this case is probably "reduce"
car.getVersions().stream()
.map(versionCar -> processObjects(versionCar))
.reduce((map1, map2) -> {
map2.forEach((key, subset) -> map1.get(key).addAll(subset));
return map1;
})
.orElse(new HashMap<>());
The lambda used in "reduce" is a BinaryOperator, that merges 2 maps and return the merged map.
The "orElse" is just here to return something in the case your initial collection (versions) is empty.
From a type point of view it gets rid of the "Optional"
You can use Collectors.toMap(keyMapper, valueMapper, mergeFunction). Last argument is used to resolve collisions between values associated with the same key.
For example:
Map<String, Set<String>> mapAllItems =
car.getVersions().stream()
.map(versionCar -> processObjects(versionCar))
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(firstSet, secondSet) -> {
Set<String> result = new HashSet<>();
result.addAll(firstSet);
result.addAll(secondSet);
return result;
}
));
To get the mapAllItems, we don't need and should not define processObjects method:
Map<String, Set<String>> mapAllItems = new HashMap<>();
mapAllItems.put("engine", car.getVersions().stream().map(v -> v.getEngineKey()).collect(Collectors.toSet()));
mapAllItems.put("wheels", car.getVersions().stream().map(v -> v.getWheelsKey()).collect(Collectors.toSet()));
Or by AbstractMap.SimpleEntry which is lighter than the Map created byprocessObjects`:
mapAllItems = car.getVersions().stream()
.flatMap(v -> Stream.of(new SimpleEntry<>("engine", v.getEngineKey()), new SimpleEntry<>("wheels", v.getWheelsKey())))
.collect(Collectors.groupingBy(e -> e.getKey(), Collectors.mapping(e -> e.getValue(), Collectors.toSet())));

Set created from complex List

So I have a list containing duplicated entities from database with the same "Id" (it's not the real Id but kind of) but a different CreatedDate.
So I would like to have the latest entity from duplicates with the latest CreatedDate.
Example I have a list of created users :
RealId|CreatedDate|Id|Name
1|20170101|1|User1
2|20170102|1|User1Modified
3|20170103|2|User2
4|20170104|2|User2Modified
From that list what is the best way to obtain :
RealId|CreatedDate|Id|Name
2|20170102|1|User1Modified
4|20170104|2|User2Modified
This is my first idea
List<T> r = query.getResultList();
Set<T> distinct = r.stream().filter(x -> {
List<T> clones = r.stream()
.filter(y -> y.getId() == x.getId())
.collect(Collectors.toList());
T max = clones.stream()
.max(Comparator.comparing(AbstractEntityHistory::getEntryDate))
.get();
return max.getNumber() == x.getNumber();
}).collect(Collectors.toSet());
An other idea I have is to make it order descending by the date then do distinct().collect() like :
Set<T> distinct2 = r.stream().sorted((x,y) -> {
if(x.getEntryDate().isBefore(y.getEntryDate())) {
return 1;
} else if(x.getEntryDate().isAfter(y.getEntryDate())) {
return -1;
} else {
return 0;
}
}).distinct().collect(Collectors.toSet());
Here, T overrides equals which watch for the RealId if they are equal else use reflection to watch every other field.
Try this:
List<YourObject> collect = activities
.stream()
.collect(Collectors.groupingBy(
YourObject::getId,
Collectors.maxBy(Comparator.comparing(YourObject::getCreatedDate))))
.entrySet()
.stream()
.map(e -> e.getValue().get())
.collect(Collectors.toList());
Here is used Collectors.groupingBy to create a Map<Integer, Optional<YourObject>>, grouped by id and most recent createDate. The you get the entrySet for this map and collect it to a List.
Without java8 functional stuff:
Map<Long, Item> map = new HashMap<>();
for (Item item: items) {
Item old = map.get(item.getId());
if (old == null || old.getDate().before(item.getDate())) {
map.put(item.getId(), item);
}
}
List<Item> result = new ArrayList<Item>(map.values());

Find the most common attribute value from a List of objects using Stream

I have two classes that are structured like this:
public class Company {
private List<Person> person;
...
public List<Person> getPerson() {
return person;
}
...
}
public class Person {
private String tag;
...
public String getTag() {
return tag;
}
...
}
Basically the Company class has a List of Person objects, and each Person object can get a Tag value.
If I get the List of the Person objects, is there a way to use Stream from Java 8 to find the one Tag value that is the most common among all the Person objects (in case of a tie, maybe just a random of the most common)?
String mostCommonTag;
if(!company.getPerson().isEmpty) {
mostCommonTag = company.getPerson().stream() //How to do this in Stream?
}
String mostCommonTag = getPerson().stream()
// filter some person without a tag out
.filter(it -> Objects.nonNull(it.getTag()))
// summarize tags
.collect(Collectors.groupingBy(Person::getTag, Collectors.counting()))
// fetch the max entry
.entrySet().stream().max(Map.Entry.comparingByValue())
// map to tag
.map(Map.Entry::getKey).orElse(null);
AND the getTag method appeared twice, you can simplify the code as further:
String mostCommonTag = getPerson().stream()
// map person to tag & filter null tag out
.map(Person::getTag).filter(Objects::nonNull)
// summarize tags
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
// fetch the max entry
.entrySet().stream().max(Map.Entry.comparingByValue())
// map to tag
.map(Map.Entry::getKey).orElse(null);
You could collect the counts to a Map, then get the key with the highest value
List<String> foo = Arrays.asList("a","b","c","d","e","e","e","f","f","f","g");
Map<String, Long> f = foo
.stream()
.collect(Collectors.groupingBy(v -> v, Collectors.counting()));
String maxOccurence =
Collections.max(f.entrySet(), Comparator.comparing(Map.Entry::getValue)).getKey();
System.out.println(maxOccurence);
This should work for you:
private void run() {
List<Person> list = Arrays.asList(() -> "foo", () -> "foo", () -> "foo",
() -> "bar", () -> "bar");
Map<String, Long> commonness = list.stream()
.collect(Collectors.groupingBy(Person::getTag, Collectors.counting()));
Optional<String> mostCommon = commonness.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey);
System.out.println(mostCommon.orElse("no elements in list"));
}
public interface Person {
String getTag();
}
The commonness map contains the information which tag was found how often. The variable mostCommon contains the tag that was found most often. Also, mostCommon is empty, if the original list was empty.
If you are open to using a third-party library, you can use Collectors2 from Eclipse Collections with a Java 8 Stream to create a Bag and request the topOccurrences, which will return a MutableList of ObjectIntPair which is the tag value and the count of the number of occurrences.
MutableList<ObjectIntPair<String>> topOccurrences = company.getPerson()
.stream()
.map(Person::getTag)
.collect(Collectors2.toBag())
.topOccurrences(1);
String mostCommonTag = topOccurrences.getFirst().getOne();
In the case of a tie, the MutableList will have more than one result.
Note: I am a committer for Eclipse Collections.
This is helpful for you,
Map<String, Long> count = persons.stream().collect(
Collectors.groupingBy(Person::getTag, Collectors.counting()));
Optional<Entry<String, Long>> maxValue = count .entrySet()
.stream().max((entry1, entry2) -> entry1.getValue() > entry2.getValue() ? 1 : -1).get().getKey();
maxValue.get().getValue();
One More solution by abacus-common
// Comparing the solution by jdk stream,
// there is no "collect(Collectors.groupingBy(Person::getTag, Collectors.counting())).entrySet().stream"
Stream.of(company.getPerson()).map(Person::getTag).skipNull() //
.groupBy(Fn.identity(), Collectors.counting()) //
.max(Comparators.comparingByValue()).map(e -> e.getKey()).orNull();
// Or by multiset
Stream.of(company.getPerson()).map(Person::getTag).skipNull() //
.toMultiset().maxOccurrences().map(e -> e.getKey()).orNull();

Categories

Resources