Java源码之String

说在前面:

为什么看源码: 最好的学习的方式就是模仿,接下来才是创造。而源码就是我们最好的模仿对象,因为写源码的人都不是一般的人,所以用心学习源码,也就可能变成牛逼的人。其次,看源码,是一项修练内功的重要方式,书看百遍其意自现,源码也是一样,前提是你不要惧怕源码,要用心的看,看不懂了,不要怀疑自己的智商,回过头来多看几遍,我就是这样做的,一遍有一遍的感受,等你那天看源码不由的惊叹一声,这个代码写得太好了,恭喜你,你已经离优秀不远了。最后,看源码,能培养我们的编程思维,当然这个层次有点高了,需要时间积累,毕竟我离这个境界也有点远。

今天就来谈谈java的string的源码实现,后续我也会写javaSe的源码系列,欢迎围观和交流。

1.继承关系

继承三个接口的说明:

Comparable接口:

实现对象之间比较的接口,它的核心方法只有一个:

public int compareTo(T o);

CharSequence接口:

CharSequence是char值的可读序列。 该接口提供对许多不同种类的char序列的统一只读访问。CharSequence是一个接口,它只包括length(), charAt(int index), subSequence(int start, int end)这几个API接口。除了String实现了CharSequence之外,StringBufferStringBuilder也实现了 CharSequence接口。

那么String为什么实现Charsequence这个接口呢。这里就要涉及一个java的重要特性,也就是多态。看下面的代码

   public void charSetTest(CharSequence charSequence){
System.out.println(charSequence+"实现了多态");
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
StringTest strTest = new StringTest();
strTest.charSetTest(new String("我是String"));
strTest.charSetTest(new StringBuffer("我是StringBuffer"));
strTest.charSetTest(new StringBuilder("我是StringBuilder"));
}

执行结果:

我是String实现了多态
我是StringBuffer实现了多态
我是StringBuilder实现了多态

继承这个接口的原因就很明显:

因为String对象是不可变的,StringBuffer和StringBuilder这两个是可变的,所以我们在构造字符串的过程中往往要用到StringBuffer和StringBuilder。如果那些方法定义String作为参数类型,那么就没法对它们用那些方法,先得转化成String才能用。但StringBuffer和StringBuilder转换为String再转换过来很化时间的,用它们而不是直接用String的“加法”来构造新String本来就是为了省时间。

Serializable接口:

继承该接口,就是表明这个类是是可以别序列化的,这里就标记了string这个类是可以被序列化的,序列化的定义以及使用时机可以移步这里

2.常用方法的使用和源码解析:

2.1构造方法

string总共提供了15种构造方法:

当然,这么多的构造方法,只需搞明白四个构造方法就可以了,因为其他的构造方法就是这四个构造方法的调用:

在看构造方法之前,先看看String的属性

 private final char value[];
private int hash; // Default to 0
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];

