Given a List of the following Transaction class, using Java 8 lambdas, I want to obtain a List of ResultantDTO, one per account type.
public class Transaction {
private final BigDecimal amount;
private final String accountType;
private final String accountNumber;
}
public class ResultantDTO {
private final List<Transaction> transactionsForAccount;
public ResultantDTO(List<Transaction> transactionsForAccount){
this.transactionsForAccount = transactionsForAccount;
}
}
So far, I use the following code to group the List<Transaction> by accountType.
Map<String, List<Transaction>> transactionsGroupedByAccountType = transactions
.stream()
.collect(groupingBy(Transaction::getAccountType));
How do I return a List<ResultantDTO>, passing the List from each map key into the constructor, containing one ResultantDTO per accountType?
You can do this in single stream operation:
public List<ResultantDTO> convert(List<Transaction> transactions) {
return transactions.stream().collect(
collectingAndThen(
groupingBy(
Transaction::getAccountType,
collectingAndThen(toList(), ResultantDTO::new)),
map -> new ArrayList<>(map.values())));
}
Here collectingAndThen used twice: once for downstream Collector to convert lists to the ResultantDTO objects and once to convert the resulting map to list of its values.
Assuming you have a:
static ResultantDTO from(List<Transaction> transactions) {...}
You could write:
Map<String, List<Transaction>> transactionsGroupedByAccountType = transactions
.stream()
.collect(groupingBy(Transaction::getAccountType));
Map<String, ResultantDTO> result = transactionsGroupedByAccountType.entrySet().stream()
.collect(toMap(Entry::getKey, e -> from(e.getValue)));
You may be able to do it in one stream but this is probably cleaner and simpler.
You could use the toMap collector:
Map<String, ResultantDTO> transactionsGroupedByAccountType = transactions
.stream()
.collect(toMap(Transaction::getAccountType,
t -> new ResultantDTO(t.getAmount(),
Stream.of(new SimpleEntry<>(t.getAccountNumber(), t.getAmount()))
.collect(toMap(SimpleEntry::getKey, SimpleEntry::getValue))),
(dt1, dt2) -> new ResultantDTO(dt1.getSumOfAmountForAccountType().add(dt2.getSumOfAmountForAccountType()),
Stream.of(dt1.getAccountNumberToSumOfAmountForAccountNumberMap(), dt2.getAccountNumberToSumOfAmountForAccountNumberMap())
.flatMap(m -> m.entrySet().stream())
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue)))));
although its not very readable. Maybe you could add a constructor that takes a single Transaction as parameter and a merger in the ResultantDTO class:
public ResultantDTO(Transaction t) {
///
}
static ResultantDTO merger(ResultantDTO r1, ResultantDTO r2) {
///
}
and there it is more readable:
Map<String, ResultantDTO> transactionsGroupedByAccountType = transactions
.stream()
.collect(toMap(Transaction::getAccountType,
ResultantDTO::new,
ResultantDTO::merger));
Related
I wrote a stream pipeline:
private void calcMin(Clazz clazz) {
OptionalInt min = listOfObjects.stream().filter(y -> (y.getName()
.matches(clazz.getFilter())))
.map(y -> (y.getUserNumber()))
.mapToInt(Integer::intValue)
.min();
list.add(min.getAsInt());
}
This pipeline gives me the lowest UserNumber.
So far, so good.
But I also need the greatest UserNumber.
And I also need the lowest GroupNumber.
And also the greatest GroupNumber.
I could write:
private void calcMax(Clazz clazz) {
OptionalInt max = listOfObjects.stream().filter(y -> (y.getName()
.matches(clazz.getFilter())))
.map(y -> (y.getUserNumber()))
.mapToInt(Integer::intValue)
.max();
list.add(max.getAsInt());
}
And I could also write the same for .map(y -> (y.getGroupNumber())).
This will work, but it is very redudant.
Is there a way to do it more variable?
There are two differences in the examples: the map() operation, and the terminal operation (min() and max()). So, to reuse the rest of the pipeline, you'll want to parameterize these.
I will warn you up front, however, that if you call this parameterized method directly from many places, your code will be harder to read. Comprehension of the caller's code will be easier if you keep a helper function—with a meaningful name—that delegates to the generic method. Obviously, there is a balance here. If you wanted to add additional functional parameters, the number of helper methods would grow rapidly and become cumbersome. And if you only call each helper from one place, maybe using the underlying function directly won't add too much clutter.
You don't show the type of elements in the stream. I'm using the name MyClass in this example as a placeholder.
private static OptionalInt extremum(
Collection<? extends MyClass> input,
Clazz clazz,
ToIntFunction<? super MyClass> valExtractor,
Function<IntStream, OptionalInt> terminalOp) {
IntStream matches = input.stream()
.filter(y -> y.getName().matches(clazz.getFilter()))
.mapToInt(valExtractor);
return terminalOp.apply(matches);
}
private OptionalInt calcMinUserNumber(Clazz clazz) {
return extremum(listOfObjects, clazz, MyClass::getUserNumber, IntStream::min);
}
private OptionalInt calcMaxUserNumber(Clazz clazz) {
return extremum(listOfObjects, clazz, MyClass::getUserNumber, IntStream::max);
}
private OptionalInt calcMinGroupNumber(Clazz clazz) {
return extremum(listOfObjects, clazz, MyClass::getGroupNumber, IntStream::min);
}
private OptionalInt calcMaxGroupNumber(Clazz clazz) {
return extremum(listOfObjects, clazz, MyClass::getGroupNumber, IntStream::max);
}
...
And here's a usage example:
calcMaxGroupNumber(clazz).ifPresent(list::add);
The solution may reduce redundancy but it removes readability from the code.
IntStream maxi = listOfObjects.stream().filter(y -> (y.getName()
.matches(clazz.getFilter())))
.map(y -> (y.getUserNumber()))
.mapToInt(Integer::intValue);
System.out.println(applier(() -> maxi, IntStream::max));
//System.out.println(applier(() -> maxi, IntStream::min));
...
public static OptionalInt applier(Supplier<IntStream> supplier, Function<IntStream, OptionalInt> predicate) {
return predicate.apply(supplier.get());
}
For the sake of variety, I want to add the following approach which uses a nested Collectors.teeing (Java 12 or higher) which enables to get all values by just streaming over the collection only once.
For the set up, I am using the below simple class :
#AllArgsConstructor
#ToString
#Getter
static class MyObject {
int userNumber;
int groupNumber;
}
and a list of MyObjects:
List<MyObject> myObjectList = List.of(
new MyObject(1, 2),
new MyObject(2, 3),
new MyObject(3, 4),
new MyObject(5, 3),
new MyObject(6, 2),
new MyObject(7, 6),
new MyObject(1, 12));
If the task was to get the max and min userNumber one could do a simple teeing like below and add for example the values to map:
Map<String , Integer> maxMinUserNum =
myObjectList.stream()
.collect(
Collectors.teeing(
Collectors.reducing(Integer.MAX_VALUE, MyObject::getUserNumber, Integer::min),
Collectors.reducing(Integer.MIN_VALUE, MyObject::getUserNumber, Integer::max),
(min,max) -> {
Map<String,Integer> map = new HashMap<>();
map.put("minUser",min);
map.put("maxUser",max);
return map;
}));
System.out.println(maxMinUserNum);
//output: {minUser=1, maxUser=7}
Since the task also includes to get the max and min group numbers, we could use the same approach as above and only need to nest the teeing collector :
Map<String , Integer> result =
myObjectList.stream()
.collect(
Collectors.teeing(
Collectors.teeing(
Collectors.reducing(Integer.MAX_VALUE, MyObject::getUserNumber, Integer::min),
Collectors.reducing(Integer.MIN_VALUE, MyObject::getUserNumber, Integer::max),
(min,max) -> {
Map<String,Integer> map = new LinkedHashMap<>();
map.put("minUser",min);
map.put("maxUser",max);
return map;
}),
Collectors.teeing(
Collectors.reducing(Integer.MAX_VALUE, MyObject::getGroupNumber, Integer::min),
Collectors.reducing(Integer.MIN_VALUE, MyObject::getGroupNumber, Integer::max),
(min,max) -> {
Map<String,Integer> map = new LinkedHashMap<>();
map.put("minGroup",min);
map.put("maxGroup",max);
return map;
}),
(map1,map2) -> {
map1.putAll(map2);
return map1;
}));
System.out.println(result);
output
{minUser=1, maxUser=7, minGroup=2, maxGroup=12}
Suppose I have this class model hierarchy:
public class A {
private Integer id;
private List<B> b;
}
And:
public class B {
private Integer id;
private List<C> c;
}
And finally:
public class C {
private Integer id;
}
And a simple Service:
#Service
public class doSome {
public void test() {
Optional<A> a = Optional.of(a) // Suppose a is an instance with full hierarchy contains values
/** *1 **/ // what I want to do
}
}
Now what I want to do at the *1 position is to use lambda to extract the Optional value (if exixsts) and map the subrelation to obtain all id of the C class. I have tried something like this:
public void test() {
Optional<A> a = Optional.of(a);
List<Integer> temp = a.get().getB()
.stream()
.map(x -> x.getC())
.flatMap(List::stream)
.map(y -> y.getId())
.collect(Collectors.toList()); // works
}
Now I would like to put inside my lambda the a.get().getB(), I have tried several ways but with no luck.
Anyway I don't understand why I can't use two consecutive map like
.map(x -> x.getC())
.flatMap(List::stream)
.map(y -> y.getId())
without using flatMap(List::stream) in the middle... the map doesn't return a new Stream of Type R (class C in this case)? Why I have to Stream it again? where am I wrong?
----------------------- UPDATE ------------------
This is just an example, It's pretty clear that the Optional here is useless but in real case could comes by a findById() JPA Query.
Holger for this reasons I would put all inside a part of code, doing something like:
public <T> T findSome(Integer id) {
Optional<T> opt = repository.findById(id);
return opt.map(opt -> opt).orElse(null);
}
I have read here some solution like follows:
Optional.ofNullable(MyObject.getPeople())
.map(people -> people
.stream()
.filter(person -> person.getName().equals("test1"))
.findFirst()
.map(person -> person.getId()))
.orElse(null);
And I would like to adapt at my case but without no luck.
As of java-9 and newer, you can call Optional#stream:
List<Integer> temp = a.map(A::getB)
.stream()
.flatMap(Collection::stream)
.map(B::getC)
.flatMap(Collection::stream)
.map(C::getId)
.collect(Collectors.toList());
If you are stuck with java-8, you need to map to Stream (or return the empty one) and continue chaining:
List<Integer> temp = a.map(A::getB)
.map(Collection::stream)
.orElse(Stream.empty())
.map(B::getC)
.flatMap(Collection::stream)
.map(C::getId)
.collect(Collectors.toList());
Note: Optional<A> a = Optional.of(a) is not valid as a is already defined.
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();
}
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())));
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.