ThinkinginJava4:一切都是物件

柳葉一刀發表於2017-12-21

“儘管以C++為基礎,但Java是一種更純粹的物件導向程式設計語言”。

1.用控制程式碼操縱物件

在Java的世界裡,任何東西都可看作物件,但操縱的識別符號實際是指向一個物件的“控制程式碼”(Handle)或稱作一個“引用”。

為了便於理解,可將這一情形想象成用遙控板(控制程式碼)操縱電視機(物件),只要握住這個遙控板,就相當於掌握了與電視機連線的通道;一旦需要“換頻道”或者“調聲音”,實際操縱的是遙控板(控制程式碼),再由遙控板去操縱電視機(物件);如果要在房間裡四處走走,並想保持對電視機的控制,那麼手上拿著的是遙控板,而非電視機。

此外,即使沒有電視機,遙控板亦可獨立存在。也就是說,擁有一個控制程式碼,並不表示必須要有一個物件同它連線。

String s;

上述建立的只是控制程式碼,並不是物件,若此時向s傳送一條訊息,就會獲得一個錯誤(執行期),這是由於s實際並未與任何東西連線(即“沒有電視機”)。

因此,一種更安全的做法是:建立一個控制程式碼時,記住無論如何都要進行初始化:

Strings="asdf";

2.所有物件都必須建立

建立控制程式碼時,我們希望它同一個新物件連線,通常用new關鍵字達到這一目的,所以在上面的例子中,可以用:

Strings=newString("asdf");

它不僅指出“將我變成一個新字串”,也通過提供一個初始字串,指出了“如何生成這個新字串”。

3.資料儲存到什麼地方?

程式執行時,最好對資料儲存到什麼地方做到心中有數,特別要注意的是記憶體的分配,有六個地方都可以儲存資料:

(1) 暫存器

這是最快的儲存區域,位於處理器(CPU)內部,但是,暫存器的數量十分有限,它是根據需要由編譯器分配,我們對此沒有直接的控制權,也不可能在自己的程式裡找到暫存器存在的任何蹤跡。

(2) 堆疊

駐留於常規RAM(隨機訪問儲存器)區域,但可通過它的“堆疊指標”獲得處理的直接支援。這是一種特別快、特別有效的資料儲存方式,僅次於暫存器。建立程式時,Java編譯器必須準確地知道堆疊內儲存的所有資料的“長度”以及“存在時間”,這一限制無疑影響了程式的靈活性,所以儘管有些Java資料要儲存在堆疊裡,特別是物件控制程式碼,但Java物件並不放到其中。

(3) 堆

一種常規用途的記憶體池(也在RAM區域),其中儲存了Java物件。和堆疊不同,“記憶體堆”或“堆”(Heap)最吸引人的地方在於編譯器不必知道要從堆裡分配多少儲存空間,也不必知道儲存的資料要在堆裡停留多長的時間,因此,用堆儲存資料時會得到更大的靈活性。

要求建立一個物件時,只需用new命令編制相關的程式碼即可,執行這些程式碼時,會在堆裡自動進行資料的儲存;當然,為達到這種靈活性,必然會付出一定的代價:在堆裡分配儲存空間時會花掉更長的時間!

(4) 靜態儲存

這兒的“靜態”(Static)是指“位於固定位置”(儘管也在RAM裡),程式執行期間,靜態儲存的資料將隨時等候呼叫,可用static關鍵字指出一個物件的特定元素是靜態的,但Java物件本身永遠都不會置入靜態儲存空間。

(5) 常數儲存

常數值通常直接置於程式程式碼內部,這樣做是安全的,因為它們永遠都不會改變。有的常數需要嚴格地保護,所以可考慮將它們置入只讀儲存器(ROM)。

(6) 非RAM儲存

若資料完全獨立於一個程式之外,則程式不執行時仍可存在,並在程式的控制範圍之外。其中兩個最主要的例子便是“流式物件”和“固定物件”,對於流式物件,物件會變成位元組流,通常會發給另一臺機器;而對於固定物件,物件儲存在磁碟中,即使程式中止執行,它們仍可保持自己的狀態不變。

對於這些型別的資料儲存,一個特別有用的技巧就是它們能存在於其他媒體中,一旦需要,甚至能將它們恢復成普通的、基於RAM的物件。

4.特殊情況:主要型別

有一系列類需特別對待,之所以要特別對待,是由於用 new 建立物件(特別是小的、簡單的變數)並不是非常有效,因為 new 將物件置於“堆”裡。

對於這些型別, Java 採納了與 C 和 C++相同的方法,也就是說,不是用new 建立變數,而是建立一個並非控制程式碼的“自動”變數,這個變數容納了具體的值,並置於堆疊中,能夠更高效地存取。

