Combine two Maps to a List of objects using Java8 - java

Given 2 Maps and an object
Map<Integer, List<String>> fruits = new HashMap<>();
fruits.put(1, Arrays.asList("apple", "banana"));
fruits.put(2, Arrays.asList("orange", "kiwi"));
Map<Integer, List<String>> veggies= new HashMap<>();
veggies.put(1, Arrays.asList("tomato", "potato"));
veggies.put(2, Arrays.asList("onion"));
Class Food
{
private id;
private List<String> fruitsList;
private List<String> veggiesList;
//getters and setters
}
I am trying to combine the given 2 maps to a single list containing Food object(List).
//Used for explanation purpose
Food food1 = new Food();
food1.setId(1);
food1.setFruitsList(Arrays.asList("apple", "banana"));
food1.setVeggiesList(Arrays.asList("tomato", "potato"));
//Used for explanation purpose
Food food2 = new Food();
food2.setId(2);
food2.setFruitsList(Arrays.asList("orange", "kiwi"));
food2.setVeggiesList(Arrays.asList("onion"));
//Wanted this list of food
List<Food> foodList = new ArrayList();
foodList.add(food1);
foodList.add(food2);
I need to get a List.
Can we achieve that using Java8 streams?
Any solutions would be appreciated.

You can do something like this:
List<Food> foodList = fruits.keySet().stream()
.concat(veggies.keySet().stream())
.distinct()
.map(id -> {
Food food = new Food();
food.setId(id);
food.setFruitsList(fruits.getOrDefault(id, new ArrayList<>()));
food.setVeggiesList(veggies.getOrDefault(id, new ArrayList<>()));
return food ;
})
.collect(Collectors.toList());
If your Food class has a constructor taking the three parameters, it gets just a little more concise:
List<Food> foodList = fruits.keySet().stream()
.concat(veggies.keySet().stream())
.distinct()
.map(id -> new Food(
id,
fruits.getOrDefault(id, new ArrayList<>()),
veggies.getOrDefault(id, new ArrayList<>())
)
.collect(Collectors.toList());
If you know the ids are the same in both maps, you can skip the .concat() and .distinct() steps (which are quite expensive), and just use get() instead of getOrDefault().
You can also (in any case) do
Set<Integer> allKeys = new HashSet<>(fruits.keySet());
allKeys.addAll(veggies.keySet());
List<Food> foodList = allKeys.stream()
.map(/* as before */)
.collect(Collectors.toList());
which is not quite a "pure stream" a solution, but is probably more efficient.

You can merge the two keys streams and use a distinct method to get only unique keys.
List<Food> foods = Stream.concat(fruits.keySet().stream() , veggies.keySet().stream())
.distinct()
.map(e -> new Food(e, fruits.get(e), veggies.get(e)))
.collect(Collectors.toList());

Related

Java Map with list value, filter on another list

I have looked on other topics but I cant find the solutions for my problem. I have a map that contains a key = cartID for example C01 and a ArrayList of products. I want to filter this map on another ArrayList that contains productIDs so I can find all the carts that contain these products. I have following code
Map<String, List<OrderLineDTO>> allCarts //contains data these are fetched
List<String> cart; //also contains fetched data for example ["Product01", "Product02"]
allCarts.values().stream().filter(list -> list.stream().allMatch(value -> cart.contains(value.getProductId()))).collect(Collectors.toList()); //filter I'm using but not working
Any ideas?
Thanks!
If you want to find IDs of the carts containing products only in the list, this should work:
List<String> cartIds = allCarts
.entrySet().stream()
.filter(e ->
e.getValue().stream().allMatch(p -> productIds.contains(p.getProductId())))
.map(e -> e.getKey())
.collect(Collectors.toList());
So the only difference is that I used entrySet() instead of values().
To find carts, containing at least one product in the list, .anyMatch predicate needs to be used.
Try to use entrySet() and anyMatch:
allCarts.entrySet().stream().filter(p -> cart.stream().anyMatch(prod -> p.getValue().contains(prod.get))).collect(Collectors.toList());
The following returns a list of all carts int the list that contain the specified product ids. Included is an OrderLineDTO class to fulfill the demo.
Map<String, List<OrderLineDTO>> allCarts = new HashMap<>();
List<String> cart = new ArrayList<>(); // also contains fetched data for example ["Product01", "Product02"]
cart.add("ID7");
cart.add("ID1");
allCarts.put("cart1", List.of(new OrderLineDTO("ID1"),
new OrderLineDTO("ID2")));
allCarts.put("cart2", List.of(new OrderLineDTO("ID3"),
new OrderLineDTO("ID5")));
allCarts.put("cart3", List.of(new OrderLineDTO("ID3"),
new OrderLineDTO("ID4")));
allCarts.put("cart4", List.of(new OrderLineDTO("ID6"),
new OrderLineDTO("ID7")));
List<String> carts = allCarts.entrySet().stream()
.filter(e -> e.getValue().stream().anyMatch(
prod -> cart.contains(prod.getProductId())))
.map(e -> e.getKey()).collect(Collectors.toList());
System.out.println(carts);
Prints
[cart1, cart4]
class OrderLineDTO {
String id;
public OrderLineDTO(String id) {
this.id = id;
}
public String getProductId() {
return id;
}
public String toString() {
return id;
}
}
}
I had to use containsAll to find my answer. Thanks everyone for the help!
List<String> carts = allCarts.entrySet().stream()
.filter(e -> (e.getValue().stream().map(f -> f.getProductId()).collect(Collectors.toList()).containsAll(cart)))
.map(e -> e.getKey()).collect(Collectors.toList());
I think this one is more simple, filters if value equal x then returns a list of keys.
List<String> result = map.entrySet().stream()
.filter(e -> e.getValue().equals(x))
.map(e -> e.getKey()).collect(Collectors.toList());

