[Java]基本数据类型与引用类型赋值的底层分析

【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://www.cnblogs.com/cnb-yuchen/p/17969159
出自【进步*于辰的博客

注:依赖类:StringInteger

1、不同类型引用分析

参考笔记一,P36.5、P74.1;笔记二,P38.1。

一个小结:

所有引用都存于栈,对象存于堆。引用所指向的可能存于堆,也可能存于方法区常量池。

1.1 int

示例:

final int a = 2;// a 是常量
int b = 3;// b 是变量

a、b、2、3 都存于栈,

int c = b;
b = 4;// 直接将b的值由3改为4,c仍是3

1.2 Integer

关于八位有符号二进制的表示范围,详述可查阅博文《二进制相关概念、运算与应用》中的【八位二进制的表示范围】一栏。

Integer 类的“自动装箱”和“自动拆箱”机制是什么?

  1. 包装类都具有“自动装箱”和“自动拆箱”的机制(以代码的角度上说,就是会在底层会自动调用某个方法)。
  2. 为包装类Integer赋值时,自动装箱是指在底层调用valueOf()。这里存在一个溢出问题。因为整型常量存储于方法区的整型常量池,而整型常量池使用8位有符号二进制表示整数。8位有符号二进制的表示范围是-128 ~ 127。若整数超出此范围,就是“溢出”。
  3. “溢出”规定:若整数在范围内,valueOf()的底层将创建整型常量,存储于整型常量池;否则创建 Integer 对象,存储于堆。

示例:

Integer i1 = 2;// 自动装箱
Integer i2 = new Integer(2);
Integer i3 = Integer.valueOf(2);
i1 == i2;// false
i1 == i3;// true

Integer i4 = 128;
Integer i5 = new Integer(128);
Integer i6 = Integer.valueOf(128);
i4 == i5;// false
i4 == i6;// false
i5 == i6;// false

PS:大家结合上文所述,很容易便可理解。如果大家想要了解valueOf()的底层,可查阅Integer类的第4.33项。

扩展一点

i1.equals(i2);// true
i1.equals(i3);// true
i2.equals(i3);// true

i4.equals(i5);// true
i4.equals(i6);// true
i5.equals(i6);// true

为何结果都为true?详述可查阅Integer类的第4.6项,这里不赘述。

1.3 int[]

示例:

int[] arr1 = {1, 2, 3}, arr2 = arr1;

arr1arr2 以及数组内所有的值都存于栈,{1, 2, 3}存于堆。

arr1[0] = 4;

直接将1改为10,则arr1是{4, 2, 3};arr2与arr1指向相同,故arr2也是{4, 2, 3}

1.4 Integer[]

示例:

Integer[] arr1 = {1, 2, 3}, arr2 = arr1;

与 int[] 不同的是,Integer[] 内的所有元素都不是直接的值,而是Integer引用,指向整型常量池或堆。

不过,无论指向哪里,由于arr2arr1指向相同,故同上。

2、byte 与其他类型间转换

参考笔记三,P44.4。

不知道大家有没有注意这个细节?

String s = "中";
Arrays.toString(s.getBytes());// [-28, -72, -83]

这是为何?这就涉及到getBytes()的源码,详述可查阅String类的第3.18项。

源码较多,但此方法的业务很明了:

使用平台默认的字符集(ISO-8859-1)将此 String 解码为字节序列,并将结果存储进一个新的字节数组中。

简言之,将 String 的每个字符按照默认字符集转换成byte

因此,[-28, -72, -83]就是汉字"中"byte,占 3 个字节。可这 3 个负数是怎么来的?完全没有头绪,再看个示例:

String s = "0";
Arrays.toString(s.getBytes());// [48]

48是什么?不就是字符'0'ASCLL码吗。因此:

char的ASCLL码在byte的表示范围-128 ~ 127之内时,byte就是char的ASCLL码。

总结:当其他基本数据类型变量的值在byte的表示范围之内时,按照char类型的ASCLL码进行转换。

扩展一点

请问:FileReader 一定比 FileInputStream 读取速度快吗?

相信大家都很熟悉这两个文件读取类,前者按照字符读取,一次读取一个字符;后者按照字节读取,一次读取一个字节。

因此,从上文不难得出答案:

当文件内容仅由字母、数字或一般标点符号组成时,两者效率相同。

3、String

启发博文:《一文带你彻底搞懂 Java String 字符串》(转发)。

3.1 赋值分析

参考笔记一,P74.1/2、P45.2;笔记三,P63.2。

1:示例1

String str1 = "Hello";
String str2 = "Hello";
str2 = "World";
sout str1;// Hello
sout str2;// World

说明:

  1. 第一行:str1是引用,创建字符串常量"Hello"
  2. 第二行:由于已存在"Hello",直接将str2指向"Hello"
  3. 第三行:将str2的指向由"Hello"转为"World",若"World"不存在则先创建,若"Hello"没有其他引用指向将被回收。

