Java基础--03--String

10 篇文章 0 订阅

一、String底层结构改变--为节约内存

//JDK8及之前
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
    private final char value[];

    ...
}

//JDK9及之后
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
    private final byte value[];

    ...
}
  1. JDK8及之前,String类的当前实现是将字符存储在char数组中,每个字符使用两个字节(16位)。
  2. 实践发现,字符串是堆的主要组成部分(占比25%)+大多数字符串对象只包含拉丁字符(Latin-1),这些字符只需要一个字节的存储空间,因此这些字符串对象的内部char数组中有一半的空间将不会使用,产生了大量浪费,因此JDK9及以后改为byte数组。
  3. 之前 String 类使用 UTF-16 的 char[] 数组存储,现在改为 byte[] 数组 外加一个编码标识存储。该编码表示如果你的字符是ISO-8859-1或者Latin-1,那么只需要一个字节存。如果你是其它字符集,比如UTF-8,仍然用两个字节存。
  4. 同时基于String的数据结构,例如StringBuffer和StringBuilder也同样做了修改。

String:不可变字符串

StringBuilder:可变字符串

StringBuffer:可变字符串+线程安全(各方法添加synchronized)

1.1 一个面试题:jdk8中,一个字符串包含了汉字,计算该字符串占用多少个字节?

在Java中:
1字符=2字节,1字节=8位
英文和数字占一个字节,中文占2个字节。

如果直接使用str.length()计算字符串占用多少个字节,得出的长度往往是不准确的,例如:

public static void main(String[] args) {
		String str= "Great大中国";
		int length = str.length();
		System.out.println(length);
}

计算结果为8,是错误的。

正确计算方法如下:

/**
     * 计算字符串占用了多少个字节
     * 1字符=2字节,1字节=8位
     * 英文和数字占一个字节,中文占2个字节。
     */
    public static int getStrlength(String str) {
        int strLength = 0;
        //chinese表示常见的汉字【一-龥】
        String chinese = "[\u4e00-\u9fa5]";
        /* 获取字段值的长度,如果含中文字符,则每个中文字符长度为2,否则为1 */
        for (int i = 0; i < str.length(); i++) {
            /* 从字符串中获取一个字符或汉字 */
            String temp = str.substring(i, i + 1);
            /* 判断是否为中文字符 */
            if (temp.matches(chinese)) {
                /* 中文字符长度为2 */
                strLength += 2;
            } else {
                /* 其他字符长度为1 */
                strLength += 1;
            }
        }
        return strLength;
    }

二、String长度限制

  • 编译期的限制:字符串的长度不能超过65534。
  • 运行时限制:字符串的长度不能超过2^31-1。

长度:指String.length()的值。String底层不管是byte数组,还是char数组,都不会影响长度,会影响的是占用的内存空间。

    public static void main(String[] args) {
        String str="abcde";
        System.out.println(str.length());//5
    }

2.1 编译期限制说明

    public static void main(String[] args) {
        String str="abcde";
        System.out.println(str);
    }

如上定义的字符串常量“abcde”会被放入方法区的常量池中,编译期Stirng 长度之所以会受限制,是因JVM规范对常量池有所限制。常量池中的每一种数据项都有自己的类型,Java中的UTF-8编码的Unicode字符串在常量池中以CONSTANT_Utf8类型表示,CONSTANT_Utf8的数据结构如下:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

u1 bytes[length]这个数组就是真正存储常量数据的地方,而 length 就是数组可以存储的最大长度。length 的类型是u2,u2是无符号的16位整数,2^16-1=65535,所以上面byte数组的最大长度是65535。

2.1 运行期限制说明

public String(char value[], int offset, int count) {

...
}

运行时的限制主要体现在 String 的构造函数上,count值就是字符串的最大长度。在Java中,int的最大长度是2^31-1,所以在运行时,String 的最大长度是2^31-1。

2^31-1对应占用的内存大小:

JDK8中LATIN1字符占用的内存=(2^31-1)*16/8/1024/1024/1024 = 4GB【长度*每个长度对应的位数/8/1024/1024/1024】

JDK9中LATIN1字符占用的内存=(2^31-1)*8/8/1024/1024/1024=2GB

三、对String不可变性的理解--【String代表不可变的字符序列,简称:不可变性】

1、通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。

2、当对字符串重新赋值、replace()方法替换、拼接等任何形式的修改时,需要重新分配内存区域赋值,不能使用原有的value进行赋值。

