为什么说 Java 只有值传递

为什么说 Java 只有值传递

结论:不管是基本数据类型还是引用类型 reference ,Java 中都是值传递,在函数中对引用类型参数所进行修改,是否会影响到实际参数。需要从是否是操作的同一个对象这个角度进行分析。

引用传递和值传递的概念

值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

在 Java 中一般认为基本数据类型、基本数据类型的包装类和 String 是值传递,引用类型是引用传递。

public class Test {
    static void test(int var1, Integer var2, String var3, Map var4) {
        var1 = 2;
        var2 = 2;
        var3 = var3 + "2";
        var4.put("2", "2");
    }

    public static void main(String[] args) {
        int var1 = 1;
        Integer var2 = 1;
        String var3 = "1";
        Map var4 = new HashMap();
        var4.put("1", "1");
        test(var1, var2, var3, var4);
        System.out.println("var1(int):" + var1);
        System.out.println("var2(Integer):" + var2);
        System.out.println("var3(String):" + var3);
        System.out.println("var4(HashMap):" + var4);

        // 输出为
        // var1(int):1
        // var2(Integer):1
        // var3(String):1
        // var4(HashMap):{1=1, 2=2}
    }
}

参数在传递到 test 方法中赋值后,在 main 方法中看只有 var4 (HashMap) 的值受到了影响。这样看来,确实是值类型是值传递,引用类型是引用传递。但是并非如此。

方法执行的内存模型是 Java 虚拟机栈,在每个方法被执行的时候都会创建一个栈桢,方法的参数和局部变量都保存在栈桢中的局部变量表中。

局部变量表的容量以局部变量槽为单位,每个局部变量槽能存放一个 boolean、byte、char、short、int、float、reference 和 returnAddress类型的数据 (Java 基本数据类型中的其他两种 long 和 double 都会占用两个局部变量槽) 。

引用类型在方法栈中只保存了 reference ,真正的对象在 Java 堆中。无论 Java 虚拟机是使用哪种方式通过 reference 定位对象 (主要有使用句柄和直接指针两种) ,都可以把 reference 理解为对象在堆中的地址。

在刚进入 test 方法的时候方法中的 var3 和 main 方法中的 var3 是指向堆中的同一个 String 对象,在运行完 test 方法最后一行的时候 Java 虚拟机内存模型是这样的。
在这里插入图片描述
同样是 reference 类型的 var3 和 var4,为何经过了 test 方法后只有 var4 的值改变了。其实 test 方法执行完第三行 var3 = var3 + “2” 后,main 方法中的 var3 和 test 方法中的 var3 已经不是指向 Java 堆中的同一个对象了。也就是在 test 方法中 reference3 的值 (字符串对象在堆中的地址) 已经发生了改变。但是未影响到 main 方法中的 reference1的值。

在 reference 类型参数传递的时候,也是值传递,复制了一份 reference (对象的地址) 传递到了 test 方法的栈桢局部变量表中,在复制的 reference 发生改变的时候,不会影响到原方法中的 reference 值。reference 的传递也是值传递,所以在 Java 中,参数传递只有值传递,没有引用传递。

为何 var4(HashMap) 还指向同一个对象,但是 var3(String) 却不指向同一个对象了呢。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

在 String 类中保存字符串的 char 数组用了 final 修饰。char 数组不能重新赋值,所以字符串的值也就不能改变。其实 test 中的 var3 = var3 + “2” 语句创建了一个新的 String 对象,等同于以下写法。

static void test(int var1, Integer var2, String var3, Map var4) {
        var1 = 2;
        var2 = 2;
        // var3 指向新的对象
        var3 = new String(var3 + "2");
        var4.put("2", "2");
    }

要是 把 String 换成 StringBuilder,结果将会改变。

public class Test {
    static void test(int var1, Integer var2, StringBuilder var3, Map var4) {
        var1 = 2;
        var2 = 2;
        var3.append("2");
        var4.put("2", "2");
    }

    public static void main(String[] args) {
        int var1 = 1;
        Integer var2 = 1;
        StringBuilder var3 = new StringBuilder("1");
        Map var4 = new HashMap();
        var4.put("1", "1");
        test(var1, var2, var3, var4);
        System.out.println("var1(int):" + var1);
        System.out.println("var2(Integer):" + var2);
        System.out.println("var3(StringBuilder):" + var3);
        System.out.println("var4(HashMap):" + var4);

        // 输出为
        // var1(int):1
        // var2(Integer):1
        // var3(String):12
        // var4(HashMap):{1=1, 2=2}
    }
}

因为 StringBuilder 继承 AbstractStringBuilder,AbstractStringBuilder 中保存字符串的 char 数组没有使用 final 修饰。并且能对 char 数组进行修改和扩容,提供了 append 等方法。

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

结论:不管是基本数据类型还是引用类型 reference ,Java 中都是值传递,在函数中对引用类型参数所进行修改,是否会影响到实际参数。需要从是否是操作的同一个对象这个角度进行分析。

public class Test {
    static void test(Map var) {
        var = new HashMap();
        var.put("2", "2");
    }

    public static void main(String[] args) {
        Map var = new HashMap();
        var.put("1", "1");
        test(var);
        System.out.println("var:" + var);

        // 输出为
        // var:{1=1}
    }
}

以上例子,test 中的进行 put 的 Map 和 main 方法中的已经不是同一个对象,所以对他操作不会影响 main 方法中 Map 的值,test 方法执行完成后其中创建的 Map 对象将不再是 GC Roots 且没有 GC Roots 引用他所以将会由垃圾收集器进行收集。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值