Java核心卷1知識點整理——第四章 物件與類(4.1-4.3)

kirwan發表於2020-12-20

4.1 物件導向設計概述

4.1.1 類

Java所有編寫的程式碼都位於某個類的內部。

OOP的一個原則是封裝(encapsulation),封裝就是將資料和方法組合在一個類中,並且對使用者隱藏資料的實現方式,不讓使用者具有直接訪問資料的許可權,只能使用類中的方法對資料進行操作。

另一個原則是可以擴充套件一個類來建立另一個新類。在擴充套件一個已有的類時,擴充套件後的新類會具有所擴充套件的類的全部資料和方法,在新的類中,只需要提供適用於新類的新方法和資料域就可以了。這個過程稱為繼承(inheritance)

Java中,所有的類都源自於一個超類Object。

例項域:物件中的資料

方法:操縱資料的過程

狀態:每個類的例項的一組特定的例項域值的集合

4.1.2 物件

物件的三個特性:

  • 物件的行為(behavior)
  • 物件的狀態(state)
  • 物件標識(identity)

物件的行為是通過可呼叫的方法所定義的。

物件的狀態是儲存該物件描述當前特徵的資料資訊。狀態是可以隨著時間改變的,但是不會自發改變,必須通過呼叫方法來改變物件的狀態。(如果不經過方法呼叫就可以改變物件狀態,說明封裝性遭到了破壞)

每個物件都具有唯一的身份,即使兩個物件的狀態完全一樣,它們的身份也是不同的。

每個物件的身份標識永遠是不同的,狀態常常也存在差異。

4.1.4 類之間的關係

在類之間,常見的關係有:

  • 依賴(“uses-a”)——一個類需要操縱另一個類的物件
  • 聚合(“has-a”)——一個類的物件包含另一個類的物件
  • 繼承(“is-a”)——一個類擴充套件了另一個類

4.2 使用預定義類

4.2.1 物件與物件變數

使用物件,使用***構造器(constructor)***構造新例項。構造器是一種特殊的方法,用來構造並初始化物件,然後才可以對物件應用方法。

構造器名字和類名必須相同。構造物件在構造器前面加一個new操作符,如下:

new Date()

這個表示式構造了一個新物件。這個物件被初始化為當前的日期和時間。

如果需要的話,也可以將這個物件傳遞給一個方法

System.out.println(new Date());

或者,也可以將一個方法應用於剛剛建立的物件Date類中有一個toString方法。這個方法將返回日期的字串描述。

String s = new Date().toString();

這兩個例子中,構造的物件只用了一次。如果希望構造的物件多次使用,可以將物件存放在一個變數中

Date birthday = new Date();

在這裡插入圖片描述

定義一個物件變數,需要進行初始化才可以使用。否則會產生編譯錯誤。

Date deadline; // deadline doesn't refer to any object
s = deadline.toString(); // not yet

初始化有兩種選擇,一種是使用新構造的物件初始化這個變數

deadline = new Date();

另一種是讓這個變數引用一個已存在的物件

deadline = birthday;

現在兩個變數引用同一個物件。

在這裡插入圖片描述

一個物件變數並沒有包含一個物件,只是引用一個物件。

在Java中,任何物件變數的值都是對儲存在另一個地方的一個物件的引用。new操作符的返回值也是一個引用:

Date deadline = new Date();

表示式new Date()構造了一個Date型別的物件,並且它的值是對新建立物件的引用。這個引用儲存在變數deadline中。

如果將一個方法應用於一個值為null的物件上,那麼就會產生執行時的錯誤:

Date birthday = null;
String s = birthday.toString(); // runtime error!!

