Java源码学习-String类源码的常用api阅读

String

初学java的时候被问最多的一个问题就是String在java里是对象还是基本类型, 现在很明显, 这是个类, String其实也就是个字符数组

private final char value[];

构造器

有一说一 这构造器估计是最多的一个类吧?

	// 无参构造器, 原英文注释也说这个构造器是不必要的, 因为String本身的value就是final修饰的
	// 它不可变, 所以直接String a = ""就好了, 除非你想要一个新的对象
	// 这里的""之所以有value是因为""其实在内存中也是一个String对象
	public String() {
        this.value = "".value;
    }
    // 可以理解为深拷贝一个String对象, 我们最常用的String a = new String("123")就是这个构造器
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }
    // 传入一个char数组, 这里的copyOf是浅拷贝操作
    // 下面有一个关于这个构造器的实验
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
    // 这个构造器赋值的value是传入的value的offset下标开始, 到offset+count结束
    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        // copyOfRange方法也是一个浅拷贝
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }
    // 这个构造器入参是一个int数组
    public String(int[] codePoints, int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= codePoints.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > codePoints.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }

        final int end = offset + count;

        // Pass 1: Compute precise size of char[]
        int n = count;
        for (int i = offset; i < end; i++) {
            int c = codePoints[i];
            // 这个方法将c右移16位, 检测这个c高十六位是否为0
            if (Character.isBmpCodePoint(c))
                continue;
            // 因为int是32位的而char是16位的, 所以一个int是可以用两个char表示的
            // 这里是判断int的高16位如果比最大的unicode要小, 就说明这个int需要用两个char表示
            else if (Character.isValidCodePoint(c))
                n++;
            // 否则抛出异常
            else throw new IllegalArgumentException(Integer.toString(c));
        }

        // Pass 2: Allocate and fill in char[]
        final char[] v = new char[n];

        for (int i = offset, j = 0; i < end; i++, j++) {
            int c = codePoints[i];
            if (Character.isBmpCodePoint(c))
                v[j] = (char)c;
            // 这一步就是将这个c用两个char来储存
            else
                Character.toSurrogates(c, v, j++);
        }

        this.value = v;
    }
	// 这里就是将byte用charsetName编码储存了
    public String(byte bytes[], String charsetName)
            throws UnsupportedEncodingException {
        this(bytes, 0, bytes.length, charsetName);
    }
    public String(byte bytes[], int offset, int length, String charsetName)
            throws UnsupportedEncodingException {
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        // 检测数组越界
        checkBounds(bytes, offset, length);
        // 进入StringCoding类的decode方法, 将bytes从offset到offset+length用charsetName
        // 编码返回一个char[]
        this.value = StringCoding.decode(charsetName, bytes, offset, length);
    }

测试入参为char[]的构造器

	// 这里的输出是123, 证明了虽然是copyOf浅拷贝对value赋值的, 但是由于value是final修饰的
	// 所以改变原数组不会对String有影响
    public static void main(String[] args) {
        char[] val = new char[]{'1', '2', '3'};
        String b = new String(val);
        val[0] = '2';
        System.out.println(b);
    }

由于String构造器是在是太多了, 所以只挑一些重要的

init和cinit

也是历史遗留问题, 顺便在这里讲一下init和cinit的区别, 我们都知道init是对象实例化的时候所调用的构造器方法

public class StringTest {

    public static void main(String[] args) {
        String a = new String();
    }
}

用javap来看看字节码

public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/String
       3: dup
       4: invokespecial #3                  // Method java/lang/String."<init>":()V
       7: astore_1
       8: return

可以看见我们new一个对象的这个操作执行的是init方法
而与之对应的还有个cinit, 这个cinit是在虚拟机对类进行加载的时候所使用的构造器,
在jvm的类加载阶段中是有一个初始化阶段的, 下面是深入理解java虚拟机一书中的原文

进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通 过程序编码制定的主观计划去初始化类变量和其他资源。我们也可以从另外一种更直接的形式来表 达:初始化阶段就是执行类构造器clinit()方法的过程。clinit()并不是程序员在Java代码中直接编写 的方法,它是Javac编译器的自动生成物

这里可以看见cinit其实可能跟我们无关, 它是编译器生成的产物, 在这个阶段jvm会给类中的静态变量赋值(这里的静态变量在准备阶段会赋初始值)

