八:面向对象(下)——封装、继承与多态

一、回顾与本篇目标

上一篇你学会了类和对象的基本概念——类是一张设计图纸,对象是按照图纸造出来的实物。你知道了怎么定义类、怎么用 new 创建对象、怎么写构造方法、怎么通过 this 区分属性和参数。

面向对象编程有三大核心特性封装继承多态。这三个概念是 Java 编程思想的精髓——理解它们,你才能真正写出结构清晰、易于维护、可复用的代码。

本篇的目标:

  1. 理解封装——把数据藏起来,通过方法访问
  2. 理解继承——子类复用父类的代码
  3. 学会方法重写——子类重新定义父类的方法
  4. 理解多态——同一个方法,不同的表现
  5. 掌握 super 关键字——调用父类的构造和方法

二、封装——把数据保护起来

2.1 为什么要封装——生活中的例子

想象你有一台电视机。电视机内部有复杂的电路、电容、芯片——但你不需要直接操作这些。厂家给你一个遥控器,上面有开机、调音量、换台几个按钮。你只能通过遥控器来操作电视,不能直接伸手去碰里面的电路板。

为什么这样设计?两个原因:

  1. 保护内部零件:如果用户随便碰电路板,可能触电或者损坏电视。
  2. 简化操作:用户只需要知道按哪个按钮,不需要了解电视内部怎么工作。

在编程中,封装就是同样的道理——把对象的内部数据(属性)藏起来,只暴露有限的方法给外部使用。外部代码不能直接修改属性,只能通过方法操作——方法内部可以做验证,确保数据不会被错误地修改。

2.2 用 private 隐藏属性

private 关键字修饰属性,这些属性就只能在当前类内部访问,外部代码看不到也改不了。

public class BankAccount {
    private String owner;      // 私有属性:外部不能直接访问
    private double balance;    // 私有属性:外部不能直接访问

    public BankAccount(String owner, double initialBalance) {
        this.owner = owner;
        this.balance = initialBalance;
    }

    // 公共方法:安全地存款
    public void deposit(double amount) {
        if (amount > 0) {
            this.balance += amount;
        }
    }

    // 公共方法:安全地取款
    public void withdraw(double amount) {
        if (amount > 0 && amount <= this.balance) {
            this.balance -= amount;
        }
    }

    // 公共方法:查询余额(只读)
    public double getBalance() {
        return this.balance;
    }

    public String getOwner() {
        return this.owner;
    }
}
public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount("张三", 1000.0);

        // account.balance = 99999;  // 编译错误!balance 是 private,外部不能直接访问
        // account.owner = "黑客";   // 编译错误!owner 也是 private

        // 正确的方式:通过公共方法操作
        account.deposit(500.0);               // 存款 500
        account.withdraw(200.0);              // 取款 200
        System.out.println(account.getBalance());  // 1300.0
    }
}

在这个例子中,balanceownerprivate 保护起来了。外部代码只能通过 deposit()withdraw()getBalance() 这些公共方法来操作余额。这些方法内部做了验证——存款金额必须大于 0,取款不能超过余额。这样就保证了 balance 的值始终是合理的。

2.3 Getter 和 Setter——标准的访问方式

对于私有属性,通常提供一对公共方法来访问:

  • Getter:读取属性的值。命名习惯:get + 属性名(首字母大写)。
  • Setter:修改属性的值。命名习惯:set + 属性名(首字母大写)。Setter 内部可以做验证。
public class Student {
    private String name;
    private int age;

    // Getter:获取姓名
    public String getName() {
        return this.name;
    }

    // Setter:设置姓名
    public void setName(String name) {
        if (name != null && !name.trim().isEmpty()) {
            this.name = name;
        }
    }

    // Getter:获取年龄
    public int getAge() {
        return this.age;
    }

    // Setter:设置年龄(带验证)
    public void setAge(int age) {
        if (age >= 0 && age <= 150) {
            this.age = age;
        }
    }
}

使用 getter 和 setter 的好处:

  • 可以在 setter 中做验证:比如年龄不能是负数。
  • 可以在 getter 中做格式化:比如返回日期时格式化显示。
  • 可以只提供 getter 不提供 setter:让属性对外只读。

