How to map a Map into another Map using Java Stream? - java

I'm learning Java with Stream and I have this Map<String, List<Customer>> map1 and I want to map it into this Map<String, List<CustomerObj>> map2. How can I do this especially using Java Stream? Any feedback will be apreciated!

You can stream the entries of map1 and collect with Collectors.toMap, using key mapper Map.Entry::getKey. For the value mapper you can get the entry value list, then stream and map from Customer to CustomerObj, and finally collect to List.
Here's and example using Integer instead of Customer and String instead of CustomerObj:
Map<String, List<Integer>> map1 = Map.of(
"A", List.of(1, 2),
"B", List.of(3, 4),
"C", List.of(5, 6));
Map<String, List<String>> map2 = map1.entrySet().stream().collect(
Collectors.toMap(Map.Entry::getKey,
entry -> entry.getValue().stream()
.map(String::valueOf).toList()));

You may do it like so:
Customer c1 = new Customer("İsmail", "Y.");
Customer c2 = new Customer("Elvis", "?.");
Map<String, List<Customer>> customerList = new HashMap<>();
customerList.put("U1", Arrays.asList(c1));
customerList.put("U2", Arrays.asList(c1, c2));
Map<String, List<CustomerObj>> customerObjList = customerList
.entrySet().stream().collect(
Collectors.toMap(Map.Entry::getKey,
entry -> entry.getValue().stream()
.map(CustomerObj::new)
// or .map(c -> new CustomerObj(c))
.collect(Collectors.toList())
)
);
}
private static class Customer {
private String firstName;
private String lastName;
public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}
private static class CustomerObj {
private String fullName;
public CustomerObj(Customer customer) {
this.fullName = String.join(" ",
customer.getFirstName(), customer.getLastName()
);
}
public String getFullName() {
return fullName;
}
}
#Oboe's answer works in Java 16+ and this answer is similar to your models and easier to understand.

Related

How can I populate a List from a HashMap in Java?

As part of my assignment, we are given an interface which we can't change, and some pre-defined tests to use to develop our methods.
The interface is as follows:
public interface ContactTracer {
public void addCourse(String courseID, String courseName);
public void addStudent(String id, String name, String email);
public void loadStudentCourseList(Map<String, String> enrolments);
public String getStudentName(String studentID);
public String getStudentEmail(String studentID);
public String getCourseForStudent(String studentID);
public String getCourseName(String courseID);
public List<String> findStudentsForCourse(String courseID);
public List<String> findContacts(String studentID);
}
The particular test I'm running:
#Test
void testLoadStudentCourseList() {
ContactTracer ct = new PartOneContactTracer();
Map<String, String> enrolments = new HashMap<>();
enrolments.put("S101", "SOFT01");
enrolments.put("S102", "NET02");
enrolments.put("S103", "SOFT01");
ct.loadStudentCourseList(enrolments);
List<String> students = ct.findStudentsForCourse("SOFT01");
assertEquals(2, students.size());
assertTrue(students.contains("S101"));
assertTrue(students.contains("S103"));
}
And the method I need to create from the test:
#Override
public List<String> findStudentsForCourse(String courseID) {
return null;
}
I can tell from the test that within the findStudentsForCourse method I need to create List<String> students and fill it with matching students from the enrolments Map, and then return the the List.
I have created the following fields:-
public class PartOneContactTracer implements ContactTracer {
private Map<String, String> courseDetails = new HashMap<>();
private Map<String, String> studentName = new HashMap<>();
private Map<String, String> studentEmail = new HashMap<>();
private Map<String, String> enrolments = new HashMap<>();
...
}
What I can't get my head around is how to pass the data in the enrolments Map into the students List in order to make the Assertion in the test true.
You don't need to create a list and fill it just search through the map all the keys (students) that has the value (course id):
public List<String> findStudentsForCourse(String courseId) {
return enrolments.entrySet().stream()
.filter(e -> e.getValue().equals(courseId))
.map(e -> e.getKey())
.collect(Collectors.toList());
}

Java Stream Grouping by multiple fields individually in declarative way in single loop

