01-String、Long源码解析和面试题(基础)

注:源码系列文章主要是对某付费专栏的总结记录。如有侵权,请联系删除。

1 String

1.1 不可变性

不可变指的是类值一旦被初始化,就不能再改变了,如果被修改,将会是新的类。例如:

String str = "hello";
str = "world";

从代码上看,str 的值好像被修改了,但从 debug 的日志来看,其实 str 的内存地址已经被修改了,也就是说 str = "world" 这个看似简单的赋值,其实已经把 str 的引用指向了新的 String,debug 的截图显示内存地址已经被修改:

Before

After

从源码角度查看原因:

public final class String
    implements java.io.Serializable, Comparable, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
}

总结:

  1. String 被 final 修饰,说明 String 类绝不可能被继承(Integer 等包装类也不可能被继承);
  2. String 中保存数据的是一个 char 的数组 value。value 也是被 final 修饰的,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部内有改变 value 数组的方法,因此可以保证 String 不可变。

不可变的好处:

1. 可以缓存 hash 值
因为 String 的 hash 值经常被使用,例如 String 用作 HashMap 的 key。不可变的特性使得 hash 值的不可变,因此只需要计算一次。
2. String Pool 的需要
如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可使用 String Pool。
3. 安全性
String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为连接的是其它主机,而实际情况却不一定是。
4. 线程安全
String 不可变性天生具备线程安全,可以在多个线程中安全的使用。

1.2 编码

我们经常碰到这样的场景,在进行二进制转换操作时,本地测试没有问题,到其它环境机器时,有时会出现字符串乱码的情况,这个主要是因为在二进制转换操作时,并没有强制规定文件编码,而不同的环境默认的文件编码是不一致导致的。

模仿字符串乱码:

String source = "Hi 韩梅梅";
// 字符串转换为 byte 数组
byte[] bytes = source.getBytes("ISO-8859-1");
// byte 数组转换为字符串
String target = new String(bytes);
System.out.println(source);
System.out.println(target);

打印结果:

Hi 韩梅梅
Hi ???

打印的结果为 ??,这就是常见的乱码表现形式。是不是说把代码改成 String target = new String(bytes, "ISO-8859-1") 就可以了?这是不行的。因为 ISO-8859-1 编码对中文的支持有限,导致中文会显示乱码。唯一的解决办法,就是在所有需要用到编码的地方,都统一使用 UTF-8,对于 String 来说,getBytesnew String 两个方法都会使用到编码,我们把这两处的编码替换成 UTF-8,打印出来的结果就正常了。

1.3 常用 API

substring

substring 方法主要是为了截取字符串连续的一部分,substring 有两个方法:

  • public String substring(int beginIndex, int endIndex)
  • public String substring(int beginIndex)

substring 方法的底层使用的是字符数组范围截取的方法:Arrays.copyOfRange(value, offset, offset+count) 从字符数组中进行一段范围的拷贝。

equals

判断相等有两种方法 equalsequalsIgnoreCase。后者判断相等时会忽略大小写。查看 equals 源码:

