String 类相信大家都不陌生,它是引用类型,同时也是工作中用的最多的一个类。那它到底是怎么实现的呢?我们看源码:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { // 用于存储字符串的值 private final char value[]; // 缓存字符串的 hash code,默认是 0 private int hash; // 其他内容
它实现了 java.io.Serializable、Comparable、CharSequence 这三个接口。分别赋予它 ** 序列化、比较以及字符属性(长度、下标、子串)** 的能力。
它还有 value []、hash 两个重要的是变量,其中 「value [] 是一个字符数组,用于存放 String 内容,我们实例化 String s = "123",其实 "123" 就存在 value [] 中,而它是被 final 修饰的,这也是我们常说 String 不可变的原因」。
而 hash 是 String 实例化的 hashcode 的一个缓存。因为 String 常用于比较,比如在 HashMap 中。如果每次比较都要重新算 hashcode,非常不友好。所以保存一个 hashcode 的缓存是非常有必要的。
一、常用方法
4 个构造方法
4 个构造方法如下面的源码,一目了然。其中以 StringBuffer 和 StringBuilder 为参数的构造函数,因为这三种数据类型,我们通常都是单独使用,所以这点还是得留意下。
// 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); } // 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()); }
equals 方法
String 的 equals 方法比较的是两个字符串是否相等。「它重写了 Object 中的 equals () 方法,equals () 方法需要传递一个 Object 类型的参数值,在比较时会先通过 instanceof 判断是否为 String 类型,如果不是则会直接返回 false」。
Object oString = "666"; Object oInt = 666; System.out.println(oString instanceof String); // 返回 true System.out.println(oInt instanceof String); // 返回 false
当判断参数为 String 类型之后,会循环对比两个字符串中的每一个字符,当所有字符都相等时返回 true,否则则返回 false。
l 类似的,还有一个和 equals () 比较类似的方法 「equalsIgnoreCase (),它是用于忽略字符串的大小写之后进行字符串对比」。
public boolean equals(Object anObject) { // 对象引用相同返回 true if (this == anObject) { return true; } // 是否为 String 类型,否则返回 false if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; // 长度是否相等,否则返回 false if (n == anotherString.value.length) { // 转为 char 数组 char v1[] = value; char v2[] = anotherString.value; int i = 0; // 循环对比每一个字符 while (n-- != 0) { if (v1[i] != v2[i]) // 只要有一个不等,返回 false return false; i++; } return true; } } return false; }
compareTo 方法
与 equals 方法不同,compareTo 会循环对比两个 String 中相同位置的每一个字符的 ASCII 值,只要有不相等的。就会输出不相等的第一个位置的 ASCII 值相减。比如:
public static void main(String[] args) { String sOne = "6611"; String sTwo = "6623"; // 输出 -1 System.out.println(sOne.compareTo(sTwo)); }
如果所有字符都一样,那就返回两个 String 的长度相减的值。简而言之,「如果两个字符串完全一样,那就是返回 0」。
public int compareTo(String anotherString) { int len1 = value.length; int len2 = anotherString.value.length; // 获取到两个字符串长度最短的那个 int 值 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; }
与 equals () 的两点不同:
- equals () 可以接收一个 Object 类型的参数,而 compareTo () 只能接收一个 String 类型的参数;
- equals () 返回值为 Boolean,而 compareTo () 的返回值则为 int。
其他方法
- indexOf ():查询字符串首次出现的下标位置
- lastIndexOf ():查询字符串最后出现的下标位置
- contains ():查询字符串中是否包含另一个字符串
- toLowerCase ():把字符串全部转换成小写
- toUpperCase ():把字符串全部转换成大写
- length ():查询字符串的长度
- trim ():去掉字符串首尾空格
- replace ():替换字符串中的某些字符
- split ():把字符串分割并返回字符串数组
- join ():把字符串数组转为字符串
二、面试问题
为什么 String 是不可变的
在文章开始,我提到 String 有 final char value [] 这个成员变量。实际上,String 类就是对字符数组的封装我们初始化一个字符串时,它是以 char 数组的形式存在内存中。比如:String s = "123456", s 仅仅是一个引用。如下图:
- value 字符数组是 private 的,String 中并没有 setValue 等方法来改变它的值。也就是说 String 一旦初始化就不可变了,并且在 String 外部并不能访问 value。
- 此外,value 是 final 修饰的, 也就是说在 String 类内部,一旦这个值初始化了, 也不能被改变。所以可以认为 String 对象是不可变的了。
这个时候,小胖问了。不对呀,我平时开发还是可以用 substring, replace, replaceAll, toLowerCase 等方法修改它的值呀。比如:
String s = "123456"; System.out.println("s = " + s); // 输出 s = 123456 s = s.replace('1', '0'); System.out.println("a = " + a); // 输出 s= 023456
这里不是改变了么?小胖:远哥,你个渣男。骗我!!!
放下刀,这里其实是个误区,「上述的 replace 等方法其实是重新生成一个对象返回的,他并没有改变原来的对象」,这点看源码就知道了。
substring,replaceAll, toLowerCase 也都是如此的。「都是在方法内部重新创建新的 String 对象,并且返回这个新的对象,原来的对象是不会被改变的」。这也是为什么像 replace, substring,toLowerCase 等方法都存在返回值的原因。也是为什么像下面这样调用不会改变对象的值:
String s = "123456"; System.out.println("s = " + s); // 输出 s = 123456 ss.replace('1', '0'); System.out.println("s = " + s); // 输出 s = 123456
写到这里。「小胖大彻大悟,直呼:远哥牛逼」。
String 真的不可变么?我非要改变怎么做?
「从上面可知 String 的成员变量是 private final 的,也就是初始化之后不可改变」。
value 是一个引用变量,而不是真正的对象。value 是 final 修饰的,也就是说 final 不能再指向其他数组对象,那么我能改变 value 指向的数组吗?比如将数组中的某个位置上的字符变为下划线 “_”。
至少在我们自己写的普通代码中不能够做到,因为我们根本不能够访问到这个 value 引用,更不能通过这个引用去修改数组。那么用什么方式可以访问私有成员呢?「没错,用反射, 可以反射出 String 对象中的 value 属性, 进而改变通过获得的 value 引用改变数组的结构」。下面是实例代码:
public static void testReflection() throws Exception { //创建字符串"Hello World", 并赋给引用s String s = "Hello World"; System.out.println("s = " + s); // 输出Hello World //获取String类中的value字段 Field valueFieldOfString = String.class.getDeclaredField("value"); //改变value属性的访问权限 valueFieldOfString.setAccessible(true); //获取s对象上的value属性的值 char[] value = (char[]) valueFieldOfString.get(s); //改变value所引用的数组中的第5个字符 value[5] = '_'; System.out.println("s = " + s); // 输出 Hello_World }
「通过两次字符串的输出,可以看到,String 被改变了。但是在代码里,几乎不会使用反射的机制去操作 String 字符串,所以,我们会认为 String 类型是不可变的」。