noteless 头像

[十二]基础数据类型之String

在正式介绍String之前,我们先介绍下CharSequence

char + sequence 就是字符的序列的意思

Java中万事万物都是对象类型

而对于字符的序列,也就是多个char, 这么一种东西, 使用CharSequence这个接口来描述

既然是接口,自然规定了作为字符序列的基本协议

CharSequence简介


char charAt(int index); 返回指定索引的char
int length() 返回字符序列的长度
CharSequence subSequence(int start, int end) 返回子序列
String toString() 返回一个包含此序列中字符的字符串该字符串与此序列的顺序相同
default IntStream chars() 返回此序列的int stream,每个char零位扩展为int
default IntStream codePoints() 返回此序列的代码点的stream
 
我们都知道1.8的一个亮点就是stream和lambda
default方法也是1.8新增的,默认实现
既然CharSequence表示了 字符序列这么一个概念
显然,String内部是char数组,就是一个char的序列
 

String简介

String 类代表字符串
Java 程序中的所有字符串字面值(如 "abc" )都是String的实例
内部有一个char[] 

注意到 上面的final, 字符串是常量;它们的值在创建之后不能更改
String str = "abc";
等效于:
char data[] = {'a', 'b', 'c'};
String str = new String(data);
Java 语言提供对字符串串联符号("+")以及将其他对象转换为字符串的特殊支持
说白了就是+被重载过了,也提供了强大的将对象转换为字符串的能力
char是UTF-16中的代码单元,所以字符序列就是代码单元的序列
仍旧是一个char可能是一个字符,也可能是半个字符
String 类提供处理 Unicode 代码点(即字符)和 Unicode 代码单元(即 char 值)的方法
 

属性CASE_INSENSITIVE_ORDER

这就是一个比较器
逻辑也很简单,两个String 按照字典顺序进行比较,忽略大小写的
以两者length小的那个作为循环次数,进行循环
如果第一个相等比较第二个,依次类推,直到不一样
如果所有的都相等,那么就比较长度了 return n1 - n2
 

字符与字节数组

在继续下去之前,再次简单介绍下字符与字节数组的关系
字符到字节,是一个编码的过程
字节到字符是一个解码的过程
同样的一个字符,在不同的字符集和编码方式下,实际存储的值,将是不同的
比如前面说的Unicode字符集,UTF8 和UTF16编码后的数据是不同的
这个编码后的数据,也就是字节  , 他们是不一样的 
同样的一个编码值,在不同的字符集中,可能代表着不同的字符
所以字符与字节之间,必然有编码参与其中
这个编码环节是必然存在的,否则,你就没办法把字节与字符联系起来
一个字符可以根据 字符集编码 进行多种方式的编码
一个字节数组也可以根据 字符集编码 进行多种方式的解码

对于同一个字符,不管进行何种编码,当他们按照当初编码的方式进行解码时,必然对应的还是同样的那个字符
 

操作系统的文件都是以字节序列的形式存储的,所以任何一个文件都是有编码的
比如你在txt文件中输入了一个字符
这个字符 底层就会使用指定的编码存储到字节中
软件本身又把这个编码以字符的形式呈现出来
所以你才看得到是一个字符
比如这个文件中11111.txt中,存储了一个汉字春天的 " 春" 
编码方式是UTF8
二进制软件查看是E6 98 A5
与我们进行UTF8 编码计算的结果是对应的
ANSI编码
不同的国家和地区制定了不同的标准
由此产生了 GB2312、GBK、Big5、Shift_JIS 等各自的编码标准
这些使用 1 至 4 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码
在简体中文Windows操作系统中,ANSI 编码代表 GBK 编码;
在日文Windows操作系统中,ANSI 编码代表 Shift_JIS 编码

