2.表达式中的陷阱

写在前面

表达式是Java程序的基本组成单元,这里有一些很容易让人出错的陷阱,例如:

》当在程序中使用算术表达式时,表达式类型的自动提升,复合赋值运算符所隐含的类型转换,会给程序带来一I些潜在陷阱

》jDK1.5新增的泛型支持也有些陷阱,Java为了兼容以前不用泛型的程序,因此引入了原始类型的概念,原始类型在泛型编程中很容易致错


1.JVM对字符串的处理

Java程序中创建对象的常见方式有如下四种:

>通过new调用构造器来创建Java对象

>通过class对象的newInstance()方法调用构造器来创建Java对象

>通过Java的反序列化机制从IO流里恢复Java对象

>通过Java对象提供的clone()方法复制一个新的Java对象

除此之外。对于字符串以及Byte,Short,Integer,Long,Character,Float,Double和Boolean这些基本类型的包装类,Java还允许以直接量的方式来创建Java对象

**对于Java中的字符串直接量,JVM会使用一个字符串池来保存他们:当第一次使用某个字符串直接量时,JVM会将它放入字符串池进行缓存。在一般情况下,字符串池中的字符串对象不会被垃圾串回收,当程序再次需要使用该字符串时,无需重新创建一个新的字符串,而是直接让引用变量指向字符串池中已有的字符串

String str1="Hello java"; 
String str2="Hello java";
System.out.println(str1==str2);
//将输出true

当用字符串连接表达式来创建字符串对象,如果这个字符串连接表达式的值可以在编译的时候确定下来,那么JVM会在编译的时候计算该字符串变量的值,并让它指向字符串池中对应的字符串。

String str1="Hello java的长度:10";
String str2="Hello "+"java"+"的长度:"+10;
System.out。println(str1==str2);
//将输出true

注意:str2的所有运算数,他们都是字符串直接量,整数直接量,没有变量参与,没有方法调用。因此JVM可以在编译时就确定该字符串连接式的值,可以让该字符串变量指向字符串池中对应的字符串。

但如果程序中使用了变量,或则调用了方法 ,那么就只能等到运行时才可以确定该字符串连接表达式的值,也就无法在编译时确定该字符串变量的值,因此无法使用JVM的字符串池。

String str1="Hello java的长度:10";
String str2="Hello "+"java"+"的长度:"+"Hello java".length();
System.out.println(str1==str2);
//输出false
int len=10;
String str1="Hello java的长度:"+len;
System.out.println(str1==str3);
//将输出false

当然有一种情况例外:

如果字符串连接运算中的所有变量都可执行“宏替换”,那么JVM一样可以在编译时就确定字符串连接表达式的值,一样可以让字符串变量指向JVM字符串池中对应字符串。

String str1="Hello java的长度:10";
final int len=10;
String str1="Hello java的长度:"+len;
System.out.println(str1==str3);
//将输出true

最后:

String str="Hello"+"java,"+"crazyit.org";

这个语句创建了几个字符串对象,,答案是只创建了一个;

**因此,我们需要注意的是:当程序中需要使用字符串,基本类型包装类实例时,应该尽量使用字符串直接量,基本类型值的直接量,避免通过new String(),new Integer()形式来创建实例,这样能保证有良好的性能。


不可变的字符串

String是一个典型的不可变类,当一个String 对象创建完成后,该String类里包含的字符序列就固定下来,以后永远都不能改变

String定义的str只是一个引用类型变量,他并不是真正的String 对象,他只是指向String 对象而已。

需要指出的是,str原先指向的字符串也许以后永远都不会再用到,但这个字符串不会被垃圾回收,因为他将一直存在于字符串池中----这就是Java内存泄漏的原因之一

tip:可以使用System提供的identityHashCode()静态方法来获取str的identityHashCode值,来检验对象是否相同


如果程序需要一个字符序列会发生改变的字符串,那么应该考虑使用StringBuffer或StringBuilder

优先使用StringBuilder

两者的区别在于:

StringBuffer是线程安全的,也就是说,StringBuffer类里的绝大部分方法都增加了synchronized修饰符,对方法使用该修饰符可以保证该方法线程安全,但会降低该方法的执行效率。

在没有多线程的环境下,应该优先使用StringBuilder类来表示字符串。

程序三次打印StringBuilder对象,将看到虽然它输出不同的字符串,但程序三次输出的identityHashCode完全相同。因为str依然引用同一个StringBuilder对象。

tip:

 StringBuilder、StringBuer都代表字符序列可变的字符串,其中SstringBuilder是线程不安全的版本,StringBufer是线程安全的版本。String则代表字符序列不可改变的字符串,但Stimg不需要线程安全、线程不安全两个版本,因为String本身是不可变类,而 不可变类总是线程安全的。 

字符串比较

如果程序需要比较两个字符串是否相同,用==进行判断就可以了

但如果要判断两个字符串所包含的字符序列是否相同,则应该用String 重写过的equals0方法进行比较。

字符串底层实际上采用一个字符数组来保存该字符串所包含的字符序列

如果当前字符串和被比较字符串底层的字符数组所包含的字符序列完全相同,程序通过equals0方法判断两个字符串是否相等就会返回true。

除此之外,由于String类还实现了Comparable接口,因此程序还可通过String提供的compareTo()方法来判断两个字符串之间的大小。当两个字符串所包含的字符序列相同时,程序通过compareTo()比较,将返回0。

compareTo0方法如何判断两个字符串的大小?

它的比较规则是这样的:先将两个字符串左对齐,然后从左向右依次比较两个字符串所包含的每个字符,包含较大字符的字符串的值比较大。例如,要比较“abcx”和“ax”两个字符串的大小,程序先将他们左对齐,然后从左到右比较每个字符。


表达式类型的陷阱

表达式类型的自动提升

Java语言中的自动提升规则如下:
》所有的byte类型、short类型和char类型将被提升到int类型。

》整个算术表达式的数据类型自动提升到与表达式中最高等级操作数同样的类型。

操作数的等级排列如下所示,位于箭头右边的类型等级高于位于箭头左边的类等级。

char-->int -->long-->float-->double

byte-->short-->int -->long-->float-->double

short svalue=5

svalue= svalue-2;//无法通过编译将提示“可能损失精度”

byte b=40;

char c='a';

int i=23;

double d=b+c+i*d;

//左边算术表达式中等级最高的是d,它是double类型,因此该表达式的类型是double类型。
int val=3;

int r= 23/val;

//虽然23/val不能整除,但由于val是int类型,因此23/val表达式也是int类型。即使程序无法整除,23/val表达式的类型依然保持为int类型,因此r的值将等于7。

表达式自动转换为字符串的情形—

当基本类型的值和Srimg进行连接运算时(+也可作为连接运算符使用),系统会将基本类型的值自动转换为String类型,这样才可让连接运算正常进行。


复合赋值运算符的陷阱

short sValue=5;
sValue=sValue-2;
//因为sVulue-2表达式的类型将自动提升为i量类型,所以程序将一个类型的值照给m类型的变量(sValue)时导致了编译错误。
//但如果将上面代码改为如下形式就没有任何问题了。
short sValue=5;
sValue-=2;
也就是说:
复合赋值运算符会自动将它计算的结果值强制类型转换为其左侧变量的类
如果结果的类型与该变量的类型相同,那么这个转型不会造成任何影响。
如果结果值的类型比该变量的类型大,那么复合赋值运算符将会执行一次强制类型转这个强制类型转换将有可能导致高位“截断”。

 由此可见,复合赋值运算符简单、方便,而且具有性能上的优势,但复合赋值运算符可能有一定的危险—它潜在的隐式类型转换可能在不知不觉中导致计算结果的高位被“截断”。
