java基础(1) 按值传递

Java 采用按值传递,方法接收的是实参值的副本。基本类型传递值的副本,引用类型传递对象地址的副本。方法内修改对象会影响原始对象,但重新赋值引用不会。可理解为“按引用传递”传递地址副本,但Java始终传递的是值的副本。设计目的是规避C/C++指针的复杂性,利于工程开发。

Posted by Hilda on March 18, 2025

首先 这篇介绍的是Java中的按值传递和按引用传递。

需要明确的是Java只有按值传递,所谓的“按引用传递”其实传递的是实参引用的对象在堆中的地址


1 实参和形参

  • 实参(实际参数,Arguments):用于传递给函数/方法的参数,必须有确定的值。
  • 形参(形式参数,Parameters):用于定义函数/方法,接收实参,不需要有确定的值。

看一个例子:

1
2
3
4
5
6
7
public static void main(String[] args) {
    String str = "Hello, the World!";//实参str
    printStr(str);
}
public static void printStr(String s){//形参s
    System.out.println(s);
}

2 按值传递 与 按引用传递

在 Java 中,数据类型主要分为两大类:基本数据类型和引用数据类型。

基本数据类型是 Java 内置的、直接存储值的类型,不涉及对象的引用。它们存储在栈内存中,具有固定的大小。

1
2
3
4
5
6
7
8
9
10
11
12
+-------------------+----------------+------------+
| 数据类型          | 描述           | 大小       |
+-------------------+----------------+------------+
| byte             | 8位整数        | 1字节      |
| short            | 16位整数       | 2字节      |
| int              | 32位整数       | 4字节      |
| long             | 64位整数       | 8字节      |
| float            | 32位浮点数     | 4字节      |
| double           | 64位浮点数     | 8字节      |
| char             | 16位Unicode字符| 2字节      |
| boolean          | /假值        | 1(理论上) |
+-------------------+----------------+------------+

所以对于下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int num1 = 10;
int num2 = 20;
swap(num1, num2);
System.out.println(num1);
System.out.println(num2);


public static void swap(int a, int b){
    int temp = a;
    a = b;
    b = temp;
    System.out.println(a);
    System.out.println(b);
}

输出是:

1
2
3
4
20
10
10
20

也就是说对于基本数据类型,按值传递。

画图的话,可以这么理解:

image-20250318111618522

swap方法中,a这个形参其实是对num1做了值的拷贝,所以不管在swap方法中,对于a,b进行交换,还是对a重新进行赋值(比如a = 100),都不会对num1的值产生影响。

总结“按值传递”:对实参的值进行拷贝,不会对实参产生影响。


下面来看引用数据类型。

引用数据类型是指向堆内存中对象的引用,变量本身存储的是内存地址,而不是实际数据。包括类、接口、数组等。

1
2
3
4
5
6
7
8
+-------------------+---------------------------+
| 数据类型          | 描述                      |
+-------------------+---------------------------+
| 类类型 (Class)    | 用户自定义类或Java类库中的类 |
| 接口类型 (Interface) | 实现接口的对象          |
| 数组类型 (Array)  | 存储一组数据的集合         |
| String           | 特殊的类类型不可变字符串|
+-------------------+---------------------------+

注:特别注意,String也属于引用数据类型。

看一个例子,理解引用数据类型的“按引用传递”。

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
    int[] arr = {1, 2, 3, 4, 5};
    System.out.println(arr[0]);
    change(arr);
    System.out.println(arr[0]);
}

public static void change(int[] array){
    array[0] = 0;
}

控制台输出:

1
2
1
0

这时,就发生了“按引用传递”:传递到change方法中的是实参引用的对象在堆中的地址

image-20250318112308098

前面提到Java中只有按值传递。怎么理解这里的按引用传递呢?

此处的“按值传递”在”按引用传递”中,指的是传递的是【地址】。(地址当然也是个值,只不过效果是按引用传递,因为给的是地址,而不是某个实参值的拷贝了)

左老师在某处说过:

传递时总是整出独立的一份,就认为是“按值传递”;传递时变量还指向老的内存地址,就认为是“引用传递”。“java总是采用按值调用”,这一句是说,哪怕按“引用传递”的变量,其实也是新的引用,这个新的引用和老的引用都指向同一个内存地址。举个例子,一个门牌上有内存地址,在传递时,总是新拷贝出新的门牌,但是新、老门牌上写的地址是一样的,指向内存的同一个区域。这就是所谓的“java总是采用按值调用”。

所以总结“按引用传递”:传递实参引用的对象在堆中的地址


那引用数据类型是不是都是按引用传递呢?不能这么认为!

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Student {
    private String name;
    public Student(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

class Test{
    public static void main(String[] args) {
        Student xiaoZhang = new Student("小张");
        Student xiaoLi = new Student("小李");
        swap(xiaoZhang, xiaoLi);
        System.out.println("xiaoZhang:" + xiaoZhang.getName());
        System.out.println("xiaoLi:" + xiaoLi.getName());
    }
    public static void swap(Student person1, Student person2) {
        Student temp = person1;
        person1 = person2;
        person2 = temp;
        System.out.println("person1:" + person1.getName());
        System.out.println("person2:" + person2.getName());
    }
}

此时控制台输出是:

1
2
3
4
person1:小李
person2:小张
xiaoZhang:小张
xiaoLi:小李

按照前面所说,swap方法还是传递的是两个引用数据类型的地址,只不过要注意,在swap方法中,交换的是地址值,并不影响main中两个实参的值。

swap 方法内的 person1 和 person2 交换了指向,但这只影响 swap 方法的栈帧,main 中的 xiaoZhang 和 xiaoLi 不受影响。

随着swap 方法的栈帧被弹出,person1、person2 和 temp 消失。main 方法中的 xiaoZhang 和 xiaoLi 保持不变。

如果 swap 方法修改的是对象内部的数据(例如 person1.setName("新名字")),堆中的对象会被改变,main 中的引用会看到这个变化,因为它们指向同一个对象。

3 总结

1.实际上 Java 只有按值传递(Pass by Value),没有真正的按引用传递(Pass by Reference)。

2.方法接收到的参数是调用者提供的值的副本,修改这个副本不会影响原始值。(基本数据类型和引用数据类型)

对于引用数据类型,传递的是引用值的副本(即内存地址的副本),方法内可以通过这个引用修改堆中的对象,但重新赋值引用不会影响原始引用。

3.其实Java没有按引用传递,但是为了和其他语言一样,区分两个概念,可以理解为(注意只是理解,并不是专业的说法):

  • 按值传递:实参值的副本
  • 按引用传递:实参引用的对象的在堆中的地址

Java 不允许方法直接操作调用者的变量本身,只能操作值的副本(所谓的“按引用传递”只不过这个值的副本指的是“地址”的副本)。

4.为什么Java这么设计?那得问詹姆斯高斯林。其实学过c/cpp会发现,指针其实很容易引发错误,这确实不利于高效的工程开发。我个人理解,詹姆斯高斯林之所以这么设计Java,其实也是希望做工程应用的时候,能够简单一些吧,规避c/cpp的很多底层的弊端(因为此时重点是在工程开发上)。