再看下面一个例子
使用ultraedit 新建了一个文件,里面写了一个汉字 "春",
其实这个默认格式就是操作系统的编码,也就是ANSI  也就是GBK
查看二进制编码为 B4 BA
然后我们再去对照GBK的码表,你会发现完全对的上
任何一个文件,他其实有自带或者说默认的一个编码
凡是呈现字符的地方,都有一个编码在默默地支撑,才能够让你看得见,看得清楚字符
这个字符的保存 , 就是字符按照编码表  编码 成字节序列的过程
这个字符的呈现 , 就是字节序列按照编码表  解码 成字符的过程
当你使用计算机,进行字符处理工作的时候,无时无刻都在进行着编码与解码

String构造方法

String是常用类之一,所以提供了非常丰富的方法
String是字符序列  内部是char[]   char就是一个十六进制的数 16位表示
所以char[] 可以用来构造String
char是16位数能够表示代码单元, int自然可以直接表示一个代码点了,所以也可以使用int来构造String
另外再加上我们刚才关于字节数组与字符关系的介绍,也可以使用字节数组构造String
 
下面表格是几个基本的构造方法
String() 空String ,没啥必要因为String是不可变的

String(char[])
String(char[], int, int)
借助于字符数组或者字符数组的一部分创建对象
内部本来就是字符数组 char[]  所以自然可以使用char[]构造
直接进行拷贝,所以对原有字符数组的修改不影响String对象

String(int[], int, int) 使用代码点构造String
public String(int[] codePoints, int offset, int count)
offset 和 count为范围限制
String(String)
String(StringBuffer)
String(StringBuilder)
 

getBytes 方法

先提一下另外一个方法,getBytes
使用指定的字符集将此 String 编码为 byte 序列
我的编辑器环境是UTF8编码的      
"春" 的UTF8编码上面已经分析了
 
也就是说我这边有一个UTF8的字符"春"  源文件中保存的是 E6 98 A5
对于下面所有的getBytes来说,"春" 这个字符形状符号是不变的
获得的字节数组就是  这个字符形状符号 根据不同字符集编码方式, 编码而得到的字节数组
下面的各种转换换一个描述就是:UTF8的字符"春" ,在其他的字符集下面,编码都是多少啊?
为什么UTF-8 是-26  -104 -91 ? 而不是e6 98 a5?进制问题
getBytes总共三种形式
指定编码或者使用默认
getBytes(String)
getBytes(Charset)
getBytes()
还有一种已经弃用 了
 

通过字节数组 byte[] 构造

String提供了6个跟byte[]  相关的构造方法
 
getBytes方法是字符是固定的, 固定的以UTF8格式存储在我的源文件中,
然后根据不同的编码方式,转换为字节数组 byte[]
 
String的构造方法,则是将各个已经编码过的字节数组 byte[] 按照指定的编码方式解析 还原成为一个字符
然后再将这个字符以char[]  也就是UTF-16的方式进行存储的
我的源文件IDE环境是UTF8那么最终构造的String就是UTF8的,不会是其他的
 
比如下面的构造方法,使用前面示例中的 bytes数组
 
然后使用  String(byte[], String)   进行构造
看得很清楚
String字符串 s1 中存储的value 是Unicode的代码点U+6695    (0号平面,一个代码单元就是一个代码点)
也就是十进制的26149
 
使用byte[] 字节数组构造String的过程是下图这样子的
字节数组,根据指定字符编码转换为那个字符
然后在把字符按照UTF16 进行编码 存储到String中的char[]
上面的例子可以很好地印证这一点,字节数组是[-76, -70] 
也就是 :    ffffffb4   ffffffba   也就是 B4 BA 明明是GBK的"春"
根本就不是6625 对应关系就是他们表示的是同一个字符
既然字节数组与字符的转换离不开编码,所以自然通过byte[] 构造String对象时,必须要有编码
不设定并不是没有,而是使用默认的
既然使用字节数组,那么有的时候可能需要指定范围,所以有两个根本的构造方法
然后还有默认字符编码的简化形式
再然后就是长度为整个字节数组的简化形式
这几个构造方法根本在于理解 字节数组与字符的转换
以及必须的byte[] 字节数组  以及  编码
 

valueOf

valueOf 系列用来包装
String中用来将基本类型 以及 Object 转换为String
char相关的都是直接构造String对象
其余(除了boolean,他是转换为字符串  true和false返回)
都是toString
 