通过属性了解到,底层声明了一个final类型的char数组,这个char数组就是String的构造方法的核心,因为String的本质就是字符char(只针对javaSE8 以前的版本),下面来分析有代表的四个构造方法:

  1. String()和String(String original)

    这两个构造方法我们平时用的比较经常,源码如下:

     public String() {   this.value = "".value; }
    
     public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
    }

    分析:

    通过源码可以看到,核心还是和String类声明的char[]数组value属性建立联系,进而来处理这个属性的值。

    在String()这个空构造的方法中,就是属性char数组的值声明为一个空的字符串赋值给属性value.

    而在String(String original),也是将方法参数的value的属性赋值给声明char[]数组的value属性,方便String的其他方法对char[]数组处理。

    记住,在java的String操作中,大多数情况下还是对char[]数组的操作,这点很重要。

    一般情况下定义一个新的字符串,有下面的两种方式:

    String chen = new String("chen");   // 这个我们一般不会使用
    String chen = "chen";

    我们一般会选择第二种,这又是什么原因呢:

    其实这两种声明的方式在JVM看来时等价的。

    划重点:

    但是String password="chen",利用了字符串缓冲池,也就是说如果缓冲池中已经存在了相同的字符串,就不会产生新的对象,而直接返回缓冲池中的字符串对象的引用。

    如:
    String a = "chen";
    String b = "chen";
    String c = new String("chen");
    String d = new String("chen"); System.out.println(a==b);//将输出"true";因为两个变量指向同一个对象。利用了字符串的缓冲池
    System.out.println(c==d);//将输出"flase";因为两个变量不指向同一个对象。虽然值相同,只有用c.equals(d)才能返回true.

    所以实际中,建议用第一种,可以减少系统资源消耗。

  2. String(char[]vlaue,int offest,int count) 与字符相关的构造方法

    源码如下:

    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;
    }
    }
    // Note: offset or count might be near -1>>>1.
    if (offset > value.length - count) {
    throw new StringIndexOutOfBoundsException(offset + count);
    }
    this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

    分析:

    这个构造方法的作用就是传入指定大小的char[] 数组,指定一个起始位置,然后构造出从起始位置开始计算的指定长度个数的字符串,具体用法:

    public static void main(String[] args) {
    char[] chars = new char[]{'a','b','c','d'}; // 从chars数组的第二位开始,总数为3个字符的字符串
    String rangeChar=new String(chars,1,3);
    System.out.println(rangeChar);
    } 输出:
    abc

    这个构造方法的核心在于:

    this.value = Arrays.copyOfRange(value, offset, offset+count);
    
    //而这个方法在追一层,就到了Arrays这个工具类copyOfRange这个方法
    public static char[] copyOfRange(char[] original, int from, int to) {
    int newLength = to - from;
    if (newLength < 0)
    throw new IllegalArgumentException(from + " > " + to);
    char[] copy = new char[newLength];
    System.arraycopy(original, from, copy, 0,
    Math.min(original.length - from, newLength));
    return copy;
    }

    其实看到这里,他的实现原理就基本清楚了,分析copyOfRange()这个方法的执行步骤:

    首先是获取原字符数组的orginal[]要构造字符串的长度,也就是 这一行代码:

    int newLength = to - from;

    然后异常判断,并声明新的数组,来存储原数组指定长度的值

     if (newLength < 0)
    throw new IllegalArgumentException(from + " > " + to);
    char[] copy = new char[newLength];

    将原字符数组指定长度的值拷贝到新数组,返回这个数组:

     System.arraycopy(original, from, copy, 0,
    Math.min(original.length - from, newLength));
    return copy;

    最后再将数组的值赋值给String的属性value,完成初始化:

     this.value = Arrays.copyOfRange(value, offset, offset+count);

    归根结底还是和String声明的属性value建立联系,完成相关的操作。

  3. String(byte[] bytes,int offest,int length,Charset charset) 字节相关的 构造方法

    这个构造方法的作用就是将指定长度的字节数组,构造成字符串,且还可以指定编码值:

    涉及的源码如下:

    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);
    } // StringCoding中的方法:
    static char[] decode(Charset cs, byte[] ba, int off, int len) {
    // 1, 构造解码器
    CharsetDecoder cd = cs.newDecoder();
    int en = scale(len, cd.maxCharsPerByte());
    char[] ca = new char[en];
    if (len == 0)
    return ca;
    boolean isTrusted = false;
    if (System.getSecurityManager() != null) {
    if (!(isTrusted = (cs.getClass().getClassLoader0() == null))) {
    ba = Arrays.copyOfRange(ba, off, off + len);
    off = 0;
    }
    }
    cd.onMalformedInput(CodingErrorAction.REPLACE)
    .onUnmappableCharacter(CodingErrorAction.REPLACE)
    .reset();
    if (cd instanceof ArrayDecoder) {
    int clen = ((ArrayDecoder)cd).decode(ba, off, len, ca);
    return safeTrim(ca, clen, cs, isTrusted);
    } else {
    ByteBuffer bb = ByteBuffer.wrap(ba, off, len);
    CharBuffer cb = CharBuffer.wrap(ca);
    try {
    CoderResult cr = cd.decode(bb, cb, true);
    if (!cr.isUnderflow())
    cr.throwException();
    cr = cd.flush(cb);
    if (!cr.isUnderflow())
    cr.throwException();
    } catch (CharacterCodingException x) {
    // Substitution is always enabled,
    // so this shouldn't happen
    throw new Error(x);
    }
    return safeTrim(ca, cb.position(), cs, isTrusted);
    }
    } private static char[] safeTrim(char[] ca, int len,
    Charset cs, boolean isTrusted) {
    if (len == ca.length && (isTrusted || System.getSecurityManager() == null))
    return ca;
    else
    return Arrays.copyOf(ca, len);
    }

    这个方法构造的方法的复杂之处就是在于对于指定编码的处理,但是我们如果看完这个方法调用的整个流程最终还是落到

    return Arrays.copyOf(ca, len);
    返回一个指定编码的字符数组,然后和String类的value属性建立联系

    字节数组构造方法的基本逻辑:就是将字节数组转化为字符数组,再和String的value属性建立联系,完成初始化。

  4. String(StringBuilder builder) 和String(StringBuffer buffer)与String扩展类相关的构造方法:

    这个构造方法就是将StringBuilder或者StringBuffer类初始化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());
    }

    分析:

    核心的代码还是这一句:

      this.value = Arrays.copyOf(builder.getValue(), builder.length());

