String 类及源码阅读、面试题详解

String 源码解读       

         在了解String类之前,先给大家总结一句话:“所有对String类进行过改变操作的方法,所返回的结果都是一个新的String对象,因为String字符串是不可变的!!!
        由于包子是刚开始写文章,所以对于一些语言描述可能不是特别的通俗易懂,在以后小编会尽量学习使用通俗易懂的语言来为大家解释。对于上边的总结,我们通过下边的内容能够更容易理解。

String 类

        java.lang.String 类代表字符串。Java程序中所有的字符串文字(被双引号包裹的内容,例如 "包子" )都可以被看作是实现此类的实例。

String类中有很多方法,例如:

  •  用于比较字符串内容的方法
  • 搜索字符串的方法
  • 提取字符串的方法
  • 将字符串转换成全大写或全小写的方法
  • 替换字符串内容的方法
  • 字符串分割等等

String 字符串的特点:

1、字符串是不可变的,所以他们可以被共享(重点)因为String底层维护的是一个 final char[] value;也就是一个 final char 类型的数组。大家都知道,被 final 修饰的常量是不可变的。
2、 所有对字符串进行修改的操作,都是操作的字符串的副本(Arrays.copy()),不会影响字符串本身。(这句话是小编自己通过阅读源码总结的,因为我看了下源码,基本上都是靠Arrays.copy()复制一个字符数组,然后对字符数组进行操作,然后返回一个新的String字符串。而且String的底层数组是final修饰的,不可变的,所以小编总结,对字符串进行的操作是对字符串的副本进行操作的,不会影响String本身。如果有错误,欢迎大佬指出。)

String 字符串创建对象的两种方式:

1、String s1 = "aa";  // 在String池中创建一个aa值,然后在栈中声明一个s1变量,最后将s1指向String 池中的 aa值。
2、String s2 = new String("bb");  // 首先在堆中创建一个String类型的变量bb,然后再栈中声明一个变量s2,然后将 s2指向变量bb,s2通过地址来访问bb值。

String 类的常用方法:

  •     构造方法

            public String() :初始化新创建的 String对象,以使其表示空字符序列。
            public String(char[] value) :通过当前参数中的字符数组来构造新的String。
            public String(byte[] bytes) :通过使用平台的默认字符集解码当前参数中的字节数组来构造新的String字符串。

  •     字符串判断

            public boolean equals (Object anObject) :将此字符串与指定对象进行比较。
            public boolean equalsIgnoreCase (String anotherString) :将此字符串与指定对象进行比较,忽略大小写。

  •     字符串获取

            public int length () :返回此字符串的长度。
            public String concat (String str) :将指定的字符串连接到该字符串的末尾,返回一个新的String字符串。
            public char charAt (int index) :返回指定索引处的 char值。
            public int indexOf (String str) :返回指定子字符串第一次出现在该字符串内的索引。
            public String substring (int beginIndex) :返回一个子字符串,从beginIndex开始截取字符串到字符串结尾。
            public String substring (int beginIndex, int endIndex) :返回一个子字符串,从beginIndex到endIndex截取字符串。包含beginIndex,不含endIndex。

  •     字符串转换

            public char[] toCharArray () :将此字符串转换为新的字符数组。
            public byte[] getBytes () :使用平台的默认字符集将该 String编码转换为新的字节数组。
            public String replace (CharSequence target, CharSequence replacement) :将与target匹配的字符串使用replacement字符串替换。

  •     字符串分割

            public String[] split(String regex) :将此字符串按照给定的regex(规则)拆分为字符串数组。


