JAVA语法:Java值传递和引用传递

为什么说java是按值传递
为什么说Java中只有值传递
求值策略

C语言中的直接传值和指针传值
值传递传递的是拷贝值,如何处理和原对象不搭噶,引用传递,传递的指向原对象的地址,相当于修改原对象。

/ 引子 /

public class Point {
    private int x;
    private int y;

    private Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    private void setLocation(int x, int y) {
        this.x = x;
        this.y = y;
    }

    private static void modifyPoint(Point p1, Point p2) {
        p1= new Point(5, 5);
        p2.setLocation(5, 5);
    }

    public static void main(String[] args) {
        Point p1 = new Point(0, 0);
        Point p2 = new Point(0, 0);
        modifyPoint(p1, p2);
        System.out.println("[" + p1.x + "," + p1.y + "],[" + p2.x + "," + p2.y + "]");
    }
}

上面程序输出的结果是什么样的呢?你可以思考一下

[0,0],[5,5]

和你想的是否有出入我不知道,和我想的确实是有出入的,我以为会是

[5,5],[5,5]

/ 值与引用:为什么p1的值没有改变,p2却改变了? /

以上面面试题为例,有 Point p1 = new Point(0, 0);
在这里插入图片描述
变量p1里存储着实际对象的地址,一般称这种变量为"引用",引用指向实际对象,我们称实际对象为该引用的值;我们只有更改堆里的数据,实际对象才会发生更改。

而赋值操作符=将引用指向值的地址的工作
p1 = new Point(5,5)情形就是这样:
在这里插入图片描述
看到了没,他新开辟了一个堆内存,使得P1这个引用指向了新的对象的地址,而不是我们原来的那个对象的地址了。
要注意到,在堆中的对象Print(0,0) 并没有发生改变,改变的只有引用p1 指向的地址。

值与引用就是以上这些,而值传递和引用传递,和这些一点关系也没有。

终于来了一篇我能看懂的文章了,感觉有C语言基础会更容易理解。
Java中原始数据类型传值和引用类型传值可以看成C语言中的直接传值和指针传值。
赋值时,如果只是对实参变量赋值,并不会影响传入的变量的值,而对引用变量的实例域的取值和赋值在Java中是经过了一个类似于C语言中的解指针操作的,就像C语言中对对实参进行解指针操作后再赋值,这样是会改变传入的变量的值的。
所以Java中的方法传值时,原始数据类型传的是拷贝值,引用类型的传的是引用的拷贝值。
最后吐槽一点,文章开头的举例中modify Point方法有点绕,解题时间完全用在了调用setLocation方法的变量到底是哪个变量上,找到后答案就出来了。
这都是我个人的理解,有不妥或理解有误的地方,还请各位指正:)

/ 值传递与引用传递 /

值传递(Call by value)和引用传递(Call by reference),描述的是函数调用时参数的求值策略(Evaluation strategy),是对调用函数时,求值和取值方式的描述,而非传递的内容。

即,值与引用描述了两种内存分配方式,值在堆上分配,引用在栈上分配(在这里要区分值类型和引用类型)。而值传递和引用传递描述的则是参数求值策略,两者之间不存在任何依赖关系。

我们可以找到网上资料对值传递的定义,大多是这样的:

在方法调用时,传入方法内部的是实参引用的拷贝,因此对形参的任何操作都不会影响到实参。

这句话本身对值传递的定义是比较准确的,但由于概念盲区造成的前后矛盾,让我们理解起来产生了歧义,概况起来歧义有三:

既然传递的是引用,那么应该是引用传递才对,为什么叫值传递;
引用本身也是值,本质就是个指针,所以所有传递都是值传递;
大部分会被上述概念的后半句“形参的任何操作都不会影响到实参”误导,因为我们很容易就可以做到使用形参改变实参,比如上述面试题中的p1.setLocation(5, 5);或下面代码:

public static void main(String[] args) {
    List<String> colorList = new ArrayList<>();
    colorList.add("BLUE");
    colorList.add("RED");
    colorList.add("GRAY");
    System.out.println(colorList);
    removeFirst(colorList);
    System.out.println(colorList);
}

private static void removeFirst(List colorList) {
    if (!colorList.isEmpty()) colorList.remove(0);
}

// [BLUE, RED, GRAY]
// [RED, GRAY]

很多时候我们对值传递有疑惑,本质原因是没有弄明白值传递和引用传递到底是在描述什么,我们错以为它们的名字是自解释的:传递的是值就是值传递,传递的是引用就是引用传递,Java是值传递意味着java在方法调用时传递的是值。这就是我们上面说的概念盲区,对值传递和引用转递的正确解释其实是这样的:

值传递(Call by value)和引用传递(Call by reference),描述的是函数调用时参数的求值策略(Evaluation strategy),是对调用函数时,求值和取值方式的描述,而非传递的内容。

