深入解析String

深入理解String

前言

String类是我们开发很经常用到的一个在java.lang包的类,也是 JDK 提供的最基础的类,所以我们要深入理解Java中的String


源码分析

类的定义
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
{

从String类的定义,可以看出String类的特点:

  1. 不可继承与不可变:当 final 关键字修饰类时, 代表此类不可继承. 所以 String 类是不能被外部继承,为什么要用final修饰呢,因为怕继承破坏了 String 的不可变的性质。实现类的不可变性, 不光是用 final 修饰类这么简单, 从源码中可以看到, String 实际上是对一个字符数组的封装, 而字符数组是私有的, 并且没有提供 任何可以修改字符数组的方法, 所以一旦初始化完成, String 对象便无法被修改.
  2. **序列化:**String 是支持序列化和反序列化的
  3. **可排序: **String 类还实现了 Comparable 接口, Comparable 接口只有一个方法 public int compareTo(T o), 实现了这个接口就意味着该类支持排序, 即可用 Collections.sort 或 Arrays.sort 等方法对该类的对象列表或数组进行排序.

属性
private final char value[];

这是一个字符数组,并且是final类型,他用于存储字符串内容,从fianl这个关键字中我们可以看出,String的内容一旦被初始化了是不能被更改的。 虽然有这样的例子: String s = “a”; s = “b” 但是,这并不是对s的修改,而是重新指向了新的字符串, 从这里我们也能知道,String其实就是用char[]实现的。

private int hash;

缓存字符串的hash Code,默认值为 0

private static final long serialVersionUID = -6849794470754667710L;
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];

因为String实现了Serializable接口,所以支持序列化和反序列化支持。Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。

private final int offset;

存储的第一个索引

private final int count;

字符串中的字符数


构造方法

1.使用字符数组、字符串构造一个String

我们知道,其实String就是使用字符数组(char[])实现的。所以我们可以使用一个字符数组来创建一个String,那么这里值得注意的是,**当我们使用字符数组创建String的时候,会用到Arrays.copyOf方法和Arrays.copyOfRange方法。这两个方法是将原有的字符数组中的内容逐一的复制到String中的字符数组中。**同样,我们也可以用一个String类型的对象来初始化一个String。这里将直接将源String中的valuehash两个属性直接赋值给目标String。因为String一旦定义之后是不可以改变的,所以也就不用担心改变源String的值会影响到目标String的值。当然,在使用字符数组来创建一个新的String对象的时候,不仅可以使用整个字符数组,也可以使用字符数组的一部分,只要多传入两个参数int offsetint count就可以了。

2.使用字节数组构造一个String

在Java中,String实例中保存有一个char[]字符数组,char[]字符数组是以unicode码来存储的,String 和 char 为内存形式,byte是网络传输或存储的序列化形式。所以在很多传输和存储的过程中需要将byte[]数组和String进行相互转化。所以,String提供了一系列重载的构造方法来将一个字符数组转化成String,提到byte[]和String之间的相互转换就不得不关注编码问题。String(byte[] bytes, Charset charset)是指通过charset来解码指定的byte数组,将其解码成unicode的char[]数组,够造成新的String。这里的bytes字节流是使用charset进行编码的,想要将他转换成unicode的char[]数组,而又保证不出现乱码,那就要指定其解码方式

同样使用字节数组来构造String也有很多种形式,按照是否指定解码方式分的话可以分为两种:

String(byte bytes[]) String(byte bytes[], int offset, int length)

String(byte bytes[], Charset charset)

String(byte bytes[], String charsetName)

String(byte bytes[], int offset, int length, Charset charset)

String(byte bytes[], int offset, int length, String charsetName)

3.使用StringBuffer和StringBuider构造一个String

作为String的两个“兄弟”,StringBuffer和StringBuider也可以被当做构造String的参数。

public String(StringBuffer buffer) {
        synchronized(buffer) {
            this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
        }
    }

public String(StringBuilder builder) {
   this.value = Arrays.copyOf(builder.getValue(), builder.length());
}

