这篇文章用于记录个人学习过程中Java中String类的一些基础知识和方法。主要记录了String类的特性、常用方法,以及和基本数据类型、包装类互转方面的内容。
源码部分
我们先简易看下Java13中String的部分源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
private final byte[] value;
不难发现下列几个特点:
- String类被声明为final,不可被继承。
- 实现了Serializable接口,该接口表明String类是可序列化的。
- 实现了Comparable接口,表明String类是可比较的。
- 字符串实际上被储存在String类中的byte类型数组value中(Java9之前为char类型数组),同时该数组也被声明为final。
源码上的特性实际反映了String类一个重要的特点:不可变性,以下我们将通过实际的应用来深入理解这一特性。
应用部分
接下来我们说说String类的应用。
String类实例化的方式
- 方式一:字面量定义。
- 方式二:利用构造器新建对象。
下面我们以代码对两种实例化方式在底层储存结构的差异进行说明:
// 字面量定义,数据声明在方法区中的字符串常量池中
String s1 = "java";
String s2 = "java";
// 利用构造器新建对象,数据在堆空间中开辟空间后储存在对应的地址值里
String s3 = new String("java");
String s4 = new String("java");
System.out.println(s1==s2); // true
System.out.println(s1==s3); // false
System.out.println(s3==s4); // false
结论:字面量定义的String类变量储存的直接是字符串常量池中的相应字符串的引用,而通过构造器定义的String类变量储存的则是堆空间中相应的String类对象的引用。
一个简单的图来说明:
ps:Java8及以后的常量池实质上属于堆内存中的一部分,本文为了易于理解,将常量池单独划分为一个区域。
事实上,在更深层次的讨论中,我们会发现在堆空间的对象里的value数组所存的依旧是字符串常量池"java"的引用,即:
既然我们已经知道,s1与s2指向相同的地址值,s3中的value与s2指向相同的地址值,那当我们试图改变s1或s3的值,是否会影响s2?
s1 = "hello";
s3 = "world";
System.out.println(s2);// java
通过实际的验证表明,s1、s3的修改并不会影响s2。换句话说,s1和s3分别指向了新的字符串常量区中对应新字符串的引用,而不是对原来指向的"java"进行修改。这里实质上体现了字符串前文提到的不可变性。
顺带一提:需要比较字符串内容时请使用equals方法,String类重写了equals方法,只需要所比较的两个String对象内容一致,结果就为true。
System.out.println(s1.equals(s3));// true
进一步探讨String类的储存模式
我们观察以下代码:
String s1 = "java";
String s2 = "hello";
// 单个字面量直接赋值时,存储在常量池
String s3 = "javahello";
String s4 = "java" + "hello";
// 两个或多个字面量的拼接,存储在常量池
String s5 = s1 + "hello";
String s6 = "java" + s2;
String s7 = s1 + s2;
System.out.println(s3==s4); // true
System.out.println(s3==s5); // false
System.out.println(s3==s6); // false
System.out.println(s3==s7); // false
System.out.println(s5==s6); // false
System.out.println(s5==s7); // false
String s8 = s5.intern();
// 此时接收返回值的s8使用的是常量池中已经存在的"javahello",即直接返回常量池中的引用
System.out.println(s3==s8); // true
简述下intern方法的作用:
(1) 当常量池中不存在"abc"这个字符串的引用,将这个对象的引用加入常量池,返回这个对象的引用。
(2) 当常量池中存在"abc"这个字符串的引用,返回这个对象的引用;
对于上述代码,我们可以得出以下结论:
- 常量与常量的拼接结果在常量池中,且常量池中不会存在相同内容的常量。
- 只要拼接时其中有一个是变量,结果就在堆中,可以近似理解与为使用构造器新建对象采取相同的存储方式。
- 如果拼接的结果调用intern方法,返回值就在常量池中。
此外,对于final修饰的字符串变量,拼接时当作常量看待。
String s1 = "javahello";
final String s2 = "java";
String s3 = s2 + "hello"; // 实际上属于常量与常量的拼接
System.out.println(s5 == s1);// true
String类的常用方法1
- int length():获取字符串长度
String s1 = "helloworld";
System.out.println(s1.length()); // 10
- char charAt(int index):获取索引位置元素
System.out.println(s1.charAt(0)); // h
- boolean isEmpty():判断字符串是否为空
System.out.println(s1.isEmpty()); // false
- String toUpperCase() / toLowerCase():转换大小写
String s2 = s1.toUpperCase(); // 变为大写,s1本身不变,s2获得结果
System.out.println(s2); // HELLOWORLD
- String trim():去除首尾的空格
String s3 = " hello wo rld ";
String s4 = s3.trim();
System.out.println(s3); // s3不改变
System.out.println(s4); // s4得到除去首尾空格的返回引用
- boolean equals(Object obj):比较字符串内容是否相等
System.out.println(s3.equals(s4)); // false
- boolean equalsIgnoreCase(String anotherString):忽略大小写比较字符串内容是否相等
System.out.println(s1.equalsIgnoreCase(s2)); // true
- String concat(String str):字符串拼接,等价于“+”
String s5 = s1.concat("!!!");
System.out.println(s5); // helloworld!!!
- int compareTo(String anotherString):字符串的比较,返回值是前者减去后者的差,区别于equals()
String s6 = "abc";
String s7 = new String("abe");
System.out.println(s6.compareTo(s7)); // -2,前者减去后者的值
// 涉及到字符串排序
- String substring(int beginIndex) / substring(int beginIndex, int endIndex):获取现有字符串的子串,有重载方法
String s8 = "abcdefg";
String s9 = s8.substring(2);
System.out.println(s8); // cdefg
String s10 = s8.substring(2, 5);
System.out.println(s10); // cde,左闭右开区间
String类的常用方法2
- boolean endsWith(String suffix):测试此字符串是否以指定后缀结束
String str1 = "helloworld";
System.out.println(str1.endsWith("ld")); // true
- boolean startsWith(String prefix) / startsWith(String prefix, int toffset):测试此字符串是否以自动前缀开始,含重载方法
System.out.println(str1.startsWith("H")); // false
System.out.println(str1.startsWith("ll",2));// true,从指定位置开始判断
- boolean contains(CharSequence s) :若此字符串包含指定的char值序列时,返回true
String str2 = "wo";
System.out.println(str1.contains(str2)); // true
- int indexOf(String str) / indexOf(String str, int fromIndex):返回子串在此字符串正向首次出现处的索引,若未找到返回-1,含重载方法
System.out.println(str1.indexOf("lo")); // 3
System.out.println(str1.indexOf("lo",5)); // -1,从指定位置开始去找
- int lastIndexOf(String str) / indexOf(String str, int formIndex):返回子串在此字符串反向首次出现处的索引,若未找到返回-1,含重载方法
System.out.println("hellorworld".lastIndexOf("or")); // 7,返回的索引位置仍然是正向的索引
System.out.println("hellorworld".lastIndexOf("or",6)); // 4,从指定位置往前开始找
String类的常用方法3
- String replace(char oldChar, char newChar) / replace(CharSequence target, CharSequence replacement):用新元素替代原字符串中的旧元素,同时返回得到的新字符串,含重载方法
String str1 = "abcdefg";
String str2 = str1.replace('a', 'b'); // 替换原字符串中的所有该字符
String str3 = str1.replace("ab", "cd"); // 替换原字符串中的所有该子串
- String replaceAll(String regex, String replacement):替换此字符串所有匹配给定的正则表达式的子字符串
String str = "12hello34world5java32235mysql325";
String string = str.replaceAll("\\d+",",").replaceAll("^,|,$","");
System.out.println(string); // hello,world,java,mysql
// 将原有数字串都换为逗号,删去开头和结尾的逗号
- String replaceFirst(String regex, String replacement):替换此字符串匹配给定的正则表达式的第一个子字符串
- boolean matches(String regex):告知此字符串是否匹配给定的正则表达式
str = "12345";
boolean matches = str.matches("\\d+");
String tel = "0571-4534289";
boolean result = tel.matches("0571-\\d{7,8}");
System.out.println(result); // true
// 正则匹配
- String[] split(String regex) / split(String regex, int limit):根据给定正则表达式的匹配拆分此字符串,含重载方法
str = "hello|world|java";
String[] strs = str.split("\\|");
for (int i = 0; i < strs.length; i++) {
System.out.println(strs[i]);
}
System.out.println();
str2 = "hello.world.java";
String[] strs2 = str2.split("\\.");
for (int i = 0; i < strs2.length; i++) {
System.out.println(strs2[i]);
}
// 依据正则表达式进行字符串切割
从上述常用方法我们可以发现,凡是对字符串内容进行了修改的方法,都会返回一个修改结果字符串的引用,而原字符串则不会改变,这也体现了字符串的不可变性。
String类与其他基本数据类型及包装类的转换
- String转基本数据类型、包装类:调用包装类的静态方法
- 基本数据类型、包装类转String:调用String重载的valueOf方法
@Test
public void test1(){
String str1 = "123";
// int num = (int)str1; // 直接强制转换是错误的
int num = Integer.parseInt(str1); // 调用包装类的静态方法
String str2 = String.valueOf(num); // 调用valueOf方法
String str3 = num + ""; // 只要有变量参与都是在堆里
System.out.println(str1 == str3); // false
}
- String与char数组之间的转换
- String转char[]:调用String的toCharryArray方法
- char[]转String:调用String的构造器
@Test
public void test2(){
String str1 = "abc123";
char[] charArray = str1.toCharArray();
for (int i = 0; i < charArray.length; i++) {
System.out.println(charArray[i]);
}
char[] arr = new char[]{'h','e','l','l','o'};
String str2 = new String(arr);
System.out.println(str2);
}
- String与byte数组之间的转换
- String转byte[]:调用String的getBytes方法
- byte[]转String:同char[],利用String的构造器
@Test
public void test3() throws UnsupportedEncodingException {
String str1 = "abc123中国"; // 若带中文,则储存对应的字符集编码
byte[] bytes = str1.getBytes(); // 使用默认的字符集进行转换,进行编码
System.out.println(Arrays.toString(bytes));// [97, 98, 99, 49, 50, 51]
byte[] gbks = str1.getBytes("gbk"); // 使用gbk字符集进行编码
System.out.println(Arrays.toString(gbks));
String str2 = new String(bytes); // 使用默认的字符集,进行解码
System.out.println(str2);
String str3 = new String(gbks,"gbk"); // 若编码与解码使用的字符集不一致,出现乱码
System.out.println(str3);
}
- String与StringBuffer/StringBuilder之间的转换
- String转StringBuffer/StringBuilder:调用构造器
- StringBuffer/StringBuilder转String:调用构造器或使用toString方法
// StringBuffer的其一构造器
public synchronized StringBuffer append(CharSequence s) {
toStringCache = null;
super.append(s);
return this;
}
// String的其二构造器
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
// 直接调用toString方法
StringBuilder sb1 = new StringBuilder("abc");
String s1 = sb1.toString();