public boolean equals(Object anObject) {
    // 判断内存地址是否相等
    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 v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            // 依次比较每个字符是否相等,若有一个不等,则直接返回 false
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

从 equals 的源码可以看出,逻辑非常清晰,完全是根据 String 底层的结构来编写出相等的代码。这也提供了一种思路给我们:如果有人问如果判断两者相等时,我们可以从两者的底层结构出发,这样可以迅速想到一种贴合实际的思路和方法,就像 String 底层的数据结构是 char 的数组一样,判断相等时,就挨个比较 char 数组中的每个字符是否相等即可。

replace

  • replace:替换所有字符
  • replaceAll:替换所有字符串
  • replaceFirst:替换遇到的第一个字符串

其中在使用 replace 时需要注意,replace 有两个方法,一个入参是 char,一个入参是 String,前者表示替换所有字符,如:str.replace('a', 'b'),后者表示替换所有字符串,如:str.replace("a", "b"),两者就是单引号和双引号的区别。
需要注意的是 replace 并不只是替换一个,是替换所有匹配到的字符或字符串。例如:

String str = "hello";
// replace char
System.out.println("replace char 替换之前:" + str);
str = str.replace('e', 'f');
System.out.println("replace char 替换之后:" + str);

// replace string
System.out.println("replace string 替换之前:" + str);
str = str.replace("f", "e");
System.out.println("replace string 替换之后:" + str);

// replaceAll
System.out.println("replaceAll 替换之前:" + str);
str = str.replaceAll("ll", "nn");
System.out.println("replaceAll 替换之后:" + str);

// replaceFirst
System.out.println("replaceFirst 替换之前:" + str);
str = str.replaceFirst("n", "m");
System.out.println("replaceFirst 替换之后:" + str);

打印结果:

replace char 替换之前:hello
replace char 替换之后:hfllo
replace string 替换之前:hfllo
replace string 替换之后:hello
replaceAll 替换之前:hello
replaceAll 替换之后:henno
replaceFirst 替换之前:henno
replaceFirst 替换之后:hemno

split

拆分使用 split 方法,该方法有两个入参数。第一个参数是我们拆分的标准字符,第二个参数是一个 int 值 limit,来限制我们需要拆分成几个元素。如果 limit 比实际能拆分的个数小,按照 limit 的个数进行拆分。例如:

String str = "hello:and:world";
String[] v1 = str.split(":");
String[] v2 = str.split(":", 2);
String[] v22 = str.split(":", 0);
String[] v3 = str.split(":", 5);
String[] v4 = str.split(":", -2);
String[] v5 = str.split("l");
String[] v6 = str.split("l", 2);

System.out.println("v1: " + Arrays.toString(v1));
System.out.println("v2: " + Arrays.toString(v2));
System.out.println("v22: " + Arrays.toString(v22));
System.out.println("v3: " + Arrays.toString(v3));
System.out.println("v4: " + Arrays.toString(v4));
System.out.println("v5: " + Arrays.toString(v5));
System.out.println("v6: " + Arrays.toString(v6));

运行结果:

v1: [hello, and, world]
v2: [hello, and:world]
v22: [hello, and, world]
v3: [hello, and, world]
v4: [hello, and, world]
v5: [he, , o:and:wor, d]
v6: [he, lo:and:world]

从演示结果来看,limit 对拆分的结果,具有限制作用,还有就是拆分结果里不会出现被拆分的字段。

那如果字符串里面有一些空值呢,拆分的结果如下:

String str = ",a,,,b,c,,d";
String[] v = str.split(",");
System.out.println(Arrays.toString(v));

执行结果:

[, a, , , b, c, , d]

从拆分结果中我们可以看到,空值是拆分不掉的,仍然成为结果数组的一员,如果我们想删除空值,只能自己拿到结果后再做操作,但 Guava 提供了一些可靠的工具类,可以帮助我们快速去掉空值,如下:

String str = ",a,,, b c ,,d";
// Splitter 是 Guava 提供的 API
List list = Splitter.on(',')
        .trimResults()  // 去掉空格
        .omitEmptyStrings() // 去掉空值
        .splitToList(str);
System.out.println(JSON.toJSONString(list));

执行结果:

["a","b c","d"]

从打印结果来看,是去掉了前后空格和空值的,这正是我们工作中常常期望的结果,所以推荐使用 Guava 的 API 对字符串进行分割。

join

合并使用 join 方法,此方法是静态的,可以直接使用。方法有两个入参,参数一是合并的分隔符,参数二是合并的数据源,数据源支持数组和 List。示例如下:

List list1 = Arrays.asList("a", "b", "c");
List list2 = Arrays.asList("d", null, "e");

String v = String.join(",", list1).join(",", list2);
System.out.println(v);

直接结果:

d,null,e

通过上面的例子我们发现有两个不太方便的地方:

  • 不支持依次 join 多个数据源,比如我们想依次 join list1 和 list2,像上面的写法最后得到的是 list2 的值,第一次 join 的值被第二次 join 覆盖了;
  • 如果 join 的是一个 List,无法过滤掉 null 值。

使用 Guava 提供的 API 演示:

Joiner joiner = Joiner.on(",").skipNulls();
String v1 = joiner.join("a", "b", "c");
String v2 = joiner.join(Arrays.asList("d", null, "", "e"));
System.out.println("依次 join 多个字符串:" + v1);
System.out.println("join 时自动删除 list 中的 null 值:" + v2);

执行结果:

依次 join 多个字符串:a,b,c
join 时自动删除 list 中的 null 值:d,,e

从结果上看到,Guava 不仅仅支持多个字符串的合并,还帮助我们去掉了 List 中的 null 值,这就是我们工作中常常需要的带的结果。

2 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 面试题

3.1 为什么使用 Long 时,推荐多使用 valueOf 方法,少使用 parseLong 方法

答:因为 Long 本身有缓存机制,缓存了 -128 到 127 范围内的 Long,valueOf 方法会从缓存中去拿值,如果命中缓存,会减少资源的开销,parseLong 方法没有这个机制。

3.2 如果解决 String 乱码问题

答:乱码问题的根源主要是两个:字符集不支持复杂汉字、二进制进行转换时字符集不匹配,所以在 String 乱码时我们可以这么做:

  1. 所有可以指定字符集的地方强制使用指定字符集,比如 new String 和 getBytes 这两个地方;
  2. 我们应该使用 UTF-8 这种能完整支持复杂汉字的字符集。

3.3 为什么说 String 是不可变的

答:主要是因为 String 和保存数据的 char 数组,都被 final 关键字修饰,所以是不可变的,具体细节描述参考上文。

3.4 String 一些常用操作问题,比如分割、合并、替换、删除、截取等问题

答:主要考察 String 的基本操作,参考上文。

------------------------------------- END -------------------------------------

你可能感兴趣的:(01-String、Long源码解析和面试题(基础))