2.4 访问修饰符总结

Java 提供了四个访问修饰符,控制类、属性、方法在哪些地方可以访问

修饰符 本类中 同包下 子类中 任何地方
private
默认(不写)
protected
public

目前阶段,只记两个private(只在本类内部使用)和 public(任何地方都能用)。另外两个(默认和 protected)后面用到的时候再学。

三、继承——子类复用父类的代码

3.1 为什么要继承——生活中的例子

想象你在描述动物:猫是动物,狗是动物,鸟也是动物。它们有一些共同的特征——都有名字、都会叫、都会吃东西。但它们又各有自己的特点——猫会抓老鼠,狗会看门,鸟会飞。

如果每描述一种动物,你都要把“有名字”“会吃东西”这些共同特征重新写一遍,那代码会非常重复。更合理的做法是:先把所有动物的共同特征定义在一个“动物”类里,然后让“猫”“狗”“鸟”继承这个类——自动获得所有共同特征,然后再加上自己独有的特征。

继承就是一个类获得另一个类的属性和方法。被继承的类叫父类(或超类),继承的类叫子类

3.2 继承的基本语法——extends

extends 关键字表示继承:

// 父类——定义共同特征
public class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

    public void eat() {
        System.out.println(this.name + " 正在吃东西。");
    }

    public void sleep() {
        System.out.println(this.name + " 正在睡觉。");
    }
}

// 子类——继承父类,添加独有特征
public class Cat extends Animal {
    public Cat(String name) {
        super(name);  // 调用父类的构造方法
    }

    public void meow() {
        System.out.println(getName() + " 喵喵叫!");
    }
}

// 另一个子类
public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    public void bark() {
        System.out.println(getName() + " 汪汪叫!");
    }
}

使用这些类:

public class Main {
    public static void main(String[] args) {
        Cat cat = new Cat("咪咪");
        Dog dog = new Dog("旺财");

        // 子类自动拥有父类的方法
        cat.eat();      // 咪咪 正在吃东西。
        cat.sleep();    // 咪咪 正在睡觉。
        cat.meow();     // 咪咪 喵喵叫!

        dog.eat();      // 旺财 正在吃东西。
        dog.sleep();    // 旺财 正在睡觉。
        dog.bark();     // 旺财 汪汪叫!
    }
}

关键点

  • Cat 继承 Animal 后,自动拥有 eat()sleep() 方法——不需要在 Cat 中重新写一遍。
  • Cat 还可以添加自己独有的方法 meow()
  • Java 只支持单继承:一个子类只能有一个直接父类。但父类也可以继承另一个父类,形成继承链。

3.3 super 关键字——调用父类的构造和方法

在上面的例子中,Cat 的构造方法里出现了 super(name)。这个 super 代表父类super(name) 就是调用父类的构造方法

重要规则:子类的构造方法中,必须调用父类的构造方法(通过 super(...)),而且必须是第一行。如果你不写,Java 会自动插入 super()(调用父类的无参构造方法)。但如果父类没有无参构造方法,编译就会报错。

public class Cat extends Animal {
    public Cat(String name) {
        super(name);  // 必须写在第一行,调用父类的 Animal(String name)
    }
}

super 还可以用来调用父类的方法:

public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

    @Override
    public void eat() {
        super.eat();  // 先调用父类的 eat()
        System.out.println(getName() + " 吃完后舔了舔爪子。");  // 再加上自己的行为
    }
}

四、方法重写——子类重新定义父类的方法

子类继承父类的方法后,如果觉得父类的方法不够好,可以重新定义一个同名的方法——这叫方法重写

public class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    public void makeSound() {
        System.out.println(this.name + " 发出了声音。");
    }
}

public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

    // 重写父类的 makeSound 方法
    @Override
    public void makeSound() {
        System.out.println(this.name + " 喵喵叫!");
    }
}

