Java之美[从菜鸟到高手演变]之字符串

一、String

1、String简介

初始化:

一般由String声明的字符串,长度是不可变的,这也是它与StringBuffer和StringBuilder最直观的一个区别。一般初始化方式:String s = "hello world";经过这条语句,JVM的栈内存中产生一个s变量,堆内存中产生hello world字符串对象。s指向了hello world的地址。像上面这种方式产生的字符串属于直接量字符串对象,JVM在处理这类字符串的时候,会进行缓存,产生时放入字符串池,当程序需要再次使用的时候,无需重新创建一个新的字符串,而是直接指向已存在的字符串。看下面程序:

[java] view plain copy

  1. package com.xtfggef.string;
  2. public class StringTest4 {
  3. public static void main(String[] args) {
  4. String s = "hello world";
  5. String s2 = "hello world";
  6. System.out.println(s == s2);
  7. }
  8. }

该程序输出:true 因为s和s2都指向了hello world字符串,他们的地址是同一个。 我们常说,String的一个很大的特点,就是它是一个“不可变的字符串”,就是说,当一个String对象完成创建后,该对象的内容就固定下来了,但是为什么还会有下面这种情况呢?

[java] view plain copy

  1. package com.xtfggef.string;
  2. public class StringInit {
  3. public static void main(String[] args) {
  4. String str = "I like";//---------1--------
  5. System.out.println(System.identityHashCode(str));
  6. str = str + "java";//--------2---------
  7. System.out.println(System.identityHashCode(str));
  8. }
  9. }

该程序输出:

14576877
12677476

说明:str似乎是变了,这是为什么呢?其实是这样的:str只是一个引用变量,当程序执行完1后,str指向“I like”。当程序执行完2之后,连接运算符会将两个字符串连在一起,并且让str指向新的串:"I like java",所以,从这里应该可以看得出来,最初的对象确实没有改变,只是str所指向的对象在不断改变。

String对象的另一种初始化方式,就是采用String类提供的构造方法进行初始化。String类提供了16种构造方法,常用的有五种:

String() --------- 初始化一个String对象,表示一个空字符序列

String(String value) --------- 利用一个直接量创建一个新串

String(char[] value) --------- 利用一个字符数组创建

String(char[] value,int offset,int count) --------- 截取字符数组,从offset开始count个字符创建

String(StringBuffer buffer) --------- 利用StringBuffer创建

形如:

String s = new String();

String s1 = new String(“hello”);

char[] c = {‘h‘,‘e‘,‘l‘,‘l‘,‘o‘};

String s2 = new String(c);

‘String s3 = new String(c,1,3);

以上就是String类的基本初始化方法。

2、String类的一些常用方法

字符串是最常用的对象,所以,我们有必要彻底的了解下它,下面我会列举常用的字符串里的方法,因为有很多,就不一一列举。

-------public int length()--------

该方法用于获取字符串的长度,实现如下:

[java] view plain copy

  1. /**
  2. * Returns the length of this string.
  3. * The length is equal to the number of <a href="Character.html#unicode">Unicode
  4. * code units</a> in the string.
  5. *
  6. * @return  the length of the sequence of characters represented by this
  7. *          object.
  8. */
  9. public int length() {
  10. return count;
  11. }

这是JDK种的原始实现,count在String类里被定义为一个整型常量:private final int count;并且不论采用哪种构造方法,最终都会为count赋值。

使用方法:

[java] view plain copy

  1. String s = "hello world";
  2. int length = s.length();

这个比较简单。

-----------public boolean equals(Object anObject)-----------

该方法用于比较给定对象是否与String相等。

JDK里是这样实现的:

[java] view plain copy

  1. /**
  2. * Compares this string to the specified object.  The result is {@code
  3. * true} if and only if the argument is not {@code null} and is a {@code
  4. * String} object that represents the same sequence of characters as this
  5. * object.
  6. *
  7. * @param  anObject
  8. *         The object to compare this {@code String} against
  9. *
  10. * @return  {@code true} if the given object represents a {@code String}
  11. *          equivalent to this string, {@code false} otherwise
  12. *
  13. * @see  #compareTo(String)
  14. * @see  #equalsIgnoreCase(String)
  15. */
  16. public boolean equals(Object anObject) {
  17. if (this == anObject) {
  18. return true;
  19. }
  20. if (anObject instanceof String) {
  21. String anotherString = (String)anObject;
  22. int n = count;
  23. if (n == anotherString.count) {
  24. char v1[] = value;  //---------1---------
  25. char v2[] = anotherString.value;//-------2----------
  26. int i = offset;
  27. int j = anotherString.offset;
  28. while (n-- != 0) {
  29. if (v1[i++] != v2[j++])
  30. return false;
  31. }
  32. return true;
  33. }
  34. }
  35. return false;
  36. }

从1和2处也看出来,String的底层是基于字符数组的。我们可以像下面这种方式使用equals():

[java] view plain copy

  1. String s1 = new String("hello world");
  2. String s2 = new String("hello world");
  3. String s3 = new String("hello");
  4. System.out.println(s1.equals(s2));;
  5. System.out.println(s1.equals(s3));

结果输出:

true

false

此处插入一个很重要的知识点,重写equals()的一般步骤及注意事项:

1. 使用==操作符检查“实参是否为指向对象的一个引用”。
2. 使用instanceof操作符检查“实参是否为正确的类型”。 
3. 把实参转换到正确的类型。 
4. 对于该类中每一个“关键”域,检查实参中的域与当前对象中对应的域值是否匹配。

a.对于既不是float也不是double类型的基本类型的域,可以使用==操作符进行比较

b.对于对象引用类型的域,可以递归地调用所引用的对象的equals方法 
   c.对于float类型的域,先使用Float.floatToIntBits转换成int类型的值,然后使用==操作符比较int类型的值

d.对于double类型的域,先使用Double.doubleToLongBits转换成long类型的值,然后使用==操作符比较long类型的值。
5. 当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的?(其他两个特性通常会自行满足)    如果答案是否定的,那么请找到这些特性未能满足的原因,再修改equals方法的代码。

稍后我再做说明,请先再看个例子:

[java] view plain copy

  1. package com.xtfggef.string;
  2. /**
  3. * 字符串比较:equals()和==的区别
  4. * @author 二青
  5. *
  6. */
  7. public class StringInit {
  8. public static void main(String[] args) {
  9. String s = "hello world";
  10. String s1 = new String("hello world");
  11. String s2 = new String("hello world");
  12. String s3 = new String("hello");
  13. String s4 = "hello world";
  14. System.out.println(s.equals(s1));;
  15. System.out.println(s1.equals(s2));
  16. System.out.println(s1.equals(s3));
  17. System.out.println("------------------");
  18. System.out.println(s == s1);
  19. System.out.println(s == s3);
  20. System.out.println(s == s4);
  21. }
  22. }

输出:

true
true
false
------------------
false
false
true

此处验证了一个问题,就是比较方法equals()和==的区别,一句话:equals()比较的是对象的内容,也就是JVM堆内存中的内容,==比较的是地址,也就是栈内存中的内容。

如上述代码中,s、s1、s2、s4他们四个String对象的内容都是"hello world",所以,用equals()比较他们,返回的都是true。但是,当s和s1用==比较时,却返回false,因为二者在堆中开辟的地址不一样,所以,返回的肯定是false。而为什么s和s4用==比较时,返回的是true呢,因为上文中提到过,直接量的字符串会产生缓存池,所以,当声明s4的时候,编译器检测到缓存池中存在相同的字符串,所以就直接使用,只要将s4指向s所指向的字符串就行了,二者指向同一字符串,所以地址当然相等!

注意:此处隐藏着一个比较细的编程习惯,尤其是用==进行比较的时候,尽量将常量放在==的左边,因为我们有的时候,会不小心将==写成=,这样的话,如果将常量放在左边,编译器会报错,提醒你,但是,如果将变量放在左边,常量放右边,即使你写成了=,编译器默认为变量赋值了,因此也不会报错。

因为String类实现了public interface Comparable<T>,而Comparable接口里有唯一的方法:public int compareTo(T o)。所以,String类还有另一个字符串比较方法:compareTo()

-----------------public int compareTo(String anotherString)---------------

compareTo()可实现比较两个字符串的大小,源码如下:

[java] view plain copy

  1. public int compareTo(String anotherString) {
  2. int len1 = count;
  3. int len2 = anotherString.count;
  4. int n = Math.min(len1, len2);
  5. char v1[] = value;
  6. char v2[] = anotherString.value;
  7. int i = offset;
  8. int j = anotherString.offset;
  9. if (i == j) {
  10. int k = i;
  11. int lim = n + i;
  12. while (k < lim) {
  13. char c1 = v1[k];
  14. char c2 = v2[k];
  15. if (c1 != c2) {
  16. return c1 - c2;
  17. }
  18. k++;
  19. }
  20. } else {
  21. while (n-- != 0) {
  22. char c1 = v1[i++];
  23. char c2 = v2[j++];
  24. if (c1 != c2) {
  25. return c1 - c2;
  26. }
  27. }
  28. }
  29. return len1 - len2;
  30. }

compareTo是怎么实现的呢?

首先,会对两个字符串左对齐,然后从左到右一次比较,如果相同,继续,如果不同,则计算不同的两个字符的ASCII值的差,返回就行了。与后面的其他字符没关系。

举个例子:

[java] view plain copy

  1. package com.xtfggef.string;
  2. /**
  3. * compareTo()测试
  4. * @author 二青
  5. *
  6. */
  7. public class CompareToTest {
  8. public static void main(String[] args) {
  9. String s = "hallo";
  10. String s2 = "ha";
  11. String s3 = "haeeo";
  12. int a = s.compareTo(s2);
  13. System.out.println("a:"+a);
  14. int b = s.compareTo(s3);
  15. System.out.println("b:"+b);
  16. int c = s2.compareTo(s3);
  17. System.out.println("c:"+c);
  18. }
  19. }

程序输出:

a:3
b:7
c:-3
s和s2相比,前两个相同,如果是这种情况,则直接返回length1-length2

s和s3相比,前两个相同,不用管,直接用第三个字符的ASCII码做差就行了。所以‘l‘-‘a‘=7

此处网友“handsomeman_wei”问我源码中的c1-c2理解不了,就是上面红字部分的解释。

s2和s3相比,同第一种情况一样,只是length1比length2小,因此值为负数。

-----------public char charAt(int index)-----------

获取指定位置的字符,比较容易理解,源码为:

[java] view plain copy

  1. public char charAt(int index) {
  2. if ((index < 0) || (index >= count)) {
  3. throw new StringIndexOutOfBoundsException(index);
  4. }
  5. return value[index + offset];
  6. }

String s = "hallo";
char a = s.charAt(2);
System.out.println(a);
输出:l

注意:参数index的值从0到字符串的长度-1,所以,如果值不在这个范围内,如下:

String s = "hallo";
char a = s.charAt(8);
System.out.println(a);

则报错:

Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 8

at java.lang.String.charAt(String.java:686)
at com.xtfggef.string.CompareToTest.main(CompareToTest.java:20)

与charAt()相对应的是indexOf():根据给定的字符串,返回他的位置。

indexOf()有多个参数:

public int indexOf(int ch)

public int indexOf(int ch, int fromIndex)

public int indexOf(String str)

public int indexOf(String str, int fromIndex)

static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex)

有兴趣的自己去试试,这儿就不多阐述了。

-----------substring()------------

[java] view plain copy

  1. public String substring(int beginIndex) {
  2. return substring(beginIndex, count);
  3. }

用于截取字符串,此处与另一个方法对比:

[java] view plain copy

  1. public String substring(int beginIndex, int endIndex) {
  2. if (beginIndex < 0) {
  3. throw new StringIndexOutOfBoundsException(beginIndex);
  4. }
  5. if (endIndex > count) {
  6. throw new StringIndexOutOfBoundsException(endIndex);
  7. }
  8. if (beginIndex > endIndex) {
  9. throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
  10. }
  11. return ((beginIndex == 0) && (endIndex == count)) ? this :
  12. new String(offset + beginIndex, endIndex - beginIndex, value);
  13. }

前者调用后者来实现,前者截取从指定位置到字符串结束的子字符串,后者截取从指定位置开始,到endIndex-1位置的子字符串。

[java] view plain copy

  1. public class CompareToTest {
  2. public static void main(String[] args) {
  3. String s = "helloworld";
  4. String s1 = s.substring(2);
  5. String s2 = s.substring(2, 7);
  6. String s3 = (String) s.subSequence(2, 7);
  7. System.out.print("s1:"+s1+"\n"+"s2:"+s2+"\n"+"s3:"+s3);
  8. }
  9. }

输出:

s1:lloworld
s2:llowo
s3:llowo

细心的读者应该看出来,该类里面包含一个subSequence(),而且该方法与substring(int,int)返回的结果一样,观察下源码,不难发现的区别:

[java] view plain copy

  1. public CharSequence subSequence(int beginIndex, int endIndex) {
  2. return this.substring(beginIndex, endIndex);
  3. }
  4. }

其实subSequence()内部就是调用的substring(beginIndex, endIndex),只是返回值不同。

subString返回的是String,subSequence返回的是实现了CharSequence接口的类,也就是说使用subSequence得到的结果,只能使用CharSequence接口中的方法。不过在String类中已经重写了subSequence,调用subSequence方法,可以直接转为String对象,如我们例子中的做法。

-----------------public String replace(char oldChar, char newChar)和public String replaceAll(String regex, String replacement)-------------------

[java] view plain copy

  1. public String replace(char oldChar, char newChar) {
  2. if (oldChar != newChar) {
  3. int len = count;
  4. int i = -1;
  5. char[] val = value; /* avoid getfield opcode */
  6. int off = offset;   /* avoid getfield opcode */
  7. while (++i < len) {
  8. if (val[off + i] == oldChar) {
  9. break;
  10. }
  11. }
  12. if (i < len) {
  13. char buf[] = new char[len];
  14. for (int j = 0 ; j < i ; j++) {
  15. buf[j] = val[off+j];
  16. }
  17. while (i < len) {
  18. char c = val[off + i];
  19. buf[i] = (c == oldChar) ? newChar : c;
  20. i++;
  21. }
  22. return new String(0, len, buf);
  23. }
  24. }
  25. return this;
  26. }

[java] view plain copy

  1. public String replaceAll(String regex, String replacement) {
  2. return Pattern.compile(regex).matcher(this).replaceAll(replacement);
  3. }

前者参数为两个字符串,用newChar替换原串里的所有oldChar。

后者从第一个参数可以看出,需要替换的东西可以用正则表达式描述。例子如下:

[java] view plain copy

  1. package com.xtfggef.string;
  2. public class ReplaceTest {
  3. public static void main(String[] args) {
  4. String s = "hello world";
  5. String s1 = s.replace("l", "d");
  6. System.out.println(s1);
  7. String s2 = "a78e5opx587";
  8. String s3 = s2.replaceAll("[0-9]", "");//用空串替换原串里所有的0-9的数字
  9. System.out.println(s3);
  10. }
  11. }

输出:

heddo wordd
aeopx

-------------public String[] split(String regex)-----------

该方法用于分割字符串,得到一个String类型的数组,根据regex可知,参数是个正则表达式。请看下面的例子:

[java] view plain copy

  1. package com.xtfggef.string;
  2. public class SpiltTest {
  3. public static void main(String[] args) {
  4. String s = "hello world";
  5. String s1 = "hello.worldd";
  6. String[] s2 = s.split(" ");
  7. String[] s3 = s1.split("\\.");
  8. for(int i=0; i<s2.length; i++){
  9. System.out.print(s2[i]+" ");
  10. }
  11. System.out.println();
  12. for(int j=0; j<s3.length; j++){
  13. System.out.print(s3[j]+" ");
  14. }
  15. }
  16. }

输出:

hello world 
hello worldd
关于spilt()的其他重载方法,可参见JDK的String类的实现。

spilt()需要注意的事项,就是当分隔符为 . 的话,处理起来不一样,必须写成\\.因为.是正则表达式里的一个特殊符号,必须进行转义

--------------------public native String intern();--------------------(补充知识点:经网友java2000_wl提醒,特此补充,欢迎广大读者及时提出建议,我必将虚心接受!)

intern()方法和前面说的equals()方法关系密切,从public native String intern()看出,它是Java的本地方法,我们先来看看Java文档里的描述:

[java] view plain copy

  1. Returns a canonical representation for the string object.
  2. A pool of strings, initially empty, is maintained privately by the
  3. class String.When the intern method is invoked, if the pool already contains a
  4. string equal to this String object as determined by
  5. theequals(Object) method, then the string from the pool is
  6. returned. Otherwise, this String object is added to the
  7. pool and a reference to this String object is returned.
  8. It follows that for any two strings s and t,
  9. s.intern()==t.intern() is true if and only if s.equals(t) is true.
  10. All literal strings and string-valued constant expressions are interned.
  11. @return  a string that has the same contents as this string, but is
  12. guaranteed to be from a pool of unique strings.

意思就是说,返回字符串一个规范的表示。进一步解释:有两个字符串s和t,s.equals(t),则s.intern()==t.intern().举个例子:

[java] view plain copy

  1. public class StringTest {
  2. public static void main(String[] args) {
  3. String s = new String("abc");
  4. String s1 = "abc";
  5. String s2 = "abc";
  6. String s3 = s.intern();
  7. System.out.println(s == s1);//false
  8. System.out.println(s == s2);//false
  9. System.out.println(s == s3);//false
  10. System.out.println(s1 == s3);//true
  11. }
  12. }

输出结果如注释所示,前两个结果前面已经说的很清楚了,现在拿最后一个说明,首先看看s3 = s.intern()这句,当调用s.intern()这句的时候,先去字符串常量池中找,是否有abc这个串,如果没有,则新增,同时返回引用,如果有,则返回已经存在的引用,此处s1和s2都指向常量池中的abc对象,所以此处是存在的,调用s.intern()后,s3和s1、s2指向同一个对象,所以s1==s3返回的是true。

intern()做到了一个很不寻常的行为:在运行期动态的在方法区创建对象,一般只有像new关键字可以在运行期在堆上面创建对象,所以此处比较特殊。属于及时编译的概念。

一般常见的字符串处理函数就这些,其它的还有很多,就不一一列举。

3、一些常见的问题,处理结果

在我们日常的开发中,总会遇到一些问题,在此我总结一下:

String s = "123" + "456"内存中产生几个字符串对象?

这是个比较有争议的问题,面试的时候,老师还挺喜欢问,论坛上大家说几个的也有,我给大家分析一下,因为我们前面有提到Java字符串的缓存机制,编译器在编译的时候会进行优化,所以在编译的过程中123和456被合成了一个字符串"123456",因此,如果缓存池中目前没有123456这个对象,那么会产生一个,即""123456",且栈中产生一个引用s指向它,如果缓存池中已经存在"123456",那么将产生0个对象,直接用s指向它。

如果spilt()函数的参数在要分割的字符串中没有怎么办?如String s = "helloworld" ,我现在调用String[] s2 = s.spilt("abc"),返回什么?

这个问题是我曾经参加红帽软件面试的时候遇到的相关题,当时懵了,像这样的题目,如果不亲自遇到过,或者看过源代码,很难准确的写出来。

做一个简单的测试,就可以看得出来:

[java] view plain copy

  1. package com.xtfggef.string;
  2. public class StringSpilt {
  3. public static void main(String[] args) {
  4. String s = "helloworld";
  5. String[] s2 = s.split("abc");
  6. for (int i = 0; i < s2.length; i++) {
  7. System.out.println(s2[i] + " " + i);
  8. }
  9. }
  10. }

输出:helloworld 0

说明当遇到源字符串中没有的字符时,会把它整个串放入到数组中。spilt()的内部实现还是挺复杂的,多层嵌套,不便于放到这儿分析。

关于字符串自动类型转换分析

首先看一下题的类型:

[java] view plain copy

  1. int i = 2;
  2. int j = 3;
  3. String s = "9";
  4. System.out.println(i+j+s);
  5. System.out.println("-----------------------");
  6. System.out.println(i+s+j);

以上运算各输出什么?不妨猜猜
59
-----------------------
293

首先i+j=5,然后5和9自然连接,这里涉及到java的自动类型转换,此处int型的直接转成String类型的。第二个依次连接,都转化为String类型的了。

补充(细节):看下面的程序:

[java] view plain copy

  1. String s = "ab";
  2. String s1 = "a";
  3. String s2 = s1 + "b";
  4. String s3 = "ab";
  5. System.out.println(s == s2);//false
  6. System.out.println(s2 == s3);//false
  7. System.out.println(s2.hashCode() == s3.hashCode());
  8. String s4 = "ad";
  9. String s5 = "a" + "d";
  10. String s6 = "ad";
  11. System.out.println(s4 == s5);//true
  12. System.out.println(s4 == s6);//true

此处主要是想说明:s1+"b"和"a"+"b"的不同,再看一段代码:

[java] view plain copy

  1. System.out.println(s1.hashCode());
  2. System.out.println(s2.hashCode());
  3. System.out.println(s3.hashCode());
  4. System.out.println(s4.hashCode());
  5. System.out.println(s5.hashCode());

输出:

97
3105
3105
3107
3107

说明s1+"b"的过程创建了新的对象,所以地址不一样了。所以用==比较的话,返回的是false。

此处继续补充:为什么s1+"b"会产生新的对象?而没有去常量池查找是否已经存在ab对象,以致于s==s2返回false。因为我们说过常量池(下文会讲常量池)是在编译期确定好的,所以如果我们的语句时String s5 = "ab"的话,这个是在编译期确定的,会去常量池查找,而此处我们的语句时s2 = s1+"b",s2的值只有在运行期才能确定,所以不会去常量池查找,也就是产生新串。再次提问:那么这里s2的值是在哪儿分配的呢?堆、JVM栈还是运行时常量池?正确回答:s2在堆上分配,因为+的内部实现是用StringBuilder来实现的。String s2 = s1+"b" 内部是这样实现的:String s2 = new StringBuilder(s1).append("b").toString();所以是在堆上来分配的

此处网友cowmich补充:调用s2.hashCode() == s3.hashCode()返回true。我解释下:

==比较的是他们的地址,s1+"b"会产生一个新的串,所以和s和s2用==比,返回false,如果用equals的话,返回肯定是true,因为equals()比较的是对象的内容(String类是这样的)。至于hashCode,是这样的:如果没有重写Object的hashCode(),那么如果对象调用equals()放回true,则这两个对象调用hashCode()后返回的整数一定相等。此处继续补充:对于Object类而言,原生的equals()方法,必须两个对象的地址和内容都一样才返回true,同时Object类原生的hashCode()是参照对象的地址和内容根据一定的算法生产的。所以原生的hashCode()只有调用equals()返回true才相等。而String类不同,String类重写了Object的equals(),放松了条件,只要对象地址或者内容相等就返回true,我们看看源码:

[java] view plain copy

  1. public boolean equals(Object anObject) {
  2. if (this == anObject) {
  3. return true;
  4. }
  5. if (anObject instanceof String) {
  6. String anotherString = (String)anObject;
  7. int n = count;
  8. if (n == anotherString.count) {
  9. char v1[] = value;
  10. char v2[] = anotherString.value;
  11. int i = offset;
  12. int j = anotherString.offset;
  13. while (n-- != 0) {
  14. if (v1[i++] != v2[j++])
  15. return false;
  16. }
  17. return true;
  18. }
  19. }
  20. return false;
  21. }

同时,String类重写了hashCode()方法,只要内容相等,则调用hashCode返回的整数值也相等,所以此处:s3和s2虽然地址不等,但是内容相等,所以会有:s2.hashCode() == s3.hashCode()返回true。但是这句话反过来讲就不一定成立了,因为毕竟hashCode()只是一种算法。继续补充:刚刚说了Object类和String类,此处补充下Integer类:Integer类,返回的哈希码就是Integer对象里所包含的那个整数的数值,例如Integer a=new Integer(50),则a.hashCode的值就是50 。由此可见,2个一样大小的Integer对象,返回的哈希码也一样。

补充:应网友  KingBoxing  的要求,我做下关于常量池、字符串常量池、运行时常量池的介绍:

常量池一般就是指字符串常量池,是用来做字符串缓存的一种机制,当我们在程序中写了形如String s = "abc"这样的语句后,JVM会在栈上为我们分配空间,存放变量s和对象”abc“,当我们再次需要abc对象时,如果我们写下:String s1 = "abc"的语句时,JVM会先去常量池中找,如果不存在,则新创建一个对象。如果存在,则直接将s1指向之前的对象”abc“,此时,如果我们用==来判断的话,返回的true。这样做的好处就是节省内存,系统响应的速度加快,(因为省去了对象的创建时间)这也是缓存系统存在的原因。常量池是针对在编译期间就确定下来的常量而言的,如上所说的String类的一些对象。但是,当类被加载后,常量池会被搬到方法区的运行时常量池,此时就不再是静态的了,那么是不是就不能向常量池中添加新的内容了呢(因为我们刚刚说过,常量池是在编译期确定好的)?答案是否定的,我们依然可以在运行时向常量池添加内容!这就是我们说过的String类有个方法叫intern(),它可以在运行时将新的常量放于常量池。因为我在上文中已经详细介绍过intern(),此处不再赘述!

个人的力量是有限的,欢迎大家积极补充,同时也欢迎读者随时批评指正!

有问题请联系:egg

邮箱:[email protected]    微博:http://weibo.com/xtfggef

You have to believe in yourself.That‘s the secretof success!

二、StringBuffer、StringBuilder

1、初始化

StringBuffer和StringBuilder就是所谓的可变字符串类,共四个构造方法:

StringBuffer()

public StringBuffer(int paramInt)

public StringBuffer(String paramString)

public StringBuffer(CharSequence paramCharSequence)

观察其源码发现,使用StringBuffer()时,默认开辟16个字符的长度的空间,使用public StringBuffer(int paramInt)时开辟指定大小的空间,使用public StringBuffer(String paramString)时,开辟paramString.length+16大小的空间。都是调用父类的构造器super()来开辟内存。这方面StringBuffer和StringBuilder都一样,且都实现AbstractStringBuilder类。

2、主要方法

二者几乎没什么区别,基本都是在调用父类的各个方法,一个重要的区别就是StringBuffer是线程安全的,内部的大多数方法前面都有关键字synchronized,这样就会有一定的性能消耗,StringBuilder是非线程安全的,所以效率要高些。

[java] view plain copy

  1. public static void main(String[] args) throws Exception {
  2. String string = "0";
  3. int n = 10000;
  4. long begin = System.currentTimeMillis();
  5. for (int i = 1; i < n; i++) {
  6. string += i;
  7. }
  8. long end = System.currentTimeMillis();
  9. long between = end - begin;
  10. System.out.println("使用String类耗时:" + between+"ms");
  11. int n1 = 10000;
  12. StringBuffer sb = new StringBuffer("0");
  13. long begin1 = System.currentTimeMillis();
  14. for (int j = 1; j < n1; j++) {
  15. sb.append(j);
  16. }
  17. long end1 = System.currentTimeMillis();
  18. long between1 = end1 - begin1;
  19. System.out.println("使用StringBuffer类耗时:" + between1+"ms");
  20. int n2 = 10000;
  21. StringBuilder sb2 = new StringBuilder("0");
  22. long begin2 = System.currentTimeMillis();
  23. for (int k = 1; k < n2; k++) {
  24. sb2.append(k);
  25. }
  26. long end2 = System.currentTimeMillis();
  27. long between2 = end2 - begin2;
  28. System.out.println("使用StringBuilder类耗时:" + between2+"ms");
  29. }

输出:

使用String类耗时:982ms
使用StringBuffer类耗时:2ms
使用StringBuilder类耗时:1ms

虽然这个数字每次执行都不一样,而且每个机子的情况也不一样,但是有几点是确定的,String类消耗的明显比另外两个多得多。还有一点就是,StringBuffer要比StringBuilder消耗的多,尽管相差不明显。

接下来介绍一些常用的方法。

-----------------------public synchronized int length()--------------------------

-------------------------public synchronized int capacity()---------------------------

二者都是获取字符串的长度,length()获取的是当前字符串的长度,capacity()获取的是当前缓冲区的大小。举个简单的例子:

[java] view plain copy

  1. StringBuffer sb = new StringBuffer();
  2. System.out.println(sb.length());;
  3. System.out.println(sb.capacity());

输出:

0

16

[java] view plain copy

  1. StringBuffer sb = new StringBuffer("hello");
  2. System.out.println(sb.length());;
  3. System.out.println(sb.capacity());

输出:

5

21

因为默认分配16个字符大小的空间,所以不难解释上面的结果。

------------------public boolean equals(Object paramObject)---------------------

[java] view plain copy

  1. StringBuffer sb = new StringBuffer("hello");
  2. StringBuffer sb2 = new StringBuffer("hello");
  3. System.out.println(sb.equals(sb2));

以上程序输出false,是不是有点惊讶?记得之前我们的文章说过,equals()比较的是字符串的内容,按理说此处应该输出的是true才对。

究其原因,String类重写了Object的equals(),所以只需要看内容是否相等即可,但是StringBuffer没有重写equals(),此处的equals()仍然是调用的Object类的,所以,调用StringBuffer类的equals(),只有地址和内容都相等的字符串,结果才会返回true。

另外StringBuffer有一系列追加、插入、删除字符串的方法,首先append(),就是在原来的字符串后面直接追加一个新的串,和String类相比有明显的好处:

String类在追加的时候,源字符串不变(这就是为什么说String是不可变的字符串类型),和新串连接后,重新开辟一个内存。这样就会造成每次连接一个新串后,都会让之前的串报废,因此也造成了不可避免的内存泄露。

[java] view plain copy

  1. //append()
  2. StringBuffer sb = new StringBuffer("helloworld, ");
  3. sb.append("I‘m ").append("erqing ").append("who ").append("are you ?");
  4. System.out.println(sb);
  5. //public synchronized StringBuffer insert(int paramInt, Object paramObject)
  6. sb.insert(12, /*9*/"nice! ");
  7. System.out.println(sb);
  8. //public synchronized StringBuffer reverse()
  9. sb.reverse();
  10. System.out.println(sb);
  11. sb.reverse();
  12. System.out.println(sb);
  13. //public synchronized StringBuffer delete(int paramInt1, int paramInt2)
  14. //public synchronized StringBuffer deleteCharAt(int paramInt)
  15. sb.delete(12, 18);
  16. System.out.println(sb);
  17. sb.deleteCharAt(5);
  18. System.out.println(sb);

输出:

helloworld, I‘m erqing who are you ?
helloworld, nice! I‘m erqing who are you ?
? uoy era ohw gniqre m‘I !ecin ,dlrowolleh
helloworld, nice! I‘m erqing who are you ?
helloworld, I‘m erqing who are you ?
helloorld, I‘m erqing who are you ?

-----------------public synchronized void trimToSize()---------------------

该方法用于将多余的缓冲区空间释放出来。

[java] view plain copy

  1. StringBuffer sb = new StringBuffer("hello erqing");
  2. System.out.println("length:"+sb.length());
  3. System.out.println("capacity:"+sb.capacity());
  4. sb.trimToSize();
  5. System.out.println("trimTosize:"+sb.capacity());

输出:

length:12
capacity:28
trimTosize:12

StringBuffer类还有很多方法,关于字符查找,截取,替换方面的方法,有兴趣的童鞋可以去研究研究源码,定会学到不少知识!

三、字符串处理类StringTokenizer

StringTokenizer是java.util包下的一个类,用来对字符串做简单的处理。

举个简单的例子:

[java] view plain copy

  1. String s = "Tonight is the answer !";
  2. StringTokenizer st = new StringTokenizer(s," ");
  3. int count = st.countTokens();
  4. System.out.println("个数为:"+count);
  5. while (st.hasMoreTokens()) {
  6. String token = st.nextToken();
  7. System.out.println(token);
  8. }

输出:

个数为:5
Tonight
is
the
answer
!

转自http://blog.csdn.net/zhangerqing/article/details/8093919

原文地址:https://www.cnblogs.com/shizhijie/p/8258639.html

时间: 2024-10-16 13:06:59

Java之美[从菜鸟到高手演变]之字符串的相关文章