區域性變數不會自動初始化為null,而必須通過呼叫new或將它們設定為null進行初始化。(原因

4.2.3 更改器方法與訪問器方法

更改器方法:修改物件的方法

訪問器方法:只訪問物件而不修改物件的方法

使用LocalDate類列印當前月的日曆,並且在當前的日後面用*標記

Class CalendarTest {
  public static void main(String[] args) {
    LocalDate date = LocalDate.now();
    int month = date.getMonthValue();
    int today = date.getDayOfMonth();
    
    date = date.minusDays(today-1); // 將日期設定為本月的第一天
    int value = date.getDayOfWeek().getValue();
    for(int i = 1; i < value; i++) {
      System.out.print("    ");
    }
    // 不論一個月有多少天,只要判斷當前日期還在本月內即可
    while(date.getMonthValue() == month) {
      System.out.printf("%3d", date.getDayOfMonth());
      if(date.getDayOfMonth() == today) {
        System.out.print("*");
      } else {
        System.out.print(" ");
      }
      date = date.plusDays(1);
      if(date.getDayOfWeek().getValue() == 1) System.out.println(); // 如果到了新的一週就換行
    }
  }
}

該示例程式的重點是展示如何使用一個類的介面來完成任務,並無須瞭解實現細節。

4.3 使用者自定義類

學習如何設計複雜應用所需要的主力類(workhorse class)。通常,這些類是沒有main方法的,卻有自己的例項域和例項方法。建立一個完整的程式,應該將若干類組合在一起,其中只有一個類有main方法。

4.3.1 Employee類

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

class ClassName {
	field1
  field2
  ...
  constructor1
  constructor2
  ...
  method1
  method2
  ...
}

下面是一個非常簡單的Employee類:

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類的具體使用:

public class EmployeeTest {
  public static void main(String[] args) {
    // 建立一個Employee陣列,並填入三個僱員物件
    Employee[] staff = new Employee[3];
    
    staff[0] = new Employee("Carl Cracker", 75000, 1987, 12, 15);
    staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
    staff[2] = new Employee("Tony Tester", 4000, 1980, 3, 15);
    
    // 給每個員工漲薪5%
    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;
  }
}

一個原始檔中只能有一個公有類,但可以有任意數目的非公有類。原始檔的檔名必須和公有類的類名一致。

當編譯這段程式碼的時候,編譯器會在目錄下建立兩個檔案:EmployeeTest.classEmployee.class

將程式中包含main方法的類名提供給位元組碼直譯器,以便啟動這個程式:

java EmployeeTest

4.3.2 多個原始檔的使用

在上面的程式碼中,一個原始檔包含了兩個類。通常程式設計師習慣將每一個類存在一個單獨的原始檔中。例如,將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編譯器會自動重新編譯這個檔案。

4.3.3 剖析Employee類

先看這個類的方法:

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 意味著任何類的任何方法都可以呼叫這些方法

接下來看這個類的例項域:

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

關鍵字private確保只有Employee類自身的方法能夠訪問這些例項域, 而其他類的方法不能夠讀寫這些域。

可以注意到,有兩個例項域本身就是物件:name域是String類物件,hireDay域是LocalDate類物件。這種情形十分常見:類通常包含型別屬於某個類型別的例項域

4.3.4 構造器

Employee類的構造器:

public Employee(String s, 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); // 1950年1月1日

構造器與其他的方法有一個重要的不同:構造器總是伴隨著new操作符的執行被呼叫,而不能對一個已經存在的物件呼叫構造器來達到重新設定例項域的目的。例如:

james.Employee("James Bond", 250000, 1950, 1, 1) // Error

這樣做將會產生編譯錯誤。

需要記住:

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

警告⚠️:請注意, 不要在構造器中定義與例項域重名的區域性變數。

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

這個構造器宣告瞭區域性變數 namesalary。這些變數只能在構造器內部訪問。這些變數遮蔽了同名的例項域。

4.3.5 隱式引數與顯式引數

方法用來操作物件以及存取它們的例項域。例如:

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

將呼叫這個方法的物件的salary例項域設為新值。例如:

number007.raiseSalary(5);

這個呼叫將執行以下指令:

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

raiseSalary方法有兩個引數。第一個引數稱為隱式引數,是出現在方法名前的Employee類物件。第二個引數是位於方法名後面括號的數值,這是一個顯式引數。(也可以把隱式引數稱為方法呼叫的目標或接受者)

在每一個方法中,關鍵字this表示隱式引數,可以用以下方式編寫raiseSalary方法:

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

這樣可以將例項域和區域性變數明顯區分開。

4.3.6 封裝的優點

最後,再看一下getNamegetSalarygetHireDay方法:

public String getName() {
  return name;
}
public double getSalary() {
  return salary;
}
public LocalDate getHireDay() {
  return hireDay;
}

這些都是典型的訪問器方法,由於它們只返回例項域值,因此又稱為域訪問器。

name是一個只讀域,在構造器中設定完畢後,就沒有任何一個辦法可以對它進行修改,這樣確保name域不會受到外界破壞。

雖然salary不是隻讀域,但是它只能通過raiseSalary方法進行修改。如果一旦這個域值出現了錯誤,只要除錯這個方法就可以了。如果salarypublic的,那麼破壞這個域值的搗亂者可能出現在任何地方。

如果需要獲得或者設定例項域的值,需要提供下面三項內容:

  • 一個私有的資料域
  • 一個公有的域訪問器方法
  • 一個公有的域更改器方法

這樣做的好處:

  • 可以改變內部的實現,除了該類的方法,而不會影響其他程式碼。例如,將儲存名字改為:

    String firstName

    String lastName

    那麼getName方法可以改為返回 firstName+ " " + lastName

    對於這點改變,程式的其他部分是完全不可見的。

  • 在更改器方法中可以執行錯誤檢查,然而直接對域進行賦值將不會進行這些處理。例如,setSalary方法可以檢查薪金是否小於0。

警告⚠️:注意不要編寫返回引用可變物件的訪問器方法。在 Employee 類中就違反了這個設計原則, 其中的 getHireDay 方法返回了一個 Date 類物件:

class Employee {
  private Date hireDay;
  ...
  public Date getHireDay() {
    return hireDay(); // Bad!!
  }
}

LocalDate 類沒有更改器方法, 與之不同, Date 類有一個更改器方法 setTime, 可以在這裡設定毫秒數。

Date物件是可變的,這一點破壞了封裝性。

如果需要返回一個可變物件的引用, 應該首先對它進行克隆(clone )。物件 clone 是指存放在另一個位置上的物件副本。 下面是修改後的程式碼:

class Employee {
  ...
  public Date getHireDay() {
    return (Date) hireDay.clone(); // Ok
  }
}
4.3.7 基於類的訪問許可權

一個方法可以訪問所屬類的所有物件的物件私有資料

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

典型的呼叫方式:

if(harry.equals(boss))...

boss是Employee類物件,而Employee類的方法可以訪問Employee類的任何一個物件的私有域

4.3.8 私有方法

儘管絕大多數方法都被設計為公有的,但在某些特殊情況下,也可能將它們設計為私有的。有時,可能希望將一個計算程式碼劃分成若干個獨立的輔助方法。通常,這些輔助方法不應該成為公有介面的一部分,最好將這樣的方法設計為 private 的。

4.3.9 final例項域

可以將例項域定義為final構建物件時必須初始化這樣的域。也就是說,必須確保在每一個構造器執行後,這個域的值被設定。並且在後面的操作中,不能夠再對它進行修改

final修飾符大都應用於**基本(primitive)型別域,或不可變(immutable)**類域

對於可變的類, 使用final修飾符可能會對讀者造成混亂。例如

private final StringBuilder evaluations;

Employee構造器中會初始化為

evaluations = new StringBuilder();

final關鍵字只是表示在evaluations變數中的物件引用不會再指示其他StringBuilder物件。不過這個物件可以更改:

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

相關文章