Lec3:继承与接口

继承的定义

  • 继承:让子类获得父类的属性和方法,实现代码复用和拓展,减少冗余度并增强课维护性。
  • 使用语法
    • 子类中,使用extends关键字继承父类。
    1
    2
    3
    class 子类 extends 父类 {
    // 子类定义
    }
    • 子类可以访问父类的公共(public)和受保护(protected)成员
    • 私有(private)成员无法被直接被访问,需调用父类的方法来访问
    • Java只支持单继承,一个类最多只能有一个父类
    • 子类中,使用super.attribute可以引用父类中定义的非私有属性
    • 子类中,使用super.methodName()关键字调用父类中定义的非私有方法。
    1
    2
    3
    4
    5
    class 子类 extends 父类 {
    public void method() {
    super.methodName();
    }
    }
    • 子类中,可以重写父类中的方法。方法名称、参数列表和返回类型必须保持一致.
    1
    2
    3
    4
    5
    6
    class 子类 extends 父类 {
    @Override
    public void method() { // 重写父类中的方法
    // 子类方法实现
    }
    }

重写(Override)

  • @Override 注解:可选的注解,用于标记重写方法,提高代码可读性
  • 访问权限:重写方法的visibility范围必须大于等于父类方法
    • 如果父类方法为public,子类方法只能为public
  • 返回类型:重写方法必须与父类方法的返回类型一致
    • 若父类方法返回类型为基本类型,则只能相同
    • 若父类方法返回类型为AClass,则只能返回AClass或者其子类的对象
  • 方法名和参数列表:重写方法必须与父类方法完全相同
    • 参数个数和参数顺序、参数类型

继承带来的便利:

  • 减少冗余:子类不必重复实现父类已有的属性和方法,直接获得
    • 如,在编写新的药水瓶类XBottle的时候,只需要使用XBottle extends Bottle来声明
    • 只需编写XBottle独有的属性或方法
    • 根据Xbottle的具体要求重写Bottle中的方法
  • 层次结构:上层类可以概括下层类
    • 可以用Bottle来引用Xbottle_objAdventurer类不需要新增容器来储存Xbottle实例
    • 任何以Bottle对象为参数的方法,都可以传入Xbottle对象进行统一处理

继承的使用情景

  • 提炼场景:多个类之间存在相同的属性和方法
    • 将相同(公共)的部分提取出来形成一个类(父类)
    • 避免出现重复代码
    • 更好的维护这些相同的属性和代码
  • 扩展场景:引入新类,避免对已有类进行修改
    • 新类对已有类进行增量式扩展
    • 避免编写冗余代码
    • 保持已有类及其使用者的稳定