String源码解读:

    // String底层维护的是一个 final char类型的数组。
    private final char value[];

    // String的构造方法,创建一个空字符层,字符串长度为0
    public String() {
        this.value = new char[0];
     }

     // 显示的初始化一个String字符串对象,该字符串是参数字符串的一个副本,除非需要{@code original }显示的副本,否则没有必要使用它,因为字符串是不可变的。
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

    // 判断char[] value是否为空,为空返回true
    public boolean isEmpty() {
        return value.length == 0;
    }

    // 返回字符在数组中的下标,用来查找指定字符在字符串中的位置
    public char charAt(int index) {
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return value[index];
    }

    /**
    *  比较两个字符串是否相同,比较的是内容
    *  首先比较两个字符串是否指向同一地址,如果指向同一地址返回true,否则向下执行
    *  如果两个字符层不指向同一地址,则去比较他们的内容,先判断目标字符串是否是String类型,
    *  如果是String类型,比较两个字符层的长度,如果长度相同,则比较两个字符串对应位置的字符是否相同,因为String底层是一个char类型的数组。
    *  如果满足 两个字符层指向同一地址 OR (两个对象都是字符串 && 两个字符串长度相同 && 对应位置的字符相同) 这些条件,
    *  则证明两个字符串的内容相同,返回true,否则返回false。
    *  扩展:String继承类Object类,在Object中的equals方法比较的只是两个对象是否指向同一地址,String类重写类equals方法。
    */
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        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;
    }

    /**
    *  忽略大小写比较两个字符串的内容是否相同
    *  equalsIgnoreCase方法内部使用了三元运算,如果两个字符串对象指向同一地址,返回true。
    *  如果两个字符串对象不是指向同一地址,则对anotherString进行一下判断:
    *            1、非空判断
    *            2、两个字符串的长度判断
    *            3、使用 regionMatches判断两个字符层的内容。
    *    regionMatches方法内部将两个字符串都转换成了大写,然后比较两个大写字符串的内容。
    */
    public boolean equalsIgnoreCase(String anotherString) {
        return (this == anotherString) ? true
                : (anotherString != null)
                && (anotherString.value.length == value.length)
                && regionMatches(true, 0, anotherString, 0, value.length);
    }
    //  将 当前字符串对象与传入的字符串对象都转换成大写,然后比较两个字符串的内容。
    public boolean regionMatches(boolean ignoreCase, int toffset,
            String other, int ooffset, int len) {
        char ta[] = value;
        int to = toffset;
        char pa[] = other.value;
        int po = ooffset;
        // Note: toffset, ooffset, or len might be near -1>>>1.
        if ((ooffset < 0) || (toffset < 0)
                || (toffset > (long)value.length - len)
                || (ooffset > (long)other.value.length - len)) {
            return false;
        }
        while (len-- > 0) {
            char c1 = ta[to++];
            char c2 = pa[po++];
            if (c1 == c2) {
                continue;
            }
            if (ignoreCase) {
                // If characters don't match but case may be ignored,
                // try converting both characters to uppercase.
                // If the results match, then the comparison scan should
                // continue.
                char u1 = Character.toUpperCase(c1);
                char u2 = Character.toUpperCase(c2);
                if (u1 == u2) {
                    continue;
                }
                // Unfortunately, conversion to uppercase does not work properly
                // for the Georgian alphabet, which has strange rules about case
                // conversion.  So we need to make one last check before
                // exiting.
                if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) {
                    continue;
                }
            }
            return false;
        }
        return true;
    }

    // 返回一个子字符串,子串的内容是当前字符串对象下标从 beginIndex开始到endIndex结束的内容,包含beginIndex不包含endIndex(因为数组下标是从0开始的)。
    public String substring(int beginIndex, int endIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        if (endIndex > value.length) {
            throw new StringIndexOutOfBoundsException(endIndex);
        }
        int subLen = endIndex - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }

    // 拼接字符串,新产生的String字符串是一个新的对象,因为String字符串是不可变的。
    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        // 创建一个新的字符数组,长度是当前字符串长度与被拼接字符串长度的总和,然后将被拼接字符串的内容追加到当前字符串的末尾
        // 有兴趣的同学可以自行阅读copyOf方法的源码。
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        // 返回一个新的String字符串对象
        return new String(buf, true);
    }

    // 使用新的字符替换当前字符串对象中指定的旧字符,然后返回一个新的String字符串
    public String replace(char oldChar, char newChar) {
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            char[] val = value; /* avoid getfield opcode */

            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            if (i < len) {
                char buf[] = new char[len];
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                return new String(buf, true);
            }
        }
        return this;
    }

        当然,String类中的方法还有很多,有兴趣的同学可以自行阅读源码,String的源码还是很好理解的,下面我们来看一下String在面试中比较常见的面试题:
问题1:以下代码 s1的输出结果是什么?       

String s1="abc";
s1+="d";
System.out.println(s1); // "abcd
因为 String 字符串是不可变的,所有内存中会有 "abc"、"abcd"两个对象,只是变量s1从指向 "abc" 改变成指向 "abcd"而已,
此时s1指向的是"abcd","abc"已经没有变量指向它了,在之后如果还没有新的变量指向"abc",它将会被GC垃圾回收器回收。

问题2:内存中有几个 "abc"常量?

 String s1="abc";
String s2="abc";
答案是 1个,此时在字符串常量池中只有一个abc,但是在堆中有两个变量 s1和 s2,他们都指向abc。

问题3:以下代码的输出结果是什么?  

String s1="abc";
System.out.println(s1=="abc"); // true
首先在String池中开辟一块空间存放常量 "abc";
然后在栈中开辟一块空间存放变量 s1的引用;
然后 s1指向String池中的"abc";
所以 s1所指代的地址就是 "abc"的地址,所以返回true。

问题4:以下代码的输出结果是什么?

