基本概念
對象:
《Java編程思想》:按照通俗的說法,每個對象都是某個類(class)的一個實例(instance)。
引用:
《Java編程思想》: 每種編程語言都有自己的數據處理方式。有些時候,程序員必須注意將要處理的數據是什麼類型。你是直接操縱元素,還是用某種基於特殊語法的間接表示(例如C/C++裏的指針)來操作對象。所有這些在 Java 裏都得到了簡化,一切都被視爲對象。因此,我們可採用一種統一的語法。儘管將一切都“看作”對象,但操縱的標識符實際是指向一個對象的“引用”(reference)。
上面兩段是摘自《Java編程思想》中的兩段話,下面我們通過例子進行進一步說明。
先定義一個簡單類:
class People {
String name;
int age;
String sex;
}
通過這個類,我們可以創建對象
People tom = new People();
通常把這條語句的動作稱之爲創建一個對象,其實,它包含了四個動作。
- 右邊的“new People”,是以People類爲模板,在堆空間裏創建一個People類對象(也簡稱爲People對象)
- 末尾的()意味着,在對象創建後,立即調用People類的構造函數,對剛生成的對象進行初始化。構造函數是肯定有的。如果你沒寫,Java會給你補上一個默認的構造函數
- 左邊的“People tom”創建了一個People類引用變量。所謂People類引用,就是以後可以用來指向People對象的對象引用,存儲於堆棧中
- “=”操作符使對象引用指向剛創建的那個People對象
我們可以把這條語句拆成兩部分:
People tom;
tom = new People();
這樣就很清楚的看出,有兩個實體:一是對象引用變量,二是對象本身。
在堆中創建的實體,與在數據段及棧空間裏創建的實體不同,儘管它們是實實在在存在的實體,但是我們看不見摸不着。因爲其沒有具體的名字,一般都是通過對象引用指向某個對象,從而進行操作。
爲了形象的說明對象與對象引用之間的關係,我們可以將對象比作一隻氣球,而把對象引用比作是一根繩子;一根繩子可以不繫氣球,也可以系一個氣球;一個氣球也可以有多個繩子繫住
People tony;
上句定義了一個對象引用,但沒有指向任何對象
tony = tom;
上句進行了一次對象引用的複製,而不是對象的複製,結果使得tony
和tom
指向同一對象
tony = new People();
上句代碼使得tony
這個對象引用重新指向另外一個新的對象。
也就是說,
(1)一個對象引用可以指向0個或1個對象;
(2)一個對象可以有N個引用指向它
當然還有一種特殊情況,如下
new People();
該對象沒有任何對象引用指向它,這樣的對象已成爲垃圾回收器處理的對象,至於什麼時候被回收,具體就要看垃圾回收器了。
參數傳遞
把對象和對象引用搞清楚之後,我們再繼續下一個比較有意思的話題。前面講了那麼多基礎知識,都是爲了這個話題準備的 —— 參數傳值。
《thinking in Java》:When you’re passing primitives into a method,you get a distinct copy of the primitive. When you’re passing a reference into a method, you get a copy of the reference.
搞懂這段話,有關參數傳遞的疑惑將不是問題。
在討論這個話題之前,我們先擺出以下幾個事實:
- 在 Java 中永遠不會傳遞對象,而只傳遞對象引用
- Java 有且僅有一種參數傳遞機制,即按值傳遞
什麼是按值傳遞?
當將一個參數傳遞給一個函數時,函數接收的是原始值的一個副本。如果函數修改了該參數,僅改變副本,而原始值保持不變。如果函數修改了該參數,調用代碼中的原始值也隨之改變。
什麼是按引用傳遞?
當將一個參數傳遞給一個函數時,函數接收的是原始值的內存地址,而不是值的副本
C++和Java中的參數傳遞有什麼異同?
在 C++ 和 Java 中,當傳遞給函數的參數不是引用時,傳遞的都是該值的一個副本(按值傳遞)。區別在於引用:在 C++ 中當傳遞給函數的參數是引用時,您傳遞的就是這個引用,或者內存地址(按引用傳遞)。在 Java 應用程序中,當對象引用是傳遞給方法的一個參數時,您傳遞的是該引用的一個副本(按值傳遞),而不是引用本身。請注意,調用方法的對象引用和副本都指向同一個對象。這是一個重要區別。Java 應用程序在傳遞不同類型的參數時,其作法與 C++ 並無不同。Java 應用程序按值傳遞所有參數,這樣就製作所有參數的副本,而不管它們的類型。
我們通過以下實例進行講解(源自對象和引用)
class value{
public int i = 15;
}
public class dataType {
public static void main(String[] args){
dataType type = new dataType();
type.first();
}
public void first(){
int i = 5;
value v = new value();
v.i = 25;
second(v, i);
System.out.println(v.i);
}
public void second(value v, int i){
i = 0;
v.i = 20;
value val = new value();
v = val;
System.out.println(v.i + " " + i);
}
}
(1)在first內,首先程序在棧內存中開闢了一塊地址編號爲AD9500內存空間,用於存放v
的引用地址,裏邊放的值是堆內存中的一個地址,示例中的值爲BE2500
(2)調用函數second,程序在棧內開闢地址爲AD9600內存空間存放v的副本,v的副本同樣指向堆地址爲BE2500的空間,然後將v的副本傳入second,並且在second內,將v的副本所指對象i=25
改爲i=20
(3)在second內,程序新建一個對象放在地址爲BE2600的堆內,並用新的引用val(棧中地址爲AD9700)指向它,所以second中輸出結果爲:15 0
(4)但原v
並未改變,改變的只是它傳入second的副本,所以在first中仍然輸出i=20
String類型和包裝器類型的參數傳遞問題
String對象是不可變的。因爲String對象具有隻讀特性,所以指向它的任何引用都不可能改變它的值,因此,也就不會對其他的引用有什麼影響。
例1:
public class StringAsParamOfMethodDemo {
public static void main(String[] srgs){
StringAsParamOfMethodDemo demo = new StringAsParamOfMethodDemo();
demo.test();
}
private void test(){
String originStr = "origin";
System.out.println("Before change:");
System.out.println("Outter String:" + originStr);
changeString(originStr);
System.out.println("After change:");
System.out.println("Outter String:" + originStr);
}
public void changeString(String str){
str = str + "is changed";
System.out.println("Inner String:" + str);
}
}
運行結果:
Before change:
Outter String:origin
Inner String:originis changed
After change:
Outter String:origin
從運行結果上看,String對象引用作爲參數傳遞時,雖然複製了其引用並賦值給新引用,但對新引用的更改並沒有影響到原引用,這表現出了String類型的“非對象特性”。歸根到底,對String的存儲實際上是通過char[]來實現的,即String相當於是char[]的包裝類。包裝類的特質之一就是對其值進行操作時會體現出其對應的基本類型的性質。下面再舉一個包裝器類的例子進行解釋。
《Java編程思想》:基本類型具有的包裝器類,使得可以在堆中創建一個非基本對象,用來表示對應的基本類型。
包裝器類型對象跟傳統java對象有着細微的差別,導致其作爲參數傳遞時跟一般對象會不一樣。
例2:
public class IntegerAsParamOfMethodDemo {
public static void main(String[] srgs){
IntegerAsParamOfMethodDemo demo = new IntegerAsParamOfMethodDemo();
demo.test();
}
private void test(){
Integer a = new Integer(1);
System.out.println("Before change:");
System.out.println("Outter Integer:" + a);
changeInteger(a);
System.out.println("After change:");
System.out.println("Outter Integer:" + a);
}
public void changeInteger(Integer a){
a = 2;
System.out.println("Inner Integer:" + a);
}
}
運行結果:
Before change:
Outter Integer:1
Inner Integer:2
After change:
Outter Integer:1
從運行結果上來看,包裝類對象引用在作爲函數參數傳遞時表現出對應的基本類型的性質。