copyValueOf

copyValueOf方法内部就是直接调用的两个构造方法
还不如直接使用new创建来的直接,只不过使用这个方法有更好的可读性
 

获取指定位置代码单元和代码点的方法

charAt(int) 
返回指定索引处的 char 值  索引范围为从 0 到 length() - 1
简单粗暴,  不管三七二十一就是代码单元  
如果是辅助平面,那就可能是代理项
codePointAt(int)
返回指定索引处的代码点,  范围从 0 到 length() - 1
他跟Character中的codePointAt方法逻辑含义是一样的
如果是高代理,如果下一个也在掌控范围内,如果下一个是低代理,那么返回代码点
否则,返回代码单元 也就是一个char
codePointBefore(int)
返回指定索引之前的字符(Unicode 代码点)  其范围从 1 到 length
他跟Character中的codePointBefore方法逻辑含义是一样的
如果index-1 是低代理,如果在往前一个index-2 也是有效范围内,如果他还恰好是一个高代理,返回代码点
否则,返回代码单元,也就是一个char
codePointCount(int, int)
此 String 的指定文本范围中的 Unicode 代码点数
文本范围始于指定的 beginIndex,一直到索引 endIndex - 1 处的 char,  包含头不包含尾
该文本范围的长度(用 char 表示)是 endIndex-beginIndex
由于一个代码点的代码单元个数可能是1个可能是2个,所以代码点的个数需要计算,就不直观了
他跟Character中的codePointCount方法逻辑含义是一样的
offsetByCodePoints(int, int)
他跟Character中的offsetByCodePoints方法逻辑含义是一样的
返回此 String 中从给定的 index 处偏移 codePointOffset 个代码点的索引
根本原因还是一个代码点的代码单元个数可能是1个可能是2个 
所以 偏移codePointOffset个代码点的 代码单元的个数不确定,需要调用方法计算
 

getChars(int, int, char[], int)复制

实例方法
就是一个复制方法,名字不太规范
复制String中指定索引开始的srcBegin 和 srcEnd   包含头不包含尾
到另一个字节数组 char dst[]中, 存放的起始位置为dstBegin
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin)

需要注意的是,复制的是char  代码单元
就是String 内部char[] 的下标索引
 

开始结束匹配校验

startsWith(String, int)
startsWith(String)
实例方法
测试String是否以指定的前缀开始
还可以指定起始位置处开始比较

从源代码看得出来,挨个比较内部的char
从头开始,全部一致才返回true

单参数是双参数的简化版本
endsWith(String) endwith就是从最后的指定参数长度的位置开始比较
 

indexOf 和lastIndexOf

indexOf 和XXXIndexOf系列都是获取下标索引相关
需要注意的是,他们的参数都是int或者String
也就是说这些方法都是真正的字符相关的
int indexOf(int ch)
int indexOf(int ch, int fromIndex)
返回   指定字符  在此字符串中第一次出现处的索引
返回的匹配的第一个

也可以指定检索的起始位置, 如果指定了索引
那么返回的值将  大于等于  指定的索引

换个说法:
如果是0号平面返回的是那个代码单元也就是代码点的索引
charAt(k) == ch   为 true 的最小 k 值  
如果是辅助平面返回的是高代理位的代码单元的索引  
codePointAt(k) == ch  为 true 的最小 k 值  
int indexOf(String str)
int indexOf(String str, int fromIndex)
返回  指定子字符串   在此字符串中第一次出现处的索引
返回匹配的第一个
 
也可以指定检索的起始位置,如果指定了索引
那么返回值需要大于等于 指定的索引
 
匹配的含义为startsWith(str) 为true
如果指定检索开始的位置,  那么
不仅仅startsWith(str) 为true 还需要索引满足指定的下标范围
否则仍旧是返回-1
lastIndexOf(int)
lastIndexOf(int, int)
返回指定字符在此字符串中最后一次出现处的索引
返回匹配的最后一个

