(这一个章节,将会提及到Java里面很特殊的一个数据类型:String,其类型的主要麻烦在于我们经常会用到它,而且针对它的“不可变”,很多时候很难去理解。附带这个章节会讲到很多关于处理字符串格式的内容,包括使用正则表达式做验证以及使用日期、货币格式化处理,还会提及到的就是如果在使用JDBC的时候针对SQL的类型[java.sql包内]和针对Java的类型[java.util]的一些相互转换问题。这一个章节涵盖的内容可能比较小范围,但是我们在开发项目过程中会经常遇到,所以是不得不去了解和不得不去学习的一个章节。方便大家看的时候容易查找代码段,代码部分都有[$]作为段落查找前缀,如果有什么笔误请大家来Email告知:silentbalanceyh@126.com,谢谢!)
本章目录
1.不可变类
2.String详解
3.StringBuilder和StringBuffer详解
4.正则表达式
5.关于转型
6.货币、时间、日期格式化
1.不可变类
Java中的不可变类最典型的代表就是String类,而不可变类到底具有一个什么样的特性呢:
- 不可变类的所有成员一般情况下都声明为private访问权限,有必要的情况下最好带上final修饰
- 不可变类的class的一般使用final修饰
- 只为该类提供get方法,而不去提供set方法
- 提供一个构造方法,并且构造方法一般都是带参数的,在构造这个对象的时候一次性初始化它所有的数据
- 类中如果出现组合的时候,从get方法得到这个组合对象的之前,先克隆这个组合对象
- 组合对象传递给构造函数的时候,也需要先克隆一份
- 若浅克隆不能符合不可变对象的行为,就要实现深度克隆【这里不再介绍深度克隆和浅克隆,可参考:http://blog.youkuaiyun.com/silentbalanceyh/archive/2009/08/25/4483600.aspx】
简单讲:
不可变类:当使用Java代码获得了不可变类的一个实例引用的时候,不可以去修改这个实例的内容,不可变类一旦创建了过后,其内在成员变量的值就不可以被修改。
可变类:当获得该类的一个实例引用的时候,可以改变这个实例的内容。
常用的数据类型有:
可变类:StringBuffer、StringBuilder、Date
不可变类:Boolean、Byte、Character、Double、Float、Integer、Long、Short、String
——[$]关于String——
packageorg.susan.java.string;
public classStringImmutable {
public static voidmain(Stringargs[]){
StringstrOne ="Hello World";
String strTwo = strOne;
strOne = strOne +" LangYu";
System.out.println(strOne == strTwo);
System.out.println(strTwo);
System.out.println(strOne);
}
}
上边这段代码可以得到输出:
false
Hello World
Hello World LangYu
那么上边这段代码究竟在JVM里面发生了什么事情呢?也许我们都会觉得strOne指向的字符串对象“Hello World”在进行+操作的时候发生了改变,实际上不是这样,真正的情况如下图【这里不再解释JVM的内存模型,而且下边的图不考虑字符串池(常量池的子集)】:
上边是三句代码的执行流程,最后一个结果就可以理解的是strTwo和strOne的输出值,而为什么strTwo和strOne使用==的时候会输出false呢?这里不要觉得是strTwo的对象内容是Hello World,而strOne的对象内容是Hello World LangYu,因为它们内容不相等才输出为false,针对String类而言==比较的是strTwo和strOne引用是否指向了同一个对象,只有Object类的==和equals方法是等价的,而String类重写了Object类的equals方法,关于String的细节这里不做详细讨论,下边会有说明。这里需要说明的是:String就是一个典型的不可变(Immutable)的类,而针对不可变的类在对它进行操作的过程中,JVM内部是按照上边的图示进行说明的。
而针对不可变类的自定义过程,上边列举的特性是需要满足的,简单分析一下原理:
【*:声明了该类为一个final过后,这个类就不能够被子类化了,这样就防止了子类对其内部的内容进行修改,而属性都是private也防止了针对这个类的属性进行直接访问,如果需要访问这个类的属性,只能通过get的方式访问,因为没有set方法,所以一旦通过构造函数初始化了这个类的属性值过后,这个类里面的属性就不能修改了,这就是该类的内容不能修改的本意,所以这个类的内容就是不可变的。而关于里面出现组合对象,为什么需要实现深度克隆而不是浅克隆呢,了解克隆的人都明白,如果仅仅是实现了浅克隆,则该组合的对象在进行了拷贝过后,其组合对象的引用还是指向了同一个对象,那么这种情况下就使得两个不可变对象的内部的组合对象引用指向了同一个对象,如果一个不可变对象里面组合对象引用的对象在初始化的时候改变了对象的内容,那么原来的不可变对象里面组合对象引用的内容也会随之改变,这样这个不可变对象的实现就出现了问题,所以在这种情况下,如果需要克隆的话,需要使用深度克隆。】
读者可以在看String的讲解之前思考一下下边这段代码的输出:
——[$]关于String的混淆点——
packageorg.susan.java.string;
public classStringTester {
public static voidmain(Stringargs[]){
StringstrOne ="Hello World";
StringstrTwo ="Hello World";
System.out.println(strOne == strTwo);
System.out.println(strOne.equals(strTwo));
strOne =newString("Hello World");
System.out.println(strOne == strTwo);
System.out.println(strOne.equals(strTwo));
}
}
这段代码的输出为:
true
true
false
true
2.String详解
i.String对象的创建原理:
上边小节的最后一段代码是一段比较容易混淆的代码,其实了解了String类的对象的创建流程过后就不会混淆了,先看前两句:
StringstrOne ="Hello World";
StringstrTwo ="Hello World";
在这两句里面,其实JVM做了不同的操作,可能这样讲不容易理解,解释一下:
JVM中存在一个字符串池,当一个String对象使用=符号创建的时候,JVM会先去检索字符串池里面是否已经存在了对象内容和新创建的对象内容相匹配的对象,如果检索这个对象已经存在了就直接将新创建的引用指向该对象,如果这个对象不存在的时候,就在字符串池中重新分配空间构造一个String对象,然后让这个引用指向该对象(注意这种构造方式为JVM内部默认构造,和new的构造方式不一样,所以这里不能和new操作符构造对象的方式混淆,这种方式构造出来的String对象由字符串池进行维护,而使用new构造出来的String对象是不在String池中的)。若不使用=符号创建该对象而是直接使用new操作符创建对象,就直接省略掉检索对象池的过程。所以StringstrOne ="Hello World";执行的时候,String池里面没有任何String对象,所以JVM构造一个以Hello World为内容的对象,并且将这个对象交给字符串池进行维护,然后让strOne引用指向该对象;当代码运行到StringstrTwo ="Hello World";的时候,JVM会直接检索字符串池里面是否有Hello World对象,而且这个Hello World对象不是通过new操作符构造的,由字符串池直接维护,检索到了过后直接将该引用指向这个对象。所以:
【*:上边两句话出现在同一个代码段的时候,第一句多了一个步骤就是让字符串池构造一个String对象的步骤。】
关于字符串池:其实从内存模型上理解,字符串池就是JVM内部的常量池的一个子集,也就是直接使用=符号的时候创建的String对象是放在常量池内的,而不是放在内存堆中的,在常量池中的内容是在编译期就被确定下来的,而且常量池维护的对象保证其内容的唯一性,所以在常量池中只可能出现一个内容相同的String对象。
new操作符可以认为是一个比较特殊的操作符,因为一旦使用new操作符,它构造的字符串没有在字符串池(常量池)里面,而是运行时的时候在内存堆中动态分配的空间。上边这段代码的分析留给读者自己分析剩余的部分,再看一段代码,这段代码更加有说服力:
——[$]关于String的构造——
packageorg.susan.java.string;
public classNewStringCreate {
public static voidmain(Stringargs[]){
StringstrOne =newString("Hello World");
StringstrTwo =newString("Hello World");
System.out.println(strOne == strTwo);
System.out.println(strOne.equals(strTwo));
strOne ="Hello World";
System.out.println(strOne == strTwo);
System.out.println(strOne.equals(strTwo));
strTwo ="Hello World";
System.out.println(strOne == strTwo);
System.out.println(strOne.equals(strTwo));
}
}
这段代码的输出为:
false
true
false
true
true
true
仔细分析一下就可以知道:其实JVM内部存在的对象池里面的String对象和使用new操作符构造的String对象是有区别的,针对同样String内容的对象的检索过程是需要仔细思考的,上边这段代码实际上在JVM里面存在三个内容一样的String对象,有两个是通过new操作符创建的,另外一个是JVM默认创建的,使用new创建的String对象是在内存堆空间中,是运行时动态分配,而使用=符号创建的对象是在String的常量池中,是编译时就已经确定好的。这里还有一种情况需要简单说明一下:
——[$]关于String中带+的构造——
packageorg.susan.java.string;
public classAddStringCreate {
public static voidmain(Stringargs[]){
StringstrOne ="Hello ";//注意这里Hello后有个空白
StringstrTwo ="World";
StringstrThree ="Hello "+"World";//注意这里Hello后有个空白
StringstrFour ="Hello World";
StringstrFive =newString("Hello ") +"World";//注意这Hello后里有个空白
StringstrSix =newString("Hello ") + strTwo;//注意这Hello后里有个空白
StringstrSeven ="Hello "+ strTwo;//注意这Hello后里有个空白
System.out.println(strThree == strFour);
System.out.println(strFour == (strOne + strTwo));
System.out.println(strThree == (strOne + strTwo));
System.out.println(strFour == strFive);
System.out.println(strThree.equals(strFive));
System.out.println(strThree.equals(strOne + strTwo));
System.out.println(strFive == strSix);
System.out.println(strSeven == strFour);
System.out.println(strSeven.equals(strFour));
}
}
仔细分析上边这段代码的输出:
true
false
false
false
true
true
false
false
true
其实很清楚一点就是上边这段代码在构造strOne、strTwo、strThree的时候,除了请求字面量不一样,步骤是一模一样的,而strFour的构造步骤比strThree少了一个创建步骤,而且strThree有可能会有冗余内存开销。【*:这里提及的步骤是JVM从编译java源代码一直到执行字节码两个过程,也就是从JVM接触这段代码开始,不要混淆和误解】,但是strFour和strThree的请求字面量都是一样的,所以这里的引用strOne、strTwo、strThree和strFour指向的对象都是存在常量池中的字符串池里面的,它们的值都是直接在编译时就已经决定好了。strFive和strSix的构造结果在进行==比较的时候会返回false,因为strFive和strSix使用的构造方式是运行时在堆内分配的,而strSeven虽然使用的是"Hello "+ strTwo的格式,但是JVM会认为它的字面量值是不确定的,因为JVM并不知道在这句话执行之前,strTwo引用的对象是否一定是原来常量池里面内容。但是如果修改一段代码:
StringstrTwo ="World";
改成:
finalStringstrTwo ="World";
就会发现System.out.println(strSeven == strFour);这句话的输出会为true,也就是倒数第二个输出为true。这里需要解释,也就是当strTwo的引用加上了final修饰过后,证明该引用是不能够改变它所指向的对象的,那么这样的情况下就是strTwo称为了一个不可变引用(不能改变引用指向其他对象),而又因为strTwo本身指向的是在常量池里面的一个不可变对象String,那么这样的情况就使得JVM在编译时已经可以知道strTwo的内容是不可变的了,所以这样的情况下strSeven在编译的时候,右边实际上是两个常量进行相加,而且不论发生任何代码改变都不会影响这句话,所以这种情况下strSeven实际上指向的就是常量池中的对象“Hello World”,所以这样的改动使得倒数第二句为true。但是如果像下边这样修改还是不会得到true的输出:
finalStringstrTwo =newString("World");
因为一旦使用了new操作符过后,JVM还是会在运行时来分配空间,只要是运行时分配空间的话,就会将字符串对象分配到堆空间上去,这样==就会返回false。
【*:常量池和对象池不一样,在Java内存模型一章有JVM存储对象以及相关属性的存储方式,所以一定要区分对象池和常量池在内存模型中的概念!】
接下来用详细的图示来解析一下上边的代码以及后边这种修改过后的方式:
——[$]直接使用字面量赋值【*:这里需要注意的是整个这步操作会在编译时完全完成】——
语句:StringstrOne ="Hello ";//Hello后边有个空白
——[$]strFive的初始化代码示例子【*:该操作是结合编译时和运行时同时完成,但是strFive是在运行时初始化的】——
语句:StringstrFive =newString("Hello ") +"World";//Hello后边有个空白
上边两个图里面的引用都是使用的紫色,这里代表这个引用是可以指向其他对象的,也就是该引用为可改变引用,非final的。
——[$]strFour和strThree的构造例子——
语句:StringstrThree ="Hello "+"World";
StringstrFour ="Hello World";
StringstrTwo ="World";
finalStringstrTwo ="World";
finalStringstrTwo =newString("World");
这三句话对于JVM而言是有区别的:
[1]针对JVM而言,直接使用字面量赋值,会在常量池中进行内存对象的创建操作,而且在不声明final的时候,JVM会认为该引用是可以变化的,一旦和该引用有关的表达式构建字符串的时候都需要在运行时在内存堆里面进行操作
[2]声明了final而不使用new操作符赋值的时候,同样在常量池中进行内存对象的创建,JVM认为该引用是不可以指向其他对象的,也可以这样认为,这种情况创建的字符串对象也可以认为是匿名的直接通过字面量赋值的不带final的字符串对象,而JVM在表达式中处理这类型的对象的时候是一致的,也就是说:
finalStringstrTwo ="World";
定义了上边这样一个变量过后,下边两句话是等价的:
StringstrSeven ="Hello "+ strTwo;
StringstrSeven ="Hello "+"World";
[3]可是即使声明了final引用,但是使用的是new操作符的时候,这种情况的对象本身就是在堆空间分配内存而创建的对象,所以这种情况下和普通的new操作符一样,唯一不同的是声明了final,那么这个引用就不能指向其他对象了,也就是说下边的代码无法通过编译:
finalStringstrTwo =newString("World");
strTwo ="Hello "+ strTwo;
总而言之,根据上边所有关于String创建的讲解,就可以知道String和Java里面所有的不可变对象一样的特性,针对不可变的特性需要仔细理解该内容。总之记住一点,不论是在常量池里面的String对象,还是处于Java内存堆里面的String对象,一旦初始化了过后就不可以更改,一旦要更改只能新建或者使用该引用指向本身就存在的对象。
【*:这里衍生一点看似重要其实不需要详细理解的问题,上边最容易迷惑的是这句话:StringstrThree ="Hello "+"World";这句话其实有两种理解,有些JVM里面进行了预处理操作,就是真正在编译的时候进行了常量池的简单初始化,而初始化过程因为使用的算法不一样,所以有可能这里实际上没有Hello 和World两个字符串在常量池里面,也就是说这种情况直接使用了字面量的整合赋值,这种情况就和下边那种情况是一样的了,也就是StringstrFour ="Hello World";其实从编程本质上讲,到达这个地步这个内容已经不是很重要了,因为到这个级别的理解过后,我们不会再过于去关注它,也就是说,上边图的解释里面strThree的图解是值得争议的。有些古老的算法里面在编译过程一旦遇到一个String的字符串就直接进行常量池的内存分配,上边的图示就是这种模式,如果是将二者相加然后进行字面量直接赋值的话,可能strThree和strFour的内存存储上应该是差不多的。其实最后二者进行==比较之所以能够为true,主要是因为常量池的常量的唯一性决定的,这里重复一下就可以证明常量池里面的量只有一个没有多的,这一点是我们真正要在这个例子里面去理解的部分。上边的图示我是觉得更加容易理解String在这个过程的每一个步骤细节,以不至于有什么理解上的歧义,这里还需要讲明的是这里的所有的图片都是自绘的,所以也是做一个图示理解,唯独不能确定的也仅仅是上边strThree的图解,因为有可能Hello 和World两个字符串确实不存在,因为JVM有办法仅仅通过字面量的运算来决定是否真正需要为出现的字符串进行内存空间分配。】
最后需要说明一点的是,如果对象都在堆里面需要让两个引用指向同样对象的时候,需要进行赋值操作,而不是使用字面量值来进行引用同对象的操作:
StringstrOne =newString("Hello");
StringstrTwo ="Hello";// strTwo == strOne返回为false
StringstrThree = strOne;// strThree == strOne返回为true
上边代码就可以说明这一点了。
根据String的创建过程做一个关于String的简单的小节:
- String类型在Java里面不属于基础类型,而是一个继承于Object的类类型,所以遵循Java里面类类型的存储原理,因为它的不可变性所以常量池里面会有一个类型池,这里就是字符串池
- JVM中存在一个字符串池,该池属于常量池的一个子集,常量池里面不仅仅存放了字符串常量,还会存在其他常量,所以字符串池仅仅是常量池空间的一个子集
- 当使用=创建字符串对象的时候,会直接检索常量池中的字符串池,如果字符串池存在请求字面量相匹配的常量字符串,就直接将引用指向常量池中的对象,如果不存在请求字面量相匹配的常量字符串,就由常量池自己创建一个String的字符串常量(也是一个对象),然后将引用指向该常量