How to collect map from the Set of objects that has a list using Collectors.toMap - java-8

I have class Element with a list, my intended output is like this:
Map<String , List<Element>>
{
1 = [Element3, Element1],
2 = [Element2, Element1],
3 = [Element2, Element1], 4=[Element2]
}
And my input is set of element objects, I used forEach to get the desired outcome, but I'm looking for how to collect it using collectors.toMap. Any inputs are much appreciated
Set<Element> changes = new HashSet();
List<String> interesetList = new ArrayList();
interesetList.add("1");
interesetList.add("2");
interesetList.add("3");
Element element = new Element(interesetList);
changes.add(element);
interesetList = new ArrayList();
interesetList.add("2");
interesetList.add("3");
interesetList.add("4");
element = new Element(interesetList);
changes.add(element);
Map<String, List<Element>> collect2 = new HashMap();
changes.forEach(element -> {
element.getInterestedList().forEach(tracker -> {
collect2.compute(tracker, ( key , val) -> {
List<Element> elementList = val == null ? new ArrayList<Element>() : val;
elementList.add(Element);
return elementList;
});
});
});
class Element {
List<String> interestedList;
static AtomicInteger sequencer = new AtomicInteger(0);
String mName;
public Element(List<String> aList) {
interestedList = aList;
mName = "Element" + sequencer.incrementAndGet();
}
public List<String> getInterestedList() {
return interestedList;
}
#Override
public String toString() {
return mName;
}
}

You can do it by using Collectors.groupingBy instead of Collectors.toMap, along with Collectors.mapping, which adapts a collector to another collector:
Map<String, List<Element>> result = changes.stream()
.flatMap(e -> e.getInterestedList().stream().map(t -> Map.entry(t, e)))
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
You need to use the Stream.flatMap method first and then pair the elements of the inner lists with the current Element instance. I did this via the new Java 9's Map.entry(key, value) method. If you're not on Java 9 yet, you could change it to new AbstractMap.SimpleEntry<>(key, value).
After flatmapping, we need to collect instances of Map.Entry. So I'm using Collectors.groupingBy to classify entries by key (where we had previously stored each element of the inner lists, aka what you call tracker in your code). Then, as we don't want to have instances of List<Map.Entry<String, Element>> as the values of the map, we need to transform each Map.Entry<String, Element> of the stream to just Element (that's why I'm using Map.Entry::getValue as the first argument of Collectors.mapping). We also need to specify a downstream collector (here Collectors.toList()), so that the outer Collectors.groupingBy collector knows where to place all the adapted elements of the stream that belong to each group.
A shorter and surely more efficient way to do the same (similar to your attempt) could be:
Map<String, List<Element>> result = new HashMap<>();
changes.forEach(e ->
e.getInterestedList().forEach(t ->
result.computeIfAbsent(t, k -> new ArrayList<>()).add(e)));
This uses Map.computeIfAbsent, which is a perfect fit for your use case.

Related