I googled for it but I mostly found cases for grouping by aggregated fields or on to alter response of stream but not the scenario below:
I have a class User with fields category and marketingChannel.
I have to write a method in the declarative style that accepts a list of users and counts users based on
category and also based on marketingChannel individually (i.e not groupingBy(... ,groupingBy(..)) ).
I am unable to do it in a single loop. This is what I have to achieve.
I coded few methods as follows:
import java.util.*;
import java.util.stream.*;
public class Main
{
public static void main(String[] args) {
List<User> users = User.createDemoList();
imperative(users);
declerativeMultipleLoop(users);
declerativeMultipleColumn(users);
}
public static void imperative(List<User> users){
Map<String, Integer> categoryMap = new HashMap<>();
Map<String, Integer> channelMap = new HashMap<>();
for(User user : users){
Integer value = categoryMap.getOrDefault(user.getCategory(), 0);
categoryMap.put(user.getCategory(), value+1);
value = channelMap.getOrDefault(user.getMarketingChannel(), 0);
channelMap.put(user.getMarketingChannel(), value+1);
}
System.out.println("imperative");
System.out.println(categoryMap);
System.out.println(channelMap);
}
public static void declerativeMultipleLoop(List<User> users){
Map<String, Long> categoryMap = users.stream()
.collect(Collectors.groupingBy(
User::getCategory, Collectors.counting()));
Map<String, Long> channelMap = users.stream()
.collect(Collectors.groupingBy(
User::getMarketingChannel, Collectors.counting()));
System.out.println("declerativeMultipleLoop");
System.out.println(categoryMap);
System.out.println(channelMap);
}
public static void declerativeMultipleColumn(List<User> users){
Map<String, Map<String, Long>> map = users.stream()
.collect(Collectors.groupingBy(
User::getCategory,
Collectors.groupingBy(User::getMarketingChannel,
Collectors.counting())));
System.out.println("declerativeMultipleColumn");
System.out.println("groupingBy category and marketChannel");
System.out.println(map);
Map<String, Long> categoryMap = new HashMap<>();
Map<String, Long> channelMap = new HashMap<>();
for (Map.Entry<String, Map<String, Long>> entry: map.entrySet()) {
String category = entry.getKey();
Integer count = entry.getValue().size();
Long value = categoryMap.getOrDefault(category,0L);
categoryMap.put(category, value+count);
for (Map.Entry<String, Long> channelEntry : entry.getValue().entrySet()){
String channel = channelEntry.getKey();
Long channelCount = channelEntry.getValue();
Long channelValue = channelMap.getOrDefault(channel,0L);
channelMap.put(channel, channelValue+channelCount);
}
}
System.out.println("After Implerative Loop on above.");
System.out.println(categoryMap);
System.out.println(channelMap);
}
}
class User{
private String name;
private String category;
private String marketChannel;
public User(String name, String category, String marketChannel){
this.name = name;
this.category = category;
this.marketChannel = marketChannel;
}
public String getName(){
return this.name;
}
public String getCategory(){
return this.category;
}
public String getMarketingChannel(){
return this.marketChannel;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(name, user.name) &&
Objects.equals(category, user.category) &&
Objects.equals(marketChannel, user.marketChannel);
}
#Override
public int hashCode() {
return Objects.hash(name, category, marketChannel);
}
public static List<User> createDemoList(){
return Arrays.asList(
new User("a", "student","google"),
new User("b", "student","bing"),
new User("c", "business","google"),
new User("d", "business", "direct")
);
}
The method declerativeMultipleLoop is declarative but it has a separate loop for each field. Complexity : O(noOfFields * No of users)
The problem is in declerativeMultipleColumn Method as I end up writing imperative code and multiple loops.
I want to write the above method in completely declarative and as efficient as possible. i.e Complexity : O(No of users)
Sample output:
imperative
{business=2, student=2}
{direct=1, google=2, bing=1}
declerativeMultipleLoop
{business=2, student=2}
{direct=1, google=2, bing=1}
declerativeMultipleColumn
groupingBy category and marketChannel
{business={direct=1, google=1}, student={google=1, bing=1}}
After Implerative Loop on above.
{business=2, student=2}
{direct=1, google=2, bing=1}
If I understand your requirement it is to use a single stream operation that results in 2 separate maps. That is going to require a structure to hold the maps and a collector to build the structure. Something like the following:
class Counts {
public final Map<String, Integer> categoryCounts = new HashMap<>();
public final Map<String, Integer> channelCounts = new HashMap<>();
public static Collector<User,Counts,Counts> countsCollector() {
return Collector.of(Counts::new, Counts::accept, Counts::combine, CONCURRENT, UNORDERED);
}
private Counts() { }
private void accept(User user) {
categoryCounts.merge(user.getCategory(), 1, Integer::sum);
channelCounts.merge(user.getChannel(), 1, Integer::sum);
}
private Counts combine(Counts other) {
other.categoryCounts.forEach((c, v) -> categoryCounts.merge(c, v, Integer::sum));
other.channelCounts.forEach((c, v) -> channelCounts.merge(c, v, Integer::sum));
return this;
}
}
That can then be used as a collector:
Counts counts = users.stream().collect(Counts.countsCollector());
counts.categoryCounts.get("student")...
(Opinion only: the distinction between imperative and declarative is pretty arbitrary in this case. Defining stream operations feels pretty procedural to me (as opposed to the equivalent in, say, Haskell)).
You can compute two maps in a single forEach method:
public static void main(String[] args) {
List<User> users = Arrays.asList(
new User("a", "student", "google"),
new User("b", "student", "bing"),
new User("c", "business", "google"),
new User("d", "business", "direct"));
Map<String, Integer> categoryMap = new HashMap<>();
Map<String, Integer> channelMap = new HashMap<>();
// group users into maps
users.forEach(user -> {
categoryMap.compute(user.getCategory(),
(key, value) -> value == null ? 1 : value + 1);
channelMap.compute(user.getChannel(),
(key, value) -> value == null ? 1 : value + 1);
});
// output
System.out.println(categoryMap); // {business=2, student=2}
System.out.println(channelMap); // {direct=1, google=2, bing=1}
}
static class User {
private final String name, category, channel;
public User(String name, String category, String channel) {
this.name = name;
this.category = category;
this.channel = channel;
}
public String getName() { return this.name; }
public String getCategory() { return this.category; }
public String getChannel() { return this.channel; }
}

How to use StringBuilder with Map

I have a following map
Map<String, String> map = new HashMap<>();
And collection of dto
List<MyDto> dtoCollection = new ArrayList<>();
class MyDto {
String type;
String name;
}
for(MyDto dto : dtoCollection) {
map.compute(dto.getType(), (key,value) -> value + ", from anonymous\n"());
}
And the question is how to replace Map<String, String> to Map<String, StrinBuilder> and make append inside the loop?
You can simply replace value + ", from anonymous\n" with value == null ? new StringBuilder(dto.getName()) : value.append(", from anonymous\n")).
Illustration:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class MyDto {
String type;
String name;
public MyDto(String type, String name) {
this.type = type;
this.name = name;
}
public String getType() {
return type;
}
public String getName() {
return name;
}
}
public class Main {
public static void main(String[] args) {
Map<String, StringBuilder> map = new HashMap<>();
List<MyDto> dtoCollection = new ArrayList<>();
for (MyDto dto : dtoCollection) {
map.compute(dto.getType(), (key, value) -> value == null ? new StringBuilder(dto.getName())
: value.append(", from anonymous\n"));
}
}
}
Am I missing something?
Such methods as Map::merge or collection to map would require creation of extra StringBuilder instances which should be concatenated then:
map.merge(
dto.getType(),
new StringBuilder(dto.getName()).append(" from anonymous\n"), // redundant StringBuilder
(v1, v2) -> v1.append(v2) // merging multiple string builders
);
It is possible to use computeIfAbsent to create only one instance of StringBuilder when it's missing in the map and after that call append to the already existing value:
Map<String, StringBuilder> map = new HashMap<>();
List<MyDto> dtoCollection = Arrays.asList(
new MyDto("type1", "aaa"), new MyDto("type2", "bbb"),
new MyDto("type3", "ccc"), new MyDto("type1", "aa2"));
for (MyDto dto : dtoCollection) {
map.computeIfAbsent(dto.getType(), (key) -> new StringBuilder()) // create StringBuilder if needed
.append(dto.getName()).append(" from anonymous\n");
}
System.out.println(map);
Output:
{type3=ccc from anonymous
, type2=bbb from anonymous
, type1=aaa from anonymous
aa2 from anonymous
}

