前言:String类在日常开发过程中使用频率非常高,平时大家可能看过String的源码,但是真的认真了解过它么,笔者在一次笔试过程中要求写出String的equals方法,瞬间有点懵逼,凭着大致的理解,算是写出来了,可是下来一翻String的源码顿悟,原来自己写得是多么的low,所以有必要把这些基础知识点记录下来,加深印象。
注:本文jdk源码版本为jdk1.8.0_172
1.String类的基本概念
首先String是不可变对象,其体现主要在String类是被final关键字修饰,因此该对象不能被继承,不能被修改。那你可能要问了为什么我们在日常操作过程中不是可以很方便的修改String的内容吗?其实我们修改其内容是new了一个对象,原来的内容并没有改变。
String内部是通过char数组来存储的内容,从以下源码中可以发现:
注:这里的char数组也是被final修饰。
我们平常说,java中char占2个字节,可又说汉字在不通的编码格式中所占的位数是不同的,比如gbk中汉字占2个字节,utf8中多数占3个字节,少数占4个。而所有汉字在java程序中我们都可以简单的用 char c = '字' 表示; 那么问题来了,在java程序运行的时候,究竟汉字占几个字节呢?
1、java中内码(运行内存)中的char使用UTF16的方式编码,一个char占用两个字节,但是某些字符需要两个char来表示。所以,一个字符会占用2个或4个字节。
2、java中外码中char使用UTF8的方式编码,一个字符占用1~6个字节。
3、UTF16编码中,英文字符占两个字节;绝大多数汉字(尤其是常用汉字)占用两个字节,个别汉字(在后期加入unicode编码的汉字,一般是极少用到的生僻字)占用四个字节。
4、UTF8编码中,英文字符占用一个字节;绝大多数汉字占用三个字节,个别汉字占用四个字节。
2.String的构造函数
不知道你注意没,String的构造函数非常的多:
这里挑选几个笔者认为有特点的构造函数进行分析,其它构造函数请查看相应源码。
#1.默认构造函数:
1 public String() {
2 this.value = "".value;
3 }
注:默认构造函数的实现非常简单,就是返回空字符串的数组形式。
#2.入参为char数组:
1 public String(char value[]) {
2 this.value = Arrays.copyOf(value, value.length);
3 }
分析:如果传入char数组,是通过Arrays#copyOf方法进行拷贝的。
#3.入参为String对象:
1 public String(String original) {
2 this.value = original.value;
3 this.hash = original.hash;
4 }
分析:如果入参为String对象,则直接进行相应的赋值即可(value和hash值)。
#4.入参为StringBuffer:
1 public String(StringBuffer buffer) {
2 synchronized(buffer) {
3 this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
4 }
5 }
分析:这里对StringBuffer进行了加锁,然后再进行拷贝操作。为什么要对StringBuffer加锁呢?StringBuffer不是线程安全的吗?其实这里对其进行加锁正是为了保证在多线程环境下只能有一个线程去操作StringBuffer对象,这里需要注意一下。
#5.入参为StringBuilder:
1 public String(StringBuilder builder) {
2 this.value = Arrays.copyOf(builder.getValue(), builder.length());
3 }
分析:对比StringBuffer入参,如果用StringBuilder作为入参是不加锁操作的,因为StringBuilder本身线程不安全,但会提升性能,并且其源码上也做出了相应注释。
注意:以StringBuffer和StringBuilder为入参的构造函数这里要特别关注一下:StringBuffer线程安全,为了保证在String中也线程安全所以需要加锁,而StringBuilder非线程安全,因此不需要加锁操作,直接进行拷贝即可。
3.hashCode方法
1 public int hashCode() {
2 int h = hash;
3 if (h == 0 && value.length > 0) {
4 char val[] = value;
5
6 for (int i = 0; i < value.length; i++) {
7 h = 31 * h + val[i];
8 }
9 hash = h;
10 }
11 return h;
12 }
分析:String的hashCode方法还是比较简单的,它是遍历char数组以31为基数做一个累加操作。注意这里是以31来作为的基数,为什么取31作为基数可参考:String hashCode 方法为什么选择数字31作为乘子
4.equals方法
1 public boolean equals(Object anObject) {
2 if (this == anObject) {
3 return true;
4 }
5 if (anObject instanceof String) {
6 String anotherString = (String)anObject;
7 int n = value.length;
8 if (n == anotherString.value.length) {
9 char v1[] = value;
10 char v2[] = anotherString.value;
11 int i = 0;
12 while (n-- != 0) {
13 if (v1[i] != v2[i])
14 return false;
15 i++;
16 }
17 return true;
18 }
19 }
20 return false;
21 }
分析:首先会判断是否是同一个对象,如果是,则直接返回true;其次判断传入对象是否为String对象,如果对象不匹配直接返回false,否则依次比较两个对象的char,只要发现一个不相等,则直接返回false,停止循环。这里的写法还是比较简洁的,在日常开发中可以利用起来。
5.关于不可变对象
关于不可变对象,这里看一段源码就清楚了
1 public String substring(int beginIndex) {
2 if (beginIndex < 0) {
3 throw new StringIndexOutOfBoundsException(beginIndex);
4 }
5 int subLen = value.length - beginIndex;
6 if (subLen < 0) {
7 throw new StringIndexOutOfBoundsException(subLen);
8 }
9 return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
10 }
截取函数,直接看第9行代码处,如果beginIndex==0,返回的是当前对象,否则这里是new的一个新对象,其实String中的很多函数都是这样的操作,具体可翻看源码阅读一下。
总结
#1.String类在日常开发中经常使用,但可能对其源码并不是十分了解,通过分析其源码,了解更多看似简单的知识点。
#2.String类的构造函数非常多,需要注意一下,特别是StringBuffer和StringBuilder为入参的构造函数。
#3.String是不可变对象。
#4.String#hashCode的计算方式,以31为计算基数。