Java系列之字符串
来源:互联网 发布:node sass下载失败 编辑:程序博客网 时间:2024/06/06 14:01
显而易见,字符串操作是程序设计过程中最常见的行为,本文,将深入学习Java中应用最广泛的String类及其相关的类及工具。
一、String
1、String不可变
String对象是不可变的,在JDK中是这样定义的:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { ...}
String是通过final来修饰的,其实,String类中每一个看起来会修改String值的方法,都是创建了一个全新的String对象,用以包含修改后的字符串内容,而最初的String对象却没有做任何改动。
先看一个例子:
public class StringDemo { public static void main(String[] args) { String s1 = "Hello World"; System.out.println(s1); s1 = "Hello Java"; System.out.println(s1); }}
输出结果:
Hello WorldHello Java
咦,String对象s1的值看上去发生了变化,那么与我们上面提到的–String类是不可变的是否矛盾呢?
答案是否定的。这需要从内存与堆说起,因为s1只是指向堆内存中的引用,存储的是对象在堆中的地址,而非对象本身,s本身存储在栈内存中。
接下来,通过String类中的toUpperCase()方法来证明,
测试用例:
public class StringDemo { public static void main(String[] args) { String s1 = "Hello World"; System.out.println(s1); String s2 = upCase(s1); System.out.println(s2); System.out.println(s1); } public static String upCase(String str) { return str.toUpperCase(); }}
输出结果:
Hello WorldHELLO WORLDHello World
String对象s1调用了String类的toUpperCase方法,将字符串中的每个字符都变成了大写,同时赋值给s2,但是s1本身却没有做任何更改,还是原来的值。
由此,我们可以得出结论,String对象是不可变的。
2、重载”+”与StringBuilder
操作符”+”可以用来连接String,
测试用例:
public class StringDemo { public static void main(String[] args) { String mqq = "mqq"; String s = mqq + "is a " + "doll" + 8; System.out.println(s); }}
输出结果:
mqqis a doll8
那么第4行代码是怎么工作的呢?
别着急,为了说清楚这个问题,需要先在Eclipse中安装一个查看字节码的插件 ByteCode Outline,
安装方法:Help -> Install new Software… -> Work with->Add,在Location中填入URL:http://andrei.gmxhome.de/eclipse/,选择 ByteCode Outline安装
安装完后并不是马上就有,需要手动打开,Eclipse菜单Windows -> Show View -> Other,然后在Java类里找到Bytecode并添加到下方的Tab中。
安装好后,再运行程序,点开Bytecode查看,主要字节码如下:
public static main([Ljava/lang/String;)V L0 LINENUMBER 10 L0 LDC "mqq" ASTORE 1 L1 LINENUMBER 11 L1 NEW java/lang/StringBuilder DUP ALOAD 1 INVOKESTATIC java/lang/String.valueOf(Ljava/lang/Object;)Ljava/lang/String; INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V LDC "is a " INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder; LDC "doll" INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder; BIPUSH 8 INVOKEVIRTUAL java/lang/StringBuilder.append(I)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String; ASTORE 2 L2 LINENUMBER 12 L2 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; ALOAD 2 INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V L3 LINENUMBER 13 L3 RETURN
通过分析字节码,我们可以得出以下结论:
Java程序在通过”+”连接字符串时,会自动引入java.lang.StringBuilder类,并且通过StringBuilder的appen()方法从左到右将每个子串拼接起来,最终调用toString()方法将其转换成String对象。
那么,我们是不是可以随意使用String呢?反正编译器都会自动为我们优化性能,接下来,再看一个例子:
public class StringDemo { public static void main(String[] args) { String[] s = {"banana", "apple", "lemen", "orange", "grape"}; useString(s); useStringBuilder(s); } public static void useStringBuilder(String[] s) { StringBuilder result = new StringBuilder(); for (int i = 0; i < s.length; i++) { result.append(s[i] + "\n"); } System.out.println(result.toString()); } public static void useString(String[] s) { String result = ""; for (int i = 0; i < s.length; i++) { result += s[i]; } System.out.println(result); }}
运行一下这段代码,可以看到两个方法对应的字节码,先看看useString()方法:
1 public static useString([Ljava/lang/String;)V 2 L0 3 LINENUMBER 19 L0 4 LDC "" 5 ASTORE 1 6 L1 7 LINENUMBER 20 L1 8 ICONST_0 9 ISTORE 210 L211 GOTO L312 L413 LINENUMBER 21 L414 FRAME APPEND [java/lang/String I]15 NEW java/lang/StringBuilder16 DUP17 ALOAD 118 INVOKESTATIC java/lang/String.valueOf(Ljava/lang/Object;)Ljava/lang/String;19 INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V20 ALOAD 021 ILOAD 222 AALOAD23 INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;24 INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String;25 ASTORE 126 L527 LINENUMBER 20 L528 IINC 2 129 L330 FRAME SAME31 ILOAD 232 ALOAD 033 ARRAYLENGTH34 IF_ICMPLT L435 L636 LINENUMBER 23 L637 GETSTATIC java/lang/System.out : Ljava/io/PrintStream;38 ALOAD 139 INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V40 L741 LINENUMBER 24 L742 RETURN
其中11-34是一个循环体,注意重点是StringBuilder对象是在循环体内构造的,意味着没循环一次,就会创建一个StringBuilder对象。
接下来是useStringBuilder方法对应的字节码:
1 public static useStringBuilder([Ljava/lang/String;)V 2 L0 3 LINENUMBER 12 L0 4 NEW java/lang/StringBuilder 5 DUP 6 INVOKESPECIAL java/lang/StringBuilder.<init>()V 7 ASTORE 1 8 L1 9 LINENUMBER 13 L110 ICONST_011 ISTORE 212 L213 GOTO L314 L415 LINENUMBER 14 L416 FRAME APPEND [java/lang/StringBuilder I]17 ALOAD 118 ALOAD 019 ILOAD 220 AALOAD21 INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;22 POP23 L524 LINENUMBER 13 L525 IINC 2 126 L327 FRAME SAME28 ILOAD 229 ALOAD 030 ARRAYLENGTH31 IF_ICMPLT L432 L633 LINENUMBER 16 L634 GETSTATIC java/lang/System.out : Ljava/io/PrintStream;35 ALOAD 136 INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String;37 INVOKEVIRTUAL java/io/PrintStream.println(Ljava/lang/String;)V38 L739 LINENUMBER 17 L740 RETURN
可以看到,不仅循环部分代码更简洁简单,而且只生成了一个StringBuilder对象。
那么,孰好孰坏,可以很直观的得出答案,因此,当我们为一个类编写toString()方法时,如果字符串的操作比较简单,那就可以信赖编译器,它会为我们合理的构造最终的字符串结果,但是,如果我们需要在toString()方法中使用循环,那么,最好自己创建一个StringBuilder对象,用它来构造最终的结果。
3.字符串常量池
JVM为了提高性能并且减少内存开销,内部维护了一个字符串常量池,当创建一个字符串常量时,JVM首先会查找常量池,若常量池中已经存在该字符串对象,则直接将该字符串对象的引用返回,否则,就创建一个新的对象放入常量池中。
但与创建字符串常量方式不同的是,当通过new String()等方式创建字符串对象时,不管内容是否相同,都会在堆内存中创建新的字符串对象,此时,需要通过equals()方法来判断字符串对象的内容是否相同
测试用例:
public class StringDemo { public static void main(String[] args) { String s1 = "abc"; String s2 = "abc"; String s3 = new String("abc"); System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s1.equals(s3)); }}
输出结果:
truefalsetrue
4.String、StringBuffer与StringBuilder的区别
性能上:StringBuilder > StringBuffer > Stirng
StringBuffer是线程安全的
StringBuilder是线程不安全的
并且,StringBuffer与StringBuilder都是可变的
5.另外
那么,一个字符串真的是永远不可变的吗?
接下来看一个例子:
public class StringDemo { public static void main(String[] args) { changeString(); } public static void changeString() { String s = "Hello World!"; Field valueField = null; try { valueField = String.class.getDeclaredField("value"); valueField.setAccessible(true); char[] value = (char[]) valueField.get(s); System.out.println(s); value[5] = '_'; System.out.println(s); } catch (Exception e) { e.printStackTrace(); } }}
输出结果:
Hello World!Hello_World!
可以看见,我们通过Java的反射机制,改变了字符串对象,不过我们一般不会通过这种情况去改变字符串,所以,正常情况下,我们认为字符串时不可变的。
参考资料:Java编程思想(第4版)
- Java系列之字符串
- java基础系列(三)之--java字符串
- 编程系列之:字符串逆序
- OJ 系列之分解字符串
- OJ 系列之字符串分割
- OJ系列之---字符串分割
- ES6系列之---模板字符串
- 字符串系列之:逆序输出字符串
- 算法中字符串系列之旋转字符串
- 数组字符串系列之,判断两个字符串互为旋转字符串
- Java系列之JNDI
- Java系列之XML
- Java系列之EJB
- Java面试宝典系列之字符串转整型、判断IP合法性、求最大公约数
- Java面试宝典系列之字符串转整型、判断IP合法性、求最大公约数
- Java程序员的Bash实用指南系列之字符串处理(目录)
- Java面试宝典系列之字符串转整型、判断IP合法性、求最大公约数
- Java 数据类型之字符串
- SSD卡的性能特性
- 使用Jetson TX2为Raspberry Pi3编译Qt5.9
- Systemtap系统诊断工具
- 过滤器
- DispatcherServlet
- Java系列之字符串
- 【剑指offer】从上往下打印二叉树
- 【深度学习框架Caffe学习与应用】第一课 Opencv安装
- 快速排序
- mysql主从复制配置
- LeetCode基础--动态规划
- zookeeper的选主机制的实现过程以及原理
- 每天一个linux命令(28):tar命令
- (4)close后续行为