Java 決定了每種主要型別的大小,就象在大多數語言裡那樣,這些大小並不隨著機器結構的變化而變化,這種大小的不可更改正是 Java 程式具有很強移植能力的原因之一。

主型別 大小 最小值 最大值 封裝器型別
boolean 1 位 – – Boolean
char 16 位 Unicode 0 Unicode 2 的 16 次方-1 Character
byte 8 位 -128 +127 Byte
short 16 位 -2 的 15 次方 +2 的 15 次方-1 Short
int 32 位 -2 的 31 次方 +2 的 31 次方-1 Integer
long 64 位 -2 的 63 次方 +2 的 63 次方-1 Long
float 32 位 IEEE754 IEEE754 Float
double 64 位 IEEE754 IEEE754 Double
void – – – Void

Java 增加了兩個類,用於進行高精度的計算: BigInteger 和 BigDecimal。

這兩個類都有自己特殊的“方法”,對應於我們針對主型別執行的操作,也就是說,能對int 或 float 做的事情,對 BigInteger 和 BigDecimal 一樣可以做,只是必須使用方法呼叫,不能使用運算子。此外,由於牽涉更多,所以運算速度會慢一些。我們犧牲了速度,但換來了精度。

BigInteger 支援任意精度的整數。也就是說,我們可精確表示任意大小的整數值,同時在運算過程中不會丟失任何資訊;

BigDecimal 支援任意精度的定點數字。例如,可用它進行精確的幣值計算。

5.作用域

大多數程式設計語言都提供了“作用域”( Scope)的概念。對於在作用域裡定義的名字,作用域同時決定了它的“可見性”以及“存在時間”。在 C, C++和 Java 裡, 作用域是由花括號的位置決定的。

作為在作用域裡定義的一個變數,它只有在那個作用域結束之前才可使用。

物件的作用域

Java 物件不具備與主型別一樣的存在時間。用 new 關鍵字建立一個 Java 物件的時候,它會超出作用域的範圍之外。所以假若使用下面這段程式碼:

{
String s = new String("a string");
} /* 作用域的終點 */

那麼控制程式碼 s 會在作用域的終點處消失。然而, s 指向的 String 物件依然佔據著記憶體空間。在上面這段程式碼裡,我們沒有辦法訪問物件,因為指向它的唯一一個控制程式碼已超出了作用域的邊界。

這樣造成的結果便是:對於用 new 建立的物件,只要我們願意,它們就會一直保留下去。這個程式設計問題在C和 C++裡特別突出,在C++裡,一旦工作完成,必須保證將物件清除。

這樣便帶來了一個有趣的問題?假如 Java 讓物件依然故我,怎樣才能防止它們大量充斥記憶體,並最終造成程式的“凝固”呢。在 C++裡,這個問題最令程式設計師頭痛。但 Java 以後,情況卻發生了改觀。

Java 有一個特別的“垃圾收集器” ,它會查詢用 new 建立的所有物件,並辨別其中哪些不再被引用。隨後,它會自動釋放由那些閒置物件佔據的記憶體,以便能由新物件使用,這意味著我們根本不必操心記憶體的回收問題,只需簡單地建立物件,一旦不再需要它們,它們就會自動離去。

這樣做可防止在 C++裡很常見的一個程式設計問題:由於程式設計師忘記釋放記憶體造成的“記憶體溢位”。

6.新建資料型別:類

如果說一切東西都是物件,那麼用什麼決定一個“類”( Class)的外觀與行為呢?換句話說,是什麼建立起了一個物件的“型別”( Type)呢?大多數物件導向的語言都用關鍵字“ class”表達這樣一個意思。

定義一個類時,可在類裡設定兩種型別的元素:資料成員(也叫“欄位”)以及成員函式(也叫“方法”)。

每個物件都為自己的資料成員保有儲存空間;資料成員不會在物件之間共享。下面是定義了一些資料成員的類示例:

class DataOnly {
int i;
float f;
boolean b;
}

這個類並沒有做任何實質性的事情,但我們可建立一個物件:

DataOnly d = new DataOnly();

可將值賦給資料成員,但首先必須知道如何引用一個物件的成員。為達到引用物件成員的目的,首先要寫上物件控制程式碼的名字,再跟隨一個點號(句點),再跟隨物件內部成員的名字。即“ 物件控制程式碼.成員”。例如:

d.i = 47;
d.f = 1.1f;
d.b = false;

一個物件也可能包含了另一個物件,而另一個物件裡則包含了我們想修改的資料。對於這個問題,只需保持“連線句點”即可。例如:

myPlane.leftTank.capacity = 100;

主成員的預設值

