前言
相信作为 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[www.michenggw.com], 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(www.dasheng178.com/ value, index, 0);
9}
maven 是 apache 的一个开源软件,纯 Java 编写的,专门用于管理 Java 项目的一个工具。
maven 就是一个工具而已,用不用都不耽误你刷刷的敲代码,那为什么我们还要学习它呢?
那肯定是有很多的好处啊,不知道大家有没有注意过,一个普通的 SSM 项目一般都会几十兆或上百兆,不要想太多,你写代码没多少,jar 包就占用了 90% 以上。
maven 管理项目的第一个好处就是节约内存,统一管理依赖,因为你每个项目都要引入这么多的 jar 包,而使用 maven 之后,一份 jar 包可以多次使用。
maven 还有一键构建的功能,想象一下给你一套源代码,你如何运行起来,要导入 eclipse 吧,要有 tomcat 吧,而我们安装了 maven 这个软件之后,只需要一个命令就可以搞定,不需要依赖外部的 eclipse 和 tomcat ,其实是 maven 自带这些插件。
其实,对于一般的开发来说,不用 maven 也完全 ok,那为什么大家还都说 maven 好呢,主要是应用在于大型项目开发中。
比如说某宝这样的互联网软件,系统架构一般都是按照业务逻辑分,不是我们传统说的 web ,service,dao 3 层模型开发。例如用户模块,支付模块,订单模块。不用说,不同模块之间肯定是需要相互调用,使用 maven 之后,一是方便模块之间的合并,二是方便模块之间的调用。
我们通过把开发好的模块打成 jar 包,放入仓库中,其它模块即可引用该模块。
工具类的技术你就是说出花来,还是要以实际应用为主,在使用 maven 搭建项目的时候有太多的坑,好在这些坑在网上都有答案,我这里主要提一个,那就 eclipse 版本的选择,不要选择老版本,用 16 年以后的。
剩下就是按照步骤一步一步的搭建,运行。我们使用 tomcat:run 这个命令来一键构建我们的项目。
想使用 maven 中的命令,那首先要安装 maven,配置环境变量。然后再到想操作的项目的目录中去即可。
maven 常见的命令。
tomcat:run 一键构建项目。
mvn clear 清理编译好的文件。
mvn compile 编译文件,只编译 main 中的文件,test 没有编译。
mvn test 编译并运行了 test 目录中的代码。
mvn package 项目打包。打成的包会放在 target 目录中。
mvn install 把项目打包发布到本地仓库,当项目是 Java 项目时,可以使用该命令,这样我们就可以在 pom.xml 中引用自己写的工具了。
maven 中主要的配置文件有两个,一个是 maven 软件的配置文件,在安装目录下 conf 中的 setting.xml 文件,这里主要是定义本地仓库的位置。而另一个 pom.xml 文件是 maven 项目的依赖管理文件。管理的不只是 jar 包,还有各种插件。
说到仓库,maven 中共有 3 个仓库,本地仓库;就是我们在 setting.xml 中设置的位置。远程仓库;也称私服,是由公司运维人员维护的一个仓库,就是一个放在公司服务器上的 jar 包文件夹,可以理解为是一个“jar 包数据库”。最后是中央仓库;这是由 maven 团队维护的。
这 3 个仓库的关系一句形象的的描述就是 ” 大河一直有水,小河不会干。“ 大河就是指中央仓库,小河就是我们本地的仓库,那为什么还要来个私服呢?因为我们公司自己也会有产出呀,会写一些工具类或是固定的模块,打成 jar 包放在私服之后,我们便可以随时随用。
我们知道在 pom.xml 文件中,像这样就可以引入 jar 包。
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api<yongshiyule178.com /artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
maven 是如何查找 jar 包呢?根据坐标,哪个组织或公司的哪个项目的什么版本。这也就对应上面的 groupId artifactId 和 version。
还有一个 scope 是什么意思呢?代表的是 jar 包的应用范围,一个项目从源码到运行会存在编译,测试,运行这几个阶段,而 scope 就是对 jar 包在不同阶段是否存在做控制。
还有一个问题,我们需要用到 XXX jar 包时,我们该怎么做呢?若是你清楚的知道哪个公司哪个项目和版本,我们可以在 pom.xml 文件中,右击- maven - add repository 来添加。
在添加之前我们需要为本地仓库创建索引,不然是找不到 jar 包的,创建索引的方式 window - show view - other - Maven Repositories - Local Repository 右键 rebuild index。
还有另一种更方便的方式,直接网上找一下 jar 包的坐标,然后自己整理一份 pom.xml 需要的时候直接拿来用。
下面是使用 Eclipse 搭建一个 Maven 项目的具体步骤。
https://blog.csdn.net/yujikui1/article/details/84632917
1public byte[] getBytes(String charsetName)
2 throws UnsupportedEncodingException {
3 if (charsetName == null) throw new NullPointerException();
4 return StringCoding.encode(charsetName, value, www.gouyiflb.cn, 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(www.mhylpt.com) {
2 return StringCoding.encode(value, 0, value.length);
3}
简单的总结
String 被 修饰符 final 修饰,是无法被继承的,不可变类
String 实现 Serializable 接口,可以被序列化
String 实现 Comparable 接口,可以用于比较大小
String 实现 CharSequence 接口,表示一直有序字符序列,实现了通用的字符序列方法
String 是一个字符序列,内部数据结构其实是一个字符数组,所有的操作方法都是围绕这个字符数组的操作。
String 中频繁使用到了 System 类的 arraycopy 方法,目的是拷贝字符数组