小瓜牛漫谈 — String

 

String 类在 Java 中代表字符串。Java 程序中的所有字符串字面值(如 "abc" )都作为此类的实例实现。

1 public static void main(String[] args) {
2     
3     String str1 = "abc";
4     String str2 = new String("cde");
5     System.out.println(str1);
6     System.out.println(str2);
7 }

在上面代码中, 第 4 行实际上创建了两个 String 对象, 一个是 "cde" 本身, 另外一个则是由 new 关键字为对象申请开辟的内存空间。

[ 可结合文章最下面给出的第 10 条来理解 ]

通常, 使用 String(String original) 构造器来创建 String 对象要比直接使用字符串字面值的开销更加的大。

 

String 字符串是常量, 它们的值在创建之后不能够被更改:

 1 package net.yeah.fancydeepin.string;
 2 
 3 public class Application {
 4 
 5     public static void main(String[] args) {
 6         
 7         String str = "abc";
 8         str += "cde";
 9         System.out.println(str);
10     }
11 }

当程序运行时, JVM 内存中的分配看起来应该像:

从上面的图来看, str 最初引用的是 "abc" 对象, 最终打印输出的结果是 abccde, 这并不是说 str 所引用的对象的内容发生了变化,

而是 str 在执行的过程中重新引用了另外的一个 String 对象 "abccde"。

 

可以使用 java 自带的反编译工具 javap 来查看编译后的字节码文件信息: javap -c Appliaction

从上面的图来看:

第 0 行, 将常量池中的 "abc" 对象压栈;

第 8 行, 调 String.valueOf(Object obj) [ 实际上是将 str 转成了 String 对象 ];

第 3、11 行, 是在创建 StringBuilder 对象, 通过 StringBuilder(String str) 构造器 [ 参数是第 8 行的 String 对象 ];

第 14 行, 将常量池中的 "cde" 对象压栈;

第 16 行, 调 StringBuilder 的 append 方法 [ 将 "cde" 拼在 "abc" 的后面 ];

第 19 行, 调 StringBuilder 的 toString() 方法。

 

使用 jad 工具, 可以更加容易的去读懂编译后的字节码文件内容: jad -o -a -s .java Application.class

结合上面的图可以看出, 在 java 中, 通过使用 "+" 符号来串联字符串的时候, 实际上底层会转成通过 StringBuilder 实例的 append() 方法来实现。

[ 关于对 String 类使用 "+" 符号来串联字符串, 在文章最下面的第 10 条继续来补充。 ]

 

String 类常用方法:

 

1> startsWith(String prefix)、endsWith(String suffix)

startsWith(prefix) 测试字符串是否是以指定的前缀 prefix 开始, endsWith(suffix) 测试字符串是否是以指定的后缀 suffix 结束:

1 public static void main(String[] args) {
2     
3     String url = "/small-snail/archive/20130421.html";
4     System.out.println(url.startsWith("/small-snail/archive/")); //true
5     System.out.println(url.endsWith(".html")); //true
6     System.out.println(url.startsWith("/small-snail/category/")); //false
7     System.out.println(url.endsWith(".php")); //false
8 }

 

2> equals(Object anObject)

在 java 中, Object 是一个顶级类, 所有类都直接或间接或默认的继承了该类。

Object 类有一个 equals(Object obj) 方法, 因此, 所有类都默认的拥有了这个方法。

但 Object 的 equals(obj) 方法默认比较的是两个引用变量所引用的对象是否相同, 只有当两个引用变量引用了相同的一个对象的时候才会返回 true。

String 类重写了 Object 类的此方法, String 类的 equals 方法比较的是两个 String 对象的内容是否相同。 

 1 package net.yeah.fancydeepin.string;
 2 
 3 public class Application {
 4 
 5     public static void main(String[] args) {
 6 
 7         String kitty1 = new String("HelloKitty");
 8         String kitty2 = new String("HelloKitty");
 9         StringBuilder kitty3 = new StringBuilder("HelloKitty");
10         System.out.println(kitty1.equals(kitty2));  //true
11         System.out.println(kitty1.equals(kitty3));  //false
12     }
13 }

