深入了解Java字符串
一:先简单说下有关类加载
在进入正文之前,我先讲一下Java的一个特性优势,那就是Java的类是动态加载的,无论是反射还是new出来的对象,都是需要通过类加载器进行按需动态加载的,这个类加载是通过一种机制——双亲委派机制(先交给上层的类加载器进行载入,如果可以的话返回加载结果,否则再自己进行尝试加载)。部分源码如下:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
@Override
protected Class<?> loadClass(String cn, boolean resolve)
throws ClassNotFoundException
{
// for compatibility reasons, say where restricted package list has
// been updated to list API packages in the unnamed module.
SecurityManager sm = System.getSecurityManager();//系统安全机制(策略)管理
if (sm != null) {
int i = cn.lastIndexOf('.');
if (i != -1) {
sm.checkPackageAccess(cn.substring(0, i));
}
}
return super.loadClass(cn, resolve);//交给上层类加载器进行加载
}
public static SecurityManager getSecurityManager() {
if (allowSecurityManager()) {
return security;
} else {
return null;
}
}
//判断是否是允许安全管理的
private static boolean allowSecurityManager() {
return (allowSecurityManager != NEVER);
}
二:正文
字符串是程序中最常用到的数据类型之一(可以说是非常重要的一种数据类型),Java中可以通过String、StringBuilder、StringBuffer这三种创建字符串的类对象来进行字符串的创建,它们各有各的优缺点。
2.1 字符串拼接程序比较String、StringBuffer、StringBuilder的拼接性能
最开始我们先通过一段程序来了解这三者在拼接字符串方面的效率:
public class Test1 {
public static void main(String[] args) {
createString();
createStringBuffer();
createStringBuilder();
}
public static void createString(){
long start= System.currentTimeMillis();
String string="0";
for(int i=1;i<100000;i++){
string+=i;
}
long end=System.currentTimeMillis();
System.out.println("String append 100000 data cost:"+(end-start)+"millis");
}
public static void createStringBuffer(){
long start= System.currentTimeMillis();
StringBuffer stringBuffer=new StringBuffer("0");
for(int i=1;i<10000000;i++){
stringBuffer.append("a"+i);
}
long end=System.currentTimeMillis();
System.out.println("StringBuffer append 10000000 data cost:"+(end-start)+"millis");
}
public static void createStringBuilder(){
long start= System.currentTimeMillis();
StringBuilder stringBuilder=new StringBuilder("0");
for(int i=1;i<10000000;i++){
stringBuilder.append("b"+i);
}
long end=System.currentTimeMillis();
System.out.println("StringBuilder append 10000000 data cost:"+(end-start)+"millis");
}
}
代码运行结果如下:
显而易见,三者在拼接字符串方面的效率:StringBuilder>StringBuffer>String
接下来我们来通过源码增进对着三者的认识。
2.2 关于String(结构、源码)
我们先来介绍下String相关内容:
2.2.1 String的结构
首先我们来看下String相关的UML图:
我们现来看下String继承了哪些接口:
简单的对这五个接口进行阐述:
- 接口CharSequence:是java.lang包下的一个接口,此接口对多种不同的对char访问的统一接口,向String、StringBuffer、StringBuilder都是CharSequence的实现类。CharSequence类和String类都可以定义字符串,但是String定义的字符串只能读,CharSequence定义的字符串是可读可写的;对于抽象类或者接口来说不可以直接使用new的方式创建对象,但是可以直接给它赋值:
CharSequence test = "test";
- 接口ConstantDesc和接口Constable一起讲:
JDK12在java.base模块新增了java.lang.constant包,包中定义了一系列基于值的符号引用类型,它们能够描述每种加载常量。
Constable表示一个类型是Constable。Constable类型是可以在JVMS4.4中描述的Java类文件的常量池中标识的常量,并且其实例可以名义上将自己的描述为ConstantDesc。一些常量类型在常量池中具有本地表示形式:String、Integer、Long、Float、Double、Class、MethodType、MethodHandle。类型String、Integer、Long、Float和Double充当其自己的名义描述符;Class、MethodType和MethodHandle具有对应的名义描述符ClassDesc,MethodTypeDesc和MethodHandlerDesc。
ConstanrDesc:在JVMS4.4中定义的可加载常量值的名义描述符。名义描述符中的类名称(如类文件的常量池中的类别名称)必须相对特定的类加载器进行解释,该类加载器不是名义描述符的一部分。 - 接口Comparable:实现compareTo方法进行字符串比较
- 接口Serializable:序列化标识,表示该字符串可以进行序列化和反序列化;
String类有哪些Field:(下文源码处有做简述)
String的构造器有这些:
可见,字符串String跟字节数组byte[]、字符数组char[]是密切相关的。
2.2.2 String的部分Field及Constructor
String部分源码:
//注意:String被关键字final修饰
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
@Native static final byte LATIN1 = 0;//是ISO-8859-1的别名
@Native static final byte UTF16 = 1;
@Stable
private final byte[] value;//传入的字符串值,被final修饰
private final byte coder;//用于对字节进行编码的标识符,被final修饰
private int hash; // Default to 0,字符串的缓存hash
private static final long serialVersionUID = -6849794470754667710L;//序列化ID
static final boolean COMPACT_STRINGS;//是否压缩字符串,如果字符串压缩被禁用,则字节总是用UTF16编码,对于具有多个可能实现路径的方法,当压缩被禁用,只采用一个代码路径。
//压缩字符串==true=>if(coder==LATIN1){。。。}
//压缩字符串==false=>if(false){。。。}
/*
这个字段的实际值由JVM注入。静电
初始化块用于在此处设置值以进行通信
这个静态的final字段不是静态可折叠的
在vm初始化期间避免任何可能的循环依赖
*/
static {
COMPACT_STRINGS = true;
}
//ObjectStreamField是Serializable类的Seriablizable字段的描述,ObjectStreamFields的数组用于声明一个类的Serializable字段
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null)
throw new NullPointerException("charsetName");
checkBoundsOffCount(offset, length, bytes.length);//检查长度是否在bytes[]数组的范围内
StringCoding.Result ret =
StringCoding.decode(charsetName, bytes, offset, length);//使用对应的编码方式进行解码,若编码方式为null,则用默认的ISO-8859-1
this.value = ret.value;
this.coder = ret.coder;
}
public String(StringBuffer buffer) {
this(buffer.toString());//后面讲到StringBuffer会补充
}
public String(char value[], int offset, int count) {
this(value, offset, count, rangeCheck(value, offset, count));
}
public String(StringBuilder builder) {
this(builder, null);
}
@HotSpotIntrinsicCandidate //被该注解标注的,在HotSpot中都有一套高效地实现,该高效实现基于CPU指令,运行时,HotSpot维护的高效实现会替代JDK的源码实现,从而获得更高效的效率
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
}
2.2.3 通过一段程序来阅读关于String的常用方法的源代码
下面通过一段程序来阅读查看String中的常用方法的源码:
public class Test1 {
public static void main(String[] args) throws Exception {
//注意,在ASCII编码中一个中文字符占俩字节
String s1 = "沈烁华";
String s2 = "沈烁华";
String s3 = "沈烁华" + "聊编程" + "沈烁华";
String s4 = "聊编程" + new String("的小华");
String s5 = "I love programming 打住";
char[] c = new char[3];
s3.getChars(0, 3, c, 0);
for (char c1 : c) {
System.out.print(c1 + ",");//沈,烁,华
}
System.out.println();
byte[] b = s1.getBytes("GBK");
for (byte t : b) {
System.out.print(t + ",");//-55,-14,-53,-72,-69,-86
}
System.out.println();
System.out.println(s1.length());//3
System.out.println(s1.charAt(0));//沈
System.out.println(s1.equals(s2));//true
System.out.println(s1.contentEquals(new StringBuilder("沈烁华")));//true
System.out.println(s1.contentEquals(new StringBuffer("沈烁华")));//true
System.out.println(s3.hashCode());
System.out.println(s2.indexOf("华"));//2,因为是从前开始找
System.out.println(s3.lastIndexOf("烁"));//7,因为是从后开始找
for (String s : s5.split(" ", 3)) {
System.out.print(s + ",");//I,love,programming 打住, 3是限制数组的长度,所以后面的programming 打住没有按空格分隔开
}
System.out.println();
System.out.println(String.valueOf(new char[]{
'a', 'b'}, 0, 1));
System.out.println(s3.toCharArray());
s1.intern();
}
}
方法源码如下:
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
//检查是都越界问题
checkBoundsBeginEnd(srcBegin, srcEnd, length());
checkBoundsOffCount(dstBegin, srcEnd - srcBegin, dst.length);
if (isLatin1()) {
//是否是IOS编码
StringLatin1.getChars(value, srcBegin, srcEnd, dst, dstBegin);
} else {
StringUTF16.getChars(value, srcBegin, srcEnd, dst, dstBegin);
}
}
//这个是StringLatin1类中的
public static void getChars(byte[] value, int srcBegin, int srcEnd, char dst[], int dstBegin) {
inflate(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
// inflatedCopy byte[] -> char[]
@HotSpotIntrinsicCandidate
public static void inflate(byte[] src, int srcOff, char[] dst, int dstOff, int len) {
for (int i = 0; i < len; i++) {
dst[dstOff++] = (char)(src[srcOff++] & 0xff);//遍历复制,注意这里进行了与运算,0xff表示一个十六进制数FF,也就是十进制的255(1111 1111)
}
}
/
public byte[] getBytes(String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null) throw new NullPointerException();
return StringCoding.encode(charsetName, coder(), value);//调用StringCode按照指定字符集进行比那吗
}
public int length() {
return value.length >> coder();//右移多少位
}
byte coder() {
return COMPACT_STRINGS ? coder : UTF16;//UTF16的值是1
}
///
public char charAt(int index) {
if (isLatin1()) {
return StringLatin1.charAt(value, index);
} else {
return StringUTF16.charAt(value, index);
}
}
//这是StringLatin1类(IOS编码)中的charAt
public static char charAt(byte[] value, int index) {
if (index < 0 || index >= value.length) {
throw new StringIndexOutOfBoundsException(index);
}
return (char)(value[index] & 0xff);
}
//
public boolean equals(Object anObject) {
if (this == anObject) {
//判断是否是自己
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
//判断编码是否一致
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
//通过遍历来查看是否相同
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) {
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}
public boolean contentEquals(CharSequence cs) {
// Argument is a StringBuffer, StringBuilder
if (cs instanceof AbstractStringBuilder) {
if (cs instanceof StringBuffer) {
synchronized(cs) {
//注意,这里加了悲观锁
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
} else {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
}
// Argument is a String
if (cs instanceof String) {
return equals(cs);
}
// Argument is a generic CharSequence
int n = cs.length();
if (n != length()) {
return false;
}
byte[] val = this.value;
if (isLatin1()) {
for (int i = 0; i < n; i++) {
if ((val[i] & 0xff) != cs.charAt(i)) {
return false;
}
}
} else {
if (!StringUTF16.contentEquals(val, cs, n)) {
return false;
}
}
return true;
}
private boolean nonSyncContentEquals(AbstractStringBuilder sb) {
int len = length();
if (len != sb.length()) {
return false;
}
byte v1[] = value;
byte v2[] = sb.getValue();
if (coder() == sb.getCoder()) {
int n = v1.length;
for (int i = 0; i < n; i++) {
if (v1[i] != v2[i]) {
return false;
}
}
} else {
if (!isLatin1()) {
// utf16 str and latin1 abs can never be "equal"
return false;
}
return StringUTF16.contentEquals(v1, v2, len);
}
return true;
}
/
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
hash = h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
}
return h;
}
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
hash = h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
}
return h;
}
//StringUTF16的hashCode
public static int hashCode(byte[] value) {
int h = 0;
int length = value.length >> 1;//长度折半
for (int i = 0; i < length; i++) {
h = 31 * h + getChar(value, i);//哈希公式计算出唯一hash值,避免哈希冲突
}
return h;
}
//
public int indexOf(String str) {
if (coder() == str.coder()) {
return isLatin1() ? StringLatin1.indexOf(value, str.value)
: StringUTF16.indexOf(value, str.value);
}
if (coder() == LATIN1) {
// str.coder == UTF16,编码不一致
return -1;
}
return StringUTF16.indexOfLatin1(value, str.value);
}
public static int indexOfLatin1Unsafe(byte[] src, int srcCount, byte[] tgt, int tgtCount, int fromIndex) {
//这里补充讲一下Java中的断言assert关键字:assert主要用来保证程序的正确性,通常在程序开发和测试时使用,为了提高程序运行的效率,在软件发布后,断言是默认关闭的
assert fromIndex >= 0;
assert tgtCount > 0;
assert tgtCount <= tgt.length;
assert srcCount >= tgtCount;
char first = (char)(tgt[0] & 0xff);//字节转为字符
int max = (srcCount - tgtCount);
//从指定起点开始往后找
for (int i = fromIndex; i <= max; i++) {
// Look for first character.
if (getChar(src, i) != first) {
while (++i <= max && getChar(src, i) != first);
}
// Found first character, now look at the rest of v2
if (i <= max) {
int j = i + 1;
int end = j + tgtCount - 1;
for (int k = 1;
j < end && getChar(src, j) == (tgt[k] & 0xff);
j++, k++);
if (j == end) {
// Found whole string.
return i;
}
}
}
return -1;
}
//从后往前查跟从前往后查是类似的
public static int lastIndexOfLatin1(byte[] src, int srcCount,
byte[] tgt, int tgtCount, int fromIndex) {
assert fromIndex >= 0;
assert tgtCount > 0;
assert tgtCount <= tgt.length;
int min = tgtCount - 1;
int i = min + fromIndex;
int strLastIndex = tgtCount - 1;
char strLastChar = (char)(tgt[strLastIndex] & 0xff);
checkIndex(i, src);
startSearchForLastChar://断言
while (true) {
while (i >= min && getChar(src, i) != strLastChar) {
i--;
}
if (i < min) {
return -1;
}
int j = i - 1;
int start = j - strLastIndex;
int k = strLastIndex - 1;
while (j > start) {
if (getChar(src, j--) != (tgt[k--] & 0xff)) {
i--;
continue startSearchForLastChar;
}
}
return start + 1;
}
}
//
//正则表达式进行匹配切分,当然也要考虑到limit对数组的限制
public String[] split(String regex, int limit) {
/* fastpath if the regex is a
(1)one-char String and this character is not one of the
RegEx's meta characters ".$|()[{^?*+\\", or
(2)two-char String and the first char is the backslash and
the second is not the ascii digit or ascii letter.
*/
char ch = 0;
if (((regex.length() == 1 &&
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
(regex.length() == 2 &&
regex.charAt(0) == '\\' &&
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 &&
((ch-'A')|('Z'-ch)) < 0)) &&
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE))
{
int off = 0;
int next = 0;
boolean limited = limit > 0;
ArrayList<String> list = new ArrayList<>();
while ((next = indexOf(ch, off)) != -1) {
if (!limited || list.size() < limit - 1) {
list.add(substring(off, next));//采用substring方法进行切分
off = next + 1;
} else {
// last one
//assert (list.size() == limit - 1);
int last = length();
list.add(substring(off, last));
off = last;
break;
}
}
// If no match was found, return this
if (off == 0)
return new String[]{
this};
// Add remaining segment
if (!limited || list.size() < limit)
list.add(substring(off, length()));
// Construct result
int resultSize = list.size();
if (limit == 0) {
while (resultSize > 0 && list.get(resultSize - 1).isEmpty()) {
resultSize--;
}
}
String[] result = new String[resultSize];
return list.subList(0, resultSize).toArray(result);
}
return Pattern.compile(regex).split(this, limit);
}
//
public static String valueOf(char data[], int offset, int count) {
return new String(data, offset, count);
}
//
public char[] toCharArray() {
return isLatin1() ? StringLatin1.toChars(value)
: StringUTF16.toChars(value);
}
public static char[] toChars(byte[] value) {
char[] dst = new char[value.length];//进行数组复制
inflate(value, 0, dst, 0, value.length);
return dst;
}
2.3 关于StringBuffer
2.3.1 StringBuffer的结构
StringBuffer
同样的,StringBuffer我们也从其UML图开始:
对StringBuffer的继承的接口和抽象接口进行补充说明:
- 接口Appendable:能够被追加char序列和值的对象。如果某个类的实例打算接收来自Formatter的格式,必须实现Appendable
- 抽象类AbstractStringBuilder:是StringBuffer、StringBuilder的直接父类。定义了很多方法的默认实现。
StringBuffer的成员变量如下(成员变量注释写在下面源码处):
由于StringBuffer继承了AbstractStringBuilder,所以StringBuffer也继承了AbstractStringBuilder的成员变量:
成员变量会在下面的源码出进行注释说明
StringBuffer的构造器有如下:
2.3.2 StringBuffer的部分Field和Constructor
先针对部分成员变量和部分构造器进行源码分析:
//注意这里final关键字修饰
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, Comparable<StringBuffer>, CharSequence
{
private transient String toStringCache;//返回上次最后一次修改的整个字符串的缓存
static final long serialVersionUID = 3388685877147921107L;
//------AbstractStringBuilder的成员变量-------------------------------------
byte coder;//编码标识
int count;//当前字符串的长度
private static final byte[] EMPTYVALUE = new byte[0];//初始模式是空的字节数组
@HotSpotIntrinsicCandidate
public StringBuffer(String str) {
super(str.length() + 16);//在原有字符串长度上进行扩充16的长度
append(str);
}
AbstractStringBuilder(int capacity) {
//调用父类AbstractStringBuilder的构造器
if (COMPACT_STRINGS) {
value = new byte[capacity];
coder = LATIN1;
} else {
value = StringUTF16.newBytesFor(capacity);
coder = UTF16;
}
}
@Override
@HotSpotIntrinsicCandidate
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
public AbstractStringBuilder append(String str) {
if (str == null) {
return appendNull();
}
int len = str.length();
ensureCapacityInternal(count + len);
putStringAt(count, str);//把str放在count位置后面
count += len;//更新字符串长度,注意这里要区分容量和长度的区别
return this;
}
private final void putStringAt(int index, String str) {
if (getCoder() != str.coder()) {
inflate();
}
str.getBytes(value, index, coder);
}
/
@HotSpotIntrinsicCandidate
public StringBuffer() {
//空字符
super(16);
}
/
@HotSpotIntrinsicCandidate
public StringBuffer(int capacity) {
super(capacity);//指定字符串长度
}
2.3.3 通过一段程序来阅读关于StringBuffer的常用方法的源代码
下面通过一段简单的程序来对StringBuffer的几个常用的方法进行源码解析
public class Test1 {
public static void main(String[] args) throws Exception {
StringBuffer sb = new StringBuffer("a");
StringBuffer sb1=new StringBuffer();
sb.append("bbbbbbbbbbbbbbbb");
System.out.println(sb.toString());
System.out.println(sb.compareTo(sb1));
System.out.println(sb.substring(0,2));
System.out.println(sb.equals("a"));
System.out.println(sb.hashCode());
}
}
@Override
@HotSpotIntrinsicCandidate
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
public AbstractStringBuilder append(String str) {
if (str == null) {
return appendNull();
}
int len = str.length();
ensureCapacityInternal(count + len);
putStringAt(count, str);
count += len;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
int oldCapacity = value.length >> coder;
if (minimumCapacity - oldCapacity > 0) {
//若不够存,则创建一个新的出来
value = Arrays.copyOf(value,
newCapacity(minimumCapacity) << coder);//复制出一个新的,更大容量的byte数组
}
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int oldCapacity = value.length >> coder;
int newCapacity = (oldCapacity << 1) + 2;//容量翻倍再加2
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
int SAFE_BOUND = MAX_ARRAY_SIZE >> coder;//保证不超过安全界限
return (newCapacity <= 0 || SAFE_BOUND - newCapacity < 0)
? hugeCapacity(minCapacity)//安全界限进行变化
: newCapacity;
}
private final void putStringAt(int index, String str) {
if (getCoder() != str.coder()) {
//编码方式是否一致,不一致的话就进行一致性编码
inflate();
}
str.getBytes(value, index, coder);//复制字节数组
}
@Override
@HotSpotIntrinsicCandidate
public synchronized String toString() {
if (toStringCache == null) {
return toStringCache =
isLatin1() ? StringLatin1.newString(value, 0, count)
: StringUTF16.newString(value, 0, count);
}
return new String(toStringCache);
}
@Override
public synchronized int compareTo(StringBuffer another) {
return super.compareTo(another);
}
int compareTo(AbstractStringBuilder another) {
if (this == another) {
return 0;
}
byte val1[] = value;
byte val2[] = another.value;
int count1 = this.count;
int count2 = another.count;
if (coder == another.coder) {
return isLatin1() ? StringLatin1.compareTo(val1, val2, count1, count2)
: StringUTF16.compareTo(val1, val2, count1, count2);
}
return isLatin1() ? StringLatin1.compareToUTF16(val1, val2, count1, count2)
: StringUTF16.compareToLatin1(val1, val2, count1, count2);
}
@Override
public synchronized String substring(int start, int end) {
return super.substring(start, end);
}
public String substring(int start, int end) {
checkRangeSIOOBE(start, end, count);//检查是否指定的范围有效
if (isLatin1()) {
return StringLatin1.newString(value, start, end - start);
}
return StringUTF16.newString(value, start, end - start);
}
public boolean equals(Object obj) {
return (this == obj);
}
@HotSpotIntrinsicCandidate
public native int hashCode();//注意,这里的hashCode被native关键字修饰(就是我们常听到的JNI[Java Native Interface]),被native修饰的代表其是由C语言实现的,调用C的API
//以下是PrintStream类的
public void println(int x) {
synchronized (this) {
print(x);
newLine();
}
}
public void print(int i) {
write(String.valueOf(i));
}
private void write(String s) {
try {
synchronized (this) {
ensureOpen();
textOut.write(s);
textOut.flushBuffer();
charOut.flushBuffer();
if (autoFlush && (s.indexOf('\n') >= 0))
out.flush();
}
}
catch (InterruptedIOException x) {
Thread.currentThread().interrupt();
}
catch (IOException x) {
trouble = true;
}
}
private void newLine() {
try {
synchronized (this) {
ensureOpen();
textOut.newLine();
textOut.flushBuffer();
charOut.flushBuffer();
if (autoFlush)
out.flush();
}
}
catch (InterruptedIOException x) {
Thread.currentThread().interrupt();
}
catch (IOException x) {
trouble = true;
}
}
2.4 关于StringBuilder(不细讲)
下面的StringBuilder就不展开详细讲了,因为StringBuffer和StringBuilder几乎是一样的,主要不同是StringBuffer的成员方法都加上了悲观锁synchronized,保证线程安全,而StringBuilder没有加。直接上一副StringBuilder的UML图:
以上是单纯针对String、StrignBuffer、StringBuilder的JDK源码进行分析,下面真正的干货来啦!!!
三 干货!!!
我将其总结为要点:
- 字符串拼接效率:StringBuilder>StrignBuffer>String
- String、StringBuilder、StringBuffer这三者都是被final关键字修饰,所以三者都是不能被继承的
- 三者的使用场景分别是:
- String使用场景:在不需要经常修改字符串的时候进行使用。
- StringBuilder使用场景:在非多线程并发的场景下且经常需要修改字符串的时候进行使用。
- StringBuffer适用场景:在多线程并发的场景下(存在线程安全隐患)且经常需要修改字符串的时候进行使用。
- String不是基本数据类型,因为String被final修饰,在Java中有八大基本数据类型:byte(整型单字节)、boolean(布尔单字节)、short(整型双字节)、char(双字节)、int(四字节)、float(四字节)、long(八字节)、double(八字节)
- 如何看方法中定义的局部变量是否是线程安全的:看是否是数据共享的(有无被static修饰)、是否是在多线程应用场景中
来看段代码:
public class Test1 {
public static void main(String[] args) throws Exception {
StringBuilder sb1=new StringBuilder("a");
StringBuilder sb2=new StringBuilder("a");
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<5;i++){
test(sb1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
test(sb2);
}
}
});
t1.start();
t2.start();
Thread.sleep(100000);
System.out.println(sb1.toString());
System.out.println(sb2.toString());
}
//线程可能不安全,倘若有俩线程同时调用进入,进行操作,这时由于sb是一个引用传递,而且static用的是同一份,这时双方的执行速度不同对导致最终的结果有所偏差
public static void test(StringBuilder sb){
sb.append("沈烁华");
sb.append("聊编程");
}
//线程可能是不安全的,与test道理类似
public static StringBuilder test2(StringBuilder sb){
sb.append("沈烁华");
sb.append("聊编程");
return sb;
}
//线程安全,因为其返回的是String类型,gen形参StringBuilder sb无关
public static String test3(StringBuilder sb){
sb.append("沈烁华");
sb.append("聊编程");
return sb.toString();
}
}
看下结果你就知道了:
-
关于String.intern():当程序中有大量的字符串操作是,使用该方法就是将字符串或字符串的引用地址放入字符串常量池中(前提是字符串常量池中没有该字符串),下次需要的话直接从字符串常量池中取,就不用再去创建,省去创建很多重复字符串,节省内存空间,GC是可以回收StringTable的,这样做加快了系统运行效率。
下图是虚拟机jdk8中的线程共享区简图:
-
jdk版本更新迭代很快,之所以把字符串常量池放在堆空间中,是因为heap是gc的主战场,这样可以提高字符串的回收效率,提升空间利用率,降低OOM的风险。
-
StringTable内部是Hashtable(线程安全),不同jdk版本有不同的默认长度,当池内的数量接近最大可容纳的数量时,就会造成哈希冲突。根据不同的应用场景,我们可以手动设置其大小:-XX:StringTableSize=N来设置
-
字符串拼接操作:
- 常量与常量之间进行字符串拼接结果是放在常量池中的,其原理是编译器优化;
- 相同的字符串在常量池中只存放一份 。
- 只要其中有一个是变量,结果就是在堆中,其拼接原理是使用了StringBuilder,但如果拼接左右两边都是被final修饰的常量引用或常量,其原理就不是StringBuilder了
- 如果拼接结果调用intern(),则主动将常量池中没有的字符串对象放入池中,并返回此对象的地址。
public class Test1 {
public static void main(String[] args) throws Exception {
String s1 = "java";//相当于拼接""进入常量池
String s2 = "python";//相当于拼接""进入常量池
String s3 = "javapython";//相当于拼接""进入常量池
String s4 = "java" + "python";//拼接进入常量池
String s5 = s1 + "python";//相当于new出一个StringBuilder,指向的是地址引用
String s6 = "java" + s2;//相当于new出一个StringBuilder,指向的是地址引用
String s7 = s1 + s2;//相当于new出一个StringBuilder,指向的是地址引用
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
String s8 = s6.intern();//看常量池中是否存在,如果存在返回地址,若不存在,将其放入并返回池中地址
System.out.println(s3 == s8);//true
System.out.println(s6 == s8);//false 池中的地址和StringBuilder堆中的地址是不一样的
System.out.println("===================");
final String s9 = "java";//常量
String s10 = s9 + "python";//常量引用+常量
System.out.println(s10==s8);//true
System.out.println(s10==s7);//false
}
}
- 在可以指定容量的类构造器中,能尽量指定容量就指定容量,因为如果后面使用默认容量不够的话,就会去进行扩容,扩容操作是需要一定开销的。例如StringBuilder、StringBuffer,二者的默认容量是16;
- 如果需要经常使用字符串拼接的就用StringBuiilder或StringBuffer,不要用String,因为String拼接过程会生成大量的String和StringBuilder对象,在GC时会消耗额外大量时间
- 重头戏来了!!!判断下面的代码创建了几个对象:(注:只单纯看该语句)
java String s=new String("a")+new String("b");
我想还是先方法答案再放字节码:
上述问题共创建了6个对象:
“a”和“b”为两个常量对象,
new String()算上两个,
因为左右两边有变量,所以底层是使用StringBuilder去append,这部分外加上一个,
最后一个是StringBuilder.toString()转为String对象
放字节码:
Last modified 2021年4月10日; size 1075 bytes
MD5 checksum 877740f339941987f066b1ba1d115504
Compiled from "Test1.java"
public class com.hua.demo.string.Test1
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #7 // com/hua/demo/string/Test1
super_class: #8 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 3
Constant pool:
#1 = Methodref #8.#27 // java/lang/Object."<init>":()V
#2 = Class #28 // java/lang/String
#3 = String #29 // a
#4 = Methodref #2.#30 // java/lang/String."<init>":(Ljava/lang/String;)V
#5 = String #31 // b
#6 = InvokeDynamic #0:#35 // #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
#7 = Class #36 // com/hua/demo/string/Test1
#8 = Class #37 // java/lang/Object
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/hua/demo/string/Test1;
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 args
#19 = Utf8 [Ljava/lang/String;
#20 = Utf8 s
#21 = Utf8 Ljava/lang/String;
#22 = Utf8 Exceptions
#23 = Class #38 // java/lang/Exception
#24 = Utf8 MethodParameters
#25 = Utf8 SourceFile
#26 = Utf8 Test1.java
#27 = NameAndType #9:#10 // "<init>":()V
#28 = Utf8 java/lang/String
#29 = Utf8 a
#30 = NameAndType #9:#39 // "<init>":(Ljava/lang/String;)V
#31 = Utf8 b
#32 = Utf8 BootstrapMethods
#33 = MethodHandle 6:#40 // REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#34 = String #41 // \u0001\u0001
#35 = NameAndType #42:#43 // makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
#36 = Utf8 com/hua/demo/string/Test1
#37 = Utf8 java/lang/Object
#38 = Utf8 java/lang/Exception
#39 = Utf8 (Ljava/lang/String;)V
#40 = Methodref #44.#45 // java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#41 = Utf8 \u0001\u0001
#42 = Utf8 makeConcatWithConstants
#43 = Utf8 (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
#44 = Class #46 // java/lang/invoke/StringConcatFactory
#45 = NameAndType #42:#50 // makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#46 = Utf8 java/lang/invoke/StringConcatFactory
#47 = Class #52 // java/lang/invoke/MethodHandles$Lookup
#48 = Utf8 Lookup
#49 = Utf8 InnerClasses
#50 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
#51 = Class #53 // java/lang/invoke/MethodHandles
#52 = Utf8 java/lang/invoke/MethodHandles$Lookup
#53 = Utf8 java/lang/invoke/MethodHandles
{
public com.hua.demo.string.Test1();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 14: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/hua/demo/string/Test1;
public static void main(java.lang.String[]) throws java.lang.Exception;
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=2, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String a
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: new #2 // class java/lang/String
12: dup
13: ldc #5 // String b
15: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
18: invokedynamic #6, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
23: astore_1
24: return
LineNumberTable:
line 21: 0
line 22: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
24 1 1 s Ljava/lang/String;
Exceptions:
throws java.lang.Exception
MethodParameters:
Name Flags
args
}
SourceFile: "Test1.java"
InnerClasses:
public static final #48= #47 of #51; // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #33 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
Method arguments:
#34 \u0001\u0001
再例如java String s="a"+"b"+"c";//编译器将其优化,等同于直接"abc"
就只有一个
但如果写法是:
public class Test1 {
public static void main(String[] args) throws Exception {
//String s="a"+"b"+"c";//编译器将其优化,等同于直接"abc"
String a1="a";
String b1="b";
String c1="c";
String s=a1+b1+c1;
}
}
String[] args不算的话,就是4个。
结束语
本人目前仍处于Java学习阶段,今后会继续写一些关于底层源码的文章,以上内容是本人翻阅源码以及之前自己接触过的相关重点内容做出的总结,如有不对之处欢迎指正,需要补充的可以私我或者在评论区说下,也欢迎小伙伴们进行技术交流,共同成长。喜欢的小伙伴可以点赞加关注,感谢!
转载:https://blog.csdn.net/weixin_44827241/article/details/115499393