Search code examples
javajava-8sumjava-stream

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?


Solution

  • 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"));