How to group list of string arrays, java 8 or java 11

{username,order}
trying to group them by username.
List<String[]> orders = new ArrayList<>();
String[] o1 = {"john, apple"};
String[] o2 = {"john, Orange"};
String[] o3 = {"jane , banana"};
String[] o4 = {"jane, Orange"};
orders.add(o1);
orders.add(o2);
orders.add(o3);
orders.add(o4);
lets say if i have the folowing orders
and i want to get to this
List<List<String[]>> groupedOrders = new ArrayList<>();
groupedOrders.add(Arrays.asList(o1,o2));
groupedOrders.add(Arrays.asList(o3,o4));
so the result I am looking for is groupedOrders,like the above one or in a map, as long as the list of orders are grouped together how do I transform orders list above into groupOrders, I have tried to use map or streams but couldn't make it work.
I would recommend against using arrays, by using a custom class:
class Order {
String username;
String order;
}
I've omitted the getters, setters, modifiers and constructors, as these 2 fields are the main component of that class.
You can then have a List<Order> orders where you add all the orders, from there it's rather easy to group them by name:
Map<String, List<Order>> grouped = orders.stream()
.collect(Collectors.groupingBy(Order::getUsername));
If you only want the actual orders, like banana, apple and so on you can use this:
Map<String, List<String>> grouped = orders.stream()
.collect(Collectors.groupingBy(
Order::getUsername,
Collectors.mapping(Order::getOrder, Collectors.toList())
));
If you really want to use arrays as your input you may aswell use this:
Map<String, List<String>> grouped = Arrays.stream(orders)
.collect(Collectors.groupingBy(
a -> a[0],
Collectors.mapping(a -> a[1], Collectors.toList())
));
Note that your array String[] o1 = {"john, apple"}; has only 1 element and you probably want something like {"john", "apple"}. Pay attention to quotes.
I would also recommend #Lino's solution, but it can be done with arrays:
Map<String, List<String[]>> map = orders.stream()
.collect(Collectors.groupingBy(s -> s[0]));

Java collect to list but specify pre-defined first two elements order

