面向对象思考
本章主要讲述问题求解和基本程序设计的技术,以体会面向过程和面向对象程序设计的不同之处。我们的焦点放在类的设计上,通过几个例子来诠释面向对象方法的优点,这些例子包括如何在应用程序中设计新类、如何使用这些类。通过这些案例的学习来学会如何高效的使用面向程序设计。
类的抽象和封装
类抽象(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; } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!