计算机程序的思惟逻辑 (29) - 剖析String

本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》(马俊昌著),由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买:京东自营连接 html

上节介绍了单个字符的封装类Character,本节介绍字符串类。字符串操做大概是计算机程序中最多见的操做了,Java中表示字符串的类是String,本节就来详细介绍String。java

字符串的基本使用是比较简单直接的,咱们来看下。正则表达式

基本用法

能够经过常量定义String变量编程

String name = "老马说编程";
复制代码

也能够经过new建立Stringc#

String name = new String("老马说编程");
复制代码

String能够直接使用+和+=运算符,如:windows

String name = "老马";
name+= "说编程";
String descritpion = ",探索编程本质";
System.out.println(name+descritpion); 
复制代码

输出为:数组

老马说编程,探索编程本质
复制代码

String类包括不少方法,以方便操做字符串。缓存

判断字符串是否为空安全

public boolean isEmpty() 复制代码

获取字符串长度bash

public int length() 复制代码

取子字符串

public String substring(int beginIndex) public String substring(int beginIndex, int endIndex) 复制代码

在字符串中查找字符或子字符串,返回第一个找到的索引位置,没找到返回-1

public int indexOf(int ch) public int indexOf(String str) 复制代码

从后面查找字符或子字符串,返回从后面数的第一个索引位置,没找到返回-1

public int lastIndexOf(int ch) public int lastIndexOf(String str) 复制代码

判断字符串中是否包含指定的字符序列。回顾一下,CharSequence是一个接口,String也实现了CharSequence

public boolean contains(CharSequence s) 复制代码

判断字符串是否以给定子字符串开头

public boolean startsWith(String prefix) 复制代码

判断字符串是否以给定子字符串结尾

public boolean endsWith(String suffix) 复制代码

与其余字符串比较,看内容是否相同

public boolean equals(Object anObject) 复制代码

忽略大小写,与其余字符串进行比较,看内容是否相同

public boolean equalsIgnoreCase(String anotherString) 复制代码

String也实现了Comparable接口,能够比较字符串大小

public int compareTo(String anotherString) 复制代码

还能够忽略大小写,进行大小比较

public int compareToIgnoreCase(String str) 复制代码

全部字符转换为大写字符,返回新字符串,原字符串不变

public String toUpperCase() 复制代码

全部字符转换为小写字符,返回新字符串,原字符串不变

public String toLowerCase() 复制代码

字符串链接,返回当前字符串和参数字符串合并后的字符串,原字符串不变

public String concat(String str) 复制代码

字符串替换,替换单个字符,返回新字符串,原字符串不变

public String replace(char oldChar, char newChar) 复制代码

字符串替换,替换字符序列,返回新字符串,原字符串不变

public String replace(CharSequence target, CharSequence replacement) 复制代码

删掉开头和结尾的空格,返回新字符串,原字符串不变

public String trim() 复制代码

分隔字符串,返回分隔后的子字符串数组,原字符串不变

public String[] split(String regex)
复制代码

例如,按逗号分隔"hello,world":

String str = "hello,world";
String[] arr = str.split(",");
复制代码

arr[0]为"hello", arr[1]为"world"。

从调用者的角度理解了String的基本用法,下面咱们进一步来理解String的内部。

走进String内部

封装字符数组

String类内部用一个字符数组表示字符串,实例变量定义为:

private final char value[];
复制代码

String有两个构造方法,能够根据char数组建立String

public String(char value[]) public String(char value[], int offset, int count) 复制代码

须要说明的是,String会根据参数新建立一个数组,并拷贝内容,而不会直接用参数中的字符数组。

String中的大部分方法,内部也都是操做的这个字符数组。好比说:

  • length()方法返回的就是这个数组的长度
  • substring()方法就是根据参数,调用构造方法String(char value[], int offset, int count)新建了一个字符串
  • indexOf查找字符或子字符串时就是在这个数组中进行查找

这些方法的实现大多比较直接,咱们就不赘述了。

String中还有一些方法,与这个char数组有关:

