记录一个Java引用传递的问题

Java中引用传递的陷阱与解决办法
本文探讨了Java中的值传递和引用传递,强调了引用类型变量如何存储对象引用而非对象本身。通过示例代码,解释了引用传递可能导致的意外修改原对象问题,并分享了解决这个问题的方法,即使用对象拷贝。同时提到了在ArrayList和HashMap中遇到的问题,以及如何利用MapStruct等工具进行对象转换。

往期精选(欢迎转发~~)

前言

前两个月一直写PHP去了,感觉好久没有写Java代码,因为最近要重构公司的Java项目,又把Java捡起来。在写代码过程中,有一个点之前其实没太注意,差点踩一脚,就通过这篇文章简单记录一下。

Java的值传递和引用传递

Java中数据类型分为两大类,基本类型和对象类型。相应的,变量也有两种类型:基本类型和引用类型。

  • 基本类型的变量保存原始值,即它代表的值就是数值本身;

  • 引用类型的变量保存引用值,"引用值"指向内存空间的地址,代表了某个对象的引用,而不是对象本身,对象本身存放在这个引用值所表示的地址的位置。

值传递只局限于四类八种基本数据类型:

除掉这四类八种基本类型,其它的都是对象,也就是引用类型,包括:类类型,接口类型和数组。

至于堆栈的内存分配,这个我就不画,也不举例,网上的资料很多。

引用传递

我们ArrayList为例看看“引用传递”,我定义了下面两个对象:

@Data
@NoArgsConstructor
public class DeptDTO {
    /**
     * 部门ID dept_code
     */
    private String deptCode;

    /**
     * 部门名称 dept_name
     */
    private String deptName;

    /**
     * 部门级别 dept_level
     */
    private Integer deptLevel;
}

示例代码:

public static void main(String[] args) {
    DeptDTO dept1 = new DeptDTO();
    DeptDTO dept2 = new DeptDTO();

    dept1.setDeptCode("111");;
    dept2.setDeptCode("222");;

    List<DeptDTO> deptList = new ArrayList<>();
    deptList.add(dept1);
    deptList.add(dept2);

    for (DeptDTO deptRecord : deptList) {
        System.out.println("Print deptCode:" + deptRecord.getDeptCode());
    }

    // 获取里面的元素,其实是获取的引用,修改该元素值,会影响personList的值
    DeptDTO dept = deptList.get(0);
    dept.setDeptCode("333");

//        // 修改该值,会直接影响deptList的值,证明deptList.add()存入的是对象的引用
//        dept.setDeptCode("444");

    for (DeptDTO deptRecord : deptList) {
        System.out.println("Print deptCode:" + deptRecord.getDeptCode());
    }
}

// 输出:
// Print deptCode:111
// Print deptCode:222
// Print deptCode:333
// Print deptCode:222

我们发现,获取deptList里面的值,然后进行修改,会直接影响到deptList里面的值,是因为我们拿到是deptList里面值的引用。可能大家觉得这个非常简单,但是写习惯Go和PHP的同学,会有些不适应,比如PHP获取数组的数据,是值传递,不是引用传递,如果你需要修改里面的数据,需要通过deptList对应的索引值直接进行修改。

我们放开上面的注释:

// 修改该值,会直接影响deptList的值,证明deptList.add()存入的是对象的引用
dept1.setDeptCode("444");

// 输出:
// Print deptCode:111
// Print deptCode:222
// Print deptCode:444
// Print deptCode:222

可以发现dept、dept1和deptList.get(0),永远都指向同一块地址区域,我们简单验证一下,最后面加入代码:

System.out.println(System.identityHashCode(dept));
System.out.println(System.identityHashCode(dept1));
System.out.println(System.identityHashCode(deptList.get(0)));
// 输出:
// 1612799726
// 1612799726
// 1612799726

问题引入

其实我的问题主要是使用ArrayList和HashMap时遇到引用传递的问题,我们再加一个对象:

@Data
public class DeptCalcuateDTO  extends DeptDTO {
    /**
     * 计算数据
     */
    private Integer calculateNumber;
}

其实我要的功能很简单,就是有一个List< DeptDTO > A,现在我需要将A中的数据转为DeptTreeDTO B,我写了如下代码:

DeptCalcuateDTO deptCalcuateDTO = (DeptCalcuateDTO) dept1;
deptCalcuateDTO.setDeptCode("333");

假如上面的语法成立,因为是引用传递,所以修改deptCalcuateDTO会影响到A中的数据,但是A是基础数据,是不允许修改的,这样就违背了我的初衷。

其实后来才发现是我想多了,因为我写Deom用例时才发现上面的强制转换行不通,因为会抛出ClassCastException()异常,但是C++支持这种类型的强制转换。

解决问题

其实就是一个拷贝的问题,我可以直接把A中的数据赋值给B,但是实际情况是里面的属性太多了,我不想每次都转换,就借用了一个工具库,感觉很好用,就记录一下。

先引入包:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.2.0.Final</version>
</dependency>

添加实体转换接口:

@Mapper(componentModel = "spring")
public interface AppDeptConverter {
    AppDeptConverter INSTANCE = Mappers.getMapper(AppDeptConverter.class);

