Java 之道/方法
在上一章中,我們在處理非整數時遇到了一些問題。我們透過測量百分比而不是分數來解決問題,但更通用的解決方案是使用浮點數,它們可以表示分數和整數。在 Java 中,浮點型別稱為 double。
您可以建立浮點變數並使用與其他型別相同的語法為它們賦值。例如
double pi;
pi = 3.14159;
同時宣告變數併為其賦值也是合法的
int x = 1;
String empty = "";
double pi = 3.14159;
實際上,這種語法非常常見。組合的宣告和賦值有時被稱為初始化。
雖然浮點數很有用,但它們通常是混淆的根源,因為整數和浮點數之間似乎存在重疊。例如,如果你有值 1,它是整數、浮點數還是兩者兼而有之?
嚴格來說,Java 區分整數 1 和浮點數 1.0,即使它們看起來是同一個數字。它們屬於不同的型別,嚴格來說,不允許在型別之間進行賦值。例如,以下操作是非法的
int x = 1.1;
因為左側的變數是 int,而右側的值是 float。但很容易忘記這條規則,尤其是因為在某些情況下,Java 會自動從一種型別轉換為另一種型別。例如
double y = 1;
理論上不應該合法,但 Java 透過自動將 int 轉換為 double 來允許它。這種寬鬆的處理很方便,但它會導致問題;例如
double y = 1 / 3;
你可能會期望變數 y 被賦予值 0.333333,這是一個合法的浮點值,但實際上它將得到值 0.0。原因是右側的表示式似乎是兩個整數的比率,因此 Java 執行整數除法,這會產生整數 0。轉換為浮點,結果為 0.0。
解決此問題(一旦你弄清楚它是什麼)的一種方法是使右側成為浮點表示式
double y = 1.0 / 3.0;
這將 y 設定為 0.333333,如預期的那樣。
我們已經看到的所有運算——加法、減法、乘法和除法——也適用於浮點值,儘管你可能想知道底層機制是完全不同的。實際上,大多數處理器都有專門的硬體用於執行浮點運算。
正如我提到的,Java 會在必要時自動將 int 轉換為 double,因為轉換過程中不會丟失任何資訊。另一方面,從 double 到 int 的轉換需要舍入。Java 不會自動執行此操作,以確保你作為程式設計師意識到數字小數部分的丟失。
將浮點值轉換為整數最簡單的方法是使用型別轉換。型別轉換之所以得名,是因為它允許你取屬於一種型別的值並將其轉換為另一種型別(在塑造或重塑的意義上,而不是丟擲)。
不幸的是,型別轉換的語法很醜陋:你將型別名稱放在括號中,並將其用作運算子。例如
int x = (int) Math.PI;
(int) 運算子的作用是將後續的內容轉換為整數,因此 x 接收的值為 3。
型別轉換優先於算術運算,因此在以下示例中,PI 的值首先被轉換為整數,因此 Java 發現了一個錯誤,因為 PI 的轉換值在乘以 20.0 時被轉換為 double,它不是 int 型別。
int x = (int) Math.PI * 20.0;
轉換為整數總是向下舍入,即使小數部分為 0.99999999。
這兩個屬性(優先順序和舍入)會使型別轉換變得很麻煩。
Java 提供了一組內建函式,包括你能想到的大多數數學運算。這些函式稱為方法。大多數數學方法對 double 進行操作。
數學方法使用與我們已經看到的列印命令類似的語法呼叫
double root = Math.sqrt (17.0);
double angle = 1.5;
double height = Math.sin (angle);
第一個示例將 root 設定為 17 的平方根。第二個示例找到 1.5 的正弦,即變數 angle 的值。Java 假設你與 sin 及其他三角函式(cos、tan)一起使用的值以弧度表示。如果你需要將度數轉換為弧度
double degrees = 90;
double angle = degrees * 2 * Math.PI / 360.0;
注意 PI 全部是大寫字母。Java 不識別 Pi、pi 或 pie。
Math 類中另一個有用的方法是 round,它將浮點值舍入到最接近的整數並返回一個 int。
int x = (int) Math.round (Math.PI * 20.0);
在這種情況下,乘法先發生,然後呼叫方法。結果是 63(從 62.8319 上舍入)。
就像數學函式一樣,Java 方法可以組合,這意味著你可以將一個表示式用作另一個表示式的一部分。例如,你可以使用任何表示式作為方法的引數
double x = Math.cos (angle + Math.PI/2);
此語句獲取 Math.PI 的值,將其除以二並將結果新增到變數 angle 的值。然後將總和作為引數傳遞給 cos 方法。(注意 PI 是變數名,而不是方法名,因此沒有引數,甚至沒有空引數 ())。
你也可以獲取一個方法的結果並將它作為引數傳遞給另一個方法
double x = Math.exp (Math.log (10.0)):
在 Java 中,log 函式始終使用以 10 為底,因此此語句找到 10 的以 10 為底的對數。結果被分配給 x;我希望你知道它是什麼。
到目前為止,我們只使用內置於 Java 的方法,但也可以新增新方法。實際上,我們已經看到一個方法定義:main。名為 main 的方法很特殊,因為它指示程式執行從哪裡開始,但 main 的語法與其他方法定義相同
public static void NAME ( LIST OF PARAMETERS )
STATEMENTS
你可以為你的方法起任何你喜歡的名字,但不能將其命名為 main 或任何其他 Java 關鍵字。引數列表指定為了使用(或呼叫)新函式需要提供什麼資訊(如果有的話)。
main 的唯一引數是 String[] args,它表明任何呼叫 main 的人都必須提供一個字串陣列(我們將在陣列章節中討論陣列)。我們將編寫的第一個方法沒有引數,因此語法如下
public static void newLine ()
System.out.println ("");
此方法名為 newLine,空括號表示它不接受任何引數。它只包含一條語句,該語句列印一個空字串,用 "" 表示。列印沒有字母的字串可能看起來不是那麼有用,但請記住,println 在列印後跳到下一行,因此此語句的作用是跳到下一行。
在 main 中,我們可以使用類似於呼叫內建 Java 命令的方式來呼叫此新方法
public static void main (String[] args)
System.out.println ("First line.");
newLine ();
System.out.println ("Second line.");
此程式的輸出為
First line. Second line.
注意兩行之間的額外空格。如果我們想要在行之間有更多空格呢?我們可以重複呼叫相同的方法
public static void main (String[] args)
System.out.println ("First line.");
newLine ();
newLine ();
newLine ();
System.out.println ("Second line.");
或者我們可以編寫一個名為 threeLine 的新方法,它列印三行新行
public static void threeLine ()
newLine (); newLine (); newLine ();
public static void main (String[] args)
System.out.println ("First line.");
threeLine ();
System.out.println ("Second line.");
您應該注意有關此程式的幾件事
- 您可以重複呼叫相同的過程。實際上,這樣做很常見也很有用。
- 您可以讓一個方法呼叫另一個方法。在這種情況下,main 呼叫 threeLine,threeLine 呼叫 newLine。同樣,這也是很常見且有用的。
- 在 threeLine 中,我將三條語句都寫在一行上,這在語法上是合法的(請記住,空格和新行通常不會改變程式的含義)。
另一方面,通常將每個語句放在單獨的一行上是一個更好的主意,以便使您的程式易於閱讀。我在這本書中有時會違反這條規則以節省空間。
到目前為止,可能還不清楚為什麼建立所有這些新方法值得如此麻煩。實際上,有很多原因,但此示例只演示了兩個
- 建立新方法為您提供了一個機會,可以為一組語句命名。方法可以透過將複雜的計算隱藏在一個命令後面,以及使用英語單詞代替神秘的程式碼來簡化程式。哪個更清楚,newLine 還是 System.out.println ("")?
- 建立新方法可以透過消除重複程式碼來使程式更小。例如,您將如何列印九個連續的新行?您可以只調用 threeLine 三次。
類
[edit | edit source]將上一節中的所有程式碼片段整合在一起,整個類定義如下
class NewLine
public static void newLine ()
System.out.println ("");
public static void threeLine ()
newLine (); newLine (); newLine ();
public static void main (String[] args)
System.out.println ("First line.");
threeLine ();
System.out.println ("Second line.");
第一行表示這是名為 NewLine 的新類的類定義。類是相關方法的集合。在本例中,名為 NewLine 的類包含三個方法,分別名為 newLine、threeLine 和 main。
我們見過的另一個類是 Math 類。它包含名為 sqrt、sin 以及許多其他方法。當我們呼叫數學函式時,我們必須指定類名(Math)和函式名。這就是為什麼內建方法和我們編寫的程式碼的語法略有不同的原因
Math.pow (2.0, 10.0);
newLine ();
第一個語句呼叫 Math 類中的 pow 方法(將第一個引數提升到第二個引數的冪)。第二個語句呼叫 newLine 方法,Java 假設(正確地)它在 NewLine 類中,這就是我們正在編寫的。
如果您嘗試從錯誤的類呼叫方法,編譯器將生成錯誤。例如,如果您輸入
pow (2.0, 10.0);
編譯器會說類似“在 NewLine 類中找不到名為 pow 的方法”。如果您看到了此訊息,您可能想知道為什麼它在您的類定義中查詢 pow。現在你知道為什麼了。
具有多個方法的程式
[edit | edit source]當您檢視包含多個方法的類定義時,您可能會想從上到下閱讀它,但這很可能令人困惑,因為這不是程式執行的順序。
執行始終從 main 的第一條語句開始,無論它在程式中的什麼位置(在本例中,我故意將它放在底部)。語句按順序一次執行一條,直到遇到方法呼叫。方法呼叫就像執行流中的一個岔路。您不會轉到下一條語句,而是轉到被呼叫方法的第一行,執行那裡的所有語句,然後返回並從您離開的地方繼續執行。
聽起來很簡單,除了您必須記住一個方法可以呼叫另一個方法。因此,當我們在 main 的中間時,我們可能必須離開並執行 threeLine 中的語句。但是當我們執行 threeLine 時,我們會被中斷三次以離開並執行 newLine。
newLine 本身呼叫內建方法 println,這會導致又一次岔路。幸運的是,Java 非常擅長跟蹤它在哪裡,因此當 println 完成後,它會在 newLine 中從它離開的地方繼續執行,然後回到 threeLine,最後回到 main,以便程式可以終止。
實際上,從技術上講,程式不會在 main 結束時終止。相反,執行從呼叫 main 的程式中它離開的地方繼續執行,該程式是 Java 直譯器。Java 直譯器負責處理刪除視窗和一般清理等事情,然後程式終止。
這個可悲故事的寓意是什麼?當您閱讀程式時,不要從上到下閱讀。相反,請遵循執行流程。
引數和引數
[edit | edit source]我們使用過的一些內建方法有引數,這些引數是您提供的值,讓方法可以完成它的工作。例如,如果您想找到一個數字的正弦,您必須指示該數字是什麼。因此,sin 以 double 值作為引數。要列印一個字串,您必須提供該字串,這就是 println 以 String 作為引數的原因。
有些方法不止一個引數,例如 pow,它接受兩個 double 值,底數和指數。
請注意,在每種情況下,我們不僅必須指定引數的數量,還要指定它們的型別。因此,當您編寫類定義時,引數列表指示每個引數的型別,這並不奇怪。例如
public static void printTwice (String phil)
System.out.println (phil);
System.out.println (phil);
此方法接受一個名為 phil 的引數,該引數的型別為 String。無論該引數是什麼(目前我們不知道是什麼),它都會被列印兩次。我選擇 phil 這個名字是為了表明您可以隨意命名引數,但一般情況下,您希望選擇比 phil 更具說明性的名稱。
為了呼叫此方法,我們必須提供一個 String。例如,我們可能有一個像這樣的 main 方法
public static void main (String[] args)
printTwice ("Don't make me say this twice!");
您提供的字串稱為引數,我們說引數被傳遞給方法。在本例中,我們正在建立一個包含文字“不要讓我重複說這句話!”的字串值,並將該字串作為引數傳遞給 printTwice,在那裡,與它的願望相反,它將被列印兩次。
或者,如果我們有一個 String 變數,我們可以使用它作為引數
public static void main (String[] args)
String argument = "Never say never.";
printTwice (argument);
請注意這裡非常重要的一點:我們作為引數傳遞的變數的名稱(argument)與引數的名稱(phil)無關。再說一遍
我們作為引數傳遞的變數的名稱與引數的名稱無關。
它們可以相同,也可以不同,但重要的是要意識到它們不是一回事,除了它們碰巧具有相同的值(在本例中是字串“永遠不要說永遠”)。
您作為引數提供的值必須與您呼叫的方法的引數具有相同的型別。這條規則非常重要,但它在 Java 中經常變得複雜,原因有兩個
- 有些方法可以接受具有許多不同型別的引數。例如,您可以將任何型別傳送到 print 和 println,它會根據需要做正確的事情,無論是什麼。不過,這類情況是例外。
- 如果您違反了這條規則,編譯器通常會生成一條令人困惑的錯誤訊息。它不會說類似“您正在將錯誤型別的引數傳遞給此方法”的話,而是可能會說類似它找不到具有該名稱並且可以接受該型別引數的方法。不過,一旦您看到此錯誤訊息幾次,您就會弄清楚如何解釋它。
堆疊圖
[edit | edit source]
引數和其他變數只存在於它們自己的方法中。在 main 的範圍內,沒有 phil 這樣的東西。如果您嘗試使用它,編譯器會抱怨。同樣,在 printTwice 內部,沒有 argument 這樣的東西。
對於每個方法,都有一個稱為幀的灰色框,其中包含該方法的引數和區域性變數。方法的名稱出現在框架之外。像往常一樣,每個變數的值都繪製在一個框中,變數名稱位於該框旁邊。
具有多個引數的方法
[edit | edit source]宣告和呼叫具有多個引數的方法的語法是錯誤的常見來源。首先,請記住您必須宣告每個引數的型別。例如
public static void printTime (int hour, int minute)
System.out.print (hour);
System.out.print (":");
System.out.println (minute);
寫成 int hour, minute 可能很誘人,但這種格式只適用於變數宣告,不適用於引數。
另一個常見的困惑是,您不必宣告引數的型別。以下程式碼是錯誤的!
int hour = 11;
int minute = 59;
printTime (int hour, int minute); // WRONG!
在這種情況下,Java 可以透過檢視 hour 和 minute 的宣告來判斷它們的型別。當您將它們作為引數傳遞時,包含型別是不必要的,也是非法的。正確的語法是:printTime (hour, minute)。
作為練習,繪製一個使用引數 11 和 59 呼叫的 printTime 的堆疊幀。
您可能已經注意到,我們使用的一些方法(例如 Math 方法)會產生結果。其他方法(例如 println 和 newLine)會執行某些操作,但不會返回值。這會引發一些問題。
- 如果您呼叫一個方法但沒有對結果做任何操作(即沒有將其分配給變數或用作更大表達式的一部分),會發生什麼?
- 如果您將列印方法用作表示式的部分,例如 System.out.println ("boo!") + 7,會發生什麼?
- 我們可以編寫產生結果的方法,還是隻能使用 newLine 和 printTwice 之類的方法?
第三個問題的答案是“是的,您可以編寫返回結果的方法”,我們將在接下來的幾章中進行介紹。我會讓您嘗試回答另外兩個問題。實際上,如果您對 Java 中什麼合法或非法有任何疑問,一個好的方法是詢問編譯器。
- 浮點數:一種可以包含分數和整數的變數(或值)型別。在 Java 中,這種型別稱為 double。
- 類 一組命名的方法。到目前為止,我們已經使用了 Math 類和 System 類,並且編寫了名為 Hello 和 NewLine 的類。
- 方法 一系列命名語句,用於執行某些有用的功能。方法可以接受引數,也可以不接受引數,可以產生結果,也可以不產生結果。
- 引數 用於呼叫方法的資訊。引數類似於變數,因為它們包含值並具有型別。
- 引數 呼叫方法時提供的數值。此值必須與相應的引數具有相同的型別。
- 呼叫 導致執行一個方法。