为了避免这种潜在的危险,在如下几种情况下需要特别注意:
》将复合赋值运算符运用于byte、short或char等类型的变量。
》将复合赋值运算符运用于int类型的变量,而表达式右侧是long、float 或double类型的值。
》将复合赋值运算符运用于float类型的变量,而表达式右侧是double类型的值。
以上三种情况中复合从值运算符的隐式类型转换都可能导致计算结果的高位被“截断”,从而导致实际数据丢失的情形。

大部分时候,因为复合赋值运算符包含一个隐式类型转换,所以复合赋值运算符比简单赋值运算符更简洁。

Java7新增的二进制整数

》直接使用整数直接量时,系统会将它当成int类型处理。 -------int it =0b1010_1010;代表正数170
》byte类型的整数虽然可以包含8位,但最高位是符号位。------byte bt = (byte)0b1010_1010;代表-86
一表面上看,0b1010_1010只占了8位,但它已经超出了byte的取值范围,因此程序将它强制转换为byte类型时,首位会被当作符号位

把0bl010_1010整数强制转换为byte类型时,其最高位的1表示它是一个负数。

tip:计算机以补码形式来保存所有的整数。正数的补码与原码相同,负数的补码等于反码加+1。把二进制数的除符号位之外的所有位按位取反即可得到反码。

输入法导致的陷阱

注意:

Java程序中通常不能包含“全角字符”,但Java程序的字符串中完全可以包含“全角字符”,Java程序的注释中也可以包含“全角字符”。


会报错“非法字符:\12288”

转义字符的陷阱

1.慎用字符的Unicode转义形式

如:abc\u000a  代表一个换行符

2.中止行注释的转义字符



泛型可能引起的错误

原始类型变量的赋值(两个角度都会堵,哈哈哈)

第二个角度:

对于程序中的intList集合而言,它的类型是List<Intege>类型,因此编译器会认为该集合每个元素都是Integer类型,而上面程序尝试将该集合元素赋给一个String类型的变量,因编译器提示编译错误。

给出的教训有三点:
》当程序把一个原始类型的变量赋给一个带泛型信息的变量时,总是可以通过编译一只是会提示一些警告信息。
》当程序试图访问带泛型声明的集合的集合元素时,编译器总是把集合元素当成泛型类型处理——它并不关心集合里集合元素的实际类型。
》当程序试图访问带泛型声明的集合的集合元素时,JVM会遍历每个集合元素自动执行强制类型转换,如果集合元素的实际类型与集合所带的泛型信息不匹配,运行时将弓发ClassCastException异常。

原始类型带来的擦除

当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有尖括号里的类型信息都将被丢弃。比如,将一个List-String>类型的对象转型为List,则该List对集合元素的类型检查变成了类型变量的上限(即Object)。

当把一个带泛型信息的Java对象赋给不带泛型信息的变量时,Java程序会发生擦除,这种擦除不仅会擦除使用该lava类时传入的类型实参,而且会擦除所有的泛型信息,也就是擦除所有尖括号里的信息。

//当把一个具有泛型信息的对象赋给另一个相同类的变量时,此时这个对象也会丢失所有的泛型信息,常常会在编译时出现“不兼容的类型“的编译错误。

创建泛型数组的陷阱

1.List<String>[]lsa=new List<String>  不可以这样使用,报错:“创建泛型数组”

2.在定义类的时候,声明一个泛型(附加条件:该类有一个内部类A,在类的构造器中有A[] as=new A[10];),除此之外没有任何地方使用这个泛型,但仍然报错


正则表达式的陷阱

》String提供的split(String regex)方法需要的参数是正则表达式。
》正则表达式中的点号(.)可匹配任意字符。 

实际上从jdk1.4开始String类增加了一些对正则表达式的支持

具体有如下方法。
》matches(String regex):判断该字符串是否匹配指定的正则表达式。
》String replaceAl( String regex,String replacement):将字符串中所有匹配指定的正则表达式的子串替换成replacement后返回。
》String replaceFirst(String regex,String replacement):将字符串中第一个匹配指定的正则表达式的子串替换成replacement后返回。
》Stringll split(String regex):以 regex正则表达式匹配的子串作为分割符来分割该字符串。

