Java学习笔记:2022年1月9日(其一)
Java学习笔记:2022年1月9日(其一)
摘要:这篇笔记主要记录了Java运行时中的两种变量、以及参数的两种传递方式。
1.不同变量的详细探讨
1.Java中的两种变量
之前我们讨论了Java运行时中的栈区的运行机制,也就是在Java中,各种方法执行的过程,关于方法中的一些变量行为,我们也进行了简要的探讨,但是关于变量的存储以及方法的参数传递,我们并没有进行深刻的讨论,因此留下了一些遗留问题,在1月9日的笔记中,我们将对这些遗留问题进行讨论,首先我们要讨论的是Java中的两种变量。
在Java中有着8中基本变量,但是我们所说的两种变量并不是8种基本变量中的两种,而是更加广义的两种变量,即引用类型变量和基本类型变量。这两种类型的变量的差异在哪里呢?接下来我们将进行深刻的讨论。
基本类型变量:基本类型变量所占用的内存空间是一定的,这是它和引用类型变量的最大区别,不同类型的引用类型的变量所占用的内存空间可能不同,但是只要两个基本类型的变量类型相同,即使二者存的数据不同,他们的所占用的内存空间也是等长的。这个特性导致基本类型的变量被操作起来非常的方便,这里的操作不是指被人操作,而是被计算机操作,只要变量类型被定义,计算机就可以知道它在内存中占用的空间大小,进而使用相应的长度对其进行存取,如基本类型的数组中,就可以很快的进行取值,因为每个元素的长度固定,根据一个公式就可以知道指定位置的元素的首地址。
引用类型变量:引用类型变量所占用的内存空间是随机大小的,Java中的数组就是引用类型,同时各种类的对象,也都是引用类型,因为这些个体都是需要以单个的独立个体的形式出现,同时它们的大小又不是固定的,如数组的大小就和它的长度有关系,而不同类的对象,根据它们自身内部的构造有直接关系,因此这些个体控制起来非常的麻烦,我们难以为它们在内存中分配一个固定的空间,因为当变量发生值变更时,如一个数组类型变量a要被扩容时,如果在其当前地址上进行扩容,很有可能会导致它占用后面的地址影像到其他的东西。因此为了解决这个问题,Java中的引用类型变量的值并不是真的是被赋予的实体值,而是一个地址,这个地址指向实体值。
2.堆区
在Java运行时中,一共有三个区域,方法区主要是存储类信息以及静态成员,栈区是运行区,而还剩下一个区域就是堆区,堆区实际上就是一个内存分配区,引用类型变量的内存空间都在这里边,也就是说,引用类型变量的真实值的内存,都在堆区被赋予,他们都被暂存在堆区之中。
3.变量的句柄
句柄是什么,句柄实际上就是变量名,在写程序时,我们想要调用变量一般都会使用变量名来直接调用,在计算机的存储中,变量名就是句柄,我们生命变量的过程就是让计算机知道句柄和某一个值得对应关系的过程,当我们使用一个变量时,编译器就可以寻找句柄对应的值并进行替换。我们可以简单理解为,在变量声明以及赋值时,在等于号左边的,就是句柄,在等于号右边的,就是值。
4.句柄和变量
句柄可以理解为变量的变量名在计算机中的叫法,有了句柄,就可以让程序员使用变量名来取得某个数值,那么这个对应关系是如何产生的呢?变量的生存,是在栈区的,一个变量只有在运行起来时才会有意义,因此句柄的存放实际上是在栈区的,对于一个基本类型变量,其句柄和其真实值,是存放在一起的,也就是说在栈区参与方法运行的时候,一个基本类型变量的句柄和其真实值都被存放在栈区的内存上,而且它们是相邻的,因此系统很容易找到基本类型变量句柄对应的值,根据句柄,往后一找便是。然而引用类型变量的真实值没有和其句柄在一起,它们被存放在堆区内存上,引用类型的句柄后边实际上是地址信息,引用类型的变量句柄确实在栈区,但是它会像一个指针那样,指向堆区内存中的一个地址,在这个地址上,存放的才是它的真实值。
因此我们可以知道,基本类型变量的句柄和真实值在一起,都被存放在栈区;引用类型变量的句柄没有和真实值在一起,引用类型变量是一个类似指针的存在,它存放的是它真实值的地址信息,当我们为其赋予新值的时候,实际上是在修改它的指向,当一个堆内存中的真实值没有被句柄指向时,会被视为垃圾数据,Java有一个自动回收机制,会专门回收这种没有被指向的数据。
5.引用地址
在Java运行时的栈空间上,句柄们也有自己的地址,这个地址被称为引用地址,这个引用地址实际上和引用类型没有关系,只不过是这么叫而已,这里需要注意。程序的运行离不来内存,因此也离不开地址,在栈空间中的变量地址的名字被称为引用地址。因此,对于基本类型变量来说,它的值就在自己变量的引用地址上,而引用类型变量则没有,它们的值并没有位于栈区,而是位于堆内存上边的一个地址上,因此它们真实值的地址和变量所在的地址不一样。
6.小结
基本类型变量的句柄和真实值被存储在一起,二者在栈区紧挨着。引用类型变量的句柄和真实值没有在一起,它的句柄在栈空间,而真实值在堆空间中,它的句柄之后存储的是一个地址信息,这个地址信息指向堆内存中的真实值,引用类型变量类似一个指针,指向自己的真实值,而不像基本类型变量那样直接存储自己的真实值。
2.Java中的传参
对于传参,主要有两种类型的传参:值传递和引用传递。在Java中,不存在引用传递,同时,引用传递和上面讲到的引用地址有着紧密的联系,之后会详细解释。
很多人都会说:在Java中不存在真正意义上的引用传递,在Java中只存在值传递,但是不知道具体是什么意思,现在我从Java底层来详细的阐述引用传递和值传递的含义。
1.值传递
值传递实际上是字面意思,所谓值传递,就是传递值,就是将实参的值传递给形参,让形参复制一份使用。在Java中的值传递有些复杂,分为两个情况,下面我们使用图来解说Java中值传递的两种情况:
如图所示,在栈区我们定义了两个变量,其中a变量是一个基本类型的变量,b变量是一个引用类型的变量,这两个变量被定义在栈区,拥有各自的地址,系统便是使用他们的地址进行使用的。当我们使用传参时,如果是传输a变量,那么,Java运行时会将实参a的值,也就是数字10,复制到形参的值这里去,然后基本类型变量a的值传递就完成了。在这个过程中,发生传送情况的,是a变量的值,也就是数字10,仅有这个值被传递了,因此我们称之为值传递。
当我们定义了一个方法,要对b进行传参呢?这时形参发生了什么样的行为?答案是,形参获得的东西,也将是b的值,b的值是什么呢?就是堆区的,b指向的数组。那么我们如何将这个值给到形参呢?我们将b指向的数组地址复制给形参,这样就完成了参数的传递。因此,对于引用类型的变量,我们同样是使用值传递,在Java中的引用类型的传参,也同样是将变量的值给到形参,只不过是这个值实际上是值的地址,可以理解为,这个值的地址,就可以代表整个值了。
而实际上,引用类型变量并不是抽象意义上的指向它的实体值,它其实也是一个固定长度的组件,只不过在栈内存中,它后边紧跟的值不是它的真实值,而是堆内存中的地址,也就是说,b实际上在堆内存中不仅仅有句柄,它也像a那样存在一个值,只不过这个值等于的是654这个真实值地址,Java虚拟机在运行时中,可以根据b的类型进行地址映射,进而在编译时将b解释为地址映射的地址处的信息,而非仅仅是这个地址值,因此在Java中,对于引用类型变量,其实传递的也是一个值,只不过传递的是实参指向的信息的地址。因此,在Java中,仅存在值传递,而非存在引用传递。
2.引用传递
引用传递也是顾名思义,在上文中,我们介绍了一种地址叫做引用地址,这引用地址是变量的地址,什么是变量的地址呢?使用上图就能很轻松的理解,在栈内存上的存储变量的地址便是引用地址,如这个图中的101,102都是引用地址,引用地址是存在于栈内存中的变量地址,和堆内存中的值地址完全不同。位于栈内存上的变量自己的地址,叫做引用地址,而引用类型变量的值,通常是堆内存中有真实值信息的值地址,二者完全不是一个概念。直接传递引用地址的传参行为,被称为引用传递,在Java中,不存在引用传递。在C语言以及C++中存在引用传递,C语言的指针类型传参就是引用传递,在C语言中,存在指针类型变量,有了指针类型的变量,我们便可以直接对地址进行操作,使用&取地址的符号,我们可以直接取到变量自身的地址,在图中表现为101和102这两个地址,使用这两个地址,我们便掌握了这个变量的一切,在C语言的指针类型传参中,我们会将一个变量的引用地址传送给指针类型变量,指针类型变量便会存储这个地址,使用*符号,就可以直接对这个地址上的值进行修改,这里,形参是引用了实参的地址,因此叫引用传递。
3.总结
引用传递似乎和Java中的引用类型的传参非常相似,但实际上并不是,指针和Java中的引用类型确实非常一样,因为他们存的都是一个地址,但是,在使用层面,二者大相径庭,引用类型只能指向类,或者数组类型的值,指针可以指向一切值;在C语言中,指针可以获取一个变量的地址,但是在Java中,一个引用类型变量永远得不到栈中的变量地址,它获得的永远是一个堆内存中的值地址;C语言中指针改变指向后,变量仍然具有自己的句柄,仍然存在,Java中,引用类型的变量的句柄本身,就指向它的值,当改变指向,原值就会失去句柄,进而被运行时回收。总体上来讲,C语言和Java的机制完全不同,由于Java自身定义的变量存储机制,导致在Java中,不存在引用传递,仅存在值传递。
实际上,Java旨在被设计成一种绝对安全的语言,C语言中的引用传递并不是一个安全行为,尽管它非常的方便,但是引用传递的滥用会导致一个对象的封装性遭到破坏,方法的使用会导致某些相对独立的类中的属性发生改变,进而破坏某个类的独立性,因此在Java中去掉了引用传递的支持,但是由于Java中引用类型变量的设计,导致在方法中仍然有机会直接操纵值,因此弥补了这个缺陷,使用方法仍然可以改变实参的值。
值传递是传递值,引用传递是传递引用地址。在Java中引用类型变量的值传递仍然是传递值,传递的是它指向的值的对象的值,因此尽管还是传递了一个地址,但是和C语言中的传递引用地址完全不同,它传递的是堆内存中的值地址,不是传递的栈内存中的引用地址。归根结底,C语言和Java的内部机制不同,Java的值有时不和句柄相邻,存在于一个第三方位置,C语言的变量,则统统和自己的值相邻。Java中的引用类型变量的实现在底层实际上就是使用了C++的指针,但是它在Java自身体系中,被加入了一些限制,同时进行了封装,因此它和指针有些区别,引用类型变量不是指针,使用引用类型变量进行传参也不被称为引用传递。我们可以将引用类型理解为:一种被特化的,封装的,退化的指针,具有部分指针的性质,但是和C语言中的指针不太一样。
在此有一个实验,就是传入两个引用类型的变量,然后对他们进行交换。结果是实参的指向不会被改变,原因就是引用类型仍然是值传递,它们在传参时将地址赋予给形参,形参在交换时使用的是等于号赋值,等于号是浅拷贝,因此只是改变了自身的指向,而形参的指向改变并不会影响外部实参的指向,因此实参的指向没有改变。这个实验现在我看来其实是理所应当的,因为我已经从根本上明白了Java中的值传递方式,然而有些学过C语言的人会认为引用类型的传参就是和C语言中的指针传参一样,它们传进来的也是引用地址,形参指向的改变就会影响外部,这是不对的,因此一定要注意。
最后一点:Java中的引用类型的值传递,在进行值拷贝时,是浅拷贝,是使用等于号的浅拷贝,而不是深拷贝,如果是深拷贝,那么它的值传递将完全和基本类型的值传递相同,形参拥有了一个位于不同地址的,完全和原值一样的,但是实际上不是同一个的值,这是改变形参的值将不会影响到实参的值,使用浅拷贝的方式只是获得地址,这样形参的指向是和实参一致的,这样一来在方法中修改形参的值是可以影响到实参的,我们有时会需要使用这个特性,因此在引用类型进行值传递时,使用的是浅拷贝,只拷贝了地址。