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

Java HashMap添加新条目时意外修改已有条目问题排查

HashMap条目被意外篡改?来看看你的代码哪里出问题了

你遇到的这个问题——添加新条目后已有条目权重被修改,看起来诡异,但其实是几个隐藏的代码问题叠加导致的,咱们一步步拆解:

先还原问题场景

你实现了一个以PathMaterial为键、Double为值的HashMap存储物品权重,按顺序添加三个条目:

  • PathMaterial(DIRT, 1),权重1
  • PathMaterial(STONE, 1),权重1
  • PathMaterial(STONE, 2),权重1
    结果添加第三个后,PathMaterial(DIRT, 1)的权重居然变成了2,调试发现是调用HashMap#put后出的问题。

先看你的核心代码:

PathMaterial类代码

package com.test; 
import java.text.DecimalFormat; 

public class PathMaterial { 
    public enum Material { 
        AIR, STONE, GRASS, DIRT; 
        int getId() { 
            return this.ordinal(); 
        } 
    } 

    public Material mat; 
    public byte data; 

    public PathMaterial(Material mat, byte data) { 
        this.mat = mat; 
        this.data = data; 
    } 

    @Override 
    public boolean equals(Object obj) { 
        if (!(obj instanceof PathMaterial)) { 
            return false; 
        } 
        PathMaterial other = (PathMaterial) obj; 
        return mat == other.mat && data == other.data; 
    } 

    @Override 
    public int hashCode() { 
        return Integer.parseInt( 
            new DecimalFormat("000").format(mat.getId()) + "" + 
            new DecimalFormat("00").format((int) data)); 
    } 
}

权重实现的Main类代码

package com.test; 
import java.text.DecimalFormat; 
import java.util.HashMap; 
import java.util.Map; 

public class Main { 
    private HashMap<PathMaterial, Double> weightedBlocks; 
    int totalWeight; 

    public static void main(String[] args) { 
        new Main().run(); 
    } 

    private void run() { 
        weightedBlocks = new HashMap<>(); 
        totalWeight = 0; 
        addBlock(PathMaterial.Material.DIRT, 1, (byte)1); 
        addBlock(PathMaterial.Material.STONE, 1, (byte)1); 
        addBlock(PathMaterial.Material.STONE, 1, (byte)2); 
        System.out.println(toString()); 
    } 

    private void denormalizeWeights() { 
        double previous = 0; 
        for (Map.Entry pair : weightedBlocks.entrySet()) { 
            weightedBlocks.put((PathMaterial) pair.getKey(), (double) pair.getValue() - previous); 
            previous = (double) pair.getValue(); 
        } 
        for (Map.Entry pair : weightedBlocks.entrySet()) { 
            weightedBlocks.put((PathMaterial) pair.getKey(), (double) pair.getValue() * totalWeight); 
        } 
    } 

    private void normalizeWeights() { 
        for (Map.Entry pair : weightedBlocks.entrySet()) { 
            weightedBlocks.put((PathMaterial) pair.getKey(), (double) pair.getValue() / totalWeight); 
        } 
        double previous = 0; 
        for (Map.Entry pair : weightedBlocks.entrySet()) { 
            weightedBlocks.put((PathMaterial) pair.getKey(), (double) pair.getValue() + previous); 
            previous = (double) pair.getValue(); 
        } 
    } 

    public void addBlock(final PathMaterial.Material mat, double weight, byte data) { 
        denormalizeWeights(); 
        weightedBlocks.put(new PathMaterial(mat, data), weight); 
        totalWeight = getTotalWeight(); 
        normalizeWeights(); 
    } 

    private int getTotalWeight() { 
        int weight = 0; 
        for (double d : weightedBlocks.values()) { 
            weight += d; 
        } 
        return weight; 
    } 

    @Override 
    public String toString() { 
        StringBuilder sb = new StringBuilder( 
            "PathBuilder with name '" + "test" + "'\nRadius: " + 3 + "\nWeightmap:"); 
        denormalizeWeights(); 
        for (Map.Entry pair : weightedBlocks.entrySet()) { 
            PathMaterial mat = (PathMaterial) pair.getKey(); 
            String matName = mat.mat.name() + ":" + mat.data; 
            String percentage = new DecimalFormat("00.0").format(((double) pair.getValue()) / totalWeight * 100); 
            sb.append("\n-" + matName + " [" + percentage + "%]"); 
        } 
        normalizeWeights(); 
        return sb.toString(); 
    } 
}

问题根源:多个隐藏bug叠加

咱们逐个揪出问题:

1. toString方法居然修改对象内部状态!

这是最坑的一点:toString的职责是输出对象的字符串表示,绝对不能修改对象的内部数据,但你在toString里调用了denormalizeWeights()和normalizeWeights(),这两个方法会直接修改weightedBlocks里的权重值。每次调用toString,你的权重数据都会被来回转换,多次调用后数据完全混乱,这也是你调试时看到put后数据异常的原因之一。

