String使用频率很是高,不管是在大型仍是小型的应用程序都会大量的使用String类。因此,理解并以高性能的方式使用String是很是重要的。java
String类提供了不少功能丰富的API,例如substring(),indexOf(),lastIndexOf()等等。String是不可变类,它没有提供任何访问内部状态的方法,即便是substring()这样的看起来是要修改字符串方法也不会真正的修改实例,而是会建立一个新的String对象并返回。String也不能被继承,继承虽然提升了灵活性,但同时也可能会破坏父类的逻辑(由于重写机制),这样会使得String类很是危险,故String的设计者将其设计成不可继承是有很充分的理由的。api
下面咱们从源码开始慢慢分析上面讲到的特性。数组
String类有不少各类功能的API,没法一一细说,我就挑选了indexOf()方法来详细讨论讨论。安全
public int indexOf(int ch) {
return indexOf(ch, 0);
}
public int indexOf(int ch, int fromIndex) {
final int max = value.length;
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
// Note: fromIndex might be near -1>>>1.
return -1;
}
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return indexOfSupplementary(ch, fromIndex);
}
}
复制代码
indexOf()除了列出来的两种,还有5种重载形式,不过不是很经常使用,比较经常使用就是这俩。只接受一个参数的就很少说了,他没有多余的逻辑,直接就调用了indexOf(int ch, int fromIndex)。indexOf(int ch, int fromIndex)方法的第一个参数是要查找的字符,是int类型,关于该参数,JDK文档是这样描述的:网络
a character (Unicode code point).app
即它是一个Unicode编码的字符(看JDK里注释文档是了解一个方法的最快速的方式),有计算机基础知识的朋友应该不难理解为何使用整形数值来代替字符,虽然是int类型,但实际上咱们使用的时候彻底能够直接传递char类型,以下所示:ide
String s = "hello";
int i = s.indexOf('h',0);
复制代码
第二个参数fromIndex即从哪一个下标开始查找,若是调用的是只有一个参数ch的API,那么fromIndex的值默认就是0,即从字符串开头查找。性能
接下来就是if-else结构,这里就是对fromIndex作验证,若是fromIndex小于0,即令其等于0,若是fromIndex大于max,那么就直接返回-1,即表示没有找到。ui
接下来的代码才是indexOf的核心:this
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
复制代码
其实也很简单,就是遍历value数组,而后一个一个比较,若是找到就直接返回,遍历完以后还没找到,就返回-1,表示没有找到。
其余的方法就很少说了,具体使用建议看看JDK API文档,或者使用IDE,直接在IDE里看注释文档。(IDEA的话可使用Ctrl/cmd + 左键单击进入方法)。
String是不可变的,但为何要设计成不可变的呢?主要有如下几点考虑:
那String是如何实现不可变的呢?实现不可变至少须要下面几个步骤:
咱们来看看String是如何实现的。
从源码中咱们能够看到,String在类上是有fianl修饰,这样整个类的全部方法就都是final方法,不可被继承重写。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
......
}
复制代码
同时,咱们能够看到除了hash以外,其余的成员变量都是final修饰的。以下所示:
public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
private int hash; // Default to 0
private static final long serialVersionUID = -6849794470754667710L;
private final char value[];
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
复制代码
咱们发现,这和咱们最开始讲的第二个原则有些不一样。但不要紧,那只是一个比较强硬的规则,只要能证实即便不将其设置成final,也不会有问题便可。那CASE_INSENSITIVE_ORDER为何是public的呢?其实这个成员变量不能算是String的内部状态,只能是算是一个常量,即便被外界访问了,也不会有太大影响。
再来看看String有没有提供setter方法,我浏览了一下其API文档,没有发现任何状态的setter方法。接下来看看getter方法,发现有个getBytes()方法,该访问会访问到String的内部状态value数组,并将其编码成字节数组,以下所示:
public byte[] getBytes() {
return StringCoding.encode(value, 0, value.length);
}
//StringCoding.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;
}
}
复制代码
encode方法并无改变value数组的内容,只是获取了内容,并根据内容按照必定的编码方式编码并将结果存入byte数组中,最后返回。从这里能够看出,这个方法没有对String内部状态作修改。
最后看看有没有一些可能修改String的API,大体浏览了一下API 文档,发现有不少方法均可能修改String,例如substring,replace等。但实际上,这些方法最终没有修改String对象的值,而是根据原String的内容生成了新的String对象,以下substring方法所示:
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
复制代码
关键看看最后的返回语句,若是beginIndex为0,那么就不须要从新建立对象了,直接返回自己便可,若是不为0,那就建立一个新的String对象,而不是将对象自己进行修改。replace等方法也是同样的,就再也不赘述了。
可见,String类基本上是知足上面提到的5个步骤的,只有少部分代码没有遵循,例如hash不是final修饰的,但不要紧,只要能保证没有任何外部途径能修改hash值便可。
咱们常常会用“+”号来拼接两个字符串,例如:
String s1 = "hello,";
String s2 = "world";
String res = s1 + s2;
System.out.println(res);
复制代码
在上一节中,咱们说到了String是不可变的,但为何这里看起来就好像String“可变”了呢?其实这里并无违反String的不可变性,只是编译器和咱们玩了个小“把戏”。咱们来看看生成的字节码是怎样的吧,先使用javac将其编译,而后使用javap -verbose xxx.class 命令来展现字节码,结果以下所示(省略了其余内容):
......
Code:
stack=2, locals=4, args_size=1
0: ldc #2 // String hello,
2: astore_1
3: ldc #3 // String world
5: astore_2
6: new #4 // class java/lang/StringBuilder
9: dup
10: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
13: aload_1
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: aload_2
18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24: astore_3
25: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream;
28: aload_3
29: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
32: return
....
复制代码
咱们能够看到,“+”号被编译器翻译成了StringBuilder.append()方法(看序号14和18的指令),换句话说,“+”号被重载了,最终拼接完毕后,调用StringBuilder.toString()方法返回新的拼接好的String对象(看序号21的指令)。
因此,String没有被修改,而是调用了StringBuilder.append()方法来辅助拼接原字符串,并生成新的字符串。
运算符重载在C++里是容许的,可是Java并不容许。到底是出于什么缘由,我不是很清楚,但我认为运算符重载是致使C++复杂的一个缘由。
咱们直接来看源码(不懂的时候直接看源码是最好的方式,比去网上查更加快速、直观):
//String.valueOf(int)
public static String valueOf(int i) {
return Integer.toString(i);
}
//Integer.toString(int i)
public static String toString(int i) {
if (i == Integer.MIN_VALUE)
return "-2147483648";
int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
char[] buf = new char[size];
getChars(i, size, buf);
return new String(buf, true);
}
复制代码
很明显,String.valueOf(int)方法直接调用了Integre.toString()方法,以此来返回一个用字符串表示的整形数字。而在Integer.toString()方法里,最后返回的是一个新的String对象。
String.valueOf()还有其余重载形式,和 valueOf(int)的方式很是类似,例如valueOf(long)会调用Long.toString(),valueOf(double)会调用Double.toString(),但Boolean就不须要了,直接返回“true”或者“false”便可,具体差异仍是建议看看源码。
Java9对String作了不小的改变,最根本的区别是再也不使用char类型数组来存储字符了,而是改用byte类型的数组。char类型占用空间是2个字节,byte占用的是1个字节,这样就节省了不少空间。其余的修改都是基于这个改变而改变的,具体改变建议网上搜索一下,这是我看到的一篇文章:JAVA9 String新特性,说说你不知道的东西。
String类是很是重要的,Java程序的方方面面都会用到。本文简单的讲了一下String.indexOf()方法、“+”号重载,String的不可变性等,还顺带着讲了一丢丢阅读源码的方式,我的以为这才是最重要的,由于咱们不可能把JDK里全部的类及其API记下来,但只要掌握了看源码、文档的方法,就随时都能快速的了解该类及其API的功能和使用方法,因此掌握方法很是重要!!!