工作三年,小胖连 String 源码都没读过?真的菜!(上)

简介: 工作三年,小胖连 String 源码都没读过?真的菜!

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 仅仅是一个引用。如下图:


640.png


  • 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 等方法其实是重新生成一个对象返回的,他并没有改变原来的对象」,这点看源码就知道了。


640.png


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 类型是不可变的」

相关文章
|
2月前
|
存储 Java
Java基础复习(DayThree):字符串基础与StringBuffer、StringBuilder源码研究
Java基础复习(DayThree):字符串基础与StringBuffer、StringBuilder源码研究
Java基础复习(DayThree):字符串基础与StringBuffer、StringBuilder源码研究
|
26天前
|
存储 缓存 安全
java源码之String详解
java源码之String详解
17 0
|
1月前
|
安全 Java 数据安全/隐私保护
Java基础4-一文搞懂String常见面试题,从基础到实战,更有原理分析和源码解析!(二)
Java基础4-一文搞懂String常见面试题,从基础到实战,更有原理分析和源码解析!(二)
26 0
|
1月前
|
JSON 安全 Java
Java基础4-一文搞懂String常见面试题,从基础到实战,更有原理分析和源码解析!(一)
Java基础4-一文搞懂String常见面试题,从基础到实战,更有原理分析和源码解析!(一)
40 0
|
2月前
qt初入门0:结构体中QString用memset导致崩溃分析及QLatin1String简单查看源码
qt初入门0:结构体中QString用memset导致崩溃分析及QLatin1String简单查看源码
124 0
|
2月前
|
安全 API
详解StringBuilder和StringBuffer(区别,使用方法,含源码讲解)
详解StringBuilder和StringBuffer(区别,使用方法,含源码讲解)
66 0
|
9月前
|
存储 安全 编译器
Go语言源码剖析-String和unsafe包
Go语言源码剖析-String和unsafe包
54 0
|
存储 消息中间件 缓存
从源码上聊聊Redis-String、List的结构实现
本文的数据类型只讲底层结构和部分机制,不讲具体的使用,使用的话自行bing,但是会提一些应用场景
151 1
从源码上聊聊Redis-String、List的结构实现
|
12月前
|
存储 安全 Java
高频面试题-JDK集合源码篇(String,ArrayList)
都是List的子集合,LinkedList继承与Dqueue双端队列,看名字就能看出来前者是基于数组实现,底层采用Object[]存储元素,数组中的元素要求内存分配连续,可以使用索引进行访问,它的优势是随机访问快,但是由于要保证内存的连续性,如果删除了元素,或者从中间位置增加了元素,会设计到元素移位的操作,所以增删比较慢。
63 0
|
存储 安全 Java
【JavaSE】Java基础语法(三十七):Java 中的 String 类(源码级别)(2)
2.11 char[] toCharArray() 2.12 String substring(int beginIndex) 从传入的索引处截取,截取到末尾,得到新的字符串 2.13 String substring(int beginIndex, int endIndex) 根据开始和结束索引进行截取,得到新的字 符串(包含头,不包含尾)