深入理解Java常用类----String(二)

     上篇介绍了String类的构造器,获取内部属性等方法,最后留下了最常用的局部操作函数没有介绍,本篇将接着上篇内容,从这些最常见的函数的操作说起,看看我们日常经常使用的这些方法的内部是怎么实现的。第一个函数:

    public boolean startsWith(String prefix, int toffset) {
        char ta[] = value;
        int to = toffset;
        char pa[] = prefix.value;
        int po = 0;
        int pc = prefix.value.length;
        // Note: toffset might be near -1>>>1.
        if ((toffset < 0) || (toffset > value.length - pc)) {
            return false;
        }
        while (--pc >= 0) {
            if (ta[to++] != pa[po++]) {
                return false;
            }
        }
        return true;
    }

该方法用于判断是否当前的字符串对象是以指定的子串开头。prefix参数指定了这个字串,toffset参数指定了要从原字符串的哪里开始查找。先看个例子:

    public static void main(String[] args){
        String str = "hello-walker";
        System.out.println(str.startsWith("wa", 0));
        System.out.println(str.startsWith("wa",6));
    }

结果如下:

这里写图片描述

源代码相对而言也是比较容易理解的,首先是做了个简单的判断,如果toffset小于0或者toffset和prefix的长度超过了原字符串的长度,直接返回false。接着通过了一个while循环从原字符串的toffset位置和prefix的0位置开始,一个字符一个字符的比较,一旦发现有两者在某个位置的字符值是不等的,返回false,否则在循环结束时返回true。该方法还有一个重载,该重载默认toffset为0,即从原字符串的开头开始搜索。

endWith这个方法其实内部调用的还是上述介绍的startWith方法。

public boolean endsWith(String suffix) {
        return startsWith(suffix, value.length - suffix.value.length);
    }

我们看到该方法内部调用的startsWith方法,第二个参数传入的是value.length - suffix.value.length,该参数将会导致程序跳过前面一部分的字符,直接跳到还剩下suffix.value.length的字符的位置处。

下面我们看看hashCode在String类中的的实现:

    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;
    }

虽然知道是这么实现的,但是我不知道为什么这么做。只知道它每次都乘31然后加上当前字符的Unicode编号。下面看一个重要的方法:

    public int indexOf(int ch, int fromIndex) {
        final int max = value.length;
        if (fromIndex < 0) {
            fromIndex = 0;
        } else if (fromIndex >= max) {
            return -1;
        }

        if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
            final char[] value = this.value;
            for (int i = fromIndex; i < max; i++) {
                if (value[i] == ch) {
                    return i;
                }
            }
            return -1;
        } else {
            return indexOfSupplementary(ch, fromIndex);
        }
    }

indexOf方法用于返回某个字符首次出现的位置,当然对应的还有lastIndexOf,我们一点点看。上述的方法,两个参数,第一个参数的值表示需要查找的指定字符(我们知道字符和int型是可以无条件互转的,所以这里用int接收),后面的代码主要分为两部分,一部分是大部分情况,另一部分则是专门用于处理增补字集情况,该情况我们暂时不去研究。第一部分的代码就比较简单了,遍历整个字符串对象,如果找到指定字符,则返回当前位置,否则返回-1。当然该方法也有一些重载,但本质都是调用了上述介绍的方法。

lastIndexOf方法类似,只不过他是从后往前查找,此处不再赘述。

下面看一个截取子串的方法:

public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }

该方法的前面两个判断主要用于处理一些极端情况,最后一条语句是该方法的核心。如果beginIndex 为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);
    }

从该重载的两个参数可以看出来,之前只提供一个beginIndex则默认从开始索引处全部截取余下字符。而此处指定endIndex则选择性的截取从beginIndex到endIndex之间的子串作为结果返回。具体的实现也是类似,只是多了一些判断。

下面介绍的方法可以连接两个不同的字符串。

    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }

该方法具有一个参数,该参数的值是一个字符串对象,用于连接在当前字符串对象的后面。前三行很简单,就是判断连接字符串str是否为空,如果是则直接返回当前字符串对象,我们看到很多的方法源码都是会把核心方法放在最后面,前面是一堆判断,这也是一种效率的体现,就是说如果不满足调用该方法的条件则直接在前面被pass了,而不用调用复杂耗时的核心方法。Arrays.copyOf 方法用于创建一个能够容纳上述两个字符串的更大的数组,然后将原字符串复制到进去,后面留给str的位置为空。接着调用getChars方法从偏移量为len的索引位置开始将str中字符拷贝到buf中,最后构建字符串对象返回。

下面看一个更为实用的方法:

    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;
    }

该方法用于替换字符串对象中指定的某个字符,当然它会替换掉所有的oldchar。该方法首先判断oldchar(需要被替换的字符)是否和newchar(替换它的字符)相等,如果相等则不用做任何操作,直接返回当前字符串对象,否则,通过while循环找到第一个oldchar,然后重新构建了一个char数组,该数组和value这个数组长度一样,接着将第一个oldchar位置之前的所有字符复制到新数组中,然后while循环一边遍历value数组查找oldchar并替换为newchar,一边将newchar添加到新数组中,最后返回新数组构造的String 对象。

