Multiline Java Stream Collect throws - java

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()
));

Related

Dynamic datatype in generics java

I have a usecase where client is sending a List<Function>. Task is to iterate and execute this function and keep it in a TypedSafeMap.
pseudo client code:
Function<String, Integer> firstFn = x -> x.length();
Function<String, String> secondFn = x -> x.substring(0);
client.runTheseFunctions(Arrays.asList(firstFn, secondFn));
Inside runtTheseFunctions in the code, task is to execute these functions and keep it in a TypedSafeMap where the key is the datatype of the type of the result of the function and value is the return of functions.apply();
The code below
public static void runTheseFunctions(List<Function<Employee, ?>> lst, Employee o) {
lst.stream().forEach( x -> {
typedSafeMap.put(????, x.apply(o));
//The key is nothing but the datatype of the x.apply(o).
//How do I add this in runtime here. Generics is all compile time safety.
});
}
public static void runTheseFunctions(List<Function<Employee, ?>> lst, Employee o) {
lst.stream().collect(Collectors.toMap(f -> f.apply(o).getClass(), f -> f.apply(o)));
}
You can implement your "runTheseFunctions" method as shown below:
public static void runTheseFunctions(List<Function<Employee, ?>> lst, Employee o) {
Map<Class<?>, Object> typedSafeMap = new HashMap<>();
lst.stream().forEach(x -> {
Object value = x.apply(o);
typedSafeMap.put(value.getClass(), value);
});
System.out.println(typedSafeMap);
}
In case the List of Functions contains two or more Functions with the same outputtype (for instance: String getFirstName, String getLastName, toMap will fail. So an alternative is:
var map = list.stream().collect(groupingBy(
f -> f.apply(e).getClass(),
mapping(f -> f.apply(e), toList())
));
Here is an example of what you want to achieve, and you can use for your tests. I assumed an trivial implementation of Employee class, just to give you an idea:
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
class Employee {
String name;
public Employee(String name) {
this.name = name;
}
public int length() {
return name.length();
}
public String substring(int index) {
return name.substring(index);
}
}
public class Test {
public static void main(String[] args) {
Employee e = new Employee("Marco");
Function<Employee, Integer> firstFn = x -> x.length();
Function<Employee, String> secondFn = x -> x.substring(0);
runTheseFunctions(Arrays.asList(firstFn, secondFn), e);
}
public static void runTheseFunctions(List<Function<Employee, ?>> lst, Employee o) {
Map<Class, Object> typedSafeMap = new HashMap<>();
lst.stream().forEach(x -> {
Object result = x.apply(o);
typedSafeMap.put(x.apply(o).getClass(), x.apply(o));
// The key is nothing but the datatype of the x.apply(o).
// How do I add this in runtime here. Generics is all compile time safety.
});
typedSafeMap.entrySet().forEach(entry -> System.out.println(entry.getKey() + " - " + entry.getValue()));
}
}
And here is the output:
class java.lang.String - Marco
class java.lang.Integer - 5
Enhancing #Yonas answer:
private static Map<?, ? extends Object> runTheseFunctions(List<Function<String, ? extends Object>> list, String o) {
return list.stream()
.map(f -> f.apply(o))
.collect(Collectors.toMap(result -> result.getClass(), Function.identity()));
}
This will call the f.apply(o) only once.

How to create a List<Object> with fields String and Map<String, Set<String>> from another List<Object2>

Class Object2 has standard getters and has String fields folder, file, and version.
It is named SourceInfo
List<SourceInfo> source contains the three fields mentioned above.
My goal is to create a List<Info> from List<SourceInfo>.
The class of the new List is Info, shown below.
public class Info {
private final String folder;
private final Map<String, Set<String>> file;
public static Builder builder() {
return new Builder();
}
public static Builder builder(Info info) {
return new Builder(info);
}
private Info(Builder builder) {
this.folder = builder.folder;
this.file = builder.file;
}
public String getFolder() {
return folder;
}
public Map<String, Set<String>> getFile() {
return file;
}
// autogenerated toString, hashCode, and equals
public static class Builder {
private String folder;
private Map<String, Set<String>> file;
private Builder() {}
private Builder(Info info) {
this.folder = info.folder;
this.file = info.file;
}
public Builder with(Consumer<Builder> consumer) {
consumer.accept(this);
return this;
}
public Builder withFolder(String folder) {
this.folder = folder;
return this;
}
public Builder withFile(Map<String, Set<String>> file) {
this.file = file;
return this;
}
public Info build() {
return new Info(this);
}
}
What I've tried so far is to create a set inside the builder pattern.
List<SourceInfo> source;
// error: gc overhead limit exceeded
List<Info> infoList = source.stream()
.map(e -> Info.builder()
.withFolder(e.getFolder())
.withFile(source.stream()
.collect(Collectors.groupingBy(SourceInfo::getKey,
Collectors.mapping(SourceInfo::getVersion, Collectors.toSet()))))
.build())
.collect(Collectors.toList());
Map<String, Set<String>> map = source.stream()
.collect(Collectors
.groupingBy(SourceInfo::getKey,
Collectors.mapping(SourceInfo::getVersion, Collectors.toSet())));
List<Info> info = source.stream()
.map(e -> Info.builder()
.withFolder(e.getFolder())
.withFile(map.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue)))
.build())
.collect(Collectors.toList());
Desired output. The below syntax may be off.
// [String, Map<String, Set<String>>]
Info [folder, [key=file [value=version]]]
...
I'm new to Java, any help is appreciated.
I would like to understand how to do this using java8 and for loops.
Thank you.
First collect to a Map<String, Map<String, Set<String>> then map it to a List<Info> using Collectors#collectingAndThen with Collectors#groupingBy
List<Info> result = list.stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(
Object2::getFolder,
Collectors.groupingBy(
Object2::getFile,
Collectors.mapping(
Object2::getVersion,
Collectors.toSet()
)
)
),
map -> map.entrySet()
.stream()
.map(entry -> new Info.Builder()
.withFolder(entry.getKey())
.withFile(entry.getValue())
.build()
)
.collect(Collectors.toList())
));

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}