getChars && getBytes

    public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
        if (srcBegin < 0) {
            throw new StringIndexOutOfBoundsException(srcBegin);
        }
        if (srcEnd > value.length) {
            throw new StringIndexOutOfBoundsException(srcEnd);
        }
        if (srcBegin > srcEnd) {
            throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
        }
        // arraycopy方法将value从srcBegin开始到srcEnd复制到dst
        // 下标位置是dstBegin到dstBegin + srcEnd - srcBegin
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }
	// 常用的应该是这个
	public byte[] getBytes(String charsetName)
            throws UnsupportedEncodingException {
        if (charsetName == null) throw new NullPointerException();
        // 就是进入StringCoding类的encode方法用charsetName来解码
        return StringCoding.encode(charsetName, value, 0, value.length);
    }

equals

在object中的equals是比较引用是否相等, 而String中则将equals方法重写了

    public boolean equals(Object anObject) {
    	// 引用相等, 可以判断是同一个对象
        if (this == anObject) {
            return true;
        }
        // 传入的如果不是String类型, 那肯定不相等
        if (anObject instanceof String) {
        	// 直接转为String
            String anotherString = (String)anObject;
            int n = value.length;
            // 如果字符串长度不相等, 那两个字符串一定不相等
            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;
    }

compareTo

比较两个字符串的大小

    public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        // 这里从0开始遍历, 返回下标数字的大小
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            // 如果c1大于c2, 那么返回的是正数
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        // 如果上面一直都是相等, 那么用长度比较
        return len1 - len2;
    }

返回正数就是当前的字符串大, 返回负数就是入参大

hashCode

这里是计算hashCode的方法, 之前也写过一篇相同hashCode的String但是值不同的文章
两个字符串的hashCode相同但这两个字符串不相等的情况

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

可知
str.charAt(0) * 31^(n-1) + str.charAt(1) * 31^(n-2) + … + str.charAt(n-1)
所以这个hashCode与object的不一样, 在object中hashCode是一个本地方法, 是根据内存地址计算得出的
至于String为什么要重写hashCode, 这里我有一个解释: 如果不重写hashCode那么我们使用equals判断出来相等的String他的hashCode可能就不相等, 这样的话在HashMap中他们就可能不在同一个桶中, 所以就不可能使用HashMap, 这就是HashMap的key一定要是重写过这两个方法的原因

indexOf

    public int indexOf(int ch, int fromIndex) {
        final int max = value.length;
        if (fromIndex < 0) {
            fromIndex = 0;
        } else if (fromIndex >= max) {
            // Note: fromIndex might be near -1>>>1.
            return -1;
        }
		// Character.MIN_SUPPLEMENTARY_CODE_POINT这个值是65535
        if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
            // handle most cases here (ch is a BMP code point or a
            // negative value (invalid code point))
            final char[] value = this.value;
            // 从起始点开始寻找ch, 返回下标
            for (int i = fromIndex; i < max; i++) {
                if (value[i] == ch) {
                    return i;
                }
            }
            return -1;
        } else {
            return indexOfSupplementary(ch, fromIndex);
        }
    }

经过我自己的测试后发现:BmpCodePoint代码点是65535是2的16次方,刚好是两个字节(即一个字)的大小。在超出两个字节后只能算是有效的代码点,并非是BmpCodePoint代码点。从代码中也可看出,BmpCodePoint代码点的整数是可以直接强转成char类型的。在java中char类型刚好占2个字节,在2个字节以内的整数都可以直接强转换成char类型!
这里需要了解Unicode字符的原理,以及代码点的概念!
————————————————
版权声明:本文为CSDN博主「岁月丶丿静好」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u013035972/article/details/51954177/

这里是转载的一段为什么要对这个ch进行判断的原因

lastIndexOf与indexOf一样, 仅仅只是从后往前遍历而已

subString

    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);
        }
        // 使用了一个三元表达式这里的语义可以理解为
        // if(beginIndex == 0 && endIndex == value.length) this;
        // else new String(value, beginIndex, subLen)
        // 其实就是判断我要获得的子字符串是不是就是当前字符串, 如果不是才new一个回去
        // 这个构造器上面也有讲到
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }

concat

将str接到this的后面

    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        // 获取buf, 这里的copyOf是将value浅拷贝到一个新的数组中返回, 数组长度是len + otherLen
        char buf[] = Arrays.copyOf(value, len + otherLen);
        // 这里使用了getChars方法, 将自己的value放入buf, 位置是len~len+otherLen
        str.getChars(buf, len);
        // 使用拼接好的char[]来构造一个新的String返回
        return new String(buf, true);
    }