String str1 = new String("abc");
System.out.println(str2 == "abc"); // false
首先在堆内存中开辟一块空间存放新建的String 对象(new String("abc"); ),
然后在栈中开辟一块空间存放声明的变量 str1的引用,然后 str1的引用指向 堆中的String对象。
 str1指向的是堆中的地址,常量 "abc"是存储在 String pool字符串池中的,所以他们不可能是同一对象,返回false。

问题5:以下代码的输出结果是什么?

String str1 = new String("abc");
String str2 = new String("abc");
System.out.println(str1 == str2); // false
首先在堆中开辟一块空间,存储新建 new String("abc"),然后在栈中开辟一块空间存储 str1的引用,将str1引用指向String对象。
然后在堆中再开辟一块空间,存储新建的 new String("abc")对象,这个String对象与str1指向的不是同一个对象,是两个值相同,地址不同的对象, 然后在栈中开辟空间存放 str2的引用,str2指向第二个String对象。
这两个对象只是值相同,但是在内存中的地址是不同的,所以str1==str2返回false。

问题6:以下代码输出结果是什么?

 String str1 = "a" + "b";
System.out.println(str1 == "ab"); // true
因为 "a"和"b"都是常量,所以他们都是被存储在 String池中的,根据 JVM的优化功能,会在 String池中开辟一块空间,用来存放两个常量合并后的结果"ab";
然后在栈中开辟一块空间用来存放 str1的引用,然后将 str1指向 "ab",所以 str1指代的就是 "ab"的地址,所以结果为true。

问题7:以下代码输出结果是什么?

 final String s = "a";
String str1 = s + "b";
System.out.println(str5 == "ab"); // true
同问题6一样,"a"是常量,放在String池中,s的引用指向"a",而且 s是被 final 修饰的,不可变,所以它只能指向 "a",所以 s也是个常量。
str1 的引用,指向的是 两个常量的结合,也就是 String池中的 "ab",所以打印结果是 true 。

问题8:以下代码输出结果是什么?

String s1 = "a";
String s2 = "b";
String str1 = s1 + s2;
System.out.println(str6 == "ab"); // false
s1、s2是栈中存放的两个引用,s1指向"a",s2指向"b";
s1+s2 是通过 StringBuilder 的 toString() 方法构建的一个新的String对象"ab",这个 "ab"是放在栈中的,str1的引用指向的是栈中的String对象"ab";
str1=="ab" 中的ab是放在String池中的,一个在堆中,一个在String池中,所以返回结果是false。

问题9:以下代码输出结果是什么?

 String str1 = "abc".toUpperCase();
System.out.println(str8 == "ABC"); // false
"abc"是String池中的常量,通过String的 toUpperCase()方法,新构建了一个字符串对象"ABC",这个对象是放在堆中的,因为toUpperCase()方法返回的是 return new String(),新构建的字符串对象,所以它是被放在堆中的;
栈中存放的是str1    的引用,它指向的是堆中的String对象"ABC";
而 str8 == "ABC"中的 "ABC"是一个常量,存储在String池中,所以返回结果是 false。

学习了以上内容,同学们是否对String类有了更详细的了解呢?下面我们看看以下代码的执行结果,知道是为什么嘛?如果你能够说出以下代码的结果,那么说明你对String已经了解的非常深刻了呢!

class Test1 {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = new String("abc");
        System.out.println(s1 == s2); //false
    }
}

class Test2 {
    public static void main(String[] args) {
        String s1 = new String("abc");
        String s2 = new String("abc");
        System.out.println(s1 == s2); //false
    }
}

class Test3 {
    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "ab" + "c";
        System.out.println(s1 == s2); //true
    }
}

class Test4 {
    public static void main(String[] args) {
        String s = "a";
        String s1 = "abc";
        String s2 = s + "bc";
        System.out.println(s1 == s2); // false
    }
}

class Test5 {
    public static void main(String[] args) {
        String s1 = "ab";
        String s2 = "ab" + getString();
        System.out.println(s1 == s2); //fasle
    }
    // 该方法返回的是一个String对象
    private  static String getString() {
        return "c";
    }
}

class Test6 {
    public static void main(String[] args) {
        String s = "a";
        String s1 = "abc";
        String s2 = s + "bc";
        System.out.println(s1 == s2.intern()); // ture
    }
}


        包子是边学习边总结,如果以上内容有错误的地方,欢迎大佬指正,希望包子的文章对大家有所帮助,包子会从基础开始,逐渐更新更多的 java技术及工作中遇到的问题总结。希望大家支持,谢谢大家!!!

        注:面试题部分引用了灭霸詹大佬的《詹哥秘笈之JVM知识图谱》,注释部分有包子自己的一些理解!!!

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值