每天都在用String,你真的了解吗?

1.String概述

java.lang.String 类代表字符串。Java程序中所有的字符串文字(例如"abc")都可以被看作是实现此类的实例

String 中包括用于检查各个字符串的方法,比如用于比较字符串,搜索字符串,提取子字符串以及创建具有翻译为大写或小写的所有字符的字符串的副本。

2.String源码分析

2.1.String成员变量

// String的属性值,String的内容本质上是使用不可变的char类型的数组来存储的。
private final char value[];

/*String类型的hash值,hash是String实例化对象的hashcode的一个缓存值,这是因为String对象经常被用来进行比较,如果每次比较都重新计算hashcode值的话,是比较麻烦的,保存一个缓存值能够进行优化 */
private int hash; // Default to 0

//serialVersionUID为序列化ID
private static final long serialVersionUID = -6849794470754667710L;

//serialPersistentFields属性用于指定哪些字段需要被默认序列化
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];

serialPersistentFields具体用法为:

private static final ObjectStreamField[] serialPersistentFields = {
    new ObjectStreamField("name", String.class),
    new ObjectStreamField("age", Integer.Type)
}

transient用于指定哪些字段不会被默认序列化,两者同时使用时,transient会被忽略。

在 Java 9 及之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder来标识使用了哪种字符集编码。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final byte[] value;

    /** The identifier of the encoding used to encode the bytes in {@code value}. */
    private final byte coder;
}

2.2.String构造方法

1、空参构造

/**
* final声明的 value数组不能修改它的引用,所以在构造函数中一定要初始化value属性
*/
public String() {
	this.value = "".value;
}

2、用一个String来构造

// 初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。 
public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

3、使用char数组构造

// 分配一个新的 String,使其表示字符数组参数中当前包含的字符序列。
public String(char value[]) {
    this.value = Arrays.copyOf(value, value.length);
}

// 分配一个新的 String,它包含取自字符数组参数一个子数组的字符。 
public String(char value[], int offset, int count) 

4、使用int数组构造

// 分配一个新的 String,它包含 Unicode 代码点数组参数一个子数组的字符。 
public String(int[] codePoints, int offset, int count) 

5、使用byte数组构造

// 通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
public String(byte bytes[]) 
    
// 通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。 
public String(byte[] bytes) 

// 通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 String。  
public String(byte[] bytes, Charset charset) 

// 通过使用平台的默认字符集解码指定的 byte 子数组,构造一个新的 String。 
public String(byte[] bytes, int offset, int length) 

// 通过使用指定的 charset 解码指定的 byte 子数组,构造一个新的 String。
public String(byte[] bytes, int offset, int length, Charset charset) 
           
// 通过使用指定的字符集解码指定的 byte 子数组,构造一个新的 String。 
public String(byte[] bytes, int offset, int length, String charsetName) 
         
//通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 String。 
public String(byte[] bytes, String charsetName) 
          

6、使用StringBuffer或者StringBuilder构造

//分配一个新的字符串,它包含字符串缓冲区参数中当前包含的字符序列。 
public String(StringBuffer buffer) 
          
    
// 分配一个新的字符串,它包含字符串生成器参数中当前包含的字符序列。
public String(StringBuilder builder) 

3.字符串常量池

作为最基础的引用数据类型,Java 设计者为 String 提供了字符串常量池以提高其性能,那么字符串常量池的具体原理是什么?

3.1常量池的实现思想

  • 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能
  • JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
    • 为字符串开辟一个字符串常量池,类似于缓存区
    • 创建字符串常量时,首先查看字符串常量池是否存在该字符串
    • 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中
  • 实现的基础
    • 实现该优化的基础是因为字符串是不可变的,可以不用担心数据冲突进行共享
    • 运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收

3.2常量池的内存位置

    • 存储的是对象,每个对象都包含一个与之对应的class
    • JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
    • 对象的由垃圾回收器负责回收,因此大小和生命周期不需要确定
    • 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象)
    • 每个栈中的数据(原始类型和对象引用)都是私有的
    • 栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)
    • 数据大小和生命周期是可以确定的,当没有引用指向数据时,这个数据就会自动消失
  • 方法区
    • 静态区,跟堆一样,被所有的线程共享
    • 方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量

