跳轉至內容

Java之道/有趣的物件

來自Wikibooks,開放世界中的開放書籍

有趣的物件

[編輯 | 編輯原始碼]

什麼是有趣的?

[編輯 | 編輯原始碼]

雖然字串是物件,但它們並不是很有趣的物件,因為

  • 它們是不可變的。
  • 它們沒有例項變數。
  • 您不必使用new構造來建立它們。

在本章中,我們將使用 Java 語言中包含的兩種新的物件型別:Point 和 Rectangle。請注意,這些不是出現在螢幕上的圖形物件,而是包含資料的變數,就像 int 和 double 一樣。與其他變數一樣,它們可以在內部用於執行計算。

Point 和 Rectangle 類的定義位於 java.awt 包中,因此我們必須匯入它們。

內建的 Java 類被分成許多包,包括 java.lang,其中包含我們迄今為止看到的大多數類,以及 java.awt,其中包含與 Java 抽象視窗工具包 (AWT) 相關的類,其中包含視窗、按鈕、圖形等的類。

為了使用一個包,您必須匯入它,這就是為什麼圖形部分中的程式以 import java.awt.* 開頭的原因。* 表示我們希望匯入 AWT 包中的所有類。如果需要,您可以顯式命名要匯入的類,但這沒有太大優勢。java.lang 中的類會自動匯入,這就是為什麼我們的大多數程式不需要 import 語句的原因。

所有 import 語句都出現在程式的開頭,在類定義之外。

點物件

[編輯 | 編輯原始碼]

在最基本的層面上,點是兩個數字(座標),我們將其作為一個整體視為單個物件。在數學符號中,點通常用括號括起來,用逗號分隔座標。例如,表示原點,表示從原點向右移動單位和向上移動單位的點。

在 Java 中,點由 Point 物件表示。要建立一個新點,您必須使用new命令

    Point blank;
    blank = new Point (3, 4);

第一行是常規的變數宣告:blank 的型別為 Point。第二行看起來有點奇怪;它呼叫了 new 命令,指定了新物件的型別,並提供了引數。您可能不會感到驚訝,這些引數是新點的座標。

new 命令的結果是對新點的引用。我稍後會更詳細地解釋引用;目前重要的是變數 blank 包含新建立物件的地址。

總而言之,程式中的所有變數、值和物件都稱為狀態。像這樣顯示程式狀態的圖表稱為狀態圖。隨著程式的執行,狀態會發生變化,因此您應該將狀態圖視為執行特定點的快照。

例項變數

[編輯 | 編輯原始碼]

構成物件的資料片段有時稱為元件、記錄或欄位。在 Java 中,它們被稱為例項變數,因為每個物件(其型別的例項)都有自己的例項變數副本。

這就像汽車的手套箱。每輛汽車都是汽車型別的例項,每輛汽車都有自己的手套箱。如果您讓我從您的汽車的手套箱裡拿東西,您必須告訴我哪輛車是您的。

類似地,如果您想從例項變數中讀取值,則必須指定要從中獲取該值的 物件。在 Java 中,這是使用點表示法完成的。

    int x = blank.x;

表示式 blank.x 表示轉到 blank 引用的物件,並獲取 x 的值。在這種情況下,我們將該值分配給名為 x 的區域性變數。請注意,名為 x 的區域性變數與名為 x 的例項變數之間沒有衝突。點表示法的目的是明確識別您正在引用的哪個變數。

您可以將點表示法用作任何 Java 表示式的一部分,因此以下內容是合法的。

    System.out.println (blank.x + ", " + blank.y);
    int distance = blank.x * blank.x + blank.y * blank.y;

第一行列印 3, 4;第二行計算值 25。

物件作為引數

[編輯 | 編輯原始碼]

您可以按照通常的方式將物件作為引數傳遞。例如

  public static void printPoint (Point p){
    System.out.println ("(" + p.x + ", " + p.y + ")");
  }

是一個以點作為引數並以標準格式列印它的方法。如果您呼叫 printPoint (blank),它將列印 (3, 4)。實際上,Java 有一個內建的方法來列印點。如果您呼叫 System.out.println (blank),您將得到:java.awt.Point[x=3,y=4]

