这篇文章不在于快速解读这些概念,而是去探讨这些概念的本质,让你明白这些所谓的“概念”到底为何物。
子程序
在开篇之前,先来了解一下子程序是什么:
子程序是指一个大型程序中的某部分代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
——wiki
简单来说,子程序就是一部分相对独立的代码块。
你可以把子程序看作是函数式编程中的函数、或者面向对象编程中的方法。在本文中,我将用“子程序”一词来描述一些概念。
// 类是一部分相对独立的代码块,所以它是子程序
public class Test {
// 方法是一部分相对独立的代码块,所以它也是子程序
public void fun() {}
}
基本类型
基本类型也可以说是原始类型。基本类型在不同语言中有不同实现,MDN对基本类型的定义为:
基本类型是一种既非对象也无方法的数据。多数情况下,基本类型直接代表了最底层的语言实现。所有基本类型的值都是不可改变的。
—— MDN
虽然基本类型是不可变的,但变量是可变的;我们可以为变量赋值一个新的基本类型值,但无法修改变量原来的那个基本类型值。
int a = 1;
int b = a;
a = 2;
// b还是1,但a已经是2了,只是修改了变量“a”,没有修改基本类型“1”
System.out.println(a, b);
这里的基本类型不是变量a
,b
,它们只是持有数字1
,2
的引用,并非数字本身;这里的基本类型是int
类型定义或产生的数字1
,2
。
总的来说,所有程序中的基本类型都基于三大类:字符、整数、小数,比如Java中的基本类型可分为:
- 字符。
char
字符几乎包括所有文字和符号。 - 整数。
byte
字节、short
短整型、int
整型、long
长整型都是整型,它们只是长度不同。 - 小数。
float
单精度类型、double
双精度类型,都属于小数,一样区别于存储的长度不同。
注:Java的基本类型不包括字符串
String
,引用类型String
的底层实现是通过char
数组来存储值。
注:这里纠正一下byte
,byte
长度为256,可以代表256个符号。Java
中封装类型Byte
继承的是Number
类,也就是说Java
实际将byte
归为数值类型。
布尔值boolean
是一个特殊存在,一般来说是使用字符串true
和false
来表示两个相对立的值,如果在二进制数据世界,则可以用0
和1
来表示。
引用类型
在这里我们不长篇大论地用堆栈结构来阐述引用类型,实际上就算没有堆栈结构也可能出现其它内存结构来实现类型引用。
假如你有一个存了很多东西的仓库,为了能够快速找到物品,你建立了一个“清单”;在这清单上的每一个“条目”,告诉你改在仓库的哪个地方找到这个物品。
在这个情景中,清单上物品的位置信息就是对物品“引用”,它告诉我们所储存的东西的位置所在。
所以,引用是描述一个类型地址信息的数据。当一个位置中的类型在另一个位置存在引用时,这个类型即是被引用的。
为什么需要引用类型?
根据值调用策略,值的求值过程会把值复制到新内存区域,简单的值复制时不会浪费太多内存;但对复杂的对象值进行操作时,如果进行拷贝,就会浪费多余的内存空间,而一个程序的内存空间是有限的。
实参与形参
在说实参与形参之前先来了解一个知乎回答:实参与形参的值传递问题? - 罗然的回答 - 知乎。
在笔者看来,形参和实参只是程序中对某一现象的代称;如果我们透读现象的本质,就可以不拘泥于代称;但因为这种称呼已形成共识,因此我们也需要遵循共识。
实参
实际参数,是指在调用子程序时传递给子程序的参数,其在调用时必须具有确定的值(子程序将用它对形参进行赋值)。
比如int i = 1;
具有确定的值,但它还不是实参;只有在它参与将参数值传递给子程序
的这个过程时,只有在过程中我们才称之为实参。
而且值只需在调用时具有确定值即可,假设变量通过方法获取值int a = count(1,3);
,程序实际运行时会先调用完count
方法并为变量a
赋值,所以后续作为实参传入时,其已经具有确定的值了。
实参的概念不难理解,就是调用方法或者函数时所传递的参数。但如果在一个新的编程语言里,没有方法这个概念,反而是一个“可调用单元(Callable unit)”的概念,那我们可以说,调用这个Callable unit
所传参数也是实参。
我之所以采用子程序来描述概念,是因为概念性的东西不应该基于语言去思考,它们在所有语言中应当是通用的。
Java中的实参:
public class Example {
public static void main(String[] args) {
Example example = new Example();
String inputText = "original";
// 调用方法时传递了一个实参
example.exampleMethod(inputText);
}
// 方法定义了一个形参:inputText
public void exampleMethod(String inputText) {}
}
另外请注意,实例化对象时会调用对象的构造方法,因此实例化对象时所传的值也是实参:
public class Example {
public Example(String inputText) {
System.out.println("构造函数通过形参得到的实参的值为:" + inputText);
}
public static void main(String[] args) {
String inputText = "original";
// 调用构造函数时传递了一个实参
Example example = new Example(inputText);
}
}
形参
形式参数,是指子程序所定义参数;它在子程序被调用时赋值并分配内存,子程序结束后释放所分配的内存。
在Java中,方法就是一个子程序,而形参就是方法定义的参数:
public class Example {
// 方法定义了一个形参:inputText
public void exampleMethod(String inputText) {}
}
因为形参的生命周期依托于子程序,它看起来只是带着实参的引用来子程序中走个形式就结束了。所以它看起来像“形式上的参数”。
值传递与引用传递
其实严格来说,Java
中的传递都是引用传递;因为即便是基本类型,也是存储在栈或堆中,变量只是持有对内存空间中的基本类型的引用。倘若将一个变量的值赋予另一个变量,实际上两个变量指向的是同一个地址。
只不过虽持有同一引用,但在修改形参时,实参的值在基本类型与引用类型中的表现是不一样的,而这种不同所衍生的概念就是“值传递与引用传递”:
-
值传递概念:对于基本类型而言其值是无法更改的,在通过形参修改值时,基本类型只能修改参数的引用,而不会修改原引用指向的那个值。但注意值计算过程中可能会产生新值。
-
引用传递概念:但对于引用类型而言,其并没有不可更改的特性,即便使用
final
修饰符也只是修饰变量的引用不可变,形参会通过引用将内存中的对象内部的数据修改掉。
数组也是一个对象,由JVM直接创建,数组也可以使用java.lang.Object
类的方法,以及获取类名[I
。
接下来演示一下形参如何通过引用修改“引用类型”在堆中的值的:
public class Example {
// 方法定义了一个形参:inputText
public void changeArray(int[] ints) {
if (ints.length > 0) {
ints[0] = 99;
}
}
public static void main(String[] args) {
Example example = new Example();
int [] ints = new int[] {1, 2, 3};
System.out.println("实参地址 调用方法前:"+ints);
example.changeArray(ints);
System.out.println("实参地址 调用方法后:"+ints);
System.out.println("修改后的值为:");
printArray(ints);
}
public static void printArray(int[] array) {
System.out.print("[");
for (int i = 0; i< array.length; i++) {
int a = array[i];
System.out.print(a);
if (i < array.length - 1) {
System.out.print(", ");
}
}
System.out.print("]");
}
}
运行结果:
实参地址 调用方法前:[I@17c68925
实参地址 调用方法后:[I@17c68925
修改后的值为:
[99, 2, 3]
可以看出,引用并没有变化,只是修改了对象内部的值。
为什么值计算过程只是可能会产生新值
比如字符串类型,其在内存中有专门的常量池,当常量池中存在相同常量值时,就不会产生新值,而是直接将引用指向此值。但如果没有,就会产生新常量
总结
- 实参:定义于子程序外的参数,具有确定的值, 在它参与“将参数值传递给子程序”的这个过程时,只有在过程中我们才称之为实参。
- 形参:定义于子程序内的参数,生命周期依托于子程序,且接受实参赋值的子程序参数称之为形参。
- 值传递:通过引用无法修改引用指向的值的内部数据,就是值传递。
- 引用传递:通过引用能够修改引用指向的值的内部数据,就是引用传递。
各位看官,码字不易,给个赞吧!