I have a List<Person> objects. From it I want to get a list of all id's, and I always want the id "abc" and "bob" to come as the 0th and 1st index of the list if available. Is there a way to do this with java streams?
class Person {
private String id;
}
List<Person> allPeople = ...
List<String> allIds = allPeople.stream().map(Person::id).collect(Collectors.toList());
My approach is:
Set<String> allIds = allPeople.stream().map(Person::id).collect(Collectors.Set());
List<String> orderedIds = new ArrayList<>();
if(allIds.contains("abc")) {
orderedIds.add("abc");
}
if(allIds.contains("bob")) {
orderedIds.add("bob");
}
//Iterate through the set and all add all entries which are not bob and abc in the list.
It seems like you need more of a PriorityQueue rather than a List here, so may be something like this:
PriorityQueue<String> pq = list.stream()
.map(Person::getId)
.distinct()
.collect(Collectors.toCollection(() -> new PriorityQueue<>(
Comparator.comparing(x -> !"abc".equals(x))
.thenComparing(x -> !"bob".equals(x)))));
If you still need a List though, just drain that pq into one:
List<String> result = new ArrayList<>();
while (!pq.isEmpty()) {
result.add(pq.poll());
}
I assume that each id occurs only once in the list. With this I would choose a simple straightforward solution:
List<Person> allPeople = ...;
List<String> allIds = allPeople.stream().map(Person::id).collect(toCollection(ArrayList::new));
boolean foundBob = allIds.remove("bob");
if (foundBob) allIds.add(0, "bob");
boolean foundAbc = allIds.remove("abc");
if (foundAbc) allIds.add(0, "abc");
Note that "bob" and "abc" are moved to the head of the list in reverse order. So "abc" is first in the end.
You can make a small utility method for moving an element:
static void moveToHead(List<String> list, String elem) {
boolean found = list.remove(elem);
if (found) list.add(0, elem);
}
With this your code is even simpler and easier to understand:
List<Person> allPeople = ...;
List<String> allIds = allPeople.stream().map(Person::id).collect(toCollection(ArrayList::new));
moveToHead(allIds, "bob");
moveToHead(allIds, "abc");
if you want to perform this in a "fully" stream pipeline you could do:
allPeople.stream()
.map(Person::id)
.distinct()
.collect(collectingAndThen(partitioningBy(s -> "abc".equals(s) || "bob".equals(s)),
map -> Stream.concat(map.get(true).stream(), map.get(false).stream())));
.collect(toList());
if you always want "abc" in front of "bob" then change
map.get(true).stream()
to
map.get(true).stream()
.sorted(Comparator.comparing((String s) -> !s.equals("abc")))
Another solution you could do is:
Set<String> allIds = allPeople.stream().map(Person::id).collect(toSet());
List<String> orderedIds = Stream.concat(allIds.stream()
.filter(s -> "abc".equals(s) || "bob".equals(s))
.sorted(Comparator.comparing((String s) -> !s.equals("abc"))),
allIds.stream().filter(s -> !"abc".equals(s) && !"bob".equals(s)))
.collect(toList());
which is pretty much doing the same thing as the above partitioningBy but just in a different approach.
Finaly, you might be surprised but your approach actually seems good, so you may want to complete it with:
Set<String> allIds = allPeople.stream().map(Person::id).collect(toSet());
List<String> orderedIds = new ArrayList<>();
if(allIds.contains("abc"))
orderedIds.add("abc");
if(allIds.contains("bob"))
orderedIds.add("bob");
orderedIds.addAll(allIds.stream().filter(s -> !"abc".equals(s) && ! "bob".equals(s)).collect(toList()));
Inspired by Stuart Marks there is an even simpler solution:
List<String> allIds = allPeople.stream()
.map(Person::getId)
.distinct()
.sorted(comparing(x -> !"abc".equals(x)).thenComparing(x -> !"bob".equals(x)))
.collect(Collectors.toList());

Java 8 extract all keys from matching values in a Map