2. 遍历HashMap时直接修改条目值

在denormalizeWeights和normalizeWeights里,你遍历entrySet的同时调用put修改当前条目的值。HashMap的entrySet遍历是基于当前的map状态,修改后的值会影响后续的遍历计算,导致逻辑完全错乱。比如第一次修改某个条目后,下一次遍历到的是修改后的值,基于这个错误的值继续计算,结果自然不对。

3. totalWeight用int类型导致精度丢失

权重是double类型,但你用int存储总权重,累加时会自动截断小数部分。如果权重出现小数,总权重计算会直接出错,进而影响后续的归一化/反归一化逻辑。

4. HashMap无序导致遍历顺序不可预测

HashMap的entrySet遍历顺序是不确定的,而你的权重转换逻辑依赖于遍历顺序,这会导致每次转换的结果可能不一样,进一步加剧数据混乱。

修复方案:逐个解决问题

1. 修正totalWeight类型

把totalWeight改成double,确保能正确累加小数权重:

private double totalWeight; // 替换原来的int类型

2. 重构权重转换方法,用临时map避免遍历中修改原数据

不要直接在原map上修改,而是创建临时map存储转换后的结果,最后替换原map:

private void denormalizeWeights() {
    Map<PathMaterial, Double> tempMap = new HashMap<>(weightedBlocks);
    double previous = 0;
    
    // 第一步:从累积权重转原始权重
    for (Map.Entry<PathMaterial, Double> entry : tempMap.entrySet()) {
        double rawWeight = entry.getValue() - previous;
        tempMap.put(entry.getKey(), rawWeight);
        previous = entry.getValue();
    }
    
    // 第二步:乘以总权重恢复原始值
    for (Map.Entry<PathMaterial, Double> entry : tempMap.entrySet()) {
        tempMap.put(entry.getKey(), entry.getValue() * totalWeight);
    }
    
    weightedBlocks = tempMap;
}

private void normalizeWeights() {
    Map<PathMaterial, Double> tempMap = new HashMap<>(weightedBlocks);
    
    // 第一步:归一化到0-1范围
    for (Map.Entry<PathMaterial, Double> entry : tempMap.entrySet()) {
        tempMap.put(entry.getKey(), entry.getValue() / totalWeight);
    }
    
    // 第二步:转换为累积权重
    double previous = 0;
    for (Map.Entry<PathMaterial, Double> entry : tempMap.entrySet()) {
        double cumulativeWeight = entry.getValue() + previous;
        tempMap.put(entry.getKey(), cumulativeWeight);
        previous = cumulativeWeight;
    }
    
    weightedBlocks = tempMap;
}

3. 修改toString方法,不碰原数据

复制一份权重map的副本,对副本进行转换操作,绝对不修改原对象的状态:

@Override
public String toString() {
    StringBuilder sb = new StringBuilder(
        "PathBuilder with name '" + "test" + "'\nRadius: " + 3 + "\nWeightmap:");
    
    // 复制当前权重,避免修改原数据
    Map<PathMaterial, Double> tempWeights = new HashMap<>(weightedBlocks);
    double tempTotal = totalWeight;
    
    // 对副本执行反归一化,得到原始权重
    double previous = 0;
    for (Map.Entry<PathMaterial, Double> entry : tempWeights.entrySet()) {
        double rawWeight = entry.getValue() - previous;
        tempWeights.put(entry.getKey(), rawWeight);
        previous = entry.getValue();
    }
    for (Map.Entry<PathMaterial, Double> entry : tempWeights.entrySet()) {
        tempWeights.put(entry.getKey(), entry.getValue() * tempTotal);
    }
    
    // 生成字符串
    for (Map.Entry<PathMaterial, Double> entry : tempWeights.entrySet()) {
        PathMaterial mat = entry.getKey();
        String matName = mat.mat.name() + ":" + mat.data;
        String percentage = new DecimalFormat("00.0").format(entry.getValue() / tempTotal * 100);
        sb.append("\n-" + matName + " [" + percentage + "%]");
    }
    
    return sb.toString();
}

4. 修正getTotalWeight方法

改成返回double类型,匹配权重的类型:

private double getTotalWeight() {
    double weight = 0;
    for (double d : weightedBlocks.values()) {
        weight += d;
    }
    return weight;
}

5. 可选:用LinkedHashMap保证遍历顺序

如果需要固定的遍历顺序,把weightedBlocks改成LinkedHashMap,保证插入顺序一致:

private LinkedHashMap<PathMaterial, Double> weightedBlocks;

// 在run方法中初始化
weightedBlocks = new LinkedHashMap<>();

测试验证

修改后再运行你的测试代码,三个条目的权重都会保持1,每个占33.3%左右,不会出现已有条目被修改的情况了。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 07:04:23