Java 流合并两个不同变量的总和

Java Stream combining sum of two different variables

我有一个问题。

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

我想做的是遍历tmpList,找到身高和体重的总和,当名字为空时加上“No-Name”。

// 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());  

我工作至今,似乎不太对。我的意思是我必须迭代每个案例。

有什么办法可以把它们结合起来吗?或者有什么改进的建议吗?

首先,循环版本似乎更适合这个聚合两个字段的任务(由 heightweight 求和),在遍历输入集合时修改空 name 字段的状态,因为它确保只传递一次整个输入。

因此,作为 forEach 的“有状态”流操作应该用于整个任务,但这与通常的 for-each 循环没有显着差异。通常,使用这种副作用操作是 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. _

因此,如果将任务拆分为两个单独的子任务,使用 Stream API 分别解决每个任务会更合适。

  1. 使用聚合字段的容器聚合多个字段(容器可以实现为字段的单独 object/record 或 array/collection)。

示例使用 Java 16+ recordStream::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. 需要时更新空白字段
tmpList.stream()
    .filter(Predicate.not(nonEmptyName))
    .forEach(t -> t.setName("No-Name"));

它可以在流中的一个 运行 中完成:

  • 替换空名称
  • 删除已替换名称的那些
  • 将它们映射到 [weight, height] 的数组
  • 通过加法减少数组。

我改了前两个的体重和身高,更明显结果是正确的

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

它给了我这个结果:

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

我不确定这比使用一种获取体重和身高的方法以及另一种替换空名称的方法更易读或更透明。

流中的这个 运行 感觉有点“hacky”。它依赖于改变对象,我认为这是一个副作用。请参阅 JavaDoc on Package java.util.stream 关于“副作用”。开头为:

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.

如果您真的想要在流中而不是循环中完成所有这些:

// 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] });

尽管通常不鼓励流操作中的副作用。