這是 Java 用於列印物件的標準格式。它列印型別的名稱,然後列印物件的內容,包括例項變數的名稱和值。

作為第二個示例,我們可以重寫距離部分中的距離方法,使其以兩個點作為引數,而不是四個雙精度數。

  public static double distance (Point p1, Point p2) {
    double dx = (double)(p2.x - p1.x);
    double dy = (double)(p2.y - p1.y);
    return Math.sqrt (dx*dx + dy*dy);
  }

型別轉換實際上並不是必需的;我只是將它們新增為提醒,Point 中的例項變數是整數。

矩形類似於點,只是它們有四個例項變數,名為 x、y、width 和 height。除此之外,其他一切都非常相似。

    Rectangle box = new Rectangle (0, 0, 100, 200);

建立一個新的 Rectangle 物件,並使 box 指向它。

如果您列印 box,您將得到

   java.awt.Rectangle[x=0,y=0,width=100,height=200]

同樣,這是內建的 Java 方法的結果,該方法知道如何列印 Rectangle 物件。


物件作為返回值型別

[編輯 | 編輯原始碼]

您可以編寫返回物件的方法。例如,findCenter 以 Rectangle 作為引數,並返回一個包含 Rectangle 中心座標的 Point

  public static Point findCenter (Rectangle box)
    int x = box.x + box.width/2;
    int y = box.y + box.height/2;
    return new Point (x, y);

請注意,您可以使用 new 建立一個新物件,然後立即將結果用作返回值。

物件是可變的

[編輯 | 編輯原始碼]

您可以透過為其一個例項變數賦值來更改物件的內容。例如,要移動矩形而不更改其大小,您可以修改 x 和 y 值

    box.x = box.x + 50;
    box.y = box.y + 100;

我們可以獲取此程式碼並將其封裝在一個方法中,並將其推廣以按任意量移動矩形

  public static void moveRect (Rectangle box, int dx, int dy)
    box.x = box.x + dx;
    box.y = box.y + dy;

變數 dx 和 dy 指示在每個方向上移動矩形的距離。呼叫此方法會修改作為引數傳遞的 Rectangle。

    Rectangle box = new Rectangle (0, 0, 100, 200);
    moveRect (box, 50, 100);
    System.out.println (box);

列印

   java.awt.Rectangle[x=50,y=100,width=100,height=200].

透過將物件作為引數傳遞給方法來修改物件可能很有用,但它也可能使除錯變得更加困難,因為並不總是清楚哪些方法呼叫會修改其引數,哪些不會。稍後,我將討論這種程式設計風格的一些優缺點。

同時,我們可以享受 Java 內建方法的便利,其中包括 translate,它與 moveRect 做完全相同的事情,儘管呼叫它的語法略有不同。我們不是將 Rectangle 作為引數傳遞,而是在 Rectangle 上呼叫 translate,並將 dx 和 dy 作為引數傳遞。

    box.translate (50, 100);

效果完全相同。

別名

aliasing
reference

請記住,當您將值賦給物件變數時,您實際上是在賦予一個對物件的引用。多個變數可以引用同一個物件。例如,這段程式碼

    Rectangle box1 = new Rectangle (0, 0, 100, 200);
    Rectangle box2 = box1;

box1 和 box2 都引用或指向同一個物件。換句話說,這個物件有兩個名稱:box1 和 box2。當一個人使用兩個名字時,我們稱之為別名。物件也一樣。

當兩個變數是別名時,任何影響一個變數的更改也會影響另一個變數。例如

    System.out.println (box2.width);
    box1.grow (50, 50);
    System.out.println (box2.width);

第一行列印 100,這是 box2 所引用的 Rectangle 的寬度。第二行在 box1 上呼叫 grow 方法,這會將 Rectangle 在各個方向上擴充套件 50 畫素(有關更多詳細資訊,請參閱文件)。

從該圖中可以清楚地看出,對 box1 做出的任何更改也適用於 box2。因此,第三行列印的值為 200,即擴充套件後的矩形的寬度。(順便說一句,Rectangle 的座標為負數是完全合法的。)

