Java 8 stream map custom function and convert it to Map - java-8

I have the following object:
public class Book {
private Long id;
private Long bookId;
private String bookName;
private String owner;
}
Represented from following table:
Basically, a book can be owned by multiple owners i.e. Owner "a" owns books 1 and 2.
I have a basic function that will when passed a book object, will give its owner(s) in a List.
private List<String> getBookToOwner(Book book) {
List<String> a = new ArrayList<>();
if (book.getOwner() != null && !book.getOwner().isEmpty()) {
a.addAll(Arrays.asList(book.getOwner().split("/")));
}
return a;
}
I want to use that to apply to each book, retrieve their owners and create the following Map.
Map<String, List<Long>> ownerToBookMap;
Like this:
How do I use streams here?
//books is List<Book>
Map<String, List<Long>> ownerToBookMap = books.stream().map(
// apply the above function to get its owners, flatten it and finally collect it to get the above Map object
// Need some help here..
);

You can get the owner list from the book, then flatten the owners and map as pair of bookId and owner using flatMap. Then grouping by owner using groupingBy and collect the list of bookId of owner.
Map<String, List<Long>> ownerToBookMap =
books.stream()
.flatMap(b -> getBookToOwner(b)
.stream()
.map(o -> new AbstractMap.SimpleEntry<>(o, b.getBookId())))
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())));

Flatmap the owners into a single one, create entries with key as an single owner and value as a bookId. Then group the structure by the key (owner). Finally use Collectors::mapping to get the List of bookIds instead of the actual entries:
List<Book> books = ...
Map<String, List<Long>> booksByOwner = books.stream()
.flatMap(book -> Arrays.stream(book.getOwner().split("/"))
.map(owner -> new AbstractMap.SimpleEntry<>(owner, book.getBookId())))
.collect(Collectors.groupingBy(
AbstractMap.SimpleEntry::getKey,
Collectors.mapping(AbstractMap.SimpleEntry::getValue, Collectors.toList())));

I use reduce instead of map.
Map<String, List<Long>> ownerToBookMap = books.stream().reduce(
HashMap::new,
(acc,b) -> {
getBookToOwner(b).stream().forEach( o -> {
if (!acc.containsKey(o))
acc.put(o, new ArrayList<Long>());
acc.get(o).put(b.bookId);
});
return acc;
}
).get();

Related

How to filter object lists and create another filtered lists from it

