Open xzhuz opened 5 years ago
https://meisen.pro/article/35e1b68bacd64a48893597c3a806a67f
困而学,学而知 说了很久要写一系列Java源码的小记,但是都由于各种忙,都没有时间来写,最近刚好空闲下来,就开始写了。
说到源码,相信大家的第一个念头就是很难、难啃,比如说Spring源码。从而每每都对源码望而却步,只要是能用就可以了。但是这样真的可以了吗?为什么在代码评审的时候,别人能够在代码中看出问题并提出优化意见,而自己却不能?为什么别人的代码bug总是很少...都是别人,我决定了,我也要做这个别人,那么就和我一起来看Java源码吧。
别人
String可以说是我们在代码开发中用的最大的类之一,本着从常用的开始说起的原则,我们就先从String的源码开始吧。本文主要说String的一些特性和常用的一些类。
String 类
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** 用来保存字符的数组. */ private final char value[]; /** 缓存String的hash值 */ private int hash; // Default to 0 }
首先来说说final,很多其他文章也说到了这个关键字。不管怎样,我这边也不得不说。
final
先来回忆一些final关键字的作用:
那这里使用final来修饰String,说明了String类是一个不可变类。请注意这句话,这句话十分重要。后面我们会讲到String如何保证不可变的。
为什么要让String是不可变类呢?
String Pool 的需要
如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool
缓存HashCode
String类型的值经常用来作为hash值。使String不可变性可以保证hash值一直不变而不用进行重复运算。
安全性
String常被用作参数。比如说网络连接、打开文件等。网络连接中,如果String是可变的,就有可能会在连接过程中改变从而导致连接到其他主机。
可变的字符串数组在反射中也可能造成安全问题?这个没有想到例子,后期看到了再说
线程安全
不可变的类本身就是线程安全的,因为初始化就不可能任何线程修改
分别来说这几个接口的作用
Serializable
说明String是可序列化的
Comparable
是一个排序接口
若一个类实现了Comparable接口,就意味着该类支持排序。 即然实现Comparable接口的类支持排序,假设现在存在“实现Comparable接口的类的对象的List列表(或数组)”,则该List列表(或数组)可以通过 Collections.sort(或 Arrays.sort)进行排序。
此外,“实现Comparable接口的类的对象”可以用作“有序映射(如TreeMap)”中的键或“有序集合(TreeSet)”中的元素,而不需要指定比较器。
CharSequence
说明String是一个可读的字符数组,这个接口为不同的字符数组操作提供了统一可读的接口。charAt、length、subSequence、toString
字符
字符数组
charAt
length
subSequence
toString
再看看String类中最重要的属性,我们可以看到这个属性其实也是被final修饰,说明value[]一旦被初始化了,就不会太修改了。请注意,final修饰对象,说明对象的内存地址是不能改变,但是对象中的属性是可变的。也就是说,我们在对value[]初始化之后就不能再new一次了(分配地址)。
value[]
new
我们可以直接把value[]直接当成String的值,我们对String的任何操作其实都是对value[]的操作。就比如说下面的代码。
说了这么多,说好是看源码,结果现在连源码都没有看到。那话不多说,还是上菜吧。
再多说一句,我们是说几个我们在工作用常用到方法的源码。
// s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 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; }
其实这个我们在平常工作中用得很少,或者说根本都没有用,但是他的作用确实巨大的。最重要的一个做法就是来判断两个String是否相等。之前有过一篇写HashMap一些概念的文章,详细说了说Hash的概念。需要详细了解Hash的概念可以参考这篇文章。
说了hashCode当然要说说equals,谁叫它俩是一对呢。
hashCode
equals
equals和==区别在于他们的用处是不同的。equals用于比较引用类型,==用于比较基本类型。究其原因是equals是比较两个对象的地址是否相等(或者每个对象域是否相等),==用于比较两个的值是否相等。
==
先来说说要实现一个高效的equals的原则。
instanceof
关键域
引用《Effecttive Java 第三版 》 第三章
通过上面实现高效equals的原则,我们来看String#equals就很简单的。
String#equals
public boolean equals(Object anObject) { // 比较两个对象的值是否相等 if (this == anObject) { return true; } // 判断anObject是否是String类型 if (anObject instanceof String) { // 转换类型 String anotherString = (String)anObject; int n = value.length; // 下面就是对比每个关键域是否相等,由于String本质是一个value[], // 这里也就是对比数组中的每一个值是否相等, 只有有一个不等,直接返回false 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; }
substring的作用是在当前的字符串的基础上截取一个字符串。请注意,调用该方法必须要接收返回值,不然也没有用呀。
substring有两个重载函数:substring(int beginIndex)和substring(int beginIndex, int endIndex)。两个方法的原理都是一样的,都是调用String的构造函数新建一个String返回。
substring(int beginIndex)
substring(int beginIndex, int endIndex)
// 用参数多的类说明 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); } // 上面都是必要的一些校验 // 如果截取的长度是字符串的长度且起点是字符串起点,则直接返回当前字符串 // 否则新建一个子字符串,返回 // 这里我们也可以进一步印证String的不可变特性,原始的字符串其实并没有改变。 return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen); } // 这个是String的一个构造函数, // value[]: 用来构建字符串的原始字符数组 // offset: 初始偏移量,也就是说从数组的那个位置开始构建字符串, 0<=offset<value.length // count: 要构建过长的字符串. 这个大小要根据offset变化 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); } // 其实这里是调用了 // System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length); // 也就是将一个数组复制到一个新的数组 this.value = Arrays.copyOfRange(value, offset, offset+count); }
public static void main(String[] args) { String str = "The movie NeZha is awesome."; // 从第五个字符开始截取,也就是"ovie NeZha is awesome." System.out.println(str.substring(5)); // 截取第2到第5个字符, 也就是"NeZha" System.out.println(str.substring(10, 15)); System.out.println(str); }
ovie NeZha is awesome. NeZha The movie NeZha is awesome. // 这里截取了两次,我们后面输出str,还是没有变,也印证了String的不变特性
replace有两个重载方法: replace(char oldChar, char newChar)和String replace(CharSequence target, CharSequence replacement)
replace(char oldChar, char newChar)
String replace(CharSequence target, CharSequence replacement)
replaceAll只有一个方法: String replaceAll(String regex, String replacement)
String replaceAll(String regex, String replacement)
这个方法其实比较简单。也就是遍历String中的value[]每个值,找到相同的,替换掉。
public String replace(char oldChar, char newChar) { // 两个值相等就不需要替换了 if (oldChar != newChar) { int len = value.length; int i = -1; // 避免:获取指定类的实例域,并将其值压入栈顶 // 在一个方法中需要大量引用实例域变量的时候, // 使用方法中的局部变量代替引用可以减少getfield操作的次数,提高性能 char[] val = value; /* avoid getfield opcode */ // 找到oldChar第一次出现的位置 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; }
public String replace(CharSequence target, CharSequence replacement) { return Pattern.compile(target.toString(), Pattern.LITERAL).matcher( this).replaceAll(Matcher.quoteReplacement(replacement.toString())); } public String replaceAll(String regex, String replacement) { return Pattern.compile(regex).matcher(this).replaceAll(replacement); }
为什么要放到一起讲,大家其实可以从源码都可以看出来了。
看了两个两个源码,我们可以看出来什么?
那就是repace(char, char)比replace(CharSequence, CharSequence)和replaceAll(String, String)的性能普遍要好,因为后面两个用了正则,而第一个只是遍历替换而已。
其实不管是startWith和endsWith最终的都是调用下面的方法:
public boolean startsWith(String prefix, int toffset) { // 原始字符串数组 char ta[] = value; // 对比开始位置 startWith是0, endsWith是value.lenght-prefix.length // 用于指向value数组的下一个位置,每次遍历都会+1 int to = toffset; // 字符串的数组 char pa[] = prefix.value; // 字符串, 用于指向prefix数组的下一个位置,每次遍历都会+1 int po = 0; // 字符串的长度 int pc = prefix.value.length; // Note: toffset might be near -1>>>1. if ((toffset < 0) || (toffset > value.length - pc)) { return false; } // 遍历prefix while (--pc >= 0) { // 对比从ta[toOffset]和prefix[0]开始的每一个位置上面的值是否相等。 // 如果有一个不相等,就直接返回false if (ta[to++] != pa[po++]) { return false; } } return true; } public boolean endsWith(String suffix) { return startsWith(suffix, value.length - suffix.value.length); } public boolean startsWith(String prefix) { return startsWith(prefix, 0); }
上面算是我理解的关于String的源码,其实String还有很多可以探究的地方,一篇文章太短,无法全部说的明明白白。学无止境,其他的还需要继续摸索。
String
https://meisen.pro/article/35e1b68bacd64a48893597c3a806a67f
说到源码,相信大家的第一个念头就是很难、难啃,比如说Spring源码。从而每每都对源码望而却步,只要是能用就可以了。但是这样真的可以了吗?为什么在代码评审的时候,别人能够在代码中看出问题并提出优化意见,而自己却不能?为什么别人的代码bug总是很少...都是别人,我决定了,我也要做这个
别人
,那么就和我一起来看Java源码吧。String可以说是我们在代码开发中用的最大的类之一,本着从常用的开始说起的原则,我们就先从String的源码开始吧。本文主要说String的一些特性和常用的一些类。
String 类
final
首先来说说
final
,很多其他文章也说到了这个关键字。不管怎样,我这边也不得不说。先来回忆一些final关键字的作用:
那这里使用final来修饰String,说明了String类是一个不可变类。请注意这句话,这句话十分重要。后面我们会讲到String如何保证不可变的。
为什么要让String是不可变类呢?
String Pool 的需要
如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool
缓存HashCode
String类型的值经常用来作为hash值。使String不可变性可以保证hash值一直不变而不用进行重复运算。
安全性
String常被用作参数。比如说网络连接、打开文件等。网络连接中,如果String是可变的,就有可能会在连接过程中改变从而导致连接到其他主机。
线程安全
不可变的类本身就是线程安全的,因为初始化就不可能任何线程修改
Serializable, Comparable, CharSequence
分别来说这几个接口的作用
Serializable
说明String是可序列化的
Comparable
是一个排序接口
若一个类实现了Comparable接口,就意味着该类支持排序。 即然实现Comparable接口的类支持排序,假设现在存在“实现Comparable接口的类的对象的List列表(或数组)”,则该List列表(或数组)可以通过 Collections.sort(或 Arrays.sort)进行排序。
此外,“实现Comparable接口的类的对象”可以用作“有序映射(如TreeMap)”中的键或“有序集合(TreeSet)”中的元素,而不需要指定比较器。
CharSequence
说明String是一个可读的
字符
数组,这个接口为不同的字符数组
操作提供了统一可读的接口。charAt
、length
、subSequence
、toString
value[]
再看看String类中最重要的属性,我们可以看到这个属性其实也是被final修饰,说明value[]一旦被初始化了,就不会太修改了。请注意,final修饰对象,说明对象的内存地址是不能改变,但是对象中的属性是可变的。也就是说,我们在对
value[]
初始化之后就不能再new
一次了(分配地址)。我们可以直接把value[]直接当成String的值,我们对String的任何操作其实都是对value[]的操作。就比如说下面的代码。
说了这么多,说好是看源码,结果现在连源码都没有看到。那话不多说,还是上菜吧。
再多说一句,我们是说几个我们在工作用常用到方法的源码。
常用方法源码
hashCode
其实这个我们在平常工作中用得很少,或者说根本都没有用,但是他的作用确实巨大的。最重要的一个做法就是来判断两个String是否相等。之前有过一篇写HashMap一些概念的文章,详细说了说Hash的概念。需要详细了解Hash的概念可以参考这篇文章。
equals
说了
hashCode
当然要说说equals
,谁叫它俩是一对呢。先来说说要实现一个高效的
equals
的原则。==
操作符检查「参数是否为这个对象的引用」,也就是说两个对象是否相等,如果是直接返回true。这一步是为了性能的考虑。instanceof
操作符检查「参数是否为正确的类型」,如果不是直接返回false。instanceof
检查,会一直正确。关键域
,检查每个关键域是否相等,如果有一个关键域不相等,则返回false。否则返回true。通过上面实现高效equals的原则,我们来看
String#equals
就很简单的。substring
substring的作用是在当前的字符串的基础上截取一个字符串。请注意,调用该方法必须要接收返回值,不然也没有用呀。
substring有两个重载函数:
substring(int beginIndex)
和substring(int beginIndex, int endIndex)
。两个方法的原理都是一样的,都是调用String的构造函数新建一个String返回。用法
replace和replaceAll
replace有两个重载方法:
replace(char oldChar, char newChar)
和String replace(CharSequence target, CharSequence replacement)
replaceAll只有一个方法:
String replaceAll(String regex, String replacement)
replace(char oldChar, char newChar)
这个方法其实比较简单。也就是遍历String中的value[]每个值,找到相同的,替换掉。
replace(CharSequence target, CharSequence replacement) 和replaceAll(String regex, String replacement)
为什么要放到一起讲,大家其实可以从源码都可以看出来了。
看了两个两个源码,我们可以看出来什么?
那就是repace(char, char)比replace(CharSequence, CharSequence)和replaceAll(String, String)的性能普遍要好,因为后面两个用了正则,而第一个只是遍历替换而已。
startWith和endsWith
其实不管是startWith和endsWith最终的都是调用下面的方法:
上面算是我理解的关于
String
的源码,其实String还有很多可以探究的地方,一篇文章太短,无法全部说的明明白白。学无止境,其他的还需要继续摸索。