问题根源:对象重用
Hadoop 为了优化性能,在 reduce 方法中会重用 key 和 value 对象。
这意味着,在 for(Person p : values) 循环中,变量 p 始终指向同一个 Person 对象实例。
框架在每次迭代时,会用新的数据覆盖 这个 Person 对象的内部属性,而不是创建一个新的对象。
因此,当你执行 plist.add(p) 时,你只是将同一个对象的引用 多次添加到了列表 plist 中。
最终,plist 里所有的元素都指向内存中的同一个 Person 对象,而这个对象的值是最后一次迭代时被覆盖的结果。
错误代码示例
假设 values 中包含三个 Person 对象,其 name 属性分别为 "Alice"、"Bob" 和 "Charlie"。
List<Person> plist = new ArrayList<>();
for(Person p : values){
plist.add(p);
}
执行后,plist 的内容将不是 ["Alice", "Bob", "Charlie"]。相反,plist 中会包含三个指向同一个对象 的引用,而这个对象的 name 属性是 "Charlie" 。所以,遍历 plist 会得到 ["Charlie", "Charlie", "Charlie"]。
正确的解决方案
你需要创建一个新的 Person 对象副本,然后将副本添加到列表中。这样,列表中的每个元素都是一个独立的对象。
方法一:手动创建新对象并复制属性
这是最直接的方法,适用于所有情况。
List<Person> plist = new ArrayList<>();
for(Person p : values){
// 创建一个新的Person对象
Person newPerson = new Person();
// 手动复制所有需要的属性
newPerson.setName(p.getName());
newPerson.setAge(p.getAge());
// ... 复制其他属性
// 将新对象的引用添加到列表中
plist.add(newPerson);
}
方法二:使用工具类复制属性
如果你的 Person 类有很多属性,可以使用像 Apache Commons BeanUtils 这样的工具类来简化属性复制过程。
import org.apache.commons.beanutils.BeanUtils;
List<Person> plist = new ArrayList<>();
for(Person p : values){
Person newPerson = new Person();
try {
// 自动复制所有同名同类型的属性
BeanUtils.copyProperties(newPerson, p);
} catch (Exception e) {
e.printStackTrace();
}
plist.add(newPerson);
}
方法三:实现拷贝构造函数
在你的 Person 类中定义一个拷贝构造函数,可以使代码更简洁。
public class Person implements Writable {
private String name;
private int age;
// 默认的无参构造函数(Hadoop序列化需要)
public Person() {}
// 拷贝构造函数
public Person(Person other) {
this.name = other.name;
this.age = other.age;
}
// ... 其他代码 (getter, setter, write, readFields)
}
然后在 reduce 方法中这样使用:
List<Person> plist = new ArrayList<>();
for(Person p : values){
// 使用拷贝构造函数创建副本
plist.add(new Person(p));
}