本文共 15506 字,大约阅读时间需要 51 分钟。
public final class String implements java.io.Serializable, Comparable, CharSequence{ .... }
CharSequence
字符序列接口public final class String implements java.io.Serializable, Comparable, CharSequence{ //之前版本的JDK的String实现是char数组,需要两个字节 //而在之后更新的JDK中实现为byte数组,加上一个下面的coder标志来表示字符 private final byte[] value; //用于对value的byte进行编码的编码标识符,即使用什么编码对value进行编码 //支持的编码有LATIN1即ISO-8859-1单字节编码和UTF-16双字节编码 //如果value中保存的字符串都可以用LATIN1保存,那么coder=0,否则就使用UTF-16保存,coder=1 private final byte coder; //缓存的hash值,默认为0 private int hash; //数字代表编码 @Native static final byte LATIN1 = 0; @Native static final byte UTF16 = 1; }
private final char value[]
的,这个变化在JDK9中发生改变的,这个改变使字符串能够占用更少的空间,因为原来实现的数组是char,是2字节长度,那么在更改为byte数组后,每个元素只有一个字节的长度,所以节省了一半的空间(并不准确,但是肯定比之前的char节省,下面介绍)比如之前存储how
单词,char[]
数组是这样的
[0][h][0][o][0][w]//之后byte单字节存储[h][o][w]
char[]
存储的一半了,但是中国汉字不止占一个字节,即一个byte存不下一个汉字了,这时候还是需要2字节去存储的,比如JDK8和JDK11分别存储期待a
,如下图coder
编码标识符改为了1
,即UTF-16
对于coder可以这样做一个实验,如下
String str = new String("xx"); //当你在构造器打断点的时候,此时coder=0String str = new String("中国"); //coder=1
这一改变,也直接影响了String.length()方法
public int length() { //即如果是 LATIN-1 编码,则右移0位.数组长度即为字符串长度.而如果是 UTF16 编码,则右移1位,数组长度的二分之一为字符串长度 return value.length >> coder();}byte coder() { //COMPACT_STRINGS默认为true //这个变量就代表了String一开始是否使用紧凑布局,这个参数由JVM注入,只能通过虚拟机参数更改 //意思就是如果是紧凑布局的话,那么我们就使用coder作为返回值,coder会根据你存的string的内容变化 //如果是False就是放弃紧凑布局,那么就是用双字节进行存储内容 return COMPACT_STRINGS ? coder : UTF16;}
既然String子层存储发生了变化,那么相关的StringBuilder
和StringBuffer
也发生了变化,如下是他们两个类的父类
abstract class AbstractStringBuilder implements Appendable, CharSequence { byte[] value; byte coder; }
从new String(char[] ch)
构造器开始
//断点代码public static void main(String[] args) { char[] chars = {'A', 'B'}; String str = new String(chars); System.out.println(str);}//String构造器public String(char value[]) { this(value, 0, value.length, null);}//String包级别构造器String(char[] value, int off, int len, Void sig) { //这的注释可以过一眼,等你看完下面的流程后,你就知道这是什么作用了 if (len == 0) { this.value = "".value; this.coder = "".coder; return; } //COMPACT_STRINGS默认为true,即代表启用压缩,即使用单字节编码 if (COMPACT_STRINGS) { //compress里面判断如果char数组存在 value > 0xFF 的值时,就返回null, 0xFF=255 //如果内容全部小于0xFF,即代表可以全部采用单字节编码 //那么返回值就不是null,那么直接赋值给String类中属性就行了 byte[] val = StringUTF16.compress(value, off, len); if (val != null) { //直接赋值给value属性,单字节编码初始化完毕 this.value = val; this.coder = LATIN1; return; } } //到这就代表上面遇到了不能直接单字节编码的String了,然后就开始采用双字节编码 this.coder = UTF16; //然后将要保存的String用UTF16编码即可,到这就初始化完毕 this.value = StringUTF16.toBytes(value, off, len);}public static byte[] compress(char[] val, int off, int len) { //这个就是存放char转到byte后的数据的临时数组 byte[] ret = new byte[len]; //内部调用,里面判断是否c>0xFF,如果都小于,就代表可以全部单字节编码,返回值就==len //如果遇到了c>0xFF的情况,那么这个条件不会成立 if (compress(val, off, ret, 0, len) == len) { //到这就代表已经保存进了byte数组内了,返回就可以 return ret; } //这就代表需要保存的String不能直接单字节编码 return null;}public static int compress(char[] src, int srcOff, byte[] dst, int dstOff, int len) { for (int i = 0; i < len; i++) { char c = src[srcOff]; //判断char中的每个是否value > 0xFF if (c > 0xFF) { //如果发现c>0xFF那么len赋值为0,跳出,所以len返回值也为0,所以会造成上层判断为false len = 0; break; } //如果不遇到break,说明要保存的String,可以直接用单字节编码,循环完成了,即char也保存到了byte了 dst[dstOff] = (byte)c; //指针++ srcOff++; dstOff++; } return len;}
JDK8中的初始化
public String(char value[]) { this.value = Arrays.copyOf(value, value.length);}
使用方法就不多说了,来看一下他们的实现:substring
,,replace
//截取字符串public String substring(int beginIndex, int endIndex) { int length = length(); //检查是否越界 checkBoundsBeginEnd(beginIndex, endIndex, length); //截取的长度 int subLen = endIndex - beginIndex; if (beginIndex == 0 && endIndex == length) { return this; } //根据编码来区分截取字符,注意他们的方法是!!newString!!,所以不用跟进去也知道他是创建一个新的子串 return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen) : StringUTF16.newString(value, beginIndex, subLen); }
//替换字符串public String replace(char oldChar, char newChar) { if (oldChar != newChar) { //StringLatin1这里面的方法有点长,如下 String ret = isLatin1() ? StringLatin1.replace(value, oldChar, newChar) : StringUTF16.replace(value, oldChar, newChar); if (ret != null) { return ret; } } return this;}//因为return可以直接返回结果,所以我们直接看返回就好了,下面是精简后的程序,详细程序我看不懂..嘻嘻//看到都是newString返回的public static String replace(byte[] value, char oldChar, char newChar) { if (canEncode(oldChar)) { ... if (i < len) { ... return new String(buf, LATIN1); } else { ... return new String(buf, UTF16); } } } return null; // for string to return this;}
当我们创建一个字符串的时候,Java会先去线程池中寻找,如果有就返回这个字符串,否则就新建一个放入池中,当然这个操作是排除new String()
操作的,仅支持直接可以能够判断出变量值的状态,比如下面
//可以直接得到变量的值String str1 = "1";String str2 = "1";System.out.println(str1 == str2); //true//下面是不能直接得到值的,比如new String()String str1 = "1";String str2 = new String("1");System.out.println(str1 == str2); //false
getNum
方法中是return "1";
也相当于在创建对象,需要到池中搜索,结果发现有1
,所以比较会返回true,这只是我的猜测,我还不知道怎么去证实,如果不对请指正,谢谢补充:虽然getNum
比较返回为true,但是只是证明是常量池中的一个对象,而这个方法依旧是运行时才会知道其返回值的,即编译器无法确定他的具体值
public void test() { String str1 = "1"; String str2 = getNum(); System.out.println("1" == str1); //true System.out.println("1" == str2); //true System.out.println(str1 == str2); //true}private String getNum(){ return "1"; //我的证实是将这里改为new String("1"),上面会返回false,所以得出上面的结论}
到这就需要提到两个概念:静态常量池
,动态常量池
*.class
文件中的常量池,class文件中的常量池不仅仅包含字符串(数字)字面量,还包含类,方法的信息,占用class文件绝大部分空间提到上面两个概念,可以解决这个问题:上面说创建字符串时会在常量池中寻找,那么new String("1")
为啥不等于String str = "1"
呢?
new
操作其实是创建了一个真正的对象,这个我们都知道,所以这个new出来的对象1
一定会在堆内存,我们之前也证实了不管从常用方法还是常量池机制都保证了不会有重复的字符串,所以这的唯一可能就是new出来的对象是引用常量池中的对象的,如果常量池中没有这个对象,new操作就会先在常量池中新建一个常量,然后再引用他,如下图String str = new String("1");String n = "1";System.out.println(str == n); //false
str = n
,其一次指向完全不同,所以返回false那怎么证明new String真的是创建了一个对象一个常量呢 ?(一个new 对象在堆,一个在常量池),我们使用到了javap -verbose 输出附加信息
public static void main(String[] args) {}
public class com.qidai.Tests//...Constant pool: //常量池出现,其中没有我们定义的字符,因为是空实现哈哈 #1 = Methodref #3.#17 // java/lang/Object."":()V #2 = Class #18 // com/qidai/Tests #3 = Class #19 // java/lang/Object #4 = Utf8 #5 = Utf8 ()V #6 = Utf8 Code #7 = Utf8 LineNumberTable #8 = Utf8 LocalVariableTable #9 = Utf8 this #10 = Utf8 Lcom/qidai/Tests; #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 args #14 = Utf8 [Ljava/lang/String; #15 = Utf8 SourceFile #16 = Utf8 Tests.java #17 = NameAndType #4:#5 // " ":()V #18 = Utf8 com/qidai/Tests #19 = Utf8 java/lang/Object{ public com.qidai.Tests(); //... public static void main(java.lang.String[]); //main方法开始 Code: stack=0, locals=1, args_size=1 0: return //无实现直接返回}
String string = new String("MyConstantString");
public class com.qidai.TestsConstant pool: #1 = Methodref #6.#22 // java/lang/Object."":()V #2 = Class #23 // java/lang/String #3 = String #24 // MyConstantString #4 = Methodref #2.#25 // java/lang/String." ":(Ljava/lang/String;)V #5 = Class #26 // com/qidai/Tests #6 = Class #27 // java/lang/Object #7 = Utf8 #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/qidai/Tests; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 string #19 = Utf8 Ljava/lang/String; #20 = Utf8 SourceFile #21 = Utf8 Tests.java #22 = NameAndType #7:#8 // " ":()V #23 = Utf8 java/lang/String #24 = Utf8 MyConstantString //!!!!!!!!!!!类文件中出现了~~~~~ #25 = NameAndType #7:#28 // " ":(Ljava/lang/String;)V #26 = Utf8 com/qidai/Tests #27 = Utf8 java/lang/Object #28 = Utf8 (Ljava/lang/String;)V{ public static void main(java.lang.String[]); Code: stack=3, locals=2, args_size=1 //因为有实现了,所以没有直接返回 0: new #2 // class java/lang/String 3: dup 4: ldc #3 // String MyConstantString !!!!!!!!!!!! 6: invokespecial #4 // Method java/lang/String." ":(Ljava/lang/String;)V 9: astore_1 10: return}
new String()
的时候会创建一个对象和一个常量,至此我们就算验证结束了,但是我们还可以验证一个其他的问题:不是说常量池不重复的嘛,那么我们再定义一个一样数据的String呢?,所以我们现在main方法中就有两行内容了,如下String string = new String("MyConstantString");String constant = "MyConstantString";
public class com.qidai.TestsConstant pool: #1 = Methodref #6.#23 // java/lang/Object."":()V #2 = Class #24 // java/lang/String #3 = String #25 // MyConstantString #4 = Methodref #2.#26 // java/lang/String." ":(Ljava/lang/String;)V #5 = Class #27 // com/qidai/Tests #6 = Class #28 // java/lang/Object #7 = Utf8 #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/qidai/Tests; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 string //常量池中会加入Strig引用变量?? #19 = Utf8 Ljava/lang/String; #20 = Utf8 constant //常量池中会加入Strig引用变量?? #21 = Utf8 SourceFile #22 = Utf8 Tests.java #23 = NameAndType #7:#8 // " ":()V #24 = Utf8 java/lang/String #25 = Utf8 MyConstantString //仅有一个~~~ #26 = NameAndType #7:#29 // " ":(Ljava/lang/String;)V #27 = Utf8 com/qidai/Tests #28 = Utf8 java/lang/Object #29 = Utf8 (Ljava/lang/String;)V{ public com.qidai.Tests(); public static void main(java.lang.String[]); Code: stack=3, locals=3, args_size=1 0: new #2 // class java/lang/String 3: dup 4: ldc #3 // String MyConstantString 6: invokespecial #4 // Method java/lang/String." ":(Ljava/lang/String;)V 初始化方法? 9: astore_1 10: ldc #3 // String MyConstantString 这应该就是咱们定义的constant了 12: astore_2 13: return}
深入理解Java虚拟机
所以对上面也是一知半解,我们用javap证明了他确实会只保存一个常量,但是我们看到常量池中会加入Strig引用变量??这个问题我还不知道怎么回答,如果您知道此问题,请评论告诉我,谢谢,暂且记住常量池中会出现引用变量吧,毕竟他是真实存在的,但我们现在分析的只是class类文件,而JVM中的动态常量池中应该会把他去掉,但这个全局的常量池中应该会有一个与常量池保存引用的一个机制,要么怎么找到常量池中的对象呢?我感觉这个在class文件中的常量池中出现只是在描述这个类信息,也就是说有点定义的意思,这是自己理解的,别信...我说真的...自己没把握...好了知道了这些内容,我们还需要知道JVM在编译的时候会进行编译优化的,比如宏变量的替换,比如上面的可确定的变量直接写为确定值了,比如
public static void main(String[] args) { String str = "1"+"2"+"3"; String method = getNum();}private static String getNum() {return "1";}
public static void main(String[] args) { String str = "123"; //直接替换为可确定值 String method = getNum(); //这是不可确定的}private static String getNum() {return "1";}
好了知道了会进行编译优化的话,我们来看几个实例
public static void main(String[] args) { String s0= "helloworld"; //直接常量池 helloworld String s1= new String("helloworld"); //堆helloworld+引用常量池helloworld //javap看是hello和一个world常量 String s2= "hello" + new String("world"); System.out.println("===========test4============"); //s0常量引用不等于s1的堆引用 System.out.println( s0==s1 ); //false //s0的常量引用不等于s2的堆引用和常量引用 System.out.println( s0==s2 ); //false //s1不等于s2,因为s2生成了两个一个hello和一个world System.out.println( s1==s2 ); //false}
helloworld
,world
,hello\u0001
,这个\u0001
有人知道是什么东西吗??连接符?G1 GC排重默认是关闭的,需要指定
-XX:+UseStringDeduplication
转载地址:http://lfjka.baihongyu.com/