举例1:

    @Test
    public void test(){
        String str1="abc";//字面量定义的方式,"abc"存储在字符串常量池中
        String str2=new String("abc");
        String str3=new String("cde");//共创建2个对象:在常量池中创建字符串cde+在堆中创建一个String对象str3

        System.out.println(str1==str2);//false
    }

例1图分析

举例2:

    @Test
    public void test(){
        String s1 = "abc";//字面量定义的方式,"abc"存储在字符串常量池中
        String s2 = "abc";
        s1 = "hello";

        System.out.println(s1 == s2);// false

        System.out.println(s1);//hello
        System.out.println(s2);//abc
    }

例2图示分析

举例3:

public class StrDemo {
    String str = new String("good");
    char[] ch = {'t', 'e', 's', 't'};

    public void change(String str, char ch[]) {
        str = "test ok";
        ch[0] = 'b';
    }

    public static void main(String[] args) {
        StrDemo ex = new StrDemo();
        ex.change(ex.str, ex.ch);
        System.out.println(ex.str);//good
        System.out.println(ex.ch);//best
    }
}

例3图示分析

四、字符串拼接

字符串拼接结论:

  1. 常量与常量的拼接结果在常量池,原理是编译期优化。
  2. 拼接前后,只要其中有一个是变量,结果就在堆中。变量拼接的原理是StringBuilder。

4.1 字符串拼接的底层原理

@Test
    public void test3(){
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        /*
        如下的s1 + s2 的执行细节:(变量s是我临时定义的)
        ① StringBuilder s = new StringBuilder();
        ② s.append("a")
        ③ s.append("b")
        ④ s.toString()  --> 约等于 new String("ab"),但不等价,原因是:存在字符串字面量时,才会在常量池生成

        补充:在jdk5.0之后使用的是StringBuilder,在jdk5.0之前使用的是StringBuffer
         */
        String s4 = s1 + s2;//
        System.out.println(s3 == s4);//false
    }

图解

4.2 举例

举例1--new String(“a”) + new String(“b”) 会创建几个对象?

 *  对象1:new StringBuilder()  //字符串拼接原理是StringBuilder,所以需要该对象
 *  对象2: new String("a")
 *  对象3: 常量池中的"a"
 *  对象4: new String("b")
 *  对象5: 常量池中的"b"
 *  对象6-str: StringBuilder的toString()会new一个String对象
 *
 *       强调一下,toString()的调用,在字符串常量池中,没有生成"ab",“ab”在堆中
 *
 */
public class StringNewTest {
    public static void main(String[] args) {

        String str = new String("a") + new String("b");
    }
}

举例2--常量拼接

@Test
    public void test1(){
        String s1 = "a" + "b" + "c";//编译期优化:等同于"abc"
        String s2 = "abc"; //"abc"一定是放在字符串常量池中,将此地址赋给s2
        /*
         * java编译成.class再执行.class,编译后的结果如下:
         * String s1 = "abc";
         * String s2 = "abc"
         */
        System.out.println(s1 == s2); //true
        System.out.println(s1.equals(s2)); //true
    }

举例3--常量拼接

  1. 在 Java 中使用 final 关键字来修饰常量。
  2. 字符串字面量也在常量池中。
    @Test
    public void test4(){
        final String s1 = "a";//使用final修饰的s1为常量
        final String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
        System.out.println(s3 == s4);//true
    }

举例3的汇编代码

0 ldc #14 <a>
2 astore_1
3 ldc #15 <b>
5 astore_2
6 ldc #16 <ab>
8 astore_3
9 ldc #16 <ab>     //s4在编译后直接优化为使用ab字面量赋值
11 astore 4
13 getstatic #3 <java/lang/System.out>
16 aload_3
17 aload 4
19 if_acmpne 26 (+7)
22 iconst_1
23 goto 27 (+4)
26 iconst_0
27 invokevirtual #4 <java/io/PrintStream.println>
30 return

举例4--字符串拼接

    @Test
    public void test2(){
        String s1 = "javaEE";
        String s2 = "hadoop";

        String s3 = "javaEEhadoop";
        String s4 = "javaEE" + "hadoop";//编译期优化
        //如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体的内容为拼接的结果:javaEEhadoop
        String s5 = s1 + "hadoop";
        String s6 = "javaEE" + s2;
        String s7 = s1 + s2;

        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
    }

结合jvm一起学习:JVM-01-JVM基础-03-运行时常量池

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值