​ 在往下看builder.getValue(),的源码

   final char[] getValue() {
return value;
}
返回一个字符数组

这样就能很好理解这个构造方法了: 先利用builder.getValue()将指定的类型转化为字符数组,通过Arrays.copyOf()方法进行拷贝,将返回的数组赋值给String的属性value,完成初始化。

2.2常用的方法分析

String的方法大概有60多个,这里只分析几个常用的方法,了解其他的方法,可以移步javaSE官方文档:

  1. 字符串转化为字符的方法:charAt(int index)

     public char charAt(int index) {
    //1. 判断异常
    if ((index < 0) || (index >= value.length)) {
    throw new StringIndexOutOfBoundsException(index);
    }
    // 2.返回指定位置的字符
    return value[index];
    }

    用法示例:

            String StrChar = "chen";
    char getChar = StrChar.charAt(1);
    System.out.println(getChar);
    输出:
    h

    2.字符串转化为字节数组的方法:getBytes()

    // 源码
    public byte[] getBytes() {
    return StringCoding.encode(value, 0, value.length);
    } // encode的源码
    static byte[] encode(char[] ca, int off, int len) {
    String csn = Charset.defaultCharset().name();
    try {
    // use charset name encode() variant which provides caching.
    return encode(csn, ca, off, len);
    } catch (UnsupportedEncodingException x) {
    warnUnsupportedCharset(csn);
    }
    try {
    return encode("ISO-8859-1", ca, off, len);
    } catch (UnsupportedEncodingException x) {
    // If this code is hit during VM initialization, MessageUtils is
    // the only way we will be able to get any kind of error message.
    MessageUtils.err("ISO-8859-1 charset not available: "
    + x.toString());
    // If we can not find ISO-8859-1 (a required encoding) then things
    // are seriously wrong with the installation.
    System.exit(1);
    return null;
    }
    }

​ 用法示例:

       String strByte = "chen";

       // 将string转化为字节数组
byte[] getBytes = strByte.getBytes(); // 遍历输出
for (byte getByte : getBytes) {
System.out.println(getByte);
}
输出结果:
99
104
101
110

3.返回字符串中指定字符的下标的方法: indexOf(String str):

这里的参数为字符串:

这个方法一共涉及了四个方法,源码如下:

 public int indexOf(String str) {
return indexOf(str, 0);
} public int indexOf(String str, int fromIndex) {
return indexOf(value, 0, value.length,
str.value, 0, str.value.length, fromIndex);
} static int indexOf(char[] source, int sourceOffset, int sourceCount,
String target, int fromIndex) {
return indexOf(source, sourceOffset, sourceCount,
target.value, 0, target.value.length,
fromIndex);
} static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) { // 1. 判断范围
if (fromIndex >= sourceCount) {
return (targetCount == 0 ? sourceCount : -1);
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
} // 2,判断目标字符串是否时原子符串的子序列,并返回目标序列的第一个字符在原字符序列的索引。
char first = target[targetOffset];
int max = sourceOffset + (sourceCount - targetCount); for (int i = sourceOffset + fromIndex; i <= max; i++) {
/* Look for first character. */
if (source[i] != first) {
while (++i <= max && source[i] != first);
} /* Found first character, now look at the rest of v2 */
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
for (int k = targetOffset + 1; j < end && source[j]
== target[k]; j++, k++); if (j == end) {
/* Found whole string. */
return i - sourceOffset;
}
}
}
return -1;
}