Java generics: Map nested json response to Java objects

Scenario:
I'm working with an unusual external API in which every attribute is a map with multiple values.
In order to convert this response into simple Java objects, I had to do some dirty unboxing. Below is one of the typical java class. As you can see how I'm unboxing data from response and mapping them to my java class:
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
import java.time.LocalDate;
import java.util.Map;
import static com.my.util.BaseUtil.unbox;
#Getter
#Setter
#Builder
#AllArgsConstructor
#NoArgsConstructor
public class PartyDetailsDto {
private String partyId;
private String partyType;
private String title;
private String firstName;
private String lastName;
private String middleName;
private LocalDate dateOfBirth;
#JsonProperty(value = "partyId")
public void unboxPartyId(Map<String, String> data) {
this.partyId = unbox(data, "evidenceId");
}
#JsonProperty(value = "partyType")
public void unboxPartyType(Map<String, String> partyType) {
this.partyType = unbox(partyType, "value");
}
#JsonProperty(value = "individual")
public void unboxIndividualDetails(Map<String, Object> individual) {
Map<String, String> title = (Map<String, String>) individual.get("title");
Map<String, String> firstName = (Map<String, String>) individual.get("firstName");
Map<String, String> lastName = (Map<String, String>) individual.get("lastName");
Map<String, String> middleName = (Map<String, String>) individual.get("middleName");
Map<String, String> dateOfBirth = (Map<String, String>) individual.get("birthDate");
this.title = unbox(title, "value");
this.firstName = unbox(firstName, "value");
this.lastName = unbox(lastName, "value");
this.middleName = unbox(middleName, "value");
this.dateOfBirth = LocalDate.parse(unbox(dateOfBirth, "value"));
}
}
This is the sample util method - unbox - which I've created in order to avoid writing such ugly code. Right now, it only works for cases where String is returned.
import java.util.Map;
public class BaseUtil {
// TODO: Make this method generic
public static String unbox(Map<String, String> data, String key) {
if (data != null && data.containsKey(key)) {
return data.get(key);
}
return null;
}
}
I'm trying to convert above method into a generic one where I could specify the return type dynamically and cast the returned data accordingly.
Can anyone help me out in creating one?
I've tried this:
public static <T> T unbox(Map<String, String> data, String key, Class<T> type) {
if (data != null && data.containsKey(key)) {
return (type) data.get(key);
}
return null;
}
But it obviously doesn't work but in theory that's the kind of solution that I'm expecting.
EDIT: Here's a sample input of complex type:
// The associatePartyRole is a list of Stings.
#JsonProperty(value = "associatedPartyRole")
public void unboxAssociatedPartyRole(Map<String, Object> data) {
this.associatedPartyRole = unbox(data, "value", List.class);
// Compilation error: Need list, provided object.
}
EDIT 2: Here's the final solution:
PartyDetailsDto.java
public class PartyDetailsDto implements Serializable {
private static final long serialVersionUID = 3851075484507637508L;
private String partyId;
private String partyType;
private String title;
private String firstName;
private String lastName;
private String middleName;
private LocalDate dateOfBirth;
#JsonProperty(value = "partyId")
public void unboxPartyId(Map<String, String> data) {
this.partyId = unbox(data, "evidenceId");
}
#JsonProperty(value = "partyType")
public void unboxPartyType(Map<String, String> partyType) {
this.partyType = unbox(partyType, "value");
}
#JsonProperty(value = "individual")
public void unboxIndividualDetails(Map<String, Object> individual) {
this.title = unbox(unbox(individual, "title", Map.class), "value");
this.firstName = unbox(unbox(individual, "firstName", Map.class), "value");
this.lastName = unbox(unbox(individual, "lastName", Map.class), "value");
this.middleName = unbox(unbox(individual, "middleName", Map.class), "value");
this.dateOfBirth = LocalDate.parse(unbox(unbox(individual, "title", Map.class), "value"));
}
}
BaseUtil.java
public class BaseLineUtil {
public static <T> T unbox(Map<String, Object> data, String key, Class<?> ofType) {
return Optional.ofNullable(data)
.map(m -> (T) ofType.cast(m.get(key)))
.orElse(null);
}
public static <T> T unbox(Map<String, T> data, String key) {
return Optional.ofNullable(data)
.map(m -> (T) m.get(key))
.orElse(null);
}
}
Thanks #deduper #davidxxx for your answers.
Maybe that :
public static <T> T unbox(Map<String, T> data, String key) {
if (data != null && data.containsKey(key)) {
return data.get(key);
}
return null;
}
Here T implies T extends Object.
That you can use so with any class:
Map<String, Integer> map = ...;
Integer value = unbox(map, "key");
Note that you could simplify your implementation such as :
public static <T> T unbox(Map<String, T> data, String key) {
return Optional.ofNullable(data)
.map(m -> m.get(key))
.orElse(null);
}
It is also more efficient (a single map access)
OP comment :
I followed your solution but it doesn't seem to work when the return
type is supposed to be a list or an array. How would i handle that
case
That is surprising. It should work. Try that sample code :
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("key", 100);
Integer value = unbox(map, "key");
System.out.println(value);
Map<String, List<Integer>> mapOfList = new HashMap<>();
mapOfList.put("key", Arrays.asList(1, 2));
List<Integer> valueList = unbox(mapOfList, "key");
System.out.println(valueList);
Map<String, int[]> mapOfArray = new HashMap<>();
mapOfArray.put("key", new int[] { 1, 2, 3 });
int[] valueArray = unbox(mapOfArray, "key");
System.out.println(Arrays.toString(valueArray));
}
It outputs :
100
[1, 2]
[1, 2, 3]
If that is not what you are looking for. Please rewrite your question by specifying exactly what you want to perform.
Edit after requiement change :
public void unboxIndividualDetails(Map<String, Object> individual) {...}
In fact here you want to perform unsafe casts. To achieve that you don't need to pass a Class instance and that not will not make your code more safe type either.
What you want is telling to the compiler to accept that the object that is declared as Object be assigned to a more specific type variable.
In terms of cast logic that looks like :
Object o = Integer.valueOf(100);
Integer i = (Integer)o;
By declaring a parameterized generic type method, the compiler infers T from the target type (the variable type that received the call method return), so you can do just return (T)myObject.
Concrete code :
public static <T> T unboxUnsafe(Map<String, Object> data, String key) {
return Optional.ofNullable(data)
.map(m -> (T)m.get(key))
.orElse(null);
}
And here a sample test :
public static void main(String[] args) {
Map<String, Object> mapOfObjects = new HashMap< >( );
mapOfObjects.put("longKey", 1L );
mapOfObjects.put("stringKey", "hello" );
Long l = unboxUnsafe(mapOfObjects, "longKey");
System.out.println(l);
String s = unboxUnsafe(mapOfObjects, "stringKey");
System.out.println(s);
}
Output :
1
hello
„…I followed [#davidxx's] solution but it doesn't seem to work when the return type is supposed to be a list or an array. How would i handle that case?…“
Through a process I call „EDD“ („Experiment-driven Development“) the following way to handle those cases emerged…
public static < T > T unbox( Map< String, T > data, String key, Class< ? > ofType ) {
if ( data != null && data.containsKey( key ) ) {
return (T)ofType.cast( data.get( key ) ) ;
}
return null;
}
You can observe in main(String[]) that the following calls successfully return the expected result…
...
List< String > unBoxedList = unbox( mapOfLists, foo, List.class );
...
List< ? >[ ] unBoxedArrayOfLists = unbox( mapOfArrayOfLists, "bar", List[ ].class );
...
String unBoxedString = unbox( mapOfStrings, foo, String.class );
...
Integer unBoxedInteger = unbox( mapOfIntegers, bar, Integer.class );
...
Click the green Start button at the top of the page in the link above, to run the experiment.
After feedback in the comments from #saran3h that clarified his use case, the following refactor emerged from a subsequent iteration of the experiment…
public class BaseUtil {
public List<Object> associatedPartyRole ;
// TODO: Make this method generic
public static < T > T unbox( Map< String, T > data, String key, Class< ? > ofType ) {
if ( data != null && data.containsKey( key ) ) {
return (T)ofType.cast( data.get( key ) ) ;
}
return null;
}
public void unboxAssociatedPartyRole(Map<String, Object> data) {
this.associatedPartyRole = (List)unbox(data, "foo", Object.class);
}
}
That new case was successfully tested with…
...
private static final Map<String, Object> mapOfObjects = new HashMap< >( );
...
mapOfObjects.put( foo, (Object)mapOfLists.get( foo ) );
...
BaseUtil user = new BaseUtil( );
user.unboxAssociatedPartyRole( mapOfObjects );
List< Object > objs = user.associatedPartyRole;
assertIsA( objs, List.class );
Observe the results of running the experiment with the above refactor (pardon my French)…
[What The Reifiable Fuck#&*%$*!?]
EXPERIMENT SUCCESSFUL

Java-8: stream or simpler solution?

I have two models, a List<ModelA> and I want to convert it to a List<ModelB>.
Here are my models:
class ModelA {
private Long id;
private String name;
private Integer value;
public ModelA(Long id, String name, Integer value) {
this.id = id;
this.name = name;
this.value = value;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public Integer getValue() {
return value;
}
}
class ModelB {
private Long id;
private Map<String, Integer> valuesByName;
public ModelB(Long id, Map<String, Integer> valuesByName) {
this.id = id;
this.valuesByName = valuesByName;
}
public Long getId() {
return id;
}
public Map<String, Integer> getValuesByName() {
return valuesByName;
}
}
Actual solution:
public static List<ModelB> convert(List<ModelA> models) {
List<ModelB> toReturn = new ArrayList<>();
Map<Long, Map<String, Integer>> helper = new HashMap<>();
models.forEach(modelA -> {
helper.computeIfAbsent(modelA.getId(), value -> new HashMap<>())
.computeIfAbsent(modelA.getName(), value -> modelA.getValue());
});
helper.forEach((id, valuesByName) -> toReturn.add(new ModelB(id,valuesByName)));
return toReturn;
}
But I think there is a simpler solution, do you have any idea how can I do it in a single stream, or simplify it somehow?
EDIT: I want to clarify that I cannot use java9 and I need to group them by Id-s then by Name. If in ModelB I have 4 elements with the same id I don't want new instances of ModelA.
I have combined both operations, but still constructs the intermediate map as you need to group all name, value pairs for a given id.
models.stream()
.collect(Collectors.groupingBy(model -> model.getId(), //ModelA::getId - Using method reference
Collectors.toMap(model -> model.getName(), model -> model.getValue(), (map1, map2) -> map1)))
.entrySet()
.stream()
.map(entry -> new ModelB(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
EDIT:
I missed (map1, map2) -> map1 in the initial answer. It is needed to avoid overwriting the already existing value for a id, name(the equivalent of your second computeIfAbsent in your code)
You need to choose one of them (or mege them), as by default it throws IllegalStateException when it finds a duplicate key.
This is easily achieved using the map function from Stream:
public static List<MobelB> convert(List<ModelA> models) {
Map<Long, Map<String, Integer>> modelAMap = models.stream()
.collect(Collectors.toMap(ModelA::getId, modelA -> computeMap(modelA)));
return models.stream()
.map(modelA -> new ModelB(modelA.getId(), modelAMap.get(modelA.getId())))
.collect(Collectors.toList());
}
private static Map<String, Integer> computeMap(ModelA model) {
Map<String, Integer> map = new HashMap<>();
map.put(model.getId(), model.getName());
return map;
}

Categories