继承的优势

  • 代码重用
    • 子类无需重复实现父类的属性和方法,提高了重用度
  • 代码拓展
    • 子类可以添加新的属性和方法,实现自己独有的功能
    • 实现了对父类的增量式扩展(保持父类不变,因而其使用者也不变
  • 继承层次
    • 形成抽象层次结构,任何子类对象都可以使用父类型来统一引用和管理
    • 实现了对各种变化的统一处理能力

接口

  • 接口是一种定义方法和常量的抽象类型,不提供方法实现。
  • 一个接口可以被多个类实现,一个类也可以实现多个接口。
  • 通过实现接口来定义程序所需的行为,消除类之间的依赖关系。
  • 使用语法
    接口中,使用public关键字定义方法。
    1
    2
    3
    interface 接口名 {
    public void method();
    }
    类中,使用implements关键字实现接口。

继承与接口的选择

  • 当存在明显父子关系时选择继承。
  • 当需要定义不同类间的共同行为时选择接口。

继承下的Junit测试注意事项

  • 测试子类时注意是否调用了父类方法和访问了父类属性。
  • 测试其他类的方法时,注意参数的类型是子类型还是父类型。

Lec4: JAVA程序的BUG分析与调试

多种输入指令的处理策略

  1. 使用 switch case 语句
    每种指令封装为一个方法,通过 switch 判断执行:

    1
    2
    3
    4
    5
    switch (operator) {
    case 1: command1(...); break;
    case 10: command10(...); break;
    //...
    }
  2. 面临的问题
    随着指令增多,main 函数中顺序处理变得复杂。可以通过设计一个父类 CommandUtil 来简化:

    • 设计父类:如 CommandUtil,提供 command(String message) 方法。
    • 子类继承:具体的指令子类如 AddBottleAddEquipment
    • 管理命令Manager 类负责管理多个 CommandUtil 对象,并根据输入动态调用合适的命令。
  3. 管理命令对象
    Manager 类通过 ArrayList<CommandUtil> 存储所有命令,并通过输入指令自动获取并执行对应命令:

    1
    2
    cmdUtil = cmdUtilArray.get(operator - 1);
    cmdUtil.command(message);

常见逻辑性错误

  1. 引用错误

    • 空指针异常:当试图通过 null 对象访问其成员时会抛出空指针异常:

      1
      2
      String str = null;
      int len = str.length(); // 抛出NullPointerException

    • 索引越界异常:访问数组或容器中元素时,索引超出范围会产生索引越界异常:

    1
    name = list.get(index);  // 当index超出范围时抛出IndexOutOfBoundsException

    • 类型转换错误:数据类型不匹配时会引发类型转换错误:
      1
      2
      Shape shape = new Circle();
      Rectangle rect = (Rectangle) shape; // 抛出ClassCastException
  2. 运算错误

    • 逻辑运算短路求值:在Java中,逻辑运算符 ||&& 存在短路行为,可能影响程序执行:
    1
    2
    3
    4
    String test = null;
    if (test == null || test.length() > 5) {
    // 正确:不会触发空指针异常
    }
    1
    2
    3
    4
    String test = null;
    if (test.length() > 5 || test == null){
    // 错误:会触发空指针异常
    }
    • 隐式强转与类型提升:数值赋值时可能发生隐式强制转换,导致溢出或精度丢失:
      1
      2
      byte b = 10;
      b += 1020; // 隐式类型提升
  3. 控制流错误
    差一错误:指循环次数或索引值与预期相差1,常见于数组遍历:

    1
    2
    for (int i = 0; i <= array.length; i++) {  // 错误:索引越界
    }

差一错误的防范

  • 只需对下述三种状态进行检测:

    • 0状态:迭代还未发生时,检查程序状态是否符合预期
    • 1状态:进行了一次迭代,检查迭代结果以及迭代后的程序状态是否符合预期
    • m状态:多次迭代,观察迭代得到的结果并判断能否在预期次数时终止
  • 对于遍历数组 string,需进行如下测试:

    • 0状态:检查(string != null && string.length > 0)
    • 1状态:进行一次迭代,观察是否得到了第0个元素
    • m状态:迭代10次,观察第10次迭代是否正常得到最后一个元素,再进行一次迭代,观察有无引发空指针异常,观察迭代是否已经终止
  • 类似地,采用这三个步骤也可解决“差 k 错误”

  1. 输入输出错误

    • Scanner的nextLine问题:使用 nextLine 吸收换行符时,未及时处理会导致错误:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class NextLineExample {
    public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    System.out.println("请输入你的年龄:");
    int age = sc.nextInt();
    System.out.println("请输入你的姓名:");
    String name = sc.next();
    System.out.println("请输入你的工资:");
    String salary = sc.nextLine();// 这里会吸收前一个输入的换行符,导致salary为空
    System.out.println("你的信息如下:");
    System.out.println("姓名:"+name+"\n"+"年龄:"+age+"\n"+"工资:"+salary);
    }
    }
    • next类方法
      • next, nextInt, nextDouble, nextFloat
      • 扫描System.in直到获得了有效输入,并把有效输入之前的其他符号过滤掉(如空格和回车等)
      • 无法获得带有空格的输入
    • nextLine方法
      • 获得回车之前的整行输入(吸收回车)
    • Bug原因:输入姓名后按下的回车不会被next吸收,nextLine 在吸收后直接返回
    • 解决办法
      • next后添加一个 nextLine 方法
      • 或者都使用next类方法
  2. 拷贝错误

    • Java中的对象传参是传递引用(即地址值),而非复制对象内容,导致对象被意外共享:
      • 产生对象共享访问
      • 一个对象可能被其他对象修改,导致状态变化
    • 如果这种对象状态变化是不受控制的,则需要限制对象共享
      • 使用一个对象的拷贝来进行传递处理
      • Object类内置有clone 方法
    • 浅拷贝:对于对象中的引用类型属性,复制其引用值(地址值)
      • 单层拷贝
      • 默认的clone方法是浅拷贝
    • 深拷贝:对于对象中的引用类型属性,复制其内容(层次迭代)
    1
    2
    3
    4
    5
    Address addr = new Address("CityA");
    Factory fact1 = new Factory("Factory1", addr);
    Factory clonedFact = (Factory) fact1.clone();
    clonedFact.getAddress().setCity("CityB");
    // 修改了clonedFact的属性,fact1的属性也发生变化
  3. String类使用不当

    • String是普遍使用的一个类
      • String s1 = “OOpre"; // 直接创建
      • String s2 = “OOpre"; // 直接创建
      • String s3 = s1; // 引用赋值
      • String s4 = new String(“OOpre"); // String 对象创建
      • String s5 = new String(“OOpre"); // String 对象创建
    • 这五个对象引用是否存在共享?
    • equals与==的区别equals 比较内容,== 比较引用地址。String 类是不可变对象,修改时返回新对象。

错误捕捉与处理

  1. 错误传播

    • 程序运行时的错误可能会通过方法调用链传播,导致问题复杂化。因此需要通过 try-catch 来捕捉异常:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class ExceptionExample {
    void m() {
    int data = 50 / 0;
    }
    void n() {
    m();
    }
    void p() {
    n();
    }
    }
    public class Test {
    public static void main(String args[]) {
    ExceptionExample obj = new ExceptionExample();
    try {
    obj.p();
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }
  2. 错误扩散与捕捉

    • 如果对象的属性取值错误,访问该对象的所有方法都会受到影响。通过 try-catch 捕捉错误,防止扩散。

黑盒测试

  1. 测试的重点

    • 构造数据覆盖不同场景,验证程序的输入输出是否符合预期。黑盒测试关注程序的外部表现,不关注内部实现。
  2. 等价类划分

    • 根据程序对输入的处理逻辑,将输入划分为若干等价类,每个等价类中的数据会触发相同的处理逻辑:
      • 有效等价类 vs 无效等价类
      • 通过划分等价类来减少测试工作量,提高测试效率。

调试方法

  1. 数据化简

    • 问题的输入数据往往很大或复杂,需要化简数据,确保在最小数据集上仍能重现问题,以便于调试。
  2. 断点调试

    • 行断点:程序执行到某行时暂停。
    • 条件断点:满足特定条件时暂停。
    • 断点反映调试人员的推理过程,动态调整断点位置可以帮助更好地发现问题。
  3. 打印语句调试

    • 通过插入打印语句来监控程序执行过程中的变量状态和逻辑分支,辅助发现问题。