每天都在用String,你真的了解吗?
点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
作者 | wugongzi
来源 | urlify.cn/fABr6j
一、String概述
java.lang.String 类代表字符串。Java程序中所有的字符串文字(例如"abc")都可以被看作是实现此类的实例
String 中包括用于检查各个字符串的方法,比如用于比较字符串,搜索字符串,提取子字符串以及创建具有翻译为大写或小写的所有字符的字符串的副本。
二、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)
三、字符串常量池
作为最基础的引用数据类型,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");
变量str1到str6的内存分布如图所示;str1 = "abc"会先去常量池中看有没有abc,如果有则引用这个字符串,没有则创建一个;str2和str3都是直接引用常量池中的abc;
String str4 = new String("abc") 这段代码会做两步操作,第一步在常量池中查找是否有"abc"对象,有则返回对应的引用实例,没有则创建对应的实例对象;在堆中new一个String("abc")对象,将对象地址赋值给Str4,创建一个引用。
四、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
}
}
下面我们来分析一下这段代码的内存分布
第一步: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",同第一步
五、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 = 3131310 + 3131val[0] + 31val[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)
六、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字符数组---->字符串
调用字符串的构造器
粉丝福利:108本java从入门到大神精选电子书领取
???
?长按上方锋哥微信二维码 2 秒 备注「1234」即可获取资料以及 可以进入java1234官方微信群
感谢点赞支持下哈