Java 继承——从 C++ 到 Java

继承是 Java 面向对象程序设计的基本概念,可基于已有类创建新类,复用已有类的方法并添加新方法和字段,是 Java 程序设计的核心技术。

类、超类和子类

在公司中普通员工完成工作仅领取薪水,而经理完成预期业绩后除薪水外还能获得奖金。为体现这种差异,需要定义一个新的 Manager 类,同时复用之前定义的 Employee 类中已有的代码和字段。经理与员工存在 “is-a” 关系,即每个经理都是员工,这是继承关系的典型特征,通过继承可以在不重复编写相同代码的基础上,为 Manager 类添加新功能。

定义子类

  • 通过 extends 关键字实现继承,如 Manager 类继承 Employee 类,前者是子类,后者是超类。
  • 子类可新增字段和方法,像 Manager 类新增了存储奖金的 bonus 字段和 setBonus 方法。
  • 同时子类能继承超类的方法和字段,即便未显式定义,也可使用超类中的 getName 等方法及 name 等字段。
public class Mangager extends Employee {
    private double bonous;
    public void setBonous(double bonus) {
        this.bonous = bonus;
    }
}

覆盖方法

  • 超类方法可能不适用于子类,子类需覆盖。以 Manager 类的 getSalary 方法为例,直接返回 salary 和 bonus 总和或递归调用自身方法都有问题,前者无法访问超类私有字段,后者会导致递归错误。正确做法是使用 super 关键字调用超类的 getSalary 方法,再加上子类特有的计算逻辑 。
public class Mangager extends Employee {
	public double getSalary() {
        double baseSalary = super.getSalary();
        return baseSalary + bonus;
    }
}

C++注释:在继承定义上,Java 用 extends,C++ 用冒号;继承类型上,Java 只有公共继承,C++ 有私有和保护继承;调用超类方法时,Java 用 super 关键字,C++ 用类名加作用域运算符。

子类构造器

  • 定义 Manager 类的构造器,通过 super 关键字调用超类 Employee 带参数的构造器来初始化从超类继承的私有字段,因为子类不能直接访问超类私有字段。
  • 若子类构造器未显式调用超类构造器,会自动调用超类无参构造器;若超类无无参构造器且子类未显式调用其他超类构造器,编译会报错。

C++注释:在C++ 的构造器中,会使用初始化列表语法调用超类的构造器,而不调用 super。在 C++ 中,Manager的构造器如下所示:

// C++
Manager::Manager(string name,double salary,int year,int month,int day)
: Employee(name, salary, year, month, day) {
    bonus = 0;
}

在 C++ 中,如果希望实现动态绑定,需要将成员函数声明为 virtul。在 Java中,动态绑定是默认的行为。如果不希望让一个方法是虚拟的,可以将它标记为 final。

继承层次

  • 继承不仅限于一个层次,可以由一个类派生多个子类,再由子类继续派生。从一个公共超类派生出来的所有类的集合称为继承层次。
  • 一个祖先类可以有多个子孙链,例如 Employee 类可以派生出 Manager、Programmer 和 Secretary 等类,Manager 类还能继续派生出 Executive 类。
Java 继承——从 C++ 到 Java_第1张图片

C++注释:Java 不支持多重继承,而 C++ 支持一个类有多个超类,Java 通过接口提供类似功能。

多态

  • 判断数据是否设计为继承关系的 “is - a” 规则,即子类的每个对象也是超类的对象,这也是替换原则的体现,程序中超类对象的任何地方都可用子类对象替换。
  • Employee 类型的变量可以引用 Employee 类或其子类(如 Manager、Executive 等)的对象。但超类引用不能直接调用子类特有的方法,子类引用也不能直接赋值给超类变量。

警告:在 Java 中,子类引用的数组可以转换成超类引用的数组,无需强制类型转换。但要注意,虽然数组可以转换,若引用的数组元素类型不匹配,在调用方法时可能会出现问题,甚至引发 ArrayStoreException 异常 。

理解方法调用

  1. 编译器检查:编译器会先查看对象声明类型中的方法,以及其超类中可访问的同名方法,确定所有可能被调用的候选方法。
  2. 重载解析:根据调用时提供的参数类型,选择与之完全匹配的方法。若有多个匹配方法,编译器会报错。
  3. 方法签名与覆盖:方法名和参数列表构成签名,子类中同名同参方法会覆盖超类方法,返回类型在覆盖时需保持兼容。
  4. 静态与动态绑定:private、static、final 方法和构造器使用静态绑定,编译器能确定调用方法;其他方法依赖对象实际类型,运行时动态绑定。虚拟机为每个类生成方法表,便于快速查找调用方法。

