You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Java泛型循环批量实现POJO字段的归一化赋值?

问题描述

我的POJO结构如下:

@Builder(toBuilder = true)
@Getter
public final class TestPOJO {
    private double fieldA;
    private double normFieldA;
    private double fieldB;
    private double normFieldB;
    private double fieldC;
    private double normFieldC;
    private double fieldD;
    private double normFieldD;
    private double fieldE;
    private double normFieldE;
}

我有一个TestPOJO列表,需要为每个字段计算归一化值,当前实现函数如下:

List<TestPOJO> setNormalizedValue(List<TestPOJO> pojos) {
    final double[][] rawValues = new double[5][pojos.size()];
    int i = 0;
    for (TestPOJO pojo: pojos) {
        rawValues[0][i] = pojo.getFieldA();
        rawValues[1][i] = pojo.getFieldB();
        rawValues[2][i] = pojo.getFieldC();
        rawValues[3][i] = pojo.getFieldD();
        rawValues[4][i] = pojo.getFieldE();
        i++;
    }
    final Pair<double[], double[]> stdPopAndMean = getStdPopAndMean(rawValues, pojos.size());
    final double[] stdPop = stdPopAndMean.getLeft();
    final double[] mean = stdPopAndMean.getRight();

    return pojos.stream()
        .map(p -> p.toBuilder()
                    .normFieldA(getNormalizedValue(p.getFieldA(), stdPop[0], mean[0]))
                    .normFieldB(getNormalizedValue(p.getFieldB(), stdPop[1], mean[1]))
                    .normFieldC(getNormalizedValue(p.getFieldC(), stdPop[2], mean[2]))
                    .normFieldD(getNormalizedValue(p.getFieldD(), stdPop[3], mean[3]))
                    .normFieldE(getNormalizedValue(p.getFieldE(), stdPop[4], mean[4]))
                    .build()
            )
        .collect(Collectors.toList());    
}

但实际我有大约20个字段,而非示例中的5个。是否可以通过Java泛型循环来实现?我尝试使用Supplier和Consumer,但遇到了Cannot refer to static content from non-static.的错误。

我原本的思路如下:

private static final Map<Integer, Pair<Consumer<Double>, Supplier<Double>>> FUNCTIONS_MAP = 
    ImmutableMap.<Integer, Pair<Consumer<Double>, Supplier<Double>>>builder()
        .put(0, Pair.of(TestPOJO.TestPOJOBuilder::normFieldA, TestPOJO::getFieldA))
        .put(1, Pair.of(TestPOJO.TestPOJOBuilder::normFieldB, TestPOJO::getFieldB))
        .put(2, Pair.of(TestPOJO.TestPOJOBuilder::normFieldC, TestPOJO::getFieldC))
        .put(3, Pair.of(TestPOJO.TestPOJOBuilder::normFieldD, TestPOJO::getFieldD))
        .put(4, Pair.of(TestPOJO.TestPOJOBuilder::normFieldE, TestPOJO::getFieldE))
        .build();

        
List<TestPOJO> setNormalizedValue(List<TestPOJO> pojos) {
    double[][] rawValues = new double[FUNCTIONS_MAP.size()][pojos.size()];
    int i = 0;
    for (TestPOJO pojo: pojos) {
        for (Map.Entry<Integer, Pair<Consumer<Double>, Supplier<Double>>> entry : FUNCTIONS_MAP.entrySet()) {
            final int position = entry.getKey();
            final Supplier<Double> getter = entry.getValue().getRight();
            rawValues[position][i] = getter.get(); // 此处Supplier无法接收pojo参数
        }
        i++;
    }
    final Pair<double[], double[]> stdPopAndMean = getStdPopAndMean(rawValues, pojos.size());
    final double[] stdPop = stdPopAndMean.getLeft();
    final double[] mean = stdPopAndMean.getRight();

    return pojos.stream()
        .map(p -> { 
                TestPOJO.TestPOJOBuilder builder = p.toBuilder();
                for (Map.Entry<Integer, Pair<Consumer<Double>, Supplier<Double>>> entry : FUNCTIONS_MAP.entrySet()) {
                    final int position = entry.getKey();
                    final Supplier<Double> getter = entry.getValue().getRight();
                    final Consumer<Double> setter = entry.getValue().getLeft();
                    final double similarity = getNormalizedValue(getter.get(p), stdPop[position], mean[position]); // Supplier无法接收参数
                    setter.accept(similarity); // Consumer无法关联到builder
                }
                return builder.build();
                    
            }
        )
        .collect(Collectors.toList());
}

显然这个实现存在问题,我相信有更优的解决方案,请问如何通过循环来实现该功能?

我知道可以通过反射实现,但我不想使用反射。

补充的工具方法:

/**
 * 计算给定二维数组的均值和总体标准差。
 * 将每个一维数组视为一个数据集,计算其均值和标准差。
 *
 * @param values      待计算的二维数组。
 * @param recordCount 一维数组的长度,假设所有行长度相同。
 * @return 包含标准差数组和均值数组的Pair,每个元素对应每行的计算结果。
 */
public Pair<double[], double[]> getStdPopAndMean(final double[][] values, final int recordCount) {
    final double[] mean = new double[values.length];
    final double[] stdPop = new double[values.length];

    for (int i = 0; i < values.length; i++) {
        mean[i] = getMean(values[i], recordCount);
    }

    for (int i = 0; i < values.length; i++) {
        double diffSquared = 0;
        for (int j = 0; j < recordCount && j < values[i].length; j++) {
            diffSquared += (values[i][j] - mean[i]) * (values[i][j] - mean[i]);
        }
        final double variance = diffSquared / recordCount;
        stdPop[i] = FastMath.sqrt(variance);
    }

    return Pair.of(stdPop, mean);
}

解决方案

核心问题是误用了Supplier和Consumer——它们无法携带目标对象(POJO或Builder)的上下文。正确做法是定义自定义元数据类,封装每个字段的getter(从POJO取原始值)、setter(给Builder设归一化值),再通过循环遍历这些元数据实现批量处理。

步骤1:定义字段元数据类

创建类封装字段的操作逻辑:

@AllArgsConstructor
public class FieldMetadata {
    private final Function<TestPOJO, Double> rawValueGetter;
    private final BiConsumer<TestPOJO.TestPOJOBuilder, Double> normalizedValueSetter;

    // 获取原始值
    public double getRawValue(TestPOJO pojo) {
        return rawValueGetter.apply(pojo);
    }

    // 设置归一化值到Builder
    public void setNormalizedValue(TestPOJO.TestPOJOBuilder builder, double value) {
        normalizedValueSetter.accept(builder, value);
    }
}

步骤2:初始化字段元数据列表

将所有需要处理的字段对应的getter和setter放入列表:

private static final List<FieldMetadata> FIELD_METADATA_LIST = List.of(
    new FieldMetadata(TestPOJO::getFieldA, TestPOJO.TestPOJOBuilder::normFieldA),
    new FieldMetadata(TestPOJO::getFieldB, TestPOJO.TestPOJOBuilder::normFieldB),
    new FieldMetadata(TestPOJO::getFieldC, TestPOJO.TestPOJOBuilder::normFieldC),
    new FieldMetadata(TestPOJO::getFieldD, TestPOJO.TestPOJOBuilder::normFieldD),
    new FieldMetadata(TestPOJO::getFieldE, TestPOJO.TestPOJOBuilder::normFieldE)
    // 继续添加剩余15个字段...
);

步骤3:重构批量处理逻辑

用元数据列表替代硬编码的字段操作,实现循环处理:

List<TestPOJO> setNormalizedValue(List<TestPOJO> pojos) {
    int fieldCount = FIELD_METADATA_LIST.size();
    int pojoCount = pojos.size();
    double[][] rawValues = new double[fieldCount][pojoCount];

    // 收集所有原始值
    for (int pojoIdx = 0; pojoIdx < pojoCount; pojoIdx++) {
        TestPOJO pojo = pojos.get(pojoIdx);
        for (int fieldIdx = 0; fieldIdx < fieldCount; fieldIdx++) {
            rawValues[fieldIdx][pojoIdx] = FIELD_METADATA_LIST.get(fieldIdx).getRawValue(pojo);
        }
    }

    // 计算均值和标准差
    Pair<double[], double[]> stdPopAndMean = getStdPopAndMean(rawValues, pojoCount);
    double[] stdPop = stdPopAndMean.getLeft();
    double[] mean = stdPopAndMean.getRight();

    // 批量设置归一化值
    return pojos.stream()
        .map(pojo -> {
            TestPOJO.TestPOJOBuilder builder = pojo.toBuilder();
            for (int fieldIdx = 0; fieldIdx < fieldCount; fieldIdx++) {
                FieldMetadata metadata = FIELD_METADATA_LIST.get(fieldIdx);
                double rawValue = metadata.getRawValue(pojo);
                double normalizedValue = getNormalizedValue(rawValue, stdPop[fieldIdx], mean[fieldIdx]);
                metadata.setNormalizedValue(builder, normalizedValue);
            }
            return builder.build();
        })
        .collect(Collectors.toList());
}

方案优势

  • Function<TestPOJO, Double>:接收TestPOJO实例并返回原始字段值,替代了无法传参的Supplier。
  • BiConsumer<TestPOJO.TestPOJOBuilder, Double>:接收Builder实例和归一化值完成设置,解决了原方案中Consumer无法关联具体Builder的问题。
  • 扩展简单:新增字段只需在FIELD_METADATA_LIST中添加对应元数据,无需修改核心逻辑。

内容的提问来源于stack exchange,提问作者Belphegor21

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 19:50:54