Java 17 面向对象综合练习:Person/Student/Employee 3类继承与覆盖实战解析

1. 项目背景与设计思路

在企业管理系统和校园信息系统中,人员信息的建模是核心需求之一。我们经常会遇到这样的场景:学生和员工都属于"人"这一基本范畴,但各自又有独特的属性和行为。这正是面向对象编程中继承机制大显身手的时机。

本次实战项目将构建一个完整的类层次结构:

Person (抽象类)
├── Student
└── Employee
    └── Company

这种设计体现了"is-a"关系:学生是人,员工也是人。通过抽象出Person类的共性特征,我们可以实现:

  • 代码复用 :避免在Student和Employee中重复定义name、age等基础属性
  • 多态支持 :允许将不同子类对象统一视为Person处理
  • 扩展性 :未来添加新的人员类型(如Teacher)时只需继承Person

2. 核心类实现详解

2.1 Person抽象类设计

作为基类,Person定义了所有人员共有的属性和行为:

public abstract class Person {
    protected String name;
    protected int age;
    protected boolean gender;
    
    public Person(String name, int age, boolean gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
    
    @Override
    public String toString() {
        return name + "-" + age + "-" + gender;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person other = (Person) obj;
        return age == other.age && 
               gender == other.gender && 
               Objects.equals(name, other.name);
    }
}

关键点说明

  • 使用 protected 修饰符确保子类能直接访问字段
  • equals() 方法实现了严格的属性比较逻辑
  • toString() 提供标准化的字符串表示形式

2.2 Student类实现

学生类在Person基础上增加了学号和班级信息:

public class Student extends Person {
    private String stuNo;
    private String clazz;
    
    public Student(String name, int age, boolean gender, 
                  String stuNo, String clazz) {
        super(name, age, gender);  // 调用父类构造器
        this.stuNo = stuNo;
        this.clazz = clazz;
    }
    
    @Override
    public String toString() {
        return "Student:" + super.toString() + "-" + stuNo + "-" + clazz;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (!super.equals(obj)) return false;
        Student other = (Student) obj;
        return Objects.equals(stuNo, other.stuNo) && 
               Objects.equals(clazz, other.clazz);
    }
}

构造器复用技巧

  • 通过 super() 调用父类构造器初始化共有属性
  • 只需处理子类特有的属性(stuNo, clazz)

2.3 Employee类实现

员工类引入了薪资和公司信息,其中公司是单独定义的类:

public class Employee extends Person {
    private Company company;
    private double salary;
    
    public Employee(String name, int age, boolean gender,
                   double salary, Company company) {
        super(name, age, gender);
        this.salary = salary;
        this.company = company;
    }
    
    @Override
    public String toString() {
        return "Employee:" + super.toString() + "-" + 
               (company != null ? company : "null") + "-" + salary;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (!super.equals(obj)) return false;
        Employee other = (Employee) obj;
        if (!Objects.equals(company, other.company)) return false;
        
        DecimalFormat df = new DecimalFormat("#.#");
        return df.format(salary).equals(df.format(other.salary));
    }
}

特殊处理点

  • 公司属性可能为null,需在toString和equals中特殊处理
  • 薪资比较使用DecimalFormat保留一位小数,避免浮点数精度问题

3. 关键技术与陷阱解析

3.1 equals方法实现规范

实现equals方法时需要遵循以下契约:

