0
点赞
收藏
分享

微信扫一扫

剖析java.lang.String类

柠檬果然酸 2022-04-04 阅读 121
java

介绍类的结构

可以看到String类父类是Object类,并且它实现了3个接口。一个CharSequence(是所有char字符的可读的根接口),一个comparable可比较的接口,以及序列化接口。

源码类上的注释是这样说明String类的:

  • String类是一个字符串,Java程序中的所有字符串都是该类的实例。

  • String类的实例都是一个常量,他们一旦创建就无法改变,由于这个特性,就设计了字符串缓存池,可以高效的利用它们。比如:

    • String s1 = "abc";

    • String s2 = "abc";

    • 它两的地址指向同一个缓存池中的地址。因为第一个abc一旦被创建就被缓存起来了。

  • 该类中提供了许多操作字符串的方法,比如比较字符串,查找字符串, 截取字符串,拷贝字符串,转大/小字符串等等。

  • Java语言提供对字符串做+操作,这个操作依靠StringBuilder or StringBuffer 的append()方法来完成,以及可以将对象转换成字符串,通过toString()完成。

  • null 类型的字符串去做操作会触发Null 异常。

  • A String表示UTF-16格式的字符串,其中补充字符由代理对表示 (有关详细信息,请参阅Character课程中的Character部分)。 索引值是指char代码单元,所以补充字符在String中使用两个String

    String类提供处理Unicode代码点(即字符)的方法,以及用于处理Unicode代码单元(即char值)的方法。

由此我们可以知道String类是操作字符串的,提供了大量对字符串的操作。为了高效,设计了缓存池,所有字符串值都会缓存起来。另外如果字符串是null类型,去操作的时候会触发空异常。

类源码

类字段

// 用来存储字符。(字符串的值本身都是字符,最终都存储在这个数组中),可以看到被final修饰,也就是为什么String一旦创建不可变的原因。
private final char value[];

// 缓存字符的哈希值,默认是0
private int hash; 

// 序列化值
private static final long serialVersionUID = -6849794470754667710L;

private static final ObjectStreamField[] serialPersistentFields=new ObjectStreamField[0];
// 忽略大小写的比较器
public static final Comparator<String> CASE_INSENSITIVE_ORDER= new CaseInsensitiveComparator();

类中的内部类

String类中持有一个内部类,就是它的忽略大小写的比较器

private static class CaseInsensitiveComparator
        implements Comparator<String>, java.io.Serializable {
    // use serialVersionUID from JDK 1.2.2 for interoperability
    private static final long serialVersionUID = 8575799808933029326L;

    public int compare(String s1, String s2) {
        int n1 = s1.length();
        int n2 = s2.length();
        int min = Math.min(n1, n2);
        for (int i = 0; i < min; i++) {
            char c1 = s1.charAt(i);
            char c2 = s2.charAt(i);
            if (c1 != c2) {
                c1 = Character.toUpperCase(c1);
                c2 = Character.toUpperCase(c2);
                if (c1 != c2) {
                    c1 = Character.toLowerCase(c1);
                    c2 = Character.toLowerCase(c2);
                    if (c1 != c2) {
                        // No overflow because of numeric promotion
                        return c1 - c2;
                    }
                }
            }
        }
        return n1 - n2;
    }

    /** Replaces the de-serialized object. */
    private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}

可以看到这个比较器里面有一个compare比较两个字符串的算法。

算法思路:

比较两个字符串,其实比较的是对应的阿斯克吗值,只要对应位置阿斯克吗值不相等就可以立马分出胜负,另外在考虑一些特殊情况,就好了。源码具体实现流程是这样的: ​ 先计算出两个字符串中的最小长度。然后以最小长度为单位打循环,每循环一次,就比较这两个字符串对应位置的阿斯克吗值,只要不相等,立马可以分出胜负。 ​ 那么当整个循环结束,都没有分出胜负的时候,只有两个情况:

  1. 两个字符串一模一样。

  2. 其中一个字符串是另一个字符串的子串。

这个时候直接return s1.len-s2.len 即可。 对于第一种情况,直接就retrun 0 了。正好说明相等。 对于第二种情况,如果s1包含s2.那么return的就是正数,说明s1大。否者s2大。 一句 return s1.len-s2.len 巧妙的解决了两种特殊情况。

补充:源码中还有一写字符转大小写,那都是它比较的定义,它这个比较器就叫做忽略大小写的比较。所以在实现的时候,都要把这个考虑进去。

