Let's say I have this code:
Map<String, String> map;
// later on
map.entrySet().stream().map(MyObject::new).collect(Collectors.toList());
And I have a MyObject Constructor which takes two arguments of type String.
I want to be able to do this but I cannot.
I know I can do e -> new MyObject(e.getKey(), e.getValue()) but prefer MyObject::new.
Similar code works for Set<String> and List<String> with one argument constructor of MyObject class.
use a lambda:
map.entrySet()
.stream()
.map(e -> new MyObject(e.getKey(), e.getValue()))
.collect(Collectors.toList());
otherwise the only way to use a method reference is by creating a function as such:
private static MyObject apply(Map.Entry<String, String> e) {
return new MyObject(e.getKey(), e.getValue());
}
then do something like:
map.entrySet()
.stream()
.map(Main::apply)
.collect(Collectors.toList());
Where Main is the class containing the apply method.
map.entrySet().stream().map(MyObject::new).collect(Collectors.toList()));
And I have a MyObject Constructor which takes two arguments of type String. I want to be able to do this but I cannot.
In map.entrySet().stream().map(...), Java is expecting a Function,
mapping one value to another value.
One value.
From the stream, the function receives a value of type Map.Entry<String, String>,
but your constructor takes two String arguments.
Java doesn't automagically expand a Map.Entry<String, String> to a pair of Strings to pass to your constructor.
You have to do that unwrapping manually.
The problem with the constructor is that it defines two parameters, while Function#apply demanded by Stream#map accepts only one.
You could write a static factory method for MyObject
class MyObject {
public static MyObject of(Map.Entry<String, String> e) {
return new MyObject(e.getKey(), e.getValue());
}
}
and refer to it like
map(MyObject::of)
Before you do so, ask yourself if one pretty-looking line in a plain processing chain somewhere is worthy of a new constructor or utility method.
Add a Map.Entry constructor
class MyObject {
private final String k;
private final String v;
MyObject(String s1, String s2) {
k = s1;
v = s2;
}
MyObject(Map.Entry<String, String> e) {
this(e.getKey(), e.getValue());
}
public String toString() {
return "key: " + k + ' ' + "value: " + v;
}
}
You will be able to call
List<MyObject> myObjectList = map.entrySet().stream().map(MyObject::new).collect(Collectors.toList());
Related
I have been searching around different groupBy and stream threads but cannot find the answer to my problem. Basically I have this object:
public class Object {
string name;
string type;
}
And I return a list of these from the database. What I would like to then do is iterate through the list of objects and remove duplicate names and save a list of the second properties under one object, in a new object that looks like this:
public class NewObject {
String name;
List<String> types;
}
You can do it like this.
use groupingBy to create a map of name, List of type
use the entryset of the map to create the new object.
I added the appropriate constructors and getters in the classes.
List<OldObject> list = ...
List<NewObject> newList = list.stream()
.collect(Collectors.groupingBy(OldObject::getName,
Collectors.mapping(OldObject::getType,
Collectors.toList())))
.entrySet().stream()
.map(e -> new NewObject(e.getKey(), e.getValue()))
.toList();
}
class OldObject {
String name;
String type;
public String getName() {
return name;
}
public String getType() {
return type;
}
}
class NewObject {
String name;
List<String> types = new ArrayList<>();
public NewObject(String name, List<String> types) {
this.types = types;
this.name = name;
}
}
For your added enjoyment, I thought I would also offer the following:
create the map
conditionally create a new object if the name key is not present
in either case, add the type to the list instance of the NewObject instance in the map which is returned by the computeIfAbsent method.
When finished, just assign the values to your Collection.
Map<String, NewObject> map = new HashMap<>();
for (OldObject ob : list) {
map.computeIfAbsent(ob.getName(),
v -> new NewObject(ob.getName(),
new ArrayList<>()))
.add(ob.getType());
}
Collection<NewObject> newLista = map.values();
Caveats:
values returns a Collection, not a list so you would need to use that or pass the Collection to a list constructor of some sort (e.g. ArrayList).
the requires the addition of a add method in the NewObject class.
you could also have a getter that returns the type list directly and do.
map.computeIfAbsent(ob.getName(),
v -> new NewObject(ob.getName(),
new ArrayList<>()))
.getTypeList().add(ob.getType());
Check out these additions to the Map interface
I understand you don't want to remove duplicates but actually merge them.
I replaced your Object class name with OldObject to avoid confusion with the actual Java Object class.
Collecting to Map<String, List<String>> and then converting to List<NewObject>
You could write your own Collector (see later in the answer), but the easiest way of writing what you need would be to use the current groupingBy and toList Collectors and then using the resulting Map and create your newObject instance based on it to then again collect to a new List:
oldObjectList.stream().collect(Collectors.groupingBy(OldObject::getName, Collectors.mapping(OldObject::getType, Collectors.toList())))
.entrySet().stream()
// Assuming a NewObject constructor that receives a String name and a List<String> types
.map(e -> new NewObject(e.getKey(), e.getValue()))
.collect(Collectors.toList());
Custom Collectors
If you are interested in learning how to make your own Collector, I made an example using a custom Collector as well:
oldObjectList.stream().collect(Collector.of(
// Supplier
() -> new ConcurrentHashMap<String, NewObject>(),
// Accumulator
(map, oldObject) -> {
// Assuming a NewObject constructor that gets a name and creates a new empty List as types.
map.computeIfAbsent(oldObject.getName(), name -> new NewObject(name)).getTypes().add(oldObject.getType());
},
// Combiner
(map1, map2) -> {
map2.forEach((k, v) -> map1.merge(k, v, (v1, v2) -> {
v1.getTypes().addAll(v2.getTypes());
return v1;
}));
return map1;
},
// Finisher
map -> new ArrayList<>(map.values())
));
You can even combine groupingBy and a custom Collector as a merge function:
new ArrayList<>(oldObjectList.stream().collect(Collectors.groupingBy(OldObject::getName, Collector.of(
// Supplier
// Assuming a NewObject constructor with no arguments that creates a new empty List as types.
NewObject::new,
// Accumulator
(newObject, oldObject) -> {
newObject.setName(oldObject.getName());
newObject.getTypes().add(oldObject.getType());
},
// Combiner
(newObject1, newObject2) -> {
newObject1.getTypes().addAll(newObject2.getTypes());
return newObject1;
}
))).values());
It may be possible to use Collectors.toMap to create NewObject instances immediately and append types as necessary within the merge function.
Assuming that there NewObject has custom constructor and overridden toString method:
class NewObject {
String name;
List<String> types = new ArrayList<>();
public NewObject(String name, String type) {
this.name = name;
this.types.add(type);
}
#Override
public String toString() {
return "{ name: " + name + "; types: " + types + "}";
}
}
and that there are helper methods to create an instance of NewObject from OldObject and merge types of two NewObject instances:
// MyClass
public static NewObject createNewObject(OldObject oo) {
return new NewObject(oo.getName(), oo.getType());
}
public static NewObject mergeTypes(NewObject no1, NewObject no2) {
no1.getTypes().addAll(no2.getTypes());
return no1;
}
The transformation may be implemented as follows:
List<OldObject> input = Arrays.asList(
new OldObject("n1", "type1"), new OldObject("n1", "type2"),
new OldObject("n2", "type1"), new OldObject("n3", "type2"),
new OldObject("n1", "type3"), new OldObject("n2", "type2")
);
List<NewObject> result = new ArrayList<>(
input
.stream()
.collect(Collectors.toMap(
OldObject::getName,
MyClass::createNewObject,
MyClass::mergeTypes,
LinkedHashMap::new // keep insertion order
))
.values());
result.forEach(System.out::println);
Output:
{ name: n1; types: [type1, type2, type3]}
{ name: n2; types: [type1, type2]}
{ name: n3; types: [type2]}
I want to write a utility for general memoization in Java, I want the code be able to look like this:
Util.memoize(() -> longCalculation(1));
where
private Integer longCalculation(Integer x) {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {}
return x * 2;
}
To do this, I was thinking I could do something like this:
public class Util{
private static final Map<Object, Object> cache = new ConcurrentHashMap<>();
public interface Operator<T> {
T op();
}
public static<T> T memoize(Operator<T> o) {
ConcurrentHashMap<Object, T> memo = cache.containsKey(o.getClass()) ? (ConcurrentHashMap<Object, T>) cache.get(o.getClass()) : new ConcurrentHashMap<>();
if (memo.containsKey(o)) {
return memo.get(o);
} else {
T val = o.op();
memo.put(o, val);
return val;
}
}
}
I was expecting this to work, but I see no memoization being done. I have tracked it down to the o.getClass() being different for each invocation.
I was thinking that I could try to get the run-time type of T but I cannot figure out a way of doing that.
The answer by Lino points out a couple of flaws in the code, but doesn't work if not reusing the same lambda.
This is because o.getClass() does not return the class of what is returned by the lambda, but the class of the lambda itself. As such, the below code returns two different classes:
Util.memoize(() -> longCalculation(1));
Util.memoize(() -> longCalculation(1));
I don't think there is a good way to find out the class of the returned type without actually executing the potentially long running code, which of course is what you want to avoid.
With this in mind I would suggest passing the class as a second parameter to memoize(). This would give you:
#SuppressWarnings("unchecked")
public static <T> T memoize(Operator<T> o, Class<T> clazz) {
return (T) cache.computeIfAbsent(clazz, k -> o.op());
}
This is based on that you change the type of cache to:
private static final Map<Class<?>, Object> cache = new ConcurrentHashMap<>();
Unfortunately, you have to downcast the Object to a T, but you can guarantee that it is safe with the #SuppressWarnings("unchecked") annotation. After all, you are in control of the code and know that the class of the value will be the same as the key in the map.
An alternative would be to use Guavas ClassToInstanceMap:
private static final ClassToInstanceMap<Object> cache = MutableClassToInstanceMap.create(new ConcurrentHashMap<>());
This, however, doesn't allow you to use computeIfAbsent() without casting, since it returns an Object, so the code would become a bit more verbose:
public static <T> T memoize(Operator<T> o, Class<T> clazz) {
T cachedCalculation = cache.getInstance(clazz);
if (cachedCalculation != null) {
return cachedCalculation;
}
T calculation = o.op();
cache.put(clazz, calculation);
return calculation;
}
As a final side note, you don't need to specify your own functional interface, but you can use the Supplier interface:
#SuppressWarnings("unchecked")
public static <T> T memoize(Supplier<T> o, Class<T> clazz) {
return (T) cache.computeIfAbsent(clazz, k -> o.get());
}
The problem you have is in the line:
ConcurrentHashMap<Object, T> memo = cache.containsKey(o.getClass()) ? (ConcurrentHashMap<Object, T>) cache.get(o.getClass()) : new ConcurrentHashMap<>();
You check whether an entry with the key o.getClass() exists. If yes, you get() it else you use a newly initialized ConcurrentHashMap. The problem now with that is, you don't save this newly created map, back in the cache.
So either:
Place cache.put(o.getClass(), memo); after the line above
Or even better use the computeIfAbsent() method:
ConcurrentHashMap<Object, T> memo = cache.computeIfAbsent(o.getClass(),
k -> new ConcurrentHashMap<>());
Also because you know the structure of your cache you can make it more typesafe, so that you don't have to cast everywhere:
private static final Map<Object, Map<Operator<?>, Object>> cache = new ConcurrentHashMap<>();
Also you can shorten your method even more by using the earlier mentioned computeIfAbsent():
public static <T> T memoize(Operator<T> o) {
return (T) cache
.computeIfAbsent(o.getClass(), k -> new ConcurrentHashMap<>())
.computeIfAbsent(o, k -> o.op());
}
(T): simply casts the unknown return type of Object to the required output type T
.computeIfAbsent(o.getClass(), k -> new ConcurrentHashMap<>()): invokes the provided lambda k -> new ConcurrentHashMap<>() when there is no mapping for the key o.getClass() in cache
.computeIfAbsent(o, k -> o.op());: this is invoked on the returned value from the computeIfAbsent call of 2.. If o doesn't exist in the nested map then execute the lambda k -> o.op() the return value is then stored in the map and returned.
I have a Section class with some attributes as below
class Section {
private String name;
private String code;
// respective getters and setters.
}
Now I have a list of Section Objects and I want to convert the list to a map of name and code.
I know it can be done in a regular way as below.
List<Section> sections = getSections();
Map<String, String> nameCodeMap = new HashMap<>();
for (Section section : sections) {
nameCodeMap.put(section.getCode(), section.getName());
}
I want to know if something similar is possible with Java-8 streams.
Not to difficult. Just use the toMap collector with the appropriate method references to the getters:
sections.stream().collect(
Collectors.toMap(Section::getName, Section::getCode)
);
If you don't have Section elements that have the same getCode() value :
Map<String, String> map = sections.stream()
.collect(toMap(Section::getCode, Section::getName);
If you have Section elements that have the same getCode() value, the previous one will raise IllegalStateException because it doesn't accept that. So you have to merge them.
For example to achieve the same thing than your actual code, that is overwriting the existing value for an existing key, use this overload :
toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction)
and return the second parameter of the merge function :
Map<String, String> map = sections.stream()
.collect(toMap(Section::getCode, Section::getName, (a, b) -> b);
Please find below code for the dame :
List<Section> sections = Arrays.asList(new Section("Pratik", "ABC"),
new Section("Rohit", "XYZ"));
Map<String, String> nameCodeMap = sections.stream().collect(
Collectors.toMap(section -> section.getName(),
section -> section.getCode()));
nameCodeMap.forEach((k, v) -> System.out.println("Key " + k + " " + "Value " + v));
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));
}
I have a situation where I have
class A {
private B b;
public B getB() {
return b;
}
}
and another class B
class B {
private List<C> c;
public List<C> getListC() {
return c;
}
}
Now class C contains two instance variables
class C {
private int id;
private String name;
public int getId() {
return id;
}
public String getName() {
return name;
}
}
Now I want to achieve the below using java 8
List<C> newListC = a.getB().getListC();
Map<Integer, String> map = new HashMap<>();
for(C c : newListC) {
map.put(c.getId,c.getName());
}
I have tried many time but every time I face different problems.
My code:
Optional<A> a=Optional.of(new A());
Map<Integer, String> map= a.map(A::getB)
.flatMap(b ->
b.getListC()
.stream()
.collect(Collectors.toMap(
C::getId,
C::getName
)
)
);
Error message :
Error:(164, 33) java: incompatible types: no instance(s) of type
variable(s) U,R,A,capture#1 of ?,T,K,U exist so that
java.util.Optional<U> conforms to
java.util.Map<java.lang.Integer,java.lang.String>
Thanks in advance
Though I hit couple of compilation errors as I can see few typos, but
Try:
List<C> newListC= new A().getB().getListC();
Map<Integer, String> stringMap = newListC.stream()
.collect(Collectors
.toMap(C::getId, C::getName));
Provided that you fixed your compilation issues, it should be emitting result equivalent to non-stream version of map.
You can’t flatMap an Optional to a Map; the function has to return an Optional. On the other hand, since the function doesn’t return an Optional, a flatMap is unnecessary and an ordinary map will do:
Map<Integer, String> map = Optional.of(new A())
.map(A::getB)
.map(b -> b.getListC().stream().collect(Collectors.toMap(C::getId, C::getName)))
.orElse(Collections.emptyMap());
But since the result of new A() can’t be null (and using of instead of ofNullable acknowledges this), the indirect processing at the beginning of the chain is unnecessary:
Map<Integer, String> map = Optional.ofNullable(new A().getB())
.map(b -> b.getListC().stream().collect(Collectors.toMap(C::getId, C::getName)))
.orElse(Collections.emptyMap());
But note that only the nullability of the result of getB is handled, as the function passed to the next mapping step unconditionally invokes stream() on the list returned by getListC. But returning null where a List is expected is bad coding style anyway; you can always return an empty list to represent the absence of values.
Maybe your confusion stems from a Stream based alternative solution:
Map<Integer, String> map = Stream.of(new A().getB())
.filter(Objects::nonNull)
.flatMap(b -> b.getListC().stream())
.collect(Collectors.toMap(C::getId, C::getName));
Here, a stream consisting of at most one element is created, followed by flatMaping it to the items of the list returned by getListC…