最近觉得自己学的东西常常会忘记,于是就想写一下笔记。总结一下String相关的一些内容,如果有什么理解有误的话欢迎大家指出。
目录
- Unicode 编码
- String属性
- 构造方法
- 常用函数
- 一些常被问到的问题
Unicode
         计算机诞生的时候,老美为了存储他们的英文单词和一些符号,与计算机有一套约定,整数45表示百分号%,整数50代表数字2,51表示数字3,这些约定成为ASCII
,他跟计算机约定好了从整数0-127所对应的字符。即1个字节就可以保存下来了。
         后来计算机被世界广泛应用,所以各个国家都需要与计算机做好约定,比如我们大中国也需要跟计算机约定好,但是128个字符明显就容不下我们中国五千年来的文化积累呀,于是我们和计算机说好,中文要占3个字节,比如0xB6A001
表示中文字“丁”,这种数值与中文的约定成为GBK
,那日本也需要与计算机做约定,韩国也需要。世界上那么多种语言,计算机在识别字符的时候还要先看看是什么国家跟他做了约定,非常麻烦。
         于是Unicode对这些编码进行了大一统,用6个字节表示所有的字符,范围从0
到0x10FFFF
,其中常用的字符放在0
到0xFFFF
的位置,每一个字符对应的数字叫做码点,
         后来人们发现,其实常用的字符用0
