小伙伴儿们,如果觉得文章干货满满,欢迎加入公众号【编程识堂】,更多干货等你们来哦!
前言
String在java中特别常用,但小伙伴儿们对String真的彻底了解了吗?今天跟着小堂我一起盘它、弄懂它。
为什么说字符串是不可变的
在工作中,我们经常要在代码中对字符串进行赋值和改变他的值,但是,为什么我们说字符串是不可变的呢?
源码解析
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/**
* Initializes a newly created {@code String} object so that it represents
* an empty character sequence. Note that use of this constructor is
* unnecessary since Strings are immutable.
*/
public String() {
this.value = "".value;
}
/**
* Initializes a newly created {@code String} object so that it represents
* the same sequence of characters as the argument; in other words, the
* newly created string is a copy of the argument string. Unless an
* explicit copy of {@code original} is needed, use of this constructor is
* unnecessary since Strings are immutable.
*
* @param original
* A {@code String}
*/
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
}
String类的值是保存在value数组中的,并且是被private final修饰的
- private修饰,表明外部的类是访问不到value的,同时⼦类也访问不到,当然String类不可能有⼦类,因为类被final修饰了。
- final修饰,表明value的引⽤是不会被改变的,⽽value只会在String的构造函数中被初始化,⽽且并没有其他⽅法可以修改value数组中的值,保证了value的引⽤和值都不会发⽣变化。
final关键字的作⽤有如下⼏种:
- final修饰类时,表明这个类不能被继承
- final修饰⽅法,表明⽅法不能被重写
- final修饰变量,如果是基本数据类型的变量,则其数值⼀旦在初始化之后便不能改变;如果是对象类型的变量,只能保证它的引⽤不变,但对象的内容是可以改变的
在Java中数组也是对象,数组即使被final修饰,内容还是可以改变的。
所以我们说String类是不可变的。⽽很多⽅法,如substring并不是在原来的String类上进⾏操作,⽽是⽣成了新的String类。
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
什么是不可变对象
首先,我们需要知道什么是不可变对象?
不可变对象是在完全创建后其内部状态保持不变的对象。这意味着,一旦对象被赋值给变量,我们既不能更新引用,也不能通过任何方式改变内部状态。
原因
可是有人会有疑惑,String为什么不可变,我的代码中经常改变String的值啊,如下:
String s = "abcd";
s = s.concat("ef");
这样,操作,不就将原本的”abcd”的字符串改变成”abcdef”了么?
但是,虽然字符串内容看上去从”abcd”变成了”abcdef”,但是实际上,我们得到的已经是一个新的字符串了。

如上图,在堆中重新创建了一个”abcdef”字符串,和”abcd”并不是同一个对象。
所以,一旦一个string对象在内存(堆)中被创建出来,他就无法被修改。而且,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。
如果我们想要一个可修改的字符串,可以选择StringBuffer 或者 StringBuilder这两个代替String。
为什么String要设计成不可变
在知道了”String是不可变”的之后,大家是不是一定都很疑惑:为什么要把String设计成不可变的呢?有什么好处呢?
这个问题,困扰过很多人,甚至有人直接问过Java的创始人James Gosling。
在一次采访中James Gosling被问到什么时候应该使用不可变变量,他给出的回答是:
I would use an immutable whenever I can.
那么,他给出这个答案背后的原因是什么呢?是基于哪些思考的呢?
其实,主要是从缓存、安全性、线程安全和性能等角度触发的。
Q:缓存、安全性、线程安全和性能?这有都是啥
A:你别急,听我一个一个给你讲就好了。
缓存
字符串是使用最广泛的数据结构。大量的字符串的创建是非常耗费资源的,所以,Java提供了对字符串的缓存功能,可以大大的节省堆空间。
JVM中专门开辟了一部分空间来存储Java字符串,那就是字符串常量池。
通过字符串常量池,两个内容相同的字符串变量,可以从池中指向同一个字符串对象,从而节省了关键的内存资源。
String s = "abcd";
String s2 = s;
对于这个例子,s和s2都表示”abcd”,所以他们会指向字符串池中的同一个字符串对象:

