java「指針」 java
Java語言的一個優勢就是取消了指針的概念,但也致使了許多程序員在編程中經常忽略了對象與引用的區別,本文會試圖澄清這一律念。而且因爲Java不能 經過簡單的賦值來解決對象複製的問題,在開發過程當中,也經常要要應用clone()方法來複制對象。本文會讓你瞭解什麼是影子clone與深度clone,認識它們的區別、優勢及缺點。
看到這個標題,是否是有點困惑:Java語言明確說明取消了指針,由於指針每每是在帶來方便的同時也是致使代碼不安全的根源,同時也會使程序的變得很是復 雜難以理解,濫用指針寫成的代碼不亞於使用早已臭名昭著的"GOTO"語句。Java放棄指針的概念絕對是極其明智的。但這只是在Java語言中沒有明確 的指針定義,實質上每個new語句返回的都是一個指針的引用,只不過在大多時候Java中不用關心如何操做這個"指針",更不用象在操做C++的指針那 樣膽戰心驚。惟一要多多關心的是在給函數傳遞對象的時候。以下例程:
package reference;
class Obj{
String str = "init value";
public String toString(){
return str;
}
}
public class ObjRef{
Obj aObj = new Obj();
int aInt = 11;
public void changeObj(Obj inObj){
inObj.str = "changed value";
}
public void changePri(int inInt){
inInt = 22;
}
public static void main(String[] args)
{
ObjRef oRef = new ObjRef();
System.out.println("Before call changeObj() method: " + oRef.aObj);
oRef.changeObj(oRef.aObj);
System.out.println("After call changeObj() method: " + oRef.aObj);
System.out.println("==================Print Primtive=================");
System.out.println("Before call changePri() method: " + oRef.aInt);
oRef.changePri(oRef.aInt);
System.out.println("After call changePri() method: " + oRef.aInt);
}
}
/* RUN RESULT
Before call changeObj() method: init value
After call changeObj() method: changed value
==================Print Primtive=================
Before call changePri() method: 11
After call changePri() method: 11
*
*/
這段代碼的主要部分調用了兩個很相近的方法,changeObj()和changePri()。惟一不一樣的是它們一個把對象做爲輸入參數,另外一個把 Java中的基本類型int做爲輸入參數。而且在這兩個函數體內部都對輸入的參數進行了改動。看似同樣的方法,程序輸出的結果卻不太同樣。 changeObj()方法真正的把輸入的參數改變了,而changePri()方法對輸入的參數沒有任何的改變。
從這個例子知道Java對對象和基本的數據類型的處理是不同的。和C語言同樣,當把Java的基本數據類型(如int,char,double等)做爲 入口參數傳給函數體的時候,傳入的參數在函數體內部變成了局部變量,這個局部變量是輸入參數的一個拷貝,全部的函數體內部的操做都是針對這個拷貝的操做, 函數執行結束後,這個局部變量也就完成了它的使命,它影響不到做爲輸入參數的變量。這種方式的參數傳遞被稱爲"值傳遞"。而在Java中用對象的做爲入口 參數的傳遞則缺省爲"引用傳遞",也就是說僅僅傳遞了對象的一個"引用",這個"引用"的概念同C語言中的指針引用是同樣的。當函數體內部對輸入變量改變 時,實質上就是在對這個對象的直接操做。
除了在函數傳值的時候是"引用傳遞",在任何用"="向對象變量賦值的時候都是"引用傳遞"。如:
package reference;
class PassObj
{
String str = "init value";
}
public class ObjPassvalue
{
public static void main(String[] args)
{
PassObj objA = new PassObj();
PassObj objB = objA;
objA.str = "changed in objA";
System.out.println("Print objB.str value: " + objB.str);
}
}
/* RUN RESULT
Print objB.str value: changed in objA
*/
第一句是在內存中生成一個新的PassObj對象,而後把這個PassObj的引用賦給變量objA,第二句是把PassObj對象的引用又賦給了變量objB。此時objA和objB是兩個徹底一致的變量,之後任何對objA的改變都等同於對objB的改變。
即便明白了Java語言中的"指針"概念也許還會不經意間犯下面的錯誤。
Hashtable真的能存儲對象嗎?
看一看下面的很簡單的代碼,先是聲明瞭一個Hashtable和StringBuffer對象,而後分四次把StriingBuffer對象放入到Hashtable表中,在每次放入以前都對這個StringBuffer對象append()了一些新的字符串:
package reference;
import java.util.*;
public class HashtableAdd{
public static void main(String[] args){
Hashtable ht = new Hashtable();
StringBuffer sb = new StringBuffer();
sb.append("abc,");
ht.put("1",sb);
sb.append("def,");
ht.put("2",sb);
sb.append("mno,");
ht.put("3",sb);
sb.append("xyz.");
ht.put("4",sb);
int numObj=0;
Enumeration it = ht.elements();
while(it.hasMoreElements()){
System.out.print("get StringBufffer "+(++numObj)+" from Hashtable: ");
System.out.println(it.nextElement());
}
}
}
若是你認爲輸出的結果是:
get StringBufffer 1 from Hashtable: abc,
get StringBufffer 2 from Hashtable: abc,def,
get StringBufffer 3 from Hashtable: abc,def,mno,
get StringBufffer 4 from Hashtable: abc,def,mno,xyz.
那麼你就要回過頭再仔細看一看上一個問題了,把對象時做爲入口參數傳給函數,實質上是傳遞了對象的引用,向Hashtable傳遞 StringBuffer對象也是隻傳遞了這個StringBuffer對象的引用!每一次向Hashtable表中put一次 StringBuffer,並無生成新的StringBuffer對象,只是在Hashtable表中又放入了一個指向同一StringBuffer對 象的引用而已。
對Hashtable表存儲的任何一個StringBuffer對象(更確切的說應該是對象的引用)的改動,實際上都是對同一個 "StringBuffer"的改動。因此Hashtable並不能真正存儲能對象,而只能存儲對象的引用。也應該知道這條原則對與Hashtable相 似的Vector, List, Map, Set等都是同樣的。
上面的例程的實際輸出的結果是:
/* RUN RESULT
get StringBufffer 1 from Hashtable: abc,def,mno,xyz.
get StringBufffer 2 from Hashtable: abc,def,mno,xyz.
get StringBufffer 3 from Hashtable: abc,def,mno,xyz.
get StringBufffer 4 from Hashtable: abc,def,mno,xyz.
*/ 程序員
2.類,對象與引用 編程
Java最基本的概念就是類,類包括函數和變量。若是想要應用類,就要把類生成對象,這個過程被稱做"類的實例化"。有幾種方法把類實例化成對象,最經常使用 的就是用"new"操做符。類實例化成對象後,就意味着要在內存中佔據一塊空間存放實例。想要對這塊空間操做就要應用到對象的引用。引用在Java語言中 的體現就是變量,而變量的類型就是這個引用的對象。雖然在語法上能夠在生成一個對象後直接調用該對象的函數或變量,如:
new String("Hello NDP")).substring(0,3)//RETURN RESULT: Hel
但因爲沒有相應的引用,對這個對象的使用也只能侷限這條語句中了。
產生:引用老是在把對象做參數"傳遞"的過程當中自動發生,不須要人爲的產生,也不能人爲的控制引用的產生。這個傳遞包括把對象做爲函數的入口參數的狀況,也包括用"="進行對象賦值的時候。
範圍:只有局部的引用,沒有局部的對象。引用在Java語言的體現就是變量,而變量在Java語言中是有範圍的,能夠是局部的,也能夠是全局的。
生存期:程序只能控制引用的生存週期。對象的生存期是由Java控制。用"new Object()"語句生成一個新的對象,是在計算機的內存中聲明一塊區域存儲對象,只有Java的垃圾收集器才能決定在適當的時候回收對象佔用的內存。
沒有辦法阻止對引用的改動。 安全
3.java中的clone app
3.1.什麼是"clone"? 函數
在實際編程過程當中,咱們經常要遇到這種狀況:有一個對象A,在某一時刻A中已經包含了一些有效值,此時可能會須要一個和A徹底相同新對象B,而且此後對B 任何改動都不會影響到A中的值,也就是說,A與B是兩個獨立的對象,但B的初始值是由A對象肯定的。在Java語言中,用簡單的賦值語句是不能知足這種需 求的。要知足這種需求雖然有不少途徑,但實現clone()方法是其中最簡單,也是最高效的手段。
Java的全部類都默認繼承java.lang.Object類,在java.lang.Object類中有一個方法clone()。JDK API的說明文檔解釋這個方法將返回Object對象的一個拷貝。要說明的有兩點:一是拷貝對象返回的是一個新對象,而不是一個引用。二是拷貝對象與用 new操做符返回的新對象的區別就是這個拷貝已經包含了一些原來對象的信息,而不是對象的初始信息。 spa
3.2.怎樣應用clone()方法? 指針
一個很典型的調用clone()代碼以下:
class CloneClass implements Cloneable{
public int aInt;
public Object clone(){
CloneClass o = null;
try{
o = (CloneClass)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return o;
}
}
有三個值得注意的地方, 對象
一是但願能實現clone功能的CloneClass類實現了Cloneable接口,這個接口屬於java.lang包, java.lang包已經被缺省的導入類中,因此不須要寫成java.lang.Cloneable。 繼承
另外一個值得請注意的是重載了clone()方法。最 後在clone()方法中調用了super.clone(),這也意味着不管clone類的繼承結構是什麼樣的,super.clone()直接或間接調 用了java.lang.Object類的clone()方法。下面再詳細的解釋一下這幾點。
應該說第三點是最重要的,仔細觀察一下Object類的clone()一個native方法,native方法的效率通常來講都是遠高於java中的非 native方法。這也解釋了爲何要用Object中clone()方法而不是先new一個類,而後把原始對象中的信息賦到新對象中,雖然這也實現了 clone功能。對於第二點,也要觀察Object類中的clone()仍是一個protected屬性的方法。這也意味着若是要應用clone()方 法,必須繼承Object類,在Java中全部的類是缺省繼承Object類的,也就不用關心這點了。而後重載clone()方法。還有一點要考慮的是爲 了讓其它類能調用這個clone類的clone()方法,重載以後要把clone()方法的屬性設置爲public。 、
那麼clone類爲何還要實現Cloneable接口呢?稍微注意一下,Cloneable接口是不包含任何方法的!其實這個接口僅僅是一個標誌,並且 這個標誌也僅僅是針對Object類中clone()方法的,若是clone類沒有實現Cloneable接口,並調用了Object的clone()方法(也就是調用了super.Clone()方法),那麼Object的clone()方法就會拋出 CloneNotSupportedException異常。
以上是clone的最基本的步驟,想要完成一個成功的clone,還要了解什麼是"影子clone"和"深度clone"。
3.3什麼是影子clone?
下面的例子包含三個類UnCloneA,CloneB,CloneMain。CloneB類包含了一個UnCloneA的實例和一個int類型變量,而且 重載clone()方法。CloneMain類初始化UnCloneA類的一個實例b1,而後調用clone()方法生成了一個b1的拷貝b2。最後考察 一下b1和b2的輸出:
package clone;
class UnCloneA {
private int i;
public UnCloneA(int ii) { i = ii; }
public void doublevalue() { i *= 2; }
public String toString() {
return Integer.toString(i);
}
}
class CloneB implements Cloneable{
public int aInt;
public UnCloneA unCA = new UnCloneA(111);
public Object clone(){
CloneB o = null;
try{
o = (CloneB)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return o;
}
}
public class CloneMain {
public static void main(String[] a){
CloneB b1 = new CloneB();
b1.aInt = 11;
System.out.println("before clone,b1.aInt = "+ b1.aInt);
System.out.println("before clone,b1.unCA = "+ b1.unCA);
CloneB b2 = (CloneB)b1.clone();
b2.aInt = 22;
b2.unCA.doublevalue();
System.out.println("=================================");
System.out.println("after clone,b1.aInt = "+ b1.aInt);
System.out.println("after clone,b1.unCA = "+ b1.unCA);
System.out.println("=================================");
System.out.println("after clone,b2.aInt = "+ b2.aInt);
System.out.println("after clone,b2.unCA = "+ b2.unCA);
}
}
/** RUN RESULT:
before clone,b1.aInt = 11
before clone,b1.unCA = 111
=================================
after clone,b1.aInt = 11
after clone,b1.unCA = 222
=================================
after clone,b2.aInt = 22
after clone,b2.unCA = 222
*/
輸出的結果說明int類型的變量aInt和UnCloneA的實例對象unCA的clone結果不一致,int類型是真正的被clone了,由於改變了 b2中的aInt變量,對b1的aInt沒有產生影響,也就是說,b2.aInt與b1.aInt已經佔據了不一樣的內存空間,b2.aInt是 b1.aInt的一個真正拷貝。相反,對b2.unCA的改變同時改變了b1.unCA,很明顯,b2.unCA和b1.unCA是僅僅指向同一個對象的 不一樣引用!從中能夠看出,調用Object類中clone()方法產生的效果是:先在內存中開闢一塊和原始對象同樣的空間,而後原樣拷貝原始對象中的內 容。對基本數據類型,這樣的操做是沒有問題的,但對非基本類型變量,咱們知道它們保存的僅僅是對象的引用,這也致使clone後的非基本類型變量和原始對 象中相應的變量指向的是同一個對象。
大多時候,這種clone的結果每每不是咱們所但願的結果,這種clone也被稱爲"影子clone"。要想讓b2.unCA指向與b2.unCA不一樣的對象,並且b2.unCA中還要包含b1.unCA中的信息做爲初始信息,就要實現深度clone。
3.4怎麼進行深度clone?
把上面的例子改爲深度clone很簡單,須要兩個改變:一是讓UnCloneA類也實現和CloneB類同樣的clone功能(實現Cloneable接 口,重載clone()方法)。二是在CloneB的clone()方法中加入一句o.unCA = (UnCloneA)unCA.clone();
程序以下:
package clone.ext;
class UnCloneA implements Cloneable{
private int i;
public UnCloneA(int ii) { i = ii; }
public void doublevalue() { i *= 2; }
public String toString() {
return Integer.toString(i);
}
public Object clone(){
UnCloneA o = null;
try{
o = (UnCloneA)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return o;
}
}
class CloneB implements Cloneable{
public int aInt;
public UnCloneA unCA = new UnCloneA(111);
public Object clone(){
CloneB o = null;
try{
o = (CloneB)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
o.unCA = (UnCloneA)unCA.clone();
return o;
}
}
public class CloneMain {
public static void main(String[] a){
CloneB b1 = new CloneB();
b1.aInt = 11;
System.out.println("before clone,b1.aInt = "+ b1.aInt);
System.out.println("before clone,b1.unCA = "+ b1.unCA);
CloneB b2 = (CloneB)b1.clone();
b2.aInt = 22;
b2.unCA.doublevalue();
System.out.println("=================================");
System.out.println("after clone,b1.aInt = "+ b1.aInt);
System.out.println("after clone,b1.unCA = "+ b1.unCA);
System.out.println("=================================");
System.out.println("after clone,b2.aInt = "+ b2.aInt);
System.out.println("after clone,b2.unCA = "+ b2.unCA);
}
}
/** RUN RESULT:
before clone,b1.aInt = 11
before clone,b1.unCA = 111
=================================
after clone,b1.aInt = 11
after clone,b1.unCA = 111
=================================
after clone,b2.aInt = 22
after clone,b2.unCA = 222
*/
能夠看出,如今b2.unCA的改變對b1.unCA沒有產生影響。此時b1.unCA與b2.unCA指向了兩個不一樣的UnCloneA實例,並且在 CloneB b2 = (CloneB)b1.clone();調用的那一刻b1和b2擁有相同的值,在這裏,b1.i = b2.i = 11。
要知道不是全部的類都能實現深度clone的。例如,若是把上面的CloneB類中的UnCloneA類型變量改爲StringBuffer類型,看一下 JDK API中關於StringBuffer的說明,StringBuffer沒有重載clone()方法,更爲嚴重的是StringBuffer仍是一個 final類,這也是說咱們也不能用繼承的辦法間接實現StringBuffer的clone。若是一個類中包含有StringBuffer類型對象或和 StringBuffer類似類的對象,咱們有兩種選擇:要麼只能實現影子clone,要麼就在類的clone()方法中加一句(假設是 SringBuffer對象,並且變量名還是unCA): o.unCA = new StringBuffer(unCA.toString()); //原來的是:o.unCA = (UnCloneA)unCA.clone();
還要知道的是除了基本數據類型能自動實現深度clone之外,String對象,Integer,Double等是一個例外,它clone後的表現好象也實現了深度clone,雖然這只是一個假象,但卻大大方便了咱們的編程。
Clone中String和StringBuffer的區別
應該說明的是,這裏不是着重說明String和StringBuffer的區別,但從這個例子裏也能看出String類的一些不同凡響的地方。
下面的例子中包括兩個類,CloneC類包含一個String類型變量和一個StringBuffer類型變量,而且實現了clone()方法。在 StrClone類中聲明瞭CloneC類型變量c1,而後調用c1的clone()方法生成c1的拷貝c2,在對c2中的String和 StringBuffer類型變量用相應的方法改動以後打印結果:
package clone;
class CloneC implements Cloneable{
public String str;
public StringBuffer strBuff;
public Object clone(){
CloneC o = null;
try{
o = (CloneC)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return o;
}
}
public class StrClone {
public static void main(String[] a){
CloneC c1 = new CloneC();
c1.str = new String("initializeStr");
c1.strBuff = new StringBuffer("initializeStrBuff");
System.out.println("before clone,c1.str = "+ c1.str);
System.out.println("before clone,c1.strBuff = "+ c1.strBuff);
CloneC c2 = (CloneC)c1.clone();
c2.str = c2.str.substring(0,5);
c2.strBuff = c2.strBuff.append(" change strBuff clone");
System.out.println("=================================");
System.out.println("after clone,c1.str = "+ c1.str);
System.out.println("after clone,c1.strBuff = "+ c1.strBuff);
System.out.println("=================================");
System.out.println("after clone,c2.str = "+ c2.str);
System.out.println("after clone,c2.strBuff = "+ c2.strBuff);
}
}
/* RUN RESULT
before clone,c1.str = initializeStr
before clone,c1.strBuff = initializeStrBuff
=================================
after clone,c1.str = initializeStr
after clone,c1.strBuff = initializeStrBuff change strBuff clone
=================================
after clone,c2.str = initi
after clone,c2.strBuff = initializeStrBuff change strBuff clone
*
*/
打印的結果能夠看出,String類型的變量好象已經實現了深度clone,由於對c2.str的改動並無影響到c1.str!難道Java把 Sring類當作了基本數據類型?其實否則,這裏有一個小小的把戲,祕密就在於c2.str = c2.str.substring(0,5)這一語句!實質上,在clone的時候c1.str與c2.str仍然是引用,並且都指向了同一個 String對象。但在執行c2.str = c2.str.substring(0,5)的時候,它做用至關於生成了一個新的String類型,而後又賦回給c2.str。這是由於String被 Sun公司的工程師寫成了一個不可更改的類(immutable class),在全部String類中的函數都不能更改自身的值。下面給出很簡單的一個例子:
package clone; public class StrTest { public static void main(String[] args) { String str1 = "This is a test for immutable"; String str2 = str1.substring(0,8); System.out.println("print str1 : " + str1); System.out.println("print str2 : " + str2); } } /* RUN RESULT print str1 : This is a test for immutable print str2 : This is */
例子中,雖然str1調用了substring()方法,但str1的值並無改變。相似的,String類中的其它方法也是如此。固然若是咱們把最上面的例子中的這兩條語句
c2.str = c2.str.substring(0,5);
c2.strBuff = c2.strBuff.append(" change strBuff clone");
改爲下面這樣:
c2.str.substring(0,5);
c2.strBuff.append(" change strBuff clone");
去掉了從新賦值的過程,c2.str也就不能有變化了,咱們的把戲也就露餡了。但在編程過程當中只調用
c2.str.substring(0,5); 語句是沒有任何意義的。
應該知道的是在Java中全部的基本數據類型都有一個相對應的類,象Integer類對應int類型,Double類對應double類型等等,這些類也 與String類相同,都是不能夠改變的類。也就是說,這些的類中的全部方法都是不能改變其自身的值的。這也讓咱們在編clone類的時候有了一個更多的 選擇。同時咱們也能夠把本身的類編成不可更改的類。