探秘Java字符串桃花源

字符串,顾名思义,就是一连串的字符序列,如果单个字符是一颗珠子的话,字符串就是靠一根线连接起来的若干珠子的集合,您拿着这根线,就拿到了线上的珠子。在不同编程语言中,这根线的具体形式有所不同,但是思想都是一样的。在C语言中,字符串是通过字符数组或字符指针来存储的,因此这根线就是指针,即指向若干珠子集合的内存地址;在C++中提供了专门的字符串类string,此时这根线就是指向string对象的指针,在Java中,提供了三个专门用来处理字符串的类String、StringBuilder、StringBuffer,此时这根线是一个引用,但本质还是指向一个字符串对象的内存地址。

程序的功能就是处理数据。这里的数据含义极广,凡是能够用计算机存储、处理的东西都可以称为数据,图像、图形、视频、声音、文本,包括您写的程序,都是数据。字符串是程序处理的数据中的一大类,各大编程语言都对字符串处理煞费苦心,字符串处理功能的强弱也成为衡量一门编程语言强大与否的一个重要参考点。Java语言专门为我们提供了三个类来处理字符串数据,这三个类上文已提及,有了这三个类,我们在用Java处理字符串数据时就游刃有余。那么,在Java中,一颗颗的字符珠子是怎么靠一根线连接成为字符串的?Java为什么要为字符串处理提供三个类?这三个类有哪些区别呢?分别适用于哪些场合?带着这些问题,本文将带领读者一起去探秘Java字符串的桃花源,一起去深入Java字符串桃花源,看看里面有没有当年陶渊明所描写的美景。

本文分四步来探秘Java字符串桃花源,第一步,总览,这一步读者将看到Java字符串的总貌,犹如当年渔人从山中小口步入桃花源后看到的美景,“土地平旷,屋舍俨然,有良田美池桑竹之属。阡陌交通,鸡犬相闻”;第二步,String类,这是渔人进入桃花源后,第一次被桃花源里的人热情款待,读者将看到“设酒杀鸡作食”场景,第三步,StingBuilder和StringBuffer类,这是渔人在桃花源里受到的更进一步的热情款待,“余人各复延至其家,皆出酒食“;第四步,总结,这一步是渔人从桃花源回来的路上,对桃花源的美好回忆,以及”处处志之“,以便再次拜访,当然,请读者放心,再次拜访的时候,还能够找到桃花源的,这点Java字符串桃花源和陶渊明的桃花源有所区别。

一、  总貌

我们在探秘Java字符串桃花源,那就要追到源头去,Java字符串的源头在哪里呢?答案是JDK源代码。关于Java字符串,网上的资料可谓浩如烟海,但是很多只是在泛泛而谈,让人看了以后,只能知其然,不能知其所以然。本文的目标是让读者不仅知其然,而且要知其所以然。

打开String、StringBuilder和StringBuffer的源代码,我们看看Java字符串桃花源里有哪些美景。首先我们来看看这三个类是怎么声明的。JDK1.8中对这三个类的声明部分如图1-1,图1-2和图1-3所示。


图1-1 String类的声明


图1-2 StringBuilder类的声明


图1-3 StringBuffer类的声明

从图1-1中我们发现,String类存放字符串的数据结构是字符数组,但是从图1-2和图1-3中,我们没有找到StringBuilder和StringBuffer类存放字符串的数据结构,那么这两个类的存放字符串的数据结构在哪里呢?我们不难发现,这两个类都继承了AbstractStringBuilder类,那就去AbstractStringBuilder类里面看看,有没有我们想要的东西。AbstractStringBuilder类的声明如图1-4所示。


图1-4 AbstractStringBuilder的声明

从AbstractStringBuilder类的声明(如图1-4)中,我们找到了StringBiulder类和StringBuffer类存放字符串的数据结构,和String类一样,这三个类都是使用字符数组来存放字符串的。

以上是我们探秘Java字符串桃花源的初级阶段,相当于找到了进入桃花源的那个山中小口,并且从小口中步入了Java字符串桃花源,接下来将是更加开阔的视野。我根据这三个类的声明,画出了这三个类的UML类图,如图1-5所示。


图1-5