具体执行过程已在方法的注释中进行了说明:

用法示例:

 String strIndex = "chen";
int getIndex = strIndex.indexOf("he");
System.out.println(getIndex);
输出:
1

注意:也就是输出字符串中的第一个字符在原子符串中的索引,前提是传入的参数必须是原子符串的子序列,以上面的列子为例,传入的字符串序列必须是chen这个字符串的子序列,才能输出正确的索引,比如传入的序列不是chen的子序列,输出为-1

 String strIndex = "chen";
int getIndex = strIndex.indexOf("hew");
System.out.println(getIndex);
输出:
-1

使用这个方法时,这点非常注意。

4.将字符串中的某个字符进行替换:replace(char oldChar, char newChar)

参数就是被替换的字符和新的字符,源码如下:

public String replace(char oldChar, char newChar) {

    if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */ // 1.遍历字符数组,找到原子符的位置
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
//2. 声明一个临时的字符数组,用来存储替换后的字符串,
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
// 3. 将原字符数组拷贝到新的字符数组中去
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
// 4.
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
// 3. 初始化一个新的字符串
return new String(buf, true);
}
}
return this;
}

具体的执行逻辑就是注释的语句。

用法示例:

 String strIndex = "chee";
String afterReplace = strIndex.replace('e','n');
System.out.println(afterReplace);
输出:
chnn

注意:这里的替换是字符串中的所有与旧字符的相同的字符,比如上面的这个例子,就是将原子符中的e全部替换为n。

5.字符串的分隔:split(String regex) :

源码如下:

   public String[] split(String regex) {
return split(regex, 0);
} public String[] split(String regex, int limit) {
/* fastpath if the regex is a
(1)one-char String and this character is not one of the
RegEx's meta characters ".$|()[{^?*+\\", or
(2)two-char String and the first char is the backslash and
the second is not the ascii digit or ascii letter.
*/
char ch = 0;
if (((regex.value.length == 1 &&
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
(regex.length() == 2 &&
regex.charAt(0) == '\\' &&
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 &&
((ch-'A')|('Z'-ch)) < 0)) &&
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE))
{
int off = 0;
int next = 0;
boolean limited = limit > 0;
ArrayList<String> list = new ArrayList<>();
while ((next = indexOf(ch, off)) != -1) {
if (!limited || list.size() < limit - 1) {
list.add(substring(off, next));
off = next + 1;
} else { // last one
//assert (list.size() == limit - 1);
list.add(substring(off, value.length));
off = value.length;
break;
}
}
// If no match was found, return this
if (off == 0)
return new String[]{this}; // Add remaining segment
if (!limited || list.size() < limit)
list.add(substring(off, value.length)); // Construct result
int resultSize = list.size();
if (limit == 0) {
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
}
String[] result = new String[resultSize];
return list.subList(0, resultSize).toArray(result);
}
return Pattern.compile(regex).split(this, limit);
}

用法示例:

// 将一句话使用空格进行分隔
String sentence = "People who hear thumb up can get rich"; // 使用空格进行分隔
String[] subSequence = sentence.split("\\s");
for (String s : subSequence) {
System.out.println(s);
} 输出:
People
who
hear
thumb
up
can
get
rich

注意: 使用这个方法,对正则表达式有所了解,才能实现更强大的功能,正则表达式的学习,可以移步菜鸟教程

6.实现字符串的指定范围的切割substring(int beginIndex, int endIndex):

源码如下:

 public String substring(int beginIndex, int endIndex) {

        // 1.判断异常
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
// 2,确定切割的长度
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
// 3.使用构造方法,返回切割后字符串
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}

具体的执行逻辑如注释所示,这个方法的逻辑总体比较简单:

具体用法:

     String sentence = "hhchennn";
String subSequence = sentence.substring(2,6);
System.out.println(subSequence);
输出:
chen

注意: 从源码了解到,这个方法的在切割的时候,一般将第一个参数包含,包含第二个参数,也就是说上面的例子中在切割后的字符串中包含2这个字符,但是不包含6这个字符。

7.当然处理这些还有一些常用的方法,比如:

// 1.去除字符串前后空格的方法
trim() //2.大小写转换的方法
toLowerCase()
toUpperCase() //3. 将字符串转化为数组
toCharArray() // 4.将基本类型转化字符串
valueOf(boolean b) // 5.返回对象本身的字符串形式
toString()

这些方法使用起来都比较简单,强烈建议看看java官方文档

3.面试常问

3.1. 为什么是不可变的

1、什么是不可变?

从 java角度来讲就是说成final的。参考Effective Java 中第 15 条 使可变性最小化 中对 不可变类 的解释:

不可变类只是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并且在对象的整个生命周期内固定不变。为了使类不可变,要遵循下面五条规则:

1. 不要提供任何会修改对象状态的方法。

2. 保证类不会被扩展。 一般的做法是让这个类称为 `final` 的,防止子类化,破坏该类的不可变行为。

3. 使所有的域都是 final 的。

4. 使所有的域都成为私有的。 防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象。

5. 确保对于任何可变性组件的互斥访问。 如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。

当然在 Java 平台类库中,包含许多不可变类,例如 String , 基本类型的包装类,BigInteger, BigDecimal 等等。综上所述,不可变类具有一些显著的通用特征:类本身是 final 修饰的;所有的域几乎都是私有 final 的;不会对外暴露可以修改对象属性的方法。通过查阅 String 的源码,可以清晰的看到这些特征。

2.为什么不可变

String real = "chen"
real = "Wei";

下图就很好解释了代码的执行过程:

执行第一行代码时,在堆上新建一个对象实例 chen , real是一个指向该实例的引用,引用包含的仅仅只是实例在堆上的内存地址而已。执行第二行代码时,仅仅只是改变了 real 这个引用的地址,指向了另一个实例 wei。所以,正如前面所说过的,不可变类只是其实例不能被修改的类。real 重新赋值仅仅只是改变了它的引用而已,并不会真正去改变它本来的内存地址上的值。这样的好处也是显而易见的,最简单的当存在多个 String 的引用指向同一个内存地址时,改变其中一个引用的值并不会对其他引用的值造成影响。

那么,String 是如何保持不可变性的呢?结合 Effective Java 中总结的五条原则,阅读它的 源码 之后就一清二楚了。

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; /** Cache the hash code for the string */
private int hash; // Default to 0 /** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L; private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];

String 类是 final 修饰的,满足第二条原则:保证类不会被扩展。 分析一下它的几个域:

  • private final char value[] : 可以看到 Java 还是使用字节数组来实现字符串的,并且用 final 修饰,保证其不可变性。这就是为什么 String 实例不可变的原因。
  • private int hash : String的哈希值缓存
  • private static final long serialVersionUID = -6849794470754667710L : String对象的 serialVersionUID
  • private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0] : 序列化时使用

其中最主要的域就是 value,代表了 String对象的值。由于使用了 private final 修饰,正常情况下外界没有办法去修改它的值的。正如第三条 使所有的域都是 final 的。 和第四条 使所有的域都成为私有的 所描述的。难道这样一个 private 加上 final 就可以保证万无一失了吗?看下面代码示例:

    final char[] value = {'a', 'b', 'c'};
value[2] = 'd';

这时候的 value 对象在内存中已经是 a b d 了。其实 final 修饰的仅仅只是 value 这个引用,你无法再将 value 指向其他内存地址,例如下面这段代码就是无法通过编译的:

    final char[] value = {'a', 'b', 'c'};
value = {'a', 'b', 'c', 'd'};

所以仅仅通过一个 final 是无法保证其值不变的,如果类本身提供方法修改实例值,那就没有办法保证不变性了。Effective Java 中的第一条原则 不要提供任何会修改对象状态的方法 。String 类也很好的做到了这一点。在 String 中有许多对字符串进行操作的函数,例如 substring concat replace replaceAll 等等,这些函数是否会修改类中的 value 域呢?我们看一下 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);
}

注意其中的每一步实现都不会对 value产生任何影响。首先使用 Arrays.copyOf() 方法来获得 value 的拷贝,最后重新 new 一个String对象作为返回值。其他的方法和 contact 一样,都采取类似的方法来保证不会对 value 造成变化。的的确确,String 类中并没有提供任何可以改变其值的方法。相比 final 而言,这更能保障 String 不可变。

