探讨实参与形参以及基本类型与引用类型的本质

这篇文章不在于快速解读这些概念,而是去探讨这些概念的本质,让你明白这些所谓的“概念”到底为何物。

子程序

在开篇之前,先来了解一下子程序是什么:

子程序是指一个大型程序中的某部分代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
——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中的基本类型可分为:

  1. 字符。char字符几乎包括所有文字和符号。
  2. 整数。byte字节、short短整型、int整型、long长整型都是整型,它们只是长度不同。
  3. 小数。float单精度类型、double双精度类型,都属于小数,一样区别于存储的长度不同。

注:Java的基本类型不包括字符串String,引用类型String的底层实现是通过char数组来存储值。
注:这里纠正一下bytebyte长度为256,可以代表256个符号。Java中封装类型Byte继承的是Number类,也就是说Java实际将byte归为数值类型。

布尔值boolean是一个特殊存在,一般来说是使用字符串truefalse来表示两个相对立的值,如果在二进制数据世界,则可以用01来表示。

引用类型

在这里我们不长篇大论地用堆栈结构来阐述引用类型,实际上就算没有堆栈结构也可能出现其它内存结构来实现类型引用。

假如你有一个存了很多东西的仓库,为了能够快速找到物品,你建立了一个“清单”;在这清单上的每一个“条目”,告诉你改在仓库的哪个地方找到这个物品。

在这个情景中,清单上物品的位置信息就是对物品“引用”,它告诉我们所储存的东西的位置所在。

所以,引用是描述一个类型地址信息的数据。当一个位置中的类型在另一个位置存在引用时,这个类型即是被引用的

为什么需要引用类型?

延伸阅读:求值策略-百度百科求值策略-wiki

根据值调用策略,值的求值过程会把值复制到新内存区域,简单的值复制时不会浪费太多内存;但对复杂的对象值进行操作时,如果进行拷贝,就会浪费多余的内存空间,而一个程序的内存空间是有限的。

实参与形参

在说实参与形参之前先来了解一个知乎回答:实参与形参的值传递问题? - 罗然的回答 - 知乎

在笔者看来,形参和实参只是程序中对某一现象的代称;如果我们透读现象的本质,就可以不拘泥于代称;但因为这种称呼已形成共识,因此我们也需要遵循共识。

实参

实际参数,是指在调用子程序时传递给子程序的参数,其在调用时必须具有确定的值(子程序将用它对形参进行赋值)。

比如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]

可以看出,引用并没有变化,只是修改了对象内部的值。

为什么值计算过程只是可能会产生新值

比如字符串类型,其在内存中有专门的常量池,当常量池中存在相同常量值时,就不会产生新值,而是直接将引用指向此值。但如果没有,就会产生新常量

总结

  • 实参:定义于子程序外的参数,具有确定的值, 在它参与“将参数值传递给子程序”的这个过程时,只有在过程中我们才称之为实参。
  • 形参:定义于子程序内的参数,生命周期依托于子程序,且接受实参赋值的子程序参数称之为形参。
  • 值传递:通过引用无法修改引用指向的值的内部数据,就是值传递。
  • 引用传递:通过引用能够修改引用指向的值的内部数据,就是引用传递。

各位看官,码字不易,给个赞吧!


  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值