細讀 Thinking in Java (一)一切都是對象

前言

《Thinking in Java》做爲Java最經典的學習書籍之一,不論是對於學習java的新手或是有一定經驗的程序員來說都有不同的學習價值,在工作的這兩年多當中由於種種雜事一直沒時間拜讀此書,近期決定堅持每天抽空細讀一下,一方面鞏固一下java基礎,另一方面要找一下學習的狀態,每天忙於項目不停趕進度寫代碼而忽略了學習也是不行的,所以感覺通過寫blog來堅持讀書學習也是很不錯的,本系列blog參照的是《Java編程思想第4版》,第一章“對象導論”簡要介紹了Java語言的一些重要特性和知識點,我們從第二章“一切都是對象”開始記錄。

用引用操縱對象

如題,這一小節介紹了編程語言操縱內存中元素的方式,包括:

  • 直接操縱——Java中的引用(reference)
  • 間接操縱——C&C++中的指針(pointer)

和C以及C++相比,這些在Java中得到了簡化,因爲一切都被視爲對象,而操縱對象的標識符也就是對象的一個引用(reference)了。例如:

String s;

這樣就創建了一個String對象的引用——s,注意此時並沒有對象被創建。作者建議更安全的做法是創建引用的同時初始化對象,例如:

String s = "asdf";

這裏展示了一個Java語言的特性:字符串可以用帶引號的文本初始化

必須由你創建所有對象

這裏提到了創建對象的通用方式——new關鍵字。new關鍵字的意思是“給我一個新對象”,上面的例子同樣可以這樣寫:

String s = new String("asdf");

儘管這些顯而易見,我們很清楚String類提供了這樣的一個構造方法,但作者必然是按照循序漸進的思路去介紹知識點,所以跟上作者的思路來繼續看下去。

存儲到什麼地方

這裏介紹了部分硬件相關的知識點,即:對象在內存中是怎樣放置和存儲?下面提到了5個可以存儲數據的地方:

  1. 寄存器。
    在學習計算機基礎時我們都知道寄存器位於CPU(中央處理器)中,而且它是最快的存儲區,但是寄存器的數量有限,它是根據需求自動分配,Java語言無法控制。
  2. 堆棧。
    位於RAM(隨機訪問存儲器),其速度僅次於寄存器,堆棧指針向下移動,則分配新內存;若向上移動,則釋放那些內存。某些Java數據會存儲於堆棧中,特別是對象的引用,但這裏強調了Java對象並不會存儲在這裏。
  3. 堆。
    也位於RAM區,是一種通用的內存池,用於存放所有Java對象。在堆中分配存儲相較於堆棧更具靈活性,但代價是效率會低於堆棧。
  4. 常量存儲。
    常量值通常直接存放在程序代碼內部,它們永遠不會被改變。在嵌入式操作系統中,可以選擇放在ROM(只讀存儲器)中。
  5. 非RAM存儲。
    分爲流對象和持久化對象,即不受程序的任何控制。流對象就是將對象轉換爲字節流進行傳輸,而持久化對象也就是常用的將對象存儲在數據庫中了,例如在Java中常用的JDBC和Hibernate。

特例:基本類型

在Java中基本類型不用通過new關鍵字來創建(同C&C++一致),而是通過直接聲明的方式去創建,例如:

char c = 'x';

這裏的c是一個並非是引用的“自動變量”,這個變量直接存儲“值”,並置於堆棧中,因此更加高效。簡單總結一下:

  • 創建基本類型無需new關鍵字,這一點和C以及C++一致。
  • 基本類型創建後並無引用(reference),它應視爲一個“自動”變量,並直接將值存儲在堆棧中(普通對象均存儲在堆中)。

Java要確定每種基本類型所佔存儲空間的大小(不像其它大多數語言隨機器硬件架構變化而變化,這也是更具可移植性的原因之一)。

下面看一下Java中所有基本類型的大小範圍:

基本類型 大小 最小值 最大值 包裝器類型
boolean - - - Boolean
char 16-bit Unicode 0 Unicode 2^16-1 Character
byte 8 bits -128 +127 Byte
short 16 bits -2^15 +2^15-1 Short
int 32 bits -2^31 +2^31-1 Integer
long 64 bits -2^63 +2^63-1 Long
float 32 bits IEEE754 IEEE754 Float
double 64 bits IEEE754 IEEE754 Double
void - - - Void

在Java中所有數值類型都有正負號(而C語言中有無符號類型unsigned)。

另外,基本類型也提供了包裝器對象(Wrapper),它和普通對象一樣在堆中被創建,每一種包裝器都對應一個基本類型。例如:

