今天读《重构》P279,

Separate Query from Modifier,将查询函数和修改函数分离。

问题的产生

突然想到 Java 的传对象作为参数的方法到底是 传引用调用,还是 传值调用?修改参数,会不会影响实参呢?

下面两个代码是不是等效的呢?

//用新值覆盖旧值,并返回
public Student updateStudentInfo(Student newStudent , Student stu){
stu.sex = newStudent.sex;
return stu;
} Student Tom = new Student(boy); Tom = updateStudentInfo(new Student(girl), Tom); //此时可爱的Tom 就从 boy 变成了 girl 了...

我就在想,如果写成下面这样是不是也对呢?

//用新值覆盖旧值
public void updateStudentInfo(Student newStudent , Student stu){
stu.sex = newStudent.sex;
} Student Girl = new Student(girl);
Student Tom = new Student(boy); updateStudentInfo(Girl, Tom); //此时,Tom就应该变成 女的了...

然后,我在网络上收集了几个例子,很有意思。

有趣的例子

例子1:交换参数值

例子1 (原文1):


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Employee {
 
  public String name=null;
 
  public Employee(String n){
    this.name=n;
  }
  //将两个Employee对象交换
  public static void swap(Employee e1,Employee e2){
    Employee temp=e1;
    e1=e2;
    e2=temp;
        System.out.println(e1.name+" "+e2.name); //打印结果:李四 张三
  }
  //主函数
  public static void main(String[] args) {
    Employee worker=new Employee("张三");
    Employee manager=new Employee("李四");
    swap(worker,manager);
    System.out.println(worker.name+" "+manager.name); //打印结果仍然是: 张三 李四
  }
}

上面的结果让人很失望,虽然形参对象e1,e2的内容交换了,但实参对象worker,manager并没有互换内容。这里面最重要的原因就在于形参e1,e2是实参worker,manager的地址拷贝。

大家都知道,在Java中对象变量名实际上代表的是对象在堆中的地址(专业术语叫做对象引用 )。在Java方法调用的时候,参数传递的是对象的引用。重要的是,形参和实参所占的内存地址并不一样,形参中的内容只是实参中存储的对象引用的一份拷贝。

如果大家对JVM内存管理中Java栈 的局部变量区 有所了解的话(可以参见《 Java 虚拟机体系结构 》),就很好理解上面这句话。在JVM运行上面的程序时,运行main方法和swap方法,会在Java栈中先后push两个叫做栈帧 的内存空间。main栈帧中有一块叫局部变量区的内存用来存储实参对象worker和manager的引用。而swap栈帧中的局部变量区则存储了形参对象e1和e2的引用。虽然e1和e2的引用值分别与worker和manager相同,但是它们占用了不同的内存空间。当e1和e2的引用发生交换时,下面的图很清晰的看出完全不会影响worker和manager的引用值。


以上是第一个例子。做了个很准确也很有原理性的图,也写出了一个很有意思的现象:交换函数的参数值,似乎并不影响实参。然而,真理到此为止,原文却进一步引申出“Java是传值调用”的鬼话。

例子2:更改参数对象的值

为什么这么说呢,我们再看例子2(原文2):