首先说明一下,在画String、StringBuilder、StringBuffer和AbstractStringBuilder四个类的类图时,我省略了他们的大量方法,如果加上方法,这四个类的类图将非常庞大,不利于我们观察,读者若想详细了解相关方法,可以阅读源代码。在图1-5中,我们看到了Java字符串桃花源的美景,可谓“土地平旷,屋舍俨然,有良田美池桑竹之属。阡陌交通,鸡犬相闻”,三个类的结构一目了然,这张图就是Java字符串桃花源的鸟瞰图,从中我们可以看到一点,String、StringBuilder和StringBuffer类底层存储字符串的数据结构都是字符数组,接下来我们将逐一去探秘桃花源里的各个部分。

二、  String类

从String类的声明(图1-1)和String类的类图(图1-5)中我们可以看出,String实现了java.io.Serializable接口,在JDK源代码中可以发现这是一个空接口,没有任何方法,它的作用是标识类是否支持序列化,实现该接口的类是可序列化的。String类还实现了Comparable<String>接口和CharSequence接口,Comparable<T>接口是两个对象比较的接口,该接口只有一个方法,已经在类图(图1-5)中给出。CharSequence接口提供了许多不同类型的字符序列的统一的只读访问权限,该接口的个方法已在类图(图1-5)中给出。以上是String类的外部结构,接下来看它的内部结构。

String类有四个属性(见图1-5),我们按照从易到难的顺序来认识这四个属性,serialVersionUID,它的作用是用于序列化和反序列化时标识类和类的版本;serialPersistentFields也是序列化和反序列化有关;hash,它的作用是缓存string对象的hash code。

接下来看看value属性,它可以完美地解释String类不可变(immutable)的真正含义。由String类的声明和类图可知,value属性是一个字符数组char [],重要的是,它受final关键字修饰。首先看看final关键字的含义,final关键字修饰变量有二种情况,修饰基本类型的变量和修饰引用类型的变量。final修饰基本类型变量时,这个变量一旦赋值,其值就不能再改变了,如图2-1所示的代码,第8行的操作是错误的。


图2-1

final修饰引用类型的变量时,变量的地址不能改变,但是变量对应的值可以改变,如图2-2所示的代码,第19行的操作是错误的。


图2-2

通过以上两个小例子,读者应该明白Java中final关键字的含义了。存放String中字符串的值的value数组被final关键字修饰,加之String类在初始化时不会给字符串分配多于的存储空间,您给它5个字符,它就老老实实地给您分配5个字符大小的字符数组来存放您给的值,多一个都不给(是不是感觉String很抠)。这样问题就来了,有朋友就会问:我定义一个String类对象,然后可以不断地存字符串呀,Java也没有告诉我存不下了,这是为什么?

带着上面的问题,我们来做个实验看看明白了。我在Eclipse中写了如图2-3所示的代码。


图2-3

接下来,我在24行设置了个断点,然后单步跟踪了一下。当第24行代码执行完的时候,Variables窗口中的信息如图2-4所示。我们需要关注的是str的id(此时为19)和value的id(此时为23),在Java中一切皆对象,在debug的时候,每个对象都有一个唯一的标识符id,不同的id代表不同的对象,这和每个人有一个唯一的身份证号码一样。


图2-4

表2-1、表2-2和表2-3是图2-3中的代码从第24行执行到第33行的过程中str和value的id变化情况统计表(不同的环境下执行id值可能不一样,但是规律是一样的)。

表2-1

代码行号

str的值

str的id

value的id

26

Hello

19

23

28

World

25

26

表2-2

代码行号

t1的值

t1的id

value的id

30

Hello

19

23

33

HelloWorld

27

28

表2-3

代码行号

t2的值

t2的id

value的id

32

World

25

26

根据表2-1和表2-2的信息,我们发现代码由25行执行到27行的过程中,虽然您拿到的那根线一直是str,但是这根线的id变化了,也就是str变化了,只是你感觉不到而已;同样,代码由29行执行到33行的过程中,虽然您拿到的那根线一直是t1,但是这根线的id变化了,也就是t1变化了。为什么我们感觉不到这种变化呢?这是因为String类一旦变化,Java是通过新生成一个字符串对象来实现的,底层的原因是存放String类字符串的value字符数组的修饰符final,而为了方便我们拿着一根线就能顺利操作String类的对象而感觉不到这根线的任何变化,Java为我们做了转换。这也就是String类不可变(immutable)的真正含义。探秘到这一步,我们发现:在逻辑空间编程的我们,就像桃花源里的人一样,对于物理空间发生的变化(桃花源外),我们有种“问今是何世,乃不知有汉,无论魏晋”的感觉。

