I have an unsorted list with an unknown amount of items, in the range of 1000. These items all contain a label that determines where it is supposed to go. To avoid iterating over every item in the list several times, I want to split this list into a certain number of sublists that contain only the items of certain labels.
List<Item> allItems = getItemsFromSomewhere();
List<Item> itemsLabeledA1 = new ArrayList<>();
List<Item> itemsLabeledA2 = new ArrayList<>();
List<Item> itemsLabeledB1 = new ArrayList<>();
...
List<Item> itemsLabeledL3 = new ArrayList<>();
To further complicate the issue, some of the lists require a range of items to be added, which is why each item is labeled something like "A1", "A2", "A3". These lists require every item with an A-label to be added to them. Not all labels have these aggregate lists, however. I might have to aggregate all A-labeled items, while not aggregating all B-labeled items, while still sorting A1, B1, etc. in their own lists.
Given the example above, how do I elegantly split the full list in a single iteration? My initial thought was using ifs or a switch block, but that is an ugly solution.
allItems.forEach(item -> {
if (item.getLabel().contains("A1")) {
itemsLabeledA1.add(item);
allItemsLabeledA.add(item);
}
else if (item.getLabel().contains("B1")) itemsLabeledB1.add(item);
...
else if (item.getLabel().contains("L3")) itemsLabeledL3.add(item);
});
Is there a better way?
I would use groupingBy in two separate streaming operations:
Map<String, List<Item>> allByLabel = allItems.stream().collect(
Collectors.groupingBy(Item::getLabel));
Map<String, List<Item>> allByLabelStart = allItems.stream().collect(
Collectors.groupingBy(item -> item.getLabel().substring(0, 1)));
Maybe you can try using a HashMap where the key is the labels and the values are the sublists?
HashMap<String, List<Item>> map = new HashMap<String, List<Item>>();
for (String label : labels) {
map.put(label, new List<Item>);
}
allItems.forEach(item -> {
String label = item.getLabel();
map.get(label).add(item);
}
It seems you are classifying your items. For this, you'd need to create different groups as per your specification, then add each item to its group. If I understood correctly your requirement, you could accomplish this as follows:
Map<String, List<Item>> map = new LinkedHashMap<>(); // keeps insertion order
allItems.forEach(it -> {
String lbl = it.getLabel();
map.computeIfAbsent(lbl, k -> new ArrayList<>()).add(it);
if (needsToBeAggregated(lbl)) {
map.computeIfAbsent(lbl.substring(0, 1), k -> new ArrayList<>()).add(it);
}
});
Where boolean needsToBeAggregated(String label) is some method where you decide whether the item should be added to an aggregate group or not.
This solution doesn't use streams, but Java 8's Map.computeIfAbsent method instead.
Related
Let's say that I have an Entity Item and OutOfStockItem which extend Item.
I have a method basket.getList() which returns List of Item and OutOfStockItems, all I want to filter OutOfStockItem from the list and receive result:
List<Item>, number of the elements in the left collection
So far I have a method like:
return basket.getList().stream()
.filter(OutOfStockItem.class::isInstance)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
but it returns Map<Item, Long> which will return a counter for every distinct entity like:
Item1 = 1;
Item2 = 1;
which is not desirable.
Desirable collection result is:
(Item1, Item2; 2)
I will be grateful for suggestions on how to reach a goal.
Not sure why you need such a result, but one way could be to use Collectors.teeing() (Java 12 or higher)
Map<List<Item>,Long> result =
basket.getList().stream()
.filter(OutOfStockItem.class::isInstance)
.collect(
Collectors.teeing(
Collectors.toList(),
Collectors.counting(),
(list, count) -> Map.of(list,count)
));
The output map (Item1, Item2; 2) always results in just one entry, so the solution that creates the entry from a list is straightforward and java-stream brings no benefit in here.
Just remember the map data structure with a list as a key is strange and unusual.
Java 9+
List<Item> items = new ArrayList<>(basket.getList());
items.removeIf(item -> !(item instanceof OutOfStockItem));
Map<List<Item>, Integer>> map = Map.of(items, items.size());
Java 8
List<Item> items = new ArrayList<>(basket.getList());
items.removeIf(item -> !(item instanceof OutOfStockItem));
Map<List<Item>, Integer>> map = new HashMap<>();
map.put(items, items.size());
Considering I have a list of objects List<Emp> where Emp has 3 properties name, id, and age. What is the fastest way to get 3 lists like List<String> names, List<String> ids, and List<Integer> ages.
The simplest I could think of is to iterate over the entire list and keep adding to these 3 lists. But, I was wondering if there is an easier way to do it with Java 8 streams?
Thanks in advance.
It's a very interesting question, however, there is no dedicated collector to handle such use case.
All you can is to use 3 iterations (Streams) respectively:
List<String> names = employees.stream().map(Emp::name).collect(Collectors.toList());
List<Integer> ids = employees.stream().map(Emp::id).collect(Collectors.toList());
List<Integer> ages = employees.stream().map(Emp::age).collect(Collectors.toList());
Edit - write the own collector: you can use the overloaded method Stream::collect(Supplier, BiConsumer, BiConsumer) to implement your own collector doing what you need:
Map<String, List<Object>> newMap = employees.stream().collect(
HashMap::new, // Supplier of the Map
(map, emp) -> { // BiConsumer accumulator
map.compute("names", remappingFunction(emp.getName()));
map.compute("ages", remappingFunction(emp.getAge()));
map.compute("ids", remappingFunction(emp.getId()));
},
(map1, map2) -> {} // BiConsumer combiner
);
Practically, all it does is extracting the wanted value (name, age...) and adding it to the List under the specific key "names", "ages" etc. using the method Map::compute that allows to compute a new value based on the existing (null by default if the key has not been used).
The remappingFunction that actually creates a new List or adds a value looks like:
private static BiFunction<String, List<Object>, List<Object>> remappingFunction(Object object) {
return (key, list) -> {
if (list == null)
list = new ArrayList<>();
list.add(object);
return list;
};
}
Java 8 Stream has some API to split the list into partition, such as:
1. Collectros.partitioningBy(..) - which create two partitions based on some Predicate and return Map<Boolean, List<>> with values;
2. Collectors.groupingBy() - which allows to group stream by some key and return resulting Map.
But, this is not really your case, since you want to put all properties of the Emp object to different Lists. I'm not sure that this can be achieved with such API, maybe with some dirty workarounds.
So, yes, the cleanest way will be to iterate through the Emp list and out all properties to the three Lists manually, as you have proposed.
I'm in a weird situation where have a JSON API that takes an array with strings of neighborhoods as keys and an array of strings of restaurants as values which get GSON-parsed into the Restaurant object (defined with a String for the neighborhood and a List<String> with the restaurants). The system stores that data in a map whose keys are the neighborhood names and values are a set of restaurant names in that neighborhood. Therefore, I want to implement a function that takes the input from the API, groups the values by neighborhood and concatenates the lists of restaurants.
Being constrained by Java 8, I can't use more recent constructs such as flatMapping to do everything in one line and the best solution I've found is this one, which uses an intermediate map to store a Set of List before concatenating those lists into a Set to be store as value in the final map:
public Map<String, Set<String>> parseApiEntriesIntoMap(List<Restaurant> restaurants) {
if(restaurants == null) {
return null;
}
Map<String, Set<String>> restaurantListByNeighborhood = new HashMap<>();
// Here we group by neighborhood and concatenate the list of restaurants into a set
Map<String, Set<List<String>>> map =
restaurants.stream().collect(groupingBy(Restaurant::getNeighborhood,
Collectors.mapping(Restaurant::getRestaurantList, toSet())));
map.forEach((n,r) -> restaurantListByNeighborhood.put(n, Sets.newHashSet(Iterables.concat(r))));
return restaurantListByNeighborhood;
}
I feel like there has to be a way do get rid of that intermediate map and do everything in one line...does someone have a better solution that would allow me to do this?
You could with Java-8 simply use toMap with a mergeFunction defined as:
public Map<String, Set<String>> parseApiEntriesIntoMap(List<Restaurant> restaurants) {
// read below about the null check
return restaurants.stream()
.collect(Collectors.toMap(Restaurant::getNeighborhood,
r -> new HashSet<>(r.getRestaurantList()), (set1, set2) -> {
set1.addAll(set2);
return set1;
}));
}
Apart from which, one should ensure that the check and the result from the first block of code from your method
if(restaurants == null) {
return null;
}
when on the other hand dealing with empty Collections and Map, it should be redundant as the above code would return empty Map for an empty List by the nature of stream and collect operation itself.
Note: Further, if you may require a much relatable code to flatMapping in your future upgrades, you can use the implementation provided in this answer.
Or a solution without using streams, in this case, would look similar to the approach using Map.merge. It would use a similar BiFunction as:
public Map<String, Set<String>> parseApiEntriesIntoMap(List<Restaurant> restaurants) {
Map<String, Set<String>> restaurantListByNeighborhood = new HashMap<>();
for (Restaurant restaurant : restaurants) {
restaurantListByNeighborhood.merge(restaurant.getNeighborhood(),
new HashSet<>(restaurant.getRestaurantList()),
(strings, strings2) -> {
strings.addAll(strings2);
return strings;
});
}
return restaurantListByNeighborhood;
}
You can also flatten the Set<List<String>> after collecting them using Collectors.collectingAndThen
Map<String, Set<String>> res1 = list.stream()
.collect(Collectors.groupingBy(Restaurant::getNeighborhood,
Collectors.mapping(Restaurant::getRestaurantList,
Collectors.collectingAndThen(Collectors.toSet(),
set->set.stream().flatMap(List::stream).collect(Collectors.toSet())))));
I have the list as follows:
List<Map<String,Object>> mapList=new ArrayList<>();
Map<String,Object> mapObject=new HashMap<String,Object>();
mapObject.put("No",1);
mapObject.put("Name","test");
mapList.add(mapObject);
Map<String,Object> mapObject1=new HashMap<String,Object>();
mapObject1.put("No",2);
mapObject1.put("Name","test");
mapList.add(mapObject1);
and so on...
Now I want to get all the values of the key "No" as a string seperated by comma as follows:
String noList="1,2,3"
Can anyone please suggest me what may best way to do it. I know we can do it by looping but instead of looping is any other ways to do it.
Explanations inline!
mapList.stream() // stream over the list
.map(m -> m.get("No")) // try to get the key "No"
.filter(Objects::nonNull) // filter any null values in case it wasn't present
.map(Object::toString) // call toString for each object
.collect(Collectors.joining(",")); // join the values
Simply map the list:
String list = mapList.stream()
.filter(x -> x.containsKey("No")) // get only the maps that has the key
.map(x -> x.get("No").toString()) // every map will be transformed like this
.collect(Collectors.joining(",")); // joins all the elements with ","
System.out.println(list);
The use of HashMap<String, Object> suggests that it might be better to create a new class for this data. Have you considered this possibility before?
You can loop like this:
List<String> noList = new ArrayList<>(mapList.size());
for (Map<String,Object> m : mapList) {
Optional.ofNullable(m.get("No")) // get value mapped to "No" or empty Optional
.map(Object::toString)
.ifPresent(noList::add); // if not empty, add to list
}
System.out.println(String.join(",", noList));
or internally (the officially preferred version IIRC):
List<String> noList = new ArrayList<>(mapList.size());
mapList.forEach(m ->
Optional.ofNullable(m.get("No")).map(Object::toString).ifPresent(noList::add));
System.out.println(String.join(",", noList));
Now that I think of it, it's shorter than the Stream version.
Answered a pretty similar question 30 minutes ago.
You are using repeated keys. This makes it look like you don't need maps, but a class with the attributes "No", "Name", etc. If you've this class you can just iterate your instances on the list and concatenating to a String.
If no matter what you want to have your maps, simply get the values of the "No" key, but note that this is a wrong practise and you should be probably using a class instead of maps:
String res = "";
for(int i = 0; i < mapList.size(); i++) {
Map<String,Object> map = mapList.get(i);
res.concat(map.get("No"));
if(i != mapList.size() - 1)
res.concat(",");
}
PS: If you are going with the bad solution practise, use the stream alternatives in the other answers if your knowledge of stream is enough to understand them.
I have two scenarios with below two domain objects :
class A{
String id;
String name;
String value;
String val1;
String val2;
}
class PropVal{
String id;
String propVal1;
String propVal2;
String propVal3;
String propVal4;
}
1) I have 4 lists
List<String> 1 = { id1,id2 ,id3}
List<String> 2 = { "one","two","three"}
Note- List 1 elements correspond to List 2 elements like id1 = "one" , id2 = "two" and so on
List<String> 3 = { "one","two","three"}
List<String> 4 = { 1,2,3}
Note- List 3 elements correspond to List 4 elements like "one" = 1 , "two" = 2 and so on
All values of these list correspond to properties of class A so more lists like above with all properties so may be cannot make map of just two above lists.
What i want is to merge these lists based on common field,ie, name ( values is List 2 & list 3) like
List<A> onjList = {[id=id1,name=one,value=1] , [id=id2,name=two,value=2] ,[id=id3,name=three,value=3]..... }
2) I have two lists
List<A> onjList - {[id=id1,name=one,value=1] , [id=id2,name=two,value=2] ,[id=id3,name=three,value=3]..... } -----obtained from Step 1
List<PropVal> list 2 = { [id=id1,propVal1=w,propVal2=x,propVal3=y,propVal4=z] , [id=id2,propVal1=a,propVal2=b,propVal3=c,propVal4=d] ....}
I want a final list like
List<A> final List = {[id=id1,name=one,value=1,val1=w ,val2=x] , [id=id2,name= two,value = 2,val1 = a ,val2 = b]..... }
note val1 = propVal1 and val2 = propVal2.
What is the best way to do both of these scenarios ? Preferably using java 8 streams and lambdas ?
Your examples are misleading. Numbers don’t make good variable names and all four lists are in the same order, but I assume, your question is supposed to imply that the first two list may have a different order than the other two, e.g.
List<String> aNames=Arrays.asList("one", "two", "three");
List<String> aIDs =Arrays.asList("id1", "id2", "id3");
List<String> bNames =Arrays.asList("two", "one", "three");
List<String> bValues=Arrays.asList("2", "1", "3");
While merging all lists in one step is possible, the repeated linear search would yield an overall quadratic time complexity, so this is discouraged. Instead, merge two associated lists into a map, allowing efficient lookup, then, merge the other two with the map:
assert bNames.size()==bValues.size();
Map<String,String> bNameToValue = IntStream.range(0, bNames.size())
.collect(HashMap::new, (m,i) -> m.put(bNames.get(i),bValues.get(i)), Map::putAll);
assert aNames.size()==aIDs.size();
List<A> list = IntStream.range(0, aNames.size())
.mapToObj(i -> new A(aIDs.get(i), aNames.get(i), bNameToValue.get(aNames.get(i))))
.collect(Collectors.toList());
The considerations for the second task are similar. If the list of PropVal elements is not in the same order, i.e. a lookup is needed, it’s recommended to have a map, which implies that it might be simpler to let the previous step generate a map in the first place.
assert bNames.size()==bValues.size();
Map<String,String> bNameToValue = IntStream.range(0, bNames.size())
.collect(HashMap::new, (m,i)->m.put(bNames.get(i),bValues.get(i)), Map::putAll);
assert aNames.size()==aIDs.size();
Map<String,A> idToA = IntStream.range(0, aNames.size())
.mapToObj(i -> new A(aIDs.get(i), aNames.get(i), bNameToValue.get(aNames.get(i))))
.collect(Collectors.toMap(A::getId, Function.identity()));
List<PropVal> list2 = …
then, if A is mutable:
list2.forEach(pVal -> {
A a = idToA.get(pVal.id);
a.setVal1(pVal.propVal1);
a.setVal2(pVal.propVal2);
a.setVal3(pVal.propVal3);
a.setVal4(pVal.propVal4);
});
List<A> finalList = new ArrayList<>(idToA.values());
or if A is immutable:
List<A> finalList = list2.stream()
.map(pVal -> {
A a = idToA.get(pVal.id);
return new A(pVal.id, a.getName(), a.getValue(),
pVal.propVal1, pVal.propVal2, pVal.propVal3, pVal.propVal4);
})
.collect(Collectors.toList());
(note that this includes only those A instances into the list, for which a PropVal exists).
Please do not abuse java 8 features! You have data design drawbacks. Provide useful methods in your classes, migrate to suitable data structures. In your model it is easy to get inconsistent data.
Do not delegate all your problems to lambdas and streams.