3

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?
  }

5 Answers 5

5

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.

Sign up to request clarification or add additional context in comments.

Comments

4

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;

Comments

2

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;
    }
}

5 Comments

Awesome! I wonder however if this can be achieved without going through the list twice?
@Riv Yes, I can post an example for that - but it's a bit hack-y (with streams). You're preserving two states. There are a few ways to approach this.
Looking at the single stream solution I like the two stream one a lot more now, considering my lists are going to be small. Thanks for help!
You still can use Stream.reduce with an accumulator that keeps track of the desired shape as you traverse the stream once. However I still prefer the first solution suggested by @ElliottFrisch for readability.
Modifying an existing array within forEach is awful. The clean alternative would be boolean[] bits = shapes.stream().collect(()->new boolean[2], (a,shape) -> { int sides = shape.getSideCount(); if(sides == 4) { a[0] = true; } else if (sides == 6) { a[1] = true; } }, (a1,a2)-> { a1[0]|=a2[0]; a1[1]|=a2[1]; });, but it would be simpler to use true bits instead of an array.
1

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...

Comments

1

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...

1 Comment

This would even make the filter obsolete, if you change the map step to .map(shape -> shape.getSideCount()==6? HEXA: shape.getSideCount()==4? RECTANGLE: GENERIC)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.