零基礎學Java(11)自定義類

2022-07-26 12:00:54

前言

  之前的例子中,我們已經編寫了一些簡單的類。但是,那些類都只包含一個簡單的main方法。現在來學習如何編寫複雜應用程式所需要的那種主力類。通常這些類沒有main方法,卻有自己的範例欄位和實體方法。要想構建一個完整的程式,會結合使用多個類,其中只有一個類有main方法。
 

自定義簡單的類

  在Java中,最簡單的類定義形式為:

class ClassName {
    // 欄位
    field1
    field2
    ...
    // 構造方法
    constructor1
    constructor2
    ...
    // 普通方法
    method1  
    method2
    ...
}

  接下來將上面的虛擬碼填充完整

class Employee {
    private String name;
    private double salary;
    private LocalDate hireDay;
    
    // constructor
    public Emploee(String n, double s, int year, int month, int day) {
        name = n;
        salary = s;
        hireDay = LocalDate.of(year, month, day);
    }
    
    public String getName() {
        return name;
    }
}

  上面就是我們定義的一個普通的類,分為3個部分,變數 + 構造器 + 方法,下面我們編寫一個完整的程式,最後輸出員工的名字、薪水和出生日期

檔案:EmployeeTest/EmployeeTest.java


import java.time.LocalDate;


public class EmployeeTest {
    public static void main(String[] args) {
        Employee[] staff = new Employee[3];

        staff[0] = new Employee("jkc1", 75000, 1987, 12, 15);
        staff[1] = new Employee("jkc2", 50000, 1987, 10, 1);
        staff[2] = new Employee("jkc3", 40000, 1990, 3, 15);

        for (Employee e: staff) {
            e.raiseSalary(5);
        }

        for (Employee e: staff) {
            System.out.println("name=" + e.getName() + ", salary=" + e.getSalary() + ", hireDay=" + e.getHireDay());
        }
    }
}
class Employee {
    private String name;
    private double salary;
    private LocalDate hireDay;

    public Employee(String n, double s, int year, int month, int day) {
        name = n;
        salary = s;
        hireDay = LocalDate.of(year, month, day);
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public LocalDate getHireDay() {
        return hireDay;
    }

    public void raiseSalary(double byPercent) {
        double raise = salary * byPercent / 100;
        salary += raise;
    }
}

  在這個程式中,我們構造了一個Employee陣列,並填入了3個Employee物件:

Employee[] staff = new Employee[3];

staff[0] = new Employee("jkc1", 75000, 1987, 12, 15);
staff[1] = new Employee("jkc2", 50000, 1987, 10, 1);
staff[2] = new Employee("jkc3", 40000, 1990, 3, 15);

  接下里,使用Employee類的raiseSalary方法將每個員工的薪水提高5%:

for (Employee e: staff) {
    e.raiseSalary(5);
}

  最後呼叫getName方法、getSalary方法和getHireDay方法列印各個員工的資訊:

for (Employee e: staff) {
    System.out.println("name=" + e.getName() + ", salary=" + e.getSalary() + ", hireDay=" + e.getHireDay());
}

  注意,在這個範例程式中包含兩個類:Employee類和帶有public存取修飾符的EmployeeTest類。EmployeeTest類包含了main方法,其中使用了前面介紹的指令。
  原始檔名是EmployeeTest.java,這是因為檔名必須與public類的名字相匹配。在一個原始檔中,只能有一個公共類,但可以有任意數目的非公共類。
  接下來,當編譯這段原始碼的時候,編譯器將在目錄下建立兩個類檔案:EmployeeTest.classEmployee.class
  將程式中包含main方法的類名提供給位元組碼直譯器,以啟動這個程式:

java EmployeeTest

  位元組碼直譯器開始執行EmployeeTest類的main方法中的程式碼。在這段程式碼中,先後構造了3個新的Employee物件,並顯示它們的狀態。
 

多個原始檔的使用

  上面那個程式包含了兩個類。我們通常習慣於將每一個類存放在一個單獨的原始檔中。例如:將Employee類存放在檔案Employee.java中,將EmployeeTest類存放在檔案EmployeeTest.java中。
  如果喜歡這樣組織檔案,可以有兩種編譯源程式的方法。一種是使用萬用字元呼叫Java編譯器:

javac Employee*.java

  這樣一來,所有與萬用字元匹配的原始檔都將被編譯成類檔案。或者寫以下命令:

javac EmployeeTest.java

  雖然我們第二種方式並沒有顯示地編譯Employee.java,但當Java編譯器發現EmployeeTest.java使用了Employee類時,它會查詢名為Employee.class的檔案。如果沒有找到這個檔案,就會自動搜尋Employee.java,然後對它進行編譯。更重要的是:如果Employee.java版本較已有的Employee.class檔案版本更新,Java編譯器就會自動地重新編譯這個檔案。

剖析Employee類