类的构造方法

String类中有许多的重载构造器。

初始化一个空字符串String实例

新创建一个字符串以至于去替代原始的。

将char数组包装成String对象(实际调用工具类Arrays.copyOf()实现)

将char数组指定长度位置包装成Stirng对象(一样通过util包下的方法实现。)

类常用方法解析

charAt(int index):返回自身char[index]元素

public char charAt(int index) {
    if ((index < 0) || (index >= value.length)) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return value[index];
}

直接就返回自身char[]数组指定index的值。当然再次之前做了一个index的判断。

compareTo(String anotherString):按字段顺序比较两个字符串

源码注释说:按照字典顺序比较,前者大返回正数,前者小,放回负数。相等返回0;

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;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }

这个方法可以说是和String内部类比较器的compare方法一模一样,之不是这个方法不忽略大小写。实现原理一模一样。

concat(String str):将指定的字符串连接到该字符串的末尾

源码注释说明:如果参数长度为0,则返回自身。否者返回一个全新拼接的String

/*
		1. 校验参数长度
		2. 调用Arrays.copyOf()方法将当前字符串扩大
		3. 调用getChars()方法将参数中的字符追加到原字符串中。
*/
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);
}

该方法是先获取参数长度,做一个校验。所以如果参数是null,会触发null异常。

当校验通过后,调用数组工具类的copyOf方法返回一个夸大长度的新char[],然后将参数值全部追加到这个新char数组中,在调用构造器new String(char[] buf)包装成String即可。

所以重点落在了Arrays.copyOf() 和 getChars()方法中。

Arrays.copyOf() 会在util包下做深究。

getChars方法则是一个本地方法,是调用c++或者c库实现的。

void getChars(char dst[], int dstBegin) {
        System.arraycopy(value, 0, dst, dstBegin, value.length);
}

contains(CharSequence s): 判断当前字符串是否包含char字符s

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

调用indexOf()方法,而indexOf()采用暴力匹配法。

valueOf(int i):将int参数转换成String

public static String valueOf(int i) {
    return Integer.toString(i);
}

调用Integer.toString(i); 而Integer.toString(i)方法实际就是先计算出i参数的长度,然后初始化一个i长度的char数组,然后底层还是调用getChars()方法填充char数组,最后调用new String(char[]) 包装返回。

valueOf(char[] data):将char数组,包装成String

public static String valueOf(char data[]) {
    return new String(data);
}

实际调用的是构造器的包装方法。而构造器中的包装技术调用的是Arrays.copyOf().

toCharArray():将字符串转换成一个新的字符数组。

public char[] toCharArray() {
    // Cannot use Arrays.copyOf because of class initialization order issues
    char result[] = new char[value.length];
    System.arraycopy(value, 0, result, 0, value.length);
    return result;
}

可以看到是调用System.arraycopy()c++代码实现copy。然后将新数组返回。

值得注意的是:还告诉你由于类初始化顺序,没有使用Arrays.copyOf(). 也就是说String类比Arrays类先加载。

substring(int beginIndex,int endIndex):截取begin~end之间的字符串,不包括end

截取一个新的子数组,新的子数组操作不会影响到原数组。(这里说数组是因为String本身就是char数组)

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);
        }
    
    	// 如果起始起始点等于0 && 终点等于本字符串的长度,就return this 。否者调用new String
        return ((beginIndex == 0) && (endIndex == value.length)) ? this
                : new String(value, beginIndex, subLen);
    }

先是参数校验一下。判断起始点是否小于0,判断终点是否大于字符串的长度,判断起始点是否大于终止点。

当校验通过后,如果beginIndex== && endIndex等于字符串的长度就return this。否者调用重载构造器。

而这个重载构造器其实还是调用Arrays.copyOfRange(value, offset, offset+count); 这行代码。所以重点还是落在了Arrays类下。

split(String regex) & split(String regex, int limit):将字符串按照指定字符切割成String数组

当调用split(String regex) ,其实调用的是split(String regex,int limit);

public String[] split(String regex) {
    return split(regex, 0);
}

源码方法的注释描述大概就这意思:

围绕正则表达式进行分割字符串,最终返回分割后的数组。第二个参数就是期待分割后数组的长度,如果<=0,则尽可能分割。