I receive a List of MediaDTO and this Object has two attributes:
String sizeType and String URL.
In 'sizeType' comes the image´s size: small, medium, large, and thumbnail.
So I have to filter the sizeType of these objects and create 4 new lists based on them.
This is how I get the List<MediaDTO> mediaDTO:
medias=[MediaDTO(sizeType=THUMBNAIL, liveloUrl=https://s3.sao01.cloud-object-storage.appdomain.cloud/catalog-media-storage/id-source/productId/skuseller2/thumbnail/celular-iphone-11-azul.png), MediaDTO(sizeType=SMALL, liveloUrl=https://s3.sao01.cloud-object-storage.appdomain.cloud/catalog-media-storage/id-source/productId/skuseller2/small/celular-iphone-11-azul.png), MediaDTO(sizeType=SMALL, liveloUrl=https://s3.sao01.cloud-object-storage.appdomain.cloud/catalog-media-storage/id-source/productId/skuseller2/medium/celular-iphone-11-azul.png), MediaDTO(sizeType=LARGE, liveloUrl=https://s3.sao01.cloud-object-storage.appdomain.cloud/catalog-media-storage/id-source/productId/skuseller2/large/celular-iphone-11-azul.png), MediaDTO(sizeType=THUMBNAIL, liveloUrl=https://s3.sao01.cloud-object-storage.appdomain.cloud/catalog-media-storage/id-source/productId/skuseller2/thumbnail/celular-iphone-11-vermelho.png), MediaDTO(sizeType=SMALL, liveloUrl=https://s3.sao01.cloud-object-storage.appdomain.cloud/catalog-media-storage/id-source/productId/skuseller2/small/celular-iphone-11-vermelho.png), MediaDTO(sizeType=MEDIUM, liveloUrl=https://s3.sao01.cloud-object-storage.appdomain.cloud/catalog-media-storage/id-source/productId/skuseller2/medium/celular-iphone-11-vermelho.png), MediaDTO(sizeType=LARGE, liveloUrl=https://s3.sao01.cloud-object-storage.appdomain.cloud/catalog-media-storage/id-source/productId/skuseller2/large/celular-iphone-11-vermelho.png)]
I achieved filtering for one of the sizes. This works!
However, I could not figure out how can I filter over the 4 sizes and create 4 new lists of it.
If I fix some error another appears ... so I´m really stuck.
And by the way I´ve been searching for a solution on the internet and in the forum for a couple of days but did´nt find something that fits.
If someone might help, I´d really be grateful.
I was thinking about using a 'forEach' to filter but even like that, I could filter just one size.
Thanks in advance.
**This is what I got till now: **
public class ProcessProductDTO {
String processId;
OperationProcess operation;
String categoryId;
ProductDTO productDTO;
}
public class ProductDTO {
String id;
Boolean active;
String displayName;
String longDescription;
List<MediaDTO> medias;
List<AttributeDTO> attributes;
}
public class MediaDTO {
String sizeType;
String liveloUrl;
}
public Properties toOccProductPropertiesDTO(ProcessProductDTO processProductDTO) throws JsonProcessingException {
String pSpecs = convertAttributes(processProductDTO.getProductDTO().getAttributes());
//List<String> medias = convertMedias(processProductDTO.getProductDTO().getMedias());
return Properties.builder()
.id(processProductDTO.getProductDTO().getId()) .active(processProductDTO.getProductDTO().getActive())
.listPrices(new HashMap())
.p_specs(pSpecs)
//.medias(medias)
.displayName(processProductDTO.getProductDTO()
.getDisplayName())
.longDescription(processProductDTO.getProductDTO().getLongDescription())
.build(); }
private String convertAttributes(List<AttributeDTO> attributes) throws JsonProcessingException {
Map<String, String> attribs = attributes.stream()
.collect(Collectors.toMap(AttributeDTO::getName, AttributeDTO::getValue));
return objectMapper.writeValueAsString(attribs);
}
private List<MediaDTO> convertMedias(ProcessProductDTO processProduct, List<MediaDTO> mediaDTO){
List<MediaDTO> filteredList = processProduct.getProductDTO().getMedias();
Set<String> filterSet = mediaDTO.stream().map(MediaDTO::getSizeType).collect(Collectors.toSet());
return filteredList.stream().filter(url -> filterSet.contains("SMALL")).collect(Collectors.toList());
}
UPDATE
I got the following result:
private Properties toOccProductPropertiesDTO(ProcessProductDTO processProductDTO) throws JsonProcessingException {
String pSpecs = convertAttributes(processProductDTO.getProductDTO().getAttributes());
MediaOccDTO medias = convertMedias(processProductDTO.getProductDTO().getMedias());
return Properties.builder()
.id(processProductDTO.getProductDTO().getId())
.active(processProductDTO.getProductDTO().getActive())
.listPrices(new HashMap())
.p_specs(pSpecs)
.medias(medias)
.displayName(processProductDTO.getProductDTO().getDisplayName())
.longDescription(processProductDTO.getProductDTO().getLongDescription())
.build();
}
private MediaOccDTO convertMedias(List<MediaDTO> mediaDTOs){
String smallImageUrls = generateOccUrl(mediaDTOs, ImageSizeType.SMALL);
String mediumImageUrls = generateOccUrl(mediaDTOs, ImageSizeType.MEDIUM);
String largeImageUrls = generateOccUrl(mediaDTOs, ImageSizeType.LARGE);
String thumbImageUrls = generateOccUrl(mediaDTOs, ImageSizeType.THUMB);
return MediaOccDTO.builder()
.p_smallImageUrls(smallImageUrls)
.p_mediumImageUrls(mediumImageUrls)
.p_largeImageUrls(largeImageUrls)
.p_thumbImageUrls(thumbImageUrls)
.build();
}
private String generateOccUrl(List<MediaDTO> mediaDTOs, ImageSizeType imageSizeType){
return mediaDTOs.stream()
.filter(m -> m.getSizeType().equals(imageSizeType))
.map(MediaDTO::getLiveloUrl)
.reduce(",", String::concat);
}
The problem is:
the comparison: m.getSizeType().equals(imageSizeType)
is always false, so the list gets created empty...
Though the question is laborious, I could think of the requirement being, you need to create 4 new lists based on sizeType.
Stream collector, can collect the results to a single data structure. It can be a list, set, Map etc.
Since you need 4 lists based on sizeType, you will need to pass through the stream 4 times to create 4 lists.
Another Alternate will be to create a Map<SizeType, List<MediaDTO>>
This can be achieved through,
mediaDTO.stream().collect(Collectors.toMap(i -> i.getSizeType(), i->i)
I think the toMap doesn't collect the values in a list. We need groupingBy instead.
mediaDTO.stream()
.collect(Collectors.groupingBy(MediaDTO::getSizeType));

Efficient way to group by a given list based on a key and collect in same list java 8

I have the below class:
class A{
String property1;
String property2;
Double property3;
Double property4;
}
So the property1 and property2 is the key.
class Key{
String property1;
String property2;
}
I already have a list of A like below:
List<A> list=new ArrayList<>();
I want to group by using the key and add to another list of A in order to avoid having multiple items with same key in the list:
Function<A, Key> keyFunction= r-> Key.valueOf(r.getProperty1(), r.getProperty2());
But then while doing group by I have to take a sum of property3 and average of property4.
I need an efficient way to do it.
Note: I have skipped the methods of the given classes.
Collecting to a Map is unavoidable since you want to group things. A brute-force way to do that would be :
yourListOfA
.stream()
.collect(Collectors.groupingBy(
x -> new Key(x.getProperty1(), x.getProperty2()),
Collectors.collectingAndThen(Collectors.toList(),
list -> {
double first = list.stream().mapToDouble(A::getProperty3).sum();
// or any other default
double second = list.stream().mapToDouble(A::getProperty4).average().orElse(0D);
A a = list.get(0);
return new A(a.getProperty1(), a.getProperty2(), first, second);
})))
.values();
This could be slightly improved for example in the Collectors.collectingAndThen to only iterate the List once, for that a custom collector would be required. Not that complicated to write one...
Try like this:
Map<A,List<A>> map = aList
.stream()
.collect(Collectors
.groupingBy(item->new A(item.property1,item.property2)));
List<A> result= map.entrySet().stream()
.map(list->new A(list.getValue().get(0).property1,list.getValue().get(0).property1)
.avgProperty4(list.getValue())
.sumProperty3(list.getValue()))
.collect(Collectors.toList());
and create avgProperty4 and sumProperty3 methods like to this
public A sumProperty3(List<A> a){
this.property3 = a.stream().mapToDouble(A::getProperty3).sum();
return this;
}
public A avgProperty4(List<A> a){
this.property4 = a.stream().mapToDouble(A::getProperty4).average().getAsDouble();
return this;
}
result = aList.stream().collect(Collectors
.groupingBy(item -> new A(item.property1, item.property2),
Collectors.collectingAndThen(Collectors.toList(), list ->
new A(list.get(0).property1, list.get(0).property1)
.avgProperty4(list).sumProperty3(list))
)
);

Java 8 Stream Merge Partial Duplicates

I have a POJO that looks something like this:
public class Account {
private Integer accountId;
private List<String> contacts;
}
The equals And hashCode methods are set to use the accountId field to identify uniqueness, so any Accounts with the same accountId are equal regardless of what contacts contain.
I have a List of accounts and there are some duplicates with the same accountId. How do I use Java 8 Stream API to merge these duplicates together?
For example, the list of account contains:
+-----------+----------+
| accountId | contacts |
+-----------+----------+
| 1 | {"John"} |
| 1 | {"Fred"} |
| 2 | {"Mary"} |
+-----------+----------+
And I want it to produce a list of accounts like this:
+-----------+------------------+
| accountId | contacts |
+-----------+------------------+
| 1 | {"John", "Fred"} |
| 2 | {"Mary"} |
+-----------+------------------+
Use Collectors.toMap Ref: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#toMap-java.util.function.Function-java.util.function.Function-java.util.function.BinaryOperator-
#lombok.Value
class Account {
Integer accountId;
List<String> contacts;
}
List<Account> accounts = new ArrayList<>();
//Fill
List<Account> result = new ArrayList<>(accounts.stream()
.collect(
Collectors.toMap(Account::getAccountId, Function.identity(), (Account account1, Account account2) -> {
account1.getContacts().addAll(account2.getContacts());
account2.getContacts().clear();
return account1;
})
)
.values());
A clean Stream API solution can be quiet complicated, so perhaps you’re better off with a Collection API solution that has less constraints to obey.
HashMap<Integer, Account> tmp = new HashMap<>();
listOfAccounts.removeIf(a -> a != tmp.merge(a.getAccountId(), a, (o,n) -> {
o.getContacts().addAll(n.getContacts());
return o;
}));
This directly removes all elements with a duplicate id from the list after having added their contacts to the first account of that id.
Of course, this assumes that the list supports removal and the list returned by getContacts() is a reference to the stored list and supports adding elements.
The solution is built around Map.merge which will add the specified object if the key didn’t exist or evaluates the merge function if the key already existed. The merge function returns the old object after having added the contacts, so we can do a reference comparison (a != …) to determine that we have a duplicate that should be removed.
You could add two constructors and a merge method to the Account class that would combine contacts:
public class Account {
private final Integer accountId;
private List<String> contacts = new ArrayList<>();
public Account(Integer accountId) {
this.accountId = accountId;
}
// Copy constructor
public Account(Account another) {
this.accountId = another.accountId;
this.contacts = new ArrayList<>(another.contacts);
}
public Account merge(Account another) {
this.contacts.addAll(another.contacts);
return this;
}
// TODO getters and setters
}
Then, you have a few alternatives. One is to use Collectors.toMap to collect accounts to a map, grouping by accountId and merging the contacts of the accounts with equal accountId by means of the Account.merge method. Finally, get the values of the map:
Collection<Account> result = accounts.stream()
.collect(Collectors.toMap(
Account::getAccountId, // group by accountId (keys)
Account::new, // use copy constructor (values)
Account::merge)) // merge values with equal key
.values();
You need to use the copy constructor for the values, otherwise you would mutate the accounts of the original list when Account.merge is invoked.
An equivalent way (without streams) would be to use the Map.merge method:
Map<Integer, Account> map = new HashMap<>();
accounts.forEach(a ->
map.merge(a.getAccountId(), new Account(a), Account::merge));
Collection<Account> result = map.values();
Again, you need to use the copy constructor to avoid undesired mutations on the accounts of the original list.
A third alternative which is more optimized (because it doesn't create a new account for every element of the list) consists of using the Map.computeIfAbsent method:
Map<Integer, Account> map = new HashMap<>();
accounts.forEach(a -> map.computeIfAbsent(
a.getAccountId(), // group by accountId (keys)
Account::new) // invoke new Account(accountId) if absent
.merge(a)); // merge account's contacts
Collection<Account> result = map.values();
All the alternatives above return a Collection<Account>. If you need a List<Account> instead, you can do:
List<Account> list = new ArrayList<>(result);

Convert ImmutableListMultimap to Map using Collectors.toMap

I would like to convert a ImmutableListMultimap<String, Character> to Map<String, List<Character>>.
I used to do it in the non-stream way as follows
void convertMultiMaptoList(ImmutableListMultimap<String, Character> reverseImmutableMultiMap) {
Map<String, List<Character>> result = new TreeMap<>();
for( Map.Entry<String, Character> entry: reverseImmutableMultiMap.entries()) {
String key = entry.getKey();
Character t = entry.getValue();
result.computeIfAbsent(key, x-> new ArrayList<>()).add(t);
}
//reverseImmutableMultiMap.entries().stream().collect(Collectors.toMap)
}
I was wondering how to write the above same logic using java8 stream way (Collectors.toMap).
Please share your thoughts
Well there is already a asMap that you can use to make this easier:
Builder<String, Character> builder = ImmutableListMultimap.builder();
builder.put("12", 'c');
builder.put("12", 'c');
ImmutableListMultimap<String, Character> map = builder.build();
Map<String, List<Character>> map2 = map.asMap()
.entrySet()
.stream()
.collect(Collectors.toMap(Entry::getKey, e -> new ArrayList<>(e.getValue())));
If on the other hand you are OK with the return type of the asMap than it's a simple method call:
ImmutableMap<String, Collection<Character>> asMap = map.asMap();
Map<String, List<Character>> result = reverseImmutableMultiMap.entries().stream()
.collect(groupingBy(Entry::getKey, TreeMap::new, mapping(Entry::getValue, toList())));
The important detail is mapping. It will convert the collector (toList) so that it collects List<Character> instead of List<Entry<String, Character>>. According to the mapping function Entry::getValue
groupingBy will group all entries by the String key
toList will collect all values with same key to a list
Also, passing TreeMap::new as an argument to groupingBy will make sure you get this specific type of Map instead of the default HashMap

Find the difference between two collections in Java 8?

I am trying to make a List of all of the books in one Collection that are not present in another. My problem is that I need to compare based on book ID, so I can't just test to see whether a book in the first is contained in the second, I have to determine whether any book in the second collection has the same ID as a book in the first.
I have the below code to compare two collections of books and filter the first collection:
List<Book> parentBooks = listOfBooks1.stream().filter(book->
!listOfBooks2.contains(book)).collect(Collectors.toList());
The code doesn't work correctly because I am comparing the objects themselves. I need to compare the objects based on the bookId instead of the whole book object. How should I change the code so it can do the comparison based on the bookId (book.getId())?
List<Book> books1 = ...;
List<Book> books2 = ...;
Set<Integer> ids = books2.stream()
.map(Book::getId)
.collect(Collectors.toSet());
List<Book> parentBooks = books1.stream()
.filter(book -> !ids.contains(book.getId()))
.collect(Collectors.toList());
The problem is complex, but it boils down to one thing, knows your data. Is it immutables, entities with an id, duplicate entries etc?
The code below works for immutables with only values (and with possible duplicates).
It first tries to remove all entries in the before list (from the copied after-list).
What is left will be the added elements. The ones from the before-list that can be removed from the after-list are the unchanged ones.
The rest are the removed ones
public class ListDiffer<T> {
private List<T> addedList = new ArrayList<>();
private List<T> unchangedList = new ArrayList<>();
private List<T> removedList = new ArrayList<>();
public ListDiffer(List<T> beforeList, List<T> afterList) {
addedList.addAll(afterList); // Will contain only new elements when all elements in the Before-list are removed.
beforeList.forEach(e -> {
boolean b = addedList.remove(e) ? unchangedList.add(e) : removedList.add(e);
});
}
public List<T> getAddedList() {
return addedList;
}
public List<T> getUnchangedList() {
return unchangedList;
}
public List<T> getRemovedList() {
return removedList;
}
}

Resources