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();
}
Related
I hava an order list that contains different id and date.
Now, I need to combine order amount of same id and date.
The core of this problem maybe is how to create a Collector that counld combine Orders with same id and date, and the result is an Order list rather than a Map<Integer, List>.
Is there any other ways to simplify this flow?
public class Order {
private Integer id;
private LocalDate date;
private double amount;
public void accept(Order other) {
setId(other.getId());
setDate(other.getDate());
setAmount(getAmount() + other.getAmount());
}
public Order combine(Order other) {
setId(other.getId());
setDate(other.getDate());
setAmount(getAmount() + other.getAmount());
return this;
}
}
List<Order> result = new ArrayList<>();
List<Order> orders = mockData();
Map<Integer, List<Order>> collect = list.stream()
.collect(groupingBy(Order::getId));
collect.forEach((id, orders) -> {
Map<LocalDate, Order> resultMap = orders.stream()
.collect(groupingBy(Order::getDate, mapping(order -> order, Collector.of(Order::new, Order::accept, Order::combine))));
result.addAll(resultMap.values());
});
I would first make a new record so that you can work with id and date at once.
record IdAndDate(Integer id, LocalDate date) {}
To not get a List<Order> as the map's value type, use the toMap collector. You can then specify a "merge function". That is where you specify Order::combine.
var result = new ArrayList<>(
orders.stream().collect(
Collectors.toMap(
x -> new IdAndDate(x.getId(), x.getDate()), // key mapper
Function.identity(), // value mapper (no change)
Order::combine // merge function
)
).values()
);
Note that Order.combine changes the instance on which it is called, which means that some of the orders in the original list would be changed by this operation. This is also true for your original code, so I'll assume this fact doesn't matter in your case. Just in case you don't want that to happen, you should make combine return a new instance of Order, instead of this.
You could also nest the groupingBy calls and do it like:
List<Order> orders = mockData();
List<Order> result = orders.stream() // Stream<Order>
.collect(Collectors.groupingBy(Order::getId,
Collectors.groupingBy(Order::getDate))) //Map<Integer, Map<LocalDate,List<Order>>>
.values() //Collection<Map<LocalDate,List<Order>>>
.stream() ////Stream<Map<LocalDate,List<Order>>>
.flatMap(m -> m.values().stream()) //Stream<List<Order>>
.map(list -> list.stream().reduce(Order::combine).get()) // Stream<Order>
.collect(Collectors.toList());
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()));
This is my course model in class named Course:
public class Course{
private int courseId;
private String courseName;
private Teacher teacher;
}
This is my teacher model in class named Teacher:
public class Teacher{
private int teacherId;
private String name;
}
I want to get a Map<String, List<Course>> but if the teacherId is repeated just add that Course into list of map.
I am using groupBy for it
Map<Integer, List<Course>> result = courses.stream()
.collect(Collectors.groupingBy(c -> c.getTeacher().getTeacherId(), Collectors.toList()));
and it's giving result as expected.
But I want to limit here that As soon as 5 teachers are found stop to process and returned the result.
How can it be done??
There’s no direct support for this, as stopping and living with potentially incomplete data is rather unusual.
A straight-forward solution collecting the first five groups completely, would be
Set<Integer> firstFive = courses.stream()
.map(c -> c.getTeacher().getTeacherId())
.distinct().limit(5)
.collect(Collectors.toSet());
Map<Integer, List<Course>> result = courses.stream()
.filter(c -> firstFive.contains(c.getTeacher().getTeacherId()))
.collect(Collectors.groupingBy(c -> c.getTeacher().getTeacherId()));
Here, the Course lists of these first five teacher ids are complete.
A solution that truly stops after encountering the 5th teacher id, would be simpler with a loop:
Map<Integer, List<Course>> result = new HashMap<>();
for(Course c: courses) {
result.computeIfAbsent(c.getTeacher().getTeacherId(), x -> new ArrayList<>()).add(c);
if(result.size() == 5) break;
}
But there is not much sense in collecting lists of Courses, when you can’t trust these lists afterwards. Keep in mind, that even the source list’s very last element could belong to the first encountered teacher ID, so you need to process the entire list even if you are interested in only one teacher’s complete list of courses.
Not sure if this is what you are asking.
Map<Integer, List<Course>> map = new HashMap<>();
courses.stream().filter(course -> map.keySet().size() < 10)
.forEach(entry -> {
// The code below can be simplified
int teacherId = entry.getTeacher().getTeacherId();
if(map.get(teacherId) != null)
map.get(teacherId).add(entry);
else
map.put(teacherId, Lists.newArrayList(entry));
});
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();
I have the following data structure -
List of Students that each holds a lists of States that each holds a list of cities.
public class Student {
private int id;
private String name;
private List<State> states = new ArrayList<>();
}
public class State {
private int id;
private String name;
private List<City> Cities = new ArrayList<>();
}
public class City {
private int id;
private String name;
}
I want to get the following.
Map<String, Students> citiesIdsToStudensList;
I write the following
Map<Integer, List<Integer>> statesToStudentsMap = students.stream()
.flatMap(student -> student.getStates().stream())
.flatMap(state -> state.getCities().stream())
.collect(Collectors.groupingBy(City::getId, Collectors.mapping(x -> x.getId(), Collectors.toList())));
But it doesn't get me the result I want.
Using the Stream API, you'll need to flat map twice and map each intermediate student and city into a tuple that is capable of holding the student.
Map<Integer, List<Student>> citiesIdsToStudentsList =
students.stream()
.flatMap(student -> student.getStates().stream().map(state -> new AbstractMap.SimpleEntry<>(student, state)))
.flatMap(entry -> entry.getValue().getCities().stream().map(city -> new AbstractMap.SimpleEntry<>(entry.getKey(), city)))
.collect(Collectors.groupingBy(
entry -> entry.getValue().getId(),
Collectors.mapping(Map.Entry::getKey, Collectors.toList())
));
However, it would maybe be cleaner to use nested for loops here:
Map<Integer, List<Student>> citiesIdsToStudentsList = new HashMap<>();
for (Student student : students) {
for (State state : student.getStates()) {
for (City city : state.getCities()) {
citiesIdsToStudentsList.computeIfAbsent(city.getId(), k -> new ArrayList<>()).add(student);
}
}
}
This leverages computeIfAbsent to populate the map and creates a list of each student with the same city id.
In addition to Tunaki’s answer, you can simplify it as
Map<Integer, List<Student>> citiesIdsToStudentsList =
students.stream()
.flatMap(student -> student.getStates().stream()
.flatMap(state -> state.getCities().stream())
.map(state -> new AbstractMap.SimpleEntry<>(student, state.getId())))
.collect(Collectors.groupingBy(
Map.Entry::getValue,
Collectors.mapping(Map.Entry::getKey, Collectors.toList())
));
It utilizes the fact that you are not actually interested in State objects, so you can flatMap them directly to the desired City objects, if you do it right within the first flatMap operation. Then, by performing the State.getId operation immediately when creating the Map.Entry, you can simplify the actual collect operation.