为什么说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++。

猜你喜欢

转载自blog.csdn.net/wangpaiblog/article/details/114242822