当然,这两个构造方法是很少用到的,至少我从来没有使用过,因为当我们有了StringBuffer或者StringBuilfer对象之后可以直接使用他们的toString方法来得到String。关于效率问题,Java的官方文档有提到说使用StringBuilder的toString方法会更快一些,原因是StringBuffer的toString方法是synchronized的,在牺牲了效率的情况下保证了线程安全。

.4.一个特殊的保护类型的构造方法(少用,因为有内存泄露的可能)

String除了提供了很多公有的供程序员使用的构造方法以外,还提供了一个保护类型的构造方法(Java 7),我们看一下他是怎么样的:

String(char[] value, boolean share) {
    // assert share : "unshared not supported";
    this.value = value;
}

从代码中我们可以看出,该方法和 String(char[] value)有两点区别,第一个,该方法多了一个参数:boolean share,其实这个参数在方法体中根本没被使用,也给了注释,目前不支持使用false,只使用true。那么可以断定,加入这个share的只是为了区分于String(char[] value)方法,不加这个参数就没办法定义这个函数,只有参数不能才能进行重载。那么,第二个区别就是具体的方法实现不同。我们前面提到过,String(char[] value)方法在创建String的时候会用到 会用到ArrayscopyOf方法将value中的内容逐一复制到String当中,而这个String(char[] value, boolean share)方法则是直接将value的引用赋值给String的value。那么也就是说,这个方法构造出来的String和参数传过来的char[] value共享同一个数组。 那么,为什么Java会提供这样一个方法呢? 首先,我们分析一下使用该构造函数的好处:

首先,**性能好**,这个很简单,一个是直接给数组赋值(相当于直接将String的value的指针指向char[]数组),一个是逐一拷贝。当然是直接赋值快了。

其次,共享内部数组**节约内存**

该方法之所以设置为protected,是因为一旦该方法设置为公有,在外面可以访问的话,那就破坏了字符串的不可变性,因为修改数组就是修改了字符串,所以,从安全性角度考虑,他也是安全的。对于调用他的方法来说,由于无论是原字符串还是新字符串,其value数组本身都是String对象的私有属性,从外部是无法访问的,因此对两个字符串来说都很安全。


其他方法
length() 返回字符串长度

isEmpty() 返回字符串是否为空

charAt(int index) 返回字符串中第(index+1)个字符

char[] toCharArray() 转化成字符数组

trim() 去掉两端空格

toUpperCase() 转化为大写

toLowerCase() 转化为小写

String concat(String str) //拼接字符串

String replace(char oldChar, char newChar) //将字符串中的oldChar字符换成newChar字符

//以上两个方法都使用了String(char[] value, boolean share);

boolean matches(String regex) //判断字符串是否匹配给定的regex正则表达式

boolean contains(CharSequence s) //判断字符串是否包含字符序列s

String[] split(String regex, int limit) 按照字符regex将字符串分成limit份。

String[] split(String regex)

字符串相等关系

/代码1  
String sa=new String("Hello world");            
String sb=new String("Hello world");      
System.out.println(sa==sb);  // false       
//代码2    
String sc="Hello world";    
String sd="Hello world";  
System.out.println(sc==sd);  // true   

代码1,sa和sb都是new一个对象,会在堆中new出这两个对象的内存地址,而‘==’比较的是地址和值,所以不相等。

带吗2,sc会去常量池找是否存在“hello world”,如果存在贼直接引用,不存在贼创建一个新的字符串并放在常量池,sd会找到常量池,然后引用,所以会相等

//代码1  
String sa = "ab";                                          
String sb = "cd";                                       
String sab=sa+sb;                                      
String s="abcd";  
System.out.println(sab==s); // false  
//代码2  
String sc="ab"+"cd";  
String sd="abcd";  
System.out.println(sc==sd); //true  

