面向对象基础(二)封装和继承


活动地址: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());
    }
​
}

关于继承语法的补充:

  1. 即使父类型的属性被修饰为private也不妨碍子类访问它们(因为getter和setter都是public的)

  2. 所有类型即使没有显式的声明继承某一个类型,默认都是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 访问修饰符

同包子类其它
publicYYY
privateNNN
protectedYYN
defaultY/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方法中,输出方法调用的是TextBoxtoString方法,因为在运行时,这个实例仍然是一个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设计之初衷,始终坚持为单根继承的语法体系,即一个子类,最多只能有一个直接父类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值