Java HashMap添加新条目时意外修改已有条目问题排查
你遇到的这个问题——添加新条目后已有条目权重被修改,看起来诡异,但其实是几个隐藏的代码问题叠加导致的,咱们一步步拆解:
先还原问题场景
你实现了一个以PathMaterial为键、Double为值的HashMap存储物品权重,按顺序添加三个条目:
PathMaterial(DIRT, 1),权重1PathMaterial(STONE, 1),权重1PathMaterial(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