代码1中局部变量sa,sb存储的是堆中两个拘留字符串对象的地址。而当执行sa+sb时,JVM首先会在堆中创建一个StringBuilder类,同时用sa指向的拘留字符串对象完成初始化,然后调用append方法完成对sb所指向的拘留字符串的合并操作,接着调用StringBuilder的toString()方法在堆中创建一个String对象,最后将刚生成的String对象的堆地址存放在局部变量sab中。而局部变量s存储的是常量池中"abcd"所对应的拘留字符串对象的地址。 sab与s地址当然不一样了。这里要注意了,代码1的堆中实际上有五个字符串对象:三个拘留字符串对象、一个String对象和一个StringBuilder对象。
代码2中"ab"+“cd"会直接在编译期就合并成常量"abcd”, 因此相同字面值常量"abcd"所对应的是同一个拘留字符串对象,自然地址也就相同。


String三兄弟

String(出生于JDK1.0时代) 不可变字符序列
StringBuffer(出生于JDK1.0时代) 线程安全的可变字符序列
StringBuilder(出生于JDK1.5时代) 非线程安全的可变字符序列


StringBuffer与String的可变性问题
//String   
public final class String  
{  
        private final char value[];  
  
       	public String(String original) {  
        // 把原字符串original切分成字符数组并赋给value[];  
       	}  
}  
  
//StringBuffer   
public final class StringBuffer extends AbstractStringBuilder  
{  
         char value[]; //继承了父类AbstractStringBuilder中的value[]  
         public StringBuffer(String str) {  
                 super(str.length() + 16); //继承父类的构造器,并创建一个大小为str.length()+16的value[]数组  
                 append(str); //将str切分成字符序列并加入到value[]中  
        }  
}  

很显然,String和StringBuffer中的value[]都用于存储字符序列。但是,
(1) String中的是常量(final)数组,只能被赋值一次。
比如:new String(“abc”)使得value[]={‘a’,‘b’,‘c’},之后这个String对象中的value[]再也不能改变了。这也正是大家常说的,String是不可变的原因 。
注意:这个对初学者来说有个误区,有人说String str1=new String(“abc”); str1=new String(“cba”);不是改变了字符串str1吗?那么你有必要先搞懂对象引用和对象本身的区别。这里我简单的说明一下,对象本身指的是存放在堆空间中的该对象的实例数据(非静态非常量字段)。而对象引用指的是堆中对象本身所存放的地址,一般方法区和Java栈中存储的都是对象引用,而非对象本身的数据。
(2) StringBuffer中的value[]就是一个很普通的数组,而且可以通过append()方法将新字符串加入value[]末尾。这样也就改变了value[]的内容和大小了。

比如:new StringBuffer(“abc”)使得value[]={‘a’,‘b’,‘c’,’’,’’…}(注意构造的长度是str.length()+16)。如果再将这个对象append(“abc”),那么这个对象中的value[]={‘a’,‘b’,‘c’,‘a’,‘b’,‘c’,’’…}。这也就是为什么大家说 StringBuffer是可变字符串 的涵义了。从这一点也可以看出,StringBuffer中的value[]完全可以作为字符串的缓冲区功能。其累加性能是很不错的,在后面我们会进行比较。

总结,讨论String和StringBuffer可不可变。本质上是指对象中的value[]字符数组可不可变,而不是对象引用可不可变。


StringBuffer与StringBuilder的线程安全性问题

StringBuffer和StringBuilder可以算是双胞胎了,这两者的方法没有很大区别。但在线程安全性方面,StringBuffer允许多线程进行字符操作。这是因为在源代码中StringBuffer的很多方法都被关键字****synchronized**** 修饰了,而StringBuilder没有。
有多线程编程经验的程序员应该知道synchronized。这个关键字是为****线程同步机制**** 设定的。我简要阐述一下synchronized的含义:
每一个类对象都对应一把锁,当某个线程A调用类对象O中的synchronized方法M时,必须获得对象O的锁才能够执行M方法,否则线程A阻塞。一旦线程A开始执行M方法,将独占对象O的锁。使得其它需要调用O对象的M方法的线程阻塞。只有线程A执行完毕,释放锁后。那些阻塞线程才有机会重新调用M方法。这就是解决线程同步问题的锁机制。
了解了synchronized的含义以后,大家可能都会有这个感觉。多线程编程中StringBuffer比StringBuilder要安全多了 ,事实确实如此。如果有多个线程需要对同一个字符串缓冲区进行操作的时候,StringBuffer应该是不二选择。
注意:是不是String也不安全呢?事实上不存在这个问题,String是不可变的。线程对于堆中指定的一个String对象只能读取,无法修改。试问:还有什么不安全的呢?