上述的该方法只能替换指定的一个字符,但是不能替换某个子串。下面的几个方法都是用于替换某个子串。

@1替换第一个子串
public String replaceFirst(String regex, String replacement) {
        return Pattern.compile(regex).matcher(this).replaceFirst(replacement);
    }
@2替换每一个符合规则的子串
public String replaceAll(String regex, String replacement) {
        return Pattern.compile(regex).matcher(this).replaceAll(replacement);
    }
@3
public String replace(CharSequence target, CharSequence replacement) {
        return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(
                this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
    }

上述的第一个方法是相对较为好理解,第二个和第三个方法都是替换所有指定的子串,他们的区别在于,replaceAll方法是基于正则表达式的,replace则只针对char串的替换。例如:

    public static void main(String[] args){
        String str = "aaabssddaa\\\\";
        System.out.println(str);
        System.out.println(str.replace("\\\\", "x"));
        System.out.println(str.replaceAll("\\\\", "x"));
    }

输出结果:

这里写图片描述

我们知道在Java中 \ 表示转义字符,也就是上述的str中 \\ 将被转义成两个 \ ,而在正则表达式中该符号也是转义字符,所以我们 replaceAll 方法中的第一个参数的实际值为:\,被转义了两次,所以针对str中的 \的替换,replaceAll 输出两个x,而在replace方法中,四个\被Java转义了一次为两个\,所以replace输出一个x。它两区别就是一个是基于正则表达式的,一个则只针对char子串。

下面看一个分割字符串的函数split,由于代码比较多,此处就不贴出来了,我大致介绍下实现原理。该方法的参数依然是依赖正则表达式的,其内部定义了一个ArrayList,定义一个用于匹配字符串的Matcher对象,然后while循环去find原字符串对象,如果找到则直接subSequence前面的所有字符集合,并添加到ArrayList中,然后起始位置从0跳到当前位置之后继续搜索,最后ArrayList对象的toArray方法,返回String类型数组。

下面看一个join方法:

    public static String join(CharSequence delimiter, CharSequence... elements) {
        Objects.requireNonNull(delimiter);
        Objects.requireNonNull(elements);
        // Number of elements not likely worth Arrays.stream overhead.
        StringJoiner joiner = new StringJoiner(delimiter);
        for (CharSequence cs: elements) {
            joiner.add(cs);
        }
        return joiner.toString();
    }

首先,该方法是静态方法。然后该方法中涉及到一个类StringJoiner ,它有一个构造方法:

    public StringJoiner(CharSequence delimiter,
                        CharSequence prefix,
                        CharSequence suffix) {
        Objects.requireNonNull(prefix, "The prefix must not be null");
        Objects.requireNonNull(delimiter, "The delimiter must not be null");
        Objects.requireNonNull(suffix, "The suffix must not be null");
        // make defensive copies of arguments
        this.prefix = prefix.toString();
        this.delimiter = delimiter.toString();
        this.suffix = suffix.toString();
        this.emptyValue = this.prefix + this.suffix;
    }

该构造函数为该类的一些字段赋值,至于这些字段时干什么的,等再次遇到的时候介绍,此处只需了解下他们的存在。此处调用该构造函数并传入delimiter分割符,然后调用了该类对象的add方法,

    public StringJoiner add(CharSequence newElement) {
        prepareBuilder().append(newElement);
        return this;
    }
    
    private StringBuilder prepareBuilder() {
        //此处value为一个StringBilder实例,是StringJoiner的一个成员
        if (value != null) {
            value.append(delimiter);
        } else {
            value = new StringBuilder().append(prefix);
        }
        return value;
    }

第一次add会走else部分,新建一个StringBuilder对象并添加prefix元素(此处在调用构造器的时候为其赋值为空)赋值给我们的成员变量,回到add方法添加该元素到StringBuilder中,第二次到prepareBuilder方法中只会向StringBuilder实例中添加delimiter分割符,然后出来add方法中又将第二个元素添加到其中。这样就完成了为这些元素连接一个分隔符,并放入到StringBuilder实例中,最后tostring返回。看个例子:

public static void main(String[] args){
        String[] strs = new String[]{"hello","walker","yam","cyy","huaaa"};
        System.out.println(String.join("-",strs));
    }
输出结果:hello-walker-yam-cyy-huaaa

最后还有两个方法,比较简单不再赘述其原理实现。

//返回内部的字符数组,之所以不直接返回value是为了封装的严密性
public char[] toCharArray() {
        char result[] = new char[value.length];
        System.arraycopy(value, 0, result, 0, value.length);
        return result;
    }
//去除头尾部的空格
public String trim() {
        int len = value.length;
        int st = 0;
        char[] val = value;    /* avoid getfield opcode */

        while ((st < len) && (val[st] <= ' ')) {
            st++;
        }
        while ((st < len) && (val[len - 1] <= ' ')) {
            len--;
        }
        return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
    }

至此,有关String源码的阅读大致结束,并没有涉及全部代码,有些源码束作者能力问题,没能完全参透,总结的不好,见谅!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容