Java总结篇系列:Java String-阿里云开发者社区

开发者社区> 长征6号> 正文

Java总结篇系列:Java String

简介:
+关注继续查看

String作为Java中最常用的引用类型,相对来说基本上都比较熟悉,无论在平时的编码过程中还是在笔试面试中,String都很受到青睐,然而,在使用String过程中,又有较多需要注意的细节之处。

1.String是不可变类。

这句话其实大家都很熟悉了,那么具体什么是不可变类呢?一般认为:当对象一旦创建完成后,在正常情况下,对象的状态不会因外界的改变而改变(对象的状态是指对象的属性,包括属性的类型及属性值)。

首先看一个基本的例子:

1 String s = "abc";
2 System.out.println("s:" + s);  // 输出s:abc
3 s = "def";
4 System.out.println("s:" + s);  // 输出s:def

此时,初看上去,输出的结果变了,发现s的值发生了变化,那么这与上面的说法——String类是不可变类是否矛盾呢?答案是否定的,因为s只是指向堆内存中的引用,存储的是对象在堆中的地址,而非对象本身,s本身存储在栈内存中。

实际上,此时堆内存中依然存在着"abc"和"def"对象。对于"abc"对象本身而言,对象的状态是没有发生任何变化的。

那么为什么String类具有不可变性呢,显然,既然不可变说明String类中肯定没有提供对外可setters方法。接下来来具体看一下String类的定义。

下面是String类中主要属性的定义(Java 1.7源码):

复制代码
1 public final class String implements java.io.Serializable, Comparable<String>, CharSequence{
2     
3     /** The value is used for character storage. */
4     private final char value[];
5     
6     /** Cache the hash code for the string */
7     private int hash; // Default to 0
8     
9 }
复制代码

与之前版本的Java String源码相比,String类减少了int offset 和 int count的定义。这样变化的结果主要体现在:

1.避免之前版本的String对象subString时可能引起的内存泄露问题;

2.新版本的subString时间复杂度将有O(1)变为O(n);

具体分析可见文章:

http://www.importnew.com/7656.html

http://www.importnew.com/14105.html

通过上面String类的定义,类名前面用了final class修饰,因此,String类不能被继承。对于其属性定义,可以看出,属性value[]和hash都是被定义成private类型,且由于没有提供对外的public setters方法,String类属性不可被改变。

其中,需要重点关注属性value[],其被final char修饰,因此字符型数组value只会被赋值一次就不可修改。其存储内容正好是String中的单个字符内容。

 

2.String相关的 +

String中的 + 常用于字符串的连接。此处为了说明清楚这个问题,首先可以安装Eclipse 查看字节码插件ByteCode Outline。

在线安装网址: http://zipeditor.sourceforge.net/update/ DisabledHelp >> install new software >> 输入网址 >> 选择 bytecode outline >> ... ... 安装成功。

Window >> show view >> other >> java >> ByteCode即可在Eclipse下方面板栏中查看。

看下面一个简单的例子:

复制代码
 1 class D {
 2 
 3     public static void main(String[] args) {
 4 
 5         String a = "aa";
 6         String b = "bb";
 7         String c = "xx" + "yy " + a + "zz" + "mm" + b;
 8         System.out.println(c);
 9     }
10 }
复制代码

编译运行后,点击ByteCode查看,主要字节码部分如下:

复制代码
 1 public static main([Ljava/lang/String;)V
 2    L0
 3     LINENUMBER 5 L0
 4     LDC "aa"
 5     ASTORE 1
 6    L1
 7     LINENUMBER 6 L1
 8     LDC "bb"
 9     ASTORE 2
10    L2
11     LINENUMBER 7 L2
12     NEW java/lang/StringBuilder
13     DUP
14     LDC "xxyy "
15     INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
16     ALOAD 1
17     INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
18     LDC "zz"
19     INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
20     LDC "mm"
21     INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
22     ALOAD 2
23     INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
24     INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
25     ASTORE 3
26    L3
27     LINENUMBER 8 L3
28     GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
29     ALOAD 3
30     INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
31    L4
32     LINENUMBER 9 L4
33     RETURN
34    L5
35     LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
36     LOCALVARIABLE a Ljava/lang/String; L1 L5 1
37     LOCALVARIABLE b Ljava/lang/String; L2 L5 2
38     LOCALVARIABLE c Ljava/lang/String; L3 L5 3
39     MAXSTACK = 3
40     MAXLOCALS = 4
41 }
复制代码

显然,通过字节码我们可以得出如下几点结论:

1.String中使用 + 字符串连接符进行字符串连接时,连接操作最开始时如果都是字符串常量,编译后将尽可能多的直接将字符串常量连接起来,形成新的字符串常量参与后续连接(通过反编译工具jd-gui也可以方便的直接看出);

2.接下来的字符串连接是从左向右依次进行,对于不同的字符串,首先以最左边的字符串为参数创建StringBuilder对象,然后依次对右边进行append操作,最后将StringBuilder对象通过toString()方法转换成String对象(注意:中间的多个字符串常量不会自动拼接)。

也就是说

String c = "xx" + "yy " + a + "zz" + "mm" + b; 实质上的实现过程是: String c = new StringBuilder("xxyy").append(a).append("zz").append("mm").append(b).toString();

由于得出结论:当使用+进行多个字符串连接时,实际上是产生了一个StringBuilder对象和一个String对象。

 

 3.String中的常用方法

1).与String类中value[]数组存储直接相关的有:

int length(); // 返回String长度,亦即value[]数组长度;

char charAt(int index); // 返回指定位置字符;

int indexOf(int ch, int fromIndex); //从fromIndex位置开始,查找ch字符在字符串中首次出现的位置。fromIndex默认为0,ch直接传入字符即可。如'C',区分大小写,未查找到返回-1;

char[] toCharArray() ;   // 将字符串转换成一个新的字符数组

2).与其他字符串即字串相关的方法有:

int indexOf(String str, int fromIndex) ;

与indexOf含义相反有lastIndexOf(..),反向索引。

boolean contains(String str); //实际上 contains内部实现也是调用的indexOf,然后将其结果与-1相比较。

boolean startsWith(String str); // 判断字符串是否以str开头

boolean endsWith(String str); //.....是否以str结尾

String replace(CharSequence target, CharSequence replacement) ;  // 替换

String substring(int beginIndex,  int endIndex);  //字符串截取,不传第二个参数则表示直接截取到字符串末尾

String[] split(String regex);  // 字符串分割

 

4.String中的equals()与hashCode()

String类重写了Object类的equlas方法,使得比较字符串内容是否相等可以直接使用equlas方法。关于equals以及相应的hashCode方法参见博文《Java总结篇系列:java.lang.Object 》

 

5.String字符串常量池

JVM为了提高性能和减少内存开销,内部维护了一个字符串常量池,每当创建字符串常量时,JVM首先检查字符串常量池,如果常量池中已经存在,则返回池中的字符串对象引用,否则创建该字符串对象并放入池中。

因此下述结果返回true。

1 String a = "abc";
2 String b = "abc";
3 System.out.print(a == b); //true

但与创建字符串常量方式不同的是,当使用new String(String str)方式等创建字符串对象时,不管字符串常量池中是否有与此相同内容的字符串,都会在堆内存中创建新的字符串对象。

因此,下面代码片段有如下结果。

1 String a = "Hello";
2 String b = new String("Hello");
3 System.out.println(a == b);  //false
4 System.out.println(a.equals(b)); //true

即使字符串内容相同,字符串常量池中的字符串与通过new String(..)等方式创建的字符串对象之间没有直接的关系,但是,可以通过字符串的intern()方法找到此种关联。intern()方法返回字符串对象在字符串常量池中的对象引用,若字符串常量池中尚未有此字符串,则创建一新的字符串常量放置于池中。

于是,很据如上理解,很自然的,可以得到如下结果。

复制代码
 1 String a = "Hello";
 2 System.out.println(a == a.intern()); //true
 3 
 4 String b = new String("corn");
 5 String c = b.intern();
 6 
 7 System.out.println(b == c); //false
 8 
 9 String d = "corn";
10 
11 System.out.println(c == d); //true
复制代码

 

6.String/StringBuilder/StringBuffer区别

String是不可变字符串对象,StringBuilder和StringBuffer是可变字符串对象(其内部的字符数组长度可变),StringBuffer线程安全,StringBuilder非线程安全。

 

7.既然String是不可变字符串对象,如何才能改变让其可变?

既然String对象中没有对外提供可用的public setters等方法,因此只能通过Java中的反射机制实现。因此,前文中说到的String是不可变字符串对象只是针对“正常情况下”。而非必然。

复制代码
 1 public static void stringReflection() throws Exception {
 2 
 3     String s = "Hello World";
 4 
 5     System.out.println("s = " + s); //Hello World
 6 
 7     //获取String类中的value字段
 8     Field valueField = String.class.getDeclaredField("value");
 9 
10     //改变value属性的访问权限
11     valueField.setAccessible(true);
12 
13     char[] value = (char[]) valueField.get(s);
14 
15     //改变value所引用的数组中的第5个字符
16     value[5] = '_';
17 
18     System.out.println("s = " + s); //Hello_World
19 }
复制代码

由此可见Java中反射的强大之处。

 

---------------------------------------------------------------------------------
笔者水平有限,若有错漏,欢迎指正,如果转载以及CV操作,请务必注明出处,谢谢!
分类: Java

本文转自Windstep博客园博客,原文链接:http://www.cnblogs.com/lwbqqyumidi/p/4060845.html,如需转载请自行联系原作者

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Class文件结构介绍[字段表集合和方法表集合]
字段表(field_info)用来描述接口或类中声明的变量,字段包括类级别变量以及实例级别变量。但不包括方法内部声明的局部变量。以如下代码来分析
1 0
Class文件结构介绍[访问标志,类索引,父类索引,接口索引集合]
在常量池结束后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public类型,是否定义为abstract类型,如果是类的话是否被声明为final等,
5 0
Java的内存模型
java的后端服务器开发中"高效并发"是我们经常会碰到的,而要写出高效的代码需要更多的积累与实践。而一些基础的内容是往这个方向发展的基石。所以我们就来介绍下
5 0
【2020 Java基础快速学习路线】写了很久,这是一份最适合普通大众、非科班的路线
【2020 Java基础快速学习路线】写了很久,这是一份最适合普通大众、非科班的路线
4 0
教妹学Java(八):初识Java变量
教妹学Java(八):初识Java变量
4 0
云迁移SaaS如何入驻阿里云工具应用市场
HyperMotion SaaS是一款基于云原生理念开发的云迁移和云灾备的SaaS平台,2020年7月,HyperMotion迁移版本正式入驻阿里云工具应用市场。用户登陆阿里云后,可以直接以SaaS模式使用产品,而无须再到云市场启动实例的方式。由于与阿里云的用户体系、RAM系统、支付系统彻底打通,用户在使用感受上更加便捷。
6 0
想学习,无方法,十年老码农告诉你方法
想学习,无方法,十年老码农告诉你方法
5 0
volatile关键字
volatile关键字是我们经常在面试过程中碰到的一个问题,本文来介绍下这个关键字 原则性,可见性,有序性
7 0
别翻了,Lambda 表达式入门,看这篇就够了(1)
别翻了,Lambda 表达式入门,看这篇就够了
4 0
JVM中的类加载器
把类加载阶段中的"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为类加载器。
4 0
+关注
785
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载