Java 8 collect to Map<String, List<Object>> - java

I have two object. The first one:
public final class Object1 {
private String a;
private String b;
// constructor getter and setter
}
The second one:
public class Object2 {
private BigDecimal value1;
private BigDecimal value2;
// constructor getter and setter
}
I have a Map<Object1, Object2>:
Object1{a="15", b="XXX"}, Object2{value1=12.1, value2=32.3}
Object1{a="15", b="YYY"}, Object2{value1=21.1, value2=24.3}
Object1{a="16", b="AAA"}, Object2{value1=34.1, value2=45.3}
Object1{a="15", b="BBB"}, Object2{value1=23.1, value2=65.3}
Object1{a="15", b="DDD"}, Object2{value1=23.1, value2=67.3}
Object1{a="17", b="CCC"}, Object2{value1=78.1, value2=2.3}
........
I want to group this map with the same a in a list of Object2 like:
a="15", {{value1=12.1, value2=32.3}, {value1=21.1, value2=24.3}, {value1=23.1, value2=65.3}, {value1=23.1, value2=67.3}},
a="16", {{value1=34.1, value2=45.3}}
...
I try something like this:
Map<String, List<Object2>> map1 = map.entrySet()
.stream()
.collect(Collectors.toMap(e -> e.getKey().getA(), list of object with this key);

yourMap.entrySet()
.stream()
.collect(Collectors.groupingBy(e -> e.getKey().getA(),
Collectors.mapping(Entry::getValue, Collectors.toList())))

Related

returning Hashmap having different mappings in stream

I am learning some cool stuff about Java StreamAPI and got stuck'd into one problem:
I have a use case where I want to return newly create hashmap using stream. I am using the traditional way of defining a HashMap in the function and adding up values to it.
I was more interested in knowing some better ways to achieve so
public Map<String,String> constructMap(List<CustomObject> lists){
Map<String,String> newMap = new HashMap<>();
lists.stream().filter(x->x!=null).forEach(map -> newMap.putAll(map.getSomeMapping(studentId));
return newMap;
}
Can I achieve this using reduceAPI or any other way without having to create a custom hashmap (directly return the stream one liner)?
Edit:
for Example:
CustomObject c1 = new CustomObject("bookId1", "book1");
CustomObject c2 = new CustomObject("bookId2", "book2");
List<CustomObject> lists = new ArrayList();
lists.add(c1); lists.add(c2);
The getter in class CustomObject is: getSomeMapping(input)
which return Map<BookID, Book>
Expected output:
{"bookId1" : "book1", "bookId2" : "book2"}
Edit2:
One more thing to clarify, the CustomObject class does not have any other getters defined. The only function I have access to is getSomeMapping(input) which returns a mapping
thank you for any help.
Assuming CustomObject has the following structure and getter getSomeMapping which returns a map:
class CustomObject {
private Map<String, String> someMapping;
public CustomObject(String key, String value) {
this.someMapping = new HashMap<>();
someMapping.put(key, value);
}
public Map<String, String> getSomeMapping() {
return someMapping;
}
}
Then constructMap will use already mentioned Collectors.toMap after flattening the entries in someMapping:
public static Map<String, String> constructMap(List<CustomObject> list) {
return list.stream()
.filter(Objects::nonNull)
.map(CustomObject::getSomeMapping)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> v1, // merge function to handle possible duplicates
LinkedHashMap::new
));
}
Test
CustomObject c1 = new CustomObject("bookId1", "book1");
CustomObject c2 = new CustomObject("bookId2", "book2");
List<CustomObject> lists = Arrays.asList(c1, c2);
Map<String, String> result = constructMap(lists);
System.out.println(result);
Output:
{bookId1=book1, bookId2=book2}
You can use Collectors#toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier) to create a LinkedHashMap using the bookId as the key, and bookName as the value.
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
class CustomObject {
private String bookId;
private String bookName;
public CustomObject(String bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
public String getBookId() {
return bookId;
}
public String getBookName() {
return bookName;
}
// Other stuff e.g. equals, hashCode etc.
}
public class Main {
public static void main(String[] args) {
List<CustomObject> list = List.of(new CustomObject("bookId1", "book1"), new CustomObject("bookId2", "book2"));
System.out.println(constructMap(list));
}
public static Map<String, String> constructMap(List<CustomObject> list) {
return list.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(CustomObject::getBookId, CustomObject::getBookName, (a, b) -> a, LinkedHashMap::new));
}
}
Output:
{bookId1=book1, bookId2=book2}
Note: The mergeFunction, (a, b) -> a resolves the collision between values associated with the same key e.g. in this case, we have defined it to select a out of a and b having the same key. If the order of elements does not matter, you can use Collectors#toMap(Function<? super T,? extends K> keyMapper, Function<? super T,? extends U> valueMapper) as shown below:
public static Map<String, String> constructMap(List<CustomObject> list) {
return list.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(CustomObject::getBookId, CustomObject::getBookName));
}
A sample output:
{bookId2=book2, bookId1=book1}
To turn a stream into a map you're better off using collect(). For instance:
public Map<String,String> toMap(List<Entry<String,String>> entries) {
return entries.stream().collect(Collectors.toMap(Entry::getKey, Entry::getValue));
}
Or if your keys are non-unique and you want the values to be combined as a list:
public Map<String,List<CustomObject>> toMap(List<CustomObject> entries) {
return entries.stream().collect(Collectors.groupingBy(CustomObject::getKey));
}
Look into [Collectors.toMap()] 1. This can return the items as a new Map.
lists.stream().filter(x->x!=null).collect(Collectors.toMap(CustomObject::getMapKey(), CustomObject::getMapValue()));
getMapKey and getMapValue are here methods returning the key and value of the CustomObject for the map. Instead of using simple getters it might also be necessary to execute some more advanced logic.
lists.stream().filter(x->x!=null).collect(Collectors.toMap(l -> {...; return key;}, l -> { ...; return value;}));
Let's assume your CustomObject class has getters to retrieve a school id with a name. You could do it like this. I declared it static as it does not appear to depend on instance fields.
public static Map<String,String> constructMap(List<CustomObject> lists){
return lists.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(CustomObject::getName, CustomObject::getID));
}
This presumes that names and Id's are one-to-one, as this does not handle duplicate keys.

