String的基本特性
String :字符串,使用一对""引起来表示。
String声明为final的,不可被继承
String实现了serializable接口:表示字符串是支持序列化的,实现了Comparable接口:表示string可以比较大小
String在jdk8及以前内部定义了final char[ ] value用于存储字符串数据。jdk9时改为byte []
为啥要改呢?
来自Oracle官网
The current implementation of the String class stores characters in a chararray, using two bytes (sixteen bits) for each character. Data gathered frommany different applications indicates that strings are a major component ofheap usage and, moreover, that most String objects contain only Latin-1 characters. 字符串类的当前实现将字符存储在字符数组中,每个字符使用两个字节(16位)。从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分,而且大多数字符串对象只包含拉丁文。
Such characters require only one byte of storage, hence half of thespace in the internal char arrays
of such String objects is going unused**.这样的字符只需要一个字节的存储**,因此这类字符串对象的内部char数组中的空间有一半是未使用的。
We propose to change the internal representation of the String class fromUTF-16 char array to a byte array plus an encoding-flag field.The new Stringclass will store characters encoded either as ISO-8859-1/Latin-1 (one byte percharacter), or as UTF-16 (two bytes per character), based upon the contentsof the string. The encoding flag will indicate which encoding is used. 我们建议将字符串类的内部表示形式从UTF-16字符数组更改为字节数组加编码标志字段,新的Stringclass将根据字符串的内容存储编码为ISO-8859-1/拉丁文-1(每个字符一个字节)或UTF-16(每个字符两个字节)的字符。编码标志将指示所使用的编码。
String:代表不可变的字符序列。简称:不可变性。
>当对字符串重新赋值时,需要重写指定内存区域赋值,不能使用原有的value进行赋值。
>当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
>当调用String的replace ()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。
代码1:
public class stringTest1 {
@Test
public void test1() {
String s1 = "abc";
string s2 = "abc";
//s1 = "hello" ;
System.out.println(s1 == s2);//true 都指向常量池的同一个地址
System.out.println(s1);//abc
System.out.println(s2);//abc
}
@Test
public void test2() {
String s1 = "abc";
String s2 = "abc";
s2+="def";
System.out.println(s2);//abcdef
System.out.println(s1);//abc
//可以看出这对s1并没有啥影响
}
@Test
public void test3() {
String s1 = "abc";
String s2 = s1.replace(oldChar: 'a',newChar: 'm' );system.out.println(s1);//
System.out.println(s2);//
}
}
代码2:
public class StringExer {
String str = new string( original: "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.st,ex.ch);
System.out.println(ex.str);//good java都是值传递,修改了内容只是修改了副本
System.out.println(ex.ch);//best
}
}
字符串常量池中是不会存储相同内容的字符串的。
String的String Pool(常量池)是一个固定大小的Hashtable(数组加链表),默认值大小长度是1009。如果放进string Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降。
使用-XX :StringTableSize可设置stringTable的长度
在jdk6中stringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTablesize设置没有要求
在jdk7中,StringTable的长度默认值是60013
在jdk8中改为1009是可设置的最小值。
String的内存分配
在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。
常量池就类似一个Java系统级别提供的缓存。8种基本数据类型的常量池都是系统协调的,String类型的常量池比较特殊。它的主要使用方法有两种。
直接使用双引号声明出来的String对象会直接存储在常量池中。比如:String info = “atguigu.com” ;
如果不是用双引号声明的String对象,可以使用String提供的intern()方法。
Java 6及以前,字符串常量池存放在永久代。
Java 7 中oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内。
所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7 中使用string.intern ( ) 。
Java8元空间,字符串常量在堆。
String的拼接
1.常量与常量的拼接结果在常量池,原理是编译期优化
2.常量池中不会存在相司内容的常量。
3.只要其中有一个是变量,结果就在堆中。变量拼接的原理是stringBuilder
4.如果拼接的结果调用intern ()方法,则主动将常量池中还没有的字符串对象入池中,并返回此对象地址。
public class Test{
@Test
public void test1(){
String s1 = "a" + "b" + "c";//
String s2 = "abc ";
/*
* 最终.java编译成.cLass,再执行.cLass
* string s1 = "abc";
* string s2 = "abc"
*/
System.out.println(s1 == s2);//true 都指向常量池
System.out.println(s1.equalE(s2)); //true
}
public void test2(){
String s1 = "javaEE";
String s2 = "hadoop" ;
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop" ;//编译期优化
//如果拼接的前后出现了变量,则相当于在堆空间new String(),具体内容为拼接的结果
String s5 = s1 + "hadoop" ;
String s6 = "javaEE" +s2;
String s7 = s1 + s2;
System.out.println(s3 ==s4);//true
System.out.println(s3 == s5);//false
System.out.println(s3 -= s6);//false
System.out.println(s3 == s7);//false
System.out.println(s5 == s6);//false
System.out.println(s5 ==s7);//false
System.out.println(s6==s7);//false
//判断常量池中是否存在与s6相等的值,如果存在则返回常量池中s6常量值的位置
//若不存在,则在string常量池中加载一份与s6值相同的,并返回地址
String s8 = s6.intern();
System.out.println(s3 ==s8);//true
}
}
上面为什么会出现false呢,底层是什么?
先举一个代码例子
public class Test{
@Test
public void test(){
String a = "a";
String b = "b";
String c = a + b;
String d = "ab";
System.out.println(d == c);
}
}
如下的s1 + s2的执行细节:
①StringBuilder s = new StringBuilder();
②s.append(“a”)
③s.append(“b”)
④s.tostring()–>类似于new string( “ab”)
public class Test{
@Test
public void test4(){
final String s1 = "a;
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);//true
}
}
s1加上final后已经被认为是常量了,就不会调用上面的StringBuilder在堆空间new一个String,仍然使用编译期优化,直接在常量池找
拼接操作和appen操作的对比
method1用了4000,method2只用了7
可知效率差距之大,其主要原因是因为在method1中,每经过一次循环都会创建一个StringBuilder调用append,然后再toString出来,效率自然很低下
intern方法
如果不是用双引号声明的string对象,可以使用string提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
·比如:string myInfo = new string (“I love atguigu”).Intern() ;
也就是说,如果在任意字符串上调用string.intern方法,那么其返回结果所指向的那个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true:
( “a” + “b” + “c” ) .intern ( ) == “abc”
通俗点讲,Interned string就是确保字符串在内存里只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意,这个值会被存放在字符串内部池
面试题
揭开下面的代码的答案之前先来拥有一些基本的知识储备
题目1:
会有两个对象,首先是new出来的String,第二个是String构造器中的"ab",会在常量池中创建一个相同的对象ab。
扩展:
new string( “a”) + new string( “b”)呢?
对象1: new stringBuilder()
对象2:new string(“a”)
对象3:常量池中的"a"
对象4:new string( “b”)
对象5:常量池中的"b"
深入剖析:
stringBuiLder的tostring() :
对象6:new String(“ab”);
强调一下toString的调用,在常量池是不会有ab的
public class StringIntern1 {
public static void main(String[ ] args) {
String s = new String( "1");
s.intern();//调用此方法之前字符串常量池已经有1了
String s2 = "1";
System.out.println(s == s2);//jdk6 : false jdk7 : false
String s3 = new String("1") + new String("1");
s3.intern();//在字符串常量区中生成11
String s4 = "11";
System.out.println(s3 == s4);//jdk6 : false jdk7 : true
}
对于第一个情况,即s == s2 //false的情况,在String s = new String(“1”);这行代码常量池已经存在一个1的对象了,调用s.intern()并没有什么用,因为1的对象地址仍然是常量池中自己的地址
但是对于第二个情况呢, String s3 = new String(“1”) + new String(“1”);此时就算用StringBuilder中的append然后再是toString的自动newString(“11”),常量池中始终不存在11,所以调用s3的intern方法,常量池中的地址就会自动引用s3的地址,让11和s3的地址一样了
intern的效率测试
public class StringIntern2 {
static final int MAX_COUNT = 1000*10000;
static final String[] arr = new String[MAX_COUNT];
public static void main(String[] args) {
Integer[] data = new Integer[]{1,2,3,4,5,6,7,8,9,10};long start = System.currentTimeMiLlis();
for (int i = 0; i <MAX_COUNT; i++){
arr[i] = new String(String. value0f ( data[i % data.Length]));
arr[i] = new String(String.valueof(data[i % data.length] ) ).intern();
}
long end = System.currentTimeMiLlis();
System.out.println("花费的时间为:" +(end - start));
try {
Thread.sleep(1000000);
} catch (InterruptedException e){
e.printStackTrace();
System.gc();
}
}
}
当用了intern方法之后,内存占用明显减少了。对于字符串大量存在的重复字符串,使用intern方法可以节省内存空间
G1的String去重操作
背景:对许多Java应用(有大的也有小的)做的测试得出以下结果:
>堆存活数据集合里面string对象占了25%
≥堆存活数据集合里面重复的string对象有13.5%
>string对象的平均长度是45
许多大规模的Java应用的瓶颈在于内存,测试表明,在这些类型的应用,Java堆中存活的数据集合差不多25%是string对象。更进一步,这里面差不多一半string对象是重复的,重复的意思是说:stringl.equals (string2) =true。堆上存在重复的string对象必然是一种内存的浪费。这个项目将在G1垃圾收集器中实现自动持续对重复的string对象进行去重,这样就能避免浪费内存。