14.关于String
14.1 常量池的概念
由于String类型描述的字符串内容是常量不可改变,因此Java虚拟机将首次出现的字符串放入常量池中,若后续代码中出现了相同字符串内容则直接使用池中已有的字符串对象而无需申请内存及创建对象,从而提高了性能。
划重点,两种声明方式并不一样!!!
String s = new String("nuc"); !!!!!!!
String x = "nuc";
System.out.println(s==x); //false
System.out.println(s.equals(x)); //true
14.2 String创建图解
当使用直接赋值法产生一个字符串对象时候,JVM会维护一个字符串常量池,若该对象在堆中还不存在,则产生一个新的字符串对象加入字符串的常量池中。
当继续使用直接赋值法产生字符串对象时,JVM发现该引用指向的内容在常量池中已经存在了,则此时不再新建字符串对象,而是复用已有对象。
String str1 = new String("hello");
String str2 = "hello";
System.out.println(str1 == str2);
//输出结果:false
因为str1使用new去产生一个对象,此时str1会先在堆中开辟一块空间,产生一个对象,此时hello这个字符串对象是第一次产生,所以会将hello这个对象放入到常量池中。
随后str2通过赋值法产生字符串对象,但是此时在常量池中已经存在hello对象,所以str2直接指向常量池中的对象。
然后调用intern(),并返回给str1
str1 = str1.intern();
先将str1产生的对象放入常量池中,因为inter方法会返回常量池中的对应的对象,所以此时str1引用会指向常量池中的对象。
注意区分new创建、直接赋值法与常量池之间的关系!!!
14.3 String底层相关
String是字符串常量的引用,String += String的本质是new了新的临时对象StringBuild,然后经过其链式调用append(),拼接后再StringBuild.toString()赋给原String。所有大量字符串拼接不要直接使用String,否则会生成大量临时对象,严重影响性能。
-
String类被final修饰
-
Java中所有的字符串文字,都实现为String类的实例
-
底层结构
JDK8及之前,其底层是 private final char[ ] value;
JDK11及之后,其底层是 private final byte[ ] value;
String有关功能代码实现 :
public static void main(String[] args) throws UnsupportedEncodingException {
// utf-8 一个汉字占三个字节
// GBK 一个汉字站两个字节
String str = "太行山下";
byte[] bytes = str.getBytes();
System.out.println(Arrays.toString(bytes));
byte[] gbks = str.getBytes("utf-8");//可以指定用于转换的编码格式
System.out.println(Arrays.toString(gbks));
System.out.println(str.charAt(2));
String str1 = "abckjamkjaoklacdf";
//按指定字符拆分
System.out.println(Arrays.toString(str1.split("a")));//[, bckj, mkj, okl, cdf]
//拆分为几个
System.out.println(Arrays.toString(str1.split("a",2)));//[, bckjamkjaoklacdf]
String str2 = " abc ";
//原 去空格 前后 (只能操作半角)
System.out.println(str2.trim());//abc
//JDK11新增 去空格 (全角+半角)
//去前后
System.out.println(str2.strip()); //abc
//去前
System.out.println(str2.stripLeading()); //abc
//去后
System.out.println(str2.stripTrailing());// abc
//返回子字符串(截取)
System.out.println(str2.substring(1)); // abc
System.out.println(str2.substring(2,4)); //ab
//将各基本类型数据转为字符串返回
boolean b = true;
String s = String.valueOf(b);
System.out.println(s);//true
}
重点!关于String的面试题 :
//第一题
String s1 = "abc";
String s2 = "xyz";
String s3 = s1 + s2;
String s4 = "abc" + "xyz";
String s5 = "zbcxyz";
System.out.println(s3 == s4); // false
System.out.println(s4 == s5); // true
s1、s2都在常量池中,而s3=s1+s2,s3不会放在常量池中(只有字面常量才会放在常量池中),所以s3放在堆中,s4是两个字面常量拼接,所以会放在常量池中,内容为“abcxyz”,s5创建时,现在常量池中找到了相同的,所以两者引用相同
所以s3和s4的引用不同,s4和s5的引用相同
//第二种
String s1 = "abc";
String s2 = "xyz";
String s3 = s1 + s2;
String s4 = s3.intern();
String s5 = "abcxyz";
System.out.println(s3 == s4); // true
System.out.println(s4 == s5); // true
s3存放在堆中,而创建s4时发现常量池中没有,则把s3对象复制一份放入常量池中(此时常量池中存放s3在堆中创建的“abcxyz”的地址),然后返回常量池中的对象,所以s3和s4指向同一个地址引用,结果为true
当创建s5时,发现常量池中有“abcxyz”地址,所以s5指向常量池中的地址,然后此地址再指向堆中“abcxyz”,所以s4和s5都指向常量池中的地址,结果为true
使用intern方法如果常量池中没有,则把对象复制一份(或对象引用)放入常量池中, 返回常量池中的对象,如果常量池中存在,则直接返回。
JDK1.7之前是复制一份放入常量池,JDK1.7之后则把对象引用到常量池。
intern方法:
intern() 方法返回字符串对象的规范化表示形式。
它遵循以下规则:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。
String str1="aaa";
String str2=new String("aaa");
System.out.println(str1==str2);//false
System.out.println(str1==str2.intern());//true
15.StringBuilder和StringBuffer个人总结
15.1关于底层核心机制
15.1.1 关于线程安全( 同步性 )
-
StringBuffer和StringBuilderd的append的方法是在AbstractStringBuilder中实现的,
-
StringBuffer的append方法加了synchronized。
-
StringBuffer是线程安全的但效率低,StringBuilder是线程不安全的但高效
-
其底层是数组 JDK8/11 字符数组/字节数组
-
其长度、内容可变原因 : 底层数组未被final修饰
另外注意区分,String底层数组被private修饰,而可变字符底层数组是被default默认修饰
-
两者都继承自AbstractStringBuilder,拥有共同父类
15.1.2 两者拥有相同的扩容机制 :
无参构造初始容量为:16
有参构造初始容量为:字符串参数的长度+16
有参和无参扩容方法都一样的。都是从当前容量开始扩容
15.1.3扩容的两种情况
一次追加长度超过当前容量,则会按照 当前容量*2+2 扩容一次
一次追加长度不仅超过初始容量,而且按照 当前容量*2+2 扩容一次也不够,其容量会直接扩容到与所添加的字符串长度相等的长度。之后再追加的话,还会按照 当前容量*2+2进行扩容
//简述扩容机制
//默认初始容量为16
StringBuilder sb = new StringBuilder();
System.out.println(sb.length()); //0
System.out.println(sb.capacity()); //16
//以字符串创建时,初始容量为 string.length()+16
StringBuilder sb1 = new StringBuilder("nuc.edu");
System.out.println(sb1.length()); //7
System.out.println(sb1.capacity()); //7+16
//追加或添加元素到等于容量时,不会提前扩容
sb1.append("1234567891234567");
System.out.println(sb1);
System.out.println(sb1.length()); //23,容量满,但没超过,所以不执行扩容机制
//当追加元素超过当前容量时,扩容机制为原容量左移两位+2,即原容量*2 +2
sb1.append("a");
System.out.println(sb1.capacity()); //23*2+2
//当扩容后,还放不下,就会将正好能存放当前元素的容量,当再进行添加时,就会执行原扩容机制
System.out.println(sb.capacity()); //16
sb.append("12345678912345678912345678912345679"); //添加35位元素
System.out.println(sb.capacity());
15.2 强调注意
!!!再者要注意,好多方法中按指定范围截取的子序列,范围基本都是取左不取右
两者兼容API
执行效率 : StringBuilder > StringBuffer > String
StringBuffer 出现在 JDK1.0 , StringBuilder 出现在 JDK1.5
转化实现
//相互转化
StringBuilder builder1 = new StringBuilder();
StringBuffer buffer2 = new StringBuffer();
//1. StringBuilder/StringBuffer --> String
//有两种方式, 使用toString() 与 构造器
String s1 = builder1.toString();
String s2 = buffer2.toString();
String s3 = new String(buffer2);
String s4 = new String(builder1);
//2. String --> StringBuilder/StringBuffer
String s = "nuc.edu";
//构造器实现
StringBuilder builder = new StringBuilder(s);
StringBuffer buffer = new StringBuffer(s);
15.3 其方法的简单调用
实现代码如下 :
public class StringBuilderTest {
public static void main(String[] args) {
//创建
StringBuilder builder = new StringBuilder();
//获得序列长度
int i = builder.length();
//返回当前容量
System.out.println(builder.capacity());//默认初始长度为16
//追加元素到末尾 append()
builder.append("nuc.");
builder.append("edu.");
System.out.println(builder); //nuc.edu.
//移除此序列的子字符串中的字符 (左闭右开)!!!
builder.delete(0,builder.length());
System.out.println(builder); //
//链式增加元素
builder.append("nuc").append(".").append("edu.");
System.out.println(builder); //nuc.edu.
//将给定的代码点参数以字符串形式附加到此延续的内容中
builder.appendCodePoint(99);
builder.appendCodePoint(110);
System.out.println(builder); //nuc.edu.cn
// setCharAt(int index, char ch) --> 将给定索引处的字符设置为 ch
builder.setCharAt(7,'!'); //注意是字符!不是所谓的字符串!!!
System.out.println(builder); //nuc.edu!cn
//insert(int offset, String str) 将 str 参数的字符串插入此序列中
builder.insert(4,"north.");
System.out.println(builder); //nuc.north.edu!cn
//replace(int start, int end, String str) 使用给定 String 中的字符替换此序列的子字符串中的字符 (左闭右开!!!)
builder.replace(0,3,"NUC"); //实际替换的是[0,2]
System.out.println(builder); //NUC.north.edu!cn
// 返回一个新的 String
// String substring(int start)
// String substring(int start, int end)
String substring1 = builder.substring(4);
System.out.println(substring1); //north.edu!cn
String substring2 = builder.substring(4, 9);//!!!左闭右开
System.out.println(substring2); //north 实际取[4,8]
}
}