值与引用描述了两种内存分配方式,值在堆上分配,引用在栈上分配(在这里要区分值类型和引用类型)。而值传递和引用传递描述的则是参数求值策略,两者之间不存在任何依赖关系。

综上,我们可以得出Java是值传递这种说法也是不准确的,完整的说法应该是 java在函数调用时采用的求值策略为值传递。 严格意义上讲,求值策略也不仅仅有值传递和引用传递,还有共享对象传递(Call by sharing)和值-返回传递(Call by copy-restore),但这些概念对于我们理解Java值传递并无益处,如果感兴趣可以点击链接参见详细。

理清了值传递和引用传递所描述的具体内容,我们现在来看关于Java值传递概念的第三点歧义:形参的任何操作都不会影响到实参。以上述colorList 代码为例,我们已经知道值传递这种求值策略在函数调用时会把实参引用的拷贝传入函数内,所以在调用removeFirst() 方法时有:

形参colorList’作为实参colorList的拷贝,它们寄存着一样的地址,都指向堆中的同一个实例ColorList [BLUE, RED, GRAY] 。

我们在removeFirst() 方法内操作的是形参colorList’。但是刚刚说到,无论是形参还是实参,它们都只会存储实例对象的地址,真正的对象仍是堆中它们共同指向的ColorList 。而我们在removeFirst() 方法内调用的remove() ,是实例对象ColorList 本身提供的可以改变自身的函数。所以,当removeFirst() 方法内执行colorList’.remove(0) 后,形参colorList’ 所指向的实例对象ColorList 就变成了ColorList[RED, GRAY] ,而实参colorList仍然指向了这个实例对象,所以当方法调用完成后,实例对象改变了。

这样看来,关于“形参的任何操作都不会影响到实参”确实是不严谨的,那么这句话在什么情况下生效呢,我们将removeFirst() 方法改成如下代码:

private static void removeFirst(List colorList) {
    //if (!colorList.isEmpty()) colorList.remove(0);
    if (!colorList.isEmpty())
        colorList = colorList.subList(1, colorList.size());
}

//再次运行的结果为:
//[BLUE, RED, GRAY]
//[BLUE, RED, GRAY]

在这个新的removeFirst() 方法中有:

按照值传递的求值策略规定,传入实参引用的拷贝这一步没变。在“值与引用”小节中我们说到 赋值操作符的实际工作是将引用指向值,在这个removeFirst() 方法中我们将形参colorList’ 的引用做了重赋值操作,所以现在它指向了一个新的实例ColorList [ RED, GRAY] ,但是随着removeFirst() 方法执行完毕后退出,形参colorList’也会被回收,而实参colorList指向的ColorList [BLUE, RED, GRAY] 并没有发生改变(变的是形参指向的实例,但是在方法退出后,方法参数colorList’ 就会被回收,而它指向的实例也就会变成垃圾对象)。

关于值传递概念的三点歧义就解释完毕了,我们在这里可以给出java值传递一个正确完整的概念了:

java 使用的是一种名为值传递的求值策略,这种策略在传值过程中会复制实参的引用,并将这份拷贝传入到方法形参中,所以对形参的任何重赋值操作都不会对实参产生影响。

在下一小节中我们会解析开篇的面试题,如果你对Java值传递仍然理解不好,我会在下一节中分享一个技巧帮助你更彻底的弄明白Java值传递。

/ 面试题解析 /

回到开篇的面试题,在modifyPoint() 方法中,头三行代码使用了一个临时变量tmpPoint 互换了形参p1 和p2 的值,所以在方法内部,形参p1 实际上指向了实参p2 ,形参p2 指向了实参p1 :

Point tmpPoint = p1;
p1 = p2;
p2 = tmpPoint;

关于Java的值传递策略,有一种比较取巧的理解,就是完全可以把函数的传参过程等同于赋值操作符=来理解,我们在调用modifyPoint(p1,p2)时,可以理解在方法内部有形参p1’=实参p1,形参p2’=实参p2,这样我们在方法里操作的p1’和p2’实际只是个临时变量,它的生命周期仅限于方法里。

在形参p1’ 与形参p2’ 互换后,堆栈信息简易表示为:

然后调用了p1’.setLocation(5, 5); 我们在上节说过,如果实例对象本身提供了改变自身的方法,那么在形参调用该方法后也会改变实参的,因为它们都指向了同一个实例,所以这时实参p2 也变为Point[5.5] 。

代码走到下一行p2’ = new Point(5, 5); 基于整篇文章的论述,这一行我们可以直接跳过不管了,因为形参的重赋值操作不会影响到实参。最后的堆栈信息如下:

所以最终答案显而易见:[0,0],[5,5]

推荐阅读:
https://mp.weixin.qq.com/s/lP-xB5yzpvqVEiA439euVQ
看完这个文章我又迷茫了

