String是我们接触最多的类,无论是学习中还是工作中,基本每天都会和字符串打交道,从字符串本身的各种拼接、切片、变形,再到和其他基本数据类型的转换,几乎无时无刻都在使用它,今天就让我们揭开String神秘的面纱,这一小节主要讲解String的源代码是怎么构建的,下一节是String的一些疑点难点,通常在面试中会被问到。

  在学习String之前,让我们先简单的看一下JVM的内存模型,附图一张,来自百度百科:

在java的每一个通过编译生成字节码文件后,运行期JVM将字节码文件(也就是class文件)的对象通过类加载器加载到JVM运行区,如上图,通过线程独享和共享分为俩部分,其中线程共享区包括方法区和java堆,线程独享区包括虚拟机栈、本地方法栈和程序计数器

我们常说的基本类型在栈中,引用类型在堆中,其中的堆就是java堆(线程共享),栈就是虚拟机栈(线程独享),而String还涉及到方法区,因为String对象是常量,因为String其实就是char数组,而这个数组被final修饰了,详情在源代码中。

/*
*
* 在java程序中,所有字符串文字都是String的实例,字符串是不变的; 它们的值在创建后无法更改。
* 字符串缓冲区(StringBuffer和StringBuild)支持可变字符串。因为String对象是不可变的,所以它们可以共享。
* 例如:
* String str =“abc”;
* 相当于:
* char data [] = {'a','b','c'};
* String str = new String(data);
* String对象并不是简单的存在于堆内存中,而是在方法区的常量池中,我会在下一节详细讲解
*
*/ public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/*
* 该值用于字符存储。到这里就为什么String字符串会在常量池中我想大家一定豁然开朗了,因为final修饰的就是常量,而且不可修改,也就是说当我们定义一个
* String a = "gaoxiaolong"
* 从你的程序开始跑到最后结束运行,这个"gaoxiaolong"都一直存在,当然如果被当成垃圾回收那就另当别论了。而我们可以改变的仅仅是这个String的引用a的指向:
* a = "gollong",这是a的指向变了,但是"gaoxiaolong"仍然在常量池中,如果再有一个b引用,String b = "gaoxiaolong",这时这个"gaoxiaolong"
* 就不用再生成了,因为已经存在了。当然如果在这期间"gaoxiaolong"已经被回收就不是这么一回事了。
*/
private final char value[]; /*
* 缓存字符串的哈希码,默认为0
*/
private int hash; /*
* 实现Serializable(序列化)接口生成的序列化UID,用于序列化和反序列化识别此对象是String对象, 我会用一章来讲解序列化和反序列化
*/
private static final long serialVersionUID = -6849794470754667710L; /*
* Java.io.ObjectStreamField类是可序列化字段来自Serializable类的描述。 ObjectStreamFields数组用来声明一个类的序列化字段。
* 在序列化和反序列化中详细详细讲解
*/
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0]; /*
* 初始化新创建的String对象,使其表示空字符序列。 请注意,由于字符串是不可变的,因此不必使用此构造函数。
*/
public String() {
this.value = "".value;
} /*
* 初始化新创建的String对象,使其表示与参数相同的字符序列;
* 换句话说,新创建的字符串是参数字符串的副本。 除非需要original的明确副本,否则不必使用此构造函数,因为字符串是不可变的。
* 可见官方极力推崇String a = "gaoxiaolong"的写法,这种构造器仅仅是创建一个副本。其实这个副本是在堆内存中的一个引用指向方法区中的真正的"gaoxiaolong"
* 他们还是同一个"gaoxiaolong",只不过栈当中的地址值不同罢了
*/
public String(String original) {
this.value = original.value;
this.hash = original.hash;
} /*
* 比较常用的一种构造方法,可以把一个字符数组转化为字符串,其实就是将这个字符数组赋值给当前引用的char[] value属性
* 分配新的String,使其表示当前包含在字符数组参数中的字符序列。 复制字符数组的内容; 后续修改字符数组不会影响新创建的字符串。
* 内部调用 Arrays工具类中的copyOf方法
*/
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
/*
* 这个构造器看源码我们可以看到将char数组直接赋值给当前对象的value属性,区别于上一个构造器,上一个是完全复制一份数组,
* 相互之间不影响,而这个是俩个引用指向同一个数组,一个被改变另一个也会被改变。
* 但是这个构造器的访问权限是包访问权限,所以我们无法使用。不必纠结
*/
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
/*
* 分配一个新的String,其中包含字符数组参数的子数组中的字符。offset参数是子数组的第一个字符的索引,
* count参数指定子数组的长度。 复制子阵列的内容; 后续修改字符数组不会影响新创建的字符串。
* 从源码中我们可以看出一种特殊情况:如果count = 0且offset <= value.length那么实际上就是一个空字符串
* 这里一定要记住,第三个参数是个数,也就是长度,千万不要记成结束位置,切记切记!
* 这一点比较特殊,在字符串构造器中,都是offset和count参数,而在切片函数substring(int beginIndex, int endIndex)中
* 所需要的参数却是开始位置和结束位置,一定不要记混了。
*/
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// 如果offset+count的值超过的数组的长度自然报错,如果错误信息返回的是offset+count的值,则是这个错误。
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset + count);
} /*
*
*/
public String(int[] codePoints, int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= codePoints.length) {
this.value = "".value;
return;
}
}
if (offset > codePoints.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
} final int end = offset + count; // Pass 1: Compute precise size of char[]
int n = count;
for (int i = offset; i < end; i++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))
continue;
else if (Character.isValidCodePoint(c))
n++;
else
throw new IllegalArgumentException(Integer.toString(c));
} // Pass 2: Allocate and fill in char[]
final char[] v = new char[n]; for (int i = offset, j = 0; i < end; i++, j++) {
int c = codePoints[i];
if (Character.isBmpCodePoint(c))
v[j] = (char) c;
else
Character.toSurrogates(c, v, j++);
} this.value = v;
}
/*
* 检查边界的一个函数,在上面将字符数组转换为String对象时,我们见过了函数内的边界判断,由于关于字节的构造器过多,所以讲反复重用的代码封装成一个函数
* 到时候直接调用即可,这就是面向对象中封装的思想。
*/
private static void checkBounds(byte[] bytes, int offset, int length) {
if (length < 0)
throw new StringIndexOutOfBoundsException(length);
if (offset < 0)
throw new StringIndexOutOfBoundsException(offset);
if (offset > bytes.length - length)
throw new StringIndexOutOfBoundsException(offset + length);
}
/*
*
*/
@Deprecated
public String(byte ascii[], int hibyte) {
this(ascii, hibyte, 0, ascii.length);
} /*
* @Deprecated注解表示此方法已经过时,但仍然可以使用,只是不推荐
* ascii为要转换为字符的字节数组,hibyte为每个16位Unicode代码单元的前8位
* java使用的是Unicode编码表,每个字符都占俩个字节,但是一个字节只是一个字节,所以此方法在转换的时候
* 想指定另一个字节的数据来达到不同的需求,但是往往是不正确的转换,而使用指定charset完全可以达到此目的。
*/
@Deprecated
public String(byte ascii[], int hibyte, int offset, int count) {
checkBounds(ascii, offset, count);
char value[] = new char[count]; if (hibyte == 0) {
for (int i = count; i-- > 0;) {
value[i] = (char) (ascii[i + offset] & 0xff);
}
} else {
hibyte <<= 8;
for (int i = count; i-- > 0;) {
value[i] = (char) (hibyte | (ascii[i + offset] & 0xff));
}
}
this.value = value;
} /*
* 下面的都是讲字节数组转换为字符串的构造器,也是方法的重载,为了适应不用的转换需求,不许参数为字节数组,
* 可变参数为开始位置offset,转换长度length,指定编码方式charset,其中charset有俩种指定方式,一种是出入String类型的charsetName
* 另一种是传入Charset类型的charset,如果不知道的话,offset默认为0,length默认为字节数组的长度。chatset默认为本地编码
*/
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
} /*
*
*/
public String(byte bytes[], int offset, int length, String charsetName) throws UnsupportedEncodingException {
if (charsetName == null)
throw new NullPointerException("charsetName");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charsetName, bytes, offset, length);
} /*
*
*/
public String(byte bytes[], int offset, int length, Charset charset) {
if (charset == null)
throw new NullPointerException("charset");
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(charset, bytes, offset, length);
} /*
*/
public String(byte bytes[], String charsetName) throws UnsupportedEncodingException {
this(bytes, 0, bytes.length, charsetName);
} /*
*
*/
public String(byte bytes[], Charset charset) {
this(bytes, 0, bytes.length, charset);
} /*
* 将字节数组转换为字符串,指定开始位置和长度。
*/
public String(byte bytes[], int offset, int length) {
checkBounds(bytes, offset, length);
this.value = StringCoding.decode(bytes, offset, length);
} /*
* 将字符串缓冲区对象转换成String对象,实际上就是将一个堆内存中的对象写入常量池中,
* 使用Arrays.copyOf复制,得到的是俩份完全独立的数组
* StringBuilder是线程不安全的,效率高
* StringBuffer是线程安全的,但代价就是效率低,效率低是相比于StringBuilder,它仍然比String快很多
*/
public String(StringBuffer buffer) {
synchronized (buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
}

最新文章

  1. Friendship
  2. linux包之procps之sysctl命令
  3. spring PropertyPlaceholderConfigurer 找不到配置文件原因
  4. gridview 一个列勾选框选中,同时选中同一行的另一列勾选框
  5. BZOJ 1878: [SDOI2009]HH的项链 离线树状数组
  6. Qt信号槽写法
  7. FitVids,一个轻视频插件,操作简单
  8. JavaScript中return的用法和this的用法详解
  9. JSON风格指南
  10. 济南清北学堂游记 Day 2.
  11. POLARDB &#183; 最佳实践 &#183; POLARDB不得不知道的秘密(二)
  12. [Swift]LeetCode4. 两个排序数组的中位数 | Median of Two Sorted Arrays
  13. Django之路12——form modelform formset modelformset的各种用法
  14. JavaWeb系列之八(Cookie&amp;amp;Session)
  15. Maven 构建
  16. leetcode231
  17. C#情怀与未来,怨天尤人还是抓住机会,能否跟上dnc新时代浪潮?
  18. sencha touch 问题汇总
  19. 【[IOI2005]Riv 河流】
  20. Linux基础之-正则表达式(grep,sed,awk)

热门文章

  1. 设计模式(四)The Factory Pattern 出厂模式
  2. 微信公众平台通用接口API指南
  3. 人猿方案Ubuntu这些软件的安装
  4. Full Stack developer and Fog Computing
  5. Expander
  6. 使用path制作各类型动画路径
  7. LINUX 蓝牙耳机的配置方法
  8. Gradle编译失败 generating the main dex list
  9. HTML5离线缓存攻击测试
  10. 微信小程序把玩(十)swiper组件