Character ch = new Character('x');

關於包裝器在後面的章節再細說,但顯而易見的一點是,和普通類型相比它的效率會低一些(存儲堆棧和堆的區別),但同時這種代價會允許它做更多的事情。

Java中還提供了兩個用於高精度計算的類:BigIntegerBigDecimal

  • BigInteger支持任意精度的整數運算。
  • BigDecimal支持任意浮點數運算(例如貨幣運算)。

Java中的數組

這裏僅僅強調了Java數組的安全性(和C&C++做對比),即:Java確保數組會被初始化,而且不能在它的範圍之外被訪問。基本類型的數組初始化由編譯器來保證(將數組所佔的內存全部置零),對象數組則是相當於創建了一個引用數組,且每個引用都會初始化爲null,若試圖使用null引用則會報運行時異常,因此常見的數組錯誤在Java中可以避免。

永遠不要銷燬對象

作用域

大多數過程性語言都有作用域概念(scope),在Java和C&C++中,作用域均是由花括號的位置決定的,例如:

{
    int x = 12;
    // Only x available
    {
        int q = 96;
        // Both x & q available
    }
    // Only x available
    // q is "out of scope"
}

這裏還提到了一點和C&C++的區別,在C&C++裏將一個較大作用域的變量“隱藏”起來的做法在Java中是不允許的,例如:

{
    int x = 12;
    {
        int x = 96; // Duplicate local variable x
    }
}

對象的作用域

Java對象不具備和基本類型一樣的生命週期,當用new創建一個Java對象時,它可以存活於作用域之外,簡言之,就是說一旦創建了一個對象,儘管對象的引用在作用域終點會消失,但該引用所指的對象依然會佔據內存空間。爲了避免創建的N多個對象佔滿內存,Java提供了垃圾回收器,它用來監視用new創建的所有對象,並辨別不會再被引用的對象並釋放其空間(消除了C++中相關的類似問題)。

創建新的數據類型:類

大多數面向對象的程序設計語言習慣用關鍵字“class”來表示“我準備告訴你一種新類型的對象看起來像什麼樣子”,Java也不例外,用class關鍵字來聲明一個新的類,例如:

class ATypeName{
    // Class body goes here
}

同樣也可以用new關鍵字來創建一個這種新類型的對象:

ATypeName a = new ATypeName();

字段和方法

一旦在Java中定義了一個類,就可以在類中設置兩種類型的元素:

  • 字段(數據成員)
  • 方法(成員函數)

字段可以是任何類型,每個對象都有用來存儲其字段的空間,普通字段不能在對象間共享。例如:

class DataOnly{
    int i;
    double d;
    boolean b;
}

可以給對象的字段賦值,具體實現爲:在對象引用的名稱之後緊接着一個句點,然後再接着是對象內部的成員名稱objectReference.member,例如:

DataOnly data = new DataOnly();
data.i = 47;
data.d = 1.1;
data.b = false;

此時的DataOnly類除了保存數據外再也沒有別的用處,因爲它沒有任何成員方法(關於成員方法後面再提)。

若類的某個成員是基本數據類型,即使沒有初始化,Java也會確保它獲得一個默認值,所有基本類型的默認值如下所示:

基本類型 默認值
boolean false
char ‘\u0000’(null)
byte (byte)0
short (short)0
int 0
float 0.0f
double 0.0d

注意,當變量作爲類的成員(全局變量)時,Java才確保給定其默認值進行初始化,並不適用於“局部變量”(方法中的變量),所以若是局部變量沒有初始化就使用的話,那麼Java會在編譯時返回一個錯誤,這一點也是Java優於C++的地方。

方法、參數和返回值

C和C++中都用函數(function)這個詞來描述命名子程序,在Java中用方法(method)這個詞來表示“做某些事情的方式”。Java的方法決定了一個對象能夠接收什麼樣的消息。方法的基本組成部分包括:方法名、參數、返回值和方法體,基本形式如下:

ReturnType methodName( /* Argument list */ ) {
    /* Method body */
}

方法名和參數列表(合起來被稱爲“方法標籤”)唯一的標識出某個方法。

Java中的方法只能作爲類的一部分來創建,方法通過對象來調用(靜態方法除外),調用方法時,需要先列出對象名,緊接着是句點,然後是方法名和參數列表,如下所示:

objectName.methodName(arg1, arg2, arg3);

再舉一個簡單的例子:

int x = a.f();

上述方法f沒有參數,返回值爲int類型,是對象a的一個成員方法,這種方法調用的行爲通常被稱爲發送消息給對象,這裏消息是f(),對象是a,面向對象的程序設計通常簡單的歸納爲“向對象發送消息”。