其中最主要的域就是 value,代表了 String对象的值。由于使用了 private final 修饰,正常情况下外界没有办法去修改它的值的。正如第三条 使所有的域都是 final 的。 和第四条 使所有的域都成为私有的 所描述的。难道这样一个 private 加上 final 就可以保证万无一失了吗?看下面代码示例:

    final char[] value = {'a', 'b', 'c'};
value[2] = 'd';

这时候的 value 对象在内存中已经是 a b d 了。其实 final 修饰的仅仅只是 value 这个引用,你无法再将 value 指向其他内存地址,例如下面这段代码就是无法通过编译的:

    final char[] value = {'a', 'b', 'c'};
value = {'a', 'b', 'c', 'd'};

所以仅仅通过一个 final 是无法保证其值不变的,如果类本身提供方法修改实例值,那就没有办法保证不变性了。Effective Java 中的第一条原则 不要提供任何会修改对象状态的方法 。String 类也很好的做到了这一点。在 String 中有许多对字符串进行操作的函数,例如 substring concat replace replaceAll 等等,这些函数是否会修改类中的 value 域呢?我们看一下 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);
}

注意其中的每一步实现都不会对 value产生任何影响。首先使用 Arrays.copyOf() 方法来获得 value 的拷贝,最后重新 new 一个String对象作为返回值。其他的方法和 contact 一样,都采取类似的方法来保证不会对 value 造成变化。的的确确,String 类中并没有提供任何可以改变其值的方法。相比 final 而言,这更能保障 String 不可变。

3.不可变类的好处:

Effective Java 中总结了不可变类的特点。

  • 不可变类比较简单。
  • 不可变对象本质上是线程安全的,它们不要求同步。不可变对象可以被自由地共享。
  • 不仅可以共享不可变对象,甚至可以共享它们的内部信息。
  • 不可变对象为其他对象提供了大量的构建。
  • 不可变类真正唯一的缺点是,对于每个不同的值都需要一个单独的对象。

3.2. 使用什么方式可以改变String类的不可变性

当然使用反射,java的反射机制可以做到我们平常做不到的很多事情:

        String str = "chen";
System.out.println(str);
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
char[] value = (char[]) field.get(str);
value[1] = 'a';
System.out.println(str); 执行结果:
chen
caen

3.3. String和stringBuffer和StringBuilder的区别

从以下三个方面来考虑他们之间的异同点:

1.可变和不可变性:

String: 字符串常量,在修改时,不会改变自身的值,若修改,就会重新生成新的字符串对象。

StringBuffer: 在修改时会改变对象本身,不会生成新的对象,使用场景:对字符经常改变的情况下,主要方法: append(),insert() 等。

2.线程是否安全

String: 定义之后不可改变,线程安全

String Buffer: 是线程安全的,但是执行效率比较低,适用于多线程下操作字符串缓冲区的大量数据。

StringBuilder: 线程不安全的,适用于单线程下操作字符串缓冲区的大量数据

3.共同点

StringBuilder和StringBuffer有共有的父类 AbstractStringBuilder(抽象类)。

StringBuilder,StringBuffer的方法都会调用AbstractStringBuilder中的公共方法,如: super().append()...

只是StringBuffer会在方法上加上synchronized关键字,进行同步。

4.优秀的工具包推荐

4.1guava

Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库,例如:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等等。 所有这些工具每天都在被Google的工程师应用在产品服务中。具体的中文参考文档,Guava中文参考文档

4.2 Hutool

Hutool是一个Java工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让Java语言也可以“甜甜的”。Hutool最初是我项目中“util”包的一个整理,后来慢慢积累并加入更多非业务相关功能,并广泛学习其它开源项目精髓,经过自己整理修改,最终形成丰富的开源工具集。Hutool参考文档

追本溯源,方能阔步前行

参考资料:

参考博客:

https://juejin.im/post/59cef72b518825276f49fe40

https://www.cnblogs.com/ChrisMurphy/p/4760197.html

参考书籍: java官方文档 《深入理解JVM虚拟机》