String和StringBuffer的效率问题

首先说明一点:StringBuffer和StringBuilder可谓双胞胎,StringBuilder是1.5新引入的,其前身就是StringBuffer。StringBuilder的效率比StringBuffer稍高,如果不考虑线程安全,StringBuilder应该是首选。另外,JVM运行程序主要的时间耗费是在创建对象和回收对象上。


String常量与String变量的"+"操作比较
▲测试①代码:     (测试代码位置1)  String str="";
               (测试代码位置2)  str="Heart"+"Raid";
            	[耗时:  0ms]
             
▲测试②代码        (测试代码位置1)  String s1="Heart";
                               String s2="Raid";
                               String str="";
                (测试代码位置2)  str=s1+s2;
            [耗时:  1516ms]

结论:String常量的“+连接” 稍优于 String变量的“+连接”。

原因: 测试①的"Heart"+“Raid"在编译阶段就已经连接起来,形成了一个字符串量"HeartRaid”,并指向堆中的拘留字符串对象。运行时只需要将"HeartRaid"指向的拘留字符串对象地址取出1W次,存放在局部变量str中。这确实不需要什么时间。
测试②中局部变量s1和s2存放的是两个不同的拘留字符串对象的地址。然后会通过下面三个步骤完成“+连接”:
1、StringBuilder temp=new StringBuilder(s1),
2、temp.append(s2);
3、str=temp.toString();
我们发现,虽然在中间的时候也用到了append()方法,但是在开始和结束的时候分别创建了StringBuilder和String对象。可想而知:调用1W次,是不是就创建了1W次这两种对象呢?不划算。但是,String变量的"+连接"操作比String常量的"+连接"操作使用的更加广泛。 这一点是不言而喻的。


String对象的"累+"连接操作与StringBuffer对象的append()累和连接操作比较
▲测试①代码:     (代码位置1)  String s1="Heart";
                           String s="";
               (代码位置2)  s=s+s1;
             [耗时:  42004500ms]
             
▲测试②代码        (代码位置1)  String s1="Heart";
                StringBuffer sb=new StringBuffer();
                (代码位置2) sb.append(s1);
             [耗时:  0ms(当循环100000次的时候,耗时大概1631ms)]

结论:大量字符串累加时,StringBuffer的append()效率远好于String对象的"累+"连接

原因:测试① 中的s=s+s1,JVM会利用首先创建一个StringBuilder,并利用append方法完成s和s1所指向的字符串对象值的合并操作,接着调用StringBuilder的 toString()方法在堆中创建一个新的String对象,其值为刚才字符串的合并结果。而局部变量s指向了新创建的String对象。因为String对象中的value[]是不能改变的,每一次合并后字符串值都需要创建一个新的String对象来存放。循环1W次自然需

​ 测试②中sb.append(s1);只需要将自己的value[]数组不停的扩大来存放s1即可。循环过程中无需在堆中创建任何新的对象。效率高就不足为奇了。


String三兄弟总结

(1) 在编译阶段就能够确定的字符串常量,完全没有必要创建String或StringBuffer对象。直接使用字符串常量的"+"连接操作效率最高。

(2) StringBuffer对象的append效率要高于String对象的"+"连接操作。

(3) 不停的创建对象是程序低效的一个重要原因。那么相同的字符串值能否在堆中只创建一个String对象那。显然拘留字符串能够做到这一点,除了程序中的字符串常量会被JVM自动创建拘留字符串之外,调用String的intern()方法也能做到这一点。当调用intern()时,如果常量池中已经有了当前String的值,那么返回这个常量指向拘留对象的地址。如果没有,则将String值加入常量池中,并创建一个新的拘留字符串对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值