【Java】String
String 的基本特性
String:字符串,使用一对 ""
引起来表示。Java 程序中的所有字符串字面值(如"abc"
)都作为此类的实例实现。
1 | String s1 = "hello"; // 字面量的定义方式 |
String
是一个final类,代表其不可被继承,从而保护其线程安全性(避免被子类继承后破坏线程安全性)- 字符串是常量,用双引号引起来表示。它们的值在创建之后不能更改
String
对象的字符内容是存储在一个字符数组常量final char value[]
中的String
实现了Serializable
接口:表示字符串是支持序列化的。实现了Comparable
接口:表示String
可以比较大小
String 的不可变性
String:代表不可变的字符序列。简称:不可变性。
- 当对字符串重新赋值时,需要重新指定内存区域赋值,不能使用原有的value进行赋值。
- 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
- 当调用
String
的replace()
方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。 - 通过字面量(如
"abc"
)的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中 - 字符串常量池中是不会存储相同内容的字符串的。
代码
1 |
|
String 的实例化方式
- 方式一:通过字面量定义的方式
String str1 = "abc";
,此对象存储在字符串常量池中 - 方式二:通过new + 构造器的方式
String str2 = new String("abc");
,此时创建两个对象,一个 String 对象"abc "
存储在字符串常量池中,另一个str2
存储在普通堆空间中
二者区别:
- 下图中的两个橙色块代表两个 String 对象
- 直接用字面量形式创建的对象
str1
存在于字符串常量池中,其内维护了一份唯一的字符串数据char[] value = "abc"
- 用 new 的方式创建的 String 对象存在于堆区中,和上一种方式的对象是不同的
二者的联系在于普通堆区内的 String 对象内部维护的 char[] value
数组其实指向的是常量池中的 String 对象内部维护的 char[] value
,即同一个字符串数据只存在一份于字符串常量池中,普通堆区的 String 对象只是引用了该数据
问:String str = new String("abc");
方式创建对象,在内存中创建了几个对象?
答:两个。一个 String
对象 "abc "
存储在字符串常量池中(在常量池的 Hashtable
结构中),另一个 str
存储在普通堆空间中。
注意这个 str
内部的 char[]
数组的地址就是在字符串常量池中 "abc"
对象的 char[]
数组地址,即在普通堆区 new 出来的多个 String 对象,他们如果内容相同,那么内部的 char[]
数组的地址就是同一个地址(指向存储在字符串常量池中的真正数据 "abc "
),本身不会再去创建新的char[]
数据了,而是共享常量池中的同一份 char[]
。
同时,在普通堆内存空间中存在的 String 对象 str
是会被 GC 的,但是在字符串常量池中的匿名 String
对象 "abc "
不会被轻易 GC,只有当所有引用该对象内的 char[] value
的 String 对象都不存在了才会被回收。
我们也可以从字节码角度分析这两种创建方式的差异:
1 | public static void main(String[] args) { |
0 ldc #2 <a>
:从字符串常量池中加载"a"
这个字符串到操作数栈中2 asrore_1
:将操作数栈中存储的"a"
保存到局部变量表中索引位置为1的位置(即赋值给对象 a)3 new #3 <java/lang/String>
:new 一个 String 对象(即 b 对象,但此时还未初始化,对象属性只是初始值)放到操作数栈中6 dup
:将 new 的 String 对象复制一份同样放到操作数栈中7 ldc #4 <b>
:从字符串常量池中加载"b"
这个字符串到操作数栈中9 invokespecial #5 <java/lang/String.<init>>
:调用 String 类的<init>
方法(包含构造器),将 6 中复制出的 String 和 7 中载入的 “b” 字符串弹栈,传入到该方法中,从而真正地完成了一个 String 类型对象(此时已经做了初始化)的创建和初始化,将该对象放回到操作数栈中12 astore_2
:将 9 中初始化完毕的 String 对象存储到局部变量表中索引位置为1的位置(即赋值给对象 b)13 return
:方法返回
从上述流程中可以看出:
- 字面量形式的对象在程序启动加载阶段就已经被创建好放到了字符串常量池中,而不是等到执行该方法时才创建
- 字面量形式没有再 new 一个对象,而是把字符串常量池里已经存在的对象的地址赋给了引用对象 a
字符串对象存储方式
下图中堆区value指向 "javaEE"
的红色箭头代表每个对象内的 char[] value
都是共享的常量池中的唯一一份 char[] value
(它同样存储在一个 String 对象中):
字符串的特性:
结论:
- 常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量。
- 只要其中有一个是变量,结果就在堆中。原理是底层创建了一个
StringBuilder
对象,将字符串拼接完毕后返回了一个存储在堆中的String
类型对象 - 如果拼接的结果调用
intern()
方法,返回值就在常量池中。
测试代码:
1 |
|
面试题
1 | public class StringExer { |
输出结果
1 | good |
原因:
ex.str
传入change()
方法后,在栈中创建了一个局部变量str
,其同样指向字符串常量池中的"good"
匿名对象,此时str = "test ok"
执行后,将在字符串常量池中新建一个匿名字符串对象"test ok"
,并且被局部变量str
所指向,但ex.str
的内容保持不变(仍然指向字符串常量"good"
);ex.ch
传入change()
方法后,在栈中创建了局部变量ch[]
,其同样指向ex.ch
数组,但修改ch[0]
会导致ex.ch
的内容同样被修改。
二者的区别在于:String
类对象和char[]
数组对象作为形参传入时都是引用类型,修改形参时原本对象也应该被修改,但String
类的不可变性导致修改形参时在常量池中创建了新的字符串内容,因此原本对象内容没有改变,但char
数组并无此特性,因此会被修改。
String 的底层结构
字符串常量池是不会存储相同内容的字符串的
String 的 String Pool(字符串常量池)是一个固定大小的Hashtable
,默认值大小长度是1009。如果放进String Pool的String对象非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern()
方法时去常量池中查找是否已经存在某个字符串时性能会大幅下降。
使用-XX:StringTablesize
可设置StringTable的长度
- 在JDK 6中
StringTable
是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快,StringTablesize
设置没有要求 - 在JDK 7中,
StringTable
的长度默认值是60013,StringTablesize
设置没有要求 - 在JDK 8中,
StringTable
的长度默认值是60013,StringTable
可以设置的最小值为1009
为什么 JDK 9 改变了 String 结构
为什么改为 byte[] 存储?
String类的当前实现将字符存储在char[]数组中,每个字符使用两个字节(16位)。
从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分,而且大多数字符串对象只包含拉丁字符(Latin-1)。这些字符只需要一个字节的存储空间,因此这些字符串对象的内部char数组中有一半的空间将不会使用,产生了大量浪费。
之前 String 类使用 UTF-16 的 char[]
数组存储,JDK 9改为 byte[]
数组 外加一个编码标识存储。该编码表示如果你的字符是ISO-8859-1或者Latin-1,那么只需要一个字节存。如果你是其它字符集,比如UTF-8,你仍然用两个字节存
结论:在 JDK 9 之后,String再也不用 char[]
来存储了,改成了 byte []
加上编码标记,节约了一些空间。
1 | // 之前 |
同时基于String的数据结构,例如StringBuffer
和StringBuilder
也同样做了修改。
String 的内存分配
在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种:
- 直接使用双引号声明出来的字面量String对象会直接存储在常量池中。比如:
String info="hello";
- 如果不是用双引号声明的String对象,可以使用String提供的
intern()
方法将对象存储在常量池中。这个后面重点谈
存放位置调整:
- Java 6及以前,字符串常量池存放在永久代
- Java 7后oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内
所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用String.intern()。
Java 8 元空间,字符串常量在堆区:
为什么 StringTable 从永久代调整到堆中
为什么要调整位置?
- 永久代的默认空间大小比较小
- 永久代垃圾回收频率低,大量的字符串无法及时回收,容易进行Full GC产生STW或者容易产生
OOM:PermGen Space
- 堆中空间足够大,字符串可被及时回收
在JDK 7中,interned字符串不再在Java堆的永久代中分配,而是在Java堆的主要部分(称为年轻代和年老代)中分配,与应用程序创建的其他对象一起分配。
此更改将导致驻留在主Java堆中的数据更多,驻留在永久生成中的数据更少,因此可能需要调整堆大小。
String 的基本操作
Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String类实例。
1 | //官方示例代码 |
分析运行时内存(foo()
方法是实例方法,其实图中少了一个 this
局部变量)
字符串拼接操作
- 常量与常量的拼接结果在常量池,原理是编译期优化
- 常量池中不会存在相同内容的变量
- 只要其中有一个是变量,结果就在堆中。变量拼接的原理是使用
StringBuilder
- 如果拼接的结果调用
intern()
方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
字符串常量间的拼接
字符串常量与常量的拼接结果在常量池,原理是编译期优化
1 |
|
从字节码指令看出:编译器做了优化,将 "a" + "b" + "c"
优化成了 "abc"
1 | 0 ldc #2 <abc> <-------- 这里直接载入的是 "abc" |
字符串变量间的拼接
- 拼接前后,只要其中有一个是变量,结果就在堆中
- 调用
intern()
方法,则主动将字符串对象存入字符串常量池中,并将其地址返回
1 |
|
底层原理:字符串变量间的拼接操作的底层其实使用了 StringBuilder
s1 + s2
的执行细节:
StringBuilder s = new StringBuilder();
s.append(s1);
s.append(s2);
s.toString();
-> 类似于new String("ab");
在JDK 5之后,使用的是 StringBuilder
,在JDK5之前使用的是 StringBuffer
String | StringBuffer | StringBuilder |
---|---|---|
String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间 | StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。每个StringBuffer对象都有一定的缓冲区容量,当字符串大小没有超过容量时,不会分配新的容量,当字符串大小超过容量时,会自动增加容量 | 可变类,速度更快 |
不可变 | 可变 | 可变 |
线程安全 | 线程不安全 | |
多线程操作字符串 | 单线程操作字符串 |
注意,我们左右两边如果是变量的话,就是需要new StringBuilder
进行拼接,但是如果使用的是 final
修饰,则是从常量池中获取。
所以说拼接符号左右两边都是字符串常量或常量引用,则仍然使用编译器优化。也就是说被 final
修饰的变量,将会变成常量。
在开发中,能够使用 final
的时候,建议使用上
1 | public static void test4() { |
运行结果
1 | true |
拼接操作和 append() 性能对比
1 | public static void method1(int highLevel) { |
方法1耗费的时间:4005ms,方法2消耗时间:7ms
结论:通过 StringBuilder
的 append()
方式添加字符串的效率,要远远高于 String 的字符串拼接方法
StringBuilder
的 append()
方式,自始至终只创建一个 StringBuilder
的对象
而使用String的字符串拼接方式:
- 还需要创建很多
StringBuilder
对象和调用toString()
时候创建的 String 对象 - 内存中由于创建了较多的
StringBuilder
和 String 对象,内存占用过大,如果进行GC那么将会耗费更多的时间
改进的空间:
- 在实际开发中,如果基本确定要前前后后添加的字符串长度不高于某个限定值highLevel的情况下,建议使用构造器实例化:
StringBuilder s = new StringBuilder(highLevel); // new char[highLevel]
- 这样可以避免频繁扩容
intern() 的使用
intern():将字符串对象放到字符串常量池的内部
1 | public native String intern(); |
intern()
是一个 native方法,调用的是底层C的方法
字符串池最初是空的,由String类私有地维护。在调用 intern()
方法时,如果池中已经包含了由 equals(object)
方法确定的与该字符串对象相等的字符串,则返回池中的字符串对象的地址。否则,该字符串对象将被添加到池中,并返回对该字符串对象的引用。
如果不是用双引号声明的String对象,可以使用String提供的 intern()
方法:intern()
方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中(好处是可以减少堆中存放的大量冗余字符串对象),并将常量池中该对象的地址返回给引用对象。
比如:
1 | String myInfo = new string("hello").intern(); |
也就是说,如果在任意字符串上调用 String.intern()
方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true
1 | ("a"+"b"+"c").intern()=="abc" |
通俗点讲,intern()
就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池(String Intern Pool)
intern() 的空间效率测试
我们通过测试一下,使用了 intern()
和不使用的时候,其实相差还挺多的
1 | public class StringIntern2 { |
结论:对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用 intern()
方法能够节省内存空间,因为其将冗余的对象都GC,只在常量池里保存一份字符串数据。
大的网站平台,需要内存中存储大量的字符串。比如社交网站,很多人都存储:北京市、海淀区等信息。这时候如果字符串都调用 intern()
方法,就会很明显降低内存的大小。
String 相关的面试题
new String(“ab”) 会创建几个对象
1 | /** |
我们转换成字节码来查看
1 | 0 new #2 <java/lang/String> |
这里面就是两个对象
- 一个对象是:new关键字在堆空间中创建
- 另一个对象:字符串常量池中的对象
new String(“a”) + new String(“b”) 会创建几个对象
1 | /** |
字节码文件为
1 | 0 new #2 <java/lang/StringBuilder> |
我们创建了6个对象
- 对象1:new StringBuilder()
- 对象2:new String(“a”)
- 对象3:常量池的 a
- 对象4:new String(“b”)
- 对象5:常量池的 b
- 对象6:toString中会创建一个 new String(“ab”)
- 调用toString方法,不会在常量池中生成ab
字节码指令分析:
0 new #2 <java/lang/StringBuilder>
:拼接字符串会创建一个StringBuilder
对象7 new #4 <java/lang/String>
:创建 String 对象,对应于new String("a")
11 ldc #5 <a>
:在字符串常量池中放入"a"
(如果之前字符串常量池中没有"a"
的话)19 new #4 <java/lang/String>
:创建 String 对象,对应于new String("b")
23 ldc #8 <b>
:在字符串常量池中放入"b"
(如果之前字符串常量池中没有"b"
的话)31 invokevirtual #9 <java/lang/StringBuilder.toString>
:调用StringBuilder
的toString()
方法,会生成一个 String 对象
但是需要注意,StringBuilder
的 toString()
方法是直接将其内维护的 char[] value
数组内的元素通过构造器赋值给新建的 String 对象,而并没有以字面量的形式出现这个拼接后的字符串,因此不会在字符串常量池中创建 "ab"
对象,只有通过 intern()
方法才会将该对象存储到常量池中。
intern() 相关的面试题
1 | /** |
解释的已经比较清楚了,下面看一下内存图
内存分析
JDK 6 :正常眼光判断即可
new String()
即在堆中str.intern()
则把字符串放入常量池中
因为 JDK 6 中字符串常量池存储在永久代,其不在堆中,因此就是单纯的将字符串放到常量池中(不像 JDK 7 后的操作,将引用保存在常量池里)
JDK 7 及后续版本,注意大坑:因为字符串常量池移动到了堆区,因此为了节省空间,就不再常量池里拷贝一份字符串副本了,而是直接创建一个对象引用已经存在于堆区中的字符串对象(后续再引用该常量池里的对象时,其地址直接等于了存在于堆区的对象)
面试题的拓展
1 | /** |
intern() 方法的练习
练习 1
1 | public class StringExer1 { |
JDK 6
JDK 7/8
练习 2
1 | public class StringExer1 { |
这种情况下,因为已经使用字面量的形式将 "ab"
添加到了常量池中,所以后续的 intern()
就不会再像 练习1 一样指向 s
了
练习 3
1 | public class StringExer2 { |
练习 4
1 | public class StringExer2 { |
输出结果:
1 | 1836019240 |
String 使用陷阱
String s1 = "a";
说明:在字符串常量池中创建了一个字面量为"a"
的字符串。s1 = s1 + "b";
说明:实际上原来的"a"
字符串对象已经丢弃了,现在在堆空间中产生了一个字符串s1+"b"
(也就是"ab"
)。如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的性能。String s2 = "ab";
说明:直接在字符串常量池中创建一个字面量为"ab"
的字符串。String s3 = "a" + "b";
说明:s3指向字符串常量池中已经创建的"ab"
的字符串(编译期间优化时就将这行代码替换成了String s3 = "ab"
)。String s4 = s1.intern();
说明:堆空间的s1对象在调用intern()
之后,会将常量池中已经存在的"ab"
字符串的地址赋值给s4。
面试题:
结果:输出 good and best。原因:
ex.str
传入change()
方法后,在栈中创建了一个局部变量str
,其同样指向ex.str
所指向的字符串常量"good",此时str = "test ok"
执行后,局部变量str所指向的字符串常量变为"test ok",但ex.str
的内容保持不变(仍然指向字符串常量"good");ex.ch
传入change()
方法后,在栈中创建了局部变量ch[]
,其同样指向ex.ch
数组,但修改ch[0]
会导致ex.ch
的内容同样被修改。
二者的区别在于:String
类对象和char
数组对象作为形参传入时都是引用类型,修改形参时原本对象也应该被修改,但String
类的不可变性导致修改形参时在常量池中创建了新的字符串内容,因此原本对象内容没有改变,但char
数组并无此特性,因此会被修改。
String 常用方法
int length()
:返回字符串的长度: return value.lengthchar charAt(int index)
: 返回某索引处的字符return value[index]boolean isEmpty()
:判断是否是空字符串:return value.length == 0String toLowerCase()
:使用默认语言环境,将 String 中的所有字符转换为小写String toUpperCase()
:使用默认语言环境,将 String 中的所有字符转换为大写String trim()
:返回字符串的副本,忽略前导空白和尾部空白boolean equals(Object obj)
:比较字符串的内容是否相同boolean equalsIgnoreCase(String anotherString)
:与equals方法类似,忽略大小写String concat(String str)
:将指定字符串连接到此字符串的结尾。 等价于用“+”int compareTo(String anotherString)
:比较两个字符串的大小String substring(int beginIndex)
:返回一个新的字符串,它是此字符串的从 beginIndex开始截取到最后的一个子字符串。String substring(int beginIndex, int endIndex)
:返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。boolean endsWith(String suffix)
:测试此字符串是否以指定的后缀结束boolean startsWith(String prefix)
:测试此字符串是否以指定的前缀开始boolean startsWith(String prefix, int toffset)
:测试此字符串从指定索引开始的子字符串是否以指定前缀开始boolean contains(CharSequence s)
:当且仅当此字符串包含指定的 char 值序列时,返回 true
索引
int indexOf(String str)
:返回指定子字符串在此字符串中第一次出现处的索引int indexOf(String str, int fromIndex)
:返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始int lastIndexOf(String str)
:返回指定子字符串在此字符串中最右边出现处的索引int lastIndexOf(String str, int fromIndex)
:返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索
注:indexOf
和lastIndexOf
方法如果未找到都是返回-1
替换
String replace(char oldChar, char newChar)
:返回一个新的字符串,它是通过用newChar
替换此字符串中出现的所有oldChar
得到的。String replace(CharSequence target, CharSequence replacement)
:使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。String replaceAll(String regex, String replacement)
:使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。String replaceFirst(String regex, String replacement)
:使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
匹配
boolean matches(String regex)
:告知此字符串是否匹配给定的正则表达式。
切片
String[] split(String regex)
:根据给定正则表达式的匹配拆分此字符串。String[] split(String regex, int limit)
:根据匹配给定的正则表达式来拆分此字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中。
1 | public class StringMethodTest { |
String 与其他数据类型转换
String 与基本数据类型转换
- 字符串 ——> 基本数据类型、包装类
Integer
包装类的public static int parseInt(String s)
:可以将由“数字”字符组成的字符串转换为整型。- 类似地,使用
java.lang
包中的Byte
、Short
、Long
、Float
、Double
类调相应的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型。
- 基本数据类型、包装类 ——> 字符串
- 调用
String
类的public static String valueOf(int n)
可将int型转换为字符串 valueOf(byte b)
、valueOf(long l)
、valueOf(float f)
、valueOf(double d)
、valueOf(boolean b)
可由参数的相应类型到字符串的转换
- 调用
1 | // 字符串 ——> 基本数据类型、包装类 |
String 与字符数组 char[] 转换
- 字符数组
char[]
——> 字符串String
类的构造器:String(char[]) 和 String(char[],int offset,int length)
分别用字符数组中的全部字符和部分字符创建字符串对象。
- 字符串 ——> 字符数组
char[]
public char[] toCharArray()
:将字符串中的全部字符存放在一个字符数组中的方法。public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
:提供了将指定索引范围内的字符串存放到数组中的方法。
1 | // 字符数组char[] ——> 字符串 |
String 与字节数组转换
- 字节数组 ——> 字符串
String(byte[])
:通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的String
。String(byte[],int offset,int length)
:用指定的字节数组的一部分, 即从数组起始位置offset开始取length个字节构造一个字符串对象。
- 字符串 ——> 字节数组
public byte[] getBytes()
:使用平台的默认字符集将此String
编码为 byte 序列,并将结果存储到一个新的 byte 数组中。public byte[] getBytes(String charsetName)
:使用指定的字符集将此String
编码到 byte 序列,并将结果存储到新的 byte 数组。
1 | // 字节数组 ——> 字符串 |
StringBuffer 和 StringBuilder
StringBuffer
java.lang.StringBuffer
代表可变的字符序列,JDK1.0中声明,可以对字符串内容进行增删,此时不会产生新的对象。很多方法与String
相同。作为参数传递时,方法内部可以改变值。
StringBuffer
类不同于String
,其对象必须使用构造器生成。有三个构造器:
StringBuffer()
:初始容量为16的字符串缓冲区StringBuffer(int size)
:构造指定容量的字符串缓冲区StringBuffer(String str)
:将内容初始化为指定字符串内容
StringBuffer 的常用方法
StringBuffer append(xxx)
:提供了很多的append()方法,用于进行字符串拼接StringBuffer delete(int start,int end)
:删除指定位置的内容StringBuffer replace(int start, int end, String str)
:把[start,end)位置替换为strStringBuffer insert(int offset, xxx)
:在指定位置插入xxxStringBuffer reverse()
:把当前字符序列逆转public int indexOf(String str)
public String substring(int start,int end)
:返回一个从start开始到end索引结束的左闭右开区间的子字符串public int length()
public char charAt(int n )
public void setCharAt(int n ,char ch)
当append
和insert
时,如果原来value数组长度不够,可扩容
总结:
- 增:
append(xxx)
- 删:
delete(int start,int end)
- 改:
setCharAt(int n ,char ch) / replace(int start, int end, String str)
- 查:
charAt(int n )
- 插:
insert(int offset, xxx)
- 长度:
length()
- *遍历:
for() + charAt() / toString()
1 |
|
StringBuilder
StringBuilder
和 StringBuffer
非常类似,均代表可变的字符序列,而且提供相关功能的方法也一样。
String、StringBuffer、StringBuilder 三者的异同
String
:不可变的字符序列;底层使用final char[]存储StringBuffer
:可变的字符序列;线程安全的,效率低;底层使用char[]存储StringBuilder
:可变的字符序列;jdk5.0新增的,线程不安全的,效率高;底层使用char[]存储
对比String、StringBuffer、StringBuilder三者的效率:从高到低排列:StringBuilder > StringBuffer > String
注意:作为参数传递时,方法内部String不会改变其值,StringBuffer和StringBuilder会改变其值(因为其内的char[] value
不是final
的)。
源码分析:
1 | String str = new String();//char[] value = new char[0]; |
- 问题1.
System.out.println(sb2.length());
答:3 - 问题2. 扩容问题:如果要添加的数据底层数组盛不下了,那就需要扩容底层的数组。默认情况下,扩容为原来容量的2倍 + 2,同时将原有数组中的元素复制到新的数组中。
开发中建议使用:StringBuffer(int capacity)
或StringBuilder(int capacity)
。提前将容量设置好,以免扩容时复制元素造成时间浪费。