public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    public void makeSound() {
        System.out.println(this.name + " 汪汪叫!");
    }
}
public class Main {
    public static void main(String[] args) {
        Cat cat = new Cat("咪咪");
        Dog dog = new Dog("旺财");

        cat.makeSound();  // 咪咪 喵喵叫!
        dog.makeSound();  // 旺财 汪汪叫!
    }
}

方法重写的规则

  • 子类方法的方法名、参数列表、返回类型必须和父类完全一样
  • 子类方法的访问权限不能比父类更严格(父类是 public,子类不能是 private)。
  • @Override 是注解,写在重写的方法前面。它不是必须的,但强烈推荐写——它让编译器帮你检查是否真的重写了父类的方法(如果方法名拼错了,编译器会报错)。

五、多态——同一个方法,不同的表现

多态是面向对象编程中最核心、也最难理解的概念。它的字面意思是“多种形态”——同一个类型的引用变量,可以指向不同的子类对象,调用同一个方法时表现出不同的行为。

听起来很抽象,看代码就清楚了:

public class Main {
    public static void main(String[] args) {
        // 用父类类型的变量,引用子类对象
        Animal a1 = new Cat("咪咪");
        Animal a2 = new Dog("旺财");

        // 调用同一个方法,实际执行的是子类重写后的版本
        a1.makeSound();  // 咪咪 喵喵叫!
        a2.makeSound();  // 旺财 汪汪叫!
    }
}

关键理解

  • a1声明类型Animal(父类),但它实际指向的对象Cat(子类)。
  • 当你调用 a1.makeSound() 时,Java 不是根据声明类型 Animal 来决定调用哪个方法,而是根据实际对象类型 Cat 来决定——这叫动态绑定
  • 所以 a1.makeSound() 调用的是 CatmakeSound()(喵喵叫),a2.makeSound() 调用的是 DogmakeSound()(汪汪叫)。

多态的威力——写更通用的代码

多态最大的价值在于:你可以写出处理父类类型的代码,但实际运行时会根据子类的不同表现出不同的行为。

public class Main {
    // 这个方法接收 Animal 类型,但可以传入任何 Animal 的子类对象
    public static void letAnimalSound(Animal animal) {
        System.out.print("让动物叫一声:");
        animal.makeSound();
    }

    public static void main(String[] args) {
        Cat cat = new Cat("咪咪");
        Dog dog = new Dog("旺财");

        // 同一个方法,传入不同的子类对象,产生不同的行为
        letAnimalSound(cat);  // 让动物叫一声:咪咪 喵喵叫!
        letAnimalSound(dog);  // 让动物叫一声:旺财 汪汪叫!
    }
}

letAnimalSound 方法的参数类型是 Animal。你可以传入 CatDog、或者任何将来新写的 Animal 子类——这个方法不需要修改就能处理所有新的子类。这就是多态带来的灵活性和可扩展性。

六、综合演示——动物管理系统

下面这个示例综合运用了封装、继承、方法重写和多态:

// Animal.java —— 父类
public class Animal {
    private String name;
    private int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return this.name; }
    public int getAge() { return this.age; }

    public void eat() {
        System.out.println(this.name + " 正在吃东西。");
    }

    public void makeSound() {
        System.out.println(this.name + " 发出了声音。");
    }

    public void showInfo() {
        System.out.println("名称:" + this.name + ",年龄:" + this.age + " 岁");
    }
}

// Cat.java —— 子类
public class Cat extends Animal {
    private String furColor;  // 猫特有的属性:毛色

    public Cat(String name, int age, String furColor) {
        super(name, age);
        this.furColor = furColor;
    }

    @Override
    public void makeSound() {
        System.out.println(getName() + " 喵喵叫!");
    }

    @Override
    public void showInfo() {
        super.showInfo();  // 调用父类的 showInfo
        System.out.println("  物种:猫,毛色:" + this.furColor);
    }

    public void catchMouse() {
        System.out.println(getName() + " 抓住了一只老鼠!");
    }
}

// Dog.java —— 子类
public class Dog extends Animal {
    private String breed;  // 狗特有的属性:品种

    public Dog(String name, int age, String breed) {
        super(name, age);
        this.breed = breed;
    }

    @Override
    public void makeSound() {
        System.out.println(getName() + " 汪汪叫!");
    }