首先放一下我觉得有道理的概念:

“在Java里面参数传递都是按值传递”这句话的意思是:按值传递是传递的值的拷贝,按引用传递其实传递的是引用的地址值,所以统称按值传递。

值传递:

在方法调用时,传递的参数,是按值的拷贝传递,方法内改变的是值的拷贝,并不改变原数据。传递的是值的拷贝,也就是说传递后就互不相关了。

引用传递:

指的是在方法调用时,是把对象在内存中的地址值拷贝了一份传给了被调用方(也就是同一个内存空间),所以你更改了同一个内存空间的数据,原数据也会改变。

总结

但是copy这个事情很浪费性能。如果是很复杂的数据,你把整个数据copy一份传过去,非常的耗费性能。所以一般复杂的数据,都使用的引用传递,只是copy了地址值传过去。比如对象,比如数组。

String虽然是对象但是他比较特殊,String类型的变量作为参数时,无论是new出对象,而是直接用字符串赋值,都会像基本类型变量那样以传值方式传递(字符串直接赋值和用new出的对象赋值的区别仅仅在于存储方式不同)。

在这个问答里的解释我觉得很靠谱String类作为参数传递时他的值改变么。该怎样理解?

引用传递和值传递 是不一样的,如果传递的是基本数据类型例如 int ,long boolean 之类的就会改变了,String是特殊的基本类型,本质上是引用类型
String类是final的,而且运用了享元模式,值是不能改变的,但是相同值的多个引用,他们其实在内存中指向的是相同的(这也是java中String对象可以使用==来判断是否相同了),所以很多时候String使用起来很像基本类型,但它确实是面向对象的。
基础介绍完了来回答上面的问题,s传入change函数,因为s本身是不能改变的,在s上自增一个量,实际是将原来的s与要增加的量拼接后赋值给一个新的对象,在内存中是一块全新的,然后将参数中的s指向这块内存。别急~
而String作为参数时,前面也提到String应用了享元模式,传值时,实际传递的是s原来值(引用)的副本,不过这两者都指向一块相同的内存。所以,在第二段中,让s重新指向拼接完成后的值时,并不会改变主函数中s的值。
那么这整个过程,就是定义一个s,内存中分配内存后让s指向它,调用change函数,传入s的副本(另一个引用,不过与正真的s指向相同的内存),在函数体中让这个s的副本重新指向新的内存,并返回他。
以上内容为个人理解,不过不用想得太复杂,只要记住String是对象,但是作为参数是比较特殊,可看作是值传递,而不是引用传递。

最后

这事我是问我们老板的,然后我们老板说,虽然有些数据类型默认是引用传递,但是很多语言里都有关键字,可以更改它的传值类型,比如c++里面的r什么玩意的,括弧,这句我没听懂。从而控制被调用方法不能任意修改原始数据

然后我就说,我一般都是把对象序列化之后传出去,然后再序列化回来,这样去控制两个不是一个对象。老板哭,说居然有这么野鸡的代码

他说序列化非常非常耗费性能,我这代码写得就是垃圾

为了搞明白一个问题,我把短板暴露给老板了
是的,我的Java基础很差,大学唯一挂的科就是JAVA
真不知道我后来为什么要去做开发

附我觉得很有道理的大神解答链接,当当当,跟着测试一遍你就全明白啦(很实在的解答)
理解Java中的引用传递和值传递

String作为方法参数传递 与 引用传递

20190815续更
1、java中控制两个不是一个对象是使用的clone方式,也就是拷贝,其中又分为深拷贝和浅拷贝
2、在c++中的&引用符号(C语言并没有&的概念。引用是C++里面的)
如下图代码中所以,虽然两个方法,都是直接swap(a,b)调用,但是第一种方法,接受的形参,是a和b的引用,这种情况它指代的是实际的对象,在方法体内交换两者数据,main方法体中的元数据a,b是会随之同步更改的。
而方法二,方法里面怎么改数据,原数据都不会变的。

总结:在一般传值调用的机制中只能把实参传送给形参,而不能把形参的值反向地传送给实参。因此在函数调用过程中,形参值发生改变,而实参中的值不会变化。而在引用调用的机制当中是将实参引用的地址传递给了形参,所以任何发生在形参上的改变实际上也发生在实参变量上。

方法一:
void swap(int &p1, int &p2) //此处函数的形参p1, p2都是引用
{ int p; p=p1; p1=p2; p2=p; }

方法二:
void swap(int p1, int p2) 
{ int p; p=p1; p1=p2; p2=p; }
主函数
main( )
{
 int a,b;
 cin>>a>>b; //输入a,b两变量的值
 swap(a,b); //直接以变量a和b作为实参调用swap函数
 cout<<a<< ' ' <<b; //输出结果
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值