方法的参数传递
在具体理解形参和实参的区别前,我们需要明白几个基本概念:
- 形参:形式参数,方法声明时的参数,它用来接收调用者传递来的实参;它只有在被调用时JVM才会为其分配内存单元,调用结束后回收内存单元,因此它的作用域只限于方法内部,对于形参的改变不会影响实参
- 实参: 方法调用时实际传给形参的参数值,实参在传递给其他参数时要先赋值
Java里方法的参数传递只有值传递一种:值传递! 即将实际参数值得副本传入方法,而参数本身不受影响。
- 值传递:如果方法的形参是基本数据类型,那么实参(实际的数据)像形参传递参数时,就是把实参的值复制给了形参
- 引用传递:如果方法的形参是对象,那么实参(实际的对象)向形参传递参数时也是把值传递给形参,而这个值是实参在栈内存中的值,这个值也是引用对象在堆内存种的地址
那么什么时候是值传递,什么时候是引用传递呢?顾名思义,当实参为Java中的八种基本数据类型时,在调用方法传递实参时就是值传递;当实参是除了八种基本类型之外的对象时,调用方法时就是引用传递,即传递给被调用方法的时对象在堆内存的地址。
- 基本数据类型(八种):整数类型(byte, short, int, long)、浮点类型(float, double)、字符类型(char)、boolean类型(true, false)
- long型变量赋值时后面要在值后面跟上字母
l
- float型变量赋值时要在值后面跟上字母
f
- byte、int、long、和short都可以用十进制、16进制以及8进制的方式来表示。当使用常量的时候,前缀 0 表示 8 进制,而前缀 0x 代表 16 进制
package com.company; public class Main { public static void main(String[] args) { // byte System.out.println("基本类型:byte 二进制位数:" + Byte.SIZE); System.out.println("包装类:java.lang.Byte"); System.out.println("最小值:Byte.MIN_VALUE=" + Byte.MIN_VALUE); System.out.println("最大值:Byte.MAX_VALUE=" + Byte.MAX_VALUE); System.out.println(); // short System.out.println("基本类型:short 二进制位数:" + Short.SIZE); System.out.println("包装类:java.lang.Short"); System.out.println("最小值:Short.MIN_VALUE=" + Short.MIN_VALUE); System.out.println("最大值:Short.MAX_VALUE=" + Short.MAX_VALUE); System.out.println(); // int System.out.println("基本类型:int 二进制位数:" + Integer.SIZE); System.out.println("包装类:java.lang.Integer"); System.out.println("最小值:Integer.MIN_VALUE=" + Integer.MIN_VALUE); System.out.println("最大值:Integer.MAX_VALUE=" + Integer.MAX_VALUE); System.out.println(); // long System.out.println("基本类型:long 二进制位数:" + Long.SIZE); System.out.println("包装类:java.lang.Long"); System.out.println("最小值:Long.MIN_VALUE=" + Long.MIN_VALUE); System.out.println("最大值:Long.MAX_VALUE=" + Long.MAX_VALUE); System.out.println(); // float System.out.println("基本类型:float 二进制位数:" + Float.SIZE); System.out.println("包装类:java.lang.Float"); System.out.println("最小值:Float.MIN_VALUE=" + Float.MIN_VALUE); System.out.println("最大值:Float.MAX_VALUE=" + Float.MAX_VALUE); System.out.println(); // double System.out.println("基本类型:double 二进制位数:" + Double.SIZE); System.out.println("包装类:java.lang.Double"); System.out.println("最小值:Double.MIN_VALUE=" + Double.MIN_VALUE); System.out.println("最大值:Double.MAX_VALUE=" + Double.MAX_VALUE); System.out.println(); // char System.out.println("基本类型:char 二进制位数:" + Character.SIZE); System.out.println("包装类:java.lang.Character"); // 以数值形式而不是字符形式将Character.MIN_VALUE输出到控制台 System.out.println("最小值:Character.MIN_VALUE=" + (int) Character.MIN_VALUE); // 以数值形式而不是字符形式将Character.MAX_VALUE输出到控制台 System.out.println("最大值:Character.MAX_VALUE=" + (int) Character.MAX_VALUE); } } /* 基本类型:byte 二进制位数:8 包装类:java.lang.Byte 最小值:Byte.MIN_VALUE=-128 最大值:Byte.MAX_VALUE=127 基本类型:short 二进制位数:16 包装类:java.lang.Short 最小值:Short.MIN_VALUE=-32768 最大值:Short.MAX_VALUE=32767 基本类型:int 二进制位数:32 包装类:java.lang.Integer 最小值:Integer.MIN_VALUE=-2147483648 最大值:Integer.MAX_VALUE=2147483647 基本类型:long 二进制位数:64 包装类:java.lang.Long 最小值:Long.MIN_VALUE=-9223372036854775808 最大值:Long.MAX_VALUE=9223372036854775807 基本类型:float 二进制位数:32 包装类:java.lang.Float 最小值:Float.MIN_VALUE=1.4E-45 最大值:Float.MAX_VALUE=3.4028235E38 基本类型:double 二进制位数:64 包装类:java.lang.Double 最小值:Double.MIN_VALUE=4.9E-324 最大值:Double.MAX_VALUE=1.7976931348623157E308 基本类型:char 二进制位数:16 包装类:java.lang.Character 最小值:Character.MIN_VALUE=0 最大值:Character.MAX_VALUE=65535 */
- 引用数据类型: 除了八种基本数据类型之外的都是引用类型,如:
- String:由0或多个字母数字符号组成的串;可由null初始化;值不可变
更详细内容可见:Java 基本数据类型
JVM的内存需要划分为5个部分:
- 栈(Stack):存放的是方法中的局部变量
- 局部变量:方法的参数或是是方法{}内部的变量
- 作用域:一旦超出作用域,立刻从栈内存中消失
- 堆(Heap):凡是new出来的东西都在堆中,堆内存的里面的东西都有一个16进制地址;堆内存里面的数据都有默认值:
- 整形:0
- 浮点型:0.0
- 字符:“\u0000”
- 布尔:false
- 引用类型:null
- 方法区(Method Area):存储.class相关信息,包含方法的信息
- 本地方法栈(Native Method Stack):与OS有关
- 寄存器(pc Register):与CPU有关
我们重点关注前三个部分,它们在JVM内存的简要示意图如下所示:
下面先看一个值传递的例子:
public class argsTest {
public static void main(String[] args) {
int a = 1;
int b = 2;
System.out.println("没有交换前:" + "a = " + a + ",b = " + b);
swapNumbers(a, b);
System.out.println("交换后:" + "a = " + a + ",b = " + b);
}
private static void swapNumbers(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("交换中:" + "a = " + a + ",b = " + b);
}
}
/**
* OUTPUT:
* 没有交换前:a = 1,b = 2
* 交换中:a = 2,b = 1
* 交换后:a = 1,b = 2
*/
如下图所示,当在main()
中初始化a,b
时,由于它们属于基本数据类型,因此JVM会为a
和b
在栈中分配内存空间用于存放初始化的值。而当在调用swapNumbers(int a, int b)
方法时,main()
中swapNumbers(a, b)
中的a
和b
是最初a和b的副本,JVM会为它们另外开辟空间存放,由于它们在栈中存放的地址不同,因此被调用方法内对于a
和b
的改变不影响最初的值。
下面再看一个引用传递的例子:
package com.company;
class Data{
int a = 1;
int b = 2;
}
public class argsTest {
public static void main(String[] args) {
Data d = new Data();
System.out.println("没有交换前:" + "a = " + d.a + ",b = " + d.b);
swapNumbers(d);
System.out.println("交换后:" + "a = " + d.a + ",b = " + d.b);
}
private static void swapNumbers(Data d) {
int temp = d.a;
d.a = d.b;
d.b = temp;
System.out.println("交换中:" + "a = " + d.a + ",b = " + d.b);
}
}
/**
* OUTPUT:
* 没有交换前:a = 1,b = 2
* 交换中:a = 2,b = 1
* 交换后:a = 2,b = 1
*/
而当我们传递给swapNumbers()
的是一个类对象时,实际传递的是该引用对象在堆中的地址。swapNumbers()
中的对象和main()
中的对象虽然在栈中地址不同,但它们保存的都是相同的内容,即所指向的引用对象在堆中的地址。因此,swapNumbers()
中对于对象的改变实际上直接改变了堆中的对象,那么main()
中的对象的值自然也就发生了改变。
如下所示,当执行Data d = new Data()
时,JVM首先会在堆中开辟空间存放创建的new Data()
,然后在栈中为d
开辟空间存放new Data()
的地址,即d
的值。然后当执行swapNumbers()
时,JVM首先为其在栈中开辟空间存放传入的对象在堆中的地址,然后执行下面的流程。因此,它们指向的是堆中同样的对象,swapNumbers()
中对于对象的改变也会影响main()
中d
对应的值。
Java 形参与实参 中讲解了关于包装类型在作为形参和实参时的一些情况,深入理解可以继续往下看~