Java 字符串
字符串 (续)
1.6 String 的 intern() 方法
intern()
这个方法是一个 native 的方法
(使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用)
- 如果常量池中存在当前字符串, 就会直接返回当前字符串, 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回
String.intern()
方法在执行时的策略
-
Java 7 之前,执行
String.intern()
方法的时候,不管对象在堆中是否已经创建,字符串常量池中仍然会创建一个内容完全相同的新对象,将创建的对象的引用放回 -
Java 7 之后,由于字符串常量池放在了堆中,执行
String.intern()
方法的时候,如果对象在堆中已经创建了,字符串常量池中就不需要再创建新的对象,而是直接保存堆中对象的引用,然后将其返回,同时节省了一部分的内存空间 -
经典例题
public static void main(String[] args) { String s = new String("1"); /** * 堆中存在一个对象: * String("1") * 字符串常量池中存在一个对象: * String("1") * 将堆中的String("1")的引用返回 */ s.intern(); // 字符串常量池中存在,不作操作 String s2 = "1"; /** * 显示声明,先看字符串常量池中是否拥有该对象(或者该对象的引用) * 存在:直接将其引用返回 * 不存在:直接在字符串常量池中创建并返回该对象的引用 * 此时存在String("1"),直接将其引用返回 */ System.out.println(s == s2); // 一个是堆中对象的引用,一个是字符串常量池中对象的引用 String s3 = new String("1") + new String("1"); /** * 堆中存在三个对象: * 一个StringBuilder("11")对象,两个String("1","11")对象 * 字符串常量池中存在一个对象: * String("1") * 将堆中的String("11")的引用返回 */ s3.intern(); /** * 此时,字符串常量池中没有Stirng("11")的对象 * JDK 7之前:直接在字符串常量池中创建一个Stirng("11")的对象 * JDK 7之后:将堆中的Stirng("11")对象的引用保存一份到字符串常量池中 */ String s4 = "11"; /** * 显示声明,先看字符串常量池中是否拥有该对象(或者该对象的引用) * 存在:直接将其引用返回 * 不存在:直接在字符串常量池中创建并返回该对象的引用 * 由于调用了intern()方法: * JDK 7之前:此时字符串常量池中存在Stirng("11")的对象 * JDK 7之后:此时字符串常量池中存在堆中Stirng("11")的对象的引用 * 将调用了intern()方法的结果返回 */ System.out.println(s3 == s4); /** * 前提:先调用intern()方法后再进行双引号创建 * JDK 7之前:一个是堆中对象的引用,一个是字符串常量池中对象的引用 * JDK 7之后:两个都是堆中对象的引用 */ } // JDK 7之前: false false // JDK 7之后: false true
调整
intern()
方法的位置:public static void main(String[] args) { String s = new String("1"); /** * 堆中存在一个对象: * String("1") * 字符串常量池中存在一个对象: * String("1") * 将堆中的String("1")的引用返回 */ String s2 = "1"; /** * 显示声明,先看字符串常量池中是否拥有该对象(或者该对象的引用) * 存在:直接将其引用返回 * 不存在:直接在字符串常量池中创建并返回该对象的引用 * 此时存在String("1"),直接将其引用返回 */ s.intern(); // 字符串常量池中存在,不作操作 System.out.println(s == s2); String s3 = new String("1") + new String("1"); /** * 堆中存在三个对象: * 一个StringBuilder("11")对象,两个String("1","11")对象 * 字符串常量池中存在一个对象: * String("1") * 将堆中的String("11")的引用返回 */ String s4 = "11"; /** * 显示声明,先看字符串常量池中是否拥有该对象(或者该对象的引用) * 存在:直接将其引用返回 * 不存在:直接在字符串常量池中创建并返回该对象的引用 * 此时由于还未调用intern()方法,所以直接在字符串常量池中创建String("11")对象,并将其引用返回 */ s3.intern(); // 字符串常量池中存在,不作操作 System.out.println(s3 == s4); /** * 创建对象完成后才调用intern()方法,此时字符串常量池中已经存在对象,所以没有影响 * 一个是堆中对象的引用,一个是字符串常量池中对象的引用 */ } // JDK 7之前: false false // JDK 7之后: false false
1.7 字符串的比较、拼接与分割
1.7.1 比较
-
==
操作符用于比较两个对象的地址是否相等 -
equals()
方法用于比较两个对象的内容是否相等// Object 类的equals 源码 public boolean equals(Object obj) { return (this == obj); }
Object 类的
equals()
方法默认采用的是==
操作符进行比较。假如子类没有重写该方法的话,那么==
操作符和equals()
方法的功效就完全一样 – > 比较两个对象的内存地址是否相等// JDK 8下的 String.equals() 源码 public boolean equals(Object anObject) { if (this == anObject) { return true; } 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; } // JDK 11下的 String.equals() 源码 public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String aString = (String)anObject; if (coder() == aString.coder()) { return isLatin1() ? StringLatin1.equals(value, aString.value) : StringUTF16.equals(value, aString.value); } } return false; }
如果两个字符串对象可以
==
,那就直接返回true
-
(JDK 8)
否则判断其类型是否为String
类型,将其强制转换为String
类型,然后将其值赋予char[]
数组,然后挨个比较两个字符数组值是否相等,只要有一个不相等就返回false -
(JDK 11)
否则判断其类型是否为String
类型,将其强制转换为String
类型,判断其编码类型,调用对应的编码其下的equals()
方法,将对应的字符串内容转化为byte[]
数组,然后挨个比较两个字节数组值是否相等,只要有一个不相等就返回false
-
1.7.2 拼接
append()
、+
、concat()
、 join()
循环体内,拼接字符串最好使用 StringBuilder 的 append()
方法,而不是 + 号操作符
-
String 字符串是不可变对象,只要对其进行相关操作就会让其发生变化,就会重新生成新对象,这样在循环时会增加很多不必要的开销
-
StringBuilder 是可变对象,其下的
append()
方法就是直接在建好的对象后面直接添加,不会新生成对象,而且其速度很快 -
简单说明:
StringBuilder
在创建对象时通过构造函数初始化,若未初始化则默认初始长度为16value[]
: 用来存储StringBuilder
对象中的字符count
: 记录StringBuilder
对象中所存字符的实际数量,即实际内容长度Stringbuilder
对象将所存储的内容存在value数组中,并通过count
来记录内容的长度,所以count
始终小于等于value
数组的长度
// StringBuilder.append() 源码 @Override public StringBuilder append(String str) { super.append(str); return this; } /** * super.append() 其父类的方法 * AbstractStringBuilder.append() 源码 * */ /** * 参数 String 的字符被追加的长度增加此序列的长度参数 * 如果 str 是 null,那么四个字符"n","u","l","l"被追加 * 否则调用ensureCapacityInternal() 方法以及 getChars */ public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); // 将拼接的字符串 str 复制到目标数组 value 中 str.getChars(0, len, value, count); // 更新数组的长度 count count += len; return this; } /** * 如果 str 是 null,那么四个字符"n","u","l","l"被追加,然后返回 */ private AbstractStringBuilder appendNull() { int c = count; ensureCapacityInternal(c + 4); final char[] value = this.value; value[c++] = 'n'; value[c++] = 'u'; value[c++] = 'l'; value[c++] = 'l'; count = c; return this; } /** * minimumCapacity : 实际内容长度 + 添加的字符串内容长度 * 如果该长度大于存储 StringBuilder 对象字符的内容长度大小 * 则需要重新对 value(StringBuilder对象的字符内容) 进行赋值 * Arrays.copyOf() 拷贝一份之前的 value ,然后追加一部分空间 * 追加空间调用 newCapacity() 方法 */ private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) { value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } } /** * minCapacity : minimumCapacity(实际内容长度 + 添加的字符串内容长度) * 此处的 value 为还没有重新赋值的 StringBuilder 对象的字符内容 * newCapacity : 新空间大小 * 如果 newCapacity 小于等于0或者空间最大值都小于 newCapacity * 此时调用 hugeCapacity */ private int newCapacity(int minCapacity) { // overflow-conscious code int newCapacity = (value.length << 1) + 2; if (newCapacity - minCapacity < 0) { newCapacity = minCapacity; } return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) ? hugeCapacity(minCapacity) : newCapacity; } /** * 如果 minCapacity 大于空间容量最大值,则抛出异常 OutOfMemoryError */ private int hugeCapacity(int minCapacity) { if (Integer.MAX_VALUE - minCapacity < 0) { // overflow throw new OutOfMemoryError(); } return (minCapacity > MAX_ARRAY_SIZE) ? minCapacity : MAX_ARRAY_SIZE; }
-
concat()
方法相较于+
:// 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); }
在遇到字符串为
null
的时候,会抛出NullPointerException
如果拼接的字符串是一个空字符串
("")
,那么concat
的效率要更高一点如果拼接的字符串非常多,
concat()
的效率就会下降,因为创建的字符串对象越来越多 -
join()
静态方法// join 源码 public static String join(CharSequence delimiter, CharSequence... elements) { Objects.requireNonNull(delimiter); Objects.requireNonNull(elements); // Number of elements not likely worth Arrays.stream overhead. StringJoiner joiner = new StringJoiner(delimiter); for (CharSequence cs: elements) { joiner.add(cs); } return joiner.toString(); }
该静态方法里面新建了一个叫
StringJoiner
的对象,然后通过 ~循环把可变参数添加了进来,最后调用toString()
方法返回String
实际的工作中,
org.apache.commons.lang3.StringUtils
的join()
方法也经常用来进行字符串拼接// 源码 public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) { if (array == null) { return null; } if (separator == null) { separator = EMPTY; } final StringBuilder buf = new StringBuilder(noOfItems * 16); for (int i = startIndex; i < endIndex; i++) { if (i > startIndex) { buf.append(separator); } if (array[i] != null) { buf.append(array[i]); } } return buf.toString(); }
- 该方法不用担心 NullPointerException
StringUtils.join(null) // null StringUtils.join([]) // "" StringUtils.join([null]) // "" StringUtils.join(["a", "b", "c"]) // "abc" StringUtils.join([null, "", "a"]) // "a"
1.7.3 分割
split()
-
在拆分之前,要先进行检查,判断一下这串字符是否包含分割内容,否则应该抛出异常
public class Test { public static void main(String[] args) { String cmower = "Hello, world!"; if (cmower.contains(",")) { String [] parts = cmower.split(","); System.out.println("第一部分:" + parts[0] +" 第二部分:" + parts[1]); } else { throw new IllegalArgumentException("当前字符串没有包含逗号"); } } } // 第一部分:Hello 第二部分:world!
-
在字符串是确定的情况下,最重要的是分隔符是确定的,否则就会抛出异常
-
一般情况下都是使用已经确定的字符进行分割,所以很少会使用到正则表达式进行分割
-
使用
split()
方法时,一定要注意该字符串是否包含特殊字符,而且需要注意的是:- 当特殊字符为
.
或者|
时需要添加转义字符 -->split("\\.")
或者split("\\|")
- 当特殊字符为
-
遇到特殊符号使用正则表达式(GitHub上开源正则学习文档)(一组由字母和符号组成的特殊文本,它可以用来从文本中找出满足你想要的格式的句子)
StringTokenizer
JDK中提供的专门用来处理字符串分割子串的工具类
public class Test {
public static void main(String[] args) {
String str01 = "Hello world!";
StringTokenizer str02 = new StringTokenizer(str01);
while (str02.hasMoreElements()){
System.out.println(str02.nextElement());
}
}
}
// Hello
// world!
-
StringTokenizer
常用的函数:-
hasMoreElements()
和hasMoreTokens()
这两个方法用法一样,功能也一样,表示
StringTokenizer
是否还有元素,当其为False,表示StringTokenzier为空 -
nextElement()
和nextToken()
这两个方法的用法是一样的,返回此
StringTokenizer
的下一个标记 -
int countTokens()
返回
nextToken()
方法被调用的次数。
-
-
StringTokenizer
有三个构造函数:-
StringTokenizer(String str)
默认以
" \t\n\r\f"
(前有一个空格,引号不是)为分割符,该方法默认以空格、回车、换行等为分隔符,默认不返回分隔符 -
StringTokenizer(String str, String delim)
指定
delim
为分割符,默认不返回分隔符// 实例 public class Test { public static void main(String[] args) { String str01 = "HelloWorld!"; StringTokenizer str02 = new StringTokenizer(str01, "o"); while (str02.hasMoreElements()){ System.out.println(str02.nextElement()); } } } // Hell // W // rld! //
-
StringTokenizer(String str, String delim, boolean returnDelims)
returnDelims
为true
时,默认返回分隔符// 实例 public class Test { public static void main(String[] args) { String str01 = "HelloWorld!"; StringTokenizer str02 = new StringTokenizer(str01, "o", true); while (str02.hasMoreElements()){ System.out.println(str02.nextElement()); } } } // Hell // o // W // o // rld! //
subString()
与indexOf()
-
这两个方法组合起来可以分割字符
// 实例
public class Test {
public static void main(String[] args) {
String str01 = "HelloWorld!";
// 获取分隔符的索引:当字符串中有多个分割符时,以找到的第一个分割符为主
int start01 = str01.indexOf("o");
String str02 = str01.subString(start01);
int start02 = str02.indexOf("o");
// 分割号的字符串子串
String str03 = str01.subString(0, start01);
String str04 = str01.subString(start01, start02);
String str05 = str01.subString(start02);
}
}
- 注意:该方法不适合用来分割,仅仅介绍有这种方法
有任何错误请指正,谢谢您的阅读,希望对您有所帮助。