  Employee類包含一個構造器和4個方法:

public Employee(String n, double s, int year, int month, int day)
public String getName()
public double getSalary()
public LocalDate getHireDay()
public void raiseSalary(double byPercent)

  這個類的所有方法都被標記為public。關鍵字public意味著任何類的任何方法都可以呼叫這些方法。
  接下來,需要注意在Employee類的範例中有3個範例欄位用來存放將要操作的資料:

private String name;
private double salary;
private LocalDate hireDay;

關鍵字private確保只有Employee類自身的方法能夠存取這些範例欄位,而其他類的方法不能夠讀寫這些欄位。
 

構造器解析

  我們先看看Employee類的構造器:

public Employee(String n, double s, int year, int month, int day) {
    name = n;
    salary = s;
    hireDay = LocalDate.of(year, month, day);
}

  可以看到,構造器與類同名。在構造Employee類的物件時,構造器會執行,從而將範例欄位初始化為所希望的初始狀態。
  例如,當使用下面這條程式碼建立Employee類的範例時:

new Employee("James Bond", 100000, 1950, 1, 1)

  將會把範例欄位設定為:

name = "James Bond";
salary = 100000;
hireDay = LocalDate.of(1950, 1, 1);

  構造器與其他方法有一個重要的不同。構造器總是結合new運運算元來呼叫。不能對一個已存在的物件呼叫構造器來達到重新設定範例欄位的目的。例如:

james.Employee("James Bond", 280000, 1950, 1, 1)

將產生編譯錯誤
 
構造器注意點

  • 構造器與類必須同名
  • 每個類可以有一個以上的構造器。
  • 構造器可以有0個、1個或多個引數。
  • 構造器沒有返回值。
  • 構造器總是伴隨著new操作符一起呼叫。

 

用var變數宣告區域性變數

  在Java10中,如果可以從變數的初始值推匯出它們的型別,那麼可以用var關鍵字宣告區域性變數,而無須指定型別。例如,可以不這樣宣告:

Employee harry = new Employee("jkc", 50000, 1989, 10, 1);

  只需寫以下程式碼:

var harry = new Employee("jkc", 50000, 1989, 10, 1);

  這一點很好,因為可以避免重複寫型別名Employee
 

使用null參照

  我們之前瞭解到一個物件變數包含一個物件的參照,或者包含一個特殊值null,後者表示沒有參照任何物件。
  聽上去這是一種處理特殊情況的便捷機制,如未知的名字或僱用日期。不過使用null值時要非常小心。
  如果對null值應用一個方法,會產生一個NullPointerException異常。

LocalDate birthday = null;
String s = birthday.toString();  // NullPointerExcetion

  這是一個很嚴重的錯誤,類似於索引越界異常。如果你的程式沒有"捕獲"異常,程式就會終止。正常情況下,程式並不捕獲這些異常,而是依賴於我們從一開始就不要帶來異常。
  定義一個類時,最好清楚地知道哪些欄位可能為null。在我們例子中,我們不希望namehireDay欄位為null。(不用擔心salary欄位。這個欄位是基本型別,所以不可能是null)。
  hireDay欄位肯定是非null的,因為它初始化一個新的LocalDate物件。但是name可能為null,如果呼叫構造器時為n提供的實參是null,name就會是null.
  對此有兩種解決辦法。"寬容型"辦法是把null引數轉換為一個適當的非null值

if (n == null) name = "unknown"; else name = n;

 

隱式引數與顯式引數

  方法用於操作物件以及存取它們的範例欄位。例如,以下方法:

public void raiseSalary(double byPercent) {
    double raise = salary * byPercent / 100;
    salary += raise;
}

  上面的方式是呼叫這個方法的物件的sarlary範例欄位設定為一個新值。現在我們考慮下面這個呼叫:

number001.raiseSsalary(5);

  它的結果是將number001.salary欄位的值增加5%。具體的說,這個呼叫將執行下列指定。

double raise = number001.salary * 5 / 100;
number001.salary += raise;

  raiseSalary方法有兩個引數。第一個引數稱為隱式引數,是出現在方法名前的Employee型別的物件。第二個引數是位於方法名後面括號中的數值,這是一個顯式引數。(有人把隱式引數稱為方法呼叫的目標或者接受者)
  可以看到,顯式引數顯式地列在方法宣告中,例如double byPercent。隱式引數沒有出現在方法宣告中。
在每一個方法中,關鍵字this指示隱式引數。我們可以改寫raiseSalary方法:

public void raiseSalary(double byPercent) {
    double raise = this.salary * byPercent / 100;
    salary += raise;
}

 

封裝的優點