返回指定索引位置的char

public char charAt(int index) 复制代码

返回字符串对应的char数组

public char[] toCharArray()
复制代码

注意,返回的是一个拷贝后的数组,而不是原数组。

将char数组中指定范围的字符拷贝入目标数组指定位置

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) 复制代码

按Code Point处理字符

与Character相似,String类也提供了一些方法,按Code Point对字符串进行处理。

public int codePointAt(int index) public int codePointBefore(int index) public int codePointCount(int beginIndex, int endIndex) public int offsetByCodePoints(int index, int codePointOffset) 复制代码

这些方法与咱们在剖析Character一节介绍的很是相似,本节就再也不赘述了。

编码转换

String内部是按UTF-16BE处理字符的,对BMP字符,使用一个char,两个字节,对于增补字符,使用两个char,四个字节。咱们在第六节介绍过各类编码,不一样编码可能用于不一样的字符集,使用不一样的字节数目,和不一样的二进制表示。如何处理这些不一样的编码呢?这些编码与Java内部表示之间如何相互转换呢?

Java使用Charset这个类表示各类编码,它有两个经常使用静态方法:

public static Charset defaultCharset() public static Charset forName(String charsetName) 复制代码

第一个方法返回系统的默认编码,好比,在个人电脑上,执行以下语句:

System.out.println(Charset.defaultCharset().name());
复制代码

输出为UTF-8

第二方法返回给定编码名称的Charset对象,与咱们在第六节介绍的编码相对应,其charset名称能够是:US-ASCII, ISO-8859-1, windows-1252, GB2312, GBK, GB18030, Big5, UTF-8,好比:

Charset charset = Charset.forName("GB18030");
复制代码

String类提供了以下方法,返回字符串按给定编码的字节表示:

public byte[] getBytes()  
public byte[] getBytes(String charsetName)
public byte[] getBytes(Charset charset) 
复制代码

第一个方法没有编码参数,使用系统默认编码,第二方法参数为编码名称,第三个为Charset。

String类有以下构造方法,能够根据字节和编码建立字符串,也就是说,根据给定编码的字节表示,建立Java的内部表示。

public String(byte bytes[]) public String(byte bytes[], int offset, int length) public String(byte bytes[], int offset, int length, String charsetName) public String(byte bytes[], int offset, int length, Charset charset) public String(byte bytes[], String charsetName) public String(byte bytes[], Charset charset) 复制代码

除了经过String中的方法进行编码转换,Charset类中也有一些方法进行编码/解码,本节就不介绍了。重要的是认识到,Java的内部表示与各类编码是不一样的,但能够相互转换。

不可变性

与包装类相似,String类也是不可变类,即对象一旦建立,就没有办法修改了。String类也声明为了final,不能被继承,内部char数组value也是final的,初始化后就不能再变了。

String类中提供了不少看似修改的方法,实际上是经过建立新的String对象来实现的,原来的String对象不会被修改。好比说,咱们来看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);
}
复制代码

经过Arrays.copyOf方法建立了一块新的字符数组,拷贝原内容,而后经过new建立了一个新的String。关于Arrays类,咱们将在后续章节详细介绍。

与包装类相似,定义为不可变类,程序能够更为简单、安全、容易理解。但若是频繁修改字符串,而每次修改都新建一个字符串,性能过低,这时,应该考虑Java中的另两个类StringBuilder和StringBuffer,咱们在下节介绍它们。

常量字符串

Java中的字符串常量是很是特殊的,除了能够直接赋值给String变量外,它本身就像一个String类型的对象同样,能够直接调用String的各类方法。咱们来看代码:

System.out.println("老马说编程".length());
System.out.println("老马说编程".contains("老马"));
System.out.println("老马说编程".indexOf("编程"));
复制代码

实际上,这些常量就是String类型的对象,在内存中,它们被放在一个共享的地方,这个地方称为字符串常量池,它保存全部的常量字符串,每一个常量只会保存一份,被全部使用者共享。当经过常量的形式使用一个字符串的时候,使用的就是常量池中的那个对应的String类型的对象