replace

    public String replace(char oldChar, char newChar) {
    	// 如果新的跟老的一样就无需替换
        if (oldChar != newChar) {
            int len = value.length;
            int i = -1;
            // 因为value是final修饰的, 所以这里必须开一个新的数组进行修改操作
            char[] val = value; /* avoid getfield opcode */
			// 寻找oldChar在val中的位置
            while (++i < len) {
                if (val[i] == oldChar) {
                    break;
                }
            }
            // 这里是在val中寻找到了这个oldChar进入的if语句
            if (i < len) {
                char buf[] = new char[len];
                // 在i之前都无oldChar出现过
                for (int j = 0; j < i; j++) {
                    buf[j] = val[j];
                }
                // 在i之后可能有多个oldChar出现, 所以用循环寻找替换
                while (i < len) {
                    char c = val[i];
                    buf[i] = (c == oldChar) ? newChar : c;
                    i++;
                }
                // 替换结束之后用buf生成一个新的String对象返回
                return new String(buf, true);
            }
        }
        return this;
    }

matches

这里是正则表达式相关的内容, 以后再看

    public boolean matches(String regex) {
        return Pattern.matches(regex, this);
    }

contains

这里直接是使用indexOf方法的, 因为indexOf返回的是第一次搜索到的下标

public boolean contains(CharSequence s) {
        return indexOf(s.toString()) > -1;
    }

split

split也是一个常用的api, 根据输入的字符串分割字符串返回一个字符串数组

public String[] split(String regex, int limit) {
        /* fastpath if the regex is a
         (1)one-char String and this character is not one of the
            RegEx's meta characters ".$|()[{^?*+\\", or
         (2)two-char String and the first char is the backslash and
            the second is not the ascii digit or ascii letter.
         */
        char ch = 0;
        // 如果regex的长度是1, 那么regex不可以是".$|()[{^?*+\\"这些字符
        // 或者regex的长度是2, 那么第二个字符不属于0-9,a-z,A-Z
        // 满足上述两条的一条并且满足ch属于某一范围时才可以进入if
        if (((regex.value.length == 1 &&
             ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
             (regex.length() == 2 &&
              regex.charAt(0) == '\\' &&
              (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
              ((ch-'a')|('z'-ch)) < 0 &&
              ((ch-'A')|('Z'-ch)) < 0)) &&
            (ch < Character.MIN_HIGH_SURROGATE ||
             ch > Character.MAX_LOW_SURROGATE))
        {
            int off = 0;
            int next = 0;
            boolean limited = limit > 0;
            ArrayList<String> list = new ArrayList<>();
            // ch在String中还存在, 我们这里假设limit就是0
            while ((next = indexOf(ch, off)) != -1) {
            	// !limited是true
                if (!limited || list.size() < limit - 1) {
                	// list中加入off到next这一段, 达成分割目的
                    list.add(substring(off, next));
                    off = next + 1;
                } else {    // last one
                    //assert (list.size() == limit - 1);
                    list.add(substring(off, value.length));
                    off = value.length;
                    break;
                }
            }
            // If no match was found, return this
            // 没有做分割
            if (off == 0)
                return new String[]{this};

            // Add remaining segment
            // 把最后一段加入list
            if (!limited || list.size() < limit)
                list.add(substring(off, value.length));

            // Construct result
            int resultSize = list.size();
            if (limit == 0) {
            	// 如果list不为空并且list的末尾字符串长度为0, 就把大小-1
                while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
                    resultSize--;
                }
            }
            // 这里就是放入list中不为空的元素
            String[] result = new String[resultSize];
            return list.subList(0, resultSize).toArray(result);
        }
        // 这里应该是regex是个正则表达式, 就调用正则的分割
        return Pattern.compile(regex).split(this, limit);
    }

intern

最后讲讲intern, 之前有一个面试题是这样的

        String a = "ab";
        String b = new String("ab");
        System.out.println(a == b);
  • 输出什么
  • 输出false
  • 那么对b进行什么操作可以输出true
  • b.intern()
    intern这个方法在String中是个本地方法
    public native String intern();

学习过jvm之后就会明白, String a = “ab” 这里的ab是分配在常量池里的, 而new String是分配在堆上的一个新对象, 在java8中, 移除了永久代这个概念

到了JDK 7的HotSpot,已经把原本放在永久代的字符串常量池、静态变量等移出,而到了 JDK 8,终于完全废弃了永久代的概念,改用与JRockit、J9一样在本地内存中实现的元空间(Meta- space)来代替,把JDK 7中永久代还剩余的内容(主要是类型信息)全部移到元空间中。

所以常量池是被放入了java堆中, 所以b调用intern方法就会将自己的value在常量池中分配, 如果常量池中已经存在则直接引用, 不存在就创建, 所以这个时候a和b做==判断就会返回true, 他们实质指向的都是常量池中的"ab"

后记

String中的方法实在是太多, 理解了jvm之后去看源码真的能透彻很多, 就比如intern, 若是不理解java虚拟机规范中的运行时数据区域, 实在会不明白这个操作

猜你喜欢

转载自blog.csdn.net/Cia_zibo/article/details/105666899