文章目录
1、String的基本特性
- string :字符串,使用一对""引起来表示。
string sl = “atguigu” ;//字面量的定义方式
string s2 = new string ( “hello”);
- string声明为final的,不可被继承
- string实现了serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示string可以比较大小
- String在jdk8及以前内部定义了final char[] value用于存储字符串数据。jdk9时改为byte[]
- String:代表不可变的字符序列。简称:不可变性。
jdk1.8字符串底层时char型数组;
jdk1.9以后是byte型数组;
数组是不能够变长变短的;
字符串在常量池中不可被改变;
public class Test01 {
public static void main(String[] args) {
String s1="123";
String s2="123";
System.out.println(s1==s2);
s2="abc";
System.out.println(s1==s2);
}
}
结果:
6.字符串常量池中是不会存储相同的字符串
String 的String Pool(字符串常量池)是一个固定大小的Hashtable,数组+链表的结构,默认长度时1009.如果放进String Pool的String非常多,就会造成hash冲突严重,导致链表很长,链表长了以后直接会造成的影响就是当调用String.intern()时性能会大幅下降。
使用-xx : StringTablesize可设置stringTable的长度
2、String的内存分配
- 在Java语言中有8种基本数据类型和一种比较特殊的类型string。这些类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。常量池就类似一个Java系统级别提供的缓存。
- 8种基本数据类型的常量池都是系统协调的,string类型的常量池比较特殊。它的主要使用方法
有两种。
- 直接使用双引号声明出来的String对象会直接存储在常量池中。
比如:string info = “atguigu. com” ;- 如果不是用双引号声明的string对象,可以使用string提供的intern()方法。这个后面重点谈
StringTable在jdk1.7以后就放在堆空间中了,主要原因①永久代空间太小②永久代垃圾回收频率低;
3、String的基本操作
public class Memory {
public static void main(String[] args) {
int i=1;
Object obj = new Object();
Memory memory = new Memory();
memory.foo(obj);
}
public void foo(Object param){
String s = param.toString();
System.out.println(s);
}
}
例子中没有重写toString方法,所以调用toString方法返回的是对象的地址,而地址是存在与Java堆中的StringPool(StringTable)
4、字符串拼接操作(重点)
- 常量与常量的拼接结果是在常量池,原理是编译期优化
演示代码:
public class Test02 {
@Test
public void test01(){
String s1="a"+"b"+"c";
String s2="abc";
System.out.println(s1==s2);//true
}
}
原理解析:
常量与常量拼接(final修饰)
@Test
public void test04(){
final String s1="abc";
final String s2="123";
String s3="abc123";
String s4=s1+s2;
System.out.println(s4);
System.out.println(s4==s3);//true
}
如果拼接符号左右两端都是常量或者常量引用,在编译期就会优化
上述果也可以得出,再前端编译时,s4就被优化成了abc123
- 常量池不会存在相同内容的常量
- 只要其中有一个是变量,拼接结果就在堆当中。变量的拼接原理是StringBuilder(线程不安全,但是快)
代码演示:
/**
*
* 如下的s1+s2的执行细节:
* StringBuilder s=new StringBuilder();//线程不安全
* s.append("abc");
* s.append("123");
* s.toString()————>约等于new String("abc123");
*/
@Test
public void test02(){
String s1="abc";
String s2="123";
String s3="abc123";
String s4=s1+s2;
System.out.println(s4==s3);
}
字节码角度看:
-
使用StringBuilder的append()方式拼接与普通拼接方式差距
注释:①在使用StringBuilder拼接时,100000次拼接,只创建了一个StringBuilder。
StringBuilder的底层是创建了长度为16的char型数组,在判断如果char型数组放不下时,会创建一个新的数组,并且把原有的数组内容copy到新数组。
改进空间:在实际开发中如果基本确定前前后前后要添加的字符串长度不高于某个限定值的情况下,建议使用构造器 StringBuilder s=new StringBuilder(capacity) -------------------- capacity:容量②在使用普通拼接操作时,100000次拼接,每一次拼接都创建了StringBuilder对象和String对象;
而且创建了大量对象的同时如果触发GC,GC也会占用时间,影响效率;
public class Test04 {
/**
* ①在使用StringBuilder拼接时,100000次拼接,只创建了一个StringBuilder。
* StringBuilder的底层是创建了长度为16的char型数组,在判断如果char型数组放不下时,
*会创建一个新的数组,并且把原有的数组内容copy到新数组。
* 改进空间:在实际开发中如果基本确定前前后前后要添加的字符串长度不高于某个限定值的情况下,建议使用
*构造器 StringBuilder s=new StringBuilder(capacity) capacity:容量
*
* ②在使用普通拼接操作时,100000次拼接,每一次拼接都创建了StringBuilder对象和String对象;
* 而且创建了大量对象的同时如果触发GC,GC也会占用时间,影响效率;
*/
public static void main(String[] args) {
long start = System.currentTimeMillis();
Test04 test04 = new Test04();
// test04.method1();//26552ms
test04.method2();//8ms
long end = System.currentTimeMillis();
System.out.println(end-start);
}
//普通方法
public void method1(){
String str="";
for (int i = 0; i < 100000; i++) {
str=str+"abc";//每次循环都会创建一个StringBuilder和String
}
}
//使用StringBuilder
public void method2(){
StringBuilder str = new StringBuilder();
for (int i = 0; i < 100000; i++) {
str.append("abc");
}
}
}
- 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中并返回此对象地址。
5、intern()的使用(要点)
- Intern()方法描述:保证你字符串常量池中每个字符串只有一份。
2. 引入一道面试题
题目一:
答案:2个,一个是new String(“ab”),一个实在字符串常量表里
public class StringTest {
public static void main(String[] args) {
String s = new String("ab");
System.out.println(s);//ab
}
}
题目一的字节码指令:
题目二:
public class StringTest {
public static void main(String[] args) {
String s1 = new String("a") + new String("b");
System.out.println(s1);//ab
}
}
题目二的字节码指令:
总共创建了六个对象:
深入剖析:StringBuilder中的toString()方法;
这个toString方法new了一个String(“ab”),但是在字符串常量池中没有生成“ab”;
题目三:(宋红康JVM视频P128)
public class StringIntern {
public static void main(String[] args) {
//1题、
String s = new String("1");//s存储的是new String("1")在堆空间中中的地址;
s.intern();
String s1="1"; //s1存储的是字符串常量表中“1”的地址;
System.out.println(s==s1); //jdk1.6/1.7/1.8 : false
//2题、
String s2 = new String("1") + new String("1");//s2保存的地址是new String("11")的地址;
//上一行代码执行完以后,字符串常量池中不存在“11”!!
s2.intern(); //在字符串常量池中生成“11”;//在jdk1.6 :在永久代的字符串常量表中创建了一个新的对象“11”
//在jdk1.7/1.8 :此时常量池中并没有创建“11”,而是创建了(保存着)在堆空间中new String("11")的地址
String s3="11"; //jdk1.6: s3记录的是上一行代码在常量池中“11”的地址; jdk1.7/1.8: s3保存着的是堆空间中new String("11")的地址;
System.out.println(s2==s3); //jdk1.6:false jdk1.7/1.8:true
}
}
2小题的图解:看不懂就看下底下总结
题目四jdk1.8(题目三第2小题变形)
public class Intern {
public static void main(String[] args) {
String s=new String("1")+new String("1");
String s2="11";
s.intern();
System.out.println(s==s2);//false
}
}
答案:false
题目五jdk1.8
public class Intern {
public static void main(String[] args) {
String s=new String("a")+new String("b");
String s1=s.intern();
System.out.println(s1=="ab");//true:
//为了节省空间在字符串常量池存着的是new String("11")的地址
System.out.println(s=="ab");//true
}
}
public class Intern {
public static void main(String[] args) {
String x="ab";//先把“ab”放进字符串常量池
String s=new String("a")+new String("b");
String s1=s.intern();
System.out.println(s1=="ab");//true
System.out.println(s=="ab");//false
}
}
总结:
总结string的intern()的使用:
- jdkl.6中,将这个字符串对象尝试放入串池(字符串常量池)。
如果串池(字符串常量池)中有,则并不会放入。返回已有的串池(字符串常量池)中的对象的地址。如果没有,会把此对象复制一份,放入串池,并返回串池(字符串常量池)中的对
象地址 - Jdk1.7起,将这个字符串对象尝试放入串池(字符串常量池)。
如果串池中有,则并不会放入。返回已有的串池(字符串常量池)中的对象的地址。如果没有,则会把对象的引用地址复制一份,放入串池(字符串常量池),并返回串池(字符串常量池)中的引用地址
6、intern()的空效率测试:空间角度
/**
* 使用intern降低空间消耗,在使用intern时不用再堆中继续new String对象了,而是会刚开始new的对象的地址;
*/
public class StringIntern {
static final int MAX_COUNT=1000*10000;
static final String[] str=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++) {
//①不用intern()
// str[i]=new String(String.valueOf(data[i%data.length]));
//②用intern()
str[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();
}
}
}
- 不用intern方法
- 使用intern方法
7、StringTable的垃圾回收
测试参数:
-XX:+PrintGCDetails
-XX:+PrintStringTableStatistics(打印StringTable的细节)
-Xms10m堆初始内存
-Xmx10m对最大内存
/**
* -XX:+PrintGCDetails -XX:+PrintStringTableStatistics(打印StringTable的细节) -Xms10m -Xmx10m
*/
public class test05 {
public static void main(String[] args) {
for (int i = 0; i < 5000; i++) {
String.valueOf(i).intern();
}
}
}
8、G1中的String去重操作<了解>
回收堆空间中的重复的String
比如:String s1=new String(“a”);String s2=new String(“a”);可以使s2指向s1所指的“a”。但是当说s1发生了变化比如:s1=“abc”。s2是不能够变,还得是S2=“a”。