以上四个方法都需要一个regex参数,这个参数就是正则表达式,因此使用这些方法时要特别小心。

值得留意的是:String提供了一个与replaceAll()功能相当的方法,如下所示:

》replace(CharSequence target,CharSequence replacement):将字符串中所有的target子串替换成replacement后返回。

这个普通的replace0方法不支持正则表达式,在开发中必须区别对待eplaceAl和replace0两个方法。

样例:

如果程序使用replace()方法进行替换,因为replace()方法的参数,只是普通字符串,并不是正则表达式,所以使用claz.replace(.”“/”);即可。如果使用replaceAll()方法进行替换,因为replaceAll()方法的参数是正则表达式,所以第一个参数需要写成“\\.”,其中用于生成转义的反斜线;第二个参数为“\\\\”,其中前两条斜线用于生成转义的反斜线,后两条斜线用于生成要替换的反斜线。


多线程的陷阱

首先:

从Java5开始,Java提供了三种方式来创建、启动多线程:
》继承Thread类来创建线程类,重写run()方法作为线程执行体。

》实现Runnable接口来创建线程类,重写run()方法作为线程执行体。

》实现Callable接口来创建线程类,重写call()方法作为线程执行体。

其中,第一种方式的效果最差,它有两点坏处。
》线程类继承了Thread类,无法再继承其他父类。
》因为每条线程都是Thread子类的实例,因此可以将多条线程的执行流代码与业务数据
分离。
对于第二种和第三种方式,它们的本质是一样的,只是Callable接口里包含的call0方法
既可以声明抛出异常,也可以拥有返回值。

陷阱

1.不要调用run方法

 如果程序从未调用线程对象的start()方法来启动它,那么这个线程对象将一直处于“新建状态” ,它永远也不会作为线程获得执行的机会,它只是一个普通的Java对象。当程序调用线程对象的rum)方法时,与调用普通Java对象的普通方法并无任何区别,因此绝对不会启动一条新线程。 

2.静态的同步方法

Java语法规定:任何线程进入同步方法、同步代码块之前,必须先获取同步方法、同步代码块对应的同步监视器(类或则对象)

对于同步代码块而言,程序必须显式地为它指定同步监视器;

譬如:

synchronzied(this)

{

   ....

}

this—即调用该方法的Java对象;对于静态的同步方法而言,该方法的同步监视器不是this,而是该类本身。

当类里提供了一个静态的同步方法及一个同步代码块。同步代码块使用this作为同步监视器,即这两个同步程序单元并没有使用相同的同步监视器,因此它们可以同时并发执行,相互之间不会有任何影响

静态同步方法和以当前类为同步监视器的同步代码块不能同时执行

当第一条线程(以test0()方法作为线程执行体的线程)进入同步代码块执行以后,该线程获得了对同步监视器(SynchronizedStatic类)的锁定;第二条线程(以testl0方法作为线程执行体的线程)尝试进入同步代码块执行,进入同步代码块之前,该线程必须获得对SynchronizedStatic类的锁定。因为第一条线程已经锁定了SynchronizedStatic类,在第一条线程执行结束之前,它不会释放对Synchronizedstatic类的锁定,所以第二条线程无法获得对SynchronizedStaic类的锁定,因此只有等到第一条线程执行结束后才可以切换到执行第二条线程


3.静态初始化块启动新线程执行初始化

...由于例子太长,就不再赘述,这个也很重要,需要经常复习一下

4.注意多线程执行环境,避免出现线程不安全的类

线程安全的类具有如下特征:
》该类的对象可以被多个线程安全地访问。

》每个线程调用该对象的任意方法之后都将得到正确结果。

》每个线程调用该对象的任意方法后,该对象状态仍然保持合理状态




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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值