    DeptCalcuateDTO toDeptCalcuate(DeptDTO deptDTO);
}

测试用例:

public static void main(String[] args) {
    DeptDTO dept1 = new DeptDTO();
    DeptDTO dept2 = new DeptDTO();
    dept1.setDeptCode("111");
    dept2.setDeptCode("222");

    List<DeptDTO> deptList = new ArrayList<>();
    deptList.add(dept1);
    deptList.add(dept2);

    DeptCalcuateDTO deptCalcuateDTO =  AppDeptConverter.INSTANCE.toDeptCalcuate(dept1);
    deptCalcuateDTO.setDeptCode("333");

    for (DeptDTO deptRecord : deptList) {
        System.out.println("Print deptCode:" + deptRecord.deptCode);
    }
}
// 输出:
// Print deptCode:111
// Print deptCode:222

其实这个工具做的事情很简单,生成一个DeptCalcuateDTO对象,然后将A的值依次赋值给B,详细使用方式可以参考:https://mapstruct.org/

欢迎大家多多点赞,更多文章,请关注微信公众号“楼仔进阶之路”,点关注,不迷路~~

<think>好的,用户想了解在Java中如何打印某个对象被哪个方法引用的调试技巧。首先,我需要回忆Java中对象引用追踪的方法。常见的办法包括使用调试工具或分析堆栈跟踪,但用户可能想要更直接的代码级解决方案。 记得Java的弱引用引用队列(ReferenceQueue)可以用来追踪对象的回收情况,但可能无法直接显示哪个方法引用了对象。另一个思路是使用Java Management Extensions(JMX)和内存分析工具,比如jmap和jhat,但这些可能需要外部工具,用户可能希望用代码实现。 接下来,想到Java的Instrumentation API,它允许在类加载时添加字节码增强,从而跟踪对象的创建和引用。不过,这需要编写Java代理,可能比较复杂。用户可能需要更简单的方法,比如在代码中插入日志,记录对象被传递的地方。 但用户的问题更偏向于运行时查找哪个方法持有引用。此时,可能需要检查堆栈跟踪。例如,可以在对象的方法中打印当前线程的堆栈,但这只能显示当前执行路径,无法追踪所有引用。 突然想到,使用Java的反射机制可能无法直接获取对象的引用者,因为Java没有内置的API来反向查找引用。因此,可能需要借助分析工具,比如VisualVM或Eclipse MAT,这些工具可以生成堆转储并分析对象引用链。 不过用户提到“打印”某个对象被哪个方法引用,可能希望在代码中实现。这时候可能需要使用Java的调试接口,如JDI(Java Debug Interface),但这通常用于调试器,不是常规代码的一部分。 综合来看,直接通过编程方式在Java中打印对象被哪个方法引用较为困难,通常推荐使用堆转储分析工具。但可以尝试在对象的finalize()方法中输出信息,当对象被回收时,可能提示没有强引用存在,但无法确切指出引用者。 另外,参考引用内容中的[^2]提到线程锁定的资源,可能通过线程堆栈跟踪来查找对象被哪个线程持有,但仅限于同步块的情况。可以使用Thread.getAllStackTraces()获取所有线程的堆栈,检查哪些方法正在操作该对象。 总结可能的步骤:1. 生成堆转储并用MAT分析;2. 在代码中插入日志,记录对象传递;3. 使用finalize方法检测对象回收;4. 检查线程堆栈跟踪。需要根据用户的具体场景选择合适的方法,并可能需要结合外部工具。</think>在Java中,要追踪对象被哪个方法引用,可以通过以下方法实现: ### 方法一:使用堆转储分析 1. 生成堆转储文件: ```bash jmap -dump:live,format=b,file=heapdump.hprof <pid> ``` 2. 使用Eclipse Memory Analyzer (MAT)分析: - 打开`heapdump.hprof`文件 - 查找目标对象 - 右键选择"Merge Shortest Paths to GC Roots" - 查看完整的引用链,包含类和方法信息 ### 方法二:运行时堆栈追踪(针对同步对象) ```java public static void printLockHolders(Object target) { for (Thread t : Thread.getAllStackTraces().keySet()) { if (Thread.holdsLock(target)) { // 仅适用于synchronized块 System.out.println("Lock held by: " + t.getName()); for (StackTraceElement ste : t.getStackTrace()) { System.out.println("\tat " + ste); } } } } ``` ### 方法三:使用WeakReference监听(需配合引用队列) ```java ReferenceQueue<Object> queue = new ReferenceQueue<>(); WeakReference<Object> ref = new WeakReference<>(targetObject, queue); // 定期检查队列 new Thread(() -> { while (true) { Reference<?> clearedRef = queue.remove(); System.out.println("对象被回收,最后引用来自:" + clearedRef.getClass().getName()); } }).start(); ``` ### 方法四:字节码增强(需Java Agent) 1. 创建Java Agent: ```java public class TrackingAgent { public static void premain(String args, Instrumentation inst) { inst.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> { // 插入对象访问追踪字节码 return transformBytecode(classfileBuffer); }); } } ``` 2. 在MANIFEST.MF中声明: ``` Premain-Class: TrackingAgent ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值