String 不变性、Long 缓存源码解析和面试题
String 和 Long 大家都很熟悉,现在我们结合实际的工作场景,来一起看下 String 和 Long 的底层源码实现,看看平时我们使用时,有无需要注意的点,总结一下这些 API 都适用于哪些场景。
一:String
1.1、不变性
我们常常看到这样的面试题:请简单的说说 String、StringBuilder 和StringBuffer 三者之间的区别。其中 String 就有一点与其他两者不一样的地方,那就是String值一旦被初始化,就不能再被改变了,如果被修改,将会是新的类。我们来看下面一段代码,虽然最后会输出 “ssssss”,看起来 String的值好像被修改了。
String str = "aaa";
str = "ssssss";
System.out.println(str);
其实我们通过 Debug 就能很直观的看出来,String 的值并没有被修改。
一开始str指向的内存地址是527,经过一轮赋值str指向的内存地址就变成了529。也就说 str = “ssssss” 这个看似简单的赋值,其实已经把 str 的引用指向了新的 String。
现在我们从 String 的源码上看一下原因
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
从 String 的源码中我们可以看出:
- String 被 final 修饰,说明 String 类绝不可能被继承了,也就是说任何对 String 的操作方法,都不会被继承覆写 ;
- String 中保存数据的是一个 char 的数组 value。我们发现 value 也是被 final 修饰的,也就是说 value 一旦被赋值,内存地址是绝对无法修改的,而且 value 的权限是 private 的,外部绝对访问不到,String 也没有开放出可以对 value 进行赋值的方法,所以说 value 一旦产生,内存地址就根本无法被修改 ;
以上两点就是 String 不变性的原因,充分利用了 final 关键字的特性,如果你自定义类时,希望也是不可变的,也可以模仿 String 的这两点操作。
1.2、相等判断
我们判断相等有两种办法,equals 和 equalsIgnoreCase。后者判断相等时,会忽略大小写,如果让你写判断两个 String 相等的逻辑,应该如何写,我们来一起看下 equals 的源码:
public boolean equals(Object anObject) {
// 判断内存地址是否相同
if (this == anObject) {
return true;
}
// 待比较的对象是否是 String,如果不是 String,直接返回不相等
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;
}
从 equals 的源码可以看出,逻辑非常清晰,完全是根据 String 底层的结构来编写出相等的代码。这也提供了一种思路给我们:如果有人问如何判断两者是否相等时,我们可以从两者的底层结构出发,这样可以迅速想到一种贴合实际的思路和方法,就像 String 底层的数据结构是 char 的数组一样,判断相等时,就挨个比较 char 数组中的字符是否相等即可。
二:Long
2.1、缓存
Long 最被我们关注的就是 Long 的缓存问题,Long 自己实现了一种缓存机制,缓存了从 -128 到 127 内的所有 Long 值,如果是这个范围内的 Long 值,就不会初始化,而是从缓存中拿,缓存初始化源码如下:
private static class LongCache {
private LongCache(){}
// 缓存,范围从 -128 到 127,+1 是因为有个 0
static final Long cache[] = new Long[-(-128) + 127 + 1];
// 容器初始化时,进行加载
static {
// 缓存 Long 值,注意这里是 i - 128 ,所以再拿的时候就需要 + 128
for(int i = 0; i < cache.length; i++)
cache[i] = new Long(i - 128);
}
}
三:面试题
3.1、为什么使用 Long 时,大家推荐多使用 valueOf 方法,少使用 parseLong 方法?
因为 Long 本身有缓存机制,缓存了 -128 到 127 范围内的 Long,valueOf 方法会从缓存中去拿值,如果命中缓存,会减少资源的开销,parseLong 方法就没有这个机制。
3.2、为什么大家都说 String 是不可变的?
要是因为 String 和保存数据的 char 数组,都被 final 关键字所修饰,所以是不可变的,具体细节描述可以参考上文。
三:总结
String 和 Long 在我们工作中使用频率很高,在面试的过程中,考官也喜欢问一些关于实际操作的问题,来考察我们的使用熟练度。