Reputation: 357
I have a list of objects, lets say Shapes. I would like to process them using a stream and return another object - ShapeType - based on what is in the list.
Normally I will just return ShapeType.GENERIC, however if there is a Rectangle in there, I would like to return ShapeType.RECT. If there is a hexagon in a list I would like to return ShapeType.HEXA. When both rectangle and square are present, I would like to return ShapeType.HEXA.
Now, when it comes to code I would like something like this:
public ShapeType resolveShapeType(final List<Shape> shapes) {
shapes.stream()
.filter(shape -> shape.getSideCount() == 6 || shape.getSideCount() == 4)
// I should have a stream with just rectangles and hexagons if present.
// what now?
}
Upvotes: 3
Views: 12199
Reputation: 298143
You can use
public ShapeType resolveShapeType(final List<Shape> shapes) {
int sides = shapes.stream()
.mapToInt(Shape::getSideCount)
.filter(count -> count==4 || count==6)
.max().orElse(0);
return sides==6? ShapeType.HEXA: sides==4? ShapeType.RECT: ShapeType.GENERIC;
}
This maps each element to its side count and reduces them to the preferred type, which happens to be the maximum count here, so no custom reduction function is needed.
This isn’t short-circuiting, but for most use cases, it will be sufficient. If you want to reduce the number of operations to the necessary minimum, things will be more complicated.
public ShapeType resolveShapeType(final List<Shape> shapes) {
OptionalInt first = IntStream.range(0, shapes.size())
.filter(index -> {
int count = shapes.get(index).getSideCount();
return count == 6 || count == 4;
})
.findFirst();
if(!first.isPresent()) return ShapeType.GENERIC;
int ix = first.getAsInt(), count = shapes.get(ix).getSideCount();
return count==6? ShapeType.HEXA: shapes.subList(ix+1, shapes.size()).stream()
.anyMatch(shape -> shape.getSideCount()==6)? ShapeType.HEXA: ShapeType.RECT;
}
We know that we can stop at the first HEXA
, but to avoid a second pass, it’s necessary to remember whether there was an occurence of RECT
for the case there is no HEXA
. So this searches for the first element that is either, a RECT
or HEXA
. If there is none, GENERIC
is returned, otherwise, if the first was not a HEXA
, the remaining elements are checked for an element of the HEXA
kind. Note that for processing the remainder after the first RECT
, no filter
is needed as it is implied that shapes that are neither, RECT
nor HEXA
, can’t fulfill the condition.
But it should also be obvious that this code, trying to minimize the numbers of checks, is harder to read than an equivalent for
loop.
Upvotes: 5
Reputation: 2381
Sounds like a case for 'reduce' or 'collect/max'.
Assume you have a method that selects the 'dominant' type (you can put it in a lambda, but IMHO it's more readable as a method):
public class Util{
public static ShapeType dominantType(ShapeType t1, ShapeType t2){
if(t1==HEXA || t2==HEXA) return HEXA;
else if (t1==RECTANGLE || t2==RECTANGLE) return RECTANGLE;
else return GENERIC;
}
}
There are several ways to use it, one reduce example would be:
shapes.stream()
.filter(shape -> shape.getSideCount() == 6 || shape.getSideCount() == 4)
.map(shape -> shape.getSideCount()==6? HEXA:RECTANGLE)
.reduce( GENERIC, Util::dominantType);
// using GENERIC in case of empty list
You may also want to look into collectors.maxBy. BTW whatever approach you take, please give some thought to the behavior in case of an empty list...
Upvotes: 1
Reputation: 33655
Can also do something like this:
ShapeType r = shapes.stream()
.map(s -> ShapeType.parse(s.getSides()))
.filter(c -> c == ShapeType.Hexagon || c==ShapeType.Square)
.max(ShapeType::compareTo)
.orElse(ShapeType.Generic);
Here, I've taken a little liberty with your ShapeType
:
enum ShapeType {
Square(4), Hexagon(6), Generic(Integer.MAX_VALUE);
int v;
ShapeType(int v) {
this.v = v;
}
static ShapeType parse(int v) {
switch (v) {
case 4: return Square;
case 6: return Hexagon;
default:
break;
}
return Generic;
}
public String toString(){
return Integer.toString(v);
}
}
TBH you can avoid the parse operation if you add a getShapeType()
method which returned the correct type per Derived type. Then the map()
operation will only extract the type, for example .map(Shape::getShapeType)
.
The .filter()
will find the group you are interested in, the largest shape is deemed the label of the collection...
Upvotes: 1
Reputation: 328598
Assuming that only the three types of shapes can be present in the list, an alternative would be:
Set<Integer> sides = shapes.stream()
.map(Shape::getSideCount)
.collect(toSet());
if (sides.contains(6)) return HEXA;
else if (sides.contains(4)) return RECTANGLE;
else return GENERIC;
But I think the most straightforward (and efficient) way would be a good old for loop:
ShapeType st = GENERIC;
for (Shape s : shapes) {
if (s.getSideCount() == 6) return HEXA;
if (s.getSideCount() == 4) st = RECTANGLE;
}
return st;
Upvotes: 4
Reputation: 201439
If I understand what you're trying to do, then you can use anyMatch
. Like,
public ShapeType resolveShapeType(final List<Shape> shapes) {
if (shapes.stream().anyMatch(shape -> shape.getSideCount() == 6)) {
return ShapeType.HEXA;
} else if (shapes.stream().anyMatch(shape -> shape.getSideCount() == 4)) {
return ShapeType.RECT;
} else {
return ShapeType.GENERIC;
}
}
One way to do this (streaming shapes
once) would be to preserve the shape presence with an array. Like,
public ShapeType resolveShapeType(final List<Shape> shapes) {
boolean[] bits = new boolean[2];
shapes.stream().forEach(shape -> {
int sides = shape.getSideCount();
if (sides == 4) {
bits[0] = true;
} else if (sides == 6) {
bits[1] = true;
}
});
if (bits[1]) {
return ShapeType.HEXA;
} else if (bits[0]) {
return ShapeType.RECT;
} else {
return ShapeType.GENERIC;
}
}
Upvotes: 2