一:Java中String中最核心的问题是字符串的不可变性
为什么是不可变的?
简单的来说:String
类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]
,所以String
对象是不可变的。
字符串变化并不指的是赋值这种变化。
通过字符串类型和值类型在内存中的存储方式对比看看,字符串中的不可变到底指的是什么?
字符串类型:
不可变性:当你给一个字符串重新赋值之后,老值并没有在内存中销毁,而是重新开辟一块空间存储新值。
如果我们在实际开发中对很含有大量字符的字符串进行遍历赋值修改,会对内存中产生很多无法释放的字符串对象,造成内存垃圾。
二:字符串拼接的几种方式和区别
1.使用+
拼接字符串
在Java中,拼接字符串最简单的方式就是直接使用符号+
来拼接。如:
String wechat = "Hollis";
String introduce = "每日更新Java相关技术文章";
String hollis = wechat + "," + introduce;
这里要特别说明一点,有人把Java中使用+
拼接字符串的功能理解为运算符重载。其实并不是,Java是不支持运算符重载的。这其实只是Java提供的一个语法糖。
想要了解什么是语法糖,可以看一下这篇文章:https://zhuanlan.zhihu.com/p/78454816
concat
除了使用+
拼接字符串之外,还可以使用String类中的方法concat方法来拼接字符串。如:
String wechat = "Hollis";
String introduce = "每日更新Java相关技术文章";
String hollis = wechat.concat(",").concat(introduce);
StringBuffer
关于字符串,Java中除了定义了一个可以用来定义字符串常量的String
类以外,还提供了可以用来定义字符串变量的StringBuffer
类,它的对象是可以扩充和修改的。
使用StringBuffer
可以方便的对字符串进行拼接。如:
StringBuffer wechat = new StringBuffer("Hollis");
String introduce = "每日更新Java相关技术文章";
StringBuffer hollis = wechat.append(",").append(introduce);
StringBuilder
除了StringBuffer
以外,还有一个类StringBuilder
也可以使用,其用法和StringBuffer
类似。如:
StringBuilder wechat = new StringBuilder("Hollis");
String introduce = "每日更新Java相关技术文章";
StringBuilder hollis = wechat.append(",").append(introduce);
StringUtils.join
除了JDK中内置的字符串拼接方法,还可以使用一些开源类库中提供的字符串拼接方法名,如apache.commons中
提供的StringUtils
类,其中的join
方法可以拼接字符串。
String wechat = "Hollis";
String introduce = "每日更新Java相关技术文章";
System.out.println(StringUtils.join(wechat, ",", introduce));
这里简单说一下,StringUtils中提供的join方法,最主要的功能是:将数组或集合以某拼接符拼接到一起形成新的字符串,如:
String []list ={"Hollis","每日更新Java相关技术文章"};
String result= StringUtils.join(list,",");
System.out.println(result);
//结果:Hollis,每日更新Java相关技术文章
常用的字符串拼接方式有五种,分别是使用+
、使用concat
、使用StringBuilder
、使用StringBuffer
以及使用StringUtils.join
。
由于字符串拼接过程中会创建新的对象,所以如果要在一个循环体中进行字符串拼接,就要考虑内存问题和效率问题。
因此,经过对比,我们发现,直接使用StringBuilder
的方式是效率最高的。因为StringBuilder
天生就是设计来定义可变字符串和字符串的变化操作的。
但是,还要强调的是:
1、如果不是在循环体中进行字符串拼接的话,直接使用+
就好了。
2、如果在并发场景中进行字符串拼接的话,要使用StringBuffer
来代替StringBuilder
。
三:String,StringBuffer,StringBuilder
可变性
简单的来说:String
类中使用 final 关键字修饰字符数组来保存字符串,private final char value[]
,所以String
对象是不可变的。
补充(来自issue 675):在 Java 9 之后,String 、
StringBuilder
与StringBuffer
的实现改用 byte 数组存储字符串private final byte[] value
而 StringBuilder
与 StringBuffer
都继承自 AbstractStringBuilder
类,在 AbstractStringBuilder
中也是使用字符数组保存字符串char[]value
但是没有用 final
关键字修饰,所以这两种对象都是可变的。
线程安全性
String
中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder
是 StringBuilder
与 StringBuffer
的公共父类,定义了一些字符串的基本操作,如 expandCapacity
、append
、insert
、indexOf
等公共方法。StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。StringBuffer
每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder
相比使用 StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
- 操作少量的数据: 适用
String
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
四:switch对String的支持
Java 7中,switch的参数可以是String类型了,这对我们来说是一个很方便的改进。到目前为止switch支持这样几种数据类型:byte
short
int
char
String
。但是,作为一个程序员我们不仅要知道他有多么好用,还要知道它是如何实现的,switch对整型的支持是怎么实现的呢?对字符型是怎么实现的呢?String类型呢?有一点Java开发经验的人这个时候都会猜测switch对String的支持是使用equals()方法和hashcode()方法。那么到底是不是这两个方法呢?接下来我们就看一下,switch到底是如何实现的。
1.switch对整形支持的实现
public class switchDemoInt {
public static void main(String[] args) {
int a = 5;
switch (a) {
case 1:
System.out.println(1);
break;
case 5:
System.out.println(5);
break;
default:
break;
}
}
}
//output 5
反编译后:
public class switchDemoInt
{
public switchDemoInt()
{
}
public static void main(String args[])
{
int a = 5;
switch(a)
{
case 1: // '\001'
System.out.println(1);
break;
case 5: // '\005'
System.out.println(5);
break;
}
}
}
2switch对字符型支持的实现
public class switchDemoInt {
public static void main(String[] args) {
char a = 'b';
switch (a) {
case 'a':
System.out.println('a');
break;
case 'b':
System.out.println('b');
break;
default:
break;
}
}
}
编译后的代码:
public class switchDemoChar
{
public switchDemoChar()
{
}
public static void main(String args[])
{
char a = 'b';
switch(a)
{
case 97: // 'a'
System.out.println('a');
break;
case 98: // 'b'
System.out.println('b');
break;
}
}
}
通过以上的代码作比较我们发现:对char类型进行比较的时候,实际上比较的是ascii码,编译器会把char型变量转换成对应的int型变量
3switch对字符串支持的实现
public class switchDemoString {
public static void main(String[] args) {
String str = "world";
switch (str) {
case "hello":
System.out.println("hello");
break;
case "world":
System.out.println("world");
break;
default:
break;
}
}
}
对代码进行反编译:
public class switchDemoString
{
public switchDemoString()
{
}
public static void main(String args[])
{
String str = "world";
String s;
switch((s = str).hashCode())
{
default:
break;
case 99162322:
if(s.equals("hello"))
System.out.println("hello");
break;
case 113318802:
if(s.equals("world"))
System.out.println("world");
break;
}
}
}
看到这个代码,你知道原来字符串的switch是通过equals()
和hashCode()
方法来实现的。记住,switch中只能使用整型,比如byte
。short
,char
(ackii码是整型)以及int
。还好hashCode()
方法返回的是int
,而不是long
。通过这个很容易记住hashCode
返回的是int
这个事实。仔细看下可以发现,进行switch
的实际是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行switch或者使用纯整数常量,但这也不是很差。因为Java编译器只增加了一个equals
方法,如果你比较的是字符串字面量的话会非常快,比如”abc” ==”abc”。如果你把hashCode()
方法的调用也考虑进来了,那么还会再多一次的调用开销,因为字符串一旦创建了,它就会把哈希值缓存起来。因此如果这个switch
语句是用在一个循环里的,比如逐项处理某个值,或者游戏引擎循环地渲染屏幕,这里hashCode()
方法的调用开销其实不会很大。
好,以上就是关于switch对整型、字符型、和字符串型的支持的实现方式,总结一下我们可以发现,其实switch只支持一种数据类型,那就是整型,其他数据类型都是转换成整型之后再使用switch的。
四:String长度有没有限制?
想要搞清楚这个问题,首先我们需要翻阅一下String的源码,看下其中是否有关于长度的限制或者定义。
String类中有很多重载的构造函数,其中有几个是支持用户传入length来执行长度的:
public String(byte bytes[], int offset, int 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常量池。
那么问题就来了,因为要进入常量池,就要遵守常量池的有关规定。
常量池限制
我们知道,javac是将Java文件编译成class文件的一个命令,那么在Class文件生成过程中,就需要遵守一定的格式。
根据《Java虚拟机规范》中第4.4章节常量池的定义,CONSTANT_String_info 用于表示 java.lang.String 类型的常量对象,格式如下:
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
其中,string_index 项的值必须是对常量池的有效索引, 常量池在该索引处的项必须是 CONSTANT_Utf8_info 结构,表示一组 Unicode 码点序列,这组 Unicode 码点序列最终会被初始化为一个 String 对象。
CONSTANT_Utf8_info 结构用于表示字符串常量的值:
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
其中,length则指明了 bytes[]数组的长度,其类型为u2,
通过翻阅《规范》,我们可以获悉。u2表示两个字节的无符号数,那么1个字节有8位,2个字节就有16位。
16位无符号数可表示的最大值位2^16 - 1 = 65535。
也就是说,Class文件中常量池的格式规定了,其字符串常量的长度不能超过65535。
那么,我们尝试使用以下方式定义字符串:
String s = "11111...1111";//其中有65535个字符"1"
尝试使用javac编译,同样会得到"错误: 常量字符串过长",那么原因是什么呢?
其实,这个原因在javac的代码中是可以找到的,在Gen类中有如下代码:
private void checkStringConstant(DiagnosticPosition var1, Object var2) {
if (this.nerrs == 0 && var2 != null && var2 instanceof String && ((String)var2).length() >= 65535) {
this.log.error(var1, "limit.string", new Object[0]);
++this.nerrs;
}
}
代码中可以看出,当参数类型为String,并且长度大于等于65535的时候,就会导致编译失败。
这个地方大家可以尝试着debug一下javac的编译过程(视频中有对java的编译过程进行debug的方法),也可以发现这个地方会报错。
如果我们尝试以65534个字符定义字符串,则会发现可以正常编译。
运行期限制
上面提到的这种String长度的限制是编译期的限制,也就是使用String s= “”;这种字面值方式定义的时候才会有的限制。
那么。String在运行期有没有限制呢,答案是有的,就是我们前文提到的那个Integer.MAX_VALUE ,这个值约等于4G,在运行期,如果String的长度超过这个范围,就可能会抛出异常。(在jdk 1.9之前)
int 是一个 32 位变量类型,取正数部分来算的话,他们最长可以有
2^31-1 =2147483647 个 16-bit Unicodecharacter
2147483647 * 16 = 34359738352 位
34359738352 / 8 = 4294967294 (Byte)
4294967294 / 1024 = 4194303.998046875 (KB)
4194303.998046875 / 1024 = 4095.9999980926513671875 (MB)
4095.9999980926513671875 / 1024 = 3.99999999813735485076904296875 (GB)
有近 4G 的容量。
很多人会有疑惑,编译的时候最大长度都要求小于65535了,运行期怎么会出现大于65535的情况呢。这其实很常见,如以下代码:
String s = "";
for (int i = 0; i <100000 ; i++) {
s+="i";
}
总结:
字符串有长度限制,在编译期,要求字符串常量池中的常量不能超过65535,并且在javac执行过程中控制了最大值为65534。
在运行期,长度不能超过Int的范围,否则会抛异常。
参考资料:http://hollischuang.gitee.io/tobetopjavaer/#/basics/java-basic/length-of-string