再深入一点,结合表2-1到表2-3的信息,我们发现当t1的值为“Hello”的时候,t1的id和value的id和str的值为“Hello”的时候完全相同;同样,t2的值为“World”的时候,t2的id和value的id和str的值为“World”的时候完全相同。这是为什么呢?这是因为JVM中存在常量池的缘故,当执行完第27行代码时,常量池中已经存在“Hello”和“World”,所以t1和t2的引用直接分别指向它们就行,没必要在生成新的对象。

三、  StringBuilder类和StringBuffer类

在桃花源中受到String类的热情款待以后,我们该继续去探秘Java字符串桃花源中的其他美景,尝尝其他的美食了。接下来,我们一起去拜访一下StringBuilder和StringBuffer类。

首先看看StringBuilder。从StringBuilder声明(图1-2)和类图(图1-5)中我们可以发现,StringBuilder实现了java.io.Serializable接口和CharSequence接口,同时继承了AbstractStringBuilder类,通过阅读JDK1.8源代码,您会发现StringBuilder类的很多方法都是通过super关键字调用父类的方法来实现的,这是StringBuilder的外部结构。StringBuilder的内部就添加了一个与序列化操作有关的serialVersionUID属性。存放StringBuilder类字符串的字符数组继承自AbstractStringBuilder类,该类有两个包访问权限的属性value和count,value就是存放字符串的字符数组,值得指出的是,它再也没有final修饰,这就注定了StringBuilder和String的区别,而count记录的是value中实际存放的字符个数。

图3-1是StringBuilder的无参构造函数,从该构造函数可知,当您new一个StringBuilder对象时,value数组的大小是16个字符,而不是0(它可没有String那么抠)。


图3-1

下面看看StringBuilder可变(mutable)的真正含义。我们做个小实验就明白了,我写了一段代码,如图3-2所示。


图3-2

接下来,我在34行设置了个断点,然后单步跟踪了一下。表3-1是图3-2中的代码从第34行执行到第45行的过程中sb和value的id变化情况统计表(不同的环境下执行id值可能不一样,但是规律是一样的)。

表3-1

代码行号

sb的值

sb的id

value的id

38

 

19

25

40

Hello

19

25

42

Hello Java

19

25

44

Hello Java programming

19

29

根据表3-2的信息,我们发现代码执行到42行之前,sb和value的id都没有变化,也就是这两个对象的引用都没有变化,然而,代码执行到44行是,value的id变化了,这是因为你调用StringBuilder的无参构造函数new一个对象时,value数组的大小是16,而代码执行到44行时,value数组已经存放不小字符串“Hello Java programming”了,而Java中数组大小是不可动态改变的,所以必须重新生成一个字符数组来存放字符串“HelloJava programming”,也就是value对象改变了,其id由25变成了29,而在这有过程中sb的id一直没有变化,这就是StringBuilder可变(mutable)的真正含义。

接下来拜访一下StringBuffer类,StringBuffer类和StringBuilder的类非常相似,但是它自己增加了一个使用transient关键字修饰的属性toStringCache,toStringCache是StringBuffer类中已经调用toString方法生成字符串的那部分字符的缓存,同时由于它被transient关键字修饰,因此它将不会被自动序列化。

在方法上面,StringBuffer和StringBuilder的方法名字相同,同时他们的方法基本是通过super关键字调用父类方法来实现的。但是,StringBuffer的方法前面加上了synchronized关键字,就像图3-3那样。这也就是相比于StringBuilder,StringBuffer线程安全的原因。


图3-3

四、  总结

至此,本文已经带领读者探秘完Java字符串桃花源,实际上,JDK源代码就是Java当之无愧的桃花源,如果您发现了Java桃花源中的处处美景,务必要“处处志之”,以便将来拜访。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值