java表达式
表达式是Java程序里
最基本的组成单元
,各种表达式是Java程序员最司空见惯的内容, 只是在这些简单用法背后,依然有一些很容易让人出错的陷阱。
1. 字符串
String java = new String("疯狂Java");
上面语句实际上创建了 2个字符串对象,一个是“疯狂Java”这个直接量
对应的字符串对象,一个是由new String()构造器返回的字符串对象
。
Java程序中创建对象的常见方式有如下4种:
- 通过new调用构造器创建Java对象。
- 通过Class对象的newInstance()方法调用构造器创建Java对象。
- 通过Java的反序列化机制从IO流中恢复Java对象。
- 通过Java对象提供的clone()方法复制一个新的Java对象。
- 除此之外,对于字符串以及Byte、Short、Integer、Long、Character、Float、Double和Boolean这些基本类型的包装类,Java还允许以直接量的方式来创建Java对象,例如如下语句所示。
String str = “abc”;
Integer in = 5;
除此之外,也可通过简单的算法表达式、连接运算来创建Java对象,例如如下语句所示。
String str2 = “abc” + “xyz”;
Long price = 23 + 12;
System提供的identityHashCode()
静态方法用于获取某个对象唯一的hashCode值,这个identityHashCode()的返回值与该类是否重写了hashCode()方法无关。只有当两个对象相同
时,它们的identityHashCode值
才会相等。
对于String类而言,它代表字符串序列不可改变的字符串
,因此如果程序需要一个字符序列会发生改变
的字符串,那应该考虑使用StringBuilder或StringBuffer
。很多资料上都推荐使用StringBuffer,那是因为这些资料都是JDK1.5问世之前的——过时了。
实际上通常应该优先考虑使用StringBuilder。StringBuilder与StringBuffer唯一的区别在于,StringBuffer是线程安全的
,也就是说StringBuffer类里绝大部分方法都增加了synchronized修饰符
。对方法增加synchronized修饰符可以保证该方法线程安全,但会降低该方法的执行效率。在没有多线程的环境下,应该优先使用StringBuilder类来表示字符串。
2. 表达式类型的陷阱
Java语言规定:当一个算术表达式中包含多个基本类型
的值时,整个算术表达式的数据类型将发生自动提升
。Java语言中的自动提升规则如下。
- 所有byte型、short型和char型将被提升到int型。
- 整个算术表达式的数据类型自动提升到与表达式中最高等级操作数同样的类型。操作数的等级排列如图5.4所示,
位于箭头右边的等级高于箭头左边类型的等级
。
short sValue = 5;
sValue = sValue - 2; //一个int类型的值赋给short类型的变量(sValue)时导致了编译错误
short sValue = 5;
sValue -= 2; //正常
Java语言几乎允许所有的双目运算符和=一起结合成符合赋值
运算符,如+=、-=、*=、/=、%=、<<=、>>=、>>>=、&=、^=和|=等。根据Java语言规范,复合赋值运算符包含了一个隐式的类型转换,也就是说,下面两条语句并不等价。
a = a + 5;
a += 5;
实际上,a += 5等价于a = (a的类型)(a + 5);,这就是复合赋值运算符中包含的隐式类型转换。
也就是说,复合赋值运算符会自动地将它计算的结果值强制类型转换
为其左侧变量的类型。如果结果的类型与该变量的类型相同,那么这个转型不会造成任何影响。
如果结果值的类型比该变量的类型要大,那么复合赋值运算符将会执行一次强制类型转换,这个强制类型转换将有可能导致高位“截断”
。
3.输入法导致的陷阱
小心把输入法切换到了全角状态
编译该程序将会提示“非法字符:\12288”的错误。
4. 注解
大部分时候,Java编译器会直接忽略到注释部分,但有一种情况例外:Java要求注释部分的所有字符必须是合法的字符。
Java程序并没有完全忽略注释部分的内容。编译器在上面程序的粗体字部分检测到一个非法字符,Java程序允许直接使用\uXXXX的形式代表字符,它要求\u后面的4个字符必须是0~F字符,而上面注释中包含了\unit5,这不符合Java对Unicode转义字符的要求。
5. 转义字符的陷阱
学习过《疯狂Java讲义》的读者应该还记得,Java程序提供了3种方式来表示字符:
- 直接使用单引号括起来的字符值,如’a’;
- 使用转义字符,如’\n’;
- 使用Unicode转义字符,如’\u0062’。
Java对待Unicode转义字符时不会进行任何处理,它会将Unicode转义字符直接替换成对应的字符,这将给Java程序带来一些潜在的陷阱。
6.慎用字符的Unicode转义形式
理论上,Unicode转义字符可以代表任何字符(不考虑那些不在Unicode码表内的字符),因此很容易想到:所有字符都应该可以使用Unicode转义字符的形式。
System.out.println("abc\u000a".length());
上面程序试图计算“abc\u000a”字符串的长度,表面上看这个程序将输出4,但编译该程序将发现程序无法通过编译,
引起这个编译错误的原因是Java对Unicode转义字符不会进行任何特殊的处理,它只是简单地将Unicode转义字符替换成相应的字符。对于\u000a而言,它相当于一个换行符(相当于\n),因此对Java编译器而言,上面程序相当于如下程序。
System.out.println("abc
".length());
7. 中止行注释的转义字符
//\u000a代表一个换行符
char c = 0x000a;
ava编译器会将程序中的\u000a替换成换行符
8. 泛型可能引起的错误
泛型是JDK 1.5新增的知识点,它允许在使用Java类、调用方法时传入一个类型实参,这样就可以让Java类、调用方法动态地改变类型。
原始类型变量的赋值
在严格的泛型程序中,使用带泛型声明的类时应该总是为之指定类型实参,但为了与老的Java代码保持一致,Java也允许使用带泛型声明的类时不指定类型参数。如果使用带泛型声明的类时没有传入类型实参,那么这个类型参数默认是声明该参数时指定的第一个上限类型,这个类型参数也被称为raw type(原始类型)。
当尝试把原始类型的变量赋给带泛型类型的变量时,会发生一些有趣的事情。示例如下。
public class RawTypeTest
{
public static void main(String[] args)
{
//创建一个RawType的List集合
List list = new ArrayList();
//为该集合添加3个元素
list.add("疯狂Java讲义");
list.add("轻量级Java EE企业应用实战");
list.add("疯狂Ajax讲义");
//将原始类型的list集合赋给带泛型声明的List集合
List<Integer> intList = list;
//遍历intList集合的每个元素
for (int i = 0 ; i < intList.size() ; i++)
{
System.out.println(intList.get(i));
}
}
}
上面程序中先定义了一个不带泛型信息List集合
,其中所有集合元素都是String类型
。接着,尝试将该List集合赋给一个List<Integer>变量
尝试编译上面程序,一切正常,可以通过编译;尝试运行上面程序,也可以正常输出intList集合的3个元素:它们都是普通字符串
。
通过上面介绍可以看出当程序把一个原始类型的变量赋给一个带泛型信息的变量时,只要它们的类型保持兼容——例如将List变量赋给List,无论List集合里实际包含什么类型的元素,系统都不会有任何问题。
不过需要指出的是,当把一个原始类型的变量(如List变量)赋给带泛型信息的变量(如List)时会有一个潜在的问题
:JVM会把集合里盛装的所有元素都当做Integer来处理。上面程序遍历List<Integer>集合时
,只是简单地输出每个集合元素,并未涉及集合元素的类型,因此程序并没有出现异常;否则,程序要么在运行时出现ClassCastException,要么在