深入理解Java中的String对象:从基础到内存管理的全面解析
[id_524346217]

String 对象是 Java 语言中重要的数据类型之一,在平时编码时我们经常会用到它。它也是内存中占据空间较大的一个对象。然而,很多人对它的了解并不全面,只是一知半解。今天,我们就来详细地聊一聊这个既熟悉又陌生的 String。
一、 String认识你,你认识它么?
假如面试时被问到,什么是 String 或者谈谈对 String 的理解?你将如何作答?“String 属于基础对象类型,也是 Java 语言中重要的数据类型之一。”或许这是多数人的回答,能力更强些的或许会说,String 的底层是通过 char[ ]数组来实现的;倘若面试官让你继续深入呢?很多人可能会感到尴尬,他们会在脑海中努力搜寻关于 String 的知识,然而最终却只能怨恨自己平时对 String 关注得太少。接下来,让我们逐步去认识 String 吧。
首先,来看一个面试经常遇到,错误率又很高的问题:
String str1 为 "java"。
创建了一个名为 str2 的字符串对象,该对象的值是“java”,通过 new String()的方式进行创建。
3 String str3= str2.intern();
System.out.println进行输出操作;判断 str1 是否等于 str2;通过 == 运算符进行比较。
System.out.println(str2 和 str3 是否相等);
System.out.println(str1 与 str3 是否相等);
答案先不揭晓,各位先想一下,咱们继续往下看:
二、String对象的实现
我们将 String 对象的实现分作三个阶段来展开分析。其一为 java7 之前的版本;其二是 java7/8 版本;其三为 java8 之后的版本。
在 java7 之前的版本里,String 对象主要包含四个成员变量。其中有 char[],还有偏移量 offset,以及字符数量 count,另外还有哈希值 hash。String 对象借助 offset 和 count 来对 char[]进行定位。这样做的好处是能够高效且快速地共享数组对象,从而节省内存空间。然而,这种方式很有可能会引发内存泄漏的问题。
在 java7/8 版本中,String 把 offset 和 count 这两个变量给去除了。这样做带来的好处在于,String 对象所占用的内存稍微减少了一些。并且,String.substring()方法也不再对 char[]进行共享,如此一来,就解决了使用该方法可能引发的内存泄漏问题。
在 java8 之后的版本里,char[] 属性被改成了 byte[] 属性,并且新增了一个属性 coder,它是用于标识编码格式的。我们要明白,一个 char 字符会占据 16 位,也就是 2 个字节。在这种状况下,用来存储单字节编码内的字符(仅占一个字节的字符)就会显得极为浪费。JDK1.9 的 String 类为节省内存空间,采用占 8 位即 1 个字节的 byte 数组来存放字符串。新属性 coder 的作用在于,在计算字符串长度或者使用 indexOf()函数时,我们需依据此字段来判断如何计算字符串长度。coder 属性有两个默认值,分别是 0 和 1。0 代表 Latin - 1(单字节编码),1 代表 UTF - 16。如果 String 判断出字符串只包含 Latin - 1,那么 coder 属性值为 0;如果 String 判断出字符串不只是包含 Latin - 1,那么 coder 属性值为 1。
三、String是不可变对象
1、为什么String是不可变对象
很多人在背面试题时想必对此很熟悉。那为什么 String 对象是不可变的呢?你有没有想过这其中的原因呢?从源码可以得知,String 类被 final 关键字修饰了,并且变量 char[]也被 final 修饰了。Java语法表明:被 final 修饰的类无法被继承。被 final 修饰的变量不能被改变,一旦赋予了初始值,这个 final 变量的值就不能再次被赋值,也就是不可更改。而 char[]被 final+private 修饰,这意味着 String 对象不可被更改。也就是说,String 对象一旦创建成功,就不能再对其进行改变。
2、为什么String被设计成不可变对象
首先,要保证 String 对象的安全性,避免其被恶意篡改。例如有一个值为“abc”的引用被赋值给了 str 对象,也就是 String str = “abc”。如果在这个时候有人恶意把“abc”改成“abcd”或者其他的值,就会引发意想不到的错误。
其次,确保属性值hash不频繁变动,保证其唯一性。
3、为实现字符串常量池提供方便
举一个反例来证明String对象的不可变性
对于 String 对象的不可变性,有人或许会提出疑问:当有一个 String str =“hello”,接着又将其改为 String str =“world”,此时 str 的值变成了“world”,str 的值确实发生了改变,那为何还说 String 对象是不可变的呢?
首先,我们来解释一下对象和引用。对象存在于内存中,它是一块内存地址。str 是一个对该内存地址的引用。在这个例子里,第一次进行赋值操作时,创建了一个“hello”对象,此时 str 引用指向“hello”的地址。第二次赋值时,重新创建了一个对象“world”,str 引用转而指向“world”,然而“hello”对象仍然存在于内存之中。也就是说 str 并非是对象,它仅仅是一个对象的引用。真正的对象依旧在内存之中,并且没有发生改变。因此在 Java 里,若要比较两个对象是否相等,一般是使用“==”;而要判断两个对象的值是否相等,就需要借助 equals 方法来进行判断。
四、String常量池
在 Java 里,创建字符串一般有两种方式。一种是以字符串常量池的形式来创建,就像 String str = “abcd”这样。另一种是直接通过 new 的方式来创建,例如 String string = new String(“abcd”)。
创建字符串时若为第一种方式,JVM 会先检查该对象是否在字符串常量池中。若存在,就返回其引用。若不存在,就在常量池中创建新的字符串对象,之后将引用返回。这种方式能减少同一个值的字符串对象的重复创建,从而节约内存。
创建字符串采用 new 形式时,首先在编译类文件阶段,“abcd”这个常量字符串会被放入常量结构里;其次,在类加载的时候,“abcd”会在常量池中被创建;然后,调用 new 时,JVM 命令会调用 String 的构造函数,并且引用常量池中的“abcd”字符串,在堆内存中创建一个 String 对象;最后,string 会引用这个 String 对象。
五、String.intern()方法详解
先来看一个示例:
创建了一个字符串对象,其值为"abc",然后调用 intern()方法,将该字符串对象进行 intern 操作。
创建一个新的字符串对象,其内容为"abc",然后调用该字符串对象的 intern 方法。
System.out.print(a==b);
你觉得输出的是false还是true?
答案是:true
字符串常量中,默认会把对象放进常量池。字符串变量里,对象会创建在堆内存中,并且会在常量池中创建一个字符串对象,将其复制到堆内存对象里,然后返回堆内存对象的引用。调用 intern()方法时,会查看字符串常量池中是否存在等于该对象的字符串。若不存在,就在常量池中新增该对象,并返回该对象引用;若存在,就返回常量池中的字符串引用。堆内存中原有的对象,因为没有引用指向它,所以将会被垃圾回收器回收。
所以在上述例子中,一开始创建 a 变量时,会在堆内存里创建一个对象。并且在加载类的时候,会在常量池中创建一个字符串对象。当调用 intern()方法之后,会去常量池中查找是否存在等于该字符串的对象,如果有就返回引用。在创建 b 字符串变量时,也会在堆中创建一个对象,而此时常量池中已经有该字符串对象了,就不会再创建。调用 intern 方法时,会在常量池中判断是否存在等于该字符串的对象。当发现有等于"abc"字符串的对象时,就会直接返回其引用。而在堆内存中的对象,如果没有引用指向它,那么它将会被垃圾回收。因此,a 和 b 引用的是同一个对象。
看完这些内容后,对于文章开头的问题,你也有了答案。答案分别是:false、false、true。
StringBuilder 也是可变的字符序列,非线程安全,在单线程环境下性能比 StringBuffer 高。
1.对象的可变与不可变
String 是不可变对象。上面的内容已经对其原因进行了解释。这里就不再详细说明了。
StringBuffer 也继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中同样使用字符数组保存数据,StringBuffer 也是可变的。
char[ ] value;
2.是否是线程安全
String 中的对象是不可变的。这意味着它们可以被视为常量。因此,String 对象是线程安全的。
AbstractStringBuilder 是 StringBuilder 和 StringBuffer 的公共父类。它定义了一些字符串的基本操作,包括 expandCapacity 这个方法,还有 append 方法、insert 方法以及 indexOf 方法等公共方法。
StringBuffer 对方法加上了同步锁,或者对所调用的方法加上了同步锁,正因如此它是线程安全的。请看如下的源码:
public synchronized 这个方法会返回一个 StringBuffer 类型的对象,并且这个对象是经过反转操作后的结果。
2 super.reverse();
3 return this;
4 }
5
如果字符串中不包含指定的字符串,则返回 -1。
返回在字符串中从指定索引开始查找指定子字符串的第一次出现的索引。此方法存在于 public synchronized int indexOf(String str, int fromIndex) 中,这里是从索引 0 开始查找。即返回 indexOf(str, 0) 的结果。
8 }
StringBuilder 没有给方法加上同步锁,因此它不是线程安全的。
StringBuilder 有其特点,StringBuffer 也有其特点。StringBuilder 和 StringBuffer 都可以用于字符串的操作。它们都能够对字符串进行修改和拼接等操作。在多线程环境下,StringBuffer 是线程安全的,而 StringBuilder 不是线程安全的。但在单线程环境下,StringBuilder 的性能通常比 StringBuffer 更好。
StringBuilder 和 StringBuffer 都有一个共同的抽象父类叫做 AbstractStringBuilder。
抽象类有这样一个区别:它能够定义一些供子类使用的公共方法。对于这些公共方法,子类只需增添新的功能即可,无需再次编写已经存在的方法。然而,接口只是对方法进行申明以及对常量进行定义。
StringBuilder 的方法会调用 AbstractStringBuilder 中的公共方法,例如 super.append(…)。StringBuffer 的方法也会调用 AbstractStringBuilder 中的公共方法,像 super.append(…)。并且 StringBuffer 会在其方法上加 synchronized 关键字以实现同步。
程序如果不是多线程的,那么使用 StringBuilder 的效率会比使用 StringBuffer 高。
下面来几道测试题,看看自己对String究竟掌握了多少
七、测试题
test1、如下代码中创建了几个对象
ini复制代码1 String str1 = "abc";
有一个 String 类型的变量 str2,它被初始化为 "abc"。
对于 1 中的 String str1 = “abc”,首先会去查看字符串常量池中是否存有字符串 abc 。若存有,就直接指向该字符串。若没有,就会在字符串常量池中添加 abc 字符串并指向它。所以这种方式最多能创建一个对象,也有可能不创建对象。
对于 2 中的 String str2 = new String(“abc”),首先会在堆内存里申请一块内存来存储字符串“abc”,接着 str2 会指向这块内存块所对应的对象。与此同时,还会去检查字符串常量池中是否已经包含了“abc”这个字符串,如果没有的话,就会把“abc”添加到字符串常量池中。所以,new String()有可能会创建出两个对象。
如果上述两行代码处于同一个程序中,那么 1 中会创建 1 个对象,2 中也会创建 1 个对象。要是把这两行代码的顺序调换一下,String str2 = new String(“abc”)就会创建两个对象,而 String str1 = "abc"不会创建对象。
test2、看看下面的代码创建了多少个对象:
String 为 "apple" 的是 temp 。
2 for(int i=0;i<1000;i++) {
3 temp=temp+i;
4 }
答案:1001个对象。
test3、下面的代码创建了多少个对象:
String temp 被创建并初始化为 "apple" 。
2 for(int i=0;i<1000;i++) {
3 temp = temp+i;
4 }
答案:1002个对象。
test4:
ini复制代码1 String ok = "ok";
String ok1 被创建,其内容为 "ok" 。
在这种情况下,比较的结果为 false ,说明 ok 和 ok1 的值不相等 。
ok 指向字符串常量池,ok1 指向通过 new 生成的堆内存块,因为在编译期无法确定 new 出来的字符串,所以输出 false。
test5:
String ok 等于 "apple1"。
2 String ok1 = "apple"+1;
System.out.println(ok 和 ok1 的比较结果为 true);
编译期的 ok 是确定的,其字符串为 apple1。编译期的 ok1 也是确定的,其字符串同样为 apple1。因为它们的字符串都为 apple1,所以 ok 和 ok1 都指向字符串常量池里的字符串 apple1。它们指向同一个对象,所以结果为 true。
test6:
ini复制代码1 String ok = "apple1";
2 int temp = 1;
3 String ok1 = "apple"+temp;
这行代码的输出结果为 false。
主要看 ok 能否在编译期确定,ok 是确定的,会被放进并指向常量池。同时看 ok1 能否在编译期确定,ok1 含有变量所以不确定,不是同一个对象,因此输出 false。
test7:
ini复制代码1 String ok = "apple1";
2 final int temp = 1;
3 String ok1 = "apple"+temp;
System.out.println(ok 等于 ok1 时输出 true);
确定 ok 后,加上 final 能让 ok1 在编译期确定,所以输出 true。
test8:
String[] args 表示一个字符串数组参数。
2 String ok = "apple1";
获取温度值,将其赋值给一个名为 temp 的整型变量,该变量为 final 类型,其值等于获取到的温度值。
System.out.println(ok 等于 ok1 为 false);
6 }
7
这个函数主要的作用就是获取温度并返回给调用者。
9 return 1;
10 }
ok 是确定的。ok1 不能确定,需要运行代码才能获得 temp,由于它们不是同一个对象,所以输出 false。
以上内容如有不对的地方,还请各位指正!多谢!