若某個主資料型別屬於一個類成員,那麼即使不明確(顯式)進行初始化,也可以保證它們獲得一個預設值。

主型別 預設值
Boolean false
Char `u0000`(null)
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d

然而,這種保證卻並不適用於“區域性”變數—— 那些變數並非一個類的欄位。

7.方法、自變數和返回值

迄今為止,我們一直用“函式”( Function)這個詞指代一個已命名的子例程,但在 Java 裡,更常用的一個詞卻是“方法”( Method),代表“完成某事的途徑”。

Java 的“方法”決定了一個物件能夠接收的訊息,方法的基本組成部分包括名字、自變數、返回型別以及主體。下面便是它最基本的形式:

返回型別 方法名( / 自變數列表/ ) {/ 方法主體 /}

返回型別是指呼叫方法之後返回的數值型別。顯然,方法名的作用是對具體的方法進行標識和引用。

自變數列表列出了想傳遞給方法的資訊型別和名稱。Java 的方法只能作為類的一部分建立。

為一個物件呼叫方法時,需要先列出物件的名字,在後面跟上一個句點,再跟上方法名以及它的引數列表。

物件名.方法名(自變數 1,自變數 2,自變數 3…)

物件導向的程式設計通常簡單地歸納為“向物件傳送訊息”。“靜態”方法可針對類呼叫,毋需一個物件。

自變數列表

自變數列表規定了我們傳送給方法的是什麼資訊,在傳遞物件時,通常都是指傳遞指向物件的控制程式碼。

對於前面提及的“特殊” 資料型別 boolean,char,byte, short, int,long, float 以及 double 來說是一個例外。

8.構建 J a v a 程式

正式構建自己的第一個 Java 程式前,還有幾個問題需要注意。

1.名字的可見性

在所有程式設計語言裡,一個不可避免的問題是對名字或名稱的控制,假設您在程式的某個模組裡使用了一個名字,而另一名程式設計師在另一個模組裡使用了相同的名字,此時,如何區分兩個名字,並防止兩個名字互相沖突呢?這個問題在 C 語言裡特別突出,因為程式未提供很好的名字管理方法。

為解決這個問題, C++用額外的關鍵字引入了“名稱空間”的概念。

由於採用全新的機制,所以 Java 能完全避免這些問題,為了給一個庫生成明確的名字,採用了與Internet域名類似的名字。

事實上, Java 的設計者鼓勵程式設計師反轉使用自己的 Internet 域名,因為它們肯定是獨一無二的,比如我的域名是 alisoft.com,所以我的實用工具庫就可命名為com. alisoft.utility,整個軟體包都以小寫字母為標準。

Java 的這種特殊機制意味著所有檔案都自動存在於自己的名稱空間裡,而且一個檔案裡的每個類都自動獲得一個獨一無二的識別符號(當然,一個檔案裡的類名必須是唯一的)。

2.使用其他元件

一旦要在自己的程式裡使用一個預先定義好的類,編譯器就必須知道如何找到它,當然,這個類可能就在發出呼叫的那個相同的原始碼檔案裡,但假若那個類位於其他檔案裡呢?為達到這個目的,要用import 關鍵字準確告訴Java 編譯器我們希望的類是什麼。

import 的作用是指示編譯器匯入一個“包”,或者說一個“類庫”(在其他語言裡,可將“庫”想象成一系列函式、資料以及類的集合。但請記住, Java 的所有程式碼都必須寫入一個類中)。

3. s t a t i c 關鍵字

通常,我們建立類時會指出那個類的物件的外觀與行為,除非用new 建立那個類的一個物件,否則實際上並未得到任何東西,只有執行了 new 後,才會正式生成資料儲存空間,並可使用相應的方法。

但在兩種特殊的情形下,上述方法並不堪用,一種情形是隻想用一個儲存區域來儲存一個特定的資料,無論要建立多少個物件,甚至根本不建立物件;另一種情形是我們需要一個特殊的方法,它沒有與這個類的任何物件關聯,也就是說,即使沒有建立物件,也需要一個能呼叫的方法。

為滿足這兩方面的要求,可使用static(靜態)關鍵字。一旦將什麼東西設為 static,資料或方法就不會同那個類的任何物件例項聯絡到一起。

對於非 static 資料和方法,我們必須建立一個物件,並用那個物件訪問資料或方法,這是由於非static 資料和方法必須知道它們操作的具體物件,當然,在正式使用前, 由於 static 方法不需要建立任何物件,所以它們不可簡單地呼叫其他那些成員,同時不引用一個已命名的物件,從而直接訪問非 static 成員或方法(因為非 static 成員和方法必須同一個特定的物件關聯到一起)。