2:示例2

String str1 = "Hello";
String str2 = str1;
str2 = "World";
sout str1;// Hello
sout str2;// World

说明:

  1. 第二行:将引用str2指向str1的指向,即"Hello"
  2. 第三行:将str2的指向由"Hello"转为"World"

3:示例3

String str1 = "Hello" + "World";
String str2 = "HelloWorld";
str1.equals(str2);// true
str1 == str2;// true

大家都知道equals()比较的是“间接”的值,即“内容”;==比较的是“直接”的值,即“地址”。所以,第一个结果是 true,那为何第二个也是 true?

这里涉及一个理论:

当使用 "+" 对两个字符串常量连接时,这个结果在编译器就确定了,由于 "+" 号两边都是常量,因此会直接连接放入常量池。并且不会将 “+” 两边的常量放入常量池。

因此,str1 在编译时,编译器就已经将由"Hello""World"连接而成的字符串常量"HelloWorld"存入常量池,在编译 str2 时,直接将 s2 指向"HelloWorld"

str1 与 str2 指向相同,自然地址相同,故str1 == str2

4:示例4

String str1 = "Hello" + "World";
str1 == "HelloWorld";// true

与上一个示例同理。

5:示例5

String str1 = "Hello";
String str2 = "World";
String str3 = str1 + "World";
String str4 = str1 + str2;
String str5 = "HelloWorld";
str3 == str5;// false
str4 == str5;// false

str3 = str3.intern();
str3 == str5;// true
str4 = str4.intern();
str4 == str5;// true

这里涉及一个“+”连接字符串的理论:

+” 将两个字符串连接生成一个新的对象,也就是内存中新开辟了一块空间。

也就是说,str3 最终指向new String("HelloWorld"),而 str5 指向字符串常量“HelloWorld”,指向不同,地址自然不同,故str3 != str5

$str3 在编译时经历了哪些执行过程?$

str1 → 封装成new StringBuilder("Hello") → 调用append("World") → 调用toString() → 字符串对象"HelloWorld"

PS:大家自行 debug 一下就明白了。

第二个结果为 false,同理。

为何最后两个结果都变成了 true? 这就涉及intern()的功能了,详述可查阅String类的第3.27项。

3.2 String与StringBuilder的区别

大家看完上文【赋值分析】中的5个示例,肯定产生了疑问:为何字符串在运算时会经历这些过程,直接修改或者连接不就 OK 了吗?

为何如此?在说明之前,我摘录两段启发博文中的阐述:

  1. 不可变对象:如果对象创建完成之后,其状态不会被修改,那么这个对象就是不可变对象。
  2. 对象状态:类里面定义的成员变量叫做属性,运行时创建出来的对象的属性的具体值就是该对象的状态。

也就是说,String是不可变对象。从此类源码可见,其底层存储结构(使用什么类型变量存储数据)是final char value[],这便是缘由所在。(PS:启发博文的【不可变 String】一栏中有具体说明,在此不赘述)

因此,如果我们使用上文【赋值分析】中示例5的方式循环修改字符串,性能将很低。测试一下:

long time1 = System.currentTimeMillis();
String str = "hello";
for (int i = 0; i < 10000; i++) {
    str = str + i;
}
long time2 = System.currentTimeMillis();
time2 - time1;// 474(毫秒)

我们再来看一下StringBuilder类的源码,其底层存储结构是char[] value;(其父类AbstractStringBuilder的属性),故是可变的。因此,当修改时,是直接进行修改,而不是如String类一般重新创建,性能将提升很多,测试一下:

long time1 = System.currentTimeMillis();
StringBuilder str = new StringBuilder("hello");
for (int i = 0; i < 10000; i++) {
    str = str.append(i);
}
long time2 = System.currentTimeMillis();
time2 - time1;// 4(毫秒)

3.3 String与char[]的比较

参考笔记一,P74.3。

示例:

String str1 = "csdn";
char[] arr = {'c', 's', 'd', 'n'};
str1.equals(arr);// false
str1.toString().equals(arr.toString());// false

String重写了equals(),在底层会先调用toString(),返回内容;而char[]不是具体类型,不存在重写,当然,仍会调用toString(),但调用的是Object类的toString(),返回地址,故两个结果都为 false。

3.4 扩展:String能存储多少个字符?

参考笔记三,P69.2。

我暂且未整理相关内容,大家可查阅博文《面试官问我String能存储多少个字符?》(转发)。

最后

本文中的例子是为了方便大家理解基本数据类型和引用类型赋值时的底层而简单举例的,不一定有实用性,仅是抛砖引玉。

本文完结。

posted @ 2024-05-31 18:53  进步·于辰  阅读(105)  评论(0编辑  收藏  举报