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


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

字符串的基本使用是比较简单直接的,我们来看下。

基本用法

可以通过常量定义String变量

String name = "老马说编程";

也可以通过new创建String

String name = new String("老马说编程");

String可以直接使用+和+=运算符,如:

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

输出为:老马说编程,探索编程本质

String类包括很多方法,以方便操作字符串。

判断字符串是否为空

public boolean isEmpty()

获取字符串长度

 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方法,代码如下:

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编程及计算机技术的本质。用心写作,原创文章,保留所有版权。

-----------

相关好评原创文章

计算机程序的思维逻辑 (6) - 如何从乱码中恢复 (上)?

计算机程序的思维逻辑 (7) - 如何从乱码中恢复 (下)?

计算机程序的思维逻辑 (8) - char的真正含义

计算机程序的思维逻辑 (28) - 剖析包装类 (下)

计算机程序的思维逻辑 (29) - 剖析String的更多相关文章

  1. Java编程的逻辑 (29) - 剖析String

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

  2. 计算机程序的思维逻辑 (53) - 剖析Collections - 算法

    之前几节介绍了各种具体容器类和抽象容器类,上节我们提到,Java中有一个类Collections,提供了很多针对容器接口的通用功能,这些功能都是以静态方法的方式提供的. 都有哪些功能呢?大概可以分为两 ...

  3. 计算机程序的思维逻辑 (30) - 剖析StringBuilder

    上节介绍了String,提到如果字符串修改操作比较频繁,应该采用StringBuilder和StringBuffer类,这两个类的方法基本是完全一样的,它们的实现代码也几乎一样,唯一的不同就在于,St ...

  4. 计算机程序的思维逻辑 (31) - 剖析Arrays

    数组是存储多个同类型元素的基本数据结构,数组中的元素在内存连续存放,可以通过数组下标直接定位任意元素,相比我们在后续章节介绍的其他容器,效率非常高. 数组操作是计算机程序中的常见基本操作,Java中有 ...

  5. 计算机程序的思维逻辑 (48) - 剖析ArrayDeque

    前面我们介绍了队列Queue的两个实现类LinkedList和PriorityQueue,LinkedList还实现了双端队列接口Deque,Java容器类中还有一个双端队列的实现类ArrayDequ ...

  6. 计算机程序的思维逻辑 (51) - 剖析EnumSet

    上节介绍了EnumMap,本节介绍同样针对枚举类型的Set接口的实现类EnumSet.与EnumMap类似,之所以会有一个专门的针对枚举类型的实现类,主要是因为它可以非常高效的实现Set接口. 之前介 ...

  7. 计算机程序的思维逻辑 (38) - 剖析ArrayList

    从本节开始,我们探讨Java中的容器类,所谓容器,顾名思义就是容纳其他数据的,计算机课程中有一门课叫数据结构,可以粗略对应于Java中的容器类,我们不会介绍所有数据结构的内容,但会介绍Java中的主要 ...

  8. 计算机程序的思维逻辑 (40) - 剖析HashMap

    前面两节介绍了ArrayList和LinkedList,它们的一个共同特点是,查找元素的效率都比较低,都需要逐个进行比较,本节介绍HashMap,它的查找效率则要高的多,HashMap是什么?怎么用? ...

  9. 计算机程序的思维逻辑 (54) - 剖析Collections - 设计模式

    上节我们提到,类Collections中大概有两类功能,第一类是对容器接口对象进行操作,第二类是返回一个容器接口对象,上节我们介绍了第一类,本节我们介绍第二类. 第二类方法大概可以分为两组: 接受其他 ...

随机推荐

  1. 恋爱虽易,相处不易:当EntityFramework爱上AutoMapper

    剧情开始 为何相爱? 相处的问题? 女人的伟大? 剧情收尾? 有时候相识即是一种缘分,相爱也不需要太多的理由,一个眼神足矣,当EntityFramework遇上AutoMapper,就是如此,恋爱虽易 ...

  2. 继电器是如何成为CPU的(1)

    继电器是如何成为CPU的(1) ——<穿越计算机的迷雾>整理和总结 究竟是如何设计的电路,具有计算和控制的智力? 这一点也不高深.本系列文章从初中学的最简单的电路图说起,看看能不能从最初的 ...

  3. java中servlet的各种路径

    1. web.xml中<url-pattern>路径,(叫它Servlet路径!) > 要么以“*”开关,要么为“/”开头 2. 转发和包含路径 > *****以“/”开头:相 ...

  4. 07. Web大前端时代之:HTML5+CSS3入门系列~H5 地理位置

    Web大前端时代之:HTML5+CSS3入门系列:http://www.cnblogs.com/dunitian/p/5121725.html 源码:https://github.com/duniti ...

  5. 浅谈web攻防

    CSRF 跨站请求伪造(Cross-Site Request Forgery) -原理- 从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤: 1.登录受信任网站A,并在本地生成Coo ...

  6. SignalR SelfHost实时消息,集成到web中,实现服务器消息推送

    先前用过两次SignalR,但是中途有段时间没弄了,今天重新弄,发现已经忘得差不多了,做个笔记! 首先创建一个控制台项目Nuget添加引用联机搜索:Microsoft.AspNet.SignalR.S ...

  7. 【热门技术】EventBus 3.0,让事件订阅更简单,从此告别组件消息传递烦恼~

    一.写在前面 还在为时间接收而烦恼吗?还在为各种组件间的消息传递烦恼吗?EventBus 3.0,专注于android的发布.订阅事件总线,让各组件间的消息传递更简单!完美替代Intent,Handl ...

  8. 使用NUnit为游戏项目编写高质量单元测试的思考

    0x00 单元测试Pro & Con 最近尝试在我参与的游戏项目中引入TDD(测试驱动开发)的开发模式,因此单元测试便变得十分必要.这篇博客就来聊一聊这段时间的感悟和想法.由于游戏开发和传统软 ...

  9. bzoj3208--记忆化搜索

    题目大意: 花花山峰峦起伏,峰顶常年被雪,Memphis打算帮花花山风景区的人员开发一个滑雪项目.    我们可以把风景区看作一个n*n的地图,每个点有它的初始高度,滑雪只能从高处往低处滑[严格大于] ...

  10. React Native 之 Text的使用

    前言 学习本系列内容需要具备一定 HTML 开发基础,没有基础的朋友可以先转至 HTML快速入门(一) 学习 本人接触 React Native 时间并不是特别长,所以对其中的内容和性质了解可能会有所 ...