Jan
Jan

Reputation: 73

Java Stream from Map to Map: Change Value inside Object while streaming

I defined a map and filled it with 5 objects of the type Rectangle. Each Rectangle-object has the attributes rectangleId, aLength, bLength and color.

I want to use the java stream api to stream the given map into the new map. While streaming, the value of bLength shall be increased by 100 for all rectangles that are red. Below is what I got so far. I can't seem to figure out how to change the value of bLength.

public class Main {
    public static void main(String[] args){

        Rectangle r1 = new Rectangle(800, 100,200,"green");
        Rectangle r2 = new Rectangle(900, 200,300,"red");
        Rectangle r3 = new Rectangle(1000, 300,400,"yellow");
        Rectangle r4 = new Rectangle(1100, 400,500,"blue");
        Rectangle r5 = new Rectangle(1200, 500,600,"orange");

        TreeMap<Integer, Rectangle> myMap = new TreeMap<>();
        myMap.put(r1.rectangleId, r1);
        myMap.put(r2.rectangleId, r2);
        myMap.put(r3.rectangleId, r3);
        myMap.put(r4.rectangleId, r4);
        myMap.put(r5.rectangleId, r5);

        Map<Integer, Rectangle> myMapNew = myMap.entrySet().stream()
                .filter(r -> r.getValue().color == "red")
                .collect(Collectors.toMap(r -> r.getKey(), r -> r.getValue()));

    }

    public class Rectangle {
        public int rectangleId;
        public int aLength;
        public int bLength;
        public String color;

        public Rectangle(int rectangleId, int aLength, int bLength, String color){
            this.rectangleId = rectangleId;
            this.aLength = aLength;
            this.bLength = bLength;
            this.color = color;
    }
}

Upvotes: 1

Views: 4136

Answers (3)

hc_dev
hc_dev

Reputation: 9418

So you have 2 steps in your streaming then:

  1. incoming Rectangle to outgoing Rectangle (= identity)
  2. convert one or many attributes of Rectangle based on a predicate (= conditional attribute-modification)

Solve it in functions

  1. identity (actually obsoletes this step in streaming), anyway:
Function<Rectangle, Rectangle> toIdentical = Function.identity();
  1. modification rule:

bLength shall be increased by 100 for all Rectangles that have property color with value "red"

Function<Rectangle, Rectangle> toConditialModified = (rectangle) -> {
  if (rectangle.color.equals("red")) { // conditional
    rectangle.bLength += 100; // modified
  }
  return rectangle;
};

Then combine in a stream

Build a pipeline using these functional steps:

Map<Integer, Rectangle> newRectanglesMap = oldRectanglesMap.values().stream()
  .map( toIdentical )  // can leave this out
  .map( toConditionalModified )
  .collect(Collectors.toMap(r -> r.rectangleId, r -> r));

Alternatively: update partly

Just work on the values, modify if needed in-place:

oldRectanglesMap.values().stream()
  .filter(r -> r.color.equals("red")) // get only the red ones
  .forEach(r -> { r.bLength += 100; }) // modify the filtered

Over-readable & functionally decomposed

This would be better readable if decomposing Function<Rectangle, Rectangle> toConditialModified of previous step 2:

Predicate<Rectangle> isRed = r -> r.color.equals("red");
Consumer<Rectangle> updateBLengthPlus100 = r -> r.bLength += 100;

oldRectanglesMap.values().stream()
  .filter( isRed )
  .forEach( updateBLengthPlus100 );

I just played with your code and requirement:

  • looked on different aspects of the problem
  • decomposed it differently
  • solved these parts differently
  • but: always functional using streams

Upvotes: 2

Bohemian
Bohemian

Reputation: 425448

By modifying the rectangles, there is still only one instance of each, so the rectangles in both maps are the same rectangles and so new map would be identical to the old map after your proposed operation. Thus a new map is not required - modifying the old one is sufficient:

myMap.values().stream()
  .filter(r -> r.color.equals("red"))
  .forEach(r -> r.length += 100);

For the new map to be different, you would need to create new instance of the rectangles for the néw map, but that is not what you proposed.

Also note that comparing Strings using == is not guaranteed to work the way you expect in the general case; always use .equals().

Upvotes: 4

Tobias
Tobias

Reputation: 2575

If you want to have all of the rectangles from the original map in the new map (and modify some of them) you can't use the filter method, because that will restrict the result to only the red ones.

You need to use the map method to change the rectangles while streaming:

Map<Integer, Rectangle> myMapNew = myMap.entrySet().stream()
                .map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue().increaseBLengthOrReturnIdentity()))
                .collect(Collectors.toMap(r -> r.getKey(), r -> r.getValue()));

And you need to define the method increaseBLengthOrReturnIdentity in you Rectangle class:

public class Rectangle {
    public int rectangleId;
    public int aLength;
    public int bLength;
    public String color;

    public Rectangle(int rectangleId, int aLength, int bLength, String color){
        this.rectangleId = rectangleId;
        this.aLength = aLength;
        this.bLength = bLength;
        this.color = color;
    }

    public Rectangle increaseBLengthOrReturnIdentity() {
        if (color.equals("red")) {//btw.: better use equals to compare strings instead of ==
            return new Rectangle(rectangleId, aLength, bLength + 100, color);
        }

        // you could also return 'this' here; depends on whether you need a new object or not
        return new Rectangle(rectangleId, aLength, bLength, color);
    }
}

Upvotes: 1

Related Questions