本系列文章经补充和完善,已修订整理成书《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. mybatis_个人总结

    在使用mybatis框架开发数据访问层的过程中,我在这段时间遇到很多细节问题困住我,在这里我来分享一下我遇到的坑,希望能帮到大家. 一.mybatis动态代理方式开发的规范: 1.注意在mybatis ...

  2. spring源码分析之freemarker整合

    FreeMarker是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页.电子邮件.配置文件.源代码等)的通用工具. 它不是面向最终用户的,而是一个Java类库,是一款程 ...

  3. hadoop2.7之Mapper/reducer源码分析

    一切从示例程序开始: 示例程序 Hadoop2.7 提供的示例程序WordCount.java package org.apache.hadoop.examples; import java.io.I ...

  4. .Net Core上也可以使用的二维码组件

    我Fork了QRCoder,并且兼容了.Net Core,图形库用的是ZKWeb.System.Drawing Github: https://github.com/zkweb-framework/Q ...

  5. .NET应用和AEAI CAS集成详解

    1 概述 数通畅联某综合SOA集成项目的统一身份认证工作,需要第三方系统配合进行单点登录的配置改造,在项目中有需要进行单点登录配置的.NET应用系统,本文专门记录.NET应用和AEAI CAS的集成过 ...

  6. SQL*Plus生成html文件

    最近使用SQL*Plus命令生成html文件,遇到一些有意思的知识点,顺便记录一下,方便以后需要的时候而这些知识点又忘记而捉急.好记性不如烂笔头吗! 为什么要用SQL*Plus生成html文件?   ...

  7. MEF学习

    一.   什么是MEF MEF(Managed Extensibility Framework)是一个用于创建可扩展的轻型应用程序的库. 应用程序开发人员可利用该库发现并使用扩展,而无需进行配置. 扩 ...

  8. 【腾讯Bugly干货分享】程序员们也该知道的事——“期权和股票”

    本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq.com/s/pfj9NLLuKYAfJJF84R9WAw 作者:B ...

  9. lucene 基础知识点

    部分知识点的梳理,参考<lucene实战>及网络资料 1.基本概念 lucence 可以认为分为两大组件: 1)索引组件 a.内容获取:即将原始的内容材料,可以是数据库.网站(爬虫).文本 ...

  10. CSharpGL(9)解析OBJ文件并用CSharpGL渲染

    CSharpGL(9)解析OBJ文件并用CSharpGL渲染 2016-08-13 由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了.CSharpGL源码中包含10多个独立的Demo ...