The method forEach(Consumer<? super Map<String,Object>>) in the type Iterable<Map<String,Object>> is not applicable for the arguments ((<no type> k

Using Java 8 and getting the below error. I have a List<Map<String, Object>>, Map<String, Object> contains ACC_NO and true or ACC_NO and false.
Have the Map like this:
[{14125=false}, {1210=false}, {13118=false}, {10607=false}, {4973=false}, {12175=false}..............,
Multiple markers at this line
- Lambda expression's signature does not match the signature of the functional interface method accept(Map)
- The method forEach(Consumer>) in the type Iterable> is not applicable for the arguments (( k,
v) -> {})
I need to simply create another map out of it to used
// If value is false, I need to add that again
List<Map<String, Object>> excludeAccounts = new ArrayList<>();
detailsOfPans.forEach((k, v) -> {
System.out.println("Item : " + k + " Count : " + v);
Map<String, Object> myMap = new HashMap<>();
if(!v) {
excludePANs.add(myMap.put(k, v));
}
});
You are currently only iterating over the outer list instead of the maps which form the items of the list. To solve this, you would use
detailsOfPans.forEach(map -> map.forEach((k, v) -> {
// Do something with k, v
}));

Map first element of stream differently than rest

Is there a way in Java's Stream API to map first element of stream differently than other?
Equivalent of this code:
List<Bar> barList = new ArrayList<>();
for (int i=0; i<fooList.size(); i++) {
Foo foo = fooList.get(i);
Foo modifiedFoo = foo.getModifiedFoo();
if (i == 0) {
barList.add(new Bar(modifiedFoo, false));
}else {
barList.add(new Bar(modifiedFoo, true));
}
}
Stream<Bar> = barList.stream();
Note: I already have a stream setup and I would want some operation after first mapping
fooList.stream()
.map(Foo::getModifiedFoo)
.(May be Some operation here to get different new Bar for first modifiedFoo)
.map(modifiedFoo -> new Bar(modifiedFoo, true));
I would get the first element, create a Stream out of it and apply the needed mappings. Then, I'd take the rest of the list, create a stream out of it and apply the different mappings. Then concat the streams. Something like this:
Stream<Bar> first = Stream.of(fooList.get(0))
.map(Foo::getModifiedFoo)
.map(modifiedFoo -> new Bar(modifiedFoo, false));
Stream<Bar> others = fooList.subList(1, fooList.size()).stream()
.map(Foo::getModifiedFoo)
.map(modifiedFoo -> new Bar(modifiedFoo, true));
Stream<Bar> bars = Stream.concat(first, others).flatMap(s -> s);
Another approach:
Stream<Bar> bars = IntStream.range(0, fooList.size())
.mapToObj(i -> new Bar(fooList.get(i).getModifiedFoo(), i > 0));
This way is succinct and does the job pretty well.
Use an IntStream to iterate over the indices, then mapToObj to create an object for that index, and finally collect into a list:
List<Bar> barList = IntStream.range(0, fooList.size())
.mapToObj(i -> (i == 0 ? new Bar (fooList.get(i), false) :
new Bar(fooList.get(i),true)))
.collect(Collectors.toList());
What would be more readable though, is doing the first item handling outside the loop, and using IntStream starting with 1.
Here is a demo using simple lists.
I can propose two ways but I find your way straighter.
With IntStream such as :
List<Bar> barList = new ArrayList<>();
IntStream.range(0, fooList.size())
.forEach(i->{
if (i == 0) {
barList.add(new Bar(foo, false));
}else {
barList.add(new Bar(foo, true));
}
}
);
It is not a real functional approach (forEach() use and no Collector) because it maintains the current index of the List.
As alternative, you could use a more functional approach but I don't find it straighter either :
List<Bar> barList = IntStream.range(0, fooList.size())
.mapToObj(i->{
Foo foo = fooList.get(i);
if (i == 0) {
return new Bar(foo, false);
}
return new Bar(foo, true));
})
.collect(Collectors.toList());
Although I think the accepted answer is better, here is an alternate approach.
int[] counter = {-1};
Stream<Bar> barListStream = fooList.stream().map(foo -> {
counter[0]++;
return new Bar(mfoo.getModifiedFoo(), counter[0]>0);
}).collect(Collectors.toList()).stream();
You can have an object to hold a flag e.g. AtomicBoolean or AtomicInteger - that you can reset on first or nth iteration (you would need something like AtomicInteger or some Integer holder to reset on nth iteration) e.g. following code using HashMap as the holder class - will print first line of the stream differently than the other lines:
Map<String, Boolean> firstTime = new HashMap<>(Map.of("firstTime", true)); // to make the map modifiable
try (Stream<String> lines = Files.lines(Paths.get(filename), Charset.defaultCharset())) {
lines.forEachOrdered(line -> System.out.println(firstTime.remove("firstTime") != null ? ("firstTime: " + line) : line));
}
Use an AtomicBoolean initially set to true to determine when is the first item.
final AtomicBoolean first = new AtomicBoolean(true);
System.out.println("** Print all numbers 1..10");
IntStream.range(1, 11).forEach(number -> {
System.out.print((first.get() ? "" : ",") + number);
first.set(false);
});
System.out.println();

For loop optimisation with Java8

How do achieve this with one line ?
I am currently trying to do this
Example :
{{"id" :"2", values: ["10","11", "12"]} , {"id" : "3", values : ["23"]}}
to
{{"id" :"2","value": "10"},{"id": "2","value":"11"},
{"id" :"3","value":"23"} , {"id" : "2", "value":"12"}}
My java code is
Map<Integer, List<Integer>> attrMap = new HashMap<>();
//getAllData() & item.getValues() both returns List
getAllData().forEach(item - > {
item.getValues().forEach(val - > {
attrMap.computeIfAbsent(item.getId(), (k) - >
new ArrayList < > ()).add(val.getValue());
});
});
How can i do it only 1 line ?
Since the IDs are unique, you can do it like
Map<Integer, List<Integer>> attrMap = getAllData().stream()
.collect(Collectors.toMap(
item -> item.getId(),
item -> item.getValues().stream().map(i->i.getValue()).collect(Collectors.toList())));
But, of course, this will still have the performance characteristics of two nested loops. It would support parallel processing, though, but I doubt that your data will be large enough to draw a benefit from parallel processing.
Further, note that the resulting map still structurally matches your first pattern,
{{"id" :"2", values: ["10","11", "12"]} , {"id" : "3", values : ["23"]}}
you just converted item to an entry of the result Map and val to an element of a List<Integer>.
Assuming you have your input like this:
static class Data {
private final int id;
private final List<Integer> values;
public int getId() {
return id;
}
public List<Integer> getValues() {
return values;
}
public Data(int id, List<Integer> values) {
super();
this.id = id;
this.values = values;
}
}
It could be done via:
List<SimpleEntry<Integer, Integer>> result = Arrays.asList(
new Data(2, Arrays.asList(10, 11, 12)),
new Data(3, Arrays.asList(23)))
.stream()
.flatMap(d -> d.getValues().stream().map(x -> new AbstractMap.SimpleEntry<>(d.getId(), x)))
.collect(Collectors.toList());
System.out.println(result); // [2=10, 2=11, 2=12, 3=23]
I am collecting those to Pair or AbstractMap.SimpleEntry.

Java 8 sort Map<String, List<Double>>

How to sort a Map > according to average of all the values in the list using Java8 streams?
I couldn't figure out how to collect the sorted map to another instance of map. Thanks for the help in advance.
Here is the code that I tried
Map<String,List<Double>> map = new HashMap<String,List<Double>>();
LinkedList<Double> list1 = new LinkedList<Double>(Arrays.asList(12.5,45.67));
map.put("1",list1);
LinkedList<Double> list2 = new LinkedList<Double>(Arrays.asList(13.5,49.67));
map.put("2", list2);
LinkedList<Double> list3 = new LinkedList<Double>(Arrays.asList(10.5,9.67));
map.put("3", list3);
LinkedList<Double> list4 = new LinkedList<Double>(Arrays.asList(1.5,40.67));
map.put("4", list4);
map.entrySet().stream().sorted(new Comparator<Map.Entry<String, List<Double>>>() {
#Override
public int compare(Entry<String, List<Double>> arg0, Entry<String, List<Double>> arg1) {
return (int)(((LinkedList<Double>)arg1.getValue()).stream().mapToDouble(Double::doubleValue).sum() - ((LinkedList<Double>)arg0.getValue()).stream().mapToDouble(Double::doubleValue).sum());
}
});
System.out.println(map);
The output I get is still the same Map with out sorting. The expected output here is a map with the entries in this order
<"2", list2>
<"1", list1>
<"4", list4>
<"3", list3>
Edit:
Here is the solution I got to
Map<String,List<Double>> map = new HashMap<String,List<Double>>();
LinkedList<Double> list1 = new LinkedList<Double>(Arrays.asList(12.5,45.67));
map.put("1",list1);
LinkedList<Double> list2 = new LinkedList<Double>(Arrays.asList(13.5,49.67));
map.put("2", list2);
LinkedList<Double> list3 = new LinkedList<Double>(Arrays.asList(10.5,9.67));
map.put("3", list3);
LinkedList<Double> list4 = new LinkedList<Double>(Arrays.asList(1.5,40.67));
map.put("4", list4);
LinkedHashMap<String,List<Double>> orderedMap = new LinkedHashMap<String,List<Double>>();
Iterator<Entry<String, List<Double>>> iterator = map.entrySet().stream().sorted(new Comparator<Map.Entry<String, List<Double>>>() {
#Override
public int compare(Entry<String, List<Double>> arg0,
Entry<String, List<Double>> arg1) {
return (int)((((LinkedList<Double>) arg1.getValue()).stream().mapToDouble(Double::doubleValue).sum() - ((LinkedList<Double>)arg0.getValue()).stream().mapToDouble(Double::doubleValue).sum()));
}
}).iterator();
while(iterator.hasNext()){
Entry<String, List<Double>> next = iterator.next();
orderedMap.put(next.getKey(), next.getValue());
}
System.out.println(orderedMap);
I am not happy with this solution. Is there a precise and better way of doing this?
Your comparator doesn’t do the job you want, or at least not always. It works only if all the lists have the same size because you are comparing the sum not the average values for each list. I created a new Comparator based on lists average values and simplified the code. Below is my code:
Comparator<Map.Entry<String,List<Double>>> byAverange =
(Entry<String,List<Double>> o1, Entry<String,List<Double>> o2)->
{
return ((Double)o2.getValue().stream().mapToDouble(a -> a).average().getAsDouble())
.compareTo(
o1.getValue().stream().mapToDouble(a -> a).average().getAsDouble());
};
LinkedHashMap<String, List<Double>> ordered = new LinkedHashMap<>();
map.entrySet().stream().sorted(byAverange).forEach(entry -> ordered.put(entry.getKey(), entry.getValue()));
System.out.println(ordered);
output:
{2=[13.5, 49.67], 1=[12.5, 45.67], 4=[1.5, 40.67], 3=[10.5, 9.67]}
hope this can help.

How to sort String in Arraylist

I have an ArrayList where I add customers. What i wnat to do is that i want to sort them, so they appear sorted on Console.
private static ArrayList <Kund> klista = new ArrayList<>();
Kund kundd = new Kund("a","b");
System.out.print("Namn: ");
String namn = scr.next();
System.out.print("Adress: ");
String adress = scr.next();
if (!namnKontroll(namn)){
System.out.println (namn + " " +"har lagts till \n");
klista.add(kundd);
Kund k = new Kund(namn, adress);
klista.add(k);
}else{
System.out.println("Kunden med det namnet finns redan i systemet!");
}
// this is how i add customers to my ArrayList. So now, how it is possible to sort those names in ArrayList. I want to sort them with Collections. thanks
Try use Collections.sort(klista, theComparator). You will need create a Comparator, like this:
public class KundComparator implements Comparator<Kund> {
#Override
public int compare(Kund o1, Kund o2) {
// write comparison logic here
return o1.getID().compareTo(o2.getID());
}
}
Then use the Comparator:
Collections.sort(klista, new KundComparator());
If you are using Java 8, you can do like this:
Collections.sort(klista, (Kund k1, Kund k2) -> k1.getId().compareTo(k2.getId()));

Resources