对于大部分Java程序员来说,总有些难以绕过的错误,也许你第一次犯了这个错误后,第二次还会再犯,因为这些错误具有隐蔽性,我们把这种错误称为陷阱。为了避免错误一犯再犯,我们就需要把这些陷阱记录下来,以免在未来的开发之中踩坑。
表达式是Java里最基本的组成单元,但在简单的表达式背后,依然有一些很容易让我们犯错的陷阱。
一、字符串陷阱
- JVM对字符串的处理
首先,我们来看一条简单的语句
String java=new String("linkai");
请问,上面语句创建了几个字符串对象?答案是:2个,一个是”linkai”这个直接量对应的字符串对象,一个是由new String()构造器返回的字符串对象。
再问,下面语句创建了几个字符串对象:
String str="Hello"+"linkai"+"boss";
创建了一个!因为str的值可以在编译时确定下来,JVM会在编译时就计算出str的值。
Java程序中创建对象的常规方式有如下4种:
- new构造器创建
- 通过Class对象的newInstance()方法调用构造器创建Java对象
- 通过Java反序列化机制从IO流中恢复Java对象
- 通过Java对象提供的clone()方法复制一个新的Java对象
除此之外,对于字符串以及Byte、Short、Int、Long、Character、Float、Double、Boolean这些基本类型的包装类,Java还允许以直接量的方式创建Java对象。也可以通过简单的算法表达式、连接运算来创建Java对象。
对于Java中的字符串直接量,JVM会使用一个字符串池来保存,当第一次使用某个字符串直接量时,JVM会将它放入字符串池进行缓存。当程序需要再次使用时就无需创建一个新的字符串了。因此,当程序中需要使用字符串、基本类型包装类实例时,尽量使用字符串直接量、基本类型值得直接量,避免通过new String()、new Integer()形式创建,保持程序较好的性能
2.不可变字符串
String类是一个典型的不可变类。当一个String对象创建完成后,该String类里面包含的字符串就被固定下来了,以后永远不会改变。示例如下:
public class ImmutableString
{
public static void main(String[] args)
{
//定义一个字符串变量
String str="Hello"; //①
System.out.println(System.identityHashCode(str));
str=str+"Java"; //②
System.out.println(System.identityHashCode(str));
str=str+"linkai";
System.out.println(System.identityHashCode(str));
}
}
前面说过,String对象创建完成后,里面包含的字符序列将不能被改变。但你可能会感到疑惑:上面str变量对应的字符序列不是一直在改变吗?其实是这样的,str只是一个引用类型变量,它并不真是String对象,它只是指向String对象而已。
程序①、②代码执行之后程序在内存中分配如下所示:
可以看出,发生改变的不是String对象,而是str变量本身,它改变了指向。
而且需要注意的还有,堆内存中的”hello”字符串可能以后都不会被用到,但是并不会被垃圾回收,将一直存在于字符串池中——这就是Java内存泄漏原因之一。
上面程序中的identityHashCode()方法用于获取一个对象唯一的hashCode值。只有当两个对象相同时identityHashCode才会相等,因此,上面三次返回的值并不同。
对于String类而言,它代表字符串序列不可改变的字符串,因此,如果程序要一个字符序列发生改变的字符串,则应该考虑StringBuilder(线程不安全)或StringBuffer(线程安全,但执行效率低)。
二、表达式类型的陷阱
- 表达式类型的自动提升
Java规定:当一个算术表达式中包含多个基本类型的值时,整个算术表达式的数据类型将会发生自动提升。自动提升规则如下:
a、所有byte、short、char型都被提升到int型
b、整个算术表达式类型自动提升到与表达式中最高等级操作数同样的类型。操作数等级排列如下图:
下面程序演示了自动转换的几种类型
public class AutoPromote
{
public static void main(String[] args)
{
short sValue=5;
sValue=sValue-2;//这行代码将引发编译错误,因为等号右边表达式类型为int类型
byte b=40;
char c='a';
int i=23;
double d=.31;
double result=b+c+i+d;//等号右边表达式将为double类型,故赋值给一个double类型变量
System.out.println("Hello!"+'a'+7);//输出Hello!a7
System.out.println('a'+7+"Hello!");//输出104Hello!
}
}
- 复合赋值运算符的陷阱
前面说到当sValue=sValue-2
时会引起编译错误,但是,如果将其改为sValue-=2;
就没有任何问题了。有的人会觉得奇怪,这两句不是等同吗?怎么编译结果会不一样。
其实,这两句实际上是不同的。
复合赋值运算符包含了一个隐式的类型转换,也就是说上面那句代码等同于sValue=(short)(sValue-2)
但是,如果结果值类型比该变量类型大时,复合赋值运算符会执行一次强转,就有可能导致高位“截断”!如下:
short st=10;
st+=90000;
那么将会看到输出24479,这是因为short类型变量只能接受-32768到32768之间的整数,因此程序将会将90010的高位”截断”。
因此使用复合运算符是应该注意一下几种情况:
a、用于byte、short、char等类型的变量
b、用于int类型变量,而表达式右边是long、float、double类的值
c、用于float类型,而表达式右边是double类的值
d、当成连接字符串使用时(要注意,+=运算符左边的变量只能是String变量,不能是其父类型)
三、正则表达式的陷阱
先来看一段程序:
public class StringSplit
{
public static void main(String[] args)
{
String str="java.is.funny.i.am.linkai";
String[] strArr=str.split("."); //①
for(String s:strArr)
{
System.out.println(s);
}
}
}
上面程序希望显示出该字符串被分割后得到的字符串数组。运行该程序,结果却什么都没输出。为什么呢?想要知道为什么,我们需要注意以下两点:
a、String提供的split(String regex)方法需要的参数是正则表达式;
b、正则表达式中的点号(.)可匹配任意字符。
知道了上面两点后我们就不难理解这个输出结果了(因为上面程序是以任意字符作为分隔符的)。要得出正确结果,必须对程序在第①行做如下修改
String[] strArr=str.split("\\.");
即是对点号进行转义!由此可见,这并不是Java的bug,而是我们对Java中某些特性掌握不精准造成的。
对于以上这些表达式陷阱,我们要引起注意,平时注意防范,项目开发时才不会不断地踩坑。