java 基础篇-05-String 字符串又长度限制吗?常量池详解 String 类源码分析
问题
字符串的不可变性
String、StringBuilder和StingBuffer之间的区别与联系
字符串拼接的几种方式和区别
String对“+”的重载
String.valueOf和Integer.toString的区别
switch对String的支持
String的长度限制
字符串池、Integer的缓存机制
常量池(运行时常量池、Class常量池)
Java String 有长度限制吗?
理论长度
/**
* Allocates a new {@code String} that contains characters from a subarray
* of the Unicode code point array
* argument. The {@code offset} argument is the index of the first code
* point of the subarray and the {@code count} argument specifies the
* length of the subarray. The contents of the subarray are converted to
* {@code char}s; subsequent modification of the {@code int} array does not
* affect the newly created string.
*
* @param codePoints
* Array that is the source of Unicode code points
*
* @param offset
* The initial offset
*
* @param count
* The length
*
* @throws IllegalArgumentException
* If any invalid Unicode code point is found in {@code
* codePoints}
*
* @throws IndexOutOfBoundsException
* If the {@code offset} and {@code count} arguments index
* characters outside the bounds of the {@code codePoints} array
*
* @since 1.5
*/
public String(int[] codePoints, int offset, int count) {
//...
}
/**
* Returns the length of this string.
* The length is equal to the number of Unicode
* code units in the string.
*
* @return the length of the sequence of characters represented by this
* object.
*/
public int length() {
return value.length;
}
基本可以看出 String 的一些信息是 int 存储的,理论值应该是 Integer.MAX_VALUE=0x7fffffff=2^31 - 1,约等于 4G。
计算过程
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)
实际
当然实际情况看要看 jvm 配置等信息。
String 作为对象,所以要关注下 jvm 堆内存的大小。
测试
实际编码过程中有时候可能没有这么乐观,你可以验证下 65535 应该就是直接声明的最长的长度了。
String s = "a....a"; //65535 个a
直接编译会报错:
Error:(13, 20) java: 常量字符串过长
如果你是使用循环的话,则不会报错,比如下面这样:
String s = "";
for(int i = 0; i 当创建一个string对象的时候,去字符串常量池看是否有相应的字面量,如果没有就创建一个。 这个说法从来都不正确。 对象在堆里。常量池存引用。
### 实现常量池的条件
实际上这里牵扯出了另外一个非常有趣的问题。
(1)那就是 String 为什么是不可变得?
从这个角度也许你能得到自己想要的答案。
常量池,顾名思义,就是一个放常量的池子。那么里面存放的值就应该是常量(有点废话),String 如果放在里面变来变去,是无法复用的。
字符串作为常量,不用担心被修改,所以共享不存在任何问题。
(2)引用的维护
运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串一般不会被垃圾收集器回收。
这个和基本类型是类似的,一般不会参与垃圾回收。
ps: 只能说 jvm 作为基础,稍微学习下就涉及到 jvm 的相关知识。所以直接搞定是最好的。jvm 系列已经学过几遍了,有时间整理一下。
(3)GC 的问题
因为字符串常量池中持有了共享的字符串对象的引用,这就是说是不是会导致这些对象无法回收?
首先问题中共享的对象一般情况下都比较小。
据我查证了解,在早期的版本中确实存在这样的问题,但是随着弱引用的引入,目前这个问题应该没有了。
> [弱引用](https://houbb.github.io/2018/08/20/java-weak-reference)
## 常量池放在哪里?
这个涉及到 jmm(java 内存模型),详情参考:
> [java 虚拟机(jvm)-02-java 内存模型(jmm)介绍](https://www.jianshu.com/p/a276b307f887)

### 基本概念
我们就其中的堆、栈、方法做下讲解。
- 堆
存储的是对象,每个对象都包含一个与之对应的class
JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定
- 栈
每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)
每个栈中的数据(原始类型和对象引用)都是私有的
栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)
数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会自动消失
- 方法区
静态区,跟堆一样,被所有的线程共享
方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量
### jdk 的版本
有很多会说字符串就是存放在方法区的。
实际上,这个字符串常量池的位置也是随着jdk版本的不同而位置不同。
在jdk6中,常量池的位置在永久代(方法区)中,此时常量池中存储的是对象。
在jdk7中,常量池的位置在堆中,此时,常量池存储的就是引用了。
在jdk8中,永久代(方法区)被元空间取代了。
ps: 很多人都停留在 jdk7
## 例子
```java
String str1 = "abc";
String str2 = "abc";
String str3 = "abc";
String str4 = new String("abc");
String str5 = new String("abc");

经典面试题
面试题:String str4 = new String("abc")
创建多少个对象?
分析:
(1)在常量池中查找是否有“abc”对象
1.1 有则返回对应的引用实例
1.2 没有则创建对应的实例对象
(2)在堆中 new 一个 String("abc") 对象
(3)将对象地址赋值给str4,创建一个引用
所以,常量池中没有“abc”字面量则创建两个对象,否则创建一个对象,以及创建一个引用
操作字符串常量池的方式
- new
String str1 = "hello";
String str2 = "hello";
System.out.printl("str1 == str2" : str1 == str2 ) //true
- String.intern()
通过new操作符创建的字符串对象不指向字符串池中的任何对象,但是可以通过使用字符串的intern()方法来指向其中的某一个。
java.lang.String.intern()返回一个保留池字符串,就是一个在全局字符串池中有了一个入口。
如果以前没有在全局字符串池中,那么它就会被添加到里面。
字面量是何时进入常量池
HotSpot VM的实现来说,加载类的时候,那些字符串字面量会进入到当前类的运行时常量池,不会进入全局的字符串常量池;
在字面量赋值的时候,会翻译成字节码ldc指令,ldc指令触发lazy resolution动作
到当前类的运行时常量池(runtime constant pool,HotSpot VM里是ConstantPool + ConstantPoolCache)去查找该index对应的项
如果该项尚未resolve则resolve之,并返回resolve后的内容。
在遇到String类型常量时,resolve的过程如果发现StringTable已经有了内容匹配的java.lang.String的引用,则直接返回这个引用;
如果StringTable里尚未有内容匹配的String实例的引用,则会在Java堆里创建一个对应内容的String对象,然后在StringTable记录下这个引用,并返回这个引用出去。
String +符号的实现
在我们使用中经常会用到+符号来拼接字符串,但是这个+符号在String中的实现还是有讲究的。
如果是相加含有String对象,则底部是使用StringBuilder实现的拼接的
String str1 ="str1";
String str2 ="str2";
String str3 = str1 + str2;
如果相加的参数只有字面量或者常量或基础类型变量,则会直接编译为拼接后的字符串。
String str1 =1+"str2"+"str3";
细节
如果使用字面量拼接的话,java常量池里是不会保存拼接的参数的,而是直接编译成拼接后的字符串保存,我们看看这段代码:
String str1 = new String("aa"+"bb");
//String str3 = "aa";
String str2 = new StringBuilder("a").append("a").toString();
System.out.println(str2==str2.intern());
这段代码的输出是true。
可以得知,在str1变量的创建中,虽然我们用了字面量“aa”,但是我们常量池里并没有aa,所以str2==str.intern()才会返回true。
如果我们去掉str3的注释,重新运行,就会输出false。
String 转换的差异
问题
String.valueOf和Integer.toString的区别
源码
jdk 版本
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)
String 源码
/**
* Returns the string representation of the int argument.
*
* The representation is exactly the one returned by the
* Integer.toString method of one argument.
*
* @param i an int.
* @return a string representation of the int argument.
* @see java.lang.Integer#toString(int, int)
*/
public static String valueOf(int i) {
return Integer.toString(i);
}
/**
* Returns the string representation of the Object argument.
*
* @param obj an Object.
* @return if the argument is null, then a string equal to
* "null"; otherwise, the value of
* obj.toString() is returned.
* @see java.lang.Object#toString()
*/
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
Integer 源码
/**
* Returns a {@code String} object representing this
* {@code Integer}'s value. The value is converted to signed
* decimal representation and returned as a string, exactly as if
* the integer value were given as an argument to the {@link
* java.lang.Integer#toString(int)} method.
*
* @return a string representation of the value of this object in
* base 10.
*/
public String toString() {
return toString(value);
}
/**
* Returns a {@code String} object representing the
* specified integer. The argument is converted to signed decimal
* representation and returned as a string, exactly as if the
* argument and radix 10 were given as arguments to the {@link
* #toString(int, int)} method.
*
* @param i an integer to be converted.
* @return a string representation of the argument in base 10.
*/
public static String toString(int i) {
if (i == Integer.MIN_VALUE)
return "-2147483648";
int size = (i , CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
/**
* Class String is special cased within the Serialization Stream Protocol.
*
* A String instance is written into an ObjectOutputStream according to
*
* Object Serialization Specification, Section 6.2, "Stream Elements"
*/
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
看的出来,使用的是 char 数组维护的内容。
构造器
仔细一看,构造器的类别还是挺多的。
public String() {
this.value = "".value;
}
public String(String original) {
this.value = original.value;
// 计算了哈希
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
public String(char value[], int offset, int count) {
if (offset >>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
public String(int[] codePoints, int offset, int count) {
if (offset >>1.
if (offset > codePoints.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
final int end = offset + count;
// Pass 1: Compute precise size of char[]
int n = count;
for (int i = offset; i 0;) {
value[i] = (char)(ascii[i + offset] & 0xff);
}
} else {
hibyte 0;) {
value[i] = (char)(hibyte | (ascii[i + offset] & 0xff));
}
}
this.value = value;
}
@Deprecated
public String(byte ascii[], int hibyte) {
this(ascii, hibyte, 0, ascii.length);
}
PS: 看了一下,各种构造器,很多种方法。此处不再赘述。
常见方法
charAt
public char charAt(int index) {
if ((index = value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
这个方法,会比直接 value[index] 多进行范围的校验。
所以 leetcode 很多解法,都是建议先把 String 转为 char 数组,然后遍历字符。
equals
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
hashCode
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
小结
很多资料都是可能存在谬误的,包括本文。
但是我们力求准确,抱着质疑的心态去学习,大胆质疑,小心求证。
前段时间看到一句话很喜欢,正是因为质疑的人多了,才有了真理。
但是也不能陷入怀疑主义,这会令人陷入虚无。(老哲学家了~)
参考资料
java基础:String — 字符串常量池与intern(二)