jlc488
jlc488

Reputation: 1061

Java Stream combining sum of two different variables

I have a question.

public class TestVO {

    private String name;
    private String id;
    private int weight;
    private int height;

    public TestVO() {}
    
    public TestVO(String name, String id, int weight, int height) {
        this.name = name;
        this.id = id;
        this.height = height;
        this.weight = weight;
    }
    
    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "Name: " + getName() + " ID: " + getId() + " Height: " + getHeight() + " Weight: " + getWeight();
    }
    
}
List<TestVO> tmpList = new ArrayList<>();
        
        tmpList.add(new TestVO("David", "id1", 10, 10));
        tmpList.add(new TestVO("", "id2", 11, 11));
        tmpList.add(new TestVO("Michael", "id3", 12, 12));
        tmpList.add(new TestVO("Jack", "id4", 13, 13));
        tmpList.add(new TestVO("Subodh", "id5", 14, 14));
        tmpList.add(new TestVO("", "id6", 15, 15));
        tmpList.add(new TestVO("Mrinal", "id7", 16, 16));
        tmpList.add(new TestVO("", "id8", 17, 17));
        tmpList.add(new TestVO("Eddie", "id9", 18, 18));
        tmpList.add(new TestVO("Peter", "id10", 19, 19));

What I want to do is that I want to iterate through the tmpList and find the sum of height and weight and add "No-Name" when the name is empty.

// below into stream 
int totalWeight = 0;
int totalHeight = 0;
for (TestVO testVO : tmpList) {
    if( !"".equals(testVO.getName())){
        totalWeight += totalWeight;
        totalHeight += totalHeight;
    }else {
        testVO.setName("No-Name");
    }
}
Map<Boolean, List<TestVO>>tmpMap =  tmpList.stream()
            .collect(Collectors.partitioningBy(test -> !"".equals(test.getName())));
        
        
        int totalWeigt = tmpMap.get(true).stream()
                                            .mapToInt(test -> test.getWeight())
                                            .sum();
        
        int totalHeight = tmpMap.get(true).stream()
                                            .mapToInt(test -> test.getHeight())
                                            .sum();
            
        tmpMap.get(false).stream()
                            .forEach(test -> {
                                test.setName("No-Name");
                            });
        
        // method 1
        List<TestVO> tmpRet1 = Stream.of(tmpMap.get(true),  tmpMap.get(false)).flatMap(Collection::stream).collect(Collectors.toList());
        //method 2
        List<TestVO> tmpRet2 = Stream.concat(tmpMap.get(true).stream(), tmpMap.get(false).stream()).collect(Collectors.toList());  

I have worked so far and It does not seem to be right. I mean I have to iterate each cases.

Is there any way to combine them all together? or any suggestions to make it better?

Upvotes: 2

Views: 952

Answers (3)

Shawn
Shawn

Reputation: 52409

If you really want to do all of this in a stream instead of a loop:

// sums[0] = total weight, sums[1] = total height
int[] sums = tmpList.stream()
    .reduce(new int[]{0, 0},
            (acc, t) -> {
                if (t.getName().equals("")) {
                    t.setName("No-Name");
                    return acc;
                } else {                    
                    return new int[]{ acc[0] + t.getWeight(), acc[1] + t.getHeight()};
                }
            },
            (a, b) -> new int[]{ a[0] + b[0], a[1] + b[1] });

though side-effects in stream operations are generally discouraged.

Upvotes: 1

Nowhere Man
Nowhere Man

Reputation: 19545

First, the loop version seems to be more appropriate for this task which aggregates over two fields (sum by height and weight), and modifies the state of empty name fields while traversing the input collection because it ensures only one pass over the entire input.

Therefore, "stateful" stream operation as forEach should be used for this entire task but this does not make significant difference with usual for-each loop. Generally, use of such side-effect operations is NOT recommended:

_Side-effects in behavioral parameters to stream operations are, in general, discouraged, as they can often lead to unwitting violations of the statelessness requirement, as well as other thread-safety hazards. _

So, if the task is split into two separate subtasks, resolving each task separately using Stream API would be more appropriate.

  1. Aggregate the multiple fields using a container for the aggregated fields (the container can be implemented as a separate object/record, or array/collection of the fields).

Example using Java 16+ record and Stream::reduce(BinaryOperator<T> accumulator):

