Let's say my query result is like this
MyClass(name=AAA, action=action1, price=5),
MyClass(name=BBB, action=action13, price=7),
MyClass(name=AAA, action=action31, price=2)
and want to create a map grouped by name, where value will be a set of (k,v) on action and price, something like below
(AAA=((action1, 5),(action31, 2)), BBB=(action13, 7))
so been trying as below, but I'm getting error -> "non-static method cannot be referred from static content" when trying to use Map.Entry
Stream<Object> trying = results
.stream()
.flatMap(it -> {
Map<String,String> innerMap = new HashMap<>();
innerMap.put(it.getAction(),it.getPrice());
Map<String,Map<String,String>> upperMap = new HashMap<>();
upperMap.put(it.getName(), innerMap);
return upperMap.entrySet().stream();
}
)
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue,
Collectors.toList())));
You could provide a Collector along with groupingBy to produce the inner map:
(Assume static import for Collectors.groupingBy and Collectors.toMap)
results.stream()
.collect(groupingBy(MyClass::getName,
toMap(MyClass::getAction,MyClass::getPrice)));
You need to process them in a complete oops way. Here is one clean, and readable way:-
public class Builder {
public static void main(String[] args) throws IOException {
List<MyClass> list = Arrays.asList(new MyClass("AAA", "action1", 5)
, new MyClass("BBB", "action13", 7), new MyClass("AAA", "action31", 2));
Map<String, List<Pairs>> listList = new HashMap<String, List<Pairs>>();
list.stream().map(l -> {
List<Pairs> pairs = listList.getOrDefault(l.name, new ArrayList<>());
pairs.add(new Pairs(l.action, l.price));
listList.put(l.name, pairs);
return pairs;
}).collect(Collectors.toList());
System.out.println(listList);
// {AAA=[Pairs{action='action1', price=5}, Pairs{action='action31', price=2}], BBB=[Pairs{action='action13', price=7}]}
}
}
class Pairs {
String action;
int price;
public Pairs(String action, int price) {
this.action = action;
this.price = price;
}
#Override
public String toString() {
final StringBuilder sb = new StringBuilder("Pairs{");
sb.append("action='").append(action).append('\'');
sb.append(", price=").append(price);
sb.append('}');
return sb.toString();
}
}
class MyClass {
String name;
String action;
int price = 5;
public MyClass(String name, String action, int price) {
this.name = name;
this.action = action;
this.price = price;
}
}
My suggestion will be to try using a map of type Map<String, List<MyClass>>
this way it will be more manageable.
Here an idea how it can be coded.
import java.util.*;
public class MyClass {
String name;
String action;
int price;
public MyClass(String name, String action, int price) {
this.name = name;
this.action = action;
this.price = price;
}
#Override
public String toString() {
return "{" + name + ", " + action + ", " + price +"}";
}
public static void main(String[] args) {
Map<String, List<MyClass>> map = new HashMap<>();
MyClass[] objs = {
new MyClass("AAA", "action1", 5),
new MyClass("BBB", "action2", 7),
new MyClass("AAA", "action3", 2)
};
for(MyClass myClass: objs) {
if(!map.containsKey(myClass.name)) {
map.put(myClass.name, new ArrayList<MyClass>());
}
map.get(myClass.name).add(myClass);
}
System.out.println(map);
}
}
Why? Having a Map<String, Map<String, String>> is a typing disaster. Java is nominal; the language works better if you use what it's good at, which is not that.
Your problem description is ambiguous. Imagine the following input:
MyClass(name=AAA, action=action1, price=5),
MyClass(name=BBB, action=action13, price=7),
MyClass(name=AAA, action=action1, price=2)
What should the output be?
An exception
(AAA=((action1, 2)), BBB=(action13, 7))
(AAA=((action1, 5)), BBB=(action13, 7))
(AAA=((action1, [2, 5])), BBB=(action13, [7]))
That last one is somewhat sensible, because the others, particularly #2 and #3, are utterly arbitrary. Even if you want one of those, hopefully it helps you to realize that it won't be an easy, understandable series of functional operations to get there, and most likely you don't want to use streams at all - because not using them would lead to more readable code.
The signatures, in any case, do not match up with what you wrote; I assume you messed up (you seem to have set the type of the price variant as a String which seems a little crazy. In this code I'll represent it as an Integer, representing atomic units (eurocents, satoshis, etc).
If you really want this (And I highly doubt you do!), then let's break down the steps:
You first want to group by, giving you the name (AAA) as group key, and then a list of MyClass instances.
You then want to group-by again, grouping all MyClass instances with the same action name together, in order to end up with that [2, 5] integer list.
You also want to map your MyClass instance to just its price somewhere along this process.
Perusing the API of the stream stuff, specifically, Collectors, gives us the call you want:
groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)
Returns a Collector implementing a cascaded "group by" operation on input elements of type T, grouping elements according to a classification function, and then performing a reduction operation on the values associated with a given key using the specified downstream Collector.
So let's get to it, assuming you want the 4th option:
#lombok.Value public class MyClass {
String name, action;
int price;
}
List<MyClass> results = List.of(
new MyClass("AAA", "action1", 5),
new MyClass("BBB", "action2", 4),
new MyClass("AAA", "action1", 3),
new MyClass("AAA", "action3", 2));
Map<String, Map<String, List<MyClass>>> answer =
results.stream()
.collect(Collectors.groupingBy(MyClass::getName,
Collectors.groupingBy(MyClass::getAction)));
System.out.println(answer);
That's a good first step, but instead of a list of MyClass instances, you wanted just a List of integers. We can do that, too:
Map<String, Map<String, List<Integer>>> answer =
results.stream()
.collect(Collectors.groupingBy(MyClass::getName,
Collectors.groupingBy(MyClass::getAction,
Collectors.collectingAndThen(Collectors.toList(),
listOfMc -> listOfMc.stream().map(MyClass::getPrice).collect(Collectors.toList())
))));
And this gets you:
{AAA={action1=[5, 3], action3=[2]}, BBB={action2=[4]}}
Exactly what you wanted, I think.
But, then look back at that code and realize that somewhere, somehow, you made some wrong choices :P - that's quite a far sight removed from easily readable code!
Related
I am reading data from an excel file using apache poi and transforming it into a list of object. But now I want to extract any duplicates based on certain rules into another list of that object and also get the non-duplicate list.
Condition to check for a duplicate
name
email
phone number
gst number
Any of these properties can result in a duplicate. which mean or not an and
Party Class
public class Party {
private String name;
private Long number;
private String email;
private String address;
private BigDecimal openingBalance;
private LocalDateTime openingDate;
private String gstNumber;
// Getter Setter Skipped
}
Let's say this is the list returned by the logic to excel data so far
var firstParty = new Party();
firstParty.setName("Valid Party");
firstParty.setAddress("Valid");
firstParty.setEmail("Valid");
firstParty.setGstNumber("Valid");
firstParty.setNumber(1234567890L);
firstParty.setOpeningBalance(BigDecimal.ZERO);
firstParty.setOpeningDate(DateUtil.getDDMMDateFromString("01/01/2020"));
var secondParty = new Party();
secondParty.setName("Valid Party");
secondParty.setAddress("Valid Address");
secondParty.setEmail("Valid Email");
secondParty.setGstNumber("Valid GST");
secondParty.setNumber(7593612247L);
secondParty.setOpeningBalance(BigDecimal.ZERO);
secondParty.setOpeningDate(DateUtil.getDDMMDateFromString("01/01/2020"));
var thirdParty = new Party();
thirdParty.setName("Valid Party 1");
thirdParty.setAddress("address");
thirdParty.setEmail("email");
thirdParty.setGstNumber("gst");
thirdParty.setNumber(7593612888L);
thirdParty.setOpeningBalance(BigDecimal.ZERO);
secondParty.setOpeningDate(DateUtil.getDDMMDateFromString("01/01/2020"));
var validParties = List.of(firstParty, secondParty, thirdParty);
What I have attempted so far :-
var partyNameOccurrenceMap = validParties.parallelStream()
.map(Party::getName)
.collect(Collectors.groupingBy(Function.identity(), HashMap::new, Collectors.counting()));
var partyNameOccurrenceMapCopy = SerializationUtils.clone(partyNameOccurrenceMap);
var duplicateParties = validParties.stream()
.filter(party-> {
var occurrence = partyNameOccurrenceMap.get(party.getName());
if (occurrence > 1) {
partyNameOccurrenceMap.put(party.getName(), occurrence - 1);
return true;
}
return false;
})
.toList();
var nonDuplicateParties = validParties.stream()
.filter(party -> {
var occurrence = partyNameOccurrenceMapCopy.get(party.getName());
if (occurrence > 1) {
partyNameOccurrenceMapCopy.put(party.getName(), occurrence - 1);
return false;
}
return true;
})
.toList();
The above code only checks for party name but we also need to check for email, phone number and gst number.
The code written above works just fine but the readability, conciseness and the performance might be an issue as the data set is large enough like 10k rows in excel file
Never ignore Equals/hashCode contract
name, email, number, gstNumber
Any of these properties can result in a duplicate, which mean or
Your definition of a duplicate implies that any of these properties should match, whilst others might not.
It means that it's impossible to provide an implementation equals/hashCode that would match the given definition and doesn't violate the hashCode contract.
If two objects are equal according to the equals method, then calling the hashCode method on each of the two objects must produce the same integer result.
I.e. if you implement equals in such a way they any (not all) of these properties: name, email, number, gstNumber could match, and that would enough to consider the two objects equal, then there's no way to implement hashCode correctly.
And as the consequence of this, you can't use the object with a broken equals/hashCode implementation in with a hash-based Collection because equal objects might end up the in the different bucket (since they can produce different hashes). I.e. HashMap would not be able to recognize the duplicated keys, hence groupingBy with groupingBy() with Function.identity() as a classifier function would not work properly.
Therefore, to address this problem, you need to implement equals() based on all 4 properties: name, email, number, gstNumber (i.e. all these values have to be equal), and similarly all these values must contribute to hash-code.
How to determine Duplicates
There's no easy way to determine duplicates by multiple criteria. The solution you've provided is not viable, since we can't rely on the equals/hashCode.
The only way is to generate a HashMap separately for each end every attribute (i.e. in this case we need 4 maps). But can we alternate this, avoiding repeating the same steps for each map and hard coding the logic?
Yes, we can.
We can create a custom generic accumulation type (it would be suitable for any class - no hard-coded logic) that would encapsulate all the logic of determining duplicates and maintain an arbitrary number of maps under the hood. After consuming all the elements from the given collection, this custom object would be aware of all the duplicates in it.
That's how it can be implemented.
A custom accumulation type that would be used as container of a custom Collector. Its constructor expects varargs of functions, each function correspond to the property that should be taken into account while checking whether an object is a duplicate.
public static class DuplicateChecker<T> implements Consumer<T> {
private List<DuplicateHandler<T>> handles;
private Set<T> duplicates;
#SafeVarargs
public DuplicateChecker(Function<T, ?>... keyExtractors) {
this.handles = Arrays.stream(keyExtractors)
.map(DuplicateHandler::new)
.toList();
}
#Override
public void accept(T t) {
handles.forEach(h -> h.accept(t));
}
public DuplicateChecker<T> merge(DuplicateChecker<T> other) {
for (DuplicateHandler<T> handler: handles) {
other.handles.forEach(handler::merge);
}
return this;
}
public DuplicateChecker<T> finish() {
duplicates = handles.stream()
.flatMap(handler -> handler.getDuplicates().stream())
.flatMap(Set::stream)
.collect(Collectors.toSet());
return this;
}
public boolean isDuplicate(T t) {
return duplicates.contains(t);
}
}
A helper class representing a single createrion (like name, email, etc.) which encapsulates a HashMap. keyExtractor is used to obtain a key from an object of type T.
public static class DuplicateHandler<T> implements Consumer<T> {
private Map<Object, Set<T>> itemByKey = new HashMap<>();
private Function<T, ?> keyExtractor;
public DuplicateHandler(Function<T, ?> keyExtractor) {
this.keyExtractor = keyExtractor;
}
#Override
public void accept(T t) {
itemByKey.computeIfAbsent(keyExtractor.apply(t), k -> new HashSet<>()).add(t);
}
public void merge(DuplicateHandler<T> other) {
other.itemByKey.forEach((k, v) ->
itemByKey.merge(k,v,(oldV, newV) -> { oldV.addAll(newV); return oldV; }));
}
public Collection<Set<T>> getDuplicates() {
Collection<Set<T>> duplicates = itemByKey.values();
duplicates.removeIf(set -> set.size() == 1); // the object is proved to be unique by this particular property
return duplicates;
}
}
And that is the method, responsible for generating the map of duplicates, that would be used from the clean code. The given collection would be partitioned into two parts: one mapped to the key true - duplicates, another mapped to the key false - unique objects.
public static <T> Map<Boolean, List<T>> getPartitionByProperties(Collection<T> parties,
Function<T, ?>... keyExtractors) {
DuplicateChecker<T> duplicateChecker = parties.stream()
.collect(Collector.of(
() -> new DuplicateChecker<>(keyExtractors),
DuplicateChecker::accept,
DuplicateChecker::merge,
DuplicateChecker::finish
));
return parties.stream()
.collect(Collectors.partitioningBy(duplicateChecker::isDuplicate));
}
And that how you can apply it for your particular case.
main()
public static void main(String[] args) {
List<Party> parties = // initializing the list of parties
Map<Boolean, List<Party>> isDuplicate = partitionByProperties(parties,
Party::getName, Party::getNumber,
Party::getEmail, Party::getGstNumber);
}
I would use create a map for each property where
key is the property we want to check duplicate
value is a Set containing all the index of element in the list with same key.
Then we can
filter values in the map with more that 1 index (i.e. duplicate indexes).
union all the duplicate index
determine if the element is duplicate/unique by using the duplicate index.
The time complexity is roughly O(n).
public class UniquePerEachProperty {
private static void separate(List<Party> partyList) {
Map<String, Set<Integer>> nameToIndexesMap = new HashMap<>();
Map<String, Set<Integer>> emailToIndexesMap = new HashMap<>();
Map<Long, Set<Integer>> numberToIndexesMap = new HashMap<>();
Map<String, Set<Integer>> gstNumberToIndexesMap = new HashMap<>();
for (int i = 0; i < partyList.size(); i++) {
Party party = partyList.get(i);
nameToIndexesMap.putIfAbsent(party.getName(), new HashSet<>());
nameToIndexesMap.get(party.getName()).add(i);
emailToIndexesMap.putIfAbsent(party.getEmail(), new HashSet<>());
emailToIndexesMap.get(party.getEmail()).add(i);
numberToIndexesMap.putIfAbsent(party.getNumber(), new HashSet<>());
numberToIndexesMap.get(party.getNumber()).add(i);
gstNumberToIndexesMap.putIfAbsent(party.getGstNumber(), new HashSet<>());
gstNumberToIndexesMap.get(party.getGstNumber()).add(i);
}
Set<Integer> duplicatedIndexes = Stream.of(
nameToIndexesMap.values(),
emailToIndexesMap.values(),
numberToIndexesMap.values(),
gstNumberToIndexesMap.values()
).flatMap(Collection::stream).filter(indexes -> indexes.size() > 1)
.flatMap(Set::stream).collect(Collectors.toSet());
List<Party> duplicatedList = new ArrayList<>();
List<Party> uniqueList = new ArrayList<>();
for (int i = 0; i < partyList.size(); i++) {
Party party = partyList.get(i);
if (duplicatedIndexes.contains(i)) {
duplicatedList.add(party);
} else {
uniqueList.add(party);
}
}
System.out.println("duplicated:" + duplicatedList);
System.out.println("unique:" + uniqueList);
}
public static void main(String[] args) {
separate(List.of(
// name duplicate
new Party("name1", 1L, "email1", "gstNumber1"),
new Party("name1", 2L, "email2", "gstNumber2"),
// number duplicate
new Party("name3", 3L, "email3", "gstNumber3"),
new Party("name4", 3L, "email4", "gstNumber4"),
// email duplicate
new Party("name5", 5L, "email5", "gstNumber5"),
new Party("name6", 6L, "email5", "gstNumber6"),
// gstNumber duplicate
new Party("name7", 7L, "email7", "gstNumber7"),
new Party("name8", 8L, "email8", "gstNumber7"),
// unique
new Party("name9", 9L, "email9", "gstNumber9")
));
}
}
Assume Party has below constructor and toString()(for testing)
public class Party {
public Party(String name, Long number, String email, String gstNumber) {
this.name = name;
this.number = number;
this.email = email;
this.address = "";
this.openingBalance = BigDecimal.ZERO;
this.openingDate = LocalDateTime.MIN;
this.gstNumber = gstNumber;
}
#Override
public String toString() {
return "Party{" +
"name='" + name + '\'' +
", number=" + number +
", email='" + email + '\'' +
", gstNumber='" + gstNumber + '\'' +
'}';
}
...
}
I'm trying to group by JAVA array list elements.
I have an array of JSONObjects like the following, I want to group by the fields price and side and then sum the shares with new fileds buyShares and sellShares
[{"shares":20,"side":"B","orderId":"001","price":"500"},
{"shares":20,"side":"S","orderId":"002","price":"501"},
{"shares":25,"side":"B","orderId":"003","price":"500"},
{"shares":10,"side":"S","orderId":"004","price":"501"},
{"shares":30,"side":"B","orderId":"005","price":"505"},
{"shares":35,"side":"B","orderId":"006","price":"505"},
{"shares":35,"side":"S","orderId":"007","price":"500"}]
and I want to group by price and by side to have something like the following :
[{"price":"500","buyShares":45, "sellShares":35}, {"sellShares":30,"price":"501"}, {"price":"505","buyShares":65}]
I'm using the following java code :
ArrayList<JSONObject> aOrdersArray = new ArrayList<>(aOrders.values());
System.out.println(aOrdersArray);
List<JSONObject> test = aOrdersArray.stream()
.distinct()
.collect(Collectors.groupingBy(jsonObject -> jsonObject.getInt("price")))
.entrySet().stream()
.map(e -> e.getValue().stream()
.reduce((f1,f2) -> {
JSONObject h = new JSONObject();
h.put("price",f1.get("price"));
System.out.println(f1);
if (f1.get("side").equals("B") && f2.get("side").equals("B")) {
h.put("buyShares", f1.getInt("shares") + f2.getInt("shares"));
}
else if (f1.get("side").equals("S") && f2.get("side").equals("S")) {
h.put("sellShares", f1.getInt("shares") + f2.getInt("shares"));
}
else if (f1.get("side").equals("S")) {
h.put("sellShares", f1.get("shares"));
}
else if (f1.get("side").equals("B")) {
h.put("buyShares",f1.get("shares"));
}
else if (f2.get("side").equals("S")) {
h.put("sellShares", f2.get("shares"));
}
else if (f2.get("side").equals("B")) {
h.put("buyShares",f2.get("shares"));
}
return h;
}))
.map(f -> f.get())
.collect(Collectors.toList());
System.out.println(test);
and I get sometimes the following error
Exception in thread "main" org.json.JSONException: JSONObject["side"] not found.
What you're trying to do is compicated and errorprone because you've started on the wrong foot.
You shouldn't be having a JSONObject in the first place. The data you do have in your JSON input is regular, and you want to operate on its contents.
The right way to start is to make a java object that represents such a record, and then turn your JSON input into a proper, idiomatic java version of that. You want:
#Value class Record {
int shares;
RecordKind kind;
int orderId;
int price;
public PriceGroup toGroup() {
return new PriceGroup(price,
kind == BUY ? shares : 0,
kind == SELL ? shares : 0);
}
}
#Value class PriceGroup {
int price;
int buyShares;
int sellShares;
public PriceGroup merge(PriceGroup other) {
if (other.price != this.price) throw new IllegalArgumentException();
return new PriceGroup(price,
this.buyShares + other.buyShares,
this.sellShares + other.sellShares);
}
}
NB: Uses Lombok's #Value. Assume this thing has a constructor, all fields are final, toString and equals and hashCode are in their right place, etc.
Once you have the above two types, then you can convert your input JSON into a List<Record>. Armed with this List<Record>, then and only then should you start down the path of mapping, grouping, etc.
That error will then, of course, never occur, and your map/group code will be significantly easier to read. Any typos you make will result in compile time errors. auto complete will work fine, etcetera: All the advantages.
To turn JSON into a given type, use Jackson. If you somehow just don't want to add that dependency (you should, really), then write a public static Record fromJson(JSONObject) method in your Record class, and use .map(Record::fromJson) to get to a stream of Record objects right away, and never go back to JSONObject.
List<Record> orders = ...;
List<PriceGroup> test = orders.stream()
.distinct()
.map(Record::toGroup)
.collect(Collectors.groupingBy(PriceGroup::getPrice))
.entrySet().stream()
.map(e -> e.getValue().stream()
.reduce((f1, f2) -> f1.merge(f2))
.collect(Collectors.toList());
Your code is now vastly simpler to read, all methods have proper names (it is getBuyShares(), not .getInt("buyShares")) and are discoverable via e.g. auto-complete, and you can for example individually test your merge functionality.
To use gson you can create a class :
public class Share{
private int shares;
private String side;
private String orderId;
private String price;
//getters and setters
public int getShares() {
return shares;
}
public void setShares(int shares) {
this.shares = shares;
}
public String getSide() {
return side;
}
public void setSide(String side) {
this.side = side;
}
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String getPrice() {
return price;
}
public void setPrice(String price) {
this.price = price;
}
}
then you can use this one to map object from json:
String yourJson = "[{\"shares\":20,\"side\":\"B\",\"orderId\":\"001\",\"price\":\"500\"},{\"shares\":35,\"side\":\"S\",\"orderId\":\"007\",\"price\":\"500\"}]";
Gson gson = new Gson();
Type shareListType = new TypeToken<ArrayList<Share>>(){}.getType();
ArrayList<Share> userArray = gson.fromJson(yourJson, shareListType);
Whilst everybody is suggesting to use object mapped values to simplify your calculations, it is not a must per se. The reasons I can think of are:
you need to create a mapping for every reasonable type of JSON node (it can be automated to some extent though) -- this is totally fine if your mappings are widely used in your codebase, but probably (!) not worth doing it for ad-hoc one-time-in-use stuff;
manipulating a JSON tree directly guarantees that bindings (de/serialization) do not affect the original JSON tree structure (i.e. in Gson (and most likely in Jackson) it is possible to implement a type adapter that does not guarantee a perfect round-trip for data mappings: in-JSON -> mappings -> out-JSON => in-JSON != out-JSON; the same for mappings to JSON and back);
some JSON libraries (just like yours?) do not provide the object mapping/binding feature at all.
#SuppressWarnings("unchecked")
final Iterable<JSONObject> input = (Iterable<JSONObject>) new JSONTokener(inputReader).nextValue();
final JSONArray actual = StreamSupport.stream(input.spliterator(), false)
.collect(Collectors.groupingBy(jsonObject -> jsonObject.getString("price")))
.entrySet()
.stream()
.map(entry -> entry.getValue()
.stream()
.collect(Collector.of(
() -> {
final JSONObject jsonObject = new JSONObject();
jsonObject.put("price", entry.getKey());
return jsonObject;
},
(a, e) -> {
final String side = e.getString("side");
final String key;
final BigDecimal shares;
switch ( side ) {
case "B":
key = "buyShares";
shares = e.getBigDecimal("shares");
break;
case "S":
key = "sellShares";
shares = e.getBigDecimal("shares");
break;
default:
throw new AssertionError(side);
}
if ( !a.has(key) ) {
a.put(key, shares);
} else {
a.put(key, a.getBigDecimal(key).add(shares));
}
},
(a1, a2) -> {
throw new UnsupportedOperationException();
},
Function.identity()
))
)
.collect(Collector.of(
JSONArray::new,
JSONArray::put,
(a1, a2) -> {
throw new UnsupportedOperationException();
},
Function.identity()
));
final JSONArray expected = (JSONArray) new JSONTokener(expectedReader).nextValue();
Assertions.assertEquals(expected.toString(), actual.toString()); // why? because neither JSONArray nor JSONObject implement equals/hashCode
Given a list of objects that need to be sorted and grouped:
static class Widget {
// ...
public String getCode() { return widgetCode; }
public String getName() { return widgetName; }
}
List<Widget> widgetList = Arrays.asList(
// several widgets with codes and names
);
I want to group the list into a list-of-lists, grouped by widgetCode, with the elements of each sub-list in the order they were encountered in the original list. I know that I can group them into a Map of lists using the groupingBy Collector:
Map<String,List<Widget>> widgetMap = widgetList.stream()
.collect(groupingBy(Widget::getCode));
I do not take for granted that the keys are sorted, so I've taken the extra step of loading the whole thing into a SortedMap type:
SortedMap<String,List<Widget>> sortedWidgetMap = new TreeMap<String,List<Widget>>(
widgetList.stream()
.collect(groupingBy(Widget::getCode))
);
I know I can get a Collection from sortedWidgetMap by using .values(), and I guess it is an ordered collection because it comes from an ordered map type, so that's my current solution:
Collection<List<Widget>> widgetListList = new TreeMap<String,List<Widget>>(
widgetList.stream()
.collect(groupingBy(Widget::getCode))
).values();
widgetListList.forEach(System.out::println); // do something with the data
This works so far, but I'm not confident that the resulting widgetListList is actually guaranteed to be in the right order (i.e. by widgetCode) or that the sub-lists will be built in the order they were found in the original list. Also, I think it must be possible to use the Stream API alone to achieve the output I want. So, how can I do this better?
As mentioned in a comment, referring to a question that is very similar (in fact, I nearly considered it to be a duplicate...), the groupBy call comes in different flavors, and one of them allows passing in a factory for the map that is to be created.
So there is no need to explicitly wrap the result of the "simple" groupBy call into the creation of a new TreeMap, because you can create the TreeMap directly. This map (and its values() collection!) will be ordered by the key. The values of the map are lists, which are created using the downstream collector toList(), which explicitly says that it will collect the results in encounter order.
So the following should indeed be a simple, correct and efficient solution:
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class CollectToListOfList {
static class Widget {
String code;
String name;
Widget(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
#Override
public String toString() {
return code + ": " + name;
}
}
public static void main(String[] args) {
List<Widget> widgetList = Arrays.asList(
new Widget("0", "A"),
new Widget("1", "B"),
new Widget("2", "C"),
new Widget("3", "D"),
new Widget("0", "E"),
new Widget("1", "F"),
new Widget("2", "G"),
new Widget("3", "H"),
new Widget("0", "I"),
new Widget("1", "J"));
Collection<List<Widget>> result = widgetList.stream()
.collect(Collectors.groupingBy(Widget::getCode, TreeMap::new, Collectors.toList()))
.values();
for (List<Widget> list : result) {
System.out.println(list);
}
}
}
Edited to correct previous post based on clarification. Only difference between answer(s) by others is that the values() result is fed into an ArrayList constructor to create a List of Lists.
// Create some data
Random r = new Random(29);
String codes = "ABCD";
List<Widget> widgetList = r.ints(10, 0, 4).mapToObj(
n -> new Widget(codes.substring(n, n + 1), "N" + i++)).collect(
Collectors.toList());
// Now create the list of lists.
List<List<Widget>> listofWidgets = new ArrayList<>(
widgetList.stream().collect(Collectors.groupingBy(Widget::getCode,
TreeMap::new,
Collectors.toList())).values());
// Display them
for (List<?> list : listofWidgets) {
System.out.println(list);
}
Widget class
class Widget {
String widgetCode;
String widgetName;
public Widget(String wc, String wn) {
widgetCode = wc;
widgetName = wn;
}
public String getCode() {
return widgetCode;
}
public String getName() {
return widgetName;
}
public String toString() {
return "{" + widgetCode + ":" + widgetName + "}";
}
}
I had a list as below :
List<Address> address;
where
Address:
city
country
state
I want to convert it into something like below
Map <String,String> convertedMap=list.stream().collect
(Collectors.toMap(Address:getCity+Address:getCountry ,Address:getState));
I want to keep all duplicate key and values in the generated map as below .
(key=City1country1, value= state1) ,(key=City1country1, value=state2),(key=City1country1,value= state1) ;
As mentioned in the comment, maps do not store duplicated keys so you have to use Map<String, List<String>> instead Map<String, String>.
To sum up, you just have to use Collectors.toMap method with mergeFunction parameter where you can handle duplicated keys. This operation will be called every time when same keys appears. In this case, we simply merge two lists to one. Have a look at below code (compiled with JDK 11), I believe it does exactly what you need and prints an expected result (using List<String> of course).
import java.util.*;
import java.util.stream.Collectors;
public class ListToMapWithDuplicatedKeysSample {
public static void main(String[] args) {
List<Address> addresses = List.of(
new Address("City1", "country1", "state1"),
new Address("City1", "country1", "state2"),
new Address("City1", "country1", "state1"),
new Address("City3", "country3", "state3")
);
Map<String, List<String>> result = addresses.stream()
.collect(
Collectors.toMap(
address -> address.getCity() + address.getCountry(),
address -> Collections.singletonList(address.getState()),
ListToMapWithDuplicatedKeysSample::mergeEntriesWithDuplicatedKeys
)
);
System.out.println(result);
}
private static List<String> mergeEntriesWithDuplicatedKeys(List<String> existingResults, List<String> newResults) {
List<String> mergedResults = new ArrayList<>();
mergedResults.addAll(existingResults);
mergedResults.addAll(newResults);
return mergedResults;
}
private static class Address {
private final String city;
private final String country;
private final String state;
public Address(String city, String country, String state) {
this.city = city;
this.country = country;
this.state = state;
}
String getState() {
return state;
}
String getCountry() {
return country;
}
String getCity() {
return city;
}
}
}
You can’t use Collectors.toMap, because it requires unique keys. What you want instead is groupingBy, which returns collected values for each key:
class Location {
public final String city;
public final String country;
Location(Address address) {
this.city = address.getCity();
this.country = address.getCountry();
}
#Override
public boolean equals(Object obj) {
if (obj instanceof Location) {
Location other = (Location) obj;
return Objects.equals(this.city, other.city) &&
Objects.equals(this.country, other.country);
}
return false;
}
#Override
public int hashCode() {
return Objects.hash(city, country);
}
}
Map<Location, Set<String>> statesByLocation = addresses.stream().collect(
Collectors.groupingBy(Location::new,
Collectors.mapping(Address::getState, Collectors.toSet())));
You could employ various hacks for combining city and country (like a list of two Strings), but you really should just make a key class as shown above. The code will be far easier to work with, especially if you (or another developer) should have a reason to come back to it in six months.
As for the Collector methods:
Collectors.groupingBy(Location::new is equivalent to address -> new Location(address) and creates each key in the Map.
Collectors.mapping(Address::getState, Collectors.toSet()) means that each Address which corresponds to a particular key will have getState() invoked on it, and the resulting strings will be aggregated in a Set to form the Map value.
Personally, I think I would opt for a simple for-loop instead of the hard-to-read Stream approach:
Map<Location, Set<String>> statesByLocation = new HashMap<>();
for (Address address : addresses) {
statesByLocation.computeIfAbsent(new Location(address),
k -> new HashSet<String>()).add(address.getState());
}
This type of thing is a common requirement for lots of coding challenges, here is a quick way to accomplish what you need if you have time constraints, I chose to use a set rather than a list for the lookup time to be O(1) rather than O(N):
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
class MapExample {
public static void main(String[] args) {
List<String> addressesList = new ArrayList<>();
addressesList.add("Portland USA ME");
addressesList.add("Portland USA OR");
addressesList.add("Boston USA MA");
System.out.println(cityToStatesMap(addressesList));
}
private static Map<String, Set<String>> cityToStatesMap(List<String> addressesList) {
Map<String, Set<String>> result = new HashMap<>();
if (addressesList == null || addressesList.size() == 0) {
return result;
}
for (String address : addressesList) {
String[] addressComponents = address.split(" ");
String city = addressComponents[0];
String country = addressComponents[1];
String state = addressComponents[2];
String key = city + country;
Set<String> states = result.computeIfAbsent(key, k -> new HashSet<>());
states.add(state);
result.put(key, states);
}
return result;
}
}
Output:
{PortlandUSA=[ME, OR], BostonUSA=[MA]}
Note: I think you should use something like a , for a delimiter rather than a space so it will be easier to deal with cities with multiple words e.g. New York, San Francisco etc.
use the toMap with the following method signature
toMap(Function k, Function v,BinaryOperator m)
BinaryOperator informs the JVM on what to do if it encounters duplicates keys. In your case if it encounters duplicates it is suppose to concat them.
thus s1 +","+s2
hence your solution becomes
Map<String, String> map = list.stream().
collect(Collectors.toMap((address -> {
String y = address.getCity() + address.getCountry();
return y + s;
}), Address::getState, (s1, s2) -> s1 + "," + s2));
output
country3City3 state3
country1City1 state1,state2,state1
Process finished with exit code 0
I have a List which can be null;
List<T> list; // may or may not null
I want to process for each element with a consumer.
So far, I do.
ofNullable(list)
.map(List::stream)
.ifPresent(stream -> stream.forEach(e -> {}));
or
ofNullable(eventDataList).ifPresent(v -> v.forEach(e -> {}));
Is there any easy or concise way to do this?
To avoid ugly null checking, use orElse(Collections.emptyList())
Optional.ofNullable(eventDataList)
.orElse(Collections.emptyList())
.forEach(e -> {});
With static imports, it's pretty concise:
ofNullable(eventDataList).orElse(emptyList()).forEach(e -> {});
Technically, if (list != null) { list.stream().forEach(e -> {...}); } is both shorter and more efficient in terms of CPU/memory usage than your variants.
Architecturally, if you have control over initialization of the list and its usage, it's often better to use either Collections.emptyList() instead of null (if the logic of your program allows) or make the list Optional from the very beginning. That would save you from necessity to make checks or create Optionals every time you want to use the list.
If you require to do something with every value in the list and say return a value then ifPresent will not work. Rather you can do something like below. In my example the optional list contains a user defined object Person which has a few attributes. I am iterating over the list and concatenating the values of a specific attribute and returning it.
public static class Person
{
String name;
int age;
public Person(final String name, final int age)
{
this.name = name;
this.age = age;
}
public String getName()
{
return name;
}
public int getAge()
{
return age;
}
}
public static void main(String[] args)
{
Person a = new Person("Alice", 1);
Person b = new Person("Bob", 2);
List<Person> personList = Lists.newArrayList(a, b);
String concatNames = Optional.of(personList).map(people -> people.stream().map(Person::getName).collect(Collectors.joining(" "))).orElse(null);
System.out.println("Names: "+concatNames);
}
I'm not sure that you can make it more concise. However, if you are frequently using the construct of looping over a nullable list and consuming each element, you could make a small class which does just that:
public class ListConsumer {
public static <H> Consumer<List<H>> of(Consumer<H> consumer) {
return hs -> hs.forEach(consumer);
}
}
You can then consume each element in a list as follows (e.g. print all Strings in list):
List<String> list = Arrays.asList("A", "B", "C");
Consumer<String> consumer = System.out::println;
Optional.ofNullable(list).ifPresent(ListConsumer.of(consumer));