Java之String类的底层原理

String类的特性

  • Java中的String类是Java语言中的一个基本类,位于java.lang.String代表字符串。

  • Java中所有的字符串文字(如:"hello")都可以看做是实现类的实例。

  • 字符串是常量,用双引号引起来表示,它们的值在创建之后不能更改。

  • 字符串类型本身是final声明的,意味着不能被继承。

  • String对象的字符内容底层是存储在一个字符数组value[]中的。

"hello" 相当于 char[] data = {'h','e','l','l','o'}

String类的底层存储结构
  • JDK1.9之前:存储在一个字符数组char[] value中
//JDK1.8中的源码
public final class String
 implements java.io.Serializable, Comparable<String>, CharSequence {

 private final char value[]; //String对象的字符内容是存储在此数组中
  1. private意味着外面无法直接获取字符数组,而且String没有提供value的get和set方法。
  2. final意味着字符数组的引用不可改变,而且String也没有提供方法来修改value数组某个元素值
  3. 因此字符串的字符数组内容也不可变的,即String代表着不可变的字符序列。即,一旦对字符串进行修改,就会产生新对象。
  • JDK1.9及之后,底层使用byte[]数组存储
//JDK1.17中的源码
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {
    @Stable
    private final byte[] value;

针对char[] value改为byte[] value数组的说明:
官方说明:大多数String对象只包含Latin-1字符。这样的字符只需要1字节的存储空间,因此这些字符串对象的内部字符数组中有一半的空间没有使用。
细节:新的String类将根据字符串的内容存储编码为ISO-8859-1/Latin-1(每个字符1字节)或UTF-16(每个字符2字节)的字符。encoding标志表示使用哪种编码。

  • Java 语言提供对字符串串联符号("+")以及将其他对象转换为字符串的特殊支持(toString()方法)。

String底层内存结构

因为字符串对象设计为不可变,所以字符串有常量池来保存很多常量对象。

JDK6中,字符串常量池在方法区。JDK7开始,就移到堆空间,直到目前JDK17版本。


String的内存结构
public class StringTest {

    /*
    String:字符串。使用一对""引起来表示
    1. String声明为final的,不可被继承
    2. String实现了Serializable接口:表示字符串支持序列化
    3. String实现了Comparable接口:表示String可以比较大小
    4. String内部定义了final char[] value用于存储字符串数据
    5. String代表了不可变的字符序列。简称:不可变性
    String代码的实现逻辑:
    1. 当对字符串重新赋值时,需要重新指定内存区域赋值,不能使用原有的char[] value进行赋值;
       因为字符串不可变序列,第一次赋值以及固定数组长度,当重新赋值是,需要重新开辟一片内存区域
    2. 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在方法区的字符串常量池中
    3. 方法区常量池中是不会存储相同内容的字符串
    4. 当对现有的字符串进行拼接操作时,也需要重新分配内存区域赋值,不能使用原有的char[] value进行赋值
    5. 当调用String中replace()方法替换指定字符或字符串时,也需要重新分配内存区域赋值,
        不能使用原有的char[] value进行赋值
     */
   @Test
    public void test1() {
        String s1 = "abc";
        String s2 = "abc";

        // 内存中只有一个"abc"对象被创建,同时被s1和s2共享。
        System.out.println(s1 == s2); //true:比较的两个常量对象的地址值

        s2 = "hello";
        System.out.println(s2 == s1); //false: 重新赋值时,方法区中重新分配了字符串常量
    }
}

String字面量定义&实例化对象的方式

/*
String实例化的方式:
      方式一:通过字面量定义的方式
      方式二:通过new+构造器的方式
*/
@Test
public void test2(){
    //通过字面量定义的方式:此时的s1和s2的数据声明在方法区的字符串常量池中
    String str1 = "abc";
    String str2 = "abc";

    //通过new+构造器的方式:此时的s3和s4都保存的地址值,是数据在堆空间中开辟内存空间以后生成对应的地址值
    String str3 = new String("JavaEE");
    String str4 = new String("C++");
    String str5 = str3;

    System.out.println(str1 == str2); //true
    System.out.println(str3 == str4); //false
    System.out.println(str1 == str3); //false
    System.out.println(str5 == str3); //true
}
String字面量定义的内存结构
String字面量定义&实例化对象的内存结构

结论:
str2 首先指向堆中的一个字符串对象,然后堆中字符串的value数组指向常量池中常量对象的value数组。
1、字符串常量存储在字符串常量池,目的是共享。
2、字符串非常量对象存储在堆中。

String定义字面量的注意点

/*
String不同拼接的内存解析:
     1. 常量和常量的拼接结果在常量池中。且常量池中不会存在相同内容的常量
     2. 只要其中有一个是变量,结果就在堆空间中
     3. 如果拼接的结果调用intern()方法,返回值就在常量池中
*/
@Test
public void test2() {
    String s1 = "hello";
    String s2 = "world";
    String s3 = "hello" + "world";
    String s4 = s1 + "world";
    String s5 = s1 + s2;
    String s6 = (s1 + s2).intern();

    System.out.println(s3 == s4); //false
    System.out.println(s3 == s5); //false
    System.out.println(s4 == s5); //false
    System.out.println(s3 == s6); //true
}

结论:
(1)常量+常量:结果是常量池。且常量池中不会存在相同内容的常量。
(2)常量与变量 或 变量与变量:结果在堆中
(3)拼接后调用intern方法:返回值在常量池中

String常用方法

  • int Length():返回字符串的长度。 return value.length
  • char charAt(int index):返回指定索引的字符。return value[index]
  • boolean isEmpty():判断是否是空字符串。 return value.length == 0
  • String toLowerCase():使用默认语言环境,将String中的所有字符转换为小写。
  • String toUpperCase():使用默认语言环境,将String中的锁字符转换为大写。
  • String trim():返回字符串的副本,忽略前空白符和后空白符。
  • boolean equals(Object obj):比较字符串的内容是否相同。
  • boolean equalslgnoreCase(String anotherString):与equals方法类似,忽略大小写。
  • String concat(String str):将指定字符串连接到此字符串方结尾。等价于用 "+"
  • int compareTo(String anotherString):比较两个字符串的大小。
  • String substring(int beginIndex):返回一个新的字符串,它是此字符串的从beginIndex位置开始截取到最后一个子字符串。
  • String substring(int beginIndex,int endIndex):返回一个新的字符串,他是此字符串从beginInde开始截取到entIndex(不含包endIndex)的一个子字符串。
  • boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束。
  • boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始。
  • boolean startsWith(String prefix,int toffset):测试此字符串从指定索引开始到子字符串是否以指定前缀开始。
  • boolean contains(CharSequence s):当且仅当此字符串包含指定的char值序列时,返回true。
  • int indexOf(String str):返回指定子字符串在此字符串中第一次出现的索引。
  • int indexOf(String str,int fromIndex):返回指定子字符串在此字符串中第一次出现的索引,从指定的索引开始。
  • int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现的索引。
  • int lastIndexOf(String str,int fromIndex):返回指定子字符串在此字符串红最后一次出现的索引,从指定的索引开始反向搜索。
    • 注意:indexOf和lastIndexOf方法如果未找到都是返回-1。
  • String replace(char oldChar,char newChar):返回一个新的字符串,它是通过用newChar替换此字符串中出现的所有oldChar得到的。
  • String replace(CharSequence target,CharSequence replacement):使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
  • String replaceAll(String regex,String replacement):使用给定的replacement替换此字符串所有匹配给定的正则表达式的子字符串。
  • String replaceFirst(String regex,String replacement):使用给定的replacement替换此字符串匹配给定的正则表达式的第一个子字符串。
  • boolean matches(String regex):告知此字符串是否匹配给定的正则表达式。
  • String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。
  • String[] split(String regex,int limit):根据匹配给定的正在表达式来拆分此字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中。
@Test
public void test01(){
    //将用户输入的单词全部转为小写,如果用户没有输入单词,重新输入
    Scanner input = new Scanner(System.in);
    String word;
    while(true){
        System.out.print("请输入单词:");
        word = input.nextLine();
        if(word.trim().length()!=0){
            word = word.toLowerCase();
            break;
        }
    }
    System.out.println(word);
}

@Test
public void test02(){
    //随机生成验证码,验证码由0-9,A-Z,a-z的字符组成
    char[] array = new char[26*2+10];
    for (int i = 0; i < 10; i++) {
        array[i] = (char)('0' + i);
    }
    for (int i = 10,j=0; i < 10+26; i++,j++) {
        array[i] = (char)('A' + j);
    }
    for (int i = 10+26,j=0; i < array.length; i++,j++) {
        array[i] = (char)('a' + j);
    }
    String code = "";
    Random rand = new Random();
    for (int i = 0; i < 4; i++) {
        code += array[rand.nextInt(array.length)];
    }
    System.out.println("验证码:" + code);
    //将用户输入的单词全部转为小写,如果用户没有输入单词,重新输入
    Scanner input = new Scanner(System.in);
    System.out.print("请输入验证码:");
    String inputCode = input.nextLine();

    if(!code.equalsIgnoreCase(inputCode)){
        System.out.println("验证码输入不正确");
    }
}

@Test
public void test01(){
    String str = "尚硅谷是一家靠谱的培训机构,尚硅谷可以说是IT培训的小清华,JavaEE是尚硅谷的当家学科,尚硅谷的大数据培训是行业独角兽。尚硅谷的前端和UI专业一样独领风骚。";
    System.out.println("是否包含清华:" + str.contains("清华"));
    System.out.println("培训出现的第一次下标:" + str.indexOf("培训"));
    System.out.println("培训出现的最后一次下标:" + str.lastIndexOf("培训"));
}

@Test
public void test01(){
    String str = "helloworldjavaatguigu";
    String sub1 = str.substring(5);
    String sub2 = str.substring(5,10);
    System.out.println(sub1);
    System.out.println(sub2);
}

@Test
public void test02(){
    String fileName = "快速学习Java的秘诀.dat";
    //截取文件名
    System.out.println("文件名:" + fileName.substring(0,fileName.lastIndexOf(".")));
    //截取后缀名
    System.out.println("后缀名:" + fileName.substring(fileName.lastIndexOf(".")));
}

@Test
public void test01(){
    //将字符串中的字符按照大小顺序排列
    String str = "helloworldjavaatguigu";
    char[] array = str.toCharArray();
    Arrays.sort(array);
    str = new String(array);
    System.out.println(str);
}

@Test
public void test02(){
    //将首字母转为大写
    String str = "jack";
    str = Character.toUpperCase(str.charAt(0))+str.substring(1);
    System.out.println(str);
}
@Test
public void test03(){
    char[] data = {'h','e','l','l','o','j','a','v','a'};
    String s1 = String.copyValueOf(data);
    String s2 = String.copyValueOf(data,0,5);
    int num = 123456;
    String s3 = String.valueOf(num);

    System.out.println(s1);
    System.out.println(s2);
    System.out.println(s3);
}
@Test
public void test1(){
    String name = "张三";
    System.out.println(name.startsWith("张"));
}

@Test
public void test2(){
    String file = "Hello.txt";
    if(file.endsWith(".java")){
        System.out.println("Java源文件");
    }else if(file.endsWith(".class")){
        System.out.println("Java字节码文件");
    }else{
        System.out.println("其他文件");
    }
}
@Test
public void test1(){
    String str1 = "hello244world.java;887";
    //把其中的非字母去掉
    str1 = str1.replaceAll("[^a-zA-Z]", "");
    System.out.println(str1);

    String str2 = "12hello34world5java7891mysql456";
    //把字符串中的数字替换成,,如果结果中开头和结尾有,的话去掉
    String string = str2.replaceAll("\\d+", ",").replaceAll("^,|,$", "");
    System.out.println(string);

}

String与其他类型结构间的转换

字符串 --> 基本数据类型、包装类:

  • Integer包装类的public static int parseInt(String s):可以将由“数字”字符组成的字符串转换为整型。
  • 类似地,使用java.lang包中的Byte、Short、Long、Float、Double类调相应的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型。

基本数据类型、包装类 --> 字符串:

  • 调用String类的public String valueOf(int n)可将int型转换为字符串
  • 相应的valueOf(byte b)、valueOf(long l)、valueOf(float f)、valueOf(double d)、valueOf(boolean b)可由参数的相应类型到字符串的转换。

字符数组 --> 字符串:

  • String 类的构造器:String(char[]) 和 String(char[],int offset,int length) 分别用字符数组中的全部字符和部分字符创建字符串对象。

字符串 --> 字符数组:

  • public char[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法。

  • public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin):提供了将指定索引范围内的字符串存放到数组中的方法。

字符串 --> 字节数组:(编码)

  • public byte[] getBytes() :使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。
  • public byte[] getBytes(String charsetName) :使用指定的字符集将此 String 编码到 byte 序列,并将结果存储到新的 byte 数组。

字节数组 --> 字符串:(解码)

  • String(byte[]):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
  • String(byte[],int offset,int length) :用指定的字节数组的一部分,即从数组起始位置offset开始取length个字节构造一个字符串对象。
  • String(byte[], String charsetName ) 或 new String(byte[], int, int,String charsetName ):解码,按照指定的编码方式进行解码。
@Test
public void test2(){
    //String 转基本数据类型
    String str1 = "123";
    int i = Integer.parseInt(str1);
    System.out.println(i);

    //基本数据类型转字符串
    String s = String.valueOf(i);
    String s1 = i + "";
    System.out.println(s);
    System.out.println(s1);
}

@Test
public void test01() throws Exception {
    String str = "中国";
    System.out.println(str.getBytes("ISO8859-1").length);// 2
    // ISO8859-1把所有的字符都当做一个byte处理,处理不了多个字节
    System.out.println(str.getBytes("GBK").length);// 4 每一个中文都是对应2个字节
    System.out.println(str.getBytes("UTF-8").length);// 6 常规的中文都是3个字节

    /*
     * 不乱码:(1)保证编码与解码的字符集名称一样(2)不缺字节
     */
    System.out.println(new String(str.getBytes("ISO8859-1"), "ISO8859-1"));// 乱码
    System.out.println(new String(str.getBytes("GBK"), "GBK"));// 中国
    System.out.println(new String(str.getBytes("UTF-8"), "UTF-8"));// 中国
}
  • 字符数组转换为字符串

    • String类的构造器:String(char[])和String(char[],int offset,int length)分别用字符数组中的全部字符和部分字符创建字符串对象。
  • 字符串转换为字符数组

    • public chat[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法。
    • public void getChars(int srcBegin,int srcEnd,char[] dst,int dstBegin):提供了将指定索引范围内的字符串存放到数组中的方法。
@Test
public void test3(){
    //字符串转char[]
    String str1 = "hello";
    char[] c = str1.toCharArray();
    for (int i = 0; i < c.length; i++) {
        System.out.println(c[i]);
    }

    //char[]转字符串
    char arr[] = new char[]{'w', 'o', 'r', 'l', 'd'};
    String str2 = new String(arr);
    System.out.println(str2);
}
  • 字节数组转换为字符串
    • String(byte[]):通过使用平台的默认字符集解码指定的byte数组,构造一个新的String。
    • String(byte[],int offset,int length):用指定的字节数组的一部分,即从数组起始位置offset开始取length哥字节构造一哥字符串对象。
  • 字符串转换为字节数组
    • public byte[] getBytes():使用平台默认的字符集将此String编码为byte序列,并将结果存储到一个新的byte数组中。
    • public byte[] getBytes(String charsetName):使用指定的字符集将此String编码到byte序列,并将结果存放到新的byte数组中。
@Test
public void test4() throws UnsupportedEncodingException {
    System.out.println("*************编码************");
    //字符串转byte[]
    String str1 = "abcd中国";
    byte[] b1 = str1.getBytes(); //使用默认字符集,进行编码
    System.out.println(Arrays.toString(b1));

    byte[] gbks = str1.getBytes("GBK");  //使用指定字符集编码,进行编码
    System.out.println(Arrays.toString(gbks));

    System.out.println("***************解码*****************");
    String str2 = new String(b1);  //使用默认的字符集,进行解码
    System.out.println(str2);

    String str3 = new String(gbks);  //中文乱码,原因:解码和编码字符集不一致
    System.out.println(str3);

    String gbk = new String(gbks, "GBK");  //指定字符集进行解码,编码解码保持一致
    System.out.println(gbk);
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容