第 11 行, 虽然 kitty3 的内容与 kitty1 的内容一样, 但由于 kitty3 不是一个 String 对象, 因此调 equals 方法的返回值为 false。

 

下面附上 String 类的 equals 方法的源码:

 

3> equalsIgnoreCase(String anotherString)

比较两个 String 对象的内容是否相同, 忽略大小写。

1 public static void main(String[] args) {
2      
3     String param1 = "helloKitty";
4     String param2 = "HelloKitty";
5     System.out.println(param1.equals(param2));  //false
6     System.out.println(param1.equalsIgnoreCase(param2));  //true 
7 }

 

4> getBytes(String charsetName)、String(byte[] bytes)

getBytes(String charsetName) 是使用指定的字符集 charset 将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。

 1 package net.yeah.fancydeepin.string;
 2 
 3 import java.io.UnsupportedEncodingException;
 4 
 5 public class Application {
 6 
 7     public static void main(String[] args) throws UnsupportedEncodingException {
 8 
 9         String param = "哈喽Kitty";
10         String charset;
11         
12         charset = new String(param.getBytes("GBK")); //乱码
13         System.out.println(charset);
14         
15         charset = new String(param.getBytes("GB2312")); //乱码
16         System.out.println(charset);
17         
18         charset = new String(param.getBytes("ISO-8859-1")); //乱码
19         System.out.println(charset);
20         
21         charset = new String(param.getBytes("UTF-8")); //正常
22         System.out.println(charset);
23         
24         charset = new String(param.getBytes("UTF-16")); //乱码
25         System.out.println(charset);
26     }
27 }

上面给出来的是 java 开发过程中比较经常遇到的字符集编码。当一个字符串中含有中文字符的时候, 如果该字符串在编码和解码前后所使用的字符编码不一致,

就会导致中文乱码的问题。

由于我的 eclipse SDK 工作区所使用的是 UTF-8 编码, 所以上面只有 "UTF-8" 字符编码输出的内容是正常的, 其他情况就会出现中文乱码的问题。


5> getBytes(String charsetName)、String(byte[] bytes, String charsetName)

上面示例中出现了中文乱码问题, 其实现在网络上关于中文乱码这点事儿, 资料已经是非常的多了。下面接下来将首先模拟出一个中文乱码的问题, 然后来解决它:

 1 package net.yeah.fancydeepin.string;
 2 
 3 import java.io.UnsupportedEncodingException;
 4 
 5 public class Application {
 6 
 7     public static void main(String[] args) throws UnsupportedEncodingException {
 8 
 9         String param = "哈喽Kitty";
10         
11         param = new String(param.getBytes("UTF-8"), "ISO-8859-1"); //中文乱码
12         System.out.println(param);
13         
14         //解决中文乱码
15         param = new String(param.getBytes("ISO-8859-1"), "UTF-8"); //恢复正常
16         System.out.println(param);
17     }
18 }

param = new String(param.getBytes("UTF-8"), "ISO-8859-1"); 意思是说:

将 param 以 UTF-8 编码方式去编码, 然后再按 ISO-8859-1 编码方式去解码编码后的内容, 来构造一个新的 String 对象 param。

由于 param 原本是按 UTF-8 编码方式编码出来的, 现在却使用 ISO-8859-1 编码方式去解码, 这个时候出现中文乱码是很正常的事情。

再者, ISO-8859-1 的编码方式本身是不支持中文的。

解决中文乱码问题, 无非就是使用正确的字符集编码去解码字符串的内容:

param = new String(param.getBytes("ISO-8859-1"), "UTF-8");

首先是将 param 以 ISO-8859-1 的编码方式编码出来, 因为在 java 的 JVM 中, 任何 String 都是一个 unicode 字符串,

接着再使用 UTF-8 去解码, 这个时候的中文就不再是乱码啦。。

 

为了避免引起误解, 补充说明一下, 上面不是一定要使用与 IDE 相同的编码方式 UTF-8 才不会引起中文乱码, 实际上也可以换成 GBK、GB2312 等兼容中文的

编码方式也是可以的, 只需要保证编码和解码使用的是相同的字符集编码方式即可。

 

6> indexOf(String str)、lastIndexOf(String str)、substring(int beginIndex, int endIndex)

indexOf 用于返回指定的子字符串在主字符串中第一次出现处的索引值; lastIndexOf 用于返回指定的子字符串在主字符串中最后一次出现处的索引值。

substring 则是用来切割主字符串, 根据开始索引值和结束索引值切割并返回一个新字符串。

 1 package net.yeah.fancydeepin.string;
 2 
 3 public class Application {
 4 
 5     public static void main(String[] args) {
 6          
 7         String param = "archive.logo.ico";
 8         int firstIndex = param.indexOf("a");
 9         int lastIndex = param.lastIndexOf("o");
10         int length = param.length();
11         System.out.println(firstIndex);  // 0
12         System.out.println(lastIndex);   // 15
13         System.out.println(length);      // 16
14         System.out.println(param.substring(firstIndex, lastIndex)); // archive.logo.ic
15         param = param.substring(param.lastIndexOf("."), length);    // .ico
16         System.out.println(param);
17     }
18     
19 }

从上面代码可以看出, 索引值是从 0 开始的, substring(beginIndex, endIndex) 方法切割字符串的区间其实是左闭右开: [ beginIndex, endIndex )

 

7> replaceAll(String regex, String replacement)

用子字符串 replacement 来替换主字符串中所有由正则表达式 regex 匹配的子字符串。

 1 package net.yeah.fancydeepin.string;
 2 
 3 public class Application {
 4 
 5     public static void main(String[] args) {
 6          
 7         String packageName = Application.class.getPackage().getName();
 8         
 9         String packagePath1 = packageName.replaceAll(".", "/");  //将所有的字符换成了'/'
10         System.out.println(packagePath1); // 打印 ///////////////////////////
11         
12         String packagePath2 = packageName.replaceAll("\\.", "/");  //将所有的'.'换成'/'
13         System.out.println(packagePath2);  // net/yeah/fancydeepin/string
14         
15         String packagePath3 = packageName.replaceAll("e+", "E");  //凡是出现'e'一次或以上的用'E'替换
16         System.out.println(packagePath3);  // nEt.yEah.fancydEpin.string
17     }
18     
19 }

上面代码中需要注意的是 packagePack1, replaceAll 的第一个参数使用的是正则表达式, 正则表达式中的 '.' 可以匹配除“\n”之外的任何单个字符,

因此 packagePath1 打印输出的全是反斜杠'/', 如果要匹配'.', 则应该使用转义字符, 像上面代码中的 packagePath2。

 

8> split(String regex)

根据给定的正则表达式 regex 将主字符串拆分成一个字符串数组。

 1 package net.yeah.fancydeepin.string;
 2 
 3 public class Application {
 4 
 5     public static void main(String[] args) {
 6          
 7         String param = "Java,Android,PHP,C,C++,C#";
 8         String[] languages = param.split(",");
 9         for(String language : languages){
10             System.out.println(language);
11         }
12     }
13     
14 }

 

9> trim()

忽略字符串的前导空白和尾部空白。

1 public static void main(String[] args) {
2      
3     String param = "  Hello Kitty  ";
4     System.out.println(param.trim());  //Hello Kitty
5 }

从上面示例可以看出, 调 trim() 方法只是会忽略字符串的前导空白和尾部空白, 对于串中间的空白是不会被处理的。

 

10> intern()

java 在运行期间会维护一个常量池 ( 运行时常量池, Runtime Constant Pool ), 用来存放编译期生成的各种字面量和符号引用。

首先是先来一个常量池的小例子:

1 public static void main(String[] args) {
2      
3     String param1 = "Rose";
4     String param2 = "Rose";
5     String param3 = "Ro" + "se";
6     System.out.println(param1 == param2);  //true
7     System.out.println(param1 == param3);  //true
8 }

以上示例代码中, param1、param2 的值都是字符串常量, 它们在编译期间就能够被确定了的。 

java 虚拟机在载入 class 类文件信息的时候, 会确保字符串常量在 class 文件常量池中只存在一份拷贝。