阻止继承:final 类和方法

  • final 类:使用 final 修饰符定义的类不能被继承,可阻止其他类派生其子类。
  • final 方法:类中特定方法声明为 final,子类不能覆盖该方法。声明为 final 的主要原因是确保方法语义在子类中不被改变。

强制类型转换

  • 强制类型转换是将一种类型转换为另一种类型,数值转换如(int)x ,对象引用转换则是用圆括号括住目标类名置于对象引用前,像(Manager) staff[0]

  • 为在忽视对象实际类型时使用其全部功能,如从Employee数组中还原Manager对象以访问新增变量 。

  • 只能在继承层次内转换,子类引用赋给超类变量可直接进行,反之需强制转换并接受运行时检查;向下转换若对象类型不符会抛异常,建议转换前用instanceof检查;不可能成功的转换编译器会报错。

C++注释:Java 强制类型转换语法源于 C 语言,处理类似 C++ 的dynamic_cast ,但 Java 转换失败抛异常,而 C++ 生成null对象 。

抽象类概念

  • 在类的继承层次中,上层类更具一般性和抽象性,比如Person类可作为Employee类和Student类的基类。通过引入抽象类和抽象方法,能更好地组织具有共性的属性和方法,如Person类中的getNamegetDescription方法 。

  • 使用abstract关键字声明抽象类和抽象方法,抽象类可包含字段、具体方法和抽象方法。若子类未完全实现抽象类中的抽象方法,子类也需声明为抽象类;若子类实现了全部抽象方法,则子类为具体类。抽象类不能实例化,但可以定义抽象类的对象变量来引用其非抽象子类的对象 。

public abstract class Persion {
    private String name;
    
    public Person(String name) {
        this.name = name;
    }
    
    public abstract Stirng getDescription();
    
    public String getName() {
        return name;
    }
}

public class Student extends Person {
	private String major;
    
    public Student(String name, String major) {
        super(name);
        this.major = major;
    }
    
    public String getDescription() {
        return "a student majoring in " + major;
    }
}

C++注释:C++ 中通过在函数末尾用= 0标记纯虚函数来定义类似抽象方法的概念,含有至少一个纯虚函数的类为抽象类,且 C++ 没有专门表示抽象类的关键字 。

受保护访问

  • 通常类中字段标记为private,方法标记为public,但有时希望限制超类中的方法仅允许子类访问,或让子类访问超类的某些字段,这时就需要将相关方法或字段声明为protected。例如Employee类的hireDay字段声明为protected后,Manager类可直接访问 。
  • 当子类和超类在不同包中时,子类的方法只能查看自身对象的受保护字段,不能查看其他超类对象的该字段。

C++注释:Java 中的受保护部分对所有子类及同一个包中的其他类都可见,安全性比 C++ 中的保护机制差。

Java 中 4 个访问控制修饰符:private仅对本类可见;public对外部完全可见;protected对本包和所有子类可见;默认(无修饰符)对本包可见。

Object:所有类的超类

Object类是 Java 中所有类的始祖,若类定义时未明确指出超类,则默认继承自Object类。

Object 类型的变量

  • Object类型的变量可引用任何类型的对象,不过对其内容进行具体操作时,需明确对象原始类型并进行强制类型转换。
  • 所有数组类型(包括对象数组和基本类型数组)都扩展了Object类。

C++注释:在 C++ 中没有所有类的根类,不过,每个指针都可以转换成void*指针。

equals 方法

  • Object类的equals方法用于检测两个对象是否相等,默认比较对象引用是否相同。但实际中常需基于对象状态判断相等性,如两个员工对象,需比较姓名、薪水和雇佣日期等。
  • 重写equals方法时,应先快速判断对象是否相同引用、参数是否为null、对象所属类是否相同,再比较具体字段。同时,为处理字段可能为null的情况,可使用Objects.equals方法。
  • 子类重写equals方法时,通常先调用超类的equals方法,若超类比较失败,则对象不相等,若超类比较成功,再进一步比较子类特有的字段 。
public class Employee {
    public boolean equals(Object otherObject) {
        if (this  == otherObject)
        	return true;
        if (otherObject == null)
           	reutrn false;
        if (getClass() != otherObject.getClass())
            return false;
        Employee other = (Employee)otherObject;
        return name.equals(other.name)
            && salary == other.salary
            && hireDay.equals(other.hireDay);
    }
}

