I would like to convert a stream of Objects to a Map. The key is the object itself and the value is Function.identity(). My goal is to create an incremental index for every Person.
public class Person {
private String firstName;
private String lastName;
}
/* Expected Result
Key:[Person1], value:1
Key:[Person2], value:2
Key:[Person3], value:3
*/
public Map<Person, Integer> getMapOfPersons(Stream<Person> persons) {
return persons.filter(p -> "John".equalIgnoreCase(p.getFirstName)
.collect(Collectors.toMap(Person, Function.identity()));
}
My problem is that after applying the .filter(), I can't put my object as a key (or even value) in .toMap() method.
You could make it in two steps:
public Map<Person, Integer> getMapOfPersons(Stream<Person> persons) {
List<Person> filterd = persons.filter(p -> "John".equalIgnoreCase(p.getFirstName))
.collect(Collectors.toList());
return IntStream.range(0, filterd.size())
.boxed()
.collect(Collectors.toMap(filterd::get, i -> i + 1));
}
You could use AtomicInteger to generate an increasing value as value to your Map:
public Map<Person, Integer> getMapOfPersons(Stream<Person> persons) {
final AtomicInteger atomicInteger = new AtomicInteger();
return persons
.filter(p -> "John".equalsIgnoreCase(p.getFirstName()))
.collect(Collectors.toMap(Function.identity(), notUsed -> atomicInteger.incrementAndGet()));
}
Beware of using this with parallel streams since it relies on the order of the processing. It will, however, render unique values since AtomicInteger is thread safe.
You can try something like this:
public Map<Person, Integer> getMapOfPersons(Stream<Person> persons) {
AtomicInteger counter = new AtomicInteger(0);
return persons.filter(p -> "John".equalIgnoreCase(p.getFirstName())
.collectors.toMap(p->p, counter.getAndIncrement());
}
Related
I have a fun puzzler. Say I have a list of String values:
["A", "B", "C"]
Then I have to query another system for a Map<User, Long> of users with an attribute that corresponds to those values in the list with a count:
{name="Annie", key="A"} -> 23
{name="Paul", key="C"} -> 16
I need to return a new List<UserCount> with a count of each key. So I expect:
{key="A", count=23},
{key="B", count=0},
{key="C", count=16}
But I'm having a hard time computing when one of my User objects has no corresponding count in the map.
I know that map.computeIfAbsent() does what I need, but how can I apply it based on what's on the contents of the original list?
I think I need to stream the over the original list, then apply compute? So I have:
valuesList.stream()
.map(it -> valuesMap.computeIfAbsent(it.getKey(), k-> OL))
...
But here's where I get stuck. Can anyone provide any insight as to how I accomplish what I need?
You can create an auxiliary Map<String, Long> which will associate each string key with the count and then generate a list of UserCount based on it.
Example:
public record User(String name, String key) {}
public record UserCount(String key, long count) {}
public static void main(String[] args) {
List<String> keys = List.of("A", "B", "C");
Map<User, Long> countByUser =
Map.of(new User("Annie", "A"), 23L,
new User("Paul", "C"), 16L));
Map<String, Long> countByKey = countByUser.entrySet().stream()
.collect(Collectors.groupingBy(entry -> entry.getKey().key(),
Collectors.summingLong(Map.Entry::getValue)));
List<UserCount> userCounts = keys.stream()
.map(key -> new UserCount(key, countByKey.getOrDefault(key, 0L)))
.collect(Collectors.toList());
System.out.println(userCounts);
}
Output
[UserCount[key=A, count=23], UserCount[key=B, count=0], UserCount[key=C, count=16]]
Regarding the idea of utilizing computeIfAbsent() with stream - this approach is wrong and discouraged by the documentation of the Stream API.
Sure, you can use computeIfAbsent() to solve this problem, but not in conjunction with streams. It's not a good idea to create a stream that operates via side effects (at least without compelling reason).
And I guess you even don't need Java 8 computeIfAbsent(), plain and simple putIfAbsent() will be sufficient.
The following code will produce the same result:
Map<String, Long> countByKey = new HashMap<>();
countByUser.forEach((k, v) -> countByKey.merge(k.key(), v, Long::sum));
keys.forEach(k -> countByKey.putIfAbsent(k, 0L));
List<UserCount> userCounts = keys.stream()
.map(key -> new UserCount(key, countByKey.getOrDefault(key, 0L)))
.collect(Collectors.toList());
And instead of applying forEach() on a map and list, you can create two enhanced for loops if this options looks convoluted.
Another educational and parallel friendly version would be to gather the logic in one place and build your own custom accumulator and combiner for the Collector
public static void main(String[] args) {
Map<User, Long> countByUser =
Map.of(new User("Alice", "A"), 23L,
new User("Bob", "C"), 16L);
List<String> keys = List.of("A", "B", "C");
UserCountAggregator userCountAggregator =
countByUser.entrySet()
.parallelStream()
.collect(UserCountAggregator::new,
UserCountAggregator::accumulator,
UserCountAggregator::combiner);
List<UserCount> userCounts = userCountAggregator.getUserCounts(keys);
System.out.println(userCounts);
}
Output
[UserCount(key=A, count=23), UserCount(key=B, count=0), UserCount(key=C, count=16)]
User and UserCount classes with Lombok's #Value
#Value
class User {
private String name;
private String key;
}
#Value
class UserCount {
private String key;
private long count;
}
And the UserCountAggregator which contains your custom accumulator and combiner
class UserCountAggregator {
private Map<String, Long> keyCounts = new HashMap<>();
public void accumulator(Map.Entry<User, Long> userLongEntry) {
keyCounts.put(userLongEntry.getKey().getKey(),
keyCounts.getOrDefault(userLongEntry.getKey().getKey(), 0L)
+ userLongEntry.getValue());
}
public void combiner(UserCountAggregator other) {
other.keyCounts
.forEach((key, value) -> keyCounts.merge(key, value, Long::sum));
}
public List<UserCount> getUserCounts(List<String> keys) {
return keys.stream()
.map(key -> new UserCount(key, keyCounts.getOrDefault(key, 0L)))
.collect(Collectors.toList());
}
}
final Map<User,Long> valuesMap = ...
// First, map keys to counts (assuming keys are unique for each user)
final Map<String,Long> keyToCountMap = valuesMap.entrySet().stream()
.collect(Collectors.toMap(e -> e.getKey().key, e -> e.getValue()));
final List<UserCount> list = valuesList.stream()
.map(key -> new UserCount (key, keyToCountMap.getOrDefault(key, 0L)))
.collect(Collectors.toList());
I have a String - Array map that looks like this
dishIdQuantityMap[43]=[Ljava.lang.String;#301d55ce
dishIdQuantityMap[42]=[Ljava.lang.String;#72cb31c2
dishIdQuantityMap[41]=[Ljava.lang.String;#1670799
dishIdQuantityMap[40]=[Ljava.lang.String;#a5b3d21
What I need to do is
Create a new map, where key - only numbers extracted from String like this ( key -> key.replaceAll("\\D+","");
Value - first value from array like this value -> value[0];
Filter an array so that only this paris left where value > 0
I've spent an hour trying to solve it myself, but fail with .collect(Collectors.toMap()); method.
UPD:
The code I've done so far. I fail to filter the map.
HashMap<Long, Integer> myHashMap = request.getParameterMap().entrySet().stream()
.filter(e -> Integer.parseInt(e.getValue()[0]) > 0)
.collect(Collectors.toMap(MapEntry::getKey, MapEntry::getValue));
You can do it by using stream and an auxiliary KeyValuePair class.
The KeyValuePair would be as simple as:
public class KeyValuePair {
public KeyValuePair(String key, int value) {
this.key = key;
this.value = value;
}
private String key;
private int value;
//getters and setters
}
Having this class you can use streams as bellow:
Map<String, Integer> resultMap = map.entrySet().stream()
.map(entry -> new KeyValuePair(entry.getKey().replaceAll("Key", "k"), entry.getValue()[0]))
.filter(kvp -> kvp.value > 0)
.collect(Collectors.toMap(KeyValuePair::getKey, KeyValuePair::getValue));
In the example I'm not replacing and filtering exactly by the conditions you need but, as you said you are having problems with the collector, you probably just have to adapt the code you currently have.
I addressed this problem in the following order:
filter out entries with value[0] > 0. This step is the last on your list, but with regards to performance, it's better to put this operation at the beginning of the pipeline. It might decrease the number of objects that have to be created during the execution of the map() operation;
update the entries. I.e. replace every entry with a new one. Note, this step doesn't require creating a custom class to represent a key-value pair, AbstractMap.SimpleEntry has been with us for a while. And since Java 9 instead of instantiating AbstractMap.SimpleEntry we can make use of the static method entry() of the Map interface;
collect entries into the map.
public static Map<Long, Integer> processMap(Map<String, String[]> source) {
return source.entrySet().stream()
.filter(entry -> Integer.parseInt(entry.getValue()[0]) > 0)
.map(entry -> updateEntry(entry))
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue));
}
private static Map.Entry<Long, Integer> updateEntry(Map.Entry<String, String[]> entry) {
return Map.entry(parseKey(entry.getKey()), parseValue(entry.getValue()));
}
The logic for parsing keys and values was extracted into separate methods to make the code cleaner.
private static Long parseKey(String key) {
return Long.parseLong(key.replaceAll("\\D+",""));
}
private static Integer parseValue(String[] value) {
return Integer.parseInt(value[0]);
}
public static void main(String[] args) {
System.out.println(processMap(Map.of("48i;", new String[]{"1", "2", "3"},
"129!;", new String[]{"9", "5", "9"})));
}
Output
{48=1, 129=9}
I want to do a Map<Person, Double>, where Double is average of Integer values that is stored in another Map <String, Integer> which is one of the fields of stream's elements.
public Map<Person,Double> totalScores(Stream<CourseResult> programmingResults) {
return
programmingResults.collect(Collectors.groupingBy(
CourseResult::getPerson,
// And there is a problem, I want to get values from `Map <String, Integer>`
// and do the `averagingInt`, but only get
//`Bad return type in lambda expression:
// Collection<Integer> cannot be converted to int`
Collectors.averagingInt(
s -> s.getTaskResults().values()
)
));
}
How can I get these values in the right way?
There's some of the classes I'm using:
public class CourseResult {
private final Person person;
private final Map<String, Integer> taskResults;
public CourseResult(final Person person, final Map<String, Integer> taskResults) {
this.person = person;
this.taskResults = taskResults;
}
public Person getPerson() {
return person;
}
public Map<String, Integer> getTaskResults() {
return taskResults;
}
}
Note that values() is a Collection<Integer>. You can't average by that. You can use a flat-mapping Collector, to flatten each group of persons to just integers, rather than CourseResult. After that, you can do an average by the identity function.
return programmingResults.collect(
Collectors.groupingBy(
CourseResult::getPerson,
Collectors.flatMapping(s->s.getTaskResults().values().stream(),
Collectors.averagingInt(x -> x)
)
)
);
Edit: If there is only one CourseResult per Person, then you don't need groupingBy. Just use toMap and calculate the average using another stream.
return programmingResults.collect(
Collectors.toMap(
CourseResult::getPerson,
result -> result.getTaskResults().values()
.stream().mapToInt(x -> x).average().orElse(0))
);
If it is guaranteed that the input stream contains CourseResult instances which have unique persons (and grouping + flatMapping of the task results may not be needed), it may be sufficient to use toMap collector:
public Map<Person,Double> totalScores(Stream<CourseResult> results) {
return
results.collect(Collectors.toMap(
CourseResult::getPerson,
cr -> cr.getTaskResults().values() // Collection<Integer>
.stream() // Stream<Integer>
.collect(Collectors.averagingInt(Integer::intValue))
)
));
}
Now I have an object:
public class Room{
private long roomId;
private long roomGroupId;
private String roomName;
... getter
... setter
}
I want sort list of rooms by 'roomId', but in the meantime while room objects has 'roomGroupId' greator than zero and has same value then make them close to each other.
Let me give you some example:
input:
[{"roomId":3,"roomGroupId":0},
{"roomId":6,"roomGroupId":0},
{"roomId":1,"roomGroupId":1},
{"roomId":2,"roomGroupId":0},
{"roomId":4,"roomGroupId":1}]
output:
[{"roomId":6,"roomGroupId":0},
{"roomId":4,"roomGroupId":1},
{"roomId":1,"roomGroupId":1},
{"roomId":3,"roomGroupId":0},
{"roomId":2,"roomGroupId":0}]
As shown above, the list sort by 'roomId', but 'roomId 4' and 'roomId 1' are close together, because they has the same roomGroupId.
This does not have easy nice solution (maybe I am wrong).
You can do this like this
TreeMap<Long, List<Room>> roomMap = new TreeMap<>();
rooms.stream()
.collect(Collectors.groupingBy(Room::getRoomGroupId))
.forEach((key, value) -> {
if (key.equals(0L)) {
value.forEach(room -> roomMap.put(room.getRoomId(), Arrays.asList(room)));
} else {
roomMap.put(
Collections.max(value, Comparator.comparing(Room::getRoomId))
.getRoomId(),
value
.stream()
.sorted(Comparator.comparing(Room::getRoomId)
.reversed())
.collect(Collectors.toList())
);
}
});
List<Room> result = roomMap.descendingMap()
.entrySet()
.stream()
.flatMap(entry -> entry.getValue()
.stream())
.collect(Collectors.toList());
If you're in Java 8, you can use code like this
Collections.sort(roomList, Comparator.comparing(Room::getRoomGroupId)
.thenComparing(Room::getRoomId));
If not, you should use a comparator
class SortRoom implements Comparator<Room>
{
public int compare(Room a, Room b)
{
if (a.getRoomGroupId().compareTo(b.getRoomGroupId()) == 0) {
return a.getRoomId().compareTo(b.getRoomId());
}
return a.getRoomGroupId().compareTo(b.getRoomGroupId();
}
}
and then use it like this
Collections.sort(roomList, new SortRoom());
We have the following:
public List<Balance> mapToBalancesWithSumAmounts(List<MonthlyBalancedBooking> entries) {
return entries
.stream()
.collect(
groupingBy(
MonthlyBalancedBooking::getValidFor,
summingDouble(MonthlyBalancedBooking::getAmount)
)
)
.entrySet()
.stream()
.map(localDateDoubleEntry -> new Balance(localDateDoubleEntry.getValue(), localDateDoubleEntry.getKey()))
.collect(toList());
}
Is there a possibility to avoid the second stream() path in the code, so the result of the groupingBy() should be a list in our case. We need a possibility to pass the map()-function to collect or groupingBy is that possible in Java 8?
That wouldn't be possible since the value that you are looking for as you map to the Balance objects could only be evaluated once all the entries of the MonthlyBalancedBooking list are iterated.
new Balance(localDateDoubleEntry.getValue(), localDateDoubleEntry.getKey())
An alternate way though with moving the stream though within a single terminal operation could be by using collectingAndThen as:
public List<Balance> mapToBalancesWithSumAmounts(List<MonthlyBalancedBooking> entries) {
return entries.stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(MonthlyBalancedBooking::getValidFor,
Collectors.summingDouble(MonthlyBalancedBooking::getAmount)),
map -> map.entrySet().stream()
.map(entry -> new Balance(entry.getValue(), entry.getKey()))))
.collect(Collectors.toList());
}
The simple way is just using toMap() collector with merge function like this:
List<Balance> balances = new ArrayList<>(entries.stream()
.collect(toMap(MonthlyBalancedBooking::getValidFor, m -> new Balance(m.getAmount(),
m.getValidFor()),Balance::merge)).values());
I supposed for Balance class these properties:
class Balance {
private Double value;
private Integer key;
public Balance merge(Balance b) {
this.value += b.getValue();
return this;
}
}