JDK源码之String、StringBuffer、StringBuilder
就String而言,平时工作中用得最多,但是很多时候还是用不好,有必要对他进行整体的分析下。如果看过Thinking in java,再看下JDK的源码,很多东西就会变得十分明了。现在对String的底层实现进行下分析。
首先是对构造函数而言,我工作中最常用到的可能就是new String(str)这个构造函数了,所以再在此关注这一个。这个构造函数是对传进来的String进行解析,将其放进一个数组当中,我们设定为arrayOfChar,String类定义了全局变量offset(偏移量),count(大小),value(char类型的数组),此时,会将offset设为0,count设置为arrayOfChar.length,value设置为arrayOfChar,此时整个String构造器完成了初始化工作,三个全局变量全部设定了初始值,当String类型的对象调用类中的方法时,其实就是对这三个变量进行的操作,下面的方法分析中会涉及到。在String的方法中,很多方法返回的都是新的String对象,原因是因为String初始化后,大小就固定了,如下面讲解到的方法SubString、concat等方法。
常用方法分析:1、最简单的isEmpty,底层也是很简单的一行代码,return this.count == 0,清晰明了,很好的完成了相应的功能。在此,就使用了构造器中的count。
2、charAt,获取指定下标的字符。之前说过,构造器将String处理成了一个数组,此时要想得到指定下标字符,就很easy了,直接使用数组中的[n]就可以拿到了,不过它加入了偏移量,之前做了简单的异常处理,防止数组越界。
3、equals将字符串与指定对象的比较,开发中最常用的方法之一。在方法开始,就做了一个判断,如果当前对象和指定对象一样,直接返回true,这里当前对象使用的是this关键字,如果不等于,则进行接下来的操作。当指定对象不是String类型(使用instanceof判断),直接返回false,如果是String类型,则取出当前对象的数组value和指定对象的数组value1,然后根据偏移量对数组进行逐个比较,若数组相等,则返回true,反之有任何一个元素不等,当即返回false。
4、compareTo将两个字符串按照字典顺序比较。返回负数,0,正数。其实底层实现方式跟equals相似,也是使用数组实现的。但是,他的逻辑却不一样,它会逐个拿到相同下标的数组元素,然后拿到字典顺序进行比较,如果相等,继续,如果不等,直接相减并return,这是就是正数或者负数了,如果全部相等,最后返回0。
5、startWith(String,int)判断字符串是否是已制定String开始,这里分析带两个参数的,原因是因为startWith(String),endsWith(String)都是调用这个方法实现的,这也进一步展现了代码重用性带来的方便,搞笑、简洁。它的实现依旧是使用数组,将参数String与当前对象数组逐个进行比较,最后得出结果。不过,第二个参数int说明了指定String要从当前对象数组中的哪个下标进行比较。 startWith(String)实际上默认给了startWith(String,int)中int的值为0,表明从数组下标为0的开始比较。endsWith(String)实际就是从数组下标为当前对象数组的长度-指定字符数组的长度进行比较。这样一来,所以的方法最后实现都是startWith(String,int)来实现的。这一点在工作中也应该使用,即代码的重用性,最后上升到代码的简洁。代码虽短(一行到两行),但是却十分完美的实现了其功能,且清晰明了。个人觉得,方法contains也可以使用startWith(String,int)方法来实现,JDK使用indexOf实现的,不过使用startWith需要循环,效率当然会低很多的。【此处我只是举一反三,考虑效率,肯定是indexOf好多了】。
6、substring(int1,int2)截取字符串,substring(int)调用前一个,不过第二个int2设置为当前对象数组的长度。它表示从下标int1到下标int2的元素取出来,如果缺失int2,默认为数组长度,最后的实现使用的是String的一个构造函数Stirng(int,int,char[])实现的,返回一个新的String对象。在这个构造函数中,对新的String对象初始化了,包括offset,count,value,这样截取的方法就返回了一个符合要求的全新的String对象。
7、其实还有concat、replace等比较常用的方法,但是,其底层都是针对数组进行的操作,以上讲了几个典型的,这几个就不在一一讲了,可以直接看源码,很容易就理解了。只不过这两个都是返回了全新的String对象,还使用了一些其他的方法,但是思想都差不多一样。其他还有很多常用方法如getChars(char[], int),replaceAll(String, String),split都是调用了相关类的方法,就不在这里讲了。
8、在String中有个静态内部类CaseInsensitiveComparator,此处还是看下比较好。这个类中只包含了一个方法compare,这个方法是比较连个字符串的,但是忽略其大小写。方法中使用charAt取出对应的数组元素,然后Character.toUpperCase进行转换比较,为了安全,他还使用了Character.toLowerCase进行转换比较,如果相等,进行下一个比较,如果不等,直接求差返回。这个静态内部类中的方法是不能在其他类中方法进行直接访问的,而必须要通过外部类的compareToIgnoreCase方法进行访问。这样做的目的,我个人认为是防止其他类直接访问静态类中的方法,有点类似代理模式,不过这点我还有疑惑,在下面的疑惑中我会提出。这种做法在遍历器中也用到了,不过在遍历器的做法稍有不同,那里的做法是返回内部类的对象,然后利用这个对象去访问内部类中的方法。
总体来说,String我就理解了这么多,了解了底层怎么实现的,在使用String方法时也就更加得心应手了。看完了底层实现,收获很多,但是也有很多疑惑,先将疑惑列出,希望知道的IT朋友们帮助解答。
疑惑:1、方法equalsIgnoreCase的实现为return (this == paramString),为什么这样就可以忽略大小写?具体的实现方式是怎么样的?
2、刚刚提到的内部类,为什么要使用内部类,我直接将内部类的方法设置为private的,然后在相应的方法中调用,也能起到同样的效果,可能是我对内部类不了解,但是为什么要这样做?好处是什么?
3、hashCode方法给类的全局变量hash赋值了,但是在哪个地方使用到了呢?仅仅赋值而已?会不会跟String的==有关?有的话是什么关系?
续:StringBuffer、StringBuilder:
其实搞明白了String的相关方法,再回头看StringBuffer和StringBuilder,就会很easy了。其中很多方法的实现都差不多一样的,例如subString方法。之所以StringBuffer是安全的,是因为他的方法都有关键字synchronized,所以速度较慢,很多地方都推荐优先使用StringBuider。其实,看了他们的源码就知道了,他两的实现方式其实都是一样的,很多方法都是调用他们的父类方法AbstractStringBuilder的方法的。其实在这里分析AbstractStringBuilder类更为好,相当于分析了SB和SF两个类的实现。ASB类中的方法主要是insert和append,ASB类的对象不想String那样,大小后期可以动态更改的,原因是ASB维护的是数组,当数组大小不满足的时候,他会扩展数组的大小,不需要重新创建对象。所以说StringBuffer、StringBuilder的初始化以后,大小是可以动态变化的,原因就是底层维护的是数组,可以进行扩展,而不需要新的对象。当然Insert的原理也差不多一样,也是操作数组,所以大小是动态变化的。API文档中给这两个类的定义为:一个可变的字符序列。
想要弄清楚StringBuffer、StringBuilder,个人觉得弄清了String就行了,很多东西实现逻辑都一样,只是底层的实现稍微存在差别,所以导致在性能上有些不同而已。
总体的字符串就分析到这,理解了底层的实现,在日常工作中用得也就更加得心应手了。
以上纯属个人阅读JDK源码的理解,欢迎大虾们指出不对或者补充,同时对于疑惑部分肯定大家赐教,可以评论或者私信,也可以发邮件给我aaddi@sina.com,有什么好的学习方法,也可以随时交流。
欢迎各位IT朋友私信我,相互交流、学习!