字符串常量池则存在于方法区

3.3案例分析

String str1 = "abc";
String str2 = "abc";
String str3 = "abc";
String str4 = new String("abc");
String str5 = new String("abc");
String str6 = new String("abc");

image-20200818054152095

变量str1到str6的内存分布如图所示;str1 = "abc"会先去常量池中看有没有abc,如果有则引用这个字符串,没有则创建一个;str2和str3都是直接引用常量池中的abc;

String str4 = new String("abc") 这段代码会做两步操作,第一步在常量池中查找是否有"abc"对象,有则返回对应的引用实例,没有则创建对应的实例对象;在堆中new一个String("abc")对象,将对象地址赋值给Str4,创建一个引用。

4.String内存分析

我们先来看一段代码

public class TestString {
    public static void main(String[] args) {
        String str1 = "wugongzi";
        String str2 = new String("wugongzi");
        String str3 = str2; //引用传递,str3直接指向st2的堆内存地址
        String str4 = "wugongzi";
        /**
         *  ==:
         * 基本数据类型:比较的是基本数据类型的值是否相同
         * 引用数据类型:比较的是引用数据类型的地址值是否相同
         * 所以在这里的话:String类对象==比较,比较的是地址,而不是内容
         */
         System.out.println(str1==str2);//false
         System.out.println(str1==str3);//false
         System.out.println(str3==str2);//true
         System.out.println(str1==str4);//true
    }
}

下面我们来分析一下这段代码的内存分布

image-20200818054357

第一步:String str1 = "wugongzi" ,首先会去常量池中看有没有wugongzi,发现没有,则在常量池中创建了一个wugongzi,然后将wugongzi的内存地址赋值给str1;

第二步:String str2 = new String("wugongzi"),这段代码因为new了一个String对象,它首先常量池中查找是否有wugongzi,发现已经有了,则返回对应的引用实例;然后再去堆中new一个String("wugongzi")对象,将对象地址赋值给Str2,创建一个引用。

第三步:String str3 = str2,// 引用传递,str3直接指向st2的堆内存地址;

第四步:String str4 = "wugongzi",同第一步

5.String常用方法

5.1.equals方法

这里重写了Object中的equals方法,用来判断两个对象实际意义上是否相等,也就是值是否相等

public boolean equals(Object anObject) {
    //如果引用的是同一个对象,则返回真
    if (this == anObject) {
        return true;
    }
    //如果不是String类型的数据,返回假
    if (anObject instanceof String) {
        String anotherString = (String) anObject;
        int n = value.length;
        //如果char数组长度不相等,返回假
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            //从后往前单个字符逐步判断,如果有不相等,则返回假
            while (n-- != 0) {
                if (v1[i] != v2[i])
                        return false;
                i++;
            }
            //每个字符都相等,则返回真
            return true;
        }
    }
    return false;
}

5.2.compareTo方法

用于比较两个字符串的大小,如果两个字符串长度相等则返回0,如果长度不相等,则返回当前字符串的长度减去被比较的字符串的长度。

public int compareTo(String anotherString) {
    //自身对象字符串长度len1
    int len1 = value.length;
    //被比较对象字符串长度len2
    int len2 = anotherString.value.length;
    //取两个字符串长度的最小值lim
    int lim = Math.min(len1, len2);
    char v1[] = value;
    char v2[] = anotherString.value;
 
    int k = 0;
    //从value的第一个字符开始到最小长度lim处为止,如果字符不相等,返回自身(对象不相等处字符-被比较对象不相等字符)
    while (k < lim) {
        char c1 = v1[k];
        char c2 = v2[k];
        if (c1 != c2) {
            return c1 - c2;
        }
        k++;
    }
    //如果前面都相等,则返回(自身长度-被比较对象长度)
    return len1 - len2;
}

5.3.hashCode方法

这里重写了hashCode方法,采用多项式进行计算,可以通过不同的字符串得到相同的hash,所以两个String对象的hashCode相同,并不代表两个String是相同的。

算法:假设n = 3