死啃了String源码之后的更多相关文章

  1. String源码中的"avoid getfield opcode"

    引言: 之前一篇文章梳理了String的不变性原则,还提到了一段源码中注释"avoid getfield opcode",当时通过查阅资料发现,这是为了防止 getfield(获取 ...

  2. String源码学习

    String源码学习 零散的收获 数组的元素类型可以通过getComponentType来获取到 子类型的数组可以赋值给父类型的数组,.但是并不存在继承关系.数组的父类是Object. 通过声明如下代 ...

  3. 基于JDK1.8的String源码学习笔记

    String,可能是学习Java一上来就学习的,经常用,但是却往往只是一知半解,甚至API有时也得现查.所以还是老规矩,倒腾源码. 一.java doc 这次首先关注String的doc,因为其实作为 ...

  4. String 源码浅析————终结篇

    写在前面 说说这几天看源码的感受吧,其实 jdk 中的源码设计是最值得进阶学习的地方.我们在对 api 较为熟悉之后,完全可以去尝试阅读一些 jdk 源码,打开 jdk 源码后,如果你英文能力稍微过得 ...

  5. String源码中hashCode算法

    针对java中String源码hashcode算法源码分析 /** The value is used for character storage. */ private final char val ...

  6. 程序兵法:Java String 源码的排序算法(一)

    摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 这是泥瓦匠的第103篇原创 <程序兵法:Java Str ...

  7. Java基础(八)--String(源码)、StringBuffer、StringBuilder

    String源码:基于jdk1.8 public final class String implements Serializable, Comparable<String>, CharS ...

  8. Java源码解析|String源码与常用方法

    String源码与常用方法 1.栗子 代码: public class JavaStringClass { public static void main(String[] args) { Strin ...

  9. (转)Java中的String为什么是不可变的? -- String源码分析

    背景:被问到很基础的知识点  string  自己答的很模糊 Java中的String为什么是不可变的? -- String源码分析 ps:最好去阅读原文 Java中的String为什么是不可变的 什 ...

随机推荐

  1. MyBatis入门知识汇总

     为什么要使用MyBatis? 我们都知道,在学习mybatis之前,要在Java中操作数据库,需要用到JDBC,但是在使用JDBC时会有许多缺陷. 比如: 1.使用时需要先进行数据库连接,不用后要立 ...

  2. keepalived的一些。。

    继续采坑..有些坑,踩了才知道. 1.文件位置不能错. 首先是安装, 源码编译,--prefix=/usr/local/keepalive 然后用 sbin/keepalive -f  ...conf ...

  3. React-Router 4 两个常用路由变量

    讲真我个人不太喜欢4.x版本,虽然作者自信动态路由的形式符合React组件化的哲学,但是路由和一般组件耦合太深,而且后期组件分片也麻烦,以后需要重构的话怕是会一番折腾.同学公司用的还是3.x版本. 不 ...

  4. Java POI 读取Excel数据转换为XML格式

    1.首先要下载poi相关的包:http://poi.apache.org/  ,以下是所需的jar包 2.贴上详细的代码 public class ExcelToXml { /** * 将excel的 ...

  5. SpringBoot教程——检视阅读

    SpringBoot教程--检视阅读 参考 SpringBoot教程--一点--蓝本--springboot2.1.1 SpringBoot教程--易百--springboo2.0.5.RELEASE ...

  6. 王艳 201771010127《面向对象程序设计(java)》第十七周学习总结

    实验十七  线程同步控制 实验时间 2018-12-10 一.理论部分 1.线程同步:多线程并发运行不确定性问题解决方案:引入线程同步机制,使得另一线程要使用该方法,就只能等待. 解决方案: 1)锁对 ...

  7. 201771010128王玉兰《面向对象程序设计(Java)》第十三周学习总结

    第一部分:基础理论知识 1.事件处理基础 事件源(event source):能够产生事件的对象都可 以成为事件源,如文本框.按钮等.一个事件源是一个 能够注册监听器并向监听器发送事件对象的对象. 事 ...

  8. hdu5967数学找规律+逆元

    Detachment Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)Total ...

  9. poj 2296

    Map Labeler Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 2047   Accepted: 682 Descri ...

  10. 最小比率树 poj2728

    以下内容均为转载 http://www.cnblogs.com/ftae/p/6947497.html poj2728(最小比率生成树)   poj2728 题意 给出 n 个点的坐标和它的高度,求一 ...