继承是 Java 面向对象程序设计的基本概念,可基于已有类创建新类,复用已有类的方法并添加新方法和字段,是 Java 程序设计的核心技术。
在公司中普通员工完成工作仅领取薪水,而经理完成预期业绩后除薪水外还能获得奖金。为体现这种差异,需要定义一个新的 Manager 类,同时复用之前定义的 Employee 类中已有的代码和字段。经理与员工存在 “is-a” 关系,即每个经理都是员工,这是继承关系的典型特征,通过继承可以在不重复编写相同代码的基础上,为 Manager 类添加新功能。
定义子类
public class Mangager extends Employee {
private double bonous;
public void setBonous(double bonus) {
this.bonous = bonus;
}
}
覆盖方法
public class Mangager extends Employee {
public double getSalary() {
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
}
C++注释:在继承定义上,Java 用 extends,C++ 用冒号;继承类型上,Java 只有公共继承,C++ 有私有和保护继承;调用超类方法时,Java 用 super 关键字,C++ 用类名加作用域运算符。
子类构造器
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。
继承层次
C++注释:Java 不支持多重继承,而 C++ 支持一个类有多个超类,Java 通过接口提供类似功能。
多态
警告:在 Java 中,子类引用的数组可以转换成超类引用的数组,无需强制类型转换。但要注意,虽然数组可以转换,若引用的数组元素类型不匹配,在调用方法时可能会出现问题,甚至引发 ArrayStoreException 异常 。
理解方法调用
阻止继承:final 类和方法
强制类型转换
强制类型转换是将一种类型转换为另一种类型,数值转换如(int)x
,对象引用转换则是用圆括号括住目标类名置于对象引用前,像(Manager) staff[0]
。
为在忽视对象实际类型时使用其全部功能,如从Employee
数组中还原Manager
对象以访问新增变量 。
只能在继承层次内转换,子类引用赋给超类变量可直接进行,反之需强制转换并接受运行时检查;向下转换若对象类型不符会抛异常,建议转换前用instanceof
检查;不可能成功的转换编译器会报错。
C++注释:Java 强制类型转换语法源于 C 语言,处理类似 C++ 的
dynamic_cast
,但 Java 转换失败抛异常,而 C++ 生成null
对象 。
抽象类概念
在类的继承层次中,上层类更具一般性和抽象性,比如Person
类可作为Employee
类和Student
类的基类。通过引入抽象类和抽象方法,能更好地组织具有共性的属性和方法,如Person
类中的getName
和getDescription
方法 。
使用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
类是 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
方法并加入子类特有字段 。toString
方法。println
方法也会调用该方法输出对象信息 。在许多编程语言(如 C/C++)中,数组大小需在编译时确定,这可能导致资源浪费或不便。而 Java 允许在运行时确定数组大小,不过普通数组一旦确定大小就难以更改。ArrayList
类类似于数组,但能自动调整容量,无需手动编写调整代码 。
声明数组列表
ArrayList
是泛型类,使用尖括号指定保存元素的类型,如ArrayList
。add
方法向数组列表添加元素。在填充数组前,可调用ensureCapacity
方法指定内部数组容量,以减少重新分配空间的开销。size
方法返回实际元素个数,类似数组的length
属性。若要调整数组列表大小以匹配当前元素数量,可使用trimToSize
方法 。C++ 注释:
ArrayList
类似于 C++ 的vector
模板,都是泛型类型。但 C++ 的vector
重载了[]
运算符方便访问元素,而 Java 没有运算符重载,需调用显式方法。C++ 中向量按值拷贝,与 Java 的操作方式不同 。
访问数组元素列表
ArrayList
不是 Java 语言内置部分,而是标准库中的实用工具类,不能像普通数组那样用[]
访问或修改元素,需使用get
和set
方法。使用set
方法替换元素时,要确保数组列表大小大于指定索引;添加新元素应使用add
方法。get
方法用于获取元素,在没有泛型时,原始ArrayList
的get
方法返回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 中为每个基本类型(如 int
、long
、float
等)提供了对应的包装器类,如 Integer
、Long
、Float
等。这些类派生自 Number
(除 Boolean
、Character
外),是不可变类且不能有子类。
自动装箱:可以将基本类型的值自动包装成对应的包装器类对象,例如向 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++ 中的函数指针) 。
Class
类:Class
类是反射的核心,它代表一个类或接口。通过 Class
对象,我们可以获取类的各种信息,如类的名称、方法、字段等。Constructor
类:Constructor
类表示类的构造方法,通过它可以在运行时创建类的实例。Method
类:Method
类表示类的方法,通过它可以在运行时调用类的方法。Field
类:Field
类表示类的字段,通过它可以在运行时访问和修改类的字段值。// 此处只进行简单介绍,后续可能再更新。
Person
类,而不是在Employee
和Student
类中重复定义。protected
修饰的实例字段能方便多数情况下的访问,但因子类集合无限制,易破坏封装性,且所有子类都能访问,不利于在子类中重新定义方法,所以要谨慎使用。Holiday
类继承GregorianCalendar
类不合适,因父类方法会破坏子类的封闭性,而LocalDate
类因不可变则无此问题 。