How do you do the equivalent of the following transform() method using pure functional programming (without the if-conditional).
Meta: I'd appreciate a title edit, I'm not sure how to word this question in "functionalese"
public class Playground {
private static Optional<Map<String,Integer>> transform(List<Tuple<String,Optional<Integer>>> input) {
if (input.stream().anyMatch(t->t.second.isEmpty())) return Optional.empty();
Map<String, Integer> theMap = input.stream()
.map(t -> new Tuple<>(t.first, t.second.get()))
.collect(Collectors.groupingBy(
t1 -> t1.first,
Collectors.mapping(t2 -> t2.second, toSingle())));
return Optional.of(theMap);
}
#Test
public void collect() {
List<Tuple<String,Optional<Integer>>> input1 = new ArrayList<>();
input1.add(new Tuple<>("foo", Optional.of(1)));
input1.add(new Tuple<>("bar", Optional.empty()));
Optional<Map<String,Integer>> result1 = transform(input1);
assertTrue(result1.isEmpty());
List<Tuple<String,Optional<Integer>>> input2 = new ArrayList<>();
input2.add(new Tuple<>("foo", Optional.of(1)));
input2.add(new Tuple<>("bar", Optional.of(2)));
Optional<Map<String,Integer>> result2 = transform(input2);
assertTrue(result2.isPresent());
assertEquals((int)1, (int)result2.get().get("foo"));
assertEquals((int)2, (int)result2.get().get("bar"));
}
private static class Tuple<T1,T2> {
public T1 first;
public T2 second;
public Tuple(T1 first, T2 second) {
this.first = first;
this.second = second;
}
}
public static <T> Collector<T, ?, T> toSingle() {
return Collectors.collectingAndThen(
Collectors.toList(),
list -> list.get(0)
);
}
}
This might work for you:
private static Optional<Map<String, Integer>> transform(
List<Tuple<String, Optional<Integer>>> input) {
return Optional.of(input)
.filter(t -> t.stream().allMatch(a -> a.second.isPresent()))
.map(
in ->
in.stream()
.filter(t -> t.second.isPresent())
.map(t -> new Tuple<>(t.first, t.second.get()))
.collect(
Collectors.groupingBy(
t1 -> t1.first, Collectors.mapping(t2 -> t2.second, toSingle()))));
}
Although my solution does not satisfy your result, I can offer a solution with the ternary operator
private static Map<String, Integer> transform(List<Tuple<String, Optional<Integer>>> input) {
return input.stream().anyMatch(t -> t.second.isEmpty()) ? Collections.emptyMap() :
input.stream()
.map(t -> new Tuple<>(t.first, t.second.get()))
.collect(Collectors.groupingBy(
t1 -> t1.first,
Collectors.mapping(t2 -> t2.second, toSingle())));
}
“pure functional programming” is not necessarily a sign of quality and not an end in itself.
If you want to make the code simpler and more efficient, which may include getting rid of the if-conditional, especially as it bears a second iteration over the source data, you can do it in various ways. E.g.
private static <K,V> Optional<Map<K,V>> transform(List<Tuple<K,Optional<V>>> input) {
final class AbsentValue extends RuntimeException {
AbsentValue() { super(null, null, false, false); }
}
try {
return Optional.of(input.stream().collect(Collectors.toMap(
t1 -> t1.first,
t2 -> t2.second.orElseThrow(AbsentValue::new),
(first,next) -> first)));
} catch(AbsentValue av) {
return Optional.empty();
}
}
When empty optionals are truly the exceptional case, you can make flagging via exception part of the method’s contract, e.g.
public static class AbsentValueException extends RuntimeException {
}
private static <K,V> Map<K,V> transform(List<Tuple<K,Optional<V>>> input)
throws AbsentValueException {
return input.stream().collect(Collectors.toMap(
t1 -> t1.first,
t2 -> t2.second.orElseThrow(AbsentValueException::new),
(first,next)->first));
}
#Test(expected = AbsentValueException.class)
public void collect1() {
List<Tuple<String,Optional<Integer>>> input1 = new ArrayList<>();
input1.add(new Tuple<>("foo", Optional.of(1)));
input1.add(new Tuple<>("bar", Optional.empty()));
Map<String,Integer> result1 = transform(input1);
}
#Test
public void collect2() {
List<Tuple<String,Optional<Integer>>> input2 = new ArrayList<>();
input2.add(new Tuple<>("foo", Optional.of(1)));
input2.add(new Tuple<>("bar", Optional.of(2)));
Map<String,Integer> result2 = transform(input2);
assertEquals((int)1, (int)result2.get("foo"));
assertEquals((int)2, (int)result2.get("bar"));
}
Even better would be not to put optionals into the list of tuples in the first place.
Related
I have an expensive method that I only want to call it when necessary in a stream. Here is an example:
public static Optional<MyObject> findTarget(String input, List<MyObject> myList) {
return Stream.concat(myList.stream(), expensive().stream()).filter(o -> o.hasName(input)).findFirst();
}
The goal is to find the target MyObject from myList based on the input value, but if its not in myList ONLY then it will call expensive() to return a bigger list and look from there.
The above example does not do that, as it seems Stream.concat will call expensive() already before consuming all of myList.
An ugly solution I can think of is to do it in two steps, e.g.:
return myList.stream().filter(o -> o.hasName(input)).findFirst().or(
() -> expensive().stream().filter(o -> o.hasName(input)).findFirst());
But then I will have to repeat the filter and the rest twice.
Is there any better solution or even a single liner of Stream that does that?
You can lazily evaluate by concatenating Supplier<List<MyObject>> instead of List<MyObject>.
public static Optional<MyObject> findTarget(String input, List<MyObject> myList) {
List<Supplier<List<MyObject>>> concat = List.of(() -> myList, () -> expensive());
return concat.stream()
.flatMap(supplier -> supplier.get().stream())
.filter(o -> o.hasName(input))
.findFirst();
}
Test:
record MyObject(String s) {
public boolean hasName(String in) {
return s.equals(in);
}
}
static List<MyObject> expensive() {
System.out.println("expensive() called");
return List.of(new MyObject("z"));
}
public static void main(String[] args) {
List<MyObject> myList = List.of(new MyObject("a"));
System.out.println("case 1: " + findTarget("a", myList));
System.out.println("case 2: " + findTarget("x", myList));
}
Output:
case 1: Optional[MyObject[s=a]]
expensive() called
case 2: Optional.empty
Alternatively you can do this:
public static Optional<MyObject> findTarget(String input, List<MyObject> myList) {
return Stream.of(
(Supplier<List<MyObject>>) () -> myList,
(Supplier<List<MyObject>>) () -> expensive())
.flatMap(supplier -> supplier.get().stream())
.filter(o -> o.hasName(input))
.findFirst();
}
Another alternative, which might be simpler to understand, is to extract the stream logic in a separate method:
private static Optional<MyObject> findInternal(String input, List<MyObject> myList) {
return myList.stream().filter(o -> o.hasName(input)).findFirst();
}
and then simply call it twice:
public static Optional<MyObject> findTarget(String input, List<MyObject> myList) {
return findInternal(input, myList).or(() -> findInternal(input, expensive()));
}
I have a list of class A and class B with no duplicate elements. "code" attribute will be same across both class A and B. I want to convert them to Map<String, C> using java 8 streams. Please help
public class A {
private String code;
private boolean status;
field 3...
field 4..
}
public class B {
private String code;
private String location;
field 5...
field 5..
}
public class C {
private String location;
private boolean status;
}
List1 : [A(code=NY, status=false), A(code=NJ, status=true),A(code=TX, status=true), A(code=NM, status=false)]
List2 : [B(code=NY, location=NewYork), B(code=NJ, location=NewJersey),B( code=TX, location=Texas), B(code=NM, location=NewMexico)]
Map = map{NY=C(location=NewYork, status=false), NJ=C(location=NewJersey, status=true), TX=C(location=Texas, status=true),NM=C(location=NewMexico, status=false)}
Final map should be in the same order as the elements in List1
~A
UPDATE :
i updated the code to below, but its not compiling. any idea about whats wrong?
package test;
import java.util.*;
import java.util.stream.Collectors;
public class Test {
static void main(String[] args){
new Test().testFunction();
}
void testFunction(){
System.out.println("Hello World");
List<A> listA = Arrays.asList(new A("NY", false), new A("NJ", true), new A("TX", false), new A("AZ", true));
List<B> listB = Arrays.asList(new B("NY", "New York"), new B("NJ", "New Jersey"),
new B("TX", "Texas"), new B("NM", "New Mexico"));
Map<String, B> mapB = listB
.stream()
.collect(Collectors.toMap(B::getCode, b -> b, (b1, b2) -> b1));
Map<String, C> innerJoin = listA
.stream()
.filter(a -> mapB.containsKey(a.getCode())) // make sure a B instance exists with the code
.map(a -> Map.entry(a.getCode(), new C(mapB.get(a.getCode()).getLocation(), a.getStatus())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (c1, c2) -> c1, HashMap::new));
innerJoin.forEach((code, c) -> System.out.println(code + " -> " + c));
}
}
class A {
private String code;
private boolean status;
public A(String code, boolean status) {
this.code = code;
this.status = status;
}
public String getCode() {
return this.code;
}
public boolean getStatus() {
return this.status;
}
}
class B {
private String code;
private String location;
public B(String code, String location) {
this.code = code;
this.location = location;
}
public String getCode() {
return this.code;
}
public String getLocation() {
return this.location;
}
}
class C {
private String location;
private boolean status;
public C(String location, boolean status) {
this.location = location;
this.status = status;
}
public String getLocation() {
return this.location;
}
public boolean getStatus() {
return this.status;
}
}
You should use somethins like:
Map<String, B> mapList2 = list2.stream().collect(Collectors.toMap(B::getCode,
Function.identity()); // create temporary map
Map<String,B> result = list1.stream().collect(
LinkedHashMap::new,
(map, a) -> map.put(
a.getCode(), // get code from A
new C(
mapList2.get(a.getCode()).getLocation() // get location from B
a.getStatus() // get status from A
),
Map::putAll); // use LinkedHashMap to keep order
Map<String, C> map = IntStream.range(0, list1.size())
.mapToObj(i -> new CodeIndex(list1.get(i).code(), i))
.collect(Collectors.toMap(
CodeIndex::code,
ci -> new C(list2.get(ci.index()).location(), list1.get(ci.index()).status()),
(a, b) -> a,
LinkedHashMap::new
));
What happens is that we first get all valid indexes of both lists, then we map to a (custom) container class holding both the index and the code. (I called it CodeIndex, but you may as well use Map.Entry or some Pair class.)
At last, we collect it into a LinkedHashMap (because it retains insertion order), mapping the keys to simply CodeIndex::code and the values to new C(«location_from_list2», «status_from_list1»).
Note that this code traverses the lists once.
Of course, this assumes that:
both lists have the same codes;
all codes of both list1 and list2 are in the same order.
Note that I used record-style getters, that is, getters with the same name as their corresponding field names. For example, location() instead of getLocation().
This task seems like to join two lists of objects by one key property. Taking into account the requirements, it should be like inner join with the help of temporary map created from the listB.
Map<String, B> mapB = listB
.stream()
.(Collectors.toMap(B::getCode, b -> b, (b1, b2) -> b1)); // use merge function as a defense measure
Map<String, C> innerJoin = listA
.stream()
.filter(a -> mapB.containsKey(a.getCode())) // make sure a B instance exists with the code
.map(a -> Map.entry(a.getCode(), new C(mapB.get(a.getCode()).getLocation(), a.isStatus())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (c1, c2) -> c1, LinkedHashMap::new));
Test:
List<A> listA = Arrays.asList(new A("NY", false), new A("NJ", true), new A("TX", false), new A("AZ", true));
List<B> listB = Arrays.asList(new B("NY", "New York"), new B("NJ", "New Jersey"), new B("TX", "Texas"), new B("NM", "New Mexico"));
// ...
innerJoin.forEach((code, c) -> System.out.println(code + " -> " + c));
Output:
NY -> C(location=New York, status=false)
NJ -> C(location=New Jersey, status=true)
TX -> C(location=Texas, status=false)
The "simplest" solution may be based on assumption that the items in both lists are "ordered" by the same code field, then no temporary map is needed:
Map<String, C> simple = IntStream
.range(0, listA.size())
.mapToObj(i -> Map.entry(listA.get(i).getCode(), new C(listB.get(i).getLocation(), listA.get(i).isStatus())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (c1, c2) -> c1, LinkedHashMap::new));
However, such solution is too fragile.
I think you have to use something like this to really maintain the order and group.
List<A> aList = Arrays.asList(new A("NY", false), new A("NJ", true));
List<B> bList = Arrays.asList(new B("NY", "NewYork"), new B("NJ", "NewJersey"));
LinkedHashMap<String, A> mapA =
aList.stream().collect(
Collectors.toMap(
A::getCode,
a -> a,
(e1, e2) -> e1,
LinkedHashMap::new));
LinkedHashMap<String, B> mapB =
bList.stream().collect(
Collectors.toMap(
B::getCode,
b -> b,
(e1, e2) -> e1,
LinkedHashMap::new));
Set<String> keys = new LinkedHashSet<>(mapA.keySet());
keys.addAll(mapB.keySet());
LinkedHashMap<String, C> result2 = keys.stream().collect(
Collectors.toMap(
key -> key,
key -> new C(
mapB.get(key).getLocation(),
mapA.get(key).isStatus()),
(e1, e2) -> e1,
LinkedHashMap::new));
System.out.println(result2);
There is code
class Person {
private ZonedDateTime date ;
private int regionId;
private int centerId;
private int amount1;
private float percent1;
}
List<Person> entityList = new ArrayList<>();
I grouping by year of month like this:
listPerson.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getMonth(),Collectors.collectingAndThen(Collectors.toList(),
l -> {
Integer sumAmount1 = l.stream().collect(Collectors.summingInt(i -> i.getAmount1()));
Double avgPerc1 = l.stream().collect(Collectors.averagingDouble(i -> i.getPercent1()));
List<String> data = new ArrayList<>();
data.add(Integer.toString(sumAmount1));
data.add(Double.toString(avgPerc1));
return data;
}
))).forEach((k,v) -> System.out.println(k.getValue() + "-" + v.toString()));
Also i group by year, regionId, centerId in same manner:
listPerson.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getYear(),Collectors ......
But i got many duplicate code where part with
l -> {...}
repeated many times. How to instead of l -> {...} use a method reference?
IntelliJ can literally just do this for you. You don't even have to think about it.
Keyboard shortcut for hints (yellow) is AltEnter
Here's what I ended up with
public static void main(String[] args)
{
List<Person> listPerson = null;
listPerson.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getMonth(), Collectors.collectingAndThen(Collectors.toList(),
Scratch::apply
)))
.forEach((k,v) -> System.out.println(k.getValue() + "-" + v.toString()));
}
private static List<String> apply(List<Person> l)
{
int sumAmount1 = l.stream().mapToInt(Person::getAmount1).sum();
Double avgPerc1 = l.stream().collect(Collectors.averagingDouble(Person::getPercent1));
List<String> data = new ArrayList<>();
data.add(Integer.toString(sumAmount1));
data.add(Double.toString(avgPerc1));
return data;
}
You can create a method reference like this:
private List<String> methodReference(List<Person> l) {
Integer sumAmount1 = l.stream().collect(Collectors.summingInt(i -> i.getAmount1()));
Double avgPerc1 = l.stream().collect(Collectors.averagingDouble(i -> i.getPercent1()));
List<String> data = new ArrayList<>();
data.add(Integer.toString(sumAmount1));
data.add(Double.toString(avgPerc1));
return data;
}
I have created a methodReference in my Test class. You can replace it with your own class name. And now in your stream() you can refer to it like this:
entityList.stream()
.collect(Collectors.groupingBy(i -> i.getDate().getMonth(), Collectors.collectingAndThen(Collectors.toList(),
Test::methodReference // replace Test with your class name
))).forEach((k, v) -> System.out.println(k.getValue() + "-" + v.toString()));
Might be a little of topic, but I think the beauty of using Stream API is allowing you to build a data pipeline. I would strive to build something that looks like a pipeline, with steps I can customize with pluggable functions.
I think the code would be more readable by refactoring towards a pipeline, and I would try below with the help of a new data structure called Tuple2, epscially its map method. It's easy to build, you can also use one from libraries like vavr.
For reuse, one can consider a function like groupAndSummarize (the name suggests it does two things, so is a smell).
class Tuple2<T1, T2> {
private final T1 t1;
private final T2 t2;
public Tuple2(final T1 t1, final T2 t2) {
this.t1 = t1;
this.t2 = t2;
}
public <U1, U2> Tuple2<U1, U2> map(final Function<T1, U1> func1,
final Function<T2, U2> func2) {
return new Tuple2<>(func1.apply(t1), func2.apply(t2));
}
public T1 _1() { return t1; }
public T2 _2() { return t2; }
}
private <T, K, V> List<Tuple2<K, V>> groupAndSummarize(final List<T> list, final Function<T, K> groupFn, final Function<List<T>, V> summarizeFn) {
return list.stream()
.collect(Collectors.groupingBy(groupFn))
.entrySet()
.stream()
.map(this::toTuple)
.map(t -> t.map(
Function.identity(),
summarizeFn
))
.collect(Collectors.toList());
}
private <K, V> Tuple2<K, V> toTuple(final Map.Entry<K, V> entry) {
return new Tuple2<>(entry.getKey(), entry.getValue());
}
private List<String> summarize(final List<Person> l) {
// your logic
}
public void test() {
final List<Person> entityList = new ArrayList<>();
groupAndSummarize(entityList, i -> i.getDate().getMonth(), this::summarize)
.forEach(t -> System.out.println(t.t1.getValue() + "-" + t.t2.toString()));
}
I have a problem with conversion List Object to Map String, List Object. I'm looking for Map with a keys name of all components in cars, and a value is represented by cars with this component
public class Car {
private String model;
private List<String> components;
// getters and setters
}
I write a solution but looking for a better stream solution.
public Map<String, List<Car>> componentsInCar() {
HashSet<String> components = new HashSet<>();
cars.stream().forEach(x -> x.getComponents().stream().forEachOrdered(components::add));
Map<String, List<Car>> mapCarsComponents = new HashMap<>();
for (String keys : components) {
mapCarsComponents.put(keys,
cars.stream().filter(c -> c.getComponents().contains(keys)).collect(Collectors.toList()));
}
return mapCarsComponents;
}
You could do it with streams too, but I find this a bit more readable:
public static Map<String, List<Car>> componentsInCar(List<Car> cars) {
Map<String, List<Car>> result = new HashMap<>();
cars.forEach(car -> {
car.getComponents().forEach(comp -> {
result.computeIfAbsent(comp, ignoreMe -> new ArrayList<>()).add(car);
});
});
return result;
}
Or using stream:
public static Map<String, List<Car>> componentsInCar(List<Car> cars) {
return cars.stream()
.flatMap(car -> car.getComponents().stream().distinct().map(comp -> new SimpleEntry<>(comp, car)))
.collect(Collectors.groupingBy(
Entry::getKey,
Collectors.mapping(Entry::getValue, Collectors.toList())
));
}
I know this is a Java question, and there is already a Java answer. However, I would like to add that Kotlin, which is a JVM language and perfectly interoperable with Java, you can do things like this very easily and cleanly:
val carsByComponent = cars
.flatMap { it.components }
.distinct()
.map { component -> component to cars.filter { car -> component in car.components } }
.toMap()
or even more concise, allthough less readable:
val carsByComponent = cars
.flatMap { car -> car.components.map { it to car } }
.groupBy { it.first }
.mapValues {it.value.map { it.second }}
I am working with collectors' groupingBy and partioningBy functions. I an working with a list of persons, the list of persons is as follows:
List<Person> persons =
Arrays.asList(
new Person("Max", 18),
new Person("Peter", 23),
new Person("Pamela", 23),
new Person("David", 12),
new Person("Pam", 12));
What i want is to partition the list on the basis of persons whose name starts with letter "P" and then group them on the basis of their ages.
Here is my code which does the above filtration:
Map<Boolean, Map<Object, List<Person>>> rr = persons.stream()
.collect(Collectors.partitioningBy(p -> p.name.startsWith("P"),
Collectors.groupingBy(p -> p.age > 20)));
And the output which i got is:
rr = {false={false=[Max, David]}, true={false=[Pam], true=[Peter, Pamela]}}
Now, my requirement is to get only internal map from the above results. That is, i want to change the return values to:
{false=[Pam], true=[Peter, Pamela]}
That is, I want the results (or partitioned map) whose boolean value is true as returned by the partioningBy function. How can i achieve this?
You could create a custom collector (I've done it only as an exercise, please treat it as such):
static class MyCustom<T, U> implements Collector<Person, List<Person>, Map<T, List<U>>> {
private final Function<Person, T> function;
private final Predicate<Person> predicate;
private final Function<Person, U> transformingFunction;
public MyCustom(Predicate<Person> predicate, Function<Person, T> function,
Function<Person, U> transformingFunction) {
this.predicate = predicate;
this.function = function;
this.transformingFunction = transformingFunction;
}
#Override
public Supplier<List<Person>> supplier() {
return ArrayList::new;
}
#Override
public BiConsumer<List<Person>, Person> accumulator() {
return (list, person) -> {
if (predicate.test(person)) {
list.add(person);
}
};
}
#Override
public BinaryOperator<List<Person>> combiner() {
return (l1, l2) -> {
l1.addAll(l2);
return l1;
};
}
#Override
public Function<List<Person>, Map<T, List<U>>> finisher() {
return list -> {
return list.stream().collect(
Collectors.groupingBy(function, Collectors.mapping(transformingFunction, Collectors.toList())));
};
}
#Override
public Set<java.util.stream.Collector.Characteristics> characteristics() {
return EnumSet.of(Characteristics.UNORDERED);
}
}
And then apply it like this:
MyCustom<Integer, String> custom = new MyCustom<>((Person p) -> p.getName().startsWith("P"),
(Person p) -> p.getAge(), Person::getName);
System.out.println(persons.stream().collect(custom)); // {23=[Peter, Pamela], 12=[Pam]}
Filter by name
Apply partitioning by age
Map<Boolean, List<Person>> p1 = persons.stream().filter(p -> p.name.startsWith("P")).collect(Collectors.partitioningBy(p -> p.getAge() > 20));