咱们先回顾一下基本概念java
参数在编程语言中是执行程序须要的数据,这个数据通常保存在变量中。在Java中定义一个方法时,能够定义一些参数,
举个例子:编程
public class Example { public static void main(String[] args) { String myName = "hawk"; sayYourName(myName);// 实际参数是myName } public static void sayYourName(String name) {// 形式参数是name System.out.println(name); } }
上面的代码中定义一个名为sayYourName的方法,若是想要执行这个方法,那么你须要传入一个String类型的变量给这个方法,定义这个方法时声明的String类型的name就是形式参数,而在这个方法执行时传入的myName就是实际参数。编程语言
实际参数是调用有参方法的时候真正传递的内容,而形式参数是用于接收实参内容的参数。函数
按值传递(call by value)lua
按值传递,就是指在调用函数时,将实参对应的值作一个拷贝指向函数对应的形参。在函数内改变形参对应的值并不会影响外部实参的值设计
按引用传递 (call by reference)code
按引用传递,是指在调用函数时,传递给函数的是实参的地址即引用,而不是实参的拷贝。在函数内部参数的值,对外部的实参是可见的。对象
按共享传递 (call by sharing)内存
按共享传递,是指在调用函数时,传递给函数的是实参的地址的拷贝(若是实参在栈中,则直接拷贝该值)。在函数内部对参数进行操做时,须要先拷贝的地址寻找到具体的值,再进行操做。若是该值在栈中,那么由于是直接拷贝的值,因此函数内部对参数进行操做不会对外部变量产生影响。若是原来拷贝的是原值在堆中的地址,那么须要先根据该地址找到堆中对应的位置,再进行操做。由于传递的是地址的拷贝因此函数内对值的操做对外部变量是可见的。get
按共享传递能够理解为按值传递的一个特例,这里的值是对象的引用地址,而不是具体对象。
上面描述的传参方式其实是函数调用时参数的求值策略(Evaluation Strategy),这个其实比较好理解,好比咱们调用上面的sayYourName()方法
咱们能够这样:sayYourName("hawk");
能够这样:sayYourName("ha"+"wk");
能够这样:sayYourName("hawk"+x.subString(2));// 此处的x是一个变量,进行截取后和"hawk"合并
可是全部这些实参的形式,都统称为表达式(Expression)。求值(Evaluation)便是指对这些表达式的简化并求解其值的过程。
求值策略(值传递和引用传递)的关注的点在于,这些表达式在调用函数的过程当中,求值的时机、值的形式的选取等问题。求值的时机,能够是在函数调用前,也能够是在函数调用后,由被调用者本身求值。这里所谓调用后求值,能够理解为Lazy Load或On Demand的一种求值方式。
并且,除了按值传递和按引用传递,还有一些其它的求值策略。这些求值策略的划分依据是:求值的时机(调用前仍是调用中)和值自己的传递方式。详见下表:
求值策略 | 求值时间 | 传值方式 |
按值传递( pass by value ) | 调用前 | 值的结果(只是结果的副本) |
按引用传递( pass by reference ) | 调用前 | 原值(原始对象,无副本) |
按名传递( pass by name ) | 调用后(用到才求值) | 与值无关的一个名 |
按值传递 | 按引用传递 | |
本质区别 | 建立副本 | 不建立副本 |
表现结果 | 函数中没法改变原始对象 | 函数中能够改变原始对象 |
只要是按值传递,无论传递的参数类型是值类型仍是引用类型,都会在调用栈上面建立一个实参的副本,区别只是若是传递的参数类型是值类型,该副本就是实际参数的值的复制,而对于引用类型来讲,引用类型的实例(即对象)是保存在堆中的,在栈上只有一个该实例的引用(通常状况下是该实例在堆中的内存地址),此时,实际参数的副本并非该对象,而是引用的副本。
因此,综上所述,对于Java的函数调用方式最准确的描述是:参数藉由值传递方式,传递的值是个引用。(句中两个“值”不是一个意思,第一个值是evaluation result,第二个值是value content)
说到Java是值传递仍是引用传递,通常都会举下面三个例子,咱们来一一说明一下:
第一个例子,基本类型参数传值:
public class Example { public static void main(String[] args) { int i = 10; changeValue(i); System.out.println(i); } public static void changeValue(int a ) { a = 20; } }
上面的代码解释以下:
定义了一个int类型的变量i,并赋值为10
调用changeValue方法,传入i,changeValue方法将传入的变量赋值为20
输出i
结果是:10
changValue方法内部对形参a的修改并无影响到实参i
第一个结论:Java中基本类型的传值方式是按值传递
第二个例子,引用类型传值:
public class Example { public static void main(String[] args) { String i = "hawk"; changString(i); System.out.println(i); } public static void changString(String a ) { a = "HAWK"; } }
上面的代码解释以下:
定义一个String类型的变量i,赋值为"hawk"
调用changeString方法,将传入的变量赋值为"HAWK"
输出i
结果: hawk
在Java中String类型虽然能够直接赋值,可是是引用类型,由于String类型的具体值保存在堆中,而不是栈上
既然String是引用类型, 上面的例子又说明String是值传递的 那么咱们是否是就能够得出:
Java中引用类型也是值传递的,这样的结论呢?
咱们来看第三个例子:
public class Example { int i = 20; public static void main(String[] args) { Example ex = new Example(); changValue(ex); System.out.println(ex.i); } public static void changValue(Example e ) { e.i = 30; } }
上面的代码解释以下:
实例化一个Example对象ex,ex中只有一个属性i,初始值为20
调用changValue方法,将ex中的属性i的值赋值为30
输出ex中属性i的值
结果是:30
这样的结果就让不少人产生了困惑,String类型是引用类型,对象也是引用类型,为何第二个例子不能改变i的值,第三个例子却改变了ex中i的值呢?Java中引用类型的参数传递究竟是引用传递仍是值传递呢?
其实,第二个例子和第三个例子咱们所关注的点已是错误的了,天然没法得出正确的结论
咱们回顾一下引用传递和值传递的本质区别和行为表现上的区别
按值传递 | 按引用传递 | |
本质区别 | 建立副本 | 不建立副本 |
表现结果 | 函数中没法改变原始对象 | 函数中能够改变原始对象 |
重要依据:若是能够改变对象的引用,就说明是引用传递,若是不能改变对象的引用就说明是值传递
下面咱们多举一个例子:
public class Example { int i = 20; public static void main(String[] args) { Example ex = new Example(); changValue(ex); System.out.println(ex.i); } public static void changValue(Example e ) { e = new Example(); e.i = 50; } }
这个例子和上面第三个例子很类似,区别在于:
changValue方法中将一个新的原始对象的引用赋值给了形参e
若是Java是按引用传递的话,e = new Example();就是修改了实参ex的引用,即就是改变了原始对象
若是Java是按值传递的话,实参ex不会有任何变化
结果:20
这个结果说明:changValue方法并无修改到ex的引用,也就是说,e只是ex的副本,对e的引用进行的全部操做都不会影响到ex,因此咱们
从上述代码运行的结果也能够证实Java中是只有值传递的
为何说Java中只有值传递
按值传递、按引用传递、按共享传递
为何 Java 只有值传递,但 C# 既有值传递,又有引用传递,这种语言设计有哪些好处?