參數列表

方法的參數列表指定要傳遞給方法什麼樣的信息,在參數列表中必須指定每個所傳遞對象的類型和名字。例如:

int storage(String s){
    return s.length()*2;
}

此方法告訴你,需要多少個字節才能容納一個特定的String對象中的信息。

若某個方法的返回類型是void,那麼這個方法中的return關鍵字的作用只是用來退出方法,例如:

void nothing(){
    return;
}

因此,沒有必要到方法結束時才離開,可以在任何地方返回。但若返回類型不是void,那麼無論在何處返回,編譯器都會強制返回一個正確類型的返回值。

構建一個Java程序

在構建第一個Java程序之前需要了解一些問題。

名字可見性

和C以及C++相比,Java通過包(package)來解決命名衝突的問題,即每個類都應放在一個包中,而包的格式應當是自己的Internet域名的反寫,例如:net.mindview.utility.foibles,而句點用來代表子目錄的劃分。在Java 1.0和Java 1.1中,擴展名com、org、edu、net等約定爲大寫,而在Java 2開發到一半時,設計者又發現會引起一些問題於是又都換回小寫。

運用其它構件

如果想在程序中使用預先定義好的類,那麼編譯器就必須知道怎麼定位它,在Java中可以使用import關鍵字來準確的告訴編譯器你想要的是什麼類。import指示編譯器導入一個包,也就是一個類庫(在其它語言中,一個庫不僅包含類,還可能包括方法和數據,但Java中所有的代碼都必須寫在類裏)。例如:

import java.util.ArrayList;

也可以使用通配符來一次導入一羣類:

import java.util.*;

static關鍵字

考慮以下兩種情形:

  1. 只想爲某特定域分配單一存儲空間,而不去考慮究竟要創建多少個對象,甚至根本不創建任何對象。
  2. 希望某個方法不與包含它的類的任何對象相關聯,也就是說即使沒有創建對象也能調用這個方法。

通過使用static關鍵字可以滿足上面兩點。當聲明一個域或方法爲static時,就表示這個域或方法不會與它所屬的類的任何對象相關聯。所以即使某個類沒有創建任何對象,也可以訪問它的static域或調用它的static方法。只須將static關鍵字放在定義之前,就可以將字段或方法設定爲static,例如:

class StaticTest{
    static int i = 47;
}

現在,即使創建了2個StaticTest對象,StaticTest.i也只有一份存儲空間,這兩個對象會共享同一個i。

引用static變量有兩種方式,一種是通過對象去定位,另一種是通過類名直接引用。例如:

StaticTest st = new StaticTest();
int i1 = st.i;   // 通過對象定位
int i2 = StaticTest.i;  // 通過類名直接引用(推薦)
System.out.println(i1==i2); //true

使用類名直接引用是引用static變量的首選方式,這不僅是因爲它強調了變量的static結構,而且在某些情況下它還爲編譯器進行優化提供了更好的機會。

類似的邏輯也應用於靜態方法,例如:

class Incrementable{
    static void increment(){
        StaticTest.i++;
    }
}
System.out.println(StaticTest.i); // 47
Incrementable.increment();
System.out.println(StaticTest.i); // 48

如上所示,通過調用Incrementable的靜態方法increment()使得StaticTest的靜態變量i加了1。

static方法的一個重要用法就是在不創建任何對象的前提下就可以調用它,這一點對定義main()方法很重要,main()方法是運行一個應用時的入口點。

你的第一個Java程序

從簡單的打印日期和字符串開始:

import java.util.*;

public class HelloDate(){
    public static void main(String[] args){
        System.out.println("Hello,it's: ");
        System.out.println(new Date());
    }
}

首先,import語句用於引入“額外”的類,那麼什麼是額外的類,其實就是除了java.lang包外的所有類了,用於lang包下的類比較常用所以Sun已經爲我們自動導入到了每一個Java文件中了。下面再看第5行,System類有許多屬性,這裏看到的out是一個靜態的PrintStream對象,由於是靜態的所以它可以直接被類引用,println是PrintStream對象的方法,它的作用是“將我給你的數據打印到控制檯,完成後換行”。

在Java中類的名字必須和文件名相同,若是想創建一個如上一樣可以獨立運行的程序,那麼還必須包含一個main()方法,形式如下:

public static void main(String[] args){

}

public關鍵字指的是一個可由外部調用的方法,main()方法的參數是一個String數組,Java編譯器要求必須有這個參數,因爲它要用來存儲命令行參數。