    @Override
    public void showInfo() {
        super.showInfo();
        System.out.println("  物种:狗,品种:" + this.breed);
    }

    public void guardHouse() {
        System.out.println(getName() + " 在看家护院!");
    }
}

// Main.java —— 测试
public class Main {
    public static void main(String[] args) {
        // 创建一个动物数组(利用多态)
        Animal[] animals = new Animal[4];
        animals[0] = new Cat("咪咪", 2, "橘色");
        animals[1] = new Dog("旺财", 3, "金毛");
        animals[2] = new Cat("小黑", 1, "黑色");
        animals[3] = new Dog("大黄", 5, "中华田园犬");

        System.out.println("===== 动物信息 =====");
        for (Animal animal : animals) {
            animal.showInfo();
            System.out.println();
        }

        System.out.println("===== 动物叫声 =====");
        for (Animal animal : animals) {
            animal.makeSound();  // 多态:实际调用子类重写后的方法
        }

        System.out.println("\n===== 进食时间 =====");
        for (Animal animal : animals) {
            animal.eat();  // 所有动物共享父类的 eat 方法
        }
    }
}

输出:

===== 动物信息 =====
名称:咪咪,年龄:2 岁
  物种:猫,毛色:橘色

名称:旺财,年龄:3 岁
  物种:狗,品种:金毛

名称:小黑,年龄:1 岁
  物种:猫,毛色:黑色

名称:大黄,年龄:5 岁
  物种:狗,品种:中华田园犬

===== 动物叫声 =====
咪咪 喵喵叫!
旺财 汪汪叫!
小黑 喵喵叫!
大黄 汪汪叫!

===== 进食时间 =====
咪咪 正在吃东西。
旺财 正在吃东西。
小黑 正在吃东西。
大黄 正在吃东西。

七、本篇动手练习

练习 1:封装练习

新建 Product.javaProductDemo.java。定义 Product 类,私有属性:名称(String)、价格(double)、库存(int)。提供构造方法和 getter/setter。在 setter 中验证:价格不能为负数,库存不能为负数。测试封装的验证是否生效。

练习 2:继承练习

新建 Vehicle.java(父类——交通工具,属性:品牌、速度,方法:move())、Car.java(子类——汽车,加一个 seatCount 座位数)、Bicycle.java(子类——自行车,加一个 hasBasket 是否有车篮)。测试继承关系。

练习 3:方法重写练习

在练习 2 的基础上,让 CarBicycle 分别重写 move() 方法,输出不同的移动描述(比如“汽车在公路上行驶”、“自行车在骑行道上行驶”)。

练习 4:多态练习

在练习 2 和 3 的基础上,创建一个 Vehicle 类型数组,放入 Car 和 Bicycle 对象。用循环遍历数组,调用每个对象的 move() 方法,观察多态的效果。

八、本篇小结

这一篇你学会了面向对象编程的三大核心特性:

  • 封装:用 private 隐藏属性,通过公共的 getter/setter 方法访问。setter 中可以加验证逻辑,保护数据不被错误修改。
  • 继承:用 extends 让子类获得父类的属性和方法。子类可以添加自己的属性和方法。super() 调用父类构造方法(必须写在子类构造的第一行)。
  • 方法重写:子类重新定义父类的方法(方法名、参数、返回类型完全相同)。用 @Override 注解让编译器帮你检查是否正确重写。
  • 多态:父类类型的变量可以引用子类对象。调用方法时,Java 根据实际对象类型决定执行哪个版本(动态绑定)。多态让你写出更通用、更灵活的代码。

封装、继承、多态是 Java 面向对象编程的基石。理解了它们,你就能用面向对象的思想来设计和组织代码。下一篇,我们学习 Java 中最常用的两个工具类——String 和 ArrayList,它们在日常开发中无处不在。

下一篇预告

下一篇——《字符串与集合——String 和 ArrayList》:String 类的常用方法(长度、截取、替换、查找、比较)、StringBuilder 高效拼接字符串、ArrayList 灵活的动态数组、集合的遍历和常用操作。

Java 零基础入门,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容