I have a JSON file containing data in the form:
{
"type":"type1",
"value":"value1",
"param": "param1"
}
{
"type":"type2",
"value":"value2",
"param": "param2"
}
I also have an object like this:
public class TestObject {
private final String value;
private final String param;
public TestObject(String value, String param) {
this.value = value;
this.param = param;
}
}
What I want is to create a Map<String, List<TestObject>> that contains a list of TestObjects for each type.
This is what I coded:
Map<String, List<TestObject>> result = jsonFileStream
.map(this::buildTestObject)
.collect(Collectors.groupingBy(line -> JsonPath.read(line, "$.type")));
Where the method buildTestObject is:
private TestObject buildTestObject(String line) {
return new TestObject(
JsonPath.read(line, "$.value"),
JsonPath.read(line, "$.param"));
}
This does not work because the map() function returns a TestObject, so that the collect function does not work on the JSON String line anymore.
In real life, I cannot add the "type" variable to the TestObjectfile, as it is a file from an external library.
How can I group my TestObjects by the type in the JSON file?
You can move the mapping operation to a down stream collector of groupingBy:
Map<String, List<TestObject>> result = jsonFileStream
.collect(Collectors.groupingBy(line -> JsonPath.read(line, "$.type"),
Collectors.mapping(this::buildTestObject, Collectors.toList())));
This will preserve the string so you can extract the type as a classifier, and applies the mapping to the elements of the resulting groups.
You can also use the toMap collector to accomplish the task at hand.
Map<String, List<TestObject>> resultSet = jsonFileStream
.collect(Collectors.toMap(line -> JsonPath.read(line, "$.type"),
line -> new ArrayList<>(Collections.singletonList(buildTestObject(line))),
(left, right) -> {
left.addAll(right);
return left;
}
));
In addition to the Stream solution, it's worth pointing out that Java 8 also significantly improved the Map interface, making this kind of thing
much less painful to achieve with a for loop than had previously been the case. I am not familiar with the library you are using, but something like this will work (you can always convert a Stream to an Iterable).
Map<String, List<TestObject>> map = new HashMap<>();
for (String line : lines) {
map.computeIfAbsent(JsonPath.read(line, "$.type"), k -> new ArrayList<>())
.add(buildTestObject(line));
}
Related
I have a request from tech lead to rewrite this code and replace for-loop with generic lambda. I doubt that this will lead to more simpler, more readable and maintainable code.
Is there a really a good way to do that, please?
The question is about how to transform current for-loop into the lambda function. Change of item's data structure is completely out of scope. See the loop - it is a devision of the states list while simultaneously checking value in addressType at the same index.
How to do that with lambda and will it actually simplify the code?
List<String> states = Arrays.asList(item.getState().split(","));
List<String> addressType = Arrays.asList(item.getAddressType().split(","));
List<String> mailingStates = new ArrayList<>();
List<String> physicalStates = new ArrayList<>();
for(int i = 0; i<states.size(); i++){
if(Constants.MAILING.equals(addressType.get(i))){
mailingStates.add(states.get(i));
} else {
physicalStates.add(states.get(i));
}
}
Need to say - Java 8 only
The code will be the same, so I don't know what the point would be, but it will use a lambda expression block.
List<String> states = Arrays.asList(item.getState().split(","));
List<String> addressType = Arrays.asList(item.getAddressType().split(","));
List<String> mailingStates = new ArrayList<>(), physicalStates = new ArrayList<>();
IntStream.range(0, states.size()).forEach(i -> {
if (Constants.MAILING.equals(addressType.get(i))) {
mailingStates.add(states.get(i));
} else {
physicalStates.add(states.get(i));
}
});
What you want to do is go through two lists at the same name. The generic name for this operation is "zip" - when you go through two (or sometimes more) arrays/lists/streams/etc and do something with each element.
You can pick an implementation for streams from here: Zipping streams using JDK8 with lambda (java.util.stream.Streams.zip) there are many that are already implemented in existing libraries, as well. If you already have such a library in your project, you need but an import to use it.
For illustrative purposes, I'll assume there is an implementation available with this signature:
<A, B, C> Stream<C> zip(Stream<? extends A> a,
Stream<? extends B> b,
BiFunction<? super A, ? super B, ? extends C> zipper)
Also, a good simple generic utility would be a Pair class that has two values. There are many existing implementations. I'll an implementation with this this signature is available:
class Pair<LEFT, RIGHT> {
Pair(LEFT left, RIGHT right);
LEFT getLeft();
RIGHT getRight();
}
This will hold the related state and address type. But you can also consider creating a specific object that encapsulates a given state and address type.
With these generic helpers, your code can look like this:
Stream<String> states = Arrays.stream(item.getState().split(","));
Stream<String> addressType = Arrays.stream(item.getAddressType().split(","));
Map<Boolean, List<String>> splitStates = zip(states, addressTypes,
(state, addressType) -> new Pair<String, String>(state, addressType))
.collect(
Collectors.partitioningBy(pair -> Constants.MAILING.equals(pair.getRight()),
collectors.mapping(pair -> pair.getLeft())
)
);
List<String> mailingStates = split.get(true);
List<String> physicalStates = split.get(false);
If lambdas are replaced with method references and some minor rearrangement when possible, then you get:
private static final Predicate<Pair<String, String> IS_Mailing =
pair -> Constants.MAILING.equals(pair.getRight());
/* ... */
Stream<String> states = Arrays.stream(item.getState().split(","));
Stream<String> addressType = Arrays.stream(item.getAddressType().split(","));
Map<Boolean, List<String>> splitStates = zip(states, addressTypes, Pair::new)
.collect(Collectors.partitioningBy(IS_MAILING),
collectors.mapping(Pair::getLeft()));
List<String> mailingStates = split.get(true);
List<String> physicalStates = split.get(false);
And if instead of generic Pair class you implement a class like:
class StateData {
private String state;
private String addressType;
public StateData(String state, String addressType) {
this.state = state;
this.addressType = addressType;
}
public String getState() { return this.state; }
public String getAddressType() { return this.addressType; }
public boolean isMailing() {
return Constants.MAILING.equals(this.getAddressType());
}
}
The code becomes more semantic:
Stream<String> states = Arrays.stream(item.getState().split(","));
Stream<String> addressType = Arrays.stream(item.getAddressType().split(","));
Map<Boolean, List<String>> splitStates = zip(states, addressTypes, StateData::new)
.collect(Collectors.partitioningBy(StateData::isMailing),
collectors.mapping(StateData::getState()));
List<String> mailingStates = split.get(true);
List<String> physicalStates = split.get(false);
One final consideration would be to create an enum for addressType instead of comparing to a constant.
You may use the partitioningBy to separate out items based on the Constants.MAILING.equals(addressType.get(i)) condition.
Map<Boolean, List<Integer>> map
= IntStream.range(0, states.size())
.boxed()
.collect(Collectors.partitioningBy(i -> Constants.MAILING.equals(addressType.get(i))));
List<String> mailingStates = map.get(true);
List<String> physicalStates = map.get(false);
I'm having List<InstanceWrapper>, for each element I want to do some logic that will result in some String message. Then, I want to take create Map<String, String>, where key is InstanceWrapper:ID and value is message;
private String handle(InstanceWrapper instance, String status) {
return "logic result...";
}
private Map<String, String> handleInstances(List<InstanceWrapper> instances, String status) {
return instances.stream().map(instance -> handle(instance, status))
.collect(Collectors.toMap(InstanceWrapper::getID, msg -> msg));
}
But it wont compile, I'm getting, how do I put stream().map() result into collectors.toMap() value?
The method collect(Collector<? super String,A,R>) in the type Stream<String> is not applicable for the arguments (Collector<InstanceWrapper,capture#5-of ?,Map<String,Object>>)
You cannot map before collecting to map, because then you're getting Stream of Strings and loosing information about InstanceWrapper. Stream#toMap takes two lambdas - one generating keys and second - generating values. It should be like that:
instances.stream()
.collect(Collectors.toMap(InstanceWrapper::getID, instance -> handle(instance, status));
The first lambda generates keys: InstanceWrapper::getID, the second one - associated values: instance -> handle(instance, status).
You map every InstanceWrapper to a String but if you want to use the InstanceWrapper later to extract its ID you can not do this. Try something like this instead:
return instances.stream()
.collect(Collectors.toMap(InstanceWrapper::getID, (InstanceWrapper instanceWrapper) -> this.handle(instanceWrapper, status)));
Edit:
To beautify this, you could simulate currying a little bit like this:
private Function<InstanceWrapper, String> handle(String status) {
return (InstanceWrapper instanceWrapper) -> "logic result...";
}
private Map<String, String> handleInstances(List<InstanceWrapper> instances, String status) {
return instances.stream()
.collect(Collectors.toMap(InstanceWrapper::getID, this.handle(status)));
}
I need to iterate a Map with Key and List to provide another type of object. I have tried to explain in the code level.
I have tried with for loop which works fine. But I like to have in Java8 streaming
public Map<String, List<TestClassResult>> getComputed(
Map<String, SourceClass[]> sourceMapObject) {
Map<String, List<TestClassResult>> response = new HashMap<>();
// Here goes the functionality
List<TestClassResult> result;
for (Map.Entry<String, SourceClass[]> entry : sourceMapObject.entrySet()) {
result = new ArrayList<>();
String key = entry.getKey();
for (SourceClass value : entry.getValue()) {
result.add(someMethod(value.id, value.empCode));
}
response.put(key, result);
}
return response;
}
public class SourceClass{
private String id;
private String empCode;
}
public class TestClassResult{
private String empName;
private String empMartial;
private int empAge;
}
I need this to be implemented with Java 8 streams and lambdas
sourceMapObject.entrySet()
.stream()
.collect(Collectors.toMap(
Entry::getKey,
entry -> Arrays.stream(entry.getValue())
.map(value -> someMethod(value.id, value.empCode))
.collect(Collectors.toList()),
(left, right) -> right
))
If you know for sure you will not have duplicates you can omit the (left, right) -> right part. But since in your existing code, you had response.put(key, result); I'd kept it to conform to that.
The point here is that Map::put will override the previous value that you already had in the Map, while a Collectors::toMap without a merger will throw an Exception. On the other hand with (left, right) -> right, it will behave just like the put.
I have a function like this:
private static Map<String, ResponseTimeStats> perOperationStats(List<PassedMetricData> scopedMetrics, Function<PassedMetricData, String> classifier)
{
Map<String, List<PassedMetricData>> operationToDataMap = scopedMetrics.stream()
.collect(groupingBy(classifier));
return operationToDataMap.entrySet().stream()
.collect(toMap(Map.Entry::getKey, e -> StatUtils.mergeStats(e.getValue())));
}
Is there any way to have the groupBy call do the transformation that i do explicitly in line 2 so i dont have to separately stream over the map?
Update
Here is what mergeStats() looks like:
public static ResponseTimeStats mergeStats(Collection<PassedMetricData> metricDataList)
{
ResponseTimeStats stats = new ResponseTimeStats();
metricDataList.forEach(data -> stats.merge(data.stats));
return stats;
}
If you can rewrite StatUtils.mergeStats into a Collector, you could just write
return scopedMetrics.stream().collect(groupingBy(classifier, mergeStatsCollector));
And even if you can't do this, you could write
return scopedMetrics.stream().collect(groupingBy(classifier,
collectingAndThen(toList(), StatUtils::mergeStats)));
In order to group the PassedMetricData instances, you must consume the entire Stream since, for example, the first and last PassedMetricData might be grouped into the same group.
That's why the grouping must be a terminal operation on the original Stream and you must create a new Stream in order to do the transformation on the results of this grouping.
You could chain these two statements, but it won't make much of a difference :
private static Map<String, ResponseTimeStats> perOperationStats(List<PassedMetricData> scopedMetrics, Function<PassedMetricData, String> classifier)
{
return scopedMetrics.stream()
.collect(groupingBy(classifier)).entrySet().stream()
.collect(toMap(Map.Entry::getKey, e -> StatUtils.mergeStats(e.getValue())));
}
This question already has answers here:
How to convert List to Map?
(20 answers)
Closed 7 years ago.
I would like to find a way to take the object specific routine below and abstract it into a method that you can pass a class, list, and fieldname to get back a Map.
If I could get a general pointer on the pattern used or , etc that could get me started in the right direction.
Map<String,Role> mapped_roles = new HashMap<String,Role>();
List<Role> p_roles = (List<Role>) c.list();
for (Role el : p_roles) {
mapped_roles.put(el.getName(), el);
}
to this? (Pseudo code)
Map<String,?> MapMe(Class clz, Collection list, String methodName)
Map<String,?> map = new HashMap<String,?>();
for (clz el : list) {
map.put(el.methodName(), el);
}
is it possible?
Using Guava (formerly Google Collections):
Map<String,Role> mappedRoles = Maps.uniqueIndex(yourList, Functions.toStringFunction());
Or, if you want to supply your own method that makes a String out of the object:
Map<String,Role> mappedRoles = Maps.uniqueIndex(yourList, new Function<Role,String>() {
public String apply(Role from) {
return from.getName(); // or something else
}});
Here's what I would do. I am not entirely sure if I am handling generics right, but oh well:
public <T> Map<String, T> mapMe(Collection<T> list) {
Map<String, T> map = new HashMap<String, T>();
for (T el : list) {
map.put(el.toString(), el);
}
return map;
}
Just pass a Collection to it, and have your classes implement toString() to return the name. Polymorphism will take care of it.
Java 8 streams and method references make this so easy you don't need a helper method for it.
Map<String, Foo> map = listOfFoos.stream()
.collect(Collectors.toMap(Foo::getName, Function.identity()));
If there may be duplicate keys, you can aggregate the values with the toMap overload that takes a value merge function, or you can use groupingBy to collect into a list:
//taken right from the Collectors javadoc
Map<Department, List<Employee>> byDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
As shown above, none of this is specific to String -- you can create an index on any type.
If you have a lot of objects to process and/or your indexing function is expensive, you can go parallel by using Collection.parallelStream() or stream().parallel() (they do the same thing). In that case you might use toConcurrentMap or groupingByConcurrent, as they allow the stream implementation to just blast elements into a ConcurrentMap instead of making separate maps for each thread and then merging them.
If you don't want to commit to Foo::getName (or any specific method) at the call site, you can use a Function passed in by a caller, stored in a field, etc.. Whoever actually creates the Function can still take advantage of method reference or lambda syntax.
Avoid reflection like the plague.
Unfortunately, Java's syntax for this is verbose. (A recent JDK7 proposal would make it much more consise.)
interface ToString<T> {
String toString(T obj);
}
public static <T> Map<String,T> stringIndexOf(
Iterable<T> things,
ToString<T> toString
) {
Map<String,T> map = new HashMap<String,T>();
for (T thing : things) {
map.put(toString.toString(thing), thing);
}
return map;
}
Currently call as:
Map<String,Thing> map = stringIndexOf(
things,
new ToString<Thing>() { public String toString(Thing thing) {
return thing.getSomething();
}
);
In JDK7, it may be something like:
Map<String,Thing> map = stringIndexOf(
things,
{ thing -> thing.getSomething(); }
);
(Might need a yield in there.)
Using reflection and generics:
public static <T> Map<String, T> MapMe(Class<T> clz, Collection<T> list, String methodName)
throws Exception{
Map<String, T> map = new HashMap<String, T>();
Method method = clz.getMethod(methodName);
for (T el : list){
map.put((String)method.invoke(el), el);
}
return map;
}
In your documentation, make sure you mention that the return type of the method must be a String. Otherwise, it will throw a ClassCastException when it tries to cast the return value.
If you're sure that each object in the List will have a unique index, use Guava with Jorn's suggestion of Maps.uniqueIndex.
If, on the other hand, more than one object may have the same value for the index field (which, while not true for your specific example perhaps, is true in many use cases for this sort of thing), the more general way do this indexing is to use Multimaps.index(Iterable<V> values, Function<? super V,K> keyFunction) to create an ImmutableListMultimap<K,V> that maps each key to one or more matching values.
Here's an example that uses a custom Function that creates an index on a specific property of an object:
List<Foo> foos = ...
ImmutableListMultimap<String, Foo> index = Multimaps.index(foos,
new Function<Foo, String>() {
public String apply(Foo input) {
return input.getBar();
}
});
// iterate over all Foos that have "baz" as their Bar property
for (Foo foo : index.get("baz")) { ... }