How to convert a list of strings into a list of objects?

I have a list of roles in a database. They are of the form
application.Role1.read
application.Role1.write
application.Role2.read
application.Role3.read
So each role has an entry based on read/write permission.
I want to convert the roles into a POJO which I can then send as JSON to a UI. Each POJO would have the role name, and a boolean for read or write permission.
Here is the RolePermission class:
import com.fasterxml.jackson.annotation.JsonInclude;
#JsonInclude(JsonInclude.Include.NON_NULL)
public class RolePermission {
private String roleName;
private boolean readAllowed;
private boolean writeAllowed;
public String getRoleName() {
return roleName;
}
public RolePermission setRoleName(String roleName) {
this.roleName = roleName;
return this;
}
public boolean isReadAllowed() {
return readAllowed;
}
public RolePermission setReadAllowed(boolean readAllowed) {
this.readAllowed = readAllowed;
return this;
}
public boolean isWriteAllowed() {
return writeAllowed;
}
public RolePermission setWriteAllowed(boolean writeAllowed) {
this.writeAllowed = writeAllowed;
return this;
}
}
I am doing the transformation like so:
public static final String ROLE_PREFIX = "application.";
public static final String ROLE_READ_PERMISSION = "read";
public static final String ROLE_WRITE_PERMISSION = "write";
#Override
public List<RolePermission> getRoles(Backend backend) {
List<String> allRoles = backend.getRoles()
.stream()
.map(s -> s.replace(ROLE_PREFIX, ""))
.sorted()
.collect(Collectors.toList());
Map<String, RolePermission> roleMap = new HashMap<>();
for (String role : allRoles) {
String[] tokens = role.split(".");
String roleName = tokens[0];
String permission = tokens[1];
if (!roleMap.containsKey(roleName))
roleMap.put(roleName, new RolePermission().setRoleName(roleName));
RolePermission permission = roleMap.get(roleName);
if (ROLE_READ_PERMISSION.equals(permission))
permission.setReadAllowed(true);
if (ROLE_WRITE_PERMISSION.equals(permission))
permission.setWriteAllowed(true);
}
return new LinkedList<>(roleMap.values());
}
Is there a way to do the foreach loop above using Java 8 streams?
This is a mock Backend instance that just returns a list of roles:
public class Backend {
public List<String> getRoles() {
return Arrays.asList(
"application.Role1.read",
"application.Role1.write",
"application.Role2.read",
"application.Role3.read"
);
}
}
You can use groupingBy to join the different permissions for the same roleName together.
public static final String ROLE_PREFIX = "application.";
public static final String ROLE_READ_PERMISSION = "read";
public static final String ROLE_WRITE_PERMISSION = "write";
#Override
public List<RolePermission> getRoles(Backend backend) {
Map<String, List<String[]>> allRoles = backend.getRoles()
.stream()
.map(s -> s.replace(ROLE_PREFIX, "")) // something like "Role1.read"
.map(s -> s.split("\\.")) // something like ["Role1", "read"]
.collect(Collectors.groupingBy(split -> split[0]));
return allRoles.values()
.stream()
.map(this::buildPermission)
.collect(Collectors.toList());
}
private RolePermission buildPermission(List<String[]> roleEntries) {
RolePermission permission = new RolePermission().setRoleName(roleEntries.get(0)[0]);
roleEntries.stream()
.forEach(entry -> {
if (ROLE_READ_PERMISSION.equals(entry[1]))
permission.setReadAllowed(true);
if (ROLE_WRITE_PERMISSION.equals(entry[1]))
permission.setWriteAllowed(true);
});
return permission;
}
I also think that your String.split was using an incorrect regex in the original post, because . is a special regex character. I've tested this and it works correctly.
Output:
[RolePermission(roleName=Role3, readAllowed=true, writeAllowed=false),
RolePermission(roleName=Role2, readAllowed=true, writeAllowed=false),
RolePermission(roleName=Role1, readAllowed=true, writeAllowed=true)]
Your for loop can be replaced with second call of map in your existing stream and toMap collector:
public List<RolePermission> getRoles(Backend backend)
{
Map<String, RolePermission> allRoles = backend.getRoles()
.stream()
.map(s -> s.replace(ROLE_PREFIX, ""))
.sorted()
.map(this::mapStringToRolePermission)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, RolePermission::merge));
return new ArrayList<>(allRoles.values());
}
where mapStringToRolePermission:
private static Map.Entry<String, RolePermission> mapStringToRolePermission(String role)
{
String roleName = role.substring(0, role.indexOf('.'));
RolePermission rolePermission = new RolePermission();
rolePermission.setRoleName(roleName);
rolePermission.setReadAllowed(role.endsWith(ROLE_READ_PERMISSION));
rolePermission.setWriteAllowed(role.endsWith(ROLE_WRITE_PERMISSION));
return new AbstractMap.SimpleEntry<>(roleName, rolePermission);
}
and an additional method merge for RolePermission:
public RolePermission merge(RolePermission another)
{
if (another.isReadAllowed())
setReadAllowed(true);
if (another.isWriteAllowed())
setWriteAllowed(true);
return this;
}
The Java stream has a map function so you could use that to map String to RolePermission (or whatever object you want) and then collect the results to a list. Would something like this work for you? It looks like you more or less already have the String to RolePermission conversion done
final List<RolePermission> roles = getRoles().stream().map(roleString -> { <your conversion code> }).collect(Collectors.toList());

Categories

Resources