到0xFFFF
就可以表示了,后面那些字符用的很少,没必要每次都弄3个字节来存储,于是我们制定了可变长的UTF-8
和·UTF16
,在存储过程中,如果字符在0
到0xFFFF
之间就用2个字节,刚好一个char来存储,如果大于0xFFFF
的就要用4个字节存储,刚好2个char。在java中就是以这种方式存储字符的。
属性:
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
- char数组 被final修饰
- hash
-
serialVersionUID
这个与序列化有关,以后再总结
构造方法
-
默认构造函数
public String() { this.value = "".value; }
-
传入String
public String(String original) { this.value = original.value; this.hash = original.hash; }
-
传入char数组
public String(char value[]) { this.value = Arrays.copyOf(value, value.length); }
-
传入byte数组,需要传入字符集
public String(byte bytes[], String charsetName) throws UnsupportedEncodingException { this(bytes, 0, bytes.length, charsetName); }
平时我们的转码就需要依靠这个构造方法啦
-
传入
StringBuffer
public String(StringBuffer buffer) { synchronized(buffer) { this.value = Arrays.copyOf(buffer.getValue(), buffer.length()); } }
这里上了锁,所以是线程安全滴
-
传入
StringBuilder
public String(StringBuilder builder) { this.value = Arrays.copyOf(builder.getValue(), builder.length()); }
关于
StringBuilder
和StringBuffer
等等会讲 - 还有很多就不数啦
常用方法
equals
听说这个常常会被问到,本来equals方法是Object类里面的,String把他重写了一下,用来判断两字符串是否相等
public boolean equals(Object anObject) {
//判断是不是同一个引用
if (this == anObject) {
return true;
}
//判断是否为String类
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
//判断长度是否相等
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
//比较每一个字符
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
写的很精炼,里面有些地方值得我去学习的,看到instanceof我突然想起反射里面的一个小知识点
equalsIgnoreCase
忽略大小写比较两字符串是否相等
public boolean equalsIgnoreCase(String anotherString) {
return (this == anotherString) ? true
: (anotherString != null)
&& (anotherString.value.length == value.length)
&& regionMatches(true, 0, anotherString, 0, value.length);
}
这里调用了一个叫regionMatches
的方法,我也把他沾上来
public boolean regionMatches(boolean ignoreCase, int toffset,
String other, int ooffset, int len) {
char ta[] = value;
int to = toffset;
char pa[] = other.value;
int po = ooffset;
// Note: toffset, ooffset, or len might be near -1>>>1.
if ((ooffset < 0) || (toffset < 0)
|| (toffset > (long)value.length - len)
|| (ooffset > (long)other.value.length - len)) {
return false;
}
while (len-- > 0) {
char c1 = ta[to++];
char c2 = pa[po++];
if (c1 == c2) {
continue;
}
if (ignoreCase) {
// If characters don't match but case may be ignored,
// try converting both characters to uppercase.
// If the results match, then the comparison scan should
// continue.
char u1 = Character.toUpperCase(c1);
char u2 = Character.toUpperCase(c2);
if (u1 == u2) {
continue;
}
// Unfortunately, conversion to uppercase does not work properly
// for the Georgian alphabet, which has strange rules about case
// conversion. So we need to make one last check before
// exiting.
if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) {
continue;
}
}
return false;
}
return true;
}
他在比较的时候还先转大写再转小写,为什么要这么做大家看他的注释把我其实没看懂。
我想了一下,String不是带有一个toUppaerCase
的方法嘛?如果让我去写这个方法我可能就直接这样写了
s2.equals(s1.toUppaerCase());
不过看了看源码,我这样写Java会对字符串遍历两次,效率会比较低吧。
然后我又看了一下toUpperCase()
方法里面的代码
public String toUpperCase(Locale locale) {
if (locale == null) {
throw new NullPointerException();
}
int firstLower;
final int len = value.length;
/* Now check if there are any characters that need to be changed. */
scan: {
for (firstLower = 0 ; firstLower < len; ) {
int c = (int)value[firstLower];
int srcCount;
if ((c >= Character.MIN_HIGH_SURROGATE)
&& (c <= Character.MAX_HIGH_SURROGATE)) {
c = codePointAt(firstLower);
srcCount = Character.charCount(c);
} else {
srcCount = 1;
}
int upperCaseChar = Character.toUpperCaseEx(c);
if ((upperCaseChar == Character.ERROR)
|| (c != upperCaseChar)) {
break scan;
}
firstLower += srcCount;
}
return this;
}
值得注意的是其中两行代码
c = codePointAt(firstLower);
srcCount = Character.charCount(c);
在Java中有两个获取下表字符的Unicode编码,
-
charAt
获取的是0
-0xFFFFFF
的码点,3个字节,保存在int中 -
codePointAt
获取的是0
-0xFFFF
的码点,2个字节,保存在int中
大家顺着代码看,大概也能猜出来Character.charCount()
其实是计算这个字符到底是占2个字节的还是3个字节的。
顺便提一下,String的length
方法和codePointCount
方法也是一样的道理
-
length
返回的是字符串的长度,如果有超出0xFFFF
的字符会算两 -
codePointCount
返回真正的字符个数,一个字符对应一个码点
所以Java在大小写转换的时候还要考虑编码问题,学就是了。
compareTo
也是比较两个字符串,不过他与equals
不同,equals
传入的是Object,返回的是布尔值,compareTo
传入的是字符串,返回的是整型
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
返回值表示第一个出现不同的字符的编码差值。如果是相同的字符串则返回0。大家回忆一下equals
方法,他第一步是判断两个变量是否为同一个引用,这里为什么不去判断呢?解决这个问题我们要想一下字符串在内存中是怎么存储的。
其他函数
-
indexOf
可传入字符、字符数组,从左向右对入参进行索引,找不到返回-1 -
lastIndexOf
从右向左对入参进行索引,找不到返回-1 -
contains
判断是否包含子字符串,内部调用indexOf
-
toUpperCase
将字符串转换成大写 -
toLowerCase
将字符串转换成小写 -
trim
去除字符串前后的空格 -
replace
替换字符串,可传入正则表达式,内部真正做替换的是replaceAll
-
join
连接字符串,可以传入String
、StringBuffer
等
注意 : 字符串是不可变的,操作完一定要拿个新的字符串存于下返回值,例如·
trim
函数,他会新建一个char数组存放结果
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
一些常见问题
-
==
和equals
有什么区别 -
final
修饰的好处 - String、
StringBuffer
和StringBuilder
- String的创建方式
1. ==
和 equals
有什么区别
==
对于基本类型来说,是直接比较值的大小,而对于引用来说是比较地址是否相等,而String
的·equals
是比较两个字符串的内容是否相同。
可以发现Object
也有equals
方法,内部其实是调用==
public boolean equals(Object obj) {
return (this == obj);
}
2. 为什么String
用final
修饰
两个目的,安全 和 高效
看下图,现在有两个引用指向字符串,我们的字符串是存储在常量池中的,如果字符串设计成可变的话,s1
不小心对字符串的内容修改了,我们用s2
取值的时候发现他变了,会引起不堪设想的灾难,关于常量池
的内容晚点我也总结一下。
3. String
、StringBuffer
和StringBuilder
的区别
SringBuffer
和StringBuilder
主要用于字符串的拼接。
字符串的拼接方法有很多,下面我们来分析一下:
-
+
:
由于String
的不可变性,在循环体中对字符串拼接的时候每次都要创建一个对象String s = ""; for(int i = 0; i < 100000; i++) { s = s + "test" }
每次都需要在创建新的字符串变量,所以·
+
适用于常量字符串的拼接,编译器会在编译的时候帮我们拼接好。 -
String
的join
方法其实内部是使用了
StringJoin
方法实现的,StringJoiner
内部其实是使用StringBuilder
的。 -
StringBufer
我们可以看看他的
append
方法public synchronized StringBuffer append(CharSequence s) { toStringCache = null; super.append(s); return this; }
加了同步锁,所以是线程安全的,返回的是this,所以支持链式操作
由于加了同步锁,性能相对没有
StringBuilder
高 -
StringBuilder
public StringBuilder append(String str) { super.append(str); return this; }
没有带锁,线程不安全,但是效率会皮较高,关于线程安全的知识我稍后也会总结
总结:因此,如果是常量或者一两个变量之间的拼接我们可以直接使用+
,如果是需要重复拼接的话,不考虑线程安全就是用StringBuilder
,考虑则使用StringBuffer
.
关于StringJoiner
有点像Python里面的join
,它可以帮我们在拼接的时候加入分隔符、前缀和后缀
构造函数
public StringJoiner(CharSequence delimiter,
CharSequence prefix,
CharSequence suffix) {
Objects.requireNonNull(prefix, "The prefix must not be null");
Objects.requireNonNull(delimiter, "The delimiter must not be null");
Objects.requireNonNull(suffix, "The suffix must not be null");
// make defensive copies of arguments
this.prefix = prefix.toString();
this.delimiter = delimiter.toString();
this.suffix = suffix.toString();
this.emptyValue = this.prefix + this.suffix;
}
可以传入分隔符、前缀和后缀
两个StringJoiner
可以使用merge
拼接
public StringJoiner merge(StringJoiner other) {
Objects.requireNonNull(other);
if (other.value != null) {
final int length = other.value.length();
// lock the length so that we can seize the data to be appended
// before initiate copying to avoid interference, especially when
// merge 'this'
StringBuilder builder = prepareBuilder();
builder.append(other.value, other.prefix.length(), length);
}
return this;
}
4. String的创建方式
String有两种创建方式
-
String s1 = "Rhythm";
s1
在创建时,会在常量池中看看有没有这个字符串,有就直接返回句柄(地址),没有就创建在返回句柄(地址) -
String s2 = new String("rHYTHM");
直接在堆中创建,调用
intern
方法可以将其存入常量池
在JDB1.7
后的版本中,编译器会对字符串进行优化
String s1 = "AAA" + "BBB";
String s2 = "AAABBB";
System.out.println(s1 == s2); //true
返回的是true,大家平时可以稍加注意一下
第一次写文章,写得不咋地,大家见谅,下一篇打算写一下JVM的内存模型,顺便交代一下String是怎么存储的。