一、String的基本特性
1、String的基本特性
- String:字符串,使用一对 "" 引起来表示
String s1 = "lori" ; // 字面量的定义方式
String s2 = new String("lori"); // new 对象的方式
- String声明为final的,不可被继承 String实现了Serializable接口:表示字符串是支持序列化的,可以跨进程传输字符串。实现了Comparable接口:表示String可以比较大小、排序
- string在jdk8及以前内部定义了final char[] value用于存储字符串数据。JDK9时改为byte[]
2、为什么JDK9改变了结构
- String类的当前实现将字符存储在char数组中,每个字符使用两个字节(16位)。从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分,而且,大多数字符串对象只包含拉丁字符。这些字符只需要一个字节的存储空间,因此这些字符串对象的内部char数组中有一半的空间将不会使用。
- 我们建议改变字符串的内部表示clasš从utf - 16字符数组到字节数组+一个encoding-flag字段。新的String类将根据字符串的内容存储编码为ISO-8859-1/Latin-1(每个字符一个字节)或UTF-16(每个字符两个字节)的字符。编码标志将指示使用哪种编码。 结论:String再也不用char[] 来存储了,改成了byte [] 加上编码标记,节约了一些空间
// 之前
private final char value[];
// 之后
private final byte[] value
同时基于String的数据结构,例如StringBuffer和StringBuilder也同样做了修改
3、String的不可变性
String:代表不可变的字符序列。简称:不可变性。
- 当对字符串变量重新赋值时,会直接新建一个字符串(或池中本来就有的),不会影响本来的字符串
- 当对现有的字符串进行连接拼接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
- 当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。 代码:
/**
* String的不可变性
*
* @author: 陌溪
* @create: 2020-07-11-8:57
*/
public class StringTest1 {
public static void test1() {
// 字面量定义的方式,“abc”存储在字符串常量池中
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);
s1 = "hello";
System.out.println(s1 == s2);
System.out.println(s1);
System.out.println(s2);
System.out.println("----------------");
}
public static void test2() {
String s1 = "abc";
String s2 = "abc";
// 只要进行了修改,就会重新创建一个对象,这就是不可变性
s2 += "def";
System.out.println(s1);
System.out.println(s2);
System.out.println("----------------");
}
public static void test3() {
String s1 = "abc";
String s2 = s1.replace('a', 'm');
System.out.println(s1);
System.out.println(s2);
}
public static void main(String[] args) {
test1();
test2();
test3();
}
}
运行结果:
true
false
hello
abc
----------------
abc
abcdef
----------------
abc
mbc
4、面试题
/**
* 面试题
*
* @author: 陌溪
* @create: 2020-07-11-9:05
*/
public class StringExer {
String str = new String("good");
char [] ch = {'t','e','s','t'};
public void change(String str, char ch []) {
str = "test ok";
ch[0] = 'b';
}
public static void main(String[] args) {
StringExer ex = new StringExer();
ex.change(ex.str, ex.ch);
System.out.println(ex.str);
System.out.println(ex.ch);
}
}
输出结果:
good
best
字符串常量池是不会存储相同内容的字符串的
- String的string Pool是一个固定大小的Hashtable,默认值大小长度是1009。如果放进string Pool的string非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用string.intern时性能会大幅下降。
- 使用-XX:StringTablesize可设置stringTab1e的长度
- 在jdk6中stringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。stringTablesize设置没有要求
- 在jdk7中,stringTable的长度默认值是60013,
- 在JDK8中,StringTable可以设置的最小值为1009
二、String的内存分配
在Java语言中有8种基本数据类型
和一种比较特殊的非常常用的类型String
。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池
的概念。
常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种:
- 直接使用双引号声明出来的String对象会直接存储在常量池中。比如:String info="lori.com";
- 如果不是用双引号声明的String对象(new出来的,或者其他方法返回的),可以使用String提供的intern()方法。
实验
1、基于JDK6
使用JDK 6用 -XX:PermSize和 -XX: MaxPermSize限制永久代的大小。
//-XX:PermSize=5M -XX:MaxPermSize=5M
public class MethodDemo {
public static void main(String[] args) {
// 使用Set保持着常量池引用,避免Full GC回收常量池行为
Set<String> set = new HashSet<String>();
int i = 0;
while (true) {
System.out.println(i);
set.add(String.valueOf(i++).intern());
}
}
}
如果使用JDK 7或更高版本的JDK来运行实例一,结果是不会重现JDK 6中的溢出异常,程序会执行下去
2、基于JDK7
使用JDK 7或更高版本的JDK用 -XX:MetaspaceSize=N -XX:MaxMetaspaceSize=N参数限制元空间容量执行上面代码。
//-XX:MetaspaceSize=5M -XX:MaxMetaspaceSize=5M
public class MethodDemo {
public static void main(String[] args) {
// 使用Set保持着常量池引用,避免Full GC回收常量池行为
Set<String> set = new HashSet<String>();
int i = 0;
while (true) {
System.out.println(i);
set.add(String.valueOf(i++).intern());
}
}
}
由于字符串常量池放在了Java堆里,程序会执行下去。
3、JDK7+
使用JDK 7或更高版本的JDK用-Xms -Xmx参数限制Java堆容量执行上面代码。
//-Xms5M -Xmx5M -XX:-UseGCOverheadLimit
public class MethodDemo {
public static void main(String[] args) {
// 使用Set保持着常量池引用,避免Full GC回收常量池行为
Set<String> set = new HashSet<String>();
int i = 0;
while (true) {
System.out.println(i);
set.add(String.valueOf(i++).intern());
}
}
}
4、为什么StringTable从永久代调整到堆中
- 永久代的默认比较小
- 永久代垃圾回收频率低
- 堆中空间足够大,字符串可被及时回收
三、String的基本操作
Java语言规范里要求完全相同的字符串字面量,应该包含同样的Unicode字符序列(包含同一份码点序列的常量),并且必须是指向同一个String类实例。 代码示例1:验证常量池里不允许有相同的字符串
public class StringTest1 {
public static void main(String[] args) {
System.out.println();//2330
System.out.println("1");//2331 个字符串
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("10");//2340 个字符串
//如下的字符串"1" 到 "10"不会再次加载
System.out.println("1");//2341
System.out.println("2");//2341
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("10");//2341
}
}
代码示例2:
class Memory {
public static void main(String[] args) {//line 1
int i = 1;//line 2
Object obj = new Object();//line 3
Memory mem = new Memory();//line 4
mem.foo(obj);//line 5
}//line 9
private void foo(Object param) {//line 6
String str = param.toString();//line 7
System.out.println(str);
}//line 8
}
显示的调用toString()方法在字符串常量池当中生成调用者对应的字符串,并且返回常量池当中的地址