How to add a single Key value to existing Java 8 stream?

Here is what I do to populate my static map
public static final Map<String, FooBar> mapEnum =
Arrays.stream(FooBarEnum.values())
.collect(Collectors.toMap(e-> StringUtils.upperCase(e.name), e -> e));
I want to add another single key-value to this map.
mapEnum.put("xx", FooBar.A);
Here is the enum
public enum FooBar {
A("a"), B("b"), C("c");
}
My static map will look like this after map is constructed
{"a":FooBar.A, "b": FooBar.B, "c": FooBar.C, "xx": Foobar.A}
Is it possible to include the explicit put call into Collectors.toMap()?
If you're open to using a third party library you can create a static ImmutableMap inline with a Stream using Eclipse Collections.
public static final ImmutableMap<String, FooBar> MAP_ENUM =
Arrays.stream(FooBar.values())
.collect(Collectors2.toMap(FooBar::getName, fooBar -> fooBar))
.withKeyValue("xx", FooBar.A)
.toImmutable();
public enum FooBar {
A("a"), B("b"), C("c");
private String name;
FooBar(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
You can also simplify the code slightly by using native Eclipse Collections APIs.
public static final ImmutableMap<String, FooBar> MAP_ENUM =
ArrayAdapter.adapt(FooBar.values())
.groupByUniqueKey(FooBar::getName)
.withKeyValue("xx", FooBar.A)
.toImmutable();
Note: I am a committer for Eclipse Collections
I actually don't see the need to use Java Streams for that. You simply can use the static block to initialize mapEnum and put additional values in it:
public static final Map<String, FooBar> mapEnum;
static {
mapEnum = Arrays.stream(FooBar.values())
.collect(Collectors.toMap(FooBar::getName, Function.identity()));
mapEnum.put("xx", FooBar.A);
// ...
}
Collectors.toMap(): There are no guarantees on the type, mutability, serializability, or thread-safety of the {#code Map} returned.
To ensure the mutability of the Map returned by Collectors.toMap(), so you can use Map.put() afterwards better use this:
Arrays.stream(FooBar.values())
.collect(Collectors.toMap(Function.identity(), Function.identity(), (a, b) -> a, HashMap::new));
If you really want to use java streams you can use this:
public static final Map<String, FooBar> mapEnum = Stream.concat(
Stream.of(FooBar.values()).map(e -> Map.entry(e.getName(), e)),
Stream.of(Map.entry("xx", FooBar.A))
).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Or if you also want to add all names to the enum value itself you can change your class like this:
public static enum FooBar {
A("a", "xx"), B("b"), C("c");
private String[] names;
FooBar(String... names) {
this.names = names;
}
public String[] getNames() {
return names;
}
}
And use this to create the map:
public static final Map<String, FooBar> mapEnum = Stream.of(FooBar.values())
.flatMap(e -> Arrays.stream(e.getNames()).map(n -> Map.entry(n, e)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
Prior to Java 9 use new AbstractMap.SimpleEntry<>() instead of Map.entry(). If you need the map to be sorted use LinkedHashMap::new with Collectors.toMap().
you can use Collectors::collectAndThen to modify the resulted map
Arrays.stream(FooBarEnum.values())
.collect(Collectors.collectAndThen(
Collectors.toMap(e-> StringUtils.upperCase(e.name),
Function.identity()), FooBarEnum::addCustom));
the following method is in enum
static Map<String, FooBar> addCustom(Map<String, FooBarEnum> map) {
map.put("xx", FooBar.A);
return map;
}
You cannot directly pass it to Collectors.toMap(). You can see all the overrides available in the javadocs: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#toMap-java.util.function.Function-java.util.function.Function- .
However, you can make sure your stream has all the pairs needed to construct the map, before you call toMap by using Stream.concat. You concat the pairs from the enum, and the manual pairs you want to add.
My standalone code has to define the Pair class, but since you used StringUtils, I imagine you have a library that already includes Pair so you don't need to define it.
Code:
import java.util.*;
import java.util.stream.*;
public class Main {
private static enum FooBar {
A("a"), B("b"), C("c");
private String name;
FooBar(String name) {
this.name = name;
}
}
public static class Pair {
String a;
FooBar b;
Pair(String a, FooBar b) {
this.a = a;
this.b = b;
}
}
public static void main(String [] args) {
System.out.println(
Stream.concat(
Arrays.stream(FooBar.values()).map(e -> new Pair(e.name.toUpperCase(), e)),
Stream.of(new Pair("xx", FooBar.A))
)
.collect(Collectors.toMap(pair -> pair.a, pair -> pair.b))
);
}
}
Output:
{xx=A, A=A, B=B, C=C}

java8 simplify transformations to map

I have this class structure:
public class A {
private List<B> bs;
...//getters
}
public class C {
private Long id;
...//getters
}
public class B {
private Long idOfC;
...//more stuff
}
B::getIdOfC matches C::getId
In a better design B would just contain a reference to C, rather than its id (I cannot change that), so that's why now I need to create a map, so my method signature looks like this
public Map<A, List<C>> convert(Collection<A> collection)
Inside this convert method, there is a
List<C> getCsByIds(List<Long> id)
that's later used to match it against B.idOfC, but there should only be one call to this method as it's pretty expensive.
So If I go like this:
List<B> bs = Arrays.asList(new B(10L), new B(11L)); //10L and 11L are the values of idOfC
List<A> as = Arrays.asList(bs);
//And assuming getCsByIds returns Arrays.asList(new C(10L), new C(11L), new C(12L));
then
Map<A, List<C>> map = convert(as);
map.values().get(0)
returns something like Arrays.asList(new C(10L), new C(11L))
The method that does this is pretty massive in my view:
public Map<A, List<C>> convert(Collection<A> as) {
List<Long> cIds = as.stream()
.flatMap(a -> a.getBs().stream())
.map(B::getCId)
.collect(Collectors.toList());
//single call to gsCsByIds
Map<Long, C> csMap = getCsByIds(cIds)
.stream()
.collect(Collectors.toMap(C::getId, Function.identity()));
//a whole new map is created by iterating over the list called "as"
Map<A, List<C>> csByAs = new HashMap<>();
if (!csMap.isEmpty()) {
for (A a : as) {
Set<C> cs = getCsFromMap(csMap, a.getBs());
if (!cs.isEmpty()) {
csByAs.put(a, new ArrayList<>(cs));
}
}
}
return csByAs;
}
private Set<B> getCsFromMap(Map<Long, C> cMap, List<B> bs) {
return bs.stream()
.map(b -> cMap.get(b.getIdOfc()))
.collect(Collectors.toSet());
}
Is there a way to make this simpler???
If the call to getCsByIds is expensive, your initial idea is pretty decent to execute itself. It can further be cut short to :
public Map<A, List<C>> convert(Collection<A> as) {
List<Long> cIds = as.stream()
.flatMap(a -> a.getBs().stream())
.map(B::getIdOfC)
.collect(Collectors.toList());
Map<Long, C> csMap = getCsByIds(cIds).stream()
.collect(Collectors.toMap(C::getId, Function.identity()));
return as.stream()
.collect(Collectors.toMap(Function.identity(),
a -> a.getBs().stream().map(b -> csMap.get(b.getIdOfC()))
.collect(Collectors.toList()), (a, b) -> b));
}
where you can choose your merge function (a,b) -> b accordingly.
Maybe just iterate over the As directly? (no compiler at hand, so maybe the snippet is not compile-ready)
public Map<A, List<C>> convert(Collection<A> as) {
Map<A, List<C>> result = new HashMap<>();
for(A a: as){
List<Long> cIds = a.getBs().stream()
.map(B::getIdOfC)
.collect(Collectors.toList());
result.put(a, getCsByIds(cIds));
}
return result;
}
Wouldn't something like this work? I don't have a compiler so I can't really test it
public Map<A, List<C>> convert(Collection<A> as) {
return as.stream()
.collect(Collectors.toMap(Function::identity,
a -> a.getBs().stream()
.map(B::getIdOfC)
.flatMap(id -> getCsByIds(asList(id))
.values()
.stream())
.collect(Collectors.toList())
)
);
}

Multiline Java Stream Collect throws

I have two types like these:
#Data
public class SomePersonType {
private String name;
private int age;
}
and
#Data
#Builder
public class SomeOtherPersonType {
private String name;
private int age;
}
I need to convert a map of one type to the other. So, my code looks like this:
public class Main {
public static void main(final String... args) {
final Map<String, SomePersonType> somePersonTypeMap = new HashMap<>();
// ...
// populating the map above with some values
// ...
final Map<String, SomeOtherPersonType> someOtherPersonTypeMap = somePersonTypeMap.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> SomeOtherPersonType.builder()
.name(entry.getValue().getName())
.age(entry.getValue().getAge())
.build()
));
}
}
My real code contains many other attributes. So, I don't want to do entry.getValue() multiple times. I want to put it in a local variable and then use it. Something like this:
public class Main {
public static void main(final String... args) {
final Map<String, SomePersonType> somePersonTypeMap = new HashMap<>();
// ...
// populating the map above with some values
// ...
final Map<String, SomeOtherPersonType> someOtherPersonTypeMap = somePersonTypeMap.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> {
final SomePersonType somePersonType = entry.getValue();
SomeOtherPersonType.builder()
.name(somePersonType.getName())
.age(somePersonType.getAge())
.build();
}
));
}
}
But I get two compiler errors:
Map.Entry::getKey -> Non-static method cannot be referenced from a static context.
entry.getValue() -> Cannot resolve method 'getValue()'
Can someone please point me what I am doing wrong? Thanks.
As far as the lambda expression is concerned, the final statement should return a value so that the value mapping Function returns the mapped value:
Map<String, SomeOtherPersonType> someOtherPersonTypeMap = somePersonTypeMap.entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> {
final SomePersonType somePersonType = entry.getValue();
return SomeOtherPersonType.builder()
.name(somePersonType.getName())
.age(somePersonType.getAge())
.build();
}
));
However, since you're using a builder, then why not have the builder take care of creating the SomeOtherPersonType from a SomePersonType:
public SomeOtherPersonType fromSomePersonType(SomePersonType) {
...
}
This way you can do:
Map<String, SomeOtherPersonType> someOtherPersonTypeMap = somePersonTypeMap.entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> SomeOtherPersonType.builder()
.fromSomePersonType(entry.getValue())
.build()
));