public class test {

public static void main(String[] args) {
Circle c = new Circle();
c.r = 1;
bigger(c);
System.out.println(c.r);
} public static void bigger(Circle c2){
c2.r = 3;
} class Circle{
int r;
}

输出:3

为什么会是3,看上面的原理图,一目了然。 形参 c2 虽然本身的内存值是 变量 c 的内存值的复制值,但是 c2 和 c 引用的却是【同一个对象】!此时你对 c2变量做修改: c2.r =3 ,改来改去,改的永远都是“躺在堆上”的那个对象。等 bigger 函数执行完,该函数的栈帧被释放,c2 烟消云散,但它做的更改却遗留了下来。然后你再访问 c.r 其实访问的就是刚才被修改的“躺在堆上”的对象,因为 c 引用的还是“躺在堆上”的 那个对象。

注意理解Java语言中的“引用(Reference)”一词。个人认为类似C/C++的指针。还要注意 Java 的变量和它的值,二者的区别。尤其是类和对象,二者的区别。例子2 中的 Circle c 其实只是个 变量,也就是个引用而已,类似C/C++的指针,存储的是 堆上new 出来的 Circle实例 的地址值 0x33445566。理解了这个,那么 形参 Circle c2 也就很好理解,就是另外一个 变量,也是个引用,类似C/C++的指针,存储的是 还是堆上 new 出来的那个 Circle 实例的内存地址 0x33445566。

所以,从例子2可以看出,bigger 函数 是传引用调用的,bigger 函数没有改变实参 c 的值(实参/变量 c 的值其实是躺在堆上的对象的内存地址,并没有变化),但是改变了实参 c 所引用的对象的值。

综合上面2个例子,互相印证可以发现,原文2 的一条"结论"有误:

5.总结:

(1)一个方法不能修改一个基本数据类型的参数(即数值型和布尔类型值)。对

(2)一个方法可以通过对象参数对其引用的对象状态进行操控。对

(3)一个方法不能让对象参数引用一个新的对象。错

从例子1可以看出,在方法 swap 中,形参 e1 和 e2 都引用了一个新的对象的(互相调换了彼此的引用对象)。

我的结论

综上,我的结论就是(这TM也是每本教材里的废话):

  1. 对于Java的基本数据类型的参数,函数是传值调用。
  2. 对于Java的非基本数据类型的参数,函数是传引用调用。

为什么我用“传”字,我觉得“传”字更准确,更易理解。给函数传过去的是个“引用”,你是改变引用本身呢(例子1),还是改变引用指向的对象呢(例子2),都可以,悉听尊便。

另外,为什么文章1会得出错误的结论,很明显是因为文章1在 swap 函数中 交换引用的值,根本没有修改引用的对象。

打个不恰当的比方:就是两个小偷一直在交换彼此手里的钥匙,却没有动两个房间里的东西。然后你就说这俩小偷能力不行,这不是冤枉人么。

其实,说句体外话,这两条结论可以用一条概况,那就是第2条,因为 基本数据类型和字符串常量,比如3,4,"Hello",Java的处理方式是全都放到公共的堆上,如果有

String var1 = "World";
String var2 = "World";
int var3 = 999;
int var4 = 999;

其实,var1 和 var2 ,var3 和 var4 都指向的堆上的 同一个东西。如果这些基本变量出现在形参和实参中,勉强也可以算是“按引用调用吧”。

钥匙与房间的比喻

最后的最后,再次用那个小偷和钥匙作比喻:

Circle c = new Circle()

Java的变量 Circle c 就类似房间钥匙,而房间本身类似 new Circle() 这种无名的存储在堆上的对象。

变量 c 就是 “new Circle() 这种无名的存储在堆上的对象” 的一个引用。

Circle c2;

c2 = c;

这类似于 给刚才的房价又配了一把钥匙。

变量 c2 就是 “new Circle() 这种无名的存储在堆上的对象” 的另外一个引用。

所以例子1里的 swap 函数,就类似于,房间A的钥匙和房间B的钥匙开门能力调换了一下,本来房间A的钥匙现在可以开房间B了,原来房间B的钥匙可以开房间A了。但是并没有影响房间A和房间B里面的东西。然后就惊呼“Java是传值调用”,这是不对的。

例子2 中

Circle c = new Circle();  这里的 c 是无名房间 new Circle() 的一把钥匙。

然后传引用给 bigger 函数,意思是将 钥匙 c 复制一把出来,叫钥匙 c2。

c2.r  = 3; 意思就是用新配的钥匙 c2 去打开无名房间,偷走一台彩电(更改了c和c2共同引用的对象的值)。

public static void bigger(Circle c2){
c2.r = 3;
}

<Over>

2018年10月26日

【Java-Method】读《重构》有感_Java方法到底是传值调用还是传引用调用(传钥匙调用)的更多相关文章

  1. java.lang.String中的replace方法到底替换了一个还是全部替换了。