相等测试与继承

  • equals方法的隐式和显式参数不属于同一类时,处理方式存在争议。一些程序员用instanceof检测,但这种方式不符合 Java 语言规范对equals方法的特性要求,如自反性、对称性、传递性、一致性以及对null的处理规则,可能导致问题 。
  • equals方法特性
    • 自反性:x.equals(x)为真。
    • 对称性x.equals(y)为真则y.equals(x)为真。
    • 传递性(x.equals(y)y.equals(z)为真则x.equals(z)为真。
    • 一致性(多次调用结果相同)以及对null返回false等特性 。
  • 编写equals方法的建议:强制类型转换参数、检测this与参数是否相等、判断参数是否为null、比较类、转换参数类型以及比较字段,若子类重定义equals方法需包含super.equals(other)调用。

hashCode 方法

  • hashCode方法由对象生成一个整型散列码,通常无规律,不同对象的散列码基本不同。Object类的默认hashCode值由对象存储地址得出 。
  • 通过字符串示例展示,内容相同的字符串散列码相同,而StringBuilder因未定义该方法,其对象散列码由存储地址决定 。
  • 重写equals方法时需重写hashCode方法,且相等对象的hashCode值应相同。

toString 方法

  • 返回对象的字符串表示形式,多数toString方法遵循 “类名 + 字段值” 的格式 。
  • 建议通过getClass().getName()获取类名,避免硬编码。子类重写该方法时,可调用超类的toString方法并加入子类特有字段 。
  • 在与字符串拼接时,Java 编译器会自动调用toString方法。println方法也会调用该方法输出对象信息 。

泛型数组列表

在许多编程语言(如 C/C++)中,数组大小需在编译时确定,这可能导致资源浪费或不便。而 Java 允许在运行时确定数组大小,不过普通数组一旦确定大小就难以更改。ArrayList类类似于数组,但能自动调整容量,无需手动编写调整代码 。

声明数组列表

  • ArrayList是泛型类,使用尖括号指定保存元素的类型,如ArrayList
  • 可以使用add方法向数组列表添加元素。在填充数组前,可调用ensureCapacity方法指定内部数组容量,以减少重新分配空间的开销。
  • 数组列表的size方法返回实际元素个数,类似数组的length属性。若要调整数组列表大小以匹配当前元素数量,可使用trimToSize方法 。

C++ 注释ArrayList类似于 C++ 的vector模板,都是泛型类型。但 C++ 的vector重载了[]运算符方便访问元素,而 Java 没有运算符重载,需调用显式方法。C++ 中向量按值拷贝,与 Java 的操作方式不同 。

访问数组元素列表

  • ArrayList不是 Java 语言内置部分,而是标准库中的实用工具类,不能像普通数组那样用[]访问或修改元素,需使用getset方法。使用set方法替换元素时,要确保数组列表大小大于指定索引;添加新元素应使用add方法。get方法用于获取元素,在没有泛型时,原始ArrayListget方法返回Object,需强制类型转换,而泛型版本可避免这种问题和潜在错误 。

  • 使用add方法并提供索引参数可在列表中间插入元素,插入后列表大小超过容量会重新分配存储数组;remove方法可删除指定位置元素,其后元素向前移动,列表大小减 1 。

类型化与原始数组列表的兼容性

  • 在代码中使用类型参数可增加安全性,但有时需与遗留代码(使用原始数组列表)交互。例如EmployeeDB类中的方法接受和返回原始ArrayList,可以将类型化的ArrayList传递给接受原始ArrayList的方法,无需强制类型转换,但这种操作不安全,因为方法中添加的元素类型可能不匹配,访问时会引发异常 。
public class EmployeeDB {
    public void update(ArrayList list) { ... }
    public ArrayList find(String query) { ... }
}

ArrayList<EmployeeDB> staff = ...;
employeeDB.update(staf)
  • 将原始ArrayList赋给类型化ArrayList会收到编译器警告,使用强制类型转换也不能消除警告。由于 Java 运行时数组列表无类型参数,强制类型转换在运行时检查相同,无法解决问题 。

对象包装器与自动装箱

Java 中为每个基本类型(如 intlongfloat 等)提供了对应的包装器类,如 IntegerLongFloat 等。这些类派生自 Number(除 BooleanCharacter 外),是不可变类且不能有子类。

自动装箱:可以将基本类型的值自动包装成对应的包装器类对象,例如向 ArrayList 添加 int 类型元素时会自动装箱。

自动拆箱:将包装器类对象自动转换为基本类型值,如将 Integer 对象赋值给 int 变量时会自动拆箱。

  • ArrayList 因每个值都包装在对象中,效率低于 int[] 数组,仅在操作便捷性更重要时使用。

  • 基本类型与包装器类不能简单用 == 比较,因为比较的是内存位置,通常会失败,建议用 equals 方法。

  • 可利用包装器类的静态方法,如 Integer.parseInt(s) 将字符串转换为整数。

警告:包装器类不可变,不能用于编写修改数值参数的方法,可使用如 IntHolder 这样的持有者类型。

参数数量可变的方法

使用省略号(...)表示方法可以接收任意数量的参数(除特定格式参数外)。例如printf方法,其定义为public PrintStream printf(String fmt, Object... args) ,可以接收一个格式字符串参数fmt和任意数量的对象参数args。在调用时,如System.out.printf("%d", n);System.out.printf("%d %s", n, "widgets");,前者有两个参数,后者有三个参数,编译器会自动处理参数绑定和装箱操作。

  • 可以自定义变参方法,可以为参数指定任意类型,甚至是基本类型。
public static double max(double values) {
    double largest = Double.NEGATIVE_INFINITY;
    for (double v : values)
        if (v > largest)
            largest = v;
   	return largest;
}
double m = max(3.1, 40.4, -5);

枚举类

使用enum关键字定义,例如public enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE } ,定义了一个包含 4 个实例的枚举类型Size,这些实例是该类型的对象,且不能创建新的对象。