i=0 -> h = 31 * 0 + val[0]

i=1 -> h = 31 * (31 * 0 + val[0]) + val[1]

i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2]

 h = 31*31*31*0 + 31*31*val[0] + 31*val[1] + val[2]

 h = 31^(n-1)*val[0] + 31^(n-2)*val[1] + val[2]
public int hashCode() {
    int h = hash;
    //如果hash没有被计算过,并且字符串不为空,则进行hashCode计算
    if (h == 0 && value.length > 0) {
        char val[] = value;
 
        //计算过程
        //s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        //hash赋值
        hash = h;
    }
    return h;
}

5.4.startWith方法

startsWith和endWith方法也是比较常用的方法,常用来判断字符串以特定的字符开始或结尾。

public boolean startsWith(String prefix, int toffset) {
    char ta[] = value;
    int to = toffset;
    char pa[] = prefix.value;
    int po = 0;
    int pc = prefix.value.length;
    // Note: toffset might be near -1>>>1.
    //如果起始地址小于0或者(起始地址+所比较对象长度)大于自身对象长度,返回假
    if ((toffset < 0) || (toffset > value.length - pc)) {
        return false;
    }
    //从所比较对象的末尾开始比较
    while (--pc >= 0) {
        if (ta[to++] != pa[po++]) {
            return false;
        }
    }
    return true;
}
 
public boolean startsWith(String prefix) {
    return startsWith(prefix, 0);
}
 
public boolean endsWith(String suffix) {
    return startsWith(suffix, value.length - suffix.value.length);
}

5.5.concat方法

concat方法用于将指定的字符串参数连接到字符串上。