但是,之所以可以这么做,主要是因为字符串的不变性。试想一下,如果字符串是可变的,我们一旦修改了s的内容,那必然导致s2的内容也被动的改变了,这显然不是我们想看到的。
面试的时候我们经常被问到:
String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
String str4 = new String("abc");
// true
System.out.println(str1 == str2);
// false
System.out.println(str1 == str3);
// false
System.out.println(str3 == str4);
内存中的结构如下:
其中常量池中存的是引⽤
解释⼀下上⾯代码的输出,Java中有2种创建字符串对象的⽅式
String str1 = "abc";
String str2 = "abc";
// true
System.out.println(str1 == str2);
采⽤字⾯值的⽅式创建⼀个字符串时,JVM⾸先会去字符串池中查找是否存在”abc”这个对象的引⽤
如果不存在,则在堆中创建”abc”这个对象,并将其引⽤添加到字符串常量池(实际上是将引⽤放到哈希
表中),随后将引⽤赋给str1
如果存在,则不创建任何对象,直接将池中”abc”对象的引⽤返回,赋给str2。因为str1、str2指向同⼀
个对象,所以结果为true。
String str3 = new String("abc");
String str4 = new String("abc");
// false
System.out.println(str3 == str4);
采⽤new关键字新建⼀个字符串对象时,JVM⾸先在字符串池中查找有没有”abc”这个字符串对象的引⽤,
如果没有,则先在堆中创建⼀个”abc”字符串对象,并将引⽤添加到字符串常量池,随后将引⽤赋给str3。
如果有,则不往池中放”abc”对象的引⽤,直接在堆中创建⼀个”abc”字符串对象,然后将引⽤赋给 str4。这样,str4就指向了堆中创建的这个”abc”字符串对象;
因为str3和str4指向的是不同的字符串对象,结果为false。
安全性
字符串在Java应用程序中广泛用于存储敏感信息,如用户名、密码、连接url、网络连接等。JVM类加载器在加载类的时也广泛地使用它。
因此,保护String类对于提升整个应用程序的安全性至关重要。
当我们在程序中传递一个字符串的时候,如果这个字符串的内容是不可变的,那么我们就可以相信这个字符串中的内容。
但是,如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全不可信了。这样整个系统就没有安全性可言了。
线程安全
不可变会自动使字符串成为线程安全的,因为当从多个线程访问它们时,它们不会被更改。
因此,一般来说,不可变对象可以在同时运行的多个线程之间共享。它们也是线程安全的,因为如果线程更改了值,那么将在字符串池中创建一个新的字符串,而不是修改相同的值,不会发生竞争条件,也不需要进行额外的同步操作。因此,字符串对于多线程来说是安全的。
hashcode缓存
由于字符串对象被广泛地用作数据结构,它们也被广泛地用于哈希实现,如HashMap、HashTable、HashSet等。在对这些散列实现进行操作时,经常调用hashCode()方法。
不可变性保证了字符串的值不会改变。因此,hashCode()方法在String类中被重写,以方便缓存,这样在第一次hashCode()调用期间计算和缓存散列,并从那时起返回相同的值。
在String类中,有以下代码:
private int hash;//this is used to cache hash code.
性能
前面提到了的字符串池、hashcode缓存等,都是提升性能的提现。
因为字符串不可变,所以可以用字符串池缓存,可以大大节省堆内存。而且还可以提前对hashcode进行缓存,更加高效
由于字符串是应用最广泛的数据结构,提高字符串的性能对提高整个应用程序的总体性能有相当大的影响。
字符串拼接
字符串拼接是我们在Java代码中比较经常要做的事情,就是把多个字符串拼接到一起。
我们都知道,String是Java中一个不可变的类,所以他一旦被实例化就无法被修改。但是,既然字符串是不可变的,那么字符串拼接又是怎么回事呢?
字符串不变性与字符串拼接
其实,所有的所谓字符串拼接,都是重新生成了一个新的字符串。下面一段字符串拼接代码:
String s = "abcd";
s = s.concat("ef");
其实我们最后得到的是一个新字符串,如下图:
s是保存的是一个重新创建出来的String对象的引用。
那么我们在工作过程中会遇到很多种拼接方式,小伙伴儿们,平时是怎么使用的呢?今天跟着小堂来一起回顾下比较常用的方式。
使用+拼接字符串
演示
拼接字符串最简单的方式就是直接使用+号拼接,如:
String wechat = "编程识堂";
String introduce = "每日更新Java相关技术文章,关注我不迷路";
String bcst = wechat + "," + introduce;
System.out.println(bcst);
//结果为:编程识堂,每日更新Java相关技术文章,关注我不迷路
原理
通过反编译成字节码后我们发现,主要是通过StringBuilder的append方法实现的。
concat
演示
使用String类中的concat方法来拼接字符串。如:
String wechat = "编程识堂";
String introduce = "每日更新Java相关技术文章,关注我不迷路";
System.out.println(wechat.concat(introduce));
原理
源码
wechat.concat(introduce);
/**
str="每日更新Java相关技术文章,关注我不迷路"; otherLen=21
this:wechat = "编程识堂";
value: "编程识堂" length:4;
**/
public String concat(String str) {
if (str.isEmpty()) {
return this;
}
int len = value.length;
int otherLen = str.length();
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
/**
Arrays.copyOf 方法
original:{编,程,识,堂} 源数组
newLength: 25
**/
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
/**
original:{编,程,识,堂} 源数组
srcPos:0 从源数组中0位置开始复制元素到目标数组中
copy:{,,......,,,} length=25 目标数组
destPos:0 目标数组中从0位置开始存储源数组的中的元素
Math.min(original.length4, newLength) 要复制的元素个数 original.length=4个
**/
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
/**
copy:{'编','程','识','堂','\u0000',......,'\u0000'} length=25
**/
return copy;
}
/** str.getChars方法
dst[] 目标数组:{'编','程','识','堂','\u0000',......,'\u0000'} length=25
dstBegin: 4 目标数组中从4位置开始存储源数组的中的元素
* Copy characters from this string into dst starting at dstBegin.
* This method doesn't perform any range checking.
*/
void getChars(char dst[], int dstBegin) {
/**
value:源数组:{'每','日','更','新','J','a','v','a','相','关','技','术',
'文','章',',','关','注','我','不','迷','路'}
value.length:21
**/
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
System.arraycopy
System.arraycopy是Java语言中一个用于数组复制的方法。它可以将一个数组的部分或全部元素复制到另一个数组中。
方法签名: public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
参数解释:
- src:源数组
- srcPos:源数组中复制元素的起始位置
- dest:目标数组
- destPos:目标数组中复制元素的起始位置
- length:要复制的元素个数
使用示例:
int[] src = {1, 2, 3, 4, 5};
int[] dest = new int[5];
System.arraycopy(src, 0, dest, 0, 5);
//结果 dest: {1, 2, 3, 4, 5}
上述代码将src数组中的所有元素复制到dest数组中,两个数组中的元素值相同。
StringBuffer和StringBuilder
演示
关于字符串,Java中除了定义了一个可以用来定义字符串常量的String类以外,还提供了可以用来定义字符串变量的StringBuffer和StringBuilder类,它的对象是可以扩充和修改的。
使用StringBuffer可以方便的对字符串进行拼接。如:
StringBuffer sb = new StringBuffer("编程识堂");
sb.append("每日更新Java相关技术文章,关注我不迷路");
System.out.println(sb.toString());
//编程识堂每日更新Java相关技术文章,关注我不迷路
原理
根据类图,我们知道StringBuffer和StringBuilder都继承⾃AbstractStringBuilder类。
源码
AbstractStringBuilder
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
与String不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中
已经使用的字符个数,定义如下
*/
int count;
/**
* This no-arg constructor is necessary for serialization of subclasses.
*/
AbstractStringBuilder() {
}
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
//⾃动扩容机制
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
}
看到区别了吗?value数组没有⽤private和final修饰,说明了StringBuffer和StringBuilder是可变的。
与String不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数(count)。
抽象类AbstractStringBuilder内部提供了⼀个⾃动扩容机制,当发现⻓度不够的时候,会⾃动进⾏
扩容⼯作(具体扩容可以看源码,很容易理解),会创建⼀个新的数组,并将原来数组的数据复制
到新数组,不会创建新对象,拼接字符串的效率⾼.
StringBuilder
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence{
public StringBuilder append(String str) {
super.append(str);
return this;
}
public StringBuilder append(String str) {
super.append(str);
return this;
}
}
StringBuffer
public final class StringBuffer extends AbstractStringBuilder
implements java.io.Serializable, CharSequence{
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
}
StringBuffer和StringBuilder类似,最大的区别就是StringBuffer是线程安全的。
String、 StringBuffer 、StringBuilder的区别
1.都是final类,不允许被继承
2. String⻓度是不可变的,StringBuffer,StringBuilder⻓度是可变的
3. StringBuffer是线程安全的,StringBuilder不是线程安全的。但它们⽅法实现类似,StringBuffer在
⽅法之上添加了synchronized修饰,保证线程安全
4. StringBuilder⽐StringBuffer拥有更好的性能
5. 如果⼀个String类型的字符串,在编译时可以确定是⼀个字符串常量,则编译完成之后,字符串会
⾃动拼接成⼀个常量,此时String的速度⽐StringBuffer和StringBuilder的性能好的多
用例子解释下第五条:
public static void main(String[] args) {
String a = "a";
String b = "b";
String c = a + b;
String d = "a" + "b" + "c";
}
看String d ,理解了吧?编译后直接为abc.
同时看string c的拼接过程,先⽣成⼀个StringBuilder对象,再调⽤2次append⽅法,最后再返回⼀个
String对象,知道String⽐StringBuilder慢的原因了吧。
那么也就是说,Java中的+对字符串的拼接,其实现原理是使用StringBuilder.append。
但是,String的使用+字符串拼接也不全都是基于StringBuilder.append,还有种特殊情况,那就是如果是两个固定的字面量拼接,如:
String d = “a” + “b” + “c”;
编译器会进行常量折叠(因为两个都是编译期常量,编译期可知),直接变成 String d = “abc”。
StringUtils.join
除了JDK中内置的字符串拼接方法,还可以使用一些开源类库中提供的字符串拼接方法名,如apache.commons中提供的StringUtils类,其中的join方法可以拼接字符串。
String a = "a";
String b = "b";
String result = StringUtils.join(a, ",", b);
System.out.println(result);//a,b
这里简单说一下,StringUtils中提供的join方法,最主要的功能是:将数组或集合以某拼接符拼接到一起形成新的字符串,如:
public static void main(String[] args) {
List<String> list = Arrays.asList("编程识堂","每日更新Java相关技术文章,关注我不迷路");
System.out.println( StringUtils.join(list,","));
//编程识堂,每日更新Java相关技术文章,关注我不迷路
}
原理
public static String join(Object[] array, char separator, int startIndex, int endIndex) {
if (array == null) {
return null;
} else {
int noOfItems = endIndex - startIndex;
if (noOfItems <= 0) {
return "";
} else {
StringBuilder buf = new StringBuilder(noOfItems * 16);
for(int i = startIndex; i < endIndex; ++i) {
if (i > startIndex) {
buf.append(separator);
}
if (array[i] != null) {
buf.append(array[i]);
}
}
return buf.toString();
}
}
}
通过查看StringUtils.join的源代码,我们可以发现,其实他也是通过StringBuilder来实现的。
StringJoiner
StringJoiner是java.util包中的一个类,用于构造一个由分隔符分隔的字符序列(可选),并且可以从提供的前缀开始并以提供的后缀结尾。虽然这也可以在StringBuilder类的帮助下在每个字符串之后附加分隔符,但StringJoiner提供了简单的方法来实现,而无需编写大量代码。
StringJoiner类共有2个构造函数,5个公有方法。其中最常用的方法就是add方法和toString方法,类似于StringBuilder中的append方法和toString方法。
演示
public static void main(String[] args) {
List<String> list = Arrays.asList("编程识堂","每日更新Java相关技术文章,关注我不迷路");
StringJoiner stringJoiner = new StringJoiner(":","[","]");
stringJoiner.add("编程识堂").add("每日更新Java相关技术文章,关注我不迷路");
System.out.println( stringJoiner.toString());//[编程识堂:每日更新Java相关技术文章,关注我不迷路]
String result = list.stream().collect(Collectors.joining("-", "{", "}"));
System.out.println(result);//{编程识堂-每日更新Java相关技术文章,关注我不迷路}
}
原理
主要看一下add方法:
public StringJoiner(CharSequence delimiter,
CharSequence prefix,
CharSequence suffix) {
Objects.requireNonNull(prefix, "The prefix must not be null");
Objects.requireNonNull(delimiter, "The delimiter must not be null");
Objects.requireNonNull(suffix, "The suffix must not be null");
// make defensive copies of arguments
this.prefix = prefix.toString();
this.delimiter = delimiter.toString();
this.suffix = suffix.toString();
this.emptyValue = this.prefix + this.suffix;
}
public StringJoiner add(CharSequence newElement) {
prepareBuilder().append(newElement);
return this;
}
private StringBuilder prepareBuilder() {
if (value != null) {
value.append(delimiter);
} else {
value = new StringBuilder().append(prefix);
}
return value;
}
看到了一个熟悉的身影——StringBuilder ,没错,StringJoiner其实就是依赖StringBuilder实现的。
当我们发现StringJoiner其实是通过StringBuilder实现之后,我们大概就可以猜到,他的性能损耗应该和直接使用StringBuilder差不多!
为什么要用StringJoiner
在了解了StringJoiner的用法和原理后,可能很多读者就会产生一个疑问,明明已经有一个StringBuilder了,为什么Java 8中还要定义一个StringJoiner呢?到底有什么好处呢?
如果读者足够了解Java 8的话,或许可以猜出个大概,这肯定和Stream有关。
public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
CharSequence prefix,
CharSequence suffix) {
return new CollectorImpl<>(
() -> new StringJoiner(delimiter, prefix, suffix),
StringJoiner::add, StringJoiner::merge,
StringJoiner::toString, CH_NOID);
}
我们通过Collectors.joining源码发现是通过StringJoiner实现的。
总结
如果日常开发中中,需要进行字符串拼接,如何选择?
1、如果只是简单的字符串拼接,考虑直接使用”+”即可。
2、如果是在for循环中进行字符串拼接,考虑使用StringBuilder和StringBuffer。
3、如果是通过一个List进行字符串拼接,则考虑使用StringJoiner。
StringJoiner其实是通过StringBuilder实现的,所以他的性能和StringBuilder差不多,他也是非线程安全的。
字符串有没有长度限制
想要搞清楚这个问题,首先我们需要翻阅一下String的源码,看下其中是否有关于长度的限制或者定义。
String类中有很多重载的构造函数,其中有几个是支持用户传入length来执行长度的:
public String(byte bytes[], int offset, int length) {
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(bytes, offset, length);
}
可以看到,这里面的参数length是使用int类型定义的,那么也就是说,String定义的时候,最大支持的长度就是int的最大范围值。
根据Integer类的定义,java.lang.Integer#MAX_VALUE的最大值是2^31 – 1;
那么,我们是不是就可以认为String能支持的最大长度就是这个值了呢?
其实并不是,这个值只是在运行期,我们构造String的时候可以支持的一个最大长度,而实际上,在编译期,定义字符串的时候也是有长度限制的。
如以下代码:
String s = “11111…1111″;//其中有10万个字符”1”
当我们使用如上形式定义一个字符串的时候,当我们执行javac编译时,是会抛出异常的,提示如下:
错误: 常量字符串过长
那么,明明String的构造函数指定的长度是可以支持2147483647(2^31 – 1)的,为什么像以上形式定义的时候无法编译呢?
其实,形如String s = “xxx”;定义String的时候,xxx被我们称之为字面量,这种字面量在编译之后会以常量的形式进入到Class常量池。
那么问题就来了,因为要进入常量池,就要遵守常量池的有关规定。
所以字符串有长度限制,在编译期,要求字符串常量池中的常量不能超过65535,并且在javac执行过程中控制了最大值为65534。
在运行期,长度不能超过Int的范围,否则会抛异常。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/91452.html