这个方法暂时看不太懂,值得注意的是如果字符串中包含.$|()[{^?* 的话,进行分割需要前面加上转移字符//

replaceAll(String regex,String replacement):将自身的包含的regex字符,全部替换成replacement字符。

indexOf():返回子串str在大字符串中第一次出现的索引。

public int indexOf(String str) {
	// 上来会一直调用重载方法
    ....
}

// 最终会调用到这个重载方法
/*
	source:大串字符串的char数组
	sourceOffset:大串的原始偏移量,默认0
	sourceCount:大串字符串的长度
	target:子串的char数组
	targetOffset:子串的偏移量,默认0
	targetCount:子串字符串的长度
	fromIndex:从大串开始搜索的起始索引
	
	
方法解说:
	大致思路就是:遍历大串,然后判断大串的下标i的字符是不是等于子串的首字符,如果是,那么继续比对剩余的字符。
    	如果能把子串全部比对完毕,就说明找到了,直接return 大串的下标i字符。
    	
    可以看出JDK源码中用的是最普通的暴力法,并没有用到kmp算法。
*/
static int indexOf(char[] source, int sourceOffset, int sourceCount,
            char[] target, int targetOffset, int targetCount,
            int fromIndex) {
    	/*
    		判断大串是否为空字符串,如果是,就继续判断子串的情况
    		子串如果也是空,那么return 大串的长度也就是0;
    			如果不是,那么说明大串都已经为空字符串了,不可能找到,直接return -1;
    	*/
        if (fromIndex >= sourceCount) {
            return (targetCount == 0 ? sourceCount : -1);
        }
    
    	// 更新正确的fromIndex
        if (fromIndex < 0) {
            fromIndex = 0;
        }
    	// 如果子串长度等于0,直接返回fromIndex 也就是0
        if (targetCount == 0) {
            return fromIndex;
        }

  		// 获取子串的首字符
        char first = target[targetOffset];
    	// 计算出一个最大max长度,目的是为了优化代码。如果当前大串还有3个长度未遍历,而子串长度是4,那么在怎么比对也不可能全部比对上了。所以计算出一个max。遍历大串只要遍历到max位置就好了。
        int max = sourceOffset + (sourceCount - targetCount);
    
        for (int i = sourceOffset + fromIndex; i <= max; i++) {
            // 判断当前大串下标是不是和子串首字符相等。如果不是,就一直操作i下标,直到相等。
            if (source[i] != first) {
                while (++i <= max && source[i] != first);
            }

            /* 程序能执行到此处,说明当前大串的i下标位置和子串的首位置相等了,那么接下来的工作就是继续比对剩余的部分了。
            源码中的继续比对环节是这样的。先定义2个指针,一个j,一个end。j:就是大串i+1的位置。end:就是全部比对上,最后的位置。
            	然后遍历子串,从子串1位置开始,每遍历一个看看j的位置是否和子串对应的位置相等。如果相等j++,子串下标++。
            	当j指针能碰到end的位置的时候,说明子串全部比对上了,就返回大串的i下标即可。
            
            */
            if (i <= max) {
                int j = i + 1;
                int end = j + targetCount - 1;
                for (int k = targetOffset + 1; j < end && source[j]
                        == target[k]; j++, k++);

                if (j == end) {
                    return i - sourceOffset;
                }
            }
        }
        return -1;
    }

equals(Object anObject):将此字符串与指定对象比较。

源码方法注释:

将字符串与指定参数对象比较,如果参数是String,并且对应字符序列全相等则return true.

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

可以看到String 类equals还是很好理解的,

  1. 比较内存地址是否一致,如果一致,return true;

  2. 判断参数是否是String类,如果是继续判断两字符串长度是否一致。都满足开始正规比较。

    正规比较:对应位置字符是否一致,只要不一致就终止。当循环结束,return true。

总结

在String类中只要涉及到改变字符串的char[]数组,如new String(char[] char)concat(String s)substring()等就调用Arrays.copyOf()实现数组的拷贝,而Arrays.copyOf(),调用的是System.arraycopy()也就是c代码。所以重点来时落在了c原理上。这块需要深究。另外还有一些字符串匹配的方法如split(),raplaceAll等,都使用的是util包下的Pattern类实现,依靠正则表达式实现。

在String类中还有一些比较好玩的方法,如compare比较、indexOf、equals、repalceAll等都可以手动实现下。

comapre:可以刷leetcode 165. 比较版本号

indexOf:对应leetcode 28.实现strstr()

replaceAll:对应leetcode 833: 字符串中的查找与替换

举报

相关推荐

0 条评论