  最後我們再仔細看一下非常簡單的getName方法、getSalary方法和getHireDay方法。

public String getName() {
        return name;
    }

public double getSalary() {
    return salary;
}

public LocalDate getHireDay() {
    return hireDay;
}

  這些都是典型的存取器方法。由於它們只返回範例欄位值,因此又稱為欄位存取器
  如果將namesalaryhireDay欄位標記為公共,而不是編寫單獨的存取器方法,難道不是更容易一些嗎?
  不過,name是一個唯讀欄位。一旦在構造器中設定,就沒有任何辦法可以對它進行修改,這樣我們可以確保name欄位不受外界的破壞。
  雖然salary不是唯讀欄位,但是它只能用raiseSalary方法修改。特別是一旦這個值出現了錯誤,只需要偵錯這個方法就可以了。如果salary欄位是公共的,破壞這個欄位值的搗亂者有可能會出沒在任何地方。
  有些時候,可能想要獲得或設定範例欄位的值。那麼你需要提供下面三項內容:

  • 一個私有的資料欄位;
  • 一個公共的欄位存取器方法;
  • 一個公共的欄位更改器方法。

  這樣做比提供一個簡單的公共資料欄位複雜些,但卻有著下列明顯的好處:
  首先,可以改變內部實現,而除了該類的方法之外,這不會影響其他程式碼。例如,如果將儲存名字的欄位改為:

String firstName;
String lastName;

  那麼getName方法可以改為返回

firstName + " " + lastName

  這個改變對於程式的其他部分是完全不可見的。
  當然,為了進行新舊資料表示之間的轉換,存取器方法和更改器方法可能需要做許多工作。但是,這將為我們帶來第二點好處:更改器方法可以完成錯誤檢查,而只對欄位賦值的程式碼可能沒有這個麻煩。例如,setSalary方法可以檢查工資是否小於0。
 

注意:不要編寫返回可變物件參照的存取器方法,如果你需要返回一個可變物件的參照,那麼應該對它進行克隆。
 

基於類的存取許可權

  從前面已經知道,方法可以存取呼叫這個方法的物件的私有資料。一個方法可以存取所屬類的所有物件的私有資料,這令很多人感到奇怪!例如,下面看一下用來比較兩個員工的equals方法。

class Employee{
    ...
    public boolean equals(Employee other) {
        return name.euqals(other.name)
    }
}

  典型的呼叫方式是

if (harry.euqals(boss))...

  這個方法存取harry的私有欄位,這點並不會讓人奇怪,不過, 它還存取了boss的私有欄位。這是合法的,其原因是bossEmployee型別的物件,而Employee類的方法可以存取任何Employee型別物件的私有欄位。
 

私有方法

  在實現一個類時,由於公共資料非常危險,所以應該將所有的資料欄位都設定為私有的。然而,方法又應該如何設計呢?儘管絕大多數方法都被設計為公共的,但在某些特殊情況下,將方法設計為私有可能很有用。有時,你可能希望將一個計算程式碼分解成若干個獨立的輔助方法,通常,這些輔助方法不應該成為公共介面的一部分,這是由於它們往往與當前實現關係非常緊密,或者需要一個特殊協定或者呼叫次序。最好將這樣的方法設計為私有方法。
 
  在Java中,要實現私有方法,只需將關鍵字public改成private即可。
  通常將方法設計為私有,如果你改變了方法的實現方式,將沒有義務保證這個方法依然可用。如果資料的表示發生了變化,這個方法可能會變得難以實現,或者不再需要;這並不重要。重點在於,只要方法是私有的,類的設計者就可以確信它不會在別處使用,所以可以將其山區。如果一個方法是公共的,就不能簡單地將其刪除,因為可能會有其他程式碼依賴這個方法。
 

final範例欄位

  可以將範例欄位定義為final。這樣的自動斷必須在構造物件時初始化。也就是說,必須確保在每一個構造器執行之後,這個欄位的值已經設定,並且以後不能再修改這個欄位。例如,可以將Employee類中的name欄位宣告為final,因此在物件構造之後,這個值不會再改變,即沒有setName方法。

class Employee {
    private final String name;
}

  final修飾符對於型別為基本型別或者不可變類的欄位尤其有用(如果類中的所有方法都不會改變其物件,這樣的類就是不可變的類。例如,String類就是不可變的)
  對於可變的類,使用final修飾符可能會造成混亂。例如,考慮以下欄位:

private final StringBuilder evaluations;

  它在Employee構造器中初始化為

evaluations = new StringBuilder();

  final關鍵字只是表示儲存在evaluations變數中的物件參照不會再指示另一個不同的StringBuilder物件。不過這個物件可以更改:

public void giveGoldStar() {
  evaluations.append(LocalDate.now() + ":Gold star!\n")
}