活动地址:CSDN21天学习挑战赛
3.1 封装class
首要解决的问题是,我们应该封装哪些类?要回答这个问题,我们应该考虑到的是,在这个问题中,涉及到的哪些内容或者责任。
例如:在一个餐厅中,有不同的角色组合在一起来提供各种不同的服务,每个人都有相应的岗位和职责,比如厨师、服务员和收银员等。这些人或者服务都可以说是对象。
为此,我们应该封装出一些类描述这些对象,每个类都应该担当单一和独立的责任
。依据这些特定的责任,我们可以尝试归纳出以下类:
-
Console:负责从控制台读取输入
-
MortgageReport:负责生成带有格式的报表
-
MortgageCalculator:负责计算房贷相关算法
3.2 Console
public class Console {
private static Scanner scanner = new Scanner(System.in);
public static double readNumber(String prompt) {
return scanner.nextDouble();
}
public static double readNumber(String prompt, double min, double max) {
double value;
while (true) {
System.out.print(prompt);
value = scanner.nextDouble();
if (value >= min && value <= max)
break;
System.out.println("输入的数字必须在" + min + "到" + max + "之间。");
}
return value;
}
}
3.3 MortgageReport
public class MortgageReport {
public static void printMortgage(int principal, float rate, byte years) {
double mortgage = MortgageCalculator.calculateMortgage(principal, rate, years);
String formatted = NumberFormat.getCurrencyInstance().format(mortgage);
System.out.println();
System.out.println("MORTGAGE");
System.out.println("--------");
System.out.println("Mortgage:" + formatted);
}
public static void printPaymentSchedule(int principal, float rate, byte years) {
System.out.println();
System.out.println("Payment Schedule");
System.out.println("-------");
for (int month = 1; month <= years * Main.MONTHS_IN_YEAR; month++) {
double balance = MortgageCalculator.calculateBalance(principal, rate, years, month);
System.out.println(NumberFormat.getCurrencyInstance().format(balance));
}
}
}
3.4 MortgageCalculator
public class MortgageCalculator {
private int principal;
private float rate;
private byte years;
public MortgageCalculator(int principal, float rate, byte years) {
this.principal = principal;
this.rate = rate;
this.years = years;
}
public double calculateMortgage() {
float monthlyRate = rate / Main.PERCENT / Main.MONTHS_IN_YEAR;
float numOfPayments = years * Main.MONTHS_IN_YEAR;
double temp = Math.pow(1 + monthlyRate, numOfPayments);
return principal * monthlyRate * temp / (temp - 1);
}
public double calculateBalance(int numberOfPaymentsMade) {
float monthlyRate = rate / Main.PERCENT / Main.MONTHS_IN_YEAR;
float numberOfPayments = years * Main.MONTHS_IN_YEAR;
double temp1 = Math.pow(1 + monthlyRate, numberOfPayments);
double temp2 = Math.pow(1 + monthlyRate, numberOfPaymentsMade);
return principal * (temp1 - temp2) / (temp1 - 1);
}
}
3.5 去静态化
由于MortgageCalculator
的方法为实例方法,因此,MortgageReport
需要作出相应调整:
public class MortgageReport {
private MortgageCalculator calculator;
public MortgageReport(MortgageCalculator calculator) {
this.calculator = calculator;
}
public void printPaymentSchedule() {
System.out.println();
System.out.println("Payment Schedule");
System.out.println("-------");
for (int month = 1; month <= calculator.getYears() * Main.MONTHS_IN_YEAR; month++) {
double balance = calculator.calculateBalance(month);
System.out.println(NumberFormat.getCurrencyInstance().format(balance));
}
}
public void printMortgage() {
double mortgage = calculator.calculateMortgage();
String formatted = NumberFormat.getCurrencyInstance().format(mortgage);
System.out.println();
System.out.println("MORTGAGE");
System.out.println("--------");
System.out.println("Mortgage:" + formatted);
}
}
3.6 Main
public class Main {
public static void main(String[] args) {
int principal = (int) Console.readNumber("Principal:", 1000, 1_000_000);
float rate = (float) Console.readNumber("Rate:", 1, 5);
byte years = (byte) Console.readNumber("Period(Years):", 1, 30);
var calculator = new MortgageCalculator(principal, rate, years);
var report = new MortgageReport(calculator);
report.printMortgage();
report.printPaymentSchedule();
}
}
3.7 去除重复逻辑
public class MortgageCalculator {
final static byte MONTHS_IN_YEAR = 12;
final static byte PERCENT = 100;
private int principal;
private float rate;
private byte years;
public MortgageCalculator(int principal, float rate, byte years) {
this.principal = principal;
this.rate = rate;
this.years = years;
}
public double calculateMortgage() {
float monthlyRate = getMonthlyRate();
float numOfPayments = getNumOfPayments();
double temp = Math.pow(1 + monthlyRate, numOfPayments);
return principal * monthlyRate * temp / (temp - 1);
}
public double calculateBalance(int numberOfPaymentsMade) {
float monthlyRate = getMonthlyRate();
float numberOfPayments = getNumOfPayments();
double temp1 = Math.pow(1 + monthlyRate, numberOfPayments);
double temp2 = Math.pow(1 + monthlyRate, numberOfPaymentsMade);
return principal * (temp1 - temp2) / (temp1 - 1);
}
private int getNumOfPayments() {
return years * MONTHS_IN_YEAR;
}
private float getMonthlyRate() {
return rate / PERCENT / MONTHS_IN_YEAR;
}
public byte getYears() {
return years;
}
}
3.8 调整
在MortgageReport
中,部分逻辑可以调整到MortgageCalculator
中:
public double[] getRemainingBalance(){
double[] balances = new double[getNumOfPayments()];
for (int month = 1; month <= balances.length; month++)
balances[month - 1] = calculateBalance(month);
return balances;
}
public void printPaymentSchedule() {
System.out.println();
System.out.println("Payment Schedule");
System.out.println("-------");
double[] balances = calculator.getRemainingBalance();
for (double balance : balances) {
System.out.println(NumberFormat.getCurrencyInstance().format(balance));
}
}
最后,代码中有2处NumberFormat.getCurrencyInstance()
,我们可以将其变成属性,然后在构造器中进行初始化。
private final NumberFormat currencyInstance;
private MortgageCalculator calculator;
public MortgageReport(MortgageCalculator calculator) {
this.calculator = calculator;
currencyInstance = NumberFormat.getCurrencyInstance();
}
4. 继承(OOP 原则3)
4.1 简介
在我们使用class
来定义对象的时候,我们经常会发现很多对象之间又一些公共、基础的属性或行为,例如:
-
所有的UI控件(输入框、多选、单选、下拉框)都可以禁用,都有宽高
-
哺乳动物,都有身高、体重、血型等
我们在定义这些类的时候,并不想在所有的类中都定义重复的属性或者行为,那么就可以使用继承这种语法来重用代码。
我们可以把这些通用、公共的属性和行为定义在一个类中,这个类成为父类(parent) / 基类(base) / 超类(super)
,然后在定义子类(child/sub)
来继承它。
public class UIControl {
private boolean isEnabled = true;
public void enable(){
isEnabled = true;
}
public void disable(){
isEnabled = false;
}
public boolean isEnabled() {
return isEnabled;
}
}
public class TextBox extends UIControl {
private String text;
public void setText(String text) {
this.text = text;
}
public void clear() {
text = "";
}
}
public class Main {
public static void main(String[] args) {
// var control = new UIControl();
var control = new TextBox();
control.disable();
System.out.println(control.isEnabled());
}
}
关于继承语法的补充:
-
即使父类型的属性被修饰为
private
也不妨碍子类访问它们(因为getter和setter都是public
的) -
所有类型即使没有显式的声明继承某一个类型,默认都是
Object
的子类型
4.2 Object
Java 中,所有的类都直接或间接的继承自Object
。因此我们定义的类自然也从这个父类获得了如下方法:
-
hashCode:得到一个整数值,它是基于内存地址运算出来的
-
equals:用来比较两个对象是否相等。
-
toString:对象的字符串表现形式
public class Main {
public static void main(String[] args) {
var box1 = new TextBox();
var box2 = box1;
System.out.println(box1.hashCode());
System.out.println(box2.hashCode());
System.out.println(box1.equals(box2));
}
}
4.3 构造器
在继承的语法中,构造器的使用有如下语法特点:
-
在创建子类实例的时候,首先调用父类构造器,然后调用子类构造器
-
当父类构造器有参数时,子类构造器必须显式(
explicitly
)调用父类这个有参构造器
4.4 访问修饰符
同包 | 子类 | 其它 | |
---|---|---|---|
public | Y | Y | Y |
private | N | N | N |
protected | Y | Y | N |
default | Y | / | N |
4.5 方法重写
Method Overriding
,当子类不满从父类继承的方法时,可以通过方法重写来改变其具体实现方式。
方法重载:同名不同参,即方法签名不一样,仅仅只是方法名相同
方法重写:同名同参,即方法签名一样
public class TextBox extends UIControl {
private String text;
public void setText(String text) {
this.text = text;
}
public void clear() {
text = "";
}
@Override
public String toString() {
return "TextBox{" +
"text='" + text + '\'' +
'}';
}
}
@Override
,又称为注解,它会告诉编译器,来检查这个方法的签名是否符合方法重写的语法要求。
继承:两个类型之间的这种父子关系的描述,需要使用extends关键字 what
当我们想要复用、重用代码的时候,就“可以”使用继承
如果 class A extends B,那么B类型中所有的属性和方法都可以被A类型的实例所使用(私有修饰的成员变量和方法除外)
4.6 向上转型 & 向下转型
-
UpCasting:向上转换成父类类型
-
DownCasting:向下转换成子类类型
public class Main {
public static void main(String[] args) {
UIControl control = new UIControl(true);
show(control);
var box = new TextBox(true, "hello");
show(box);
}
public static void show(UIControl control) {
// control.setText(); //无法访问
System.out.println(control);
}
}
由于TextBox继承了父类的所有成员,因此我们可以说:文本框是UI控件
,两者的关系可以用is a
来描述:A textbox is a uicontrol
。
这就是上例中box可以传参成功的原因,在此处,发生了box
实例向上转型为UIControl
类型的一个实例。
在
show
方法中,输出方法调用的是TextBox
的toString
方法,因为在运行时,这个实例仍然是一个TextBox
的实例。
由于参数是UIControl
类型,虽然传递的是一个TextBox
实例,但无法编码访问TextBox
自身的方法,如果需要访问的话,需要进行向下转型:
public static void show(UIControl control) {
var box = (TextBox) control;
box.setText("Hello world");
System.out.println(control);
}
但是如果我们向上述方法传递control
这个实例时,程序运行就会报错,因为并非每一个UIControl
都是TextBox
,为了安全起见,可以进行如下检查:
public static void show(UIControl control) {
if (control instanceof TextBox) {
var box = (TextBox) control;
box.setText("Hello world");
}
System.out.println(control);
}
4.7 比较对象
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public class Main {
public static void main(String[] args) {
var p1 = new Point(1, 2);
var p2 = new Point(1, 2);
System.out.println(p1 == p2);
System.out.println(p1.equals(p2));
}
}
在上述案例中,由于==
是根据引用的地址进行比较的,因此得到false
,而equals
在默认情况下,也是基于引用的地址进行比较,所以也得到false
。
为了实现“两者只要坐标值相等,我们就认为两个实例相等”,可以通过以下方式来实现:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
4.8 多态(OOP 原则4)
多态,多种形态或形式,即允许一个对象有多种体现形式。
多态,可以理解为一个行为,在不同的条件下,有不同的效果
public class Main {
public static void main(String[] args) {
UIControl[] controls = {new TextBox(), new CheckBox()};
for (var control : controls) {
control.render();
}
}
}
public class UIControl {
private boolean isEnabled = true;
public void enable() {
isEnabled = true;
}
public void disable() {
isEnabled = false;
}
public boolean isEnabled() {
return isEnabled;
}
public void render() {
}
}
public class TextBox extends UIControl {
@Override
public void render() {
System.out.println("Render TextBox");
}
}
public class CheckBox extends UIControl {
@Override
public void render() {
System.out.println("Render CheckBox");
}
}
4.9 抽象类与抽象方法
抽象类,是在我们不想或不能实例化这个类的实例的时候封装的。
例如上例中的UIControl
:
UIControl[] controls = {new UIControl(), new TextBox(), new CheckBox()};
在上面这行代码中,我们不希望实例化UIControl
这个实例,因为UI控件这本身是一个抽象的概念,具体这个空间是什么样子,应该由某一个具体的子类来定义。
因此,抽象类存在的意义,是为了定义一些公共的成员(属性、方法)让子类来继承。起到代码复用的效果。
因此我们可以声明这个类为抽象类:
public abstract class UIControl {
//...
}
当一个类被声明为abstract
时,这个类是不能使用new
来创建一个实例的。
同时,我们还可以更进一步声明render
为抽象方法:
public abstract class UIControl {
//...
public abstract void render();
}
当一个类中存在抽象方法时,会强制要求所有子类来实现这个方法。(如果这个子类没有实现这些抽象方法,那么这个子类必须也是一个抽象类,不能被实例化)
4.10 final修饰符
如果abstract
修饰一个类,是为了让子类来继承,那么相对的,final
修饰一个类,表示它是一个最终版,不能被继承。
在JDK
中有一个典型的例子,就是String
。在Java中,字符串是不可变(immutable)对象,这意味着我们调用字符串的方法(toUpperCase、replace),或者通过拼串修改其值,得到的总是一个新的字符串实例,它的内存地址已经发生了改变。
将String
修饰为final
,是为了防止我们在继承的时候,重写它的方法,而打破上述JDK
设定好的底层规则,导致程序发生意外。
相类似的,当某一个方法被修饰为final
时,那么这个方法不能被子类重写。
4.11 多根继承
有些高级语言,允许一个子类可以有多个直接父类,但是Java
设计之初衷,始终坚持为单根继承的语法体系,即一个子类,最多只能有一个直接父类。