【基础篇-堆栈】传值?传引用?(三)

来源:互联网 发布:第一性原理 知乎 编辑:程序博客网 时间:2024/06/05 05:40

首先,原谅我这个拖延症晚期患者,说好清明更完,现在已经快到五一了……

近来诸事不顺,感情、工作、身体等等。

       在之前两篇文章里,我对String这个特殊的引用数据类型不予理睬,是因为顾忌String类型的一些特殊性质造成大家的阅读障碍,如果你还没有读过之前的文章并且你想对堆栈、传值、传引用有一个比较详尽的了解,建议你从头阅读【基础篇-堆栈】

--------------------------------------------------------------------------------分割线--------------------------------------------------------------------------------------------

特殊的引用数据类型——String

       为什么说String特殊引用数据类型呢?

       来,让我们用逆向思维来操作一下,我们把String当作一个普通的引用数据类型,举个例子:

/** * String 测试类 */public class StringTest {public static void main(String[] args) {//声明一个String类型的变量obj1,并将字符串"abc"赋值给obj1;String obj1= "abc";//控制台打印obj1;System.out.println("obj1="+obj1);//调用方法test();参数传入obj1,获得的返回值赋值予String类型的变量obj2;String obj2 = test(obj1);//再次控制台打印obj1;System.out.println("obj1="+obj1);//控制台打印obj2;System.out.println("obj2="+obj2);//控制台打印obj1与obj2在栈中的地址是否相同;System.out.println("栈内:obj1等于obj2吗?"+(obj1 == obj2));//控制台打印obj1与obj2在堆中的值是否相同;System.out.println("堆内:obj1等于obj2吗?"+obj1.equals(obj2));}//test测试方法public static String test(String obj){//将字符串"xyz"赋值给方法参数obj;obj = "xyz";//返回重新赋值后的方法参数obj;return obj;}}

        输出结果:

       呃……怎么和想象中的不太一样呢?

       按照前两篇观点来看,既然String是引用数据类型,那上述程序中在应该只有一个对象,为什么经过方法调用再次打印“同一对象”,却得出了不同的结果呢?疑惑吗?怀疑人生吗?

       来:


       这里有两个坑……

       其一,我们应该清楚的认识到,在java中,在堆中创造对象,必须通过new关键字来创造;

       其二,我们在学习java之初就应该把String当作特殊的引用数据类型和其他引用数据类型区别对待。

       第一点不用多说,只要记住就行,如果想深究,可以参考一下《深入java虚拟机》这本书。我们要说重点是第二点,我们已经尝试过将String类型当作普通的引用数据类型来处理,很显然行不通,因为我们忽略java中一个叫做字符缓冲区(字符常量池)的区域,这个区域是一个针对char(字符)String(字符串)数据类型而单独设立的,在很多现有的java书籍和教学视频中往往一带而过。这是一个非常重要、但却被大多数人忽略了的坑(包括我)。而String类型的特殊,就特殊在了它配合字符缓冲区的创建储存机制

       来,让我们画图说明:


       在我们在对String类型变量进行声明初始化的时候,如果没有使用到new关键字,那么当前变量就和堆毫无关系

       如图所示,字符缓冲区我们可以理解为一个装满一个个字符的水池,我们用到哪些字符,就把哪些字符提取出来,存放到字符缓冲区中类似堆栈的地方,然后指向栈内代表当前数据项变量。现在我们声明一个String类型变量str1,并初始化赋值"x",在字符缓冲区内,会提取出"x"这个字符作为单独数据项,并指向栈内变量str1;当我们声明变量str2时也给其赋初值"x",编译器会首先检测字符缓冲区内是否存在与之声明相匹配的数据项如果存在,就直接指向栈内变量,同理,String str3=str2;这一声明初始化语句也是将字符缓冲区内代表"x"的数据项指向栈内变量str3;而str4所需要数据项明显与字符缓冲区中已经存在的任一数据项都不相匹配,所以,编译器会从字符缓冲区中提取出"x"、"y"、"z"然后组合在一起组成数据项指向栈内变量str4

       是不是觉得这和栈内真值的形式储存十分相似,都遵循在储存块内,有则取之、无则创造的原则。那为什么不把String类型也归为基本数据类型?这样岂不是更加方便快捷。如果你有这样的想法,那就证明你又掉到坑里了……

       栈内的所有数据项都是以真值来储存的,并且所占储存空间相对固定。但是String类型数据项,所占储存空间是不固定的(这里并没有动态分配), 所以String类型不能设计为基本数据类型储存于栈中,因为那样势必会造成性能的大打折扣。

       String类型的变量即没在栈中也没在中,意思都储存于字符缓冲区中吗?那String类型的变量到底算不算对象?如果是对象,你之前不是说过所有的对象都储存在堆中吗?啪啪啪!自己打自己的脸。别急,让我们在堆中创造String类型的对象

/** * String 测试类 */public class StringTest {public static void main(String[] args) {String str1 = new String("x");}}
       显而易见:new关键字是是否创建对象的关键所在。

让我们拓展一下:

/** * String 测试类 */public class StringTest {public static void main(String[] args) {String str1 = new String("x");String str2 = new String("x");System.out.println("str1是否等于str2--------->"+(str1== str2));System.out.println("str1的值是否等于str2--------->"+str1.equals(str2));}}
       打印结果:

       这样一来,结果是不是符合预期结果呢?
       再来个变种:
/** * String 测试类 */public class StringTest {public static void main(String[] args) {String str1 = new String("x");System.out.println("执行test方法前str1="+str1);test(str1);System.out.println("执行test方法后str1="+str1);}public static void test(String str){str = "y";System.out.println("执行test方法中参数str="+str);}}
       打印结果:
       呃……为什么……我敢保证我操作的是对象引用,为什么……

先扔张图:

       通过new关键字来创造String类型的对象时,必须先在字符缓冲区创建数据项,然后才能在堆内创建与之对应数据项;并且,如果你对任意一个已经持有对象引用的String类型的对象进行字符串赋值操作(例如上述代码中test()方法中的str="y";),那么,必将断开当前对象引用和与之相对应对象之间的映射关系,重新赋值操作后,栈内当前变量仅仅代表的是字符缓冲区内的数据项

       现在,我们就应该清楚这个特殊的字符缓冲区的作用了。因为字符串在java中的巨大的使用量,无论把它的储存位置放在或者中,都会在一定程度上影响性能,所以java的设计者们采取一个折中的方法,设计出这一块储存字符串的特殊区域,也正是如此,使得String类型拥有引用数据类型的所有特性,也兼具基本数据类型的灵活方便。而字符缓冲区我们可以理解为一个特殊的拦截器如果我们没有明确的使用new关键字创造对象或者明确的把一个堆内String对象持有的引用赋值给它,所有栈内代表String类型的变量,都指向的是字符缓冲区内的数据项

结束语
       好啦!《【基础篇-堆栈】传值?传引用?》所有三篇终于更完了,前前后后拖了接近两个月,也是挺“佩服”自己的。希望那些还对这些知识点混淆不清的乡亲们,看到此篇能有一定的收获和启发;与此同时,疏漏勘误在所难免,希望各位朋友能够指正出来,在此万分感谢。

1 0
原创粉丝点击