也可以指定检索位置,但是这个检索位置与indexOf不同
indexOf中指定的索引,是从索引处往后
lastIndexOf指定的索引, 是反向,从索引处往前
指定了索引就要求 返回值 小于等于 指定索引

换个说法
如果是0号平面返回的是那个代码单元也就是代码点的索引
charAt(k) == ch   为 true 的最大 k 值 
如果是辅助平面返回的是高代理位的代码单元的索引 
codePointAt(k) == ch  为 true 的最大 k 值   并且  k 小于等于 指定的索引
lastIndexOf(String)
lastIndexOf(String, int)
返回指定 子字符串 在此字符串中最后一次出现处的索引
返回匹配的最后一个
 
也可以指定检索位置,检索索引的位置也是反向搜索
 
匹配的含义为startsWith(str) 为true
指定了索引就要求返回值 小于等于  指定索引
 
总共三个维度
匹配第一个或者最后一个 / 匹配字符或者字符串 / 是否指定查找范围 
8个方法
 
indexOf是从前往后匹配  匹配的是第一个 如果指定了下标索引,从索引处往后找  
返回的值要  大于等于 索引
 
lastIndexOf是从后往前匹配  匹配的是最后一个  如果指定了开始下表索引,是从索引处往前,反向查找
返回的值要  小于等于 索引
 
匹配字符如果是BMP,代码单元就是代码点,返回的就是那个代码单元也是代码点的索引
如果是辅助平面,一个代码点两个代码单元,返回的就是高代理位的索引  lastIndexOf和indexOf都是返回高代理项
 

length 

长度获取,内部char数组的长度
 

isEmpty()

 

hashCode

计算公式
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]


 

字符串匹配包含

测试两个字符串区域是否相等
toffset 表示当前对象this 开始的位置
other 表示另外一个String对象
ooffset 表示另外对象开始的位置
len 要匹配的长度
 
两个方法其中一个可以指定是否忽略大小写
s1.regionMatches(1,s2,3,4);  读作:
把s1 从索引1开始 同 s2 从索引3开始,比较len个长度,查看这个区域是否相等
public boolean regionMatches(                                int toffset, String other, int ooffset,int len)
public boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len)
contains 
contains也是一种匹配
当且仅当此字符串包含指定的 char 值序列时,返回 true
matches
此字符串是否匹配给定的正则表达式
public boolean matches(String regex)
 

相等比较

equals(Object)
equals方法也进行了重写
比较的是内部的char 序列是否相等
先看是否同一个对象,否则看是否String类
然后再看长度,长度相同挨个比较
 
contentEquals(StringBuffer)
contentEquals(CharSequence)
这两个方法   分别针对参数StringBuffer  和 CharSequence
他们都是  当且仅当表示相同的 char 值序列时,结果才为 true
比较的也是内容
上面的equals方法也是比较的内容
equalsIgnoreCase(String)
比较忽略大小写,底层依赖的就是区域的比较
只不过区域是整个字符串而已
compareToIgnoreCase(String)
字典顺序比较两个字符串,不考虑大小写
compareTo(String)
compareTo(String)方法是按照字典序进行排序的
如果字符本身全都相等,但是长度不同,返回长度差
 
 

子串获取

public String substring(int beginIndex)
public String substring(int beginIndex, int endIndex)
public CharSequence subSequence(int beginIndex, int endIndex)
subSequence 就是调用的subString方法
为什么还需要呢?就是为了遵循CharSequence协议
subString也是处理的char的索引,不是字符的
所以一个好好地字符,可能被你截取后,就成为乱码了
所以如果你想要截取子串也会出现乱码,可以通过offsetByCodePoints  获取指定个代码点后的索引
那么截取的绝对不会是乱码

看一个例子


0x1f310的高代理位在Character简介中计算过,它的值跟十进制的55356一样的

对于s 截取后,子串中仅仅是高代理项了
 

大小写转换

大小写的转换
可以指定Locale
不指定,等价于 指定默认值Locale.getDefault()
大小写映射关系基于 Character 类指定的 Unicode 标准版
toLowerCase(Locale)
toLowerCase()
toUpperCase(Locale)
toUpperCase()
 