public String concat(String str) {
    int otherLen = str.length();
    //如果被添加的字符串为空,则返回对象本身
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

5.6.replace方法

replace的参数是char和charSequence,即可以支持字符的替换,也支持字符串的替换(charSequence即字符串序列的意思)

replaceAll的参数是regex,即基于规则表达式的替换,比如可以通过replaceAll("\d","*")把一个字符串所有的数字字符都替换成星号;

相同点:都是全部替换,即把源字符串中的某一字符或者字符串全部替换成指定的字符或者字符串。

不同点:replaceAll支持正则表达式,因此会对参数进行解析(两个参数均是),如replaceAll("\d",""),而replace则不会,replace("\d","")就是替换"\d"的字符串,而不会解析为正则。

public String replace(char oldChar, char newChar) {
    //新旧值先对比
    if (oldChar != newChar) {
        int len = value.length;
        int i = -1;
        char[] val = value; 
 
        //找到旧值最开始出现的位置
        while (++i < len) {
            if (val[i] == oldChar) {
                break;
            }
        }
        //从那个位置开始,直到末尾,用新值代替出现的旧值
        if (i < len) {
            char buf[] = new char[len];
            for (int j = 0; j < i; j++) {
                buf[j] = val[j];
            }
            while (i < len) {
                char c = val[i];
                buf[i] = (c == oldChar) ? newChar : c;
                i++;
            }
            return new String(buf, true);
        }
    }
    return this;
}

5.7.trim方法

trim用于删除字符串的头尾的空格。

public String trim() {
    int len = value.length;
    int st = 0;
    char[] val = value;    /* avoid getfield opcode */
 
    //找到字符串前段没有空格的位置
    while ((st < len) && (val[st] <= ' ')) {
        st++;
    }
    //找到字符串末尾没有空格的位置
    while ((st < len) && (val[len - 1] <= ' ')) {
        len--;
    }
    //如果前后都没有出现空格,返回字符串本身
    return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}

5.8.其他方法

//字符串是否包含另一个字符串
public boolean contains(CharSequence s)

//返回字符串长度
public int length()

//返回在指定index位置的字符,index从0开始
public char charAt(int index)

//返回str字符串在当前字符串首次出现的位置,若没有返回-1
public int indexOf(String str)

//返回str字符串最后一次在当前字符串中出现的位置,若无返回-1
public int lastIndexOf(String str)

//返回s字符串从当前字符串startpoint位置开始的,首次出现的位置
public int indexOf(String s ,int startpoint)

//返回s字符串从当前字符串startpoint位置开始的,最后一次出现的位置
public int lastIndexOf(String s ,int startpoint)

//返回从start开始的子串
public String substring(int startpoint)

//返回从start开始到end结束的一个左闭右开的子串。start可以从0开始的
public String substring(int start,int end)

//按照regex将当前字符串拆分,拆分为多个字符串,整体返回值为String[]
public String[] split(String regex)

6.String常用转化

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

调用相应的包装类的parseXxx(String str);

String str1 = "wugongzi";
int i = Integer.parseInt(str1);
System.out.println(i);

6.2字符串---->字节数组

调用字符串的getBytes()

String str = "wugongzi520";
byte[] b = str.getBytes();
for(int j = 0;j < b.length;j++){
    System.out.println((char)b[j]);
}

6.3字节数组---->字符串

调用字符串的构造器

String str = "wugongzi520";
byte[] b = str.getBytes();
String str3 = new String(b);
System.out.println(str3);

6.4字符串---->字符数组

调用字符串的toCharArray();

String str4 = "abc123";
char[] c = str4.toCharArray();
for(int j = 0;j < c.length;j++){
    System.out.println(c[j]);
}

6.5字符数组---->字符串

调用字符串的构造器

7.String长度

面试官:对String了解吗?

同学:非常熟悉,每天都会用到它。

面试官:String有长度限制吗?最大能存放多少

同学:这个没太注意过,我知道int的取值范围,String也有范围吗?

在学习和开发过程中,我们经常会讨论 short ,int 和 long 这些基本数据类型的取值范围,但是对于 String 类型我们好像很少注意它的“取值范围”。那么对于 String 类型,它到底有没有长度限制呢?

在日常开发中,大家可能会遇到String超出长度这样的情况,比如下面这种情况。

String长度

从String的源码我们可以看出

// String的属性值,String的内容本质上是使用不可变的char类型的数组来存储的。
private final char value[];

String实际存储数据的是char数组,数组长度是int类型,而int类型的最大值为2^31 - 1 = 2147483647,所以String最多存储231-1个字符(注意这里是字符,而不是字节),那既然String可以存储这么多的字符,为什么还会出现字符串过长呢?我明明没有放这么多数据啊。

关于这个问题,那就要从两方面去分析了:

7.1编译期

通过上面章节的学习,我们知道字符串会存放在字符串常量当中,String长度之所以会受限制,是因为JVM对常量池中的数据长度有限制。常量池中的每一种数据项都有自己的类型。Java中的UTF-8编码的Unicode字符串在常量池中以CONSTANT_Utf8类型表示。

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

我们可以看到length的类型是u2,u2是无符号的16位整数,最大值为2^16-1=65535,所以String在字符串常量池里的限制为65535个字节(注意这里是字节,而不是字符)

7.2运行期

String 运行时的限制主要体现在 String 的构造函数上。下面是 String 的一个构造函数:

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

上面的count值就是字符串的最大长度。在Java中,int的最大长度是2^31-1。所以在运行时,String 的最大长度是2^31-1。

但是这个也是理论上的长度,实际的长度还要看你JVM的内存。我们来看下,最大的字符串会占用多大的内存。

(2^31-1)*2*16/8/1024/1024/1024 = 4GB

所以在最坏的情况下,一个最大的字符串要占用4GB的内存。如果你的虚拟机不能分配这么多内存的话,会直接报错的。

JDK9以后对String的存储进行了优化。底层不再使用char数组存储字符串,而是使用byte数组。对于LATIN1字符的字符串可以节省一倍的内存空间。

7.3总结

String 的长度是有限制的。

  • 编译期的限制:字符串的UTF8编码值的字节数不能超过65535,字符串的长度不能超过65534;
  • 运行时限制:字符串的长度不能超过2^31-1,占用的内存数不能超过虚拟机能够提供的最大值。

参考:

https://segmentfault.com/a/1190000009888357

https://www.cnblogs.com/liudblog/p/11196293.html

posted @ 2020-08-18 09:19  五公子说  阅读(960)  评论(1编辑  收藏  举报