String 源码浅析(一)
前言
相信作为 JAVAER
,平时编码时使用最多的必然是 String
字符串,而相信应该存在不少人对于 String
的 api
很熟悉了,但没有看过其源码实现,其实我个人觉得对于 api 的使用,最开始的阶段是看其官方文档,而随着开发经验的积累,应当尝试去看源码实现,这对自身能力的提升是至关重要的。当你理解了源码之后,后面对于 api 的使用也会更加得心应手!
备注:以下记录基于 jdk8 环境
String 只是一个类
String
其实只是一个类,我们大致可以从以下几个角度依次剖析它:
- 类继承关系
- 类成员变量
- 类构造方法
- 类成员方法
- 相关静态方法
继承关系
从 IDEA
自带插件导出 String
的 UML 类图如下:
从图中马上可以看出,String
实现了接口 Serializable
,Comparable
,CharSequence
,简单介绍一下这三个接口的作用:
Serializable
:实现该接口的类将具备序列化的能力,该接口没有任何实现,仅仅是一直标识作用。Comparable
:实现此接口的类具备比较大小的能力,比如实现此接口的对象的列表(和数组)可以由Collections
类的静态方法sort
进行自动排序。CharSequence
:字符序列统一的我接口。提供字符序列通用的操作方法,通常是一些只读方法,许多字符相关的类都实现此接口,以达到对字符序列的操作,比如:String
,StringBuffer
等。
String
类定义如下:
1public final class String
2 implements java.io.Serializable, Comparable<String>, CharSequence{
3 ...
4 }
由 final
修饰符可知, String
类是无法被继承,不可变类。
类成员变量
这里主要介绍最关键的一个成员变量 value[]
,其定义如下:
1 /** The value is used for character storage. */
2 private final char value[];
String
是一个字符串,由字符 char
所组成,因此实际上 String
内部其实就是一个字符数组,用 value[]
表示,注意这里的 value[] 是用 final 修饰的,表示该值是不允许修改的。
类构造方法
String
有很多重载的构造方法,介绍如下:
空参数构造方法,初始化字符串实例,默认为空字符,理论上不需要用到这个构造方法,实际上定义一个空字符
String = ""
就会初始化一个空字符串的String
对象,而此构造方法,也是把空字符的value[]
拷贝一遍而已,源码实现如下:1 public String() {
2 this.value = "".value;
3}通过一个字符串参数构造
String
对象,实际上 将形参的value
和hash
赋值给实例对象作为初始化,相当于深拷贝了一个形参String
对象,源码如下:1 public String(String original) {
2 this.value = original.value;
3 this.hash = original.hash;
4 }通过字符数组去构建一个新的
String
对象,这里使用Arrays.copyOf
方法拷贝字符数组1 public String(char value[]) {
2 this.value = Arrays.copyOf(value, value.length);
3 }在源字符数组基础上,通过偏移量(起始位置)和字符数量,截取构建一个新的
String
对象。1public String(char value[], int offset, int count) {
2 //如果偏移量小于0,则抛出越界异常
3 if (offset < 0) {
4 throw new StringIndexOutOfBoundsException(offset);
5 }
6 if (count <= 0) {
7 //如果字符数量小于0,则抛出越界异常
8 if (count < 0) {
9 throw new StringIndexOutOfBoundsException(count);
10 }
11 //在截取的字符数量为0的情况下,偏移量在字符串长度范围内,则返回空字符
12 if (offset <= value.length) {
13 this.value = "".value;
14 return;
15 }
16 }
17 // Note: offset or count might be near -1>>>1.
18 //如果偏移量大于字符总长度-截取的字符长度,则抛出越界异常
19 if (offset > value.length - count) {
20 throw new StringIndexOutOfBoundsException(offset + count);
21 }
22 //使用Arrays.copyOfRange静态方法,截取一定范围的字符数组,从offset开始,长度为offset+count,赋值给当前实例的字符数组
23 this.value = Arrays.copyOfRange(value, offset, offset+count);
24 }在源整数数组的基础上,通过偏移量(起始位置)和字符数量,截取构建一个新的
String
对象。这里的整数数组表示字符对应的ASCII整数值1 public String(int[] codePoints, int offset, int count) {
2 //如果偏移量小于0,则抛出越界异常
3 if (offset < 0) {
4 throw new StringIndexOutOfBoundsException(offset);
5 }
6 if (count <= 0) {
7 //如果字符数量小于0,则抛出越界异常
8 if (count < 0) {
9 throw new StringIndexOutOfBoundsException(count);
10 }
11 //在截取的字符数量为0的情况下,偏移量在字符串长度范围内,则返回空字符
12 if (offset <= codePoints.length) {
13 this.value = "".value;
14 return;
15 }
16 }
17 // Note: offset or count might be near -1>>>1.
18 如果偏移量大于字符总长度-截取的字符长度,则抛出越界异常
19 //if (offset > codePoints.length - count) {
20 throw new StringIndexOutOfBoundsException(offset + count);
21 }
22 final int end = offset + count;
23 // 这段逻辑是计算出字符数组的精确大小n,过滤掉一些不合法的int数据
24 int n = count;
25 for (int i = offset; i < end; i++) {
26 int c = codePoints[i];
27 if (Character.isBmpCodePoint(c))
28 continue;
29 else if (Character.isValidCodePoint(c))
30 n++;
31 else throw new IllegalArgumentException(Integer.toString(c));
32 }
33 // 按照上一步骤计算出来的数组大小初始化数组
34 final char[] v = new char[n];
35 //遍历填充字符数组
36 for (int i = offset, j = 0; i < end; i++, j++) {
37 int c = codePoints[i];
38 if (Character.isBmpCodePoint(c))
39 v[j] = (char)c;
40 else
41 Character.toSurrogates(c, v, j++);
42 }
43 //赋值给当前实例的字符数组
44 this.value = v;
45}通过源字节数组,按照一定范围,从offset开始截取length个长度,初始化
String
实例,同时可以指定字符编码。1public String(byte bytes[], int offset, int length, String charsetName)
2 throws UnsupportedEncodingException {
3 //字符编码参数为空,抛出空指针异常
4 if (charsetName == null)
5 throw new NullPointerException("charsetName");
6 //静态方法 检查字节数组的索引是否越界
7 checkBounds(bytes, offset, length);
8 //使用 StringCoding.decode 将字节数组按照一定范围解码为字符串,从offset开始截取length个长度
9 this.value = StringCoding.decode(charsetName, bytes, offset, length);
10}与第6个构造相似,只是编码参数重载为
Charset
类型1 public String(byte bytes[], int offset, int length, Charset charset) {
2 if (charset == null)
3 throw new NullPointerException("charset");
4 checkBounds(bytes, offset, length);
5 this.value = StringCoding.decode(charset, bytes, offset, length);
6}通过源字节数组,构造一个字符串实例,同时指定字符编码,具体实现其实是调用第6个构造器,起始位置为0,截取长度为字节数组长度
1 public String(byte bytes[], String charsetName)
2 throws UnsupportedEncodingException {
3 this(bytes, 0, bytes.length, charsetName);
4}通过源字节数组,构造一个字符串实例,同时指定字符编码,具体实现其实是调用第7个构造器,起始位置为0,截取长度为字节数组长度
1 public String(byte bytes[], Charset charset) {
2 this(bytes, 0, bytes.length, charset);
3}通过源字节数组,按照一定范围,从offset开始截取length个长度,初始化
String
实例,与第六个构造器不同的是,使用系统默认字符编码1public String(byte bytes[], int offset, int length) {
2 //检查索引是否越界
3 checkBounds(bytes, offset, length);
4 //使用系统默认字符编码解码字节数组为字符数组
5 this.value = StringCoding.decode(bytes, offset, length);
6}通过源字节数组,构造一个字符串实例,使用系统默认编码,具体实现其实是调用第10个构造器,起始位置为0,截取长度为字节数组长度
1public String(byte bytes[]) {
2 this(bytes, 0, bytes.length);
3}将
StringBuffer
构建成一个新的String
,比较特别的就是这个方法有synchronized
锁 同一时间只允许一个线程对这个buffer
构建成String
对象,是线程安全的1 public String(StringBuffer buffer) {
2 //对当前 StringBuffer 对象加同步锁
3 synchronized(buffer) {
4 //拷贝 StringBuffer 字符数组给当前实例的字符数组
5 this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
6 }
7}将
StringBuilder
构建成一个新的String
,与第12个构造器不同的是,此构造器不是线程安全的1 public String(StringBuilder builder) {
2 this.value = Arrays.copyOf(builder.getValue(), builder.length());
3}
类成员方法
获取字符串长度,实际上是获取字符数组长度
1 public int length() {
2 return value.length;
3}判断字符串是否为空,实际上是盼复字符数组长度是否为0
1public boolean isEmpty() {
2 return value.length == 0;
3}根据索引参数获取字符
1 public char charAt(int index) {
2 //索引小于0或者索引大于字符数组长度,则抛出越界异常
3 if ((index < 0) || (index >= value.length)) {
4 throw new StringIndexOutOfBoundsException(index);
5 }
6 //返回字符数组指定位置字符
7 return value[index];
8}根据索引参数获取指定字符ASSIC码(int类型)
1 public int codePointAt(int index) {
2 //索引小于0或者索引大于字符数组长度,则抛出越界异
3 if ((index < 0) || (index >= value.length)) {
4 throw new StringIndexOutOfBoundsException(index);
5 }
6 //返回索引位置指定字符ASSIC码(int类型)
7 return Character.codePointAtImpl(value, index, value.length);
8}返回index位置元素的前一个元素的ASSIC码(int型)
1public int codePointBefore(int index) {
2 //获得index前一个元素的索引位置
3 int i = index - 1;
4 //检查索引是否越界
5 if ((i < 0) || (i >= value.length)) {
6 throw new StringIndexOutOfBoundsException(index);
7 }
8 return Character.codePointBeforeImpl(value, index, 0);
9}方法返回的是代码点个数,是实际上的字符个数,功能类似于length(),对于正常的String来说,length方法和codePointCount没有区别,都是返回字符个数。但当String是Unicode类型时则有区别了。例如:String str = “/uD835/uDD6B” (即使 'Z' ), length() = 2 ,codePointCount() = 1
1 public int codePointCount(int beginIndex, int endIndex) {
2 if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) {
3 throw new IndexOutOfBoundsException();
4 }
5 return Character.codePointCountImpl(value, beginIndex, endIndex - beginIndex);
6}也是相对Unicode字符集而言的,从index索引位置算起,偏移codePointOffset个位置,返回偏移后的位置是多少,例如,index = 2 ,codePointOffset = 3 ,maybe返回 5
1public int offsetByCodePoints(int index, int codePointOffset) {
2 if (index < 0 || index > value.length) {
3 throw new IndexOutOfBoundsException();
4 }
5 return Character.offsetByCodePointsImpl(value, 0, value.length,
6 index, codePointOffset);
7}这是一个不对外的方法,是给String内部调用的,因为它是没有访问修饰符的,只允许同一包下的类访问 参数:dst[]是目标数组,dstBegin是目标数组的偏移量,既要复制过去的起始位置(从目标数组的什么位置覆盖) 作用就是将String的字符数组value整个复制到dst字符数组中,在dst数组的dstBegin位置开始拷贝
1void getChars(char dst[], int dstBegin) {
2 System.arraycopy(value, 0, dst, dstBegin, value.length);
3}得到char字符数组,原理是getChars() 方法将一个字符串的字符复制到目标字符数组中。 参数:srcBegin是原始字符串的起始位置,srcEnd是原始字符串要复制的字符末尾的后一个位置(既复制区域不包括srcEnd) dst[]是目标字符数组,dstBegin是目标字符的复制偏移量,复制的字符从目标字符数组的dstBegin位置开始覆盖。
1public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
2 if (srcBegin < 0) {
3 throw new StringIndexOutOfBoundsException(srcBegin);
4 }
5 if (srcEnd > value.length) {
6 throw new StringIndexOutOfBoundsException(srcEnd);
7 }
8 if (srcBegin > srcEnd) {
9 throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
10 }
11 System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
12}获取字符串的字节数组,按照指定字符编码将字符串解码为字节数组
1public byte[] getBytes(String charsetName)
2 throws UnsupportedEncodingException {
3 if (charsetName == null) throw new NullPointerException();
4 return StringCoding.encode(charsetName, value, 0, value.length);
5}获取字符串的字节数组,按照指定字符编码将字符串解码为字节数组
1public byte[] getBytes(Charset charset) {
2 if (charset == null) throw new NullPointerException();
3 return StringCoding.encode(charset, value, 0, value.length);
4}获取字符串的字节数组,按照系统默认字符编码将字符串解码为字节数组
1 public byte[] getBytes() {
2 return StringCoding.encode(value, 0, value.length);
3}
简单的总结
String
被 修饰符final
修饰,是无法被继承的,不可变类String
实现Serializable
接口,可以被序列化String
实现Comparable
接口,可以用于比较大小String
实现CharSequence
接口,表示一直有序字符序列,实现了通用的字符序列方法String
是一个字符序列,内部数据结构其实是一个字符数组,所有的操作方法都是围绕这个字符数组的操作。String
中频繁使用到了System
类的arraycopy
方法,目的是拷贝字符数组
最后
由于篇幅原因,String
第一篇总结先到这里,后续部分将写另外写一遍记录,会第一时间推送公众号【张少林同学】,欢迎关注!
String 源码浅析(一)的更多相关文章
- String 源码浅析————终结篇
写在前面 说说这几天看源码的感受吧,其实 jdk 中的源码设计是最值得进阶学习的地方.我们在对 api 较为熟悉之后,完全可以去尝试阅读一些 jdk 源码,打开 jdk 源码后,如果你英文能力稍微过得 ...
- String源码浅析
如果问你,开发过程中用的最多的类是哪个?你可能回答是HashMap,一个原因就是HashMap的使用量的确很多,还有就是HashMap的内容在面试中经常被问起. 但是在开发过程中使用最多的类其实并不是 ...
- 我对java String的理解 及 源码浅析
摘要: 摘要: 原创出处: http://www.cnblogs.com/Alandre/ 泥沙砖瓦浆木匠 希望转载,保留摘要,谢谢! 每天起床告诉自己,自己的目标是 ”技术 + 英语 还有生活“! ...
- 【深入浅出jQuery】源码浅析--整体架构
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- 【深入浅出jQuery】源码浅析2--奇技淫巧
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- HashSet其实就那么一回事儿之源码浅析
上篇文章<HashMap其实就那么一回事儿之源码浅析>介绍了hashMap, 本次将带大家看看HashSet, HashSet其实就是基于HashMap实现, 因此,熟悉了HashMap ...
- 【深入浅出jQuery】源码浅析2--使用技巧
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- spring源码浅析——IOC
=========================================== 原文链接: spring源码浅析--IOC 转载请注明出处! ======================= ...
- java并发:jdk1.8中ConcurrentHashMap源码浅析
ConcurrentHashMap是线程安全的.可以在多线程中对ConcurrentHashMap进行操作. 在jdk1.7中,使用的是锁分段技术Segment.数据结构是数组+链表. 对比jdk1. ...
随机推荐
- 【HDU1542】Atlantis
题意 给出n个矩形的左下角和右上角的坐标,计算总的面积(相交部分只算一次). 分析 线段树扫描线的模板题. 将每个矩形都拆成上下两条线段,然后从下网上扫,当遇到底边时就加上这个区间,遇到顶边时,就减去 ...
- Docker03 Docker基础知识、Docker实战
1 Docker基础知识 1.1 什么是Docker Docker是一个可以装应用的容器,就像杯子可以装水.书包可以装书一样:docker官网 Docker是Docker公司开发的,并开源到GitHu ...
- Linux 下启动两个tomcat
Linux 下启动两个tomcat 闲来无事学习nginx,想要配置个load balance.可是先决条件是:得有两个web容器.两个电脑是不用想了.只能想办法在一个机器上启动两个tomcat.原以 ...
- ConcurrentHashMap的实现原理与使用
一.适应ConcurrentHashMap的原因 HashMap存在线程不安全的问题,HashTable效率十分低下,因此,ConcurrentHashMap有了合适的登场机会. (1)HashTab ...
- Java常用日志框架介绍(转)
Java常用日志框架介绍 java日志概述 对于一个应用程序来说日志记录是必不可少的一部分.线上问题追踪,基于日志的业务逻辑统计分析等都离不日志.java领域存在多种日志框架,目前常用的日志框架包括L ...
- Boost智能指针使用总结
内存管理是一个比较繁琐的问题,C++中有两个实现方案: 垃圾回收机制和智能指针.垃圾回收机制因为性能等原因不被C++的大佬们推崇, 而智能指针被认为是解决C++内存问题的最优方案. 1. 智能指针定义 ...
- python用sqlite3模块操作sqlite数据库-乾颐堂
SQLite是一个包含在C库中的轻量级数据库.它并不需要独立的维护进程,并且允许使用非标准变体(nonstandard variant)的SQL查询语句来访问数据库. 一些应用可是使用SQLite保存 ...
- CentOS-yum基本使用
CentOS: yum URL: ftp://172.16.0.1/pub/ YUM: yellow dog, Yellowdog Update Modifier yum repository: yu ...
- mysql:查询数据库版本的几种方式
Mysql版本: 登入数据库的时候: select @@version; select version(); mysql> select @@version; +-----------+ | @ ...
- Mybatis:传入参数方式以及#{}与${}的区别
一.在MyBatis的select.insert.update.delete这些元素中都提到了parameterType这个属性.MyBatis现在可以使用的parameterType有基本数据类型和 ...