Java 中真的存在引用传递吗?

值传递:传递值的副本,副本再怎么样被修改 原值不会因为副本进行改变。

引用传递: 传递程序的内存地址,无论有多少变量在引用这个地址 地址的值被改变 那么所有的变量都会变。

在 Java 中,栈是用来存储变量和业务逻辑的地方 一个新的线程都会有一个新的栈空间,而堆空间却永远只有一个,栈和堆的关系可以理解为多对一的关系,即一个堆被多个栈引用。

如果现在创建了一个空的 User 对象

User user = new User();

那么在栈空间和堆空间中应该这样表示:

关系

测试1 基本数据类型

我们先来测试基本数据类型,看看是值传递还是引用传递:

public class Test1 {
    public static void main(String[] args) {
        int x = 10;
        System.out.println("before:" + x);
        test(x);
        System.out.println("after:" + x);
    }

    public static void test(int y){
        y = 5;
    }
}

执行结果:

Test1 执行结果

可以看到,基本数据类型传递时使用的是副本传递,最终的值并没有改变,所以可见基本数据类型就是值传递,下面使用引用类型进行测试。

测试2 引用数据类型

public class Test2 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("hello");

        System.out.println("before:" + list);
        test(list);
        System.out.println("after:" + list);
    }

    private static void test(List<String> arr) {
        arr.add("world");
    }
}

执行结果:

Test2 执行结果

引用数据类型的值的确被修改了,如果传递的是副本 肯定不能被修改对吧,那么我们可以推断出引用数据类型使用的是引用传递,但是真的是这样的吗? 我们看下一个例子

测试3 还是引用数据类型

public class Test3 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>(0);
        list.add("hello");

        System.out.println("before:" + list);
        test(list);
        System.out.println("after:" + list);
    }

    private static void test(List<String> arr) {
        arr = new ArrayList<String>();
        arr.add("world");
    }
}

你能想得到执行结果是什么吗? 如果按照 测试2 推断出来的理论,那么这里应该就是: [hello] 和 [world]

执行结果:

Test3 执行结果

发现被修改之后的 list 并没有发生改变,这是怎么回事? 到底是值传递还是引用传递?

下面我们根据测试代码 画出每一步测试后的 堆栈空间图。

测试1 分析

public class Test1 {
    public static void main(String[] args) {
        int x = 10;
        System.out.println("before:" + x);
        test(x);
        System.out.println("after:" + x);
    }

    public static void test(int y){
        y = 5;
    }
}

当 x = 10 执行完毕过后:

图1

传递到 test 方法时:

图2

执行 test 中 y = 5 时:

图3

当传入 test 方法中时 y 的引用任然是 x 的引用,但是在执行 y = 5 后 ,y 的引用被修改掉了 这也就是我们第一次测试看见的,值传递的参数是不能被修改的

测试2 分析

public class Test2 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("hello");

        System.out.println("before:" + list);
        test(list);
        System.out.println("after:" + list);
    }

    private static void test(List<String> arr) {
        arr.add("world");
    }
}

当 list = new Arraylist(); 时:

图1

执行 list.add(“hello”) 时:

图2

执行 test 中 arr.add(“world”) 时:

图3

arr 虽然时一个副本传递,但是副本中包含了 list 当引用地址,这个时候我们进行修改 那么修改的就是引用值 ,引用值一旦改变 原值也会改变。

测试3 分析

public class Test3 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>(0);
        list.add("hello");

        System.out.println("before:" + list);
        test(list);
        System.out.println("after:" + list);
    }

    private static void test(List<String> arr) {
        arr = new ArrayList<String>();
        arr.add("world");
    }
}

我们这里就直接画出 测试2 上半部分的图:

图1

test 中执行 arr = new ArrayList(); 时:

图2

test 中执行 arr.add(“world”) 时:

图3

这里使用了 new 关键字 ,那么会在堆空间中开辟一块内存空间,arr 会从新指向新的引用 原来的副本引用断开。

总结

在 Java 中 不管是基本数据类型还是引用数据类型 都是值传递 即副本传递,如果副本中包含了其他对象的引用 那么这个时候 我们修改副本中的对象都是直接进行堆空间的修改。

而基本数据类型在值传递过程中,由于已经是原子性的数据 没办法再分割,我们只能通过重新赋值的方式来修改 一旦使用赋值的方式 就必定会在堆空间中产生新的引用 ,抛弃旧的引用 产生一个新的对象。