正如您從這個簡單的示例中可以看出的那樣,涉及別名的程式碼很快就會變得混亂,並且很難除錯。一般來說,應避免使用別名或謹慎使用。

當您建立物件變數時,請記住您正在建立對物件的引用。在您讓變數指向某個物件之前,變數的值為 null。null 是 Java 中的一個特殊值(也是一個 Java 關鍵字),用於表示“無物件”。

宣告 Point blank; 等效於此初始化

    Point blank = null;

如果您嘗試使用 null 物件,無論是透過訪問例項變數還是呼叫方法,您都會得到一個 NullPointerException。系統將列印錯誤訊息並終止程式。

    Point blank = null;
    int x = blank.x;              // NullPointerException
    blank.translate (50, 50);     // NullPointerException

另一方面,將 null 物件作為引數傳遞或接收作為返回值是合法的。事實上,這樣做很常見,例如表示空集或指示錯誤條件。

垃圾回收

[編輯 | 編輯原始碼]

在別名部分,我們討論了當多個變數引用同一個物件時會發生什麼。當沒有變數引用物件時會發生什麼?例如

    Point blank = new Point (3, 4);
    blank = null;

第一行建立一個新的 Point 物件並使 blank 指向它。第二行更改 blank,使其不再引用該物件,而是引用空值(null 物件)。

如果沒有人引用某個物件,那麼就沒有人可以讀取或寫入它的任何值,或者在其上呼叫方法。實際上,它將不復存在。我們可以將物件保留在記憶體中,但這隻會浪費空間,因此,在程式執行期間,Java 系統會定期查詢孤立的物件並回收它們,這個過程稱為垃圾回收。稍後,物件佔用的記憶體空間將可用於作為新物件的一部分使用。

您無需執行任何操作即可使垃圾回收生效,並且通常您不會注意到它。

物件和基本型別

[編輯 | 編輯原始碼]

Java 中有兩種型別的型別:基本型別和物件型別。基本型別,如 int 和 boolean,以小寫字母開頭;物件型別以大寫字母開頭。這種區別很有用,因為它提醒我們它們之間的一些差異。

當您宣告基本型別變數時,您會獲得用於儲存基本型別值的儲存空間。當您宣告物件變數時,您會獲得用於儲存對物件的引用的空間。為了獲得物件本身的空間,您必須使用 new 命令。

如果您沒有初始化基本型別,則會為其賦予一個預設值,該值取決於型別。例如,int 的預設值為 0,boolean 的預設值為 true。物件型別的預設值為 null,表示沒有物件。

基本型別變數很好地隔離,因為您無法在一個方法中執行任何操作來影響另一個方法中的變數。物件變數可能難以使用,因為它們沒有很好地隔離。如果您將對物件的引用作為引數傳遞,則呼叫的方法可能會修改該物件,在這種情況下,您將看到效果。當您在物件上呼叫方法時,情況也是如此。當然,這可能是件好事,但您必須意識到這一點。

基本型別和物件型別之間還有另一個區別。您不能向 Java 語言中新增新的基本型別(除非您加入標準委員會),但您可以建立新的物件型別!我們將在下一章中看到如何操作。

術語表

[編輯 | 編輯原始碼]
  • 包(package):類的集合。內建 Java 類按包組織。
  • AWT:抽象視窗工具包(Abstract Window Toolkit),Java 中最大且最常用的包之一。
  • 例項:類別中的一個例子。我的貓是“貓科動物”類別的一個例項。每個物件都是某個類的例項。
  • 例項變數:構成物件的一個命名資料項。每個物件(例項)都擁有其類例項變數的副本。
  • 引用:指示物件的數值。在狀態圖中,引用顯示為箭頭。
  • 別名:兩個或多個變數引用同一個物件的情況。
  • 垃圾回收:查詢沒有引用的物件並回收其儲存空間的過程。
  • 狀態:在程式執行的給定時間點,所有變數和物件及其值的完整描述。
  • 狀態圖:以圖形方式顯示的程式狀態快照。
華夏公益教科書