split

根据匹配给定的正则表达式来拆分此字符串
子字符串按它们在此字符串中出现的顺序排列
如果表达式不匹配输入的任何部分,那么所得数组只具有一个元素,那就是这个字符串

public String[] split(String regex, int limit)
limit 不是什么索引下标,而是表达式模式应用的次数

如果该限制 n 大于 0,则模式将被最多应用 n - 1 次
数组的长度将不会大于 n,而且数组的最后一项将包含所有超出最后匹配的定界符的输入
如果 n 为非正,那么模式将被应用尽可能多的次数,而且数组可以是任何长度
如果 n 为 0,那么模式将被应用尽可能多的次数,数组可以是任何长度,并且结尾空字符串将被丢弃
 
例如,字符串 "boo:and:foo" 使用这些参数可生成以下结果:
 
split(String) 是split(String, int) 的简化形式

join

join用于将字符序列使用指定的符号进行拼接
需要注意的是,如果有元素为null ,那么"null" 将会被添加进来
public static String join(CharSequence delimiter,
                          CharSequence... elements)
 
public static String join(CharSequence delimiter,
                          Iterable<? extends CharSequence> elements)

替换

分为字符/字符序列/正则表达式替换
replace是字符/字符序列的替换
replaceXXX是正则的替换
public String replace(char oldChar, char newChar) 替换后,返回一个新的字符串
如果 oldChar 不存在,则返回这个 String 对象的引用
否则,创建一个新的 String 对象
所有的 oldChar 都被替换为 newChar
public String replace(CharSequence target, 
CharSequence replacement)
替换后,返回一个新的字符串
使用指定的字符序列进行替换
用 "b" 替换字符串 "aaa" 中的 "aa" 将生成 "ba" 而不是 "ab"
replaceFirst(String, String)
replaceAll(String, String)
 

concat 连接

将指定字符串连接到此字符串的结尾
如果参数字符串的长度为 0,则返回此 String 对象
否则,创建一个新的 String 对象,返回新创建的连接后的字符串
先复制一个到数组中
然后再把参数的复制到那个数组中
然后使用数组创建String
 

trim

trim()
最常用的String方法之一,去掉开头和结尾的空格
 
 
toString()
 
返回他自己本身
他本来就是一个String了
toCharArray() 将此字符串转换为一个新的字符数组
内部本身就是一个char[]
所以自然可以轻松的转换为char数组
数组拷贝了下
 

format

format
使用指定的格式字符串和参数返回一个格式化字符串
可以指定语言环境
内部还是使用的Formatter
 

intern

intern()
String 私有地维护了, 一个初始为空的字符串池
当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串
否则,将此 String 对象添加到池中,并返回此 String 对象的引用
 
它遵循以下规则:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true
 
对于直接定义的  "a"  "ab"  会进入这个字符串池
如果是new 创建的字符串对象不进入字符串池 
如果使用+ 得到的,两个都是字面的"a"  "ab" 形式会进入字符串池,否则如果有变量也不会进入


str5 和 str3 内容相同,String重写了equals方法,比较的是内容,所以true
str5 和 str3 一个是new出来的,所以地址不相等  false
str5.intern() 查找池中是否有"ab" 有的话返回引用,显然就是str3 所以true
str5.intern() 查找池中是否有"ab" 有的话返回引用,显然就是str3的地址
但是str4 是一个对象,他与str3 不是同一个对象所以不相等 false
最后一个都是获取"ab"的引用,显然是相等的

 

总结

String的根本就是字符序列
内部使用char[] 保存数据,而char 是UTF16中的代码单元
所以String中的很多方法自然也避免不了与Unicode UTF16的联系
在实际使用方法的时候,一定要稍微留意代码点与代码单元之间的关系
不过也不必过于担心,因为常用字符大多数都在0号平面内,很多方法用起来并不会有什么问题,哪怕你不曾留意
 
 
posted @ 2018-10-19 09:17  noteless  阅读(14543)  评论(7编辑  收藏  举报