这篇文章用于记录个人学习过程中Java中String类的一些基础知识和方法。主要记录了String类的特性、常用方法,以及和基本数据类型、包装类互转方面的内容。
我们先简易看下Java13中String的部分源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
private final byte[] value;
不难发现下列几个特点:
源码上的特性实际反映了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 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"这个字符串的引用,返回这个对象的引用;
对于上述代码,我们可以得出以下结论:
此外,对于final修饰的字符串变量,拼接时当作常量看待。
String s1 = "javahello";
final String s2 = "java";
String s3 = s2 + "hello"; // 实际上属于常量与常量的拼接
System.out.println(s5 == s1);// true
String s1 = "helloworld";
System.out.println(s1.length()); // 10
System.out.println(s1.charAt(0)); // h
System.out.println(s1.isEmpty()); // false
String s2 = s1.toUpperCase(); // 变为大写,s1本身不变,s2获得结果
System.out.println(s2); // HELLOWORLD
String s3 = " hello wo rld ";
String s4 = s3.trim();
System.out.println(s3); // s3不改变
System.out.println(s4); // s4得到除去首尾空格的返回引用
System.out.println(s3.equals(s4)); // false
System.out.println(s1.equalsIgnoreCase(s2)); // true
String s5 = s1.concat("!!!");
System.out.println(s5); // helloworld!!!
String s6 = "abc";
String s7 = new String("abe");
System.out.println(s6.compareTo(s7)); // -2,前者减去后者的值
// 涉及到字符串排序
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 str1 = "helloworld";
System.out.println(str1.endsWith("ld")); // true
System.out.println(str1.startsWith("H")); // false
System.out.println(str1.startsWith("ll",2));// true,从指定位置开始判断
String str2 = "wo";
System.out.println(str1.contains(str2)); // true
System.out.println(str1.indexOf("lo")); // 3
System.out.println(str1.indexOf("lo",5)); // -1,从指定位置开始去找
System.out.println("hellorworld".lastIndexOf("or")); // 7,返回的索引位置仍然是正向的索引
System.out.println("hellorworld".lastIndexOf("or",6)); // 4,从指定位置往前开始找
String str1 = "abcdefg";
String str2 = str1.replace('a', 'b'); // 替换原字符串中的所有该字符
String str3 = str1.replace("ab", "cd"); // 替换原字符串中的所有该子串
String str = "12hello34world5java32235mysql325";
String string = str.replaceAll("\\d+",",").replaceAll("^,|,$","");
System.out.println(string); // hello,world,java,mysql
// 将原有数字串都换为逗号,删去开头和结尾的逗号
str = "12345";
boolean matches = str.matches("\\d+");
String tel = "0571-4534289";
boolean result = tel.matches("0571-\\d{7,8}");
System.out.println(result); // true
// 正则匹配
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]);
}
// 依据正则表达式进行字符串切割
从上述常用方法我们可以发现,凡是对字符串内容进行了修改的方法,都会返回一个修改结果字符串的引用,而原字符串则不会改变,这也体现了字符串的不可变性。
@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
}
@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);
}
@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);
}
// 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();