
在Java编程中,一个常见的误解是当我们将对象添加到集合(如ArrayList)中时,集合会保存该对象的一个“副本”。然而,对于非基本数据类型,Java变量存储的是对象的引用(内存地址),而不是对象本身。这意味着,当我们将一个对象添加到ArrayList中时,实际上是向列表中添加了该对象的一个引用。如果该对象是可变的,并且在添加到列表后其内部状态被修改,那么所有持有该对象引用的地方(包括ArrayList中的元素)都将看到这些修改。
考虑以下场景,一个Matrices类封装了一个二维整数数组int[][]:
public class Matrices {
private int M[][]; // 存储矩阵的二维数组
public int[][] getM() {
return M;
}
public void setM(int[][] M) {
this.M = M;
}
}在主程序中,我们希望在每次迭代中,将当前状态的矩阵A封装到Matrices对象中,并添加到ArrayList<Matrices>中,以记录矩阵A在不同阶段的变化。然而,原始代码如下所示:
ArrayList<Matrices> matrices = new ArrayList<>();
int[][] A = panel.CreaMatriz(); // 初始矩阵
for (int k = 0; k < nodos.size(); k++) {
Matrices m = new Matrices();
m.setM(A); // 将矩阵A的引用赋给m
matrices.add(m); // 将m(包含A的引用)添加到列表
// 在这里,矩阵A被修改(例如,通过Floyd-Warshall算法)
for (int i = 0; i < nodos.size(); i++) {
for (int j = 0; j < nodos.size(); j++) {
if (A[i][k] + A[k][j] < A[i][j]) {
A[i][j] = A[i][k] + A[k][j];
}
}
}
}
// 预期:列表包含矩阵A在每次迭代后的不同状态
// 实际:列表中的所有Matrices对象都包含矩阵A的最终状态这段代码的问题在于,m.setM(A)操作仅仅是将A的引用赋值给了Matrices对象内部的M字段。因此,ArrayList中的所有Matrices对象都指向了内存中的同一个int[][] A实例。当循环内部修改A时,所有Matrices对象所引用的矩阵都会随之改变。最终,ArrayList中存储的所有Matrices对象都将显示A在循环结束时的最终状态,而不是每次迭代时的快照。
立即学习“Java免费学习笔记(深入)”;
在Java中,基本数据类型(如int, double, boolean等)采用值传递,即传递的是数据本身的副本。而对象类型(包括数组)则采用引用传递,即传递的是对象在内存中的地址。
当我们执行m.setM(A)时,Matrices对象内部的M字段并没有获得A所指向的二维数组的一个独立副本,它只是获得了A所持有的那个内存地址。这就像多个遥控器都控制着同一台电视机,无论哪个遥控器发出指令,电视机的状态都会改变,并且所有遥控器都能“看到”电视机的当前状态。
因此,要解决上述问题,我们需要确保每次添加到ArrayList中的Matrices对象都包含一个独立于原始矩阵A的、当前状态的矩阵副本。
深拷贝是指创建一个新对象,并递归地复制原对象所有字段(包括引用类型字段所指向的对象),直到所有字段都是原始对象的独立副本。对于二维数组int[][],这意味着不仅要复制外层数组的引用,还要复制内层每个一维数组的元素。
我们可以在每次迭代中,在将矩阵A添加到Matrices对象之前,手动创建一个A的深拷贝。
import java.util.ArrayList;
import java.util.Arrays; // 引入Arrays工具类
// 假设Matrices类保持不变,或者setM方法不执行深拷贝
public class Matrices {
private int M[][];
public int[][] getM() {
return M;
}
public void setM(int[][] M) {
this.M = M;
}
}
// 主程序代码
public class Main {
public static void main(String[] args) {
ArrayList<Matrices> matrices = new ArrayList<>();
// 假设 panel.CreaMatriz() 返回一个初始矩阵
int[][] A = {
{0, 3, 1, Integer.MAX_VALUE},
{Integer.MAX_VALUE, 0, Integer.MAX_VALUE, Integer.MAX_VALUE},
{Integer.MAX_VALUE, 4, 0, 1},
{3, Integer.MAX_VALUE, Integer.MAX_VALUE, 0}
}; // 示例初始矩阵
int nodosSize = 4; // 假设nodos.size()为4
for (int k = 0; k < nodosSize; k++) {
// 1. 创建当前矩阵A的深拷贝
int[][] currentMatrixCopy = new int[A.length][];
for (int i = 0; i < A.length; i++) {
// 复制内层一维数组
currentMatrixCopy[i] = Arrays.copyOf(A[i], A[i].length);
}
// 2. 创建Matrices对象并设置深拷贝的矩阵
Matrices m = new Matrices();
m.setM(currentMatrixCopy); // 现在m持有的是一个独立副本
matrices.add(m);
// 3. 继续修改原始矩阵A(这不会影响已添加到列表中的m)
// 模拟Floyd-Warshall算法的一部分
for (int i = 0; i < nodosSize; i++) {
for (int j = 0; j < nodosSize; j++) {
// 避免溢出问题,当Integer.MAX_VALUE相加时
long val1 = (A[i][k] == Integer.MAX_VALUE || A[k][j] == Integer.MAX_VALUE) ?
Integer.MAX_VALUE : (long)A[i][k] + A[k][j];
if (val1 < A[i][j]) {
A[i][j] = (int)val1;
}
}
}
}
// 打印结果,验证每个Matrices对象是否保存了不同的矩阵状态
System.out.println("--- 矩阵历史记录 ---");
for (int i = 0; i < matrices.size(); i++) {
System.out.println("迭代 " + i + " 后的矩阵:");
int[][] matrix = matrices.get(i).getM();
for (int[] row : matrix) {
for (int val : row) {
System.out.printf("%5s", (val == Integer.MAX_VALUE ? "∞" : val));
}
System.out.println();
}
System.out.println();
}
}
}将深拷贝的逻辑封装到Matrices类内部,特别是setM方法中,是一种更符合面向对象设计原则的做法。这样可以确保Matrices对象始终维护其内部状态的独立性,避免外部使用者忘记进行深拷贝而导致错误。同时,为了进一步保护内部状态,getM方法也应该返回一个深拷贝,防止外部通过getM获取到内部引用后直接修改。
import java.util.Arrays;
public class Matrices {
private int M[][]; // 存储矩阵的二维数组
// 构造函数:在创建对象时进行深拷贝
public Matrices(int[][] sourceM) {
setM(sourceM); // 调用setM方法进行深拷贝
}
// getM方法:返回M的深拷贝,保护内部状态不被外部修改
public int[][] getM() {
if (M == null) {
return null; // 或者返回一个空矩阵,取决于具体需求
}
int[][] copy = new int[M.length][];
for (int i = 0; i < M.length; i++) {
copy[i] = Arrays.copyOf(M[i], M[i].length);
}
return copy;
}
// setM方法:在设置矩阵时进行深拷贝,确保M是独立副本
public void setM(int[][] sourceM) {
if (sourceM == null) {
this.M = null;
return;
}
this.M = new int[sourceM.length][];
for (int i = 0; i < sourceM.length; i++) {
// 确保内部数组也进行拷贝
this.M[i] = Arrays.copyOf(sourceM[i], sourceM[i].length);
}
}
}使用修改后的Matrices类,主循环代码将变得更简洁和安全:
import java.util.ArrayList;
public class MainWithEncapsulatedCopy {
public static void main(String[] args) {
ArrayList<Matrices> matrices = new ArrayList<>();
int[][] A = {
{0, 3, 1, Integer.MAX_VALUE},
{Integer.MAX_VALUE, 0, Integer.MAX_VALUE, Integer.MAX_VALUE},
{Integer.MAX_VALUE, 4, 0, 1},
{3, Integer.MAX_VALUE, Integer.MAX_VALUE, 0}
}; // 示例初始矩阵
int nodosSize = 4; // 假设nodos.size()为4
for (int k = 0; k < nodosSize; k++) {
// 直接创建Matrices对象,由其构造函数或setM方法处理深拷贝
Matrices m = new Matrices(A); // 使用带参构造函数
matrices.add(m);
// 继续修改原始矩阵A
for (int i = 0; i < nodosSize; i++) {
for (int j = 0; j < nodosSize; j++) {
long val1 = (A[i][k] == Integer.MAX_VALUE || A[k][j] == Integer.MAX_VALUE) ?
Integer.MAX_VALUE : (long)A[i][k] + A[k][j];
if (val1 < A[i][j]) {
A[i][j] = (int)val1;
}
}
}
}
// 打印结果
System.out.println("--- 矩阵历史记录 (使用封装深拷贝) ---");
for (int i = 0; i < matrices.size(); i++) {
System.out.println("迭代 " + i + " 后的矩阵:");
int[][] matrix = matrices.get(i).getM(); // getM返回深拷贝,确保安全
for (int[] row : matrix) {
for (int val : row) {
System.out.printf("%5s", (val == Integer.MAX_VALUE ? "∞" : val));
}
System.out.println();
}
System.out.println();
}
}
}在Java中处理集合与可变对象时,理解引用语义至关重要。当需要将可变对象(如数组)的不同状态保存到集合中时,必须通过深拷贝来确保每个集合元素都拥有其独立的数据副本,从而避免因引用共享导致的状态混乱。将深拷贝逻辑封装在对象内部(如set方法或构造函数中)是推荐的做法,它能提高代码的健壮性和可维护性。
以上就是Java集合中对象引用陷阱:如何正确保存可变对象状态的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号