    你没有看错我说的就是那个最常用的java.lang.String,String可以说在Java中使用量最广泛的类了. 但是我却发现我弄错了他的一个API(也可以说是两个API),这个API是关于字符串 ...

  2. Java中Thread类的join方法到底是如何实现等待

    现在的场景是A线程执行:public void run(){ bThread.join(0);//把b线程加入到当前线程(a线程),等待b结束,当前a线程才会结束.}B线程执行public void ...

  3. Json学习总结(1)——Java和JavaScript中使用Json方法大全

    摘要:JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.它基于ECMAScript的一个子集. JSON采用完全独立于语言的文本格式,但是也使用了类似于C语 ...

  4. java.lang.Math中的基本方法

    java.lang.Math类提供的方法都是static的,“静态引入 ”使得不必每次在调用类方法时都在方法前写上类名:             import static java.lang.Mat ...

  5. 关于Java对象作为参数传递是传值还是传引用的问题

    前言 在Java中,当对象作为参数传递时,究竟传递的是对象的值,还是对象的引用,这是一个饱受争议的话题.若传的是值,那么函数接收的只是实参的一个副本,函数对形参的操作并不会对实参产生影响:若传的是引用 ...

  6. java解析xml的三种方法

    java解析XML的三种方法 1.SAX事件解析 package com.wzh.sax; import org.xml.sax.Attributes; import org.xml.sax.SAXE ...

  7. java多线程-读写锁

    Java5 在 java.util.concurrent 包中已经包含了读写锁.尽管如此,我们还是应该了解其实现背后的原理. 读/写锁的 Java 实现(Read / Write Lock Java ...

  8. Java获取随机数的几种方法

    Java获取随机数的几种方法 .使用org.apache.commons.lang.RandomStringUtils.randomAlphanumeric()取数字字母随机10位; //取得一个3位 ...

  9. java.net.SocketException: Connection reset 解决方法

    java.net.SocketException: Connection reset 解决方法 最近纠结致死的一个java报错java.net.SocketException: Connection ...

随机推荐

  1. IOS Intro - UIWindow UIView and CALayer

    UIWindow.UIView以及CALayer之间的关系2016-05-11 20:46 本站整理 浏览(16) UIWindow1.简介UIWindow是一种特殊的UIView,通常在一个app中 ...

  2. Unity Transform

    public class PlayerControll : MonoBehaviour { Transform playerTransform; Animation playerAnimation; ...

  3. Kudu和HBase定位的区别

    不多说,直接上干货! Kudu和HBase定位的区别 Kudu 的定位是提供 “ast analytics on fast data” ,也就是在快速更新的数据上进行快速的查询.它定位 OLAP 和少 ...

  4. spring boot基本认识

    大家眼中的spring boot:https://www.zhihu.com/question/39483566-------------------------------------------- ...

  5. [Android]JsonObject解析

    android和服务器进行交互的时候往往会有数据的传输,而数据中有一种类型就是Json型,这两天在研究API接口的问题,服务器返回的数据类型都是Json型的.例如: 1.接收到的json字符串分为两种 ...

  6. MakeFile基本使用

    MakeFile Making makefile demo # Run this line when useing `make` command # default is the target whi ...

  7. 【ubuntu】出现device not managed连接不上网络

    ubuntu安装好后显示“device not managed” 1. 编辑/etc/NetworkManager/NetworkManager.conf: sudo gedit /etc/Netwo ...

  8. 树莓派直连线连接PC

    刚入手树莓派一天不到,SSH树莓派一直用的是路由+无线网卡的配置.想到明天就要出差了,本想把树莓派也带去,可宾馆的房间只有一个网口,通常都是兄弟们连接小型无线路由用的,连接树莓派似乎成了一个难题.于是 ...

  9. Smile with face. Smile with mind.

    Smile with face. Smile with mind.微笑不仅是挂在脸上的,更是发自心底的.

  10. [QualityCenter]QC是什么?发展历程是怎样?

    QC,即Quality Center,是一个基于Web的测试管理工具.它可以组织和管理应用程序测试流程的所有阶段,包括制定测试需求.计划测试.执行测试和跟踪缺陷.此外,通过Quality Center ...