record SumHeightWeight(int weight, int height) {
    SumHeightWeight sum(SumHeightWeight that) {
        return new SumHeightWeight(
            this.weight() + that.weight(),
            this.height() + that.height()
        );
    }
}

Predicate<TestVO> nonEmptyName = t -> !"".equals(t.getName());

SumHeightWeight sum = tmpList.stream()
    .filter(nonEmptyName)
    .map(t -> new SumHeightWeight(t.getWeight(), t.getHeight()))
    .reduce(SumHeightWeight::sum) // Optional<SumHeightWeight>
    .orElseGet(()-> new SumHeightWeight(0, 0));
System.out.println(sum); // -> SumHeightWeight[weight=102, height=102]
  1. Update empty field when needed
tmpList.stream()
    .filter(Predicate.not(nonEmptyName))
    .forEach(t -> t.setName("No-Name"));

Upvotes: 2

Scratte
Scratte

Reputation: 3166

It can be done in just one run in a stream:

  • Replace the empty name
  • Remove those that had their name replaced
  • Map them to an array of [weight, height]
  • Reduce the arrays with addition.

I changed the weight and height of the first two, so it's more obvious that the result is correct.

import java.util.List;
import java.util.ArrayList;

public class Testing {
  public static void main(String[] args) {
    List<TestVO> tmpList = new ArrayList<>();
    tmpList.add(new TestVO("David", "id1", 100, 200));
    tmpList.add(new TestVO("", "id2", 2048, 1024));
    tmpList.add(new TestVO("Michael", "id3", 12, 12));
    tmpList.add(new TestVO("Jack", "id4", 13, 13));
    tmpList.add(new TestVO("Subodh", "id5", 14, 14));
    tmpList.add(new TestVO("", "id6", 15, 15));
    tmpList.add(new TestVO("Mrinal", "id7", 16, 16));
    tmpList.add(new TestVO("", "id8", 17, 17));
    tmpList.add(new TestVO("Eddie", "id9", 18, 18));
    tmpList.add(new TestVO("Peter", "id10", 19, 19));

    String NoName = "No-Name";

    int[] weigthHeight =
            tmpList.stream()
                    // modify the testVO
                   .map(testVO -> { if (testVO.getName().isEmpty()) {
                                     testVO.setName(NoName);
                                    }
                                    return testVO;
                    })
                    // it changed them:
                   .peek(testVO-> System.out.println(testVO))
                    // remove the ones we don't want anymore
                   .filter(testVO-> !NoName.equals(testVO.getName()))
                    // make an array of the Weight and Height
                   .map(testVO-> new int[]{testVO.getWeight(), testVO.getHeight()})
                    // reduce to one array with the sum of Weight and Height
                   .reduce(new int[]{0,0}, (a,b) -> add(a,b));

      System.out.println("Weight: " + weigthHeight[0]);
      System.out.println("Height: " + weigthHeight[1]);
  }

  static int[] add(int[] a, int[]b) {
    a[0] = a[0] + b[0];
    a[1] = a[1] + b[1];
    return a;
  }
}

It gives me this outout:

Name: David ID: id1 Height: 200 Weight: 100
Name: No-Name ID: id2 Height: 1024 Weight: 2048
Name: Michael ID: id3 Height: 12 Weight: 12
Name: Jack ID: id4 Height: 13 Weight: 13
Name: Subodh ID: id5 Height: 14 Weight: 14
Name: No-Name ID: id6 Height: 15 Weight: 15
Name: Mrinal ID: id7 Height: 16 Weight: 16
Name: No-Name ID: id8 Height: 17 Weight: 17
Name: Eddie ID: id9 Height: 18 Weight: 18
Name: Peter ID: id10 Height: 19 Weight: 19
Weight: 192
Height: 292

I'm not sure this is more readable nor transparent, than having one method that gets the weight and height, and another method that replaces the empty names.

This one run in a stream feels sort of "hacky". It relies on changing the objects, which I perceive to be a side-effect. See JavaDoc on Package java.util.stream about "Side-effects". It starts with:

Side-effects in behavioral parameters to stream operations are, in general, discouraged, as they can often lead to unwitting violations of the statelessness requirement, as well as other thread-safety hazards.

Upvotes: 0

Related Questions