0

0

Java集合中对象引用陷阱:如何正确保存可变对象状态

心靈之曲

心靈之曲

发布时间:2025-09-29 12:09:05

|

591人浏览过

|

来源于php中文网

原创

java集合中对象引用陷阱:如何正确保存可变对象状态

当向ArrayList中添加包含可变对象(如数组)的自定义对象时,若不进行深拷贝,所有列表元素可能引用同一个底层可变对象,导致状态意外同步。本文将详细解释这一常见问题,并提供通过深拷贝确保每个列表元素独立保存其状态的解决方案。

1. 问题背景:ArrayList中可变对象的引用语义

在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中,以记录矩阵A在不同阶段的变化。然而,原始代码如下所示:

ArrayList 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免费学习笔记(深入)”;

2. 深入理解Java的引用与值传递

在Java中,基本数据类型(如int, double, boolean等)采用值传递,即传递的是数据本身的副本。而对象类型(包括数组)则采用引用传递,即传递的是对象在内存中的地址。

当我们执行m.setM(A)时,Matrices对象内部的M字段并没有获得A所指向的二维数组的一个独立副本,它只是获得了A所持有的那个内存地址。这就像多个遥控器都控制着同一台电视机,无论哪个遥控器发出指令,电视机的状态都会改变,并且所有遥控器都能“看到”电视机的当前状态。

磁力开创
磁力开创

快手推出的一站式AI视频生产平台

下载

因此,要解决上述问题,我们需要确保每次添加到ArrayList中的Matrices对象都包含一个独立于原始矩阵A的、当前状态的矩阵副本。

3. 解决方案:实现数组的深拷贝

深拷贝是指创建一个新对象,并递归地复制原对象所有字段(包括引用类型字段所指向的对象),直到所有字段都是原始对象的独立副本。对于二维数组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 = 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类内部封装深拷贝逻辑(推荐)

将深拷贝的逻辑封装到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 = 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();
        }
    }
}

4. 注意事项与最佳实践

  • 引用类型与值类型: 始终牢记Java中对象变量存储的是引用,而非对象本身。这是理解集合行为的关键。
  • 性能考量: 深拷贝会创建新的内存区域并复制数据,这会带来额外的内存开销和CPU时间。对于大型或频繁操作的对象,深拷贝可能会影响性能。在性能敏感的场景,需要仔细权衡是否必须进行深拷贝,或者是否有其他设计模式(如不可变对象、写时复制等)可以替代。
  • 不可变对象: 如果对象是不可变的(Immutable),即其状态一旦创建就不能被修改,那么就无需进行深拷贝,直接传递引用即可。Java中的String、Integer等包装类就是不可变对象。
  • Java命名规范: 遵循Java的命名约定可以提高代码的可读性和维护性。变量和方法名通常应使用小驼峰命名法(creaMatriz而非CreaMatriz,m而非M作为局部变量)。类名使用大驼峰命名法(Matrices)。

5. 总结

在Java中处理集合与可变对象时,理解引用语义至关重要。当需要将可变对象(如数组)的不同状态保存到集合中时,必须通过深拷贝来确保每个集合元素都拥有其独立的数据副本,从而避免因引用共享导致的状态混乱。将深拷贝逻辑封装在对象内部(如set方法或构造函数中)是推荐的做法,它能提高代码的健壮性和可维护性。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
数据类型有哪几种
数据类型有哪几种

数据类型有整型、浮点型、字符型、字符串型、布尔型、数组、结构体和枚举等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

309

2023.10.31

php数据类型
php数据类型

本专题整合了php数据类型相关内容,阅读专题下面的文章了解更多详细内容。

222

2025.10.31

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

443

2023.08.02

java中boolean的用法
java中boolean的用法

在Java中,boolean是一种基本数据类型,它只有两个可能的值:true和false。boolean类型经常用于条件测试,比如进行比较或者检查某个条件是否满足。想了解更多java中boolean的相关内容,可以阅读本专题下面的文章。

350

2023.11.13

java boolean类型
java boolean类型

本专题整合了java中boolean类型相关教程,阅读专题下面的文章了解更多详细内容。

29

2025.11.30

go语言 面向对象
go语言 面向对象

本专题整合了go语言面向对象相关内容,阅读专题下面的文章了解更多详细内容。

56

2025.09.05

java面向对象
java面向对象

本专题整合了java面向对象相关内容,阅读专题下面的文章了解更多详细内容。

52

2025.11.27

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

443

2023.08.02

Python 自然语言处理(NLP)基础与实战
Python 自然语言处理(NLP)基础与实战

本专题系统讲解 Python 在自然语言处理(NLP)领域的基础方法与实战应用,涵盖文本预处理(分词、去停用词)、词性标注、命名实体识别、关键词提取、情感分析,以及常用 NLP 库(NLTK、spaCy)的核心用法。通过真实文本案例,帮助学习者掌握 使用 Python 进行文本分析与语言数据处理的完整流程,适用于内容分析、舆情监测与智能文本应用场景。

10

2026.01.27

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.9万人学习

C# 教程
C# 教程

共94课时 | 7.7万人学习

Java 教程
Java 教程

共578课时 | 52.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号