比较枚举类型的值时,直接使用==,无需调用equals方法。

枚举类可以有构造器、方法和字段。构造器是私有的,在构造枚举常量时调用。例如示例中Size枚举类有一个私有构造器和一个getAbbreviation方法。

  • toString:返回枚举常量名,如Size.SMALL.toString() 返回"SMALL"
  • valueOf:静态方法,根据字符串返回对应的枚举值,如Size s = Enum.valueOf(Size.class, "SMALL");
  • values:静态方法,返回包含全部枚举值的数组,如Size[] values = Size.values();
  • ordinal:返回枚举常量在声明中的位置,从 0 开始计数,如Size.MEDIUM.ordinal() 返回1
enum Size {
    SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");

    private Size(String abbreviation) {
        this.abbreviation = abbreviation;
    }

    public String getAbbreviation() {
        return abbreviation;
    }

    private String abbreviation;
}

反射

反射库提供了一系列工具,用于编写能动态操纵 Java 代码的程序。利用反射,Java 可支持用户界面生成器、对象关系映射器等开发工具。反射机制的功能强大,可用于分析类的能力、在运行时检查对象、实现泛型数组操作代码,以及利用Method对象(类似 C++ 中的函数指针) 。

  1. ClassClass 类是反射的核心,它代表一个类或接口。通过 Class 对象,我们可以获取类的各种信息,如类的名称、方法、字段等。
  2. ConstructorConstructor 类表示类的构造方法,通过它可以在运行时创建类的实例。
  3. MethodMethod 类表示类的方法,通过它可以在运行时调用类的方法。
  4. FieldField 类表示类的字段,通过它可以在运行时访问和修改类的字段值。

// 此处只进行简单介绍,后续可能再更新。

继承的设计技巧

  1. 抽取公共操作和字段:将公共的操作和字段放在超类中,避免在子类中重复,如将姓名字段放在Person类,而不是在EmployeeStudent类中重复定义。
  2. 谨慎使用受保护字段:虽然protected修饰的实例字段能方便多数情况下的访问,但因子类集合无限制,易破坏封装性,且所有子类都能访问,不利于在子类中重新定义方法,所以要谨慎使用。
  3. 正确使用继承关系:继承用于实现 “is-a” 关系,避免滥用。如 “钟点工” 和 “员工” 不是 “is-a” 关系,用继承实现会增加复杂性和代码量,不建议使用。
  4. 确保继承有意义:只有当继承的方法都有意义时才使用继承。例如Holiday类继承GregorianCalendar类不合适,因父类方法会破坏子类的封闭性,而LocalDate类因不可变则无此问题 。
  5. 遵循覆盖原则:覆盖方法时不应改变预期行为,否则违反替换原则。编译器不会检查,需开发者自觉遵守,避免随意修改方法行为。
  6. 优先使用多态:遇到根据类型做不同操作的代码,应考虑使用多态,通过在超类或接口中定义方法,利用动态分派机制执行,使代码更易维护和扩展。
  7. 谨慎使用反射:反射虽能让程序在运行时查看字段和方法,编写通用性程序,但不利于编译器检查错误,一般不适用于编写应用程序,要谨慎使用。

你可能感兴趣的:(Java,java,c++,开发语言)