本章主要講述問題求解和基本程序設計的技術,以體會面向過程和面向對象程序設計的不同之處。我們的焦點放在類的設計上,通過幾個例子來詮釋面向對象方法的優點,這些例子包括如何在應用程序中設計新類、如何使用這些類。通過這些案例的學習來學會如何高效的使用面向程序設計。
類的抽象和封裝
類抽象(class abstraction)是將類的實現和使用分離。類的創建者描述類的功能,讓使用者明白如何才能使用類。從類外可以訪問的方法和數據域的集合以及預期這些成員如何行為的描述稱為類的合約。
如圖所示,類的使用者不需要知道類是如何實現的。實現的細節經過封裝對用戶隱藏起來,稱為類的封裝。
類的抽象和封裝是一個問題的兩個方面。現實生活中很多例子可以說明類抽象的概念。例如:考慮建立一個計算機系統。計算機有很多組件 —— CPU、內存、磁盤、主板和風扇等。每個組件都可以看作是一個由屬性和方法的對象。要使各個組件一起工作,只需要知道每個組件是如何用的以及是如何與其它組件進行交互的,而無須了解這些組件內部是如何工作的。內部功能的實現被封裝起來,對使用者是隱藏的。所以,你可以組裝一台計算機,而不需要了解每個組件是如何實現的。
對計算機系統的模擬准確地反映了面向對象方法。每個組件可以看成組件類的對象。
再用我們作業中的一筆貸款作為另一個例子。一筆貸款可以看作貸款類Loan的一個對象,利率,貸款額以及還貸周期都是它的數據屬性,計算每月償還額和總償還額是它的行為(方法)。當你購買一個程序員鼓勵師的時候,就用貸款利率、貸款額和還貸周期實例化這個類,創建一個貸款對象。然后,就可以用這些方法計算貸款的月償還額和總償還額(再然后就是和鼓勵師過上“幸福”的生活)。作為一個貸款類Loan的用戶,是不需要知道這些方法是如何實現的。
假設希望將一個日期和這個貸款聯系起來。傳統的面向過程編程時動作驅動的,數據和動作時分離的。面向對象編程的范式重點在於對象,動作和數據一起定義在對象中。為了將日期和貸款聯系起來,可以定義一個貸款類,將日期和貸款的其它屬性作為數據域,並且貸款數據和動作在一個對象中完成。
將上面的UML圖看着Loan的合約,下面我們扮演Loan類的開發者
package edu.uestc.avatar; import java.time.LocalDate; /** * 貸款類合約 * */ public class Loan { /** * 貸款利率 */ private float annualInterestRate; /** * 貸款年限 */ private int numberOfYears; /** * 貸款金額 */ private float loanAmount; /** * 貸款日期 */ private LocalDate loanDate; /** * 無參構造方法 */ public Loan() { this(2.5f,1,10000); } public Loan(float annualInterestRate, int numberOfYears, float loanAmount) { this.annualInterestRate = annualInterestRate; this.numberOfYears = numberOfYears; this.loanAmount = loanAmount; this.loanDate = LocalDate.now(); } public float getAnnualInterestRate() { return annualInterestRate; } public void setAnnualInterestRate(float annualInterestRate) { this.annualInterestRate = annualInterestRate; } public int getNumberOfYears() { return numberOfYears; } public void setNumberOfYears(int numberOfYears) { this.numberOfYears = numberOfYears; } public float getLoanAmount() { return loanAmount; } public void setLoanAmount(float loanAmount) { this.loanAmount = loanAmount; } public LocalDate getLoanDate() { return loanDate; } public void setLoanDate(LocalDate loanDate) { this.loanDate = loanDate; } /** * 計算獲取月償還額度 * @return 月償還額 */ public float getMonthlyPayment() { float monthlyRate = this.annualInterestRate / 1200; //貸款本金×月利率×(1+月利率)^還款月數〕÷〔(1+月利率)^還款月數-1〕 return (float)(loanAmount * monthlyRate * Math.pow((1 + monthlyRate), numberOfYears * 12) / Math.pow((1 + monthlyRate), numberOfYears * 12 - 1)); } /** * 計算獲取貸款總償還額 * @return 總償還額 */ public double getTotalpayment() { return getMonthlyPayment() * numberOfYears * 12; } }
從類的開發者角度來看,設計類時為了讓很多不同的用戶所使用。為了在更大的應用范圍內使用類,類應通過構造方法、屬性和方法提供各種方式的定制
下面的程序扮演Loan的用戶(使用者)
package edu.uestc.avatar; public class LoanDemo { public static void main(String[] args) { Loan loan = new Loan(5.6f, 30, 5000000); System.out.println("貸款總金額:" + loan.getLoanAmount()); System.out.println("貸款年利率:" + loan.getAnnualInterestRate() + "%"); System.out.println("貸款總年限:" + loan.getNumberOfYears()); System.out.println("每月償還額度:" + loan.getMonthlyPayment()); System.out.println("總償還額度:" + loan.getTotalpayment()); } }
面向對象的思考
面向過程的范式重點在於設計方法。面向對象的范式將數據和方法耦合在一起構成對象。
通過改進我們前面使用程序設計給出的計算身體質量指數的程序來體會面向過程和面向對象程序設計的不同,也可以看出使用對象和類來開發可重用代碼的優勢
package edu.uestc.avatar; import java.util.Scanner; public class ComputeInterpretBMI { /** * 計算身體質量指數BMI * 計算公式:weight / 身高的平方 * BMI < 18.5 偏瘦 * 18.5 <= BMI < 25 正常 * 25 <= BMI < 30 偏胖 * BMI >= 30 過胖 */ public static void main(String[] args) { System.out.println("請輸入你的體重(kg):"); Scanner input = new Scanner(System.in); double weight = input.nextDouble(); System.out.println("請輸入你的身高(m):"); double height = input.nextDouble(); String state = ""; double bmi = weight / (height * height); if(bmi < 18.5) state = "偏瘦"; else if(bmi >= 18.5 && bmi < 25) state = "正常"; else if(bmi >= 25 && bmi < 30) state = "偏胖"; else state = "過胖"; input.close(); System.out.printf("你的身高為:%fm,體重為:%5fkg身體質量狀況:%2s",height,weight,state); } }
上面這個對於計算給定體重和身高的身體質量指數是很有用的。但是,它是有局限性的。假設需要將體重和身高同一個人的名字與出生日期關聯起來,雖然可以分別使用幾個變量來存儲這些值,但是這些值不是緊密耦合在一起的。將它們耦合在一起的理想方法就是創建一個包含它們的對象。因為這些值被綁定到單獨的對象上,所以它們應該存儲在實例數據域中。
package edu.uestc.avatar; public class BMI { private String name; private double height; private double weight; private int age; /** * 初始化年齡默認為20歲 */ public BMI(String name, double height, double weight) { //this在構造方法中可以調用其他構造方法,只能在開始的地方(第一句進行調用) this(name,height,weight,20); } public BMI(String name, double height, double weight, int age) { this.name = name; this.height = height; this.weight = weight; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getHeight() { return height; } public void setHeight(double height) { this.height = height; } public double getWeight() { return weight; } public void setWeight(double weight) { this.weight = weight; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public double getBMI() { return weight / (height * height); } public String getState() { double bmi = getBMI(); if(bmi < 18.5) return "偏瘦"; else if(bmi >= 18.5 && bmi < 25) return "正常"; else if(bmi >= 25 && bmi < 30) return "偏胖"; else return "過胖"; } }
這個例子演示了面向對象范式比面向過程有優勢的地方。面向過程的范式重點在於設計方法。面向對象的范式將數據和方法耦合在一起構成對象。使用面向對象程序設計的重點在對象和對象的操作。面向對象方法結合了面向過程范式的功能以及將數據和操作集成在對象中的特性。
在面向過程程序設計中,數據和數據上的操作是分離的,而且這種做法要求傳遞數據給方法。面向對象程序設計將數據和對它們的操作都放在一個對象中。這種方法解決了很多面向過程程序設計的固有問題。面向對象程序設計以一種反映真實世界的方式組織程序,在真實世界中,所有的對象和屬性及動作都相關聯。使用對象提高了軟件的可重用性,並且使程序更易於開發和維護。
類的關系
為了設計類,需要探究類之間的關系。類之間的關系通常是關聯、聚合、組合以及繼承。
關聯
關聯是一種常見的關系,描述兩個類之間的活動。例如在我們數據庫階段所使用的學員選課系統中學生選取課程是Student類和Course類之間的一種關聯,而教師教授課程是Faculty類和Course類之間的關聯。UML圖形標識如下:
該UML圖顯示學生可以選取任意數量的課程,教師最多可以教授3門課程,每門課程可以有5到60個學生,並且每門課程只由一位教師來教授。
關聯由兩個類之間的實線表示,可以有一個可選的標簽來描述關系(上圖中,標簽是Take和Teach)。每個關系可以有一個可選的小的黑色三角形表明關系的方向。
關系中涉及的每個類可以有一個角色名稱,描述在該關系中擔當的角色。Teacher是Faculty的角色名(Teacher是Faculty的角色名)。
關聯中涉及的每個類可以給定一個多重性,放置在類的邊上用於給定UML圖中關系所涉及的類的對象數。
關聯在java代碼中如何體現呢?可以通過使用數據域以及方法來實現關聯
注意:實現類之間的關系可以有很多種可能的方法。例如,Course中的學生和教師信息可以省略(單向關聯),因為它們已經在Student和Faculty中了。同樣的,如果不需要知道一個學生選取的課程或者教師教授的課程,Student或者Faculty類中的數據域courseList和addCourse方法也可以省略。
聚集和組合
聚集是關聯的一種特殊形式,代表了兩個對象之間的歸屬關系(整體與部分)。聚集建模has-a關系。所有者對象稱為聚集對象,它的類稱為聚集類。而從屬對象稱為被聚集對象,它的類稱為被聚集類。
一個對象可以被多個其他的聚集對象所擁有。如果一個對象只歸屬一個聚集對象,那么它和聚集類之間的關系就稱為組合(contains-a)。例如:“一個學生有一個名字”就是學生類Student與名字Name之間的一個組合關系,而“一個學生有一個地址”是學生類Student與地址類Address之間的一個聚集關系,因為一個地址可以被幾個學生所共享。在UML圖中用實心菱形表示組合,用空心菱形表示聚集:
聚集關系通常被表示為聚集類中的一個數據域:
聚集可以存在於同一個類的多個對象之間。例如,我們在練習Oracle的emp員工表時,一個員工可能有一個管理者。
public class Person{ private Person supervisor;
}
由於聚集和組合關系都以同樣的方式用類來表示,一般不區分,將兩者都稱為組合。
泛化(Generalization)
類之間繼承關系(參考下一章節:繼承和多態)。
示例學習:設計Course類與Student類
示例學習:設計棧類
棧(Stack)是一種以“先進后出”的方式存放數據的數據結構,如下圖所示:
package com.iweb.demo.client; /** * 自定義棧 * @author Adan * */ public class MyStack { /** * 存儲棧中數據的數組 */ private Object[] elements; /** * 棧中元素個數 */ private int size; public static final int DEFAULT_CAPACITY = 16; /** * 構建一個默認容量為16的空棧 */ public MyStack() { this(DEFAULT_CAPACITY); } /** * 構建一個指定大小的空棧 * @param capacity 容量大小 */ public MyStack(int capacity) { elements = new Object[capacity]; } /** * 將value壓入到棧中 * @param value value元素 */ public void push(Object value) { //棧如果存滿,需要為棧自動擴容 if(size >= elements.length) { Object[] temp = new Object[elements.length * 2]; System.arraycopy(elements, 0, temp, 0, elements.length); elements = temp; } elements[size++] = value; } /** * 彈出棧頂元素並將該元素返回 * @return 棧頂元素 */ public Object pop() {
if(empty()) throw new RuntimeException("沒有元素"); return elements[--size]; } /** * 查看棧頂元素,不刪除 * @return 棧頂元素 */ public Object peek() {
if(empty()) throw new RuntimeException("沒有元素"); return elements[size - 1]; } /** * 是否為一個空棧 * @return */ public boolean empty() { return size == 0; } /** * 獲取棧中元素個數 * @return 元素個數 */ public int getSize() { return size; } }