为什么说 Java 是按值传递的?

  很多人认为 Java 是按引用传递的。一方面,Java 有引用(reference)的概念。另一方面,可以通过函数形参修改函数外的某对象的字段,这看起来像是引用的特性。但实际上,Java 是按值传递的。这一点可以在官方及很多的权威书籍上得到印证,而且,Java 只有按值传递。

  如果对 Java 已经使用得非常熟练,对此理解不透彻也无伤大雅。不过很多行业最终都会要求专业性,对基本的概念分不清楚会降低威信力。那么,为什么说Java是按值传递的呢?

按值传递与按引用传递

  首先需要知道什么是按值传递(pass-by-value),什么是按引用传递(pass-by-reference)。无论是按值传递还是按引用传递,都是针对实参来说的。如果在传参过程中,形参得到的是实参的,那么就是按传递。如果得到的是实参的引用,那么就是按引用传递。

Java 的按值传递

  Java 的按值传递之所以令人疑惑是因为 Java 的变量语法。Java 中的非匿名变量有两种,一种是 基本类型变量、另一种是 对象引用变量。问题是 基本类型变量 的值直接是我们一般想要使用的数据,但 对象引用变量 不是这样,它储存的是某个匿名对象的引用。对象引用变量 的值 匿名对象的引用 不是我们直接需要的,我们直接需要的是这个匿名对象内部的数据——它的字段、它的方法等。注意,对象引用变量 的值和它引用的匿名对象内部的数据不是同一个概念。修改一个 对象引用变量 指向的匿名对象的字段,不等于修改这个 对象引用变量 的值。

  这个难以完全使用文字来描述清楚,笔者画了一个创建对象时的示意图如下。

在这里插入图片描述

  也就是说,在 Java 中,只能得到一个对象的引用,而得不到这个对象本身(但对于 基本类型,如果不考虑 Java 虚拟机的优化的话,在创建时的这个变量,就是这个变量本身。)。现在,让我们来看一个函数调用和传参的过程。

public class Main {
	class DemoClass {
		int demoField;

		public void setDemoField(int fieldValue) {
			this.demoField = fieldValue;
		}

		public int getDemoField() {
			return this.demoField;
		}
	}

	public static void demoFun(DemoClass formPara) {
		formPara = null;
	}

	public static void main(String[] args) {
		DemoClass demoObject = new DemoClass();
		demoObject.setDemoField(666);

		demoFun(demoObject); // 这会让 demoObject 的值变成 null 吗?

		// 这行代码会因“空引用”引发异常,还是输出 666 呢?
		System.out.println(demoObject.getDemoField());
		// TODO
	}
}

  请注意图中代码的注释,调用 demoFun(demoObject) 会让 demoObject 的值变成 null 吗?有经验的老手可能直接使用直觉判断出 demoObject 的值不会为 null ,尽管也许说不出原因。实际上,这就是按值传递的结果。通过函数调用 demoFun(demoObject),函数 demoFun 的形参 formPara 获得了实参 demoObject 的值,也就是匿名 DemoClass 对象的引用。因此,formPara 并不是 demoObject 的引用,所以修改不了 demoObject 的值,因此 demoObject 的值还为 null。也就是说,虽然 Java 有引用的概念,但不存在按引用传递。

  下面的这段代码也同样提醒了这一点。

public class Main {
	class DemoClass {
		int demoField;

		public void setDemoField(int fieldValue) {
			this.demoField = fieldValue;
		}

		public int getDemoField() {
			return this.demoField;
		}
	}

	public static void main(String[] args) {
		DemoClass demoObject = new DemoClass();
		demoObject.setDemoField(666);

		DemoClass otherReferVar = demoObject;
		otherReferVar = null; // 这会让 demoObject 的值变成 null 吗?

		// 这行代码会因“空引用”引发异常,还是输出 666 呢?
		System.out.println(demoObject.getDemoField());
		// TODO
	}
}

Java 的“传引用”

  还有一个在 Java 中流行使用的口语化词汇:传引用。由于此词汇广泛使用,因此这里不质疑它的合法性。那么,既然“传引用”是正确的,为什么前面又说 Java 是按值传递的呢?请注意,“传引用”中的“引用”与“按引用传递”中的“引用”指的不是同一个概念。在“传引用”中,只要中间有某个过程传递的是引用,就认为这是在“传引用”。比如,上面的函数调用 demoFun(demoObject) 中,函数 demoFun 的形参 formPara 获得了实参 demoObject 的值,也就是匿名DemoClass对象的引用。虽然此过程是传递的是实参 demoObject 的值(按值传递),但 demoObject 的值正好是匿名 DemoClass 对象的引用,因此此过程也可以称之为“传引用”。记住,在术语按值传递、引用传递中,衡量标准都严格要求是实参。而在 Java 中,永远也无法通过函数调用直接修改实参的值。

C++ 中的引用与指针

  虽然有引用的概念不代表函数调用时按引用传递,但不是什么编程语言的引用都只能按值传递的。比如 C++ 中的引用,可以按值传递,也可以按引用传递,这取决于定义调用函数的定义,而和实参是不是引用无关。而 JavaScript 中的引用和 Java 一样,只能按值传递。

  不过,在此处笔者还要纠正一下,C++ 中的引用与 Java 中的引用从性质上来说是不同的。Java 中的引用更像是 C++ 中的指针。也就是说,C++ 中的引用与 C++ 中的指针不是同一个性质的概念。有的“自认为喜欢钻研”的人喜欢吹捧自己的“独特见解”:C++ 中的引用在内部原理上可以由指针来实现,因此 C++ 中的引用与指针是一码事。这个观点算不上开阔。内部原理如何与概念是否相同无关。从设计模式来说,同一个问题可以由不同的设计模式来实现,不代表实现这个问题的所有的设计模式本质上都是相同的,更不代表这个问题就是一种设计模式,且与实现它的设计模式相同。C++ 的指针变量被赋值时,这个指针之前指向的对象不受影响,但 C++ 的引用就不同了。C++ 的引用变量在创建之后就几乎与其引用的对象完全相同。

  有的人认为 C++ 中的指针与 Java中的引用不同,理由是 C++ 的指针与 C++ 其它普通的类型的量纲不一致。确实如此。不过,这只是因为 C++ 中存在非匿名的对象。如果这类人对声明指针时使用的星号 * 非常敏感,C++ 中还提供了关键字 typedef。该关键字专门为有“整体”数学思想的人提供。如果拒绝使用非匿名的对象(只使用关键字 new 创建对象),且使用关键字 typedef 消除使用指针时必定使用的解引用运算符 *,这些人将会惊奇的自我发问:自己使用的究竟是哪门语言。

总结

(左值创建、右值创建指的是创建变量时位于赋值运算符 = 的左右还是右边)

  • Java:

    • 只能按值传递。

    • 对象都是匿名的。右值创建。创建对象时返回其引用。

    • 基本类型变量都是非匿名的。左值创建。匿名的基本类型都是常量。

  • JavaScript:同 Java。

  • C++:

    • 可按值传递,也可按引用传递。传递规则与目标是基本类型还是对象无关。

    • 变量和对象可匿名创建,也可不匿名创建。可左值创建,也可右值创建。创建匿名变量和对象时返回其指针值。

  • C:只能按值传递。无对象。其它同 C++。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值