“这是 JavaMemo 的第五篇原创
”
字符串,差不多是编程语言最常用的数据类型,本文会把 Java 字符串的知识点、易错点都拿出来仔细讲解一遍,无论菜鸟、老兵都能用得上,看完拿捏字符串。
以下是本文的知识大纲,大家搬好小板凳,准备上课啦。
Java 字符串--知识点大纲字符串在 Java 中实际上 String 类型的对象,String 对象可以包含一个字符序列(字符串)。字符串是 Java 中处理文本的方式。创建字符串后,可以在其中进行搜索,从中创建子字符串,对字符串的部分内容进行替换等等。
在 Java 中如果不给字符串对象进行初始化,其默认值是 null。
字符串 在 Java 9 之前在 JVM 内部使用编码为 UTF-16的字节表示。UTF-16 使用 2 个字节来表示单个字符。
UTF 是一种字符编码,可以表示来自许多不同语言(字母表)的字母。这就是为什么每个字符需要使用 2 个字节的原因 - 为了能够在字符串的单个字符中表示所有语言的字母。
从 Java 9 开始,JVM 可以使用一种称为紧凑字符串的新 Java 特性来优化字符串。紧凑字符串功能让 JVM 检测字符串是否仅包含 ISO-8859-1/Latin-1 字符。如果是这样,字符串将在内部仅使用 1 个字节表示每个字符。
字符串在创建时JVM就会检测字符串是否可以表示为紧凑字符串。为了安全字符串一旦创建就不可改变 。
在 Java 中的字符串本质上是对象。因此,需要使用 new 运算符来创建一个新的 Java String 对象。
String myString = new String("Hello World");
上面用 new 运算符创建字符串对象时,双引号内的文本是 String 对象创建出来后将包含的文本。
不过我们平时在创建字符串时,更习惯于用直接量赋值的方式直接创建。
String myString = "Hello World";
使用这种创建字符串的简短形式,无需将文本“Hello World”作为参数传递给 String 构造函数,只需将文本本身写入双引号内即可。Java 编译器将在内部计算出如何创建出表示给定文本的 Java String 对象。
Java 字符串的值可以接受一组转义字符,这些转义字符在创建的字符串中会被翻译成特殊字符。这些转义字符是:
转义字符 | 在字符串中转义后的值 |
---|---|
\\ | 在字符串中会被翻译成单个 \ 字符 |
\t | 在字符串中会被翻译成单个制表符 |
\r | 在字符串中会被翻译成单个回车符 |
\n | 在字符串中会被翻译成单个换行符 |
比如下面我们在字符串里嵌入了转移字符
String text = "\tThis text is one tab in.\r\n";
上面的字符串被解析后不会显示 \t \n 这些字符,而是会变成制表符开头(打印出来像是开头有缩进的文字),换行结尾的字符串。
如果使用相同的字符串字面值(比如"Hello World")进行不同的 String 变量声明,Java 虚拟机可能只会在内存中创建单个 String 实例。因此,字符串字面值会变成了事实上的常量或单例。初始化为相同字符串值的各种不同变量将指向内存中的那个相同 String 实例。
举例来说,有下面两个 String 变量的声明:
String myString1 = "Hello World";
String myString2 = "Hello World";
在这种情况下,Java 虚拟机会把变量 myString1 和 myString2 都指向同一个 String 对象。更准确地说,表示 Java 字符串字面量“Hello World”的对象,是从 Java 虚拟机内部保存的常量字符串池中获得的。这意味着,即使来自不同项目的类单独编译,但是只要在同一个应用程序中使用,就可以共享常量字符串池。这个共享发生在运行时。它不是编译时功能。
不过如果想确保两个值相同的 String 变量指向不同的 String 对象,可以通过 new String 构造方法的方式创建字符串来实现。
String myString1 = new String("Hello World");
String myString2 = new String("Hello World");
这样即使创建的两个 Java 字符串的值(文本)相同,Java 虚拟机也会在内存中创建两个不同的对象来表示它们。
Java 的字符串创建后就不能再修改,为什么会这样呢?我们先来看一下Java 里 String 类型的定义
public final class String
implements java.io.Serializable, Comparable, CharSequence {
/** The value is used for character storage. */
private final char value[];
String 类被 final 关键字修饰,表示 String 类不可被继承。String 类的数据存储于 char[] 数组,这个数组被 final 关键字修饰,表示创建后不可被更改。为什么 Java 要这样设计?主要有两点优势 (1)保证 String 对象安全性。避免 String 被篡改。(2)可以通过实现字符串常量池,减少同一个值的字符串对象的重复创建,节省内存。
由于 String 类型的不可变性,Java的字符串处理起来很麻烦,因为它不支持像数组一样用 [] 直接访问其中的字符,且不能直接修改,要转化成 char[] 类型后才能修改。
String s1 = "hello world";
// 这样获取s1[2]中的字符,会有编译错误
char c = s1[2]
char[] chars = s1.toCharArray();
chars[1] = 'a';
String s2 = new String(chars);
// 输出 hallo world
System.out.println(s2)
Java 的字符串不能直接修改,要用 toCharArray 方法转化成 char[] 类型的数组后进行修改,然后转换回 String 类型。
注意这里说的修改也只是用原字符串里的字符创建了 char 数组,更改的 char 数组里的元素,最后转换回String 类型时,是创建了一个新的 String 对象,原来的 String 变量 s1 并没有发生更改。
因为 String 的不可变性, 我们使用字符串对象的各种方法比如 replace,trim等,返回的都是新字符串对象,并不是在原字符串对象上做的更改。
可以使用 "+" 运算符对两个字符串进行拼接,将一个字符串附加到另一个字符串后面得到一个新的字符串。Java 中的字符串是不可变的,这意味着它们一旦创建就不能更改。因此,当将两个 Java 字符串对象相互连接时,实际上是把结果存放在了生存的第三个字符串对象中。
String one = "Hello";
String two = "World";
String three = one + " " + two;
这个例子里变量 three 所引用的字符串对象的内容是“Hello World”,而原来的两个字符串对象的内容保持不变。
在拼接字符串时,必须注意可能的性能问题。在 Java 中两个字符串的连接会被 Java 编译器编译成 StringBuilder 的方式:
String one = "Hello";
String two = " World";
// String three = one + two 会被转换成下面这种方式
String three = new StringBuilder(one).append(two).toString();
这段字符串拼接的代码实际上会创建两个新对象:一个 StringBuilder 实例和一个从 toString() 方法返回的新 String 实例。
当两个字符串拼接作为单个语句单独执行时,这种额外的对象创建开销是微不足道的。不过,当在循环中执行时,情况就不同了。
比如在一个循环语句里进行字符串拼接
String[] strings = new String[]{"one", "two", "three", "four", "five"};
String result = null;
for(String string : strings) {
result = result + string;
}
这段代码将被编译成类似下面的这种循环创建 StringBuilder 和新 String 对象的形式
String[] strings = new String[]{"one", "two", "three", "four", "five"};
String result = null;
for(String string : strings) {
result = new StringBuilder(result).append(string).toString();
}
这个循环中的每次迭代,都会创建一个新的 StringBuilder。以及由它的 toString() 方法创建的 String 对象。每次执行StringBuilder(result)
代码创建StringBuilder
对象时,都会将result
指向的字符串中的所有字符复制到StringBuilder
中。循环的迭代次数越多,result
字符串就会越大,那也就意味着将字符从result
字符串中复制到新的StringBuilder
所需的时间就越长。同理,将StringBuilder
中的字符复制到由它的toString()
方法创建的临时字符串中的时间也会越长。
从性能表现上来说就是,迭代次数越多,每次迭代就越慢。正确的方式应该是在循环外创建StringBuilder
实例,在循环内复用它完成字符串拼接,来替代使用"+"进行字符串拼接的方式。
String[] strings = new String[]{"one", "two", "three", "four", "five"};
StringBuilder temp = new StringBuilder();
for(String string : strings) {
temp.append(string);
}
String result = temp.toString();
这样避免了循环内的StringBuilder
和String
对象实例化,因此也避免了字符的两次复制。
如果是在并发编程中,String
对象的拼接涉及到线程安全的话,可以使用 StringBuffer
。但是要注意,由于StringBuffer
是线程安全的,涉及到锁竞争,所以从性能上来说,要比StringBuilder
差一些。
Java字符串相等性的比较一定要使用 String 对象的 equals 方法,不要用"=="进行比较。
““==” 比较的是两个字符串是否是完全相同的对象,而值相等的字符串不一定是同一个对象。
”
if (s1.equals(s2)) {
// s1和s2相等
} else {
// s1和s2不相等
}
当一个字符串对象是 null 的时候,再调用 equals() 方法会导致 NullPointerException 空指针异常,所以这个应该特别注意,正确的判断字符串对象是不是空的,应该先检测它是不是 null,再看是不是空字符。
str == null || str.equals("")
不过我们可以使用几个类库来完成这个判断, 不用自己写,常用的 Guava 和 Apache Common Lang 库里都有这个功能的工具方法。
Guava 库的 Strings 类有一个工具方法 isNullOrEmpty,会在给定字符串参数是 null 或者 空字符串的时候返回 true。
import com.google.common.base.Strings;
class Main
{
public static void main(String[] args)
{
System.out.println(Strings.isNullOrEmpty("")); // true
System.out.println(Strings.isNullOrEmpty(null)); // true
System.out.println(Strings.isNullOrEmpty("Java")); // false
}
}
import org.apache.commons.lang3.StringUtils;
class Main
{
public static void main(String[] args)
{
System.out.println(StringUtils.isEmpty("")); // true
System.out.println(StringUtils.isEmpty(null)); // true
System.out.println(StringUtils.isEmpty("Java")); // false
System.out.println(StringUtils.isBlank(" ")); // true
}
}
下面列出一些常用的字符串对象的方法。
获取长度绝对是字符串最常用的方法之一,使用 length() 方法可以获取字符串的长度,注意获取的字符串的长度是字符串包含的字符数,不是用于存储字符串的字节数。
String string = "Hello World";
int length = string.length();
使用 String 对象的 substring() 方法可以提取字符串的一部分作为新的字符串返回
String string1 = "Hello World";
String substring = string1.substring(0,5);
执行此代码后,返回的字符串变量的值是"Hello"。substring() 方法有两个参数。第一个是要包含在子字符串中的第一个字符的在原字符串的索引位置,第二个是要包含在子字符串中的最后一个字符之后的字符的在原字符串中的索引位置,相当于是按照前闭后开的区间截取出子字符串的。
字符串中的第一个字符的索引为 0,第二个字符的索引为 1,依此类推。字符串中的最后一个字符的索引为 String.length() - 1。
可以使用 indexOf() 方法在字符串中搜索子字符串。
String string1 = "Hello World";
int index = string1.indexOf("World");
indexOf() 方法返回在字符串中找到的第一次出现子字符串的索引位置,上面的例子匹配的子字符串 World 的 W 在字符串的索引 6 处找到,所以indexOf() 方法的返回值是 6。如果在字符串中找不到子字符串,则 indexOf() 方法返回 -1;
于indexOf() 方法相对于的还有 lastIndexOf(),它返回子串在字符串中最后一次出现的索引位置。
matches() 方法把一个正则表达式作为参数,如果正则表达式能匹配字符串,则返回true,否则返回false。
String text = "one two three two one";
boolean matches = text.matches(".*two.*");
字符串有一组用于比较字符串的方法,除了前面已经知道的 equals 方法外还有其他几个:
equals:比如str1.equals(str2) 判断 str1 和 str2 的值是否相等。
equalsIgnoreCase:忽略大小写版本的equals 方法。
startsWith,endsWith:分别用于判读字符串是否已制定参数开头或者结尾。
compareTo:比较两个字符串,并返回一个 int 值,告知字符串是否小于、等于或大于参数字符串这三种情况分别返回负数、0 和正数。
replace() 的方法,它可以替换字符串中的字符。注意 replace() 方法实际上并不替换现有字符串中的字符。而是返回了一个新的字符串对象。
String source = "123abc";
String replaced = source.replace('a', '@');
执行代码后, replaced 字符串变量的值为"123@bc"。replace 返回会替换掉字符串中所有匹配的子串,与之相对的还有replaceFirst ,只替换匹配的第一个子串。
String source = "A man drove with a car.";
String[] occurrences = source.split("a");
// 返回的数组 [0] => "A m", [1] => "n drove with " [2] => " car."
把数字类型转换成字符串类型,使用String 类的类方法 valueOf
String intStr = String.valueOf(10);
System.out.println("intStr = " + intStr);
String flStr = String.valueOf(9.99);
System.out.println("flStr = " + flStr)
调用对象的 toString 方法,返回对象的字符串表现形式。
Integer integer = new Integer(123);
String intStr = integer.toString();
String theString = "This is a good day to code";
System.out.println( theString.charAt(0) );// 返回 T
System.out.println( theString.charAt(3) );// 返回 s
String theString = "This is a good day to code";
byte[] bytes1 = theString.getBytes();
byte[] bytes2 = theString.getBytes(Charset.forName("UTF-8");// 指定字节编码
String theString = "This IS a mix of UPPERcase and lowerCASE";
String uppercase = theString.toUpperCase();
String lowercase = theString.toLowerCase();
这里只是列出了一些常用的字符串方法,更多其他方法的应用,可以查阅 String 类的 JavaDoc。
如果觉得文章有用除了收藏也记得帮我分享一下,关注公众号「JavaMemo」第一时间获取教程更新,感谢您的支持。