Java語言細節
Java作為一門優秀的面向對象的程序設計語言,正在被越來越多的人使用。本文試圖列出作者在實際開發中碰到的一些Java語言的容易被人忽視的細節,希望能給正在學習Java語言的人有所幫助。
1,位移運算越界怎么處理
考察下面的代碼輸出結果是多少?
int a=5;
System.out.println(a<<33);
按照常理推測,把a左移33位應該將a的所有有效位都移出去了,那剩下的都是零啊,所以輸出結果應該是0才對啊,可是執行后發現輸出結果是10,為什么
呢?因為Java語言對位移運算作了優化處理,Java語言對a<<b轉化為a<<(b%32)來處理,所以當要移位的位數b超
過32時,實際上移位的位數是b%32的值,那么上面的代碼中a<<33相當于a<<1,所以輸出結果是10。
2,可以讓i!=i嗎?
當你看到這個命題的時候一定會以為我瘋了,或者Java語言瘋了。這看起來是絕對不可能的,一個數怎么可能不等于它自己呢?或許就真的是Java語言瘋了,不信看下面的代碼輸出什么?
double i=0.0/0.0;
if(i==i){
System.out.println("Yes i==i");
}else{
System.out.println("No i!=i");
}
上面的代碼輸出"No i!=i",為什么會這樣呢?關鍵在0.0/0.0這個值,在IEEE
754浮點算術規則里保留了一個特殊的值用來表示一個不是數字的數量。這個值就是NaN("Not a
Number"的縮寫),對于所有沒有良好定義的浮點計算都將得到這個值,比如:0.0/0.0;其實我們還可以直接使用Double.NaN來得到這個
值。在IEEE 754規范里面規定NaN不等于任何值,包括它自己。所以就有了i!=i的代碼。
3,怎樣的equals才安全?
我們都知道在Java規范里定義了equals方法覆蓋的5大原則:reflexive(反身性),symmetric(對稱性),transitive(傳遞性),consistent(一致性),non-null(非空性)。那么考察下面的代碼:
public class Student{
private String name;
private int age;
public Student(String name,int age){
this.name=name;
this.age=age;
}
public boolean equals(Object obj){
if(obj instanceof Student){
Student s=(Student)obj;
if(s.name.equals(this.name) && s.age==this.age){
return true;
}
}
return super.equals(obj);
}
}
你認為上面的代碼equals方法的覆蓋安全嗎?表面看起來好像沒什么問題,這樣寫也確實滿足了以上的五大原則。但其實這樣的覆蓋并不很安全,假如
Student類還有一個子類CollegeStudent,如果我拿一個Student對象和一個CollegeStudent對象equals,只要
這兩個對象有相同的name和age,它們就會被認為相等,但實際上它們是兩個不同類型的對象啊。問題就出在instanceof這個運算符上,因為這個
運算符是向下兼容的,也就是說一個CollegeStudent對象也被認為是一個Student的實例。怎樣去解決這個問題呢?那就只有不用
instanceof運算符,而使用對象的getClass()方法來判斷兩個對象是否屬于同一種類型,例如,將上面的equals()方法修改為:
public boolean equals(Object obj){
if(obj.getClass()==Student.class){
Student s=(Student)obj;
if(s.name.equals(this.name) && s.age==this.age){
return true;
}
}
return super.equals(obj);
}
這樣才能保證obj對象一定是Student的實例,而不會是Student的任何子類的實例。
4,淺復制與深復制
1)淺復制與深復制概念
⑴淺復制(淺克隆)
被復制對象的所有變量都含有與原來的對象相同的值,而所有的對其他對象的引用仍然指向原來的對象。換言之,淺復制僅僅復制所考慮的對象,而不復制它所引用的對象。
⑵深復制(深克隆)
被復制對象的所有變量都含有與原來的對象相同的值,除去那些引用其他對象的變量。那些引用其他對象的變量將指向被復制過的新對象,而不再是原有的那些被引用的對象。換言之,深復制把要復制的對象所引用的對象都復制了一遍。
2)Java的clone()方法
⑴clone方法將對象復制了一份并返回給調用者。一般而言,clone()方法滿足:
①對任何的對象x,都有x.clone() !=x//克隆對象與原對象不是同一個對象
②對任何的對象x,都有x.clone().getClass()= =x.getClass()//克隆對象與原對象的類型一樣
③如果對象x的equals()方法定義恰當,那么x.clone().equals(x)應該成立。
⑵Java中對象的克隆
①為了獲取對象的一份拷貝,我們可以利用Object類的clone()方法。
②在派生類中覆蓋基類的clone()方法,并聲明為public。
③在派生類的clone()方法中,調用super.clone()。
④在派生類中實現Cloneable接口。
請看如下代碼:
class Student implements Cloneable{
String name;
int age;
Student(String name,int age){
this.name=name;
this.age=age;
}
public Object clone(){
Object obj=null;
try{
obj=(Student)super.clone();
//Object中的clone()識別出你要復制的是哪一個對象。
}
catch(CloneNotSupportedException e){
e.printStackTrace();
}
return obj;
}
}
public static void main(String[] args){
Student s1=new Student("zhangsan",18);
Student s2=(Student)s1.clone();
s2.name="lisi";
s2.age=20;
System.out.println("name="+s1.name+","+"age="+s1.age);//修改學生2
//后,不影響學生1的值。
}
說明:
①為什么我們在派生類中覆蓋Object的clone()方法時,一定要調用super.clone()呢?在運行時刻,Object中的clone()
識別出你要復制的是哪一個對象,然后為此對象分配空間,并進行對象的復制,將原始對象的內容一一復制到新對象的存儲空間中。
②繼承自java.lang.Object類的clone()方法是淺復制。以下代碼可以證明之。
class Teacher{
String name;
int age;
Teacher(String name,int age){
this.name=name;
this.age=age;
}
}
class Student implements Cloneable{
String name;
int age;
Teacher t;//學生1和學生2的引用值都是一樣的。
Student(String name,int age,Teacher t){
this.name=name;
this.age=age;
this.t=t;
}
public Object clone(){
Student stu=null;
try{
stu=(Student)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
stu.t=(Teacher)t.clone();
return stu;
}
public static void main(String[] args){
Teacher t=new Teacher("tangliang",30);
Student s1=new Student("zhangsan",18,t);
Student s2=(Student)s1.clone();
s2.t.name="tony";
s2.t.age=40;
System.out.println("name="+s1.t.name+","+"age="+s1.t.age);
//學生1的老師成為tony,age為40。
}
}
那應該如何實現深層次的克隆,即修改s2的老師不會影響s1的老師?代碼改進如下。
class Teacher implements Cloneable{
String name;
int age;
Teacher(String name,int age){
this.name=name;
this.age=age;
}
public Object clone(){
Object obj=null;
try{
obj=super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return obj;
}
}
class Student implements Cloneable{
String name;
int age;
Teacher t;
Student(String name,int age,Teacher t){
this.name=name;
this.age=age;
this.t=t;
}
public Object clone(){
Student stu=null;
try{
stu=(Student)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
stu.t=(Teacher)t.clone();
return stu;
}
}
public static void main(String[] args){
Teacher t=new Teacher("tangliang",30);
Student s1=new Student("zhangsan",18,t);
Student s2=(Student)s1.clone();
s2.t.name="tony";
s2.t.age=40;
System.out.println("name="+s1.t.name+","+"age="+s1.t.age);
//學生1的老師不改變。
}
3)利用串行化來做深復制
把對象寫到流里的過程是串行化(Serilization)過程,Java程序員又非常形象地稱為“冷凍”或者“腌咸菜(picking)”過程;而把對
象從流中讀出來的并行化(Deserialization)過程則叫做“解凍”或者“回鮮(depicking)”過程。應當指出的是,寫在流里的是對象
的一個拷貝,而原對象仍然存在于JVM里面,因此“腌成咸菜”的只是對象的一個拷貝,Java咸菜還可以回鮮。
在Java語言里深復制一個對象,常常可以先使對象實現Serializable接口,然后把對象(實際上只是對象的一個拷貝)寫到一個流里(腌成咸菜),再從流里讀出來(把咸菜回鮮),便可以重建對象。
如下為深復制源代碼。
public Object deepClone(){
//將對象寫到流里
ByteArrayOutoutStream bo=new ByteArrayOutputStream();
ObjectOutputStream oo=new ObjectOutputStream(bo);
oo.writeObject(this);
//從流里讀出來
ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi=new ObjectInputStream(bi);
return(oi.readObject());
}
這樣做的前提是對象以及對象內部所有引用到的對象都是可串行化的,否則,就需要仔細考察那些不可串行化的對象可否設成transient,從而將之排除在復制過程之外。上例代碼改進如下。
class Teacher implements Serializable{
String name;
int age;
Teacher(String name,int age){
this.name=name;
this.age=age;
}
}
class Student implements Serializable
{
String name;//常量對象。
int age;
Teacher t;//學生1和學生2的引用值都是一樣的。
Student(String name,int age,Teacher t){
this.name=name;
this.age=age;
this.p=p;
}
public Object deepClone() throws IOException,
OptionalDataException,ClassNotFoundException
{
//將對象寫到流里
ByteArrayOutoutStream bo=new ByteArrayOutputStream();
ObjectOutputStream oo=new ObjectOutputStream(bo);
oo.writeObject(this);
//從流里讀出來
ByteArrayInputStream bi=new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi=new ObjectInputStream(bi);
return(oi.readObject());
}
}
public static void main(String[] args){
Teacher t=new Teacher("tangliang",30);
Student s1=new Student("zhangsan",18,t);
Student s2=(Student)s1.deepClone();
s2.t.name="tony";
s2.t.age=40;
System.out.println("name="+s1.t.name+","+"age="+s1.t.age);
//學生1的老師不改變。
}
5,String類和對象池 我們知道得到String對象有兩種辦法: String str1="hello"; String str2=new
String("hello");
這兩種創建String對象的方法有什么差異嗎?當然有差異,差異就在于第一種方法在對象池中拿對象,第二種方法直接生成新的對象。在JDK5.0里面,
Java虛擬機在啟動的時候會實例化9個對象池,這9個對象池分別用來存儲8種基本類型的包裝類對象和String對象。當我們在程序中直接用雙引號括起
來一個字符串時,JVM就到String的對象池里面去找看是否有一個值相同的對象,如果有,就拿現成的對象,如果沒有就在對象池里面創建一個對象,并返
回。所以我們發現下面的代碼輸出true: String str1="hello"; String str2="hello";
System.out.println(str1==str2);
這說明str1和str2指向同一個對象,因為它們都是在對象池中拿到的,而下面的代碼輸出為false: String str3="hello"
String str4=new String("hello"); System.out.println(str3==str4);
因為在任何情況下,只要你去new一個String對象那都是創建了新的對象。
與此類似的,在JDK5.0里面8種基本類型的包裝類也有這樣的差異: Integer i1=5;//在對象池中拿 Integer i2
=5;//所以i1==i2 Integer i3=new Integer(5);//重新創建新對象,所以i2!=i3
對象池的存在是為了避免頻繁的創建和銷毀對象而影響系統性能,那我們自己寫的類是否也可以使用對象池呢?當然可以,考察以下代碼: class
Student{ private String name; private int age; private static HashSet pool=new HashSet();//對象池
public Student(String name,int age){
this.name=name;
this.age=age;
}
//使用對象池來得到對象的方法
public static Student newInstance(String name,int age){
//循環遍歷對象池
for(Student stu:pool){
if(stu.name.equals(name) && stu.age==age){
return stu;
}
}
//如果找不到值相同的Student對象,則創建一個Student對象
//并把它加到對象池中然后返回該對象。
Student stu=new Student(name,age);
pool.add(stu);
return stu;
}
}
public class Test{
public static void main(String[] args){
Student stu1=Student.newInstance("tangliang",30);//對象池中拿
Student stu2=Student.newInstance("tangliang",30);//所以stu1==stu2
Student stu3=new Student("tangliang",30);//重新創建,所以stu1!=stu3
System.out.println(stu1==stu2);
System.out.println(stu1==stu3);
}
}
6,2.0-1.1==0.9嗎? 考察下面的代碼: double a=2.0,b=1.1,c=0.9; if(a-b==c){
System.out.println("YES!"); }else{ System.out.println("NO!"); }
以上代碼輸出的結果是多少呢?你認為是“YES!”嗎?那么,很遺憾的告訴你,不對,Java語言再一次cheat了你,以上代碼會輸出“NO!”。為什
么會這樣呢?其實這是由實型數據的存儲方式決定的。我們知道實型數據在內存空間中是近似存儲的,所以2.0-1.1的結果不是0.9,而是
0.88888888889。所以在做實型數據是否相等的判斷時要非常的謹慎。一般來說,我們不建議在代碼中直接判斷兩個實型數據是否相等,如果一定要比
較是否相等的話我們也采用以下方式來判斷: if(Math.abs(a-b)<1e-5){ //相等 }else{ //不相等 }
上面的代碼判斷a與b之差的絕對值是否小于一個足夠小的數字,如果是,則認為a與b相等,否則,不相等。
7,判斷奇數 以下的方法判斷某個整數是否是奇數,考察是否正確: public boolean isOdd(int n){ return
(n%2==1); }
很多人認為上面的代碼沒問題,但實際上這段代碼隱藏著一個非常大的BUG,當n的值是正整數時,以上的代碼能夠得到正確結果,但當n的值是負整數時,以上
方法不能做出正確判斷。例如,當n=-3時,以上方法返回false。因為根據Java語言規范的定義,Java語言里的求余運算符(%)得到的結果與運
算符左邊的值符號相同,所以,-3%2的結果是-1,而不是1。那么上面的方法正確的寫法應該是: public boolean isOdd(int
n){ return (n%2!=0); }
8,拓寬數值類型會造成精度丟失嗎?
Java語言的8種基本數據類型中7種都可以看作是數值類型,我們知道對于數值類型的轉換有一個規律:從窄范圍轉化成寬范圍能夠自動類型轉換,反之則必須
強制轉換。請看下圖:
byte-->short-->int-->long-->float-->double
char-->int
我們把順箭頭方向的轉化叫做拓寬類型,逆箭頭方向的轉化叫做窄化類型。一般我們認為因為順箭頭方向的轉化不會有數據和精度的丟失,所以Java語言允許自
動轉化,而逆箭頭方向的轉化可能會造成數據和精度的丟失,所以Java語言要求程序員在程序中明確這種轉化,也就是強制轉換。那么拓寬類型就一定不會造成
數據和精度丟失嗎?請看下面代碼:
int i=2000000000;
int num=0;
for(float f=i;f
9,i=i+1和i+=1完全等價嗎?
可能有很多程序員認為i+=1只是i=i+1的簡寫方式,其實不然,它們一個使用簡單賦值運算,一個使用復合賦值運算,而簡單賦值運算和復合賦值運算的最
大差別就在于:復合賦值運算符會自動地將運算結果轉型為其左操作數的類型。看看以下的兩種寫法,你就知道它們的差別在哪兒了:
(1) byte i=5;
i+=1;
(2) byte i=5;
i=i+1;
第一種寫法編譯沒問題,而第二種寫法卻編譯通不過。原因就在于,當使用復合賦值運算符進行操作時,即使右邊算出的結果是int類型,系統也會將其值轉化為
左邊的byte類型,而使用簡單賦值運算時沒有這樣的優待,系統會認為將i+1的值賦給i是將int類型賦給byte,所以要求強制轉換。理解了這一點
后,我們再來看一個例子:
byte b=120;
b+=20;
System.out.println("b="+b);
說到這里你應該明白了,上例中輸出b的值不是140,而是-116。因為120+20的值已經超出了一個byte表示的范圍,而當我們使用復合賦值運
算時系統會自動作類型的轉化,將140強轉成byte,所以得到是-116。由此可見,在使用復合賦值運算符時還得小心,因為這種類型轉換是在不知不覺中
進行的,所以得到的結果就有可能和你的預想不一樣。
下面引用由zhangyue在 2007/09/01 09:07pm 發表的內容:
唐老師:
long類型為什么能自動轉換成float類型啊!!
long是64bits;
float是32bits;
...
long
能自動轉換為float,但這種轉換會造成精度的丟失,float中只保留了原來long類型的低24位的數據。這是Java語言中三種基本類型的自動轉
換會造成精度丟失的情況之一,另兩種情況是int-->float 和long-->double,詳情請參考:
8,拓寬數值類型會造成精度丟失嗎?
下面引用由plastrio在 2007/09/01 09:08am 發表的內容:
請教個問題 您給我們0703講線程時 有段課堂代碼為什么 用synchronized(Object obj) 鎖代碼塊而不是一般教程上講的 synchronized(this)
當兩個線程在運行時this不代表同一個對象時就不能用synchronized(this)來鎖。必須定義一個唯一的公共對象來聲明,如:synchronized(obj)
例如:
public class ThreadTest{
public static void main(String[] args){
Thread t1=new Thread(new ThreadA());
Thread t2=new Thread(new ThreadA());//兩個線程對應兩個不同的ThreadA對象。
t1.start();
t2.start();
}
}
class ThreadA implements Runnable{
static int i=0;
static Object obj=new Object();
public run(){
while(i<20){
synchronized(obj){//兩個線程在執行時所引用的this不是同一個對象,所以寫this就不能達到鎖的目的。
System.out.println(i);
i++;
}
}
}
}
在java語言中,位移操作共分三種,左位移(<<),右位移(>>)和無符號右位移(>>>)。如果將位移
運算表示為公式的話,即n operator
s。其中,operator表示上述的三種位移操作之一;n和s表示操作數,必須是可以轉化成int類型的,否則出現運行時錯誤。n是原始數值,s表示位
移距離。該公式的含義是n按照operator運算符含義位移s位。位移的距離使用掩碼32(類似于子網掩碼),即位移距離總是在0~31之間,超出這個
范圍的位移距離(包括負數)會被轉化在這個范圍里。也就是說真正的位移距離是n%32,所以唐老師的位移距離33實際上是1。n<<s的結果
(無論是否溢出)總是等價于n與2的n%32次冪的乘積。在唐老師的例子里面,位移距離是33%32即1,2的1次冪是2,5與2的乘積是10.所以最終
結果是10。對于右位移操作n<<s的結果(無論是否溢出)總是等價于n與2的n%32次冪的商。(以上內容參考java規范15.9)