第6行中傳遞的參數是一個Date對象,一旦創建它之後,就可以直接將它的值(它被自動轉換爲String類型)發送給println()。當這條語句執行完畢後,Date對象就不再被使用,而垃圾回收器會發現這一情況,並在任意時候將其回收。

再看一下System類的一些其它方法:

System.getProperties().list(System.out);
System.out.println(System.getProperty("user.name"));
System.out.println(System.getProperty("java.library.path"));

第一行中System類的靜態方法getProperties()可以獲取當前運行程序的操作系統的所有“屬性”,主要是環境信息,例如:系統用戶名、jdk版本等等,它返回的是一個Properties對象,而Properties對象的list方法則是結果發送給它的參數:這裏我們傳的是Sytem.out(標準輸出流),所以可以直接在控制檯顯示。而第二行和第三行中System類的靜態方法getProperty()則是根據指定的key去查詢對應的屬性值。

編譯和運行

要編譯和運行首先必須要有一個Java開發環境,比如Sun提供的免費的JDK(Java Developer’s Kit),或者由IBM提供的jikes編譯器(速度快於javac)。下面是使用javac編譯.java文件的例子(注意執行命令前需要切換到java文件所在的目錄,同時要確保計算機能找到javac和java這兩個文件):

javac HelloDate.java

編譯成功後會在當前目錄生成.class的字節碼文件,然後再通過java運行即可:

java HelloDate

現在就可以看到控制檯的輸出信息了。

註釋和嵌入式文檔

在Java中有兩種註釋風格,它們都源於C和C++,分別是:

  • /*開始並以*/結束,可用於多行註釋。
  • 以一個//開頭直到句末,僅用於單行註釋。

在多行註釋中/**/之間的所有東西都會被忽略(編譯時),所以一下兩種寫法沒有區別:

/* This is a comment
 * that continues
 * across lines
 * /
/* This is a comment
that continues across lines */

註釋文檔

關於程序文檔的生成,Java提供了javadoc這個工具,它是JDK安裝的一部分。javadoc輸出的是一個HTML文件,該工具使得我們只需創建和維護單一的源文件,就能自動生成有用的文檔了。

語法

所有的javadoc命令都只能在/**註釋中出現,和通常一樣,註釋結束於*/。使用javadoc的方式主要有兩種,分別是:

  • 嵌入HTML
  • 文檔標籤

獨立文檔標籤是一些以@字符開頭的命令,且要置於註釋行的最前面。而行內文檔標籤則可以出現在javadoc註釋中的任何地方,它們也是以@字符開頭,但要括在花括號內。共有三種類型的註釋文檔,分別作用於類、域和方法,例如:

/** A Class comment */
public class Documentation1 {

    /** A field comment */
    public int i;

    /** A method comment */
    public void f() {

    }

} // /":~

javadoc只能爲public和protected成員進行文檔註釋,這樣做是因爲只有這兩種成員才能在文件之外被使用,這也是客戶端程序員所期望的。上述代碼的輸出結果是一個HTML文件,它與其它Java文檔具有相同的標準格式。首先將這個Java類拷貝到一個新創建的目錄中,然後切換到當前目錄執行javadoc命令即可:
這裏寫圖片描述

如上圖所示,這樣就已經成功創建了文檔,再看一下剛纔新創建的目錄下的文件列表:
這裏寫圖片描述

打開index.html即可看見我們熟悉的格式的文檔了:
這裏寫圖片描述

嵌入式HTML

javadoc通過生成的HTML文檔傳送HTML命令,這使你能夠充分利用HTML。當然,其主要目的還是爲了對代碼進行格式化,例如在上面的例子中的f()方法上加入如下注釋:

/** A method comment 
 *  You can <em>even</em> insert a list:
 *  <ol>
 *  <li> Item one
 *  <li> Item two
 *  <li> Item three
 *  </ol>
 * */
public void f() {

}

再次運行javadoc重新生成註釋文檔,那麼可以看到這個方法的註釋已經包含了一定的格式:
這裏寫圖片描述

注意,不要在嵌入式HTML中使用標題標籤,例如:<h1><hr> ,因爲javadoc會插入自己的標題,而你的標題可能同它們發生衝突。所有類型的註釋文檔都支持嵌入式HTML(類、域和方法)。

一些標籤示例

這裏將介紹一些可用於代碼文檔的javadoc標籤。

1.@see: 引用其它類
@see標籤允許用戶引用其它類的文檔。javadoc會在其生成的HTML文件中,通過@see標籤鏈接到其它文檔,格式如下:

@see classname
@see fully-qualified-classname
@see fully-qualified-classname#methodname

上述每種格式都會在生成的文檔中加入一個具有超鏈接的“See Also”(參見)條目,但是javadoc**不會檢查**你所提供的超鏈接是否有效。在java源碼中可以容易的找到這種標籤:

 * @see  java.lang.Object#toString()
 * @see  java.lang.StringBuffer
 * @see  #String(byte[], int)

2.{@link package.class#member label}
該標籤與@see極其相似,只是它作用於行內,並且是用“lable”作爲超鏈接文本而不用“See Also”。

3.{@docRoot}
該標籤產生到文檔根目錄的相對路徑,用於文檔樹頁面的顯示超鏈接。

4.{@inheritDoc}
該標籤從當前這個類的最直接的基類中繼承相關文檔到當前的文檔註釋中。

5.@version
用於描述版本,格式如下:

@version version-infomation

如果javadoc命令行使用了“-version”標記,那麼就從生成的HTML文檔中特別提取出版本信息。

6.@author
用於描述作者信息,格式如下:

@author author-infomation

如果javadoc命令行使用了-author標記,那麼就從生成的HTML文檔中特別提取作者信息。可以使用多個標籤,以便列出所有作者,但是它們必須連續放置。全部作者信息會合併到同一段落,置於生成的HTML中。例如:

/**
 * 
 * @see java.lang.StringBuffer
 * 
 * @author Lee Boynton
 * @author Arthur van Hoff
 * @author Martin Buchholz
 * @author Ulf Zibis
 * 
 * */
public class Documentation1 {}

然後運行javadoc命令行並帶上-version標記,即可看到生成的HTML中的作者信息:
這裏寫圖片描述

7.@since
該標籤允許你指定程序代碼最早使用的版本,可以在HTML Java文檔中看到它被用來指定所用的JDK版本的情況。例如:

@since   JDK1.0

這就表示此類是從JDK1.0版本開始使用的。

8.@param
用於描述方法的參數,該標籤用於方法文檔中,形式如下:

@param parameter-name description

parameter-name是方法的參數列表中的標識符,description是可延續數行的文本,終止於新的文檔標籤出現之前。

9.@return
用於描述方法返回值,該標籤用於方法文檔,格式如下:

@return description

10.@throws
用於描述某個方法可能會產生的異常以及異常說明,格式如下:

@throws fully-qualified-class-name description

fully-qualified-class-name給出一個異常類的無歧義的名字,而該異常類在別處定義。description告訴你爲什麼此特殊類型的異常會在方法調用中出現。例如:

* @throws  PatternSyntaxException
*          if the regular expression's syntax is invalid

11.@deprecated
用於描述過期(已由改進的新特性取代,不建議再使用)的方法,如果使用一個標記爲@deprecated的方法,則會引起編譯器發佈警告。在Java SE5中,javadoc標籤@deprecated已經被@Deprecated註解所替代。

文檔示例

下面看一段加了完整文檔註釋的Java程序:

import java.util.*;

/**
 * The first java program. Display a string and today's date.
 * 
 * @author Wang Liang
 * @version 1.0.0
 */
public class HelloDate {

    /**
     * Entry point to class & application.
     * 
     * @param args
     *            array of string arguments
     * @throws exceptions
     *             No exceptions thrown
     */
    public static void main(String[] args) {
        System.out.println("Hello,it's: ");
        System.out.println(new Date());
    }
} /* Output: (55% match) 
Hello,it's: 
Thu Mar 24 11:29:36 CST 2016
 */// :~

編碼風格

在“Java編程語言編碼約定”中,代碼風格是這樣規定的:類名的首字母要大寫,如果類名由幾個單詞構成,那麼把它們並在一起,其中每個內部單詞的首字母都要採用大寫形式,即“駝峯風格”,例如:

class AllTheColorsOfTheRainbow{ // ...

幾乎其它所有內容——方法、字段(成員變量)以及對象引用名稱等,公認的風格與類一致,只是標識符的第一個字母採用小寫,例如:

class AllTheColorsOfTheRainbow{
    int anIntegerRepresentingColors;
    void changeTheHueOfTheColor(int newHue){
        // ...
    }
    // ...
}

總結

第一章的內容到此就結束了,整體非常簡單,重點是文檔註釋方面的內容,還有一些計算機基礎方面和部分歷史方面的知識僅作了解,本系列blog是我利用閒暇時間記錄的讀書筆記,關於書中沒有講到的例子我這裏也都給出了實例並進行了細化,旨在複習一遍Java基礎,希望對有同樣想法的朋友有所幫助,The End。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章