好比说,咱们来看代码:

String name1 = "老马说编程";
String name2 = "老马说编程";
System.out.println(name1==name2);
复制代码

输出为true,为何呢?能够认为,"老马说编程"在常量池中有一个对应的String类型的对象,咱们假定名称为laoma,上面代码实际上就相似于:

String laoma = new String(new char[]{'老','马','说','编','程'});
String name1 = laoma;
String name2 = laoma;
System.out.println(name1==name2);
复制代码

实际上只有一个String对象,三个变量都指向这个对象,name1==name2也就不言而喻了。

须要注意的是,若是不是经过常量直接赋值,而是经过new建立的,==就不会返回true了,看下面代码:

String name1 = new String("老马说编程");
String name2 = new String("老马说编程");
System.out.println(name1==name2);
复制代码

输出为false,为何呢?上面代码相似于:

String laoma = new String(new char[]{'老','马','说','编','程'});
String name1 = new String(laoma);
String name2 = new String(laoma);
System.out.println(name1==name2);
复制代码

String类中以String为参数的构造方法代码以下:

public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}
复制代码

hash是String类中另外一个实例变量,表示缓存的hashCode值,咱们待会介绍。

能够看出, name1和name2指向两个不一样的String对象,只是这两个对象内部的value值指向相同的char数组。其内存布局大概以下所示:

因此,name1==name2是不成立的,但name1.equals(name2)是true。

hashCode

咱们刚刚提到hash这个实例变量,它的定义以下:

private int hash; // Default to 0
复制代码

它缓存了hashCode()方法的值,也就是说,第一次调用hashCode()的时候,会把结果保存在hash这个变量中,之后再调用就直接返回保存的值。

咱们来看下String类的hashCode方法,代码以下:(若是用掘金app看,可能会有乱码,是掘金bug,能够经过掘金PC版查看,或者关注个人微信公众号"老马说编程"查看)

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}
复制代码

若是缓存的hash不为0,就直接返回了,不然根据字符数组中的内容计算hash,计算方法是:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
复制代码

s表示字符串,s[0]表示第一个字符,n表示字符串长度,s[0]*31^(n-1)表示31的n-1次方再乘以第一个字符的值。

为何要用这个计算方法呢?这个式子中,hash值与每一个字符的值有关,每一个位置乘以不一样的值,hash值与每一个字符的位置也有关。使用31大概是由于两个缘由,一方面能够产生更分散的散列,即不一样字符串hash值也通常不一样,另外一方面计算效率比较高,31*h与32*h-h(h<<5)-h等价,能够用更高效率的移位和减法操做代替乘法操做。

在Java中,广泛采用以上思路来实现hashCode。

正则表达式

String类中,有一些方法接受的不是普通的字符串参数,而是正则表达式,什么是正则表达式呢?它能够理解为一个字符串,但表达的是一个规则,通常用于文本的匹配、查找、替换等,正则表达式有着丰富和强大的功能,是一个比较庞大的话题,咱们将在后续章节单独介绍。

Java中有专门的类如Pattern和Matcher用于正则表达式,但对于简单的状况,String类提供了更为简洁的操做,String中接受正则表达式的方法有:

分隔字符串

public String[] split(String regex) 
复制代码

检查是否匹配

public boolean matches(String regex) 复制代码

字符串替换

public String replaceFirst(String regex, String replacement) public String replaceAll(String regex, String replacement) 复制代码

小结

本节,咱们介绍了String类,介绍了其基本用法,内部实现,编码转换,分析了其不可变性,常量字符串,以及hashCode的实现。

本节中,咱们提到,在频繁的字符串修改操做中,String类效率比较低,咱们提到了StringBuilder和StringBuffer类。咱们也看到String能够直接使用+和+=进行操做,它们的背后也是StringBuilder类。

让咱们下节来看下这两个类。


未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),深刻浅出,老马和你一块儿探索Java编程及计算机技术的本质。用心原创,保留全部版权。

相关文章
相关标签/搜索