I'm relatively new to Java8 and I have a scenario where I need to retrieve all the keys from the Map which matched with the objects.
Wanted to know if there is a way to get all keys without iterating them from the list again.
Person.java
private String firstName;
private String lastName;
//setters and getters & constructor
MAIN Class.
String inputCriteriaFirstName = "john";
Map<String, Person> inputMap = new HashMap<>();
Collection<Person> personCollection = inputMap.values();
List<Person> personList = new ArrayList<>(personCollection);
List<Person> personOutputList = personList.stream()
.filter(p -> p.getFirstName().contains(inputCriteriaFirstName ))
.collect(Collectors.toList());
//IS There a BETTER way to DO Below ??
Set<String> keys = new HashSet<>();
for(Person person : personOutputList) {
keys.addAll(inputMap.entrySet().stream().filter(entry -> Objects.equals(entry.getValue(), person))
.map(Map.Entry::getKey).collect(Collectors.toSet()));
}
inputMap.entrySet()
.stream()
.filter(entry -> personOutputList.contains(entry.getValue()))
.map(Entry::getKey)
.collect(Collectors.toCollection(HashSet::new))
Instead of iterating over all the entries of the Map for each Person, I suggest iterating over the Map once:
Set<String> keys =
inputMap.entrySet()
.stream()
.filter(e -> personOutputList.contains(e.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toCollection(HashSet::new));
This would still result in quadratic running time (since List.contains() has linear running time). You can improve that to overall linear running time if you create a HashSet containing the elements of personOutputList, since contains for HashSet takes constant time.
You can achieve that by changing
List<Person> personOutputList =
personList.stream()
.filter(p -> p.getFirstName().contains(inputCriteriaFirstName))
.collect(Collectors.toList());
to
Set<Person> personOutputSet =
personList.stream()
.filter(p -> p.getFirstName().contains(inputCriteriaFirstName))
.collect(Collectors.toCollection(HashSet::new));
You can also use foreach api provided in java8 under lambda's
Below is code for your main method :
public static void main() {
String inputCriteriaFirstName = "john";
Map<String, Person> inputMap = new HashMap<>();
Set<String> keys = new HashSet<>();
inputMap.forEach((key,value) -> {
if(value.getFirstName().contains(inputCriteriaFirstName)){
keys.add(key);
}
});
}
So, you want a personOutputList with all the selected persons, and a keys set with the keys for those selected persons?
Best (for performance) option is to not discard the keys during search, then split the result into separate person list and key set.
Like this:
String inputCriteriaFirstName = "john";
Map<String, Person> inputMap = new HashMap<>();
Map<String, Person> tempMap = inputMap.entrySet()
.stream()
.filter(e -> e.getValue().getFirstName().contains(inputCriteriaFirstName))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
List<Person> personOutputList = new ArrayList<>(tempMap.values());
Set<String> keys = new HashSet<>(tempMap.keySet());
The keys set is explicitly made an updatable copy. If you don't need that, drop the copying of the key values:
Set<String> keys = tempMap.keySet();

Lambda expressions and nested arrays

I'm trying to come up with some nice lambda expressions to build "desiredResult" from "customers" ArrayList. I implemented it in an old ugly way "for" loop. I know there should be nice one-liners, but I can't think of any method - nested arrays come into my way.
Iterable<List<?>> params;
Customer customer1 = new Customer("John", "Nowhere");
Customer customer2 = new Customer("Alma", "Somewhere");
Customer customer3 = new Customer("Nemo", "Here");
Collection<Customer> customers = new ArrayList<>();
customers.add(customer1);
customers.add(customer2);
customers.add(customer3);
Collection<List<?>> desiredResult = new ArrayList<>();
for (Customer customer : customers) {
List<Object> list = new ArrayList<>();
list.add(customer.getName());
list.add(customer.getAddress());
list.add("VIP");
desiredResult.add(list);
}
params = desiredResult;
I'd just use Arrays.asList for creating the inner lists, which makes the problem much simpler:
Collection<List<?>> desiredResult =
customers.stream()
.map(c -> Arrays.asList(c.getName(), c.getAddress(), "VIP"))
.collect(Collectors.toList());
If you absolutely must have an ArrayList, just wrap the Arrays.asList call with it.
Here is a suggestion:
Collection<List<?>> desiredResult = customers.stream()
.map(MyClass::customerToList)
.collect(toList());
I have extracted the list building into a separate method for better readability - the corresponding method would look like this:
private static List<Object> customerToList(Customer c) {
List<Object> list = new ArrayList<>();
list.add(c.getName());
list.add(c.getAddress());
list.add("VIP");
return list;
}

Categories