Java之美[从菜鸟到高手演变]之JVM内存管理及垃圾回收

很多Java面试的时候,都会问到有关Java垃圾回收的问题,提到垃圾回收肯定要涉及到JVM内存管理机制,Java语言的执行效率一直被C.C++程序员所嘲笑,其实,事实就是这样,Java在执行效率方面确实很低,一方面,Java语言采用面向对象思想,这也决定了其必然是开发效率高,执行效率低.另一方面,Java语言对程序员做了一个美好的承诺:程序员无需去管理内存,因为JVM有垃圾回收(GC),会去自动进行垃圾回收. 其实不然: 1.垃圾回收并不会按照程序员的要求,随时进行GC. 2.垃圾回收并不会及时

Java之美[从菜鸟到高手演变]之设计模式

设计模式(Design Patterns) ——可复用面向对象软件的基础 设计模式(Design pattern)是一套被反复使用.多数人知晓的.经过分类编目的.代码设计经验的总结.使用设计模式是为了可重用代码.让代码更容易被他人理解.保证代码可靠性. 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样.项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周

Java之美[从菜鸟到高手演变]之设计模式(zz)

http://blog.csdn.net/zhangerqing/article/details/8194653 设计模式(Design pattern)是一套被反复使用.多数人知晓的.经过分类编目的.代码设计经验的总结.使用设计模式是为了可重用代码.让代码更容易被他人理解.保证代码可靠性. 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样.项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理

Java之美[从菜鸟到高手演变]之eclipse连接hadoop集群

作者:二青个人站点:zhangerqing.cn    邮箱:[email protected]    微博:http://weibo.com/xtfggef 准备工具: Ubuntu 14.10 desktop 64 bit eclipse 4.3 kepler jee version hadoop 2.6.0 hadoop eclipse plugin 2.6.0 起初我是打算用win7做实验,后来遇到一个null pointer的问题很奇怪,在网上找了很多资料都不起作用,有些问题很像,单用

Java之美[从菜鸟到高手演变]之智力题【史上最全】 (转)阻缀锥抓卒租

http://www.ebay.com/cln/739_dtfz/2015-01-30/166610796012 http://www.ebay.com/cln/75r_zflf/2015-01-30/166615324010 http://www.ebay.com/cln/d77_fznp/2015-01-30/166615334010 http://www.ebay.com/cln/j1b_rndz/2015-01-30/166586262014 http://www.ebay.com/cl

Java之美[从菜鸟到高手演变]之Spring源码学习 - 环境搭建

准备工作 1.下载安装STS(Spring Tool Suite),在eclipse market里直接搜索.下载.安装.2.下载安装gradle, Spring源码使用gradle构建,下载后解压到任意目录,设置环境变量: GRADLE_HOME且配置Path. 3.下载安装github windows版本 使用github下载Spring源代码 去官网找到Spring在github上的地址,下载到文件系统下就好了. 将Spring 源代码导入eclipse 为Spring每个模块生成ecli

Java之美[从菜鸟到高手演练]之Arrays类及其方法分析

作者:二青 个人站点:zhangerqing.cn    邮箱:[email protected]    微博:http://weibo.com/xtfggef 本章主要介绍一下 java.util.Arrays类的重点方法,包括怎么使用及实现原理.这是一个算法类,主要是辅助数组类实现一些排序.搜索等功能,同时也支持数组到List的转换.本章系Java之美[从菜鸟到高手演练]系列之Arrays类及其方法分析,如果有任何问题,欢迎通过上面任何一种方式与我联系! 排序 本文使用JDK1.8.0_25

Java之美[从菜鸟到高手演练]之JDK动态代理的实现及原理

JDK动态代理的实现及原理 作者:二青 邮箱:[email protected]     微博:http://weibo.com/xtfggef 动态代理,听上去很高大上的技术,在Java里应用广泛,尤其是在Hibernate和Spring这两种框架里,在AOP,权限控制,事务管理等方面都有动态代理的实现.JDK本身有实现动态代理技术,但是略有限制,即被代理的类必须实现某个接口,否则无法使用JDK自带的动态代理,因此,如果不满足条件,就只能使用另一种更加灵活,功能更加强大的动态代理技术-- CG

Java之美[从菜鸟到高手演练]之Linux下Hadoop的完全分布式安装

作者:二青 邮箱:[email protected]     微博:http://weibo.com/xtfggef 本来是想安装一个单节点的环境就好了,后来按装完了总觉得不够过瘾,于是今天继续研究一下,来一个完全分布式的集群安装.用到的软件和上一篇单节点安装Hadoop一样,如下: Ubuntu 14.10 64 Bit Server Edition Hadoop2.6.0 JDK 1.7.0_71 ssh rsync 准备环境 依然是VirtualBox + Ubuntu 14.10 64