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));
});
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 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();
}
Using java stream, how to create a Map from a List to index by 2 keys on the same class?
I give here a code Example, I would like the map "personByName" to get all person by firstName OR lastName, so I would like to get the 3 "steves": when it's their firstName or lastname. I don't know how to mix the 2 Collectors.groupingBy.
public static class Person {
final String firstName;
final String lastName;
protected Person(String firstName, String lastName) {
super();
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}
#Test
public void testStream() {
List<Person> persons = Arrays.asList(
new Person("Bill", "Gates"),
new Person("Bill", "Steve"),
new Person("Steve", "Jobs"),
new Person("Steve", "Wozniac"));
Map<String, Set<Person>> personByFirstName = persons.stream().collect(Collectors.groupingBy(Person::getFirstName, Collectors.toSet()));
Map<String, Set<Person>> personByLastName = persons.stream().collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()));
Map<String, Set<Person>> personByName = persons.stream().collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()));// This is wrong, I want bot first and last name
Assert.assertEquals("we should search by firstName AND lastName", 3, personByName.get("Steve").size()); // This fails
}
I found a workaround by looping on the 2 maps, but it is not stream-oriented.
You can do it like this:
Map<String, Set<Person>> personByName = persons.stream()
.flatMap(p -> Stream.of(new SimpleEntry<>(p.getFirstName(), p),
new SimpleEntry<>(p.getLastName(), p)))
.collect(Collectors.groupingBy(SimpleEntry::getKey,
Collectors.mapping(SimpleEntry::getValue, Collectors.toSet())));
Assuming you add a toString() method to the Person class, you can then see result using:
List<Person> persons = Arrays.asList(
new Person("Bill", "Gates"),
new Person("Bill", "Steve"),
new Person("Steve", "Jobs"),
new Person("Steve", "Wozniac"));
// code above here
personByName.entrySet().forEach(System.out::println);
Output
Steve=[Steve Wozniac, Bill Steve, Steve Jobs]
Jobs=[Steve Jobs]
Bill=[Bill Steve, Bill Gates]
Wozniac=[Steve Wozniac]
Gates=[Bill Gates]
You could merge the two Map<String, Set<Person>> for example
Map<String, Set<Person>> personByFirstName =
persons.stream()
.collect(Collectors.groupingBy(
Person::getFirstName,
Collectors.toCollection(HashSet::new))
);
persons.stream()
.collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()))
.forEach((str, set) -> personByFirstName.merge(str, set, (s1, s2) -> {
s1.addAll(s2);
return s1;
}));
// personByFirstName contains now all personByName
One way would be by using the newest JDK12's Collector.teeing:
Map<String, List<Person>> result = persons.stream()
.collect(Collectors.teeing(
Collectors.groupingBy(Person::getFirstName,
Collectors.toCollection(ArrayList::new)),
Collectors.groupingBy(Person::getLastName),
(byFirst, byLast) -> {
byLast.forEach((last, peopleList) ->
byFirst.computeIfAbsent(last, k -> new ArrayList<>())
.addAll(peopleList));
return byFirst;
}));
Collectors.teeing collects to two separate collectors and then merges the results into a final value. From the docs:
Returns a Collector that is a composite of two downstream collectors. Every element passed to the resulting collector is processed by both downstream collectors, then their results are merged using the specified merge function into the final result.
So, the above code collects to a map by first name and also to a map by last name and then merges both maps into a final map by iterating the byLast map and merging each one of its entries into the byFirst map by means of the Map.computeIfAbsent method. Finally, the byFirst map is returned.
Note that I've collected to a Map<String, List<Person>> instead of to a Map<String, Set<Person>> to keep the example simple. If you actually need a map of sets, you could do it as follows:
Map<String, Set<Person>> result = persons.stream().
.collect(Collectors.teeing(
Collectors.groupingBy(Person::getFirstName,
Collectors.toCollection(LinkedHashSet::new)),
Collectors.groupingBy(Person::getLastName, Collectors.toSet()),
(byFirst, byLast) -> {
byLast.forEach((last, peopleSet) ->
byFirst.computeIfAbsent(last, k -> new LinkedHashSet<>())
.addAll(peopleSet));
return byFirst;
}));
Keep in mind that if you need to have Set<Person> as the values of the maps, the Person class must implement the hashCode and equals methods consistently.
If you want a real stream-oriented solution, make sure you don't produce any large intermediate collections, else most of the sense of streams is lost.
If just you want to just filter all Steves, filter first, collect later:
persons.stream
.filter(p -> p.getFirstName().equals('Steve') || p.getLastName.equals('Steve'))
.collect(toList());
If you want to do complex things with a stream element, e.g. put an element into multiple collections, or in a map under several keys, just consume a stream using forEach, and write inside it whatever handling logic you want.
You cannot key your maps by multiple values. For what you want to achieve, you have three options:
Combine your "personByFirstName" and "personByLastName" maps, you will have duplicate values (eg. Bill Gates will be in the map under the key Bill and also in the map under the key Gates). #Andreas answer gives a good stream-based way to do this.
Use an indexing library like lucene and index all your Person objects by first name and last name.
The stream approach - it will not be performant on large data sets but you can stream your collection and use filter to get your matches:
persons
.stream()
.filter(p -> p.getFirstName().equals("Steve")
|| p.getLastName().equals("Steve"))
.collect(Collectors.asList());
(I've written the syntax from memory so you might have to tweak it).
If I got it right you want to map each Person twice, once for the first name and once for the last.
To do this you have to double your stream somehow. Assuming Couple is some existing 2-tuple (Guava or Vavr have some nice implementation) you could:
persons.stream()
.map(p -> new Couple(new Couple(p.firstName, p), new Couple(p.lastName, p)))
.flatMap(c -> Stream.of(c.left, c.right)) // Stream of Couple(String, Person)
.map(c -> new Couple(c.left, Arrays.asList(c.right)))
.collect(Collectors.toMap(Couple::getLeft, Couple::getRight, Collection::addAll));
I didn't test it, but the concept is: make a stream of (name, person), (surname, person)... for every person, then simply map for the left value of each couple. The asList is to have a collection as value. If you need a Set chenge the last line with .collect(Collectors.toMap(Couple::getLeft, c -> new HashSet(c.getRight), Collection::addAll))
Try SetMultimap, either from Google Guava or my library abacus-common
SetMultimap<String, Person> result = Multimaps.newSetMultimap(new HashMap<>(), () -> new HashSet<>()); // by Google Guava.
// Or result = N.newSetMultimap(); // By Abacus-Util
persons.forEach(p -> {
result.put(p.getFirstName(), p);
result.put(p.getLastName(), p);
});
I have a simple list of strings. My goal is to get the last occurrences of each string in the list by group.
This is mode code:
List<String> newData = new ArrayList<>();
newData.add("A-something");
newData.add("A-fdfdsfds");
newData.add("A-fdsfdsfgs");
newData.add("B-something");
newData.add("B-dsafdrsafd");
newData.add("B-dsdfsad");
I wish to get only the last occurrence of each group. In other words I wanst to get "A-fdsfdsfgs" and "B-dsdfsad" only.
How to do so?
To get last occurrences for each group you can use stream api with groupingBy:
import static java.util.stream.Collectors.*;
Map<String, Optional<String>> collect = newData.stream()
.collect(groupingBy(strings -> strings.split("-")[0],
mapping(s -> s, maxBy(Comparator.comparingInt(newData::lastIndexOf)))));
Note: map has Optional as a value
To get it without Optional use toMap instead of groupingBy:
Map<String, String> collect = newData.stream()
.collect(toMap(s -> s.split("-")[0],
Function.identity(),
(s1, s2) -> newData.lastIndexOf(s1) > newData.lastIndexOf(s2) ? s1 : s2));
Also if you want to have map values without group name, then change Function.identity() with s -> s.split("-")[1]
import java.util.*;
class Solution {
public static void main(String[] args) {
List<String> newData = new ArrayList<>();
newData.add("A-something");
newData.add("A-fdfdsfds");
newData.add("A-fdsfdsfgs");
newData.add("B-something");
newData.add("B-dsafdrsafd");
newData.add("B-dsdfsad");
System.out.println(lastOccurrences(newData).toString());
}
private static List<String> lastOccurrences(List<String> data){
Set<String> set = new HashSet<>();
List<String> ans = new ArrayList<>();
for(int i=data.size()-1;i>=0;--i){
String group = data.get(i).substring(0,data.get(i).indexOf("-"));
if(set.contains(group)) continue;
set.add(group);
ans.add(data.get(i));
}
return ans;
}
}
Output:
[B-dsdfsad, A-fdsfdsfgs]
Algorithm:
Move from last to first, instead of first to last because you want last occurrences. This will make the management easier and code a little bit clean.
Get the group the string belongs to using substring() method.
Use a set to keep track of already visited groups.
If a group is not in the set, add it to the set and current string to our answer(since this will be the last occurred) for this group.
Finally, return the list.
There are several ways to this, as the other answers already show. I’d find something like the following natural:
Collection<String> lastOfEach = newData.stream()
.collect(Collectors.groupingBy((String s) -> s.split("-")[0],
Collectors.reducing("", s -> s, (l, r) -> r)))
.values();
lastOfEach.forEach(System.out::println);
With your list the output is:
A-fdsfdsfgs
B-dsdfsad
My grouping is the same as in a couple of other answers. On the grouped values I perform a reduction, each time I got two strings taking the latter of them. In the end this will give us the last string from each group as requested. Since groupingBy produces a map, I use values to discard the keys ( A and B) and get only the original strings.
Collecting via grouping should be sufficient.
final Map<String, List<String>> grouped =
newData.stream()
.collect(groupingBy(s -> s.split("-")[0]));
final List<String> lastOccurrences =
grouped.values()
.stream()
.filter(s -> !s.isEmpty())
.map(s -> s.get(s.size() - 1))
.collect(toList());
For Java 11, the filter becomes filter(not(List::isEmpty))
This will give you fdsfdsfgs, dsdfsad
Using a temporary Map. The List finalList will have only the required values
Map<String, String> tempMap = new HashMap<>();
List<String> finalList = new ArrayList<>();
newData.forEach((val) -> tempMap.put(val.split("-")[0], val.split("-")[1]));
tempMap.forEach((key, val) -> finalList.add(key + "-" + val));
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();