  1. 自反性 :x.equals(x)必须返回true
  2. 对称性 :x.equals(y)与y.equals(x)结果必须一致
  3. 传递性 :如果x.equals(y)且y.equals(z),则x.equals(z)必须为true
  4. 一致性 :多次调用结果应该相同
  5. 非空性 :x.equals(null)必须返回false

典型实现模式

@Override
public boolean equals(Object obj) {
    // 1. 检查是否同一对象
    if (this == obj) return true;
    
    // 2. 检查null和类型
    if (obj == null || getClass() != obj.getClass()) 
        return false;
        
    // 3. 类型转换
    MyClass other = (MyClass) obj;
    
    // 4. 逐个字段比较
    return Objects.equals(field1, other.field1) &&
           field2 == other.field2;
}

3.2 浮点数比较的陷阱

直接使用==比较浮点数会导致精度问题:

// 不推荐的做法
if (salary == other.salary)  // 可能因精度问题产生错误结果

// 正确做法:使用DecimalFormat格式化后比较字符串
DecimalFormat df = new DecimalFormat("#.#");
return df.format(salary).equals(df.format(other.salary));

3.3 null值安全处理

在equals方法中处理可能为null的字段:

// 处理company字段的比较
if (company == null) {
    if (other.company != null) return false;
} else if (!company.equals(other.company)) {
    return false;
}

4. 业务逻辑实现

4.1 对象创建与排序

主程序需要处理用户输入并创建相应对象:

List<Person> personList = new ArrayList<>();

// 输入处理循环
while (scanner.hasNext()) {
    String type = scanner.next();
    if ("s".equals(type)) {
        // 创建Student对象
    } else if ("e".equals(type)) {
        // 创建Employee对象
    } else {
        break;
    }
}

// 按姓名和年龄排序
Collections.sort(personList, Comparator
    .comparing(Person::getName)
    .thenComparingInt(Person::getAge));

4.2 对象分类与去重

将人员列表按类型分类并去重:

List<Student> stuList = new ArrayList<>();
List<Employee> empList = new ArrayList<>();

for (Person p : personList) {
    if (p instanceof Student) {
        addIfNotExists((Student)p, stuList);
    } else if (p instanceof Employee) {
        addIfNotExists((Employee)p, empList);
    }
}

// 辅助方法:仅添加不重复元素
private static <T> void addIfNotExists(T obj, List<T> list) {
    if (list.stream().noneMatch(existing -> existing.equals(obj))) {
        list.add(obj);
    }
}

5. 设计模式应用

5.1 工厂方法模式优化

可以引入简单工厂来封装对象创建逻辑:

public class PersonFactory {
    public static Person createPerson(Scanner sc) {
        String type = sc.next();
        if ("s".equals(type)) {
            return new Student(sc.next(), sc.nextInt(), sc.nextBoolean(),
                             sc.next(), sc.next());
        } else if ("e".equals(type)) {
            return new Employee(sc.next(), sc.nextInt(), sc.nextBoolean(),
                              sc.nextDouble(), new Company(sc.next()));
        }
        return null;
    }
}

5.2 策略模式排序

将排序逻辑封装为独立策略:

public class PersonComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
        int nameCompare = p1.getName().compareTo(p2.getName());
        if (nameCompare != 0) return nameCompare;
        return Integer.compare(p1.getAge(), p2.getAge());
    }
}

6. 测试用例设计

完善的测试是保证代码质量的关键:

public class PersonTest {
    @Test
    public void testStudentEquals() {
        Student s1 = new Student("Alice", 20, false, "001", "CS101");
        Student s2 = new Student("Alice", 20, false, "001", "CS101");
        assertTrue(s1.equals(s2));
    }
    
    @Test
    public void testEmployeeSalaryComparison() {
        Employee e1 = new Employee("Bob", 30, true, 5000.0, new Company("IBM"));
        Employee e2 = new Employee("Bob", 30, true, 5000.01, new Company("IBM"));
        assertTrue(e1.equals(e2));  // 保留一位小数应视为相等
    }
    
    @Test
    public void testNullCompanyHandling() {
        Employee e1 = new Employee("Charlie", 25, true, 4000.0, null);
        Employee e2 = new Employee("Charlie", 25, true, 4000.0, null);
        assertTrue(e1.equals(e2));
    }
}

7. 性能优化建议

  1. 缓存DecimalFormat实例

    private static final DecimalFormat SALARY_FORMAT = new DecimalFormat("#.#");
    
  2. 使用StringBuilder优化字符串拼接

    @Override
    public String toString() {
        return new StringBuilder("Employee:")
            .append(super.toString())
            .append("-")
            .append(company != null ? company : "null")
            .append("-")
            .append(salary)
            .toString();
    }
    
  3. 考虑使用HashSet去重

    Set<Person> uniquePersons = new HashSet<>(personList);
    List<Person> distinctList = new ArrayList<>(uniquePersons);
    

8. 扩展思考

  1. 接口与抽象类的选择

    • 如果需要定义行为契约,考虑使用接口
    • 如果需要共享代码和状态,使用抽象类
  2. 组合优于继承 : 对于Company这样的关联关系,使用组合(has-a)比继承更合适

  3. 不可变对象设计 : 考虑将Person及其子类设计为不可变对象:

    public final class Student {
        private final String stuNo;
        // 其他字段也声明为final
        // 只提供getter不提供setter
    }
    
  4. 记录类型(Java 16+) : 对于简单的数据传输对象,可以使用record简化代码:

    public record Company(String name) {
        // 自动生成equals, hashCode, toString等方法
    }
    
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