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

前言

  之前的例子中,我們已經編寫了一些簡單的類。但是,那些類都只包含一個簡單的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")
}
Tags: