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

简介: 本文详细分析了Java中不同类型引用的存储方式,包括int、Integer、int[]、Integer[]等,并探讨了byte与其他类型间的转换及String的相关特性。文章通过多个示例解释了引用和对象的存储位置,以及字符串常量池的使用。此外,还对比了String和StringBuilder的性能差异,帮助读者深入理解Java内存管理机制。

【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://developer.aliyun.com/article/1631769
出自【进步*于辰的博客

注:依赖类: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能存储多少个字符?》(转发)。

最后

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

本文完结。

相关文章
|
21天前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
43 4
|
19天前
|
存储 消息中间件 NoSQL
使用Java操作Redis数据类型的详解指南
通过使用Jedis库,可以在Java中方便地操作Redis的各种数据类型。本文详细介绍了字符串、哈希、列表、集合和有序集合的基本操作及其对应的Java实现。这些示例展示了如何使用Java与Redis进行交互,为开发高效的Redis客户端应用程序提供了基础。希望本文的指南能帮助您更好地理解和使用Redis,提升应用程序的性能和可靠性。
33 1
|
28天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
52 2
|
29天前
|
Java 关系型数据库 数据库
面向对象设计原则在Java中的实现与案例分析
【10月更文挑战第25天】本文通过Java语言的具体实现和案例分析,详细介绍了面向对象设计的五大核心原则:单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则。这些原则帮助开发者构建更加灵活、可维护和可扩展的系统,不仅适用于Java,也适用于其他面向对象编程语言。
20 2
ThreadLocal前奏:我理解的java四种引用类型
ThreadLocal前奏:我理解的java四种引用类型
|
12天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
3天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
3天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
24 1
|
11天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
|
11天前
|
Java 开发者
Java多线程编程的艺术与实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的技术文档,本文以实战为导向,通过生动的实例和详尽的代码解析,引领读者领略多线程编程的魅力,掌握其在提升应用性能、优化资源利用方面的关键作用。无论你是Java初学者还是有一定经验的开发者,本文都将为你打开多线程编程的新视角。 ####