Java 8 Stream API toMap converting to TreeMap

public class Message {
private int id;
private User sender;
private User receiver;
private String text;
private Date senddate;
..
}
I have
List<Message> list= new ArrayList<>();
I need to transform them to
TreeMap<User,List<Message>> map
I know how to do transform to HashMap using
list.stream().collect(Collectors.groupingBy(Message::getSender));
But I need TreeMap with:
Key - User with newest message senddate first
Value - List sorted by senddate newest first
Part of User class
public class User{
...
private List<Message> sendMessages;
...
public List<Message> getSendMessages() {
return sendMessages;
}
}
User comparator:
public class Usercomparator implements Comparator<User> {
#Override
public int compare(User o1, User o2) {
return o2.getSendMessages().stream()
.map(message -> message.getSenddate())
.max(Date::compareTo).get()
.compareTo(o1.getSendMessages().stream()
.map(message1 -> message1.getSenddate())
.max(Date::compareTo).get());
}
}
You can use overloaded groupingBy method and pass TreeMap as Supplier:
TreeMap<User, List<Message>> map = list
.stream()
.collect(Collectors.groupingBy(Message::getSender,
() -> new TreeMap<>(new Usercomparator()), toList()));
If your list is sorted then just use this code for sorted map.
Map<String, List<WdHour>> pMonthlyDataMap = list
.stream().collect(Collectors.groupingBy(WdHour::getName, TreeMap::new, Collectors.toList()));

Categories

Resources