Java编程的逻辑 (29) - 剖析String
本系列文章经补充和完善,已修订整理成书《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) - 如何从乱码中恢复 (下)?
Java编程的逻辑 (29) - 剖析String的更多相关文章
- 计算机程序的思维逻辑 (29) - 剖析String
上节介绍了单个字符的封装类Character,本节介绍字符串类.字符串操作大概是计算机程序中最常见的操作了,Java中表示字符串的类是String,本节就来详细介绍String. 字符串的基本使用是比 ...
- Java编程的逻辑 (51) - 剖析EnumSet
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
- Java编程的逻辑 (26) - 剖析包装类 (上)
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
- Java编程的逻辑 (27) - 剖析包装类 (中)
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- Java编程的逻辑 (28) - 剖析包装类 (下)
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- Java编程的逻辑 (32) - 剖析日期和时间
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
- Java编程的逻辑 (53) - 剖析Collections - 算法
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
- Java编程的逻辑 (48) - 剖析ArrayDeque
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...
- Java编程的逻辑 (30) - 剖析StringBuilder
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
随机推荐
- matlab中的Traing、Validation、Testing
<matlab神经网络30个案例分析> ROC曲线是反映敏感性和特异性连续变量的综合指标,roc曲线真阳性率为纵坐标,假阳性率为横坐标,在坐标上由无数个临界值求出的无数对真阳性率和假阳性率 ...
- C#基础课程之四集合(ArrayList、List<泛型>)
list泛型的使用 ArrayList list = new ArrayList(); ArrayList list = ); //可变数组 list.Add("我"); //Ad ...
- 使用C#和Thrift来访问Hbase实例
今天试着用C#和Thrift来访问Hbase,主要参考了博客园上的这篇文章.查了Thrift,Hbase的资料,结合博客园的这篇文章,终于搞好了.期间经历了不少弯路,下面我尽量详细的记录下来,免得大家 ...
- Zookeeper服务器配置项详解
文章转自: http://www.bug315.com/article/159.htm http://www.bug315.com/article/160.htm Zookeeper是通过一个***. ...
- Oozie工作流属性配置的方式与策略
本文原文出处: http://blog.csdn.net/bluishglc/article/details/46049817 Oozie工作流属性配置的三种方式 Oozie有三种方法可以给工作流提供 ...
- Http Post 二进制通信
客户端请求和接收(使用了httpclient4.3 和netty3.5) public static void httpPost11() { CloseableHttpClient httpClien ...
- java服务端技术
服务端框架:1.servlet2.netty 协议:1.http 1.02.http 1.1 数据库:mysql 对象关系映射(ORM)框架:mybatis 缓存:redis eclipse能运行,导 ...
- Python使用读写excel文件
Python使用openpyxl读写excel文件 这是一个第三方库,可以处理xlsx格式的Excel文件.pip install openpyxl安装.如果使用Aanconda,应该自带了. 读取E ...
- [Windows Azure] Building the web role for the Windows Azure Email Service application - 3 of 5
Building the web role for the Windows Azure Email Service application - 3 of 5. This is the third tu ...
- (原创)用c++11实现简洁的ScopeGuard
ScopeGuard的作用是确保资源面对异常时总能被成功释放,就算没有正常返回.惯用法让我们在构造函数里获取资源,当因为异常或者正常作用域结束,那么在析构函数里释放资源.总是能释放资源.如果没有异常抛 ...