有些物件導向的語言使用了“類資料”和“類方法”這兩個術語,它們意味著資料和方法只是為作為一個整體的類而存在的,並不是為那個類的任何特定物件。有時,您會在其他一些Java 書刊裡發現這樣的稱呼。

為了將資料成員或方法設為 static,只需在定義前置和這個關鍵字即可。例如,下述程式碼能生成一個 static資料成員,並對其初始化:

class StaticTest {
   static int i = 47;
}

現在,儘管我們製作了兩個 StaticTest 物件,但它們仍然只佔據 StaticTest.i 的一個儲存空間。這兩個物件都共享同樣的 i。請考察下述程式碼:

StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();

此時,無論 st1.i 還是 st2.i 都有同樣的值 47,因為它們引用的是同樣的記憶體區域。

有兩個辦法可引用一個 static 變數,正如上面展示的那樣,可通過一個物件命名它,如 st2.i。亦可直接用它的類名引用,而這在非靜態成員裡是行不通的(最好用這個辦法引用static 變數,因為它強調了那個變數的“靜態”本質)。

StaticTest.i++;

其中, ++運算子會使變數增值。此時,無論 st1.i 還是 st2.i 的值都是 48。
類似的邏輯也適用於靜態方法。既可象對其他任何方法那樣通過一個物件引用靜態方法,亦可用特殊的語法格式“類名.方法()” 加以引用。

9.第一個 J a v a 程式

最後,讓我們正式編一個程式。它能列印出與當前執行的系統有關的資料,並利用了來自Java 標準庫的 System 物件的多種方法。

import java.util.Date;
import java.util.Properties;

public class Property {
    public static void main(String[] args) {
        System.out.println(new Date());
        Properties p = System.getProperties();
        p.list(System.out);
        System.out.println("--- Memory Usage:");
        Runtime rt = Runtime.getRuntime();
        System.out.println("Total Memory = " + rt.totalMemory() 
                        + " Free Memory = " + rt.freeMemory());
    }
}

輸出參考:

Wed Dec 20 15:51:41 CST 2017
— listing properties —
java.runtime.name=Java(TM) SE Runtime Environment
sun.boot.library.path=C:Program Files (x86)Javajdk1.6.0_…
java.vm.version=20.45-b01
java.vm.vendor=Sun Microsystems Inc.

java.vendor.url=http://java.sun.com/
path.separator=;
java.vm.name=Java HotSpot(TM) Client VM
file.encoding.pkg=sun.io
user.country=CN
sun.java.launcher=SUN_STANDARD
sun.os.patch.level=Service Pack 3
java.vm.specification.name=Java Virtual Machine Specification
user.dir=F:MavenSpaceerp-parenterp-web
java.runtime.version=1.6.0_45-b06
java.awt.graphicsenv=sun.awt.Win32GraphicsEnvironment
java.endorsed.dirs=C:Program Files (x86)Javajdk1.6.0_…
os.arch=x86
java.io.tmpdir=C:UsersADMINI~1AppDataLocalTemp
line.separator=

java.vm.specification.vendor=Sun Microsystems Inc.
user.variant=
os.name=Windows XP
sun.jnu.encoding=GBK
java.library.path=C:Program Files (x86)Javajdk1.6.0_…
java.specification.name=Java Platform API Specification
java.class.version=50.0
sun.management.compiler=HotSpot Client Compiler
os.version=5.1
user.home=C:UsersAdministrator
user.timezone=Asia/Shanghai
java.awt.printerjob=sun.awt.windows.WPrinterJob
file.encoding=UTF-8
java.specification.version=1.6
user.name=Administrator
java.class.path=F:MavenSpaceerp-parenterp-webtarge…
java.vm.specification.version=1.0
sun.arch.data.model=32
java.home=C:Program Files (x86)Javajdk1.6.0_…
sun.java.command=com.jsjn.zxpt.DefaultTest
java.specification.vendor=Sun Microsystems Inc.
user.language=zh
awt.toolkit=sun.awt.windows.WToolkit
java.vm.info=mixed mode, sharing
java.version=1.6.0_45
java.ext.dirs=C:Program Files (x86)Javajdk1.6.0_…
sun.boot.class.path=C:Program Files (x86)Javajdk1.6.0_…
java.vendor=Sun Microsystems Inc.
file.separator=
java.vendor.url.bug=http://java.sun.com/cgi-bin/bugreport…

sun.cpu.endian=little
sun.io.unicode.encoding=UnicodeLittle
sun.desktop=windows
sun.cpu.isalist=pentium_pro+mmx pentium_pro pentium+m…
— Memory Usage:
Total Memory = 16252928 Free Memory = 15446800


相關文章