载入后的 class 类文件信息就存放在方法区(永久代)的运行时常量池当中。

param1 == param2 为 true。因为只存在一份拷贝, 实际上 param1 与 param2 引用的是同一个 String 对象 "Rose", 因此 param1 == param2。

再者, String 是 final 类, 也就是不可变类, 不可变类有一个重要的特性, 那就是可以被共享。

至于 param3, 由于 "Ro" 与 "se" 都是字符串常量, 当一个 String 对象是由多个字符串常量连接而成的时候, 那么, 它在编译期间也是可以被确定的,

因此也是一个字符串常量。下面来查看一下编译后的字节码文件信息: javap -c Appliaction

上面是示例代码反编译后的字节码指令部分截图, 字节码指令看不懂没有关系, 很明显能看到3个 "Rose", 故 param3 也是一个字符串常量,

因此 param1 == param3 也为 true。

 

java 在处理 String 对象 "+" 符号串联字符串的时候, 有一个很微小的差异, 先上代码:

1 public static void main(String[] args) {
2     
3     String param1 = "Ro";
4     String param2 = "Ro";
5     String param3 = "Ro" + "se";
6 
7     param1 += "se";
8     param2 = param2 + "se";
9 }

从整体上来看, param1、param2、param3 都是在使用 "+" 符号串联字符串, 但是 java 底层在处理方式上却存在很大的不同:

 

javap -c Appliaction

 

jad -o -a -s .java Application.class

同样是使用 "+" 符号来串联字符串, 从上面可以看出, param1 与 param2 的处理方式是一样的, 底层会转成使用 StringBuilder 的 append() 方法

来实现, 唯独 param3 在底层没有作转换。

这是因为 param3 在声明的时候赋值, 并且由于所串联的字符串是两个字符串常量, 因此 param3 在编译期也能够被确定是一个字符串常量。

param1 与 param2 相似的, 在编译期也能被确定是字符串常量, 只是在贴出来的源码的第 7 和 8 行, 重新的改变了 param1 和 param2 的引用,

而第 7 和 8 行是在编译期不能够被确定的, 只有在运行期间才能够被确定, 而在运行期, 凡是使用 "+" 符号来串联字符串, java 底层会将其转成使用

StringBuilder 的 append() 方法来实现。

 

以上已经引入了常量池的概念。但在 java 中, 并不要求常量一定只能在编译期间产生, 运行期间也可以将常量放入池中。例如 String 类的 intern() 方法:

 1 public static void main(String[] args) {
 2      
 3     String param1 = "Rose";
 4     String param2 = new String("Rose");
 5     String param3 = new String("Rose");
 6     
 7     System.out.println(param1 == param2);  //false
 8     System.out.println(param1 == param3);  //false
 9     
10     param2.intern();
11     param3 = param3.intern();
12     
13     System.out.println(param1 == param2);  //false
14     System.out.println(param1 == param3);  //true
15     System.out.println(param1 == param2.intern());  //true
16 }

当对 String 对象调用 intern() 方法时, 如果池中已经包含了一个等于此 String 对象的字符串(两个 String 对象调 equals 返回值为 true),

则返回池中的字符串对象的引用。否则, 将此 String 对象添加到池中, 并返回此 String 对象的引用。

上示代码中, 第7,8行容易理解, 因为 param1 引用的是池中的对象, param2 和 param3 引用的是堆中的两个不同的对象, 因此都为 false。

第13行, 由于第10行 param2 只是调了 intern() 方法, 并没有用 param2 来存储返回值, 因此 param2 引用的还是堆中的对象。因此为 false。

第14行, 由于第11行 param3 调了 intern(), 并使用 param3 本身来接收了返回值, 因此 param3 改变了指向, 去引用了池中的对象, 因此为 true。

第15行, 和上面类似的去分析, param1 引用的是池中的 "Rose" 对象, 而 param2.intern() 返回的也是池中的 "Rose" 对象的引用, 因此为 true。

 

 

posted on 2013-04-22 13:21  小瓜牛  阅读(2591)  评论(17编辑  收藏  举报

博客链接: http://www.blogjava.net/fancydeepin