面向对象编程(继承、多态)

一、Java 继承 (Inheritance) 秒懂指南:不做重复造轮子的“傻瓜”

在 Java 面向对象编程中,有三大核心特性:封装、继承、多态。今天我们来聊聊最容易理解,也是代码复用利器——继承(Inheritance)

1. 什么是继承?为什么要用它?

在现实生活中,儿子可以继承父亲的财产。在 Java 的世界里,子类可以继承父类的属性和方法

为什么要用继承?
想象一下,你要写一个系统,里面有“猫”和“狗”两种动物。

  • 猫:有名字、年龄,会吃东西,会“喵喵叫”。
  • 狗:有名字、年龄,会吃东西,会“汪汪叫”。

如果不使用继承,你要在猫和狗的类里把“名字、年龄、吃东西”的代码写两遍。如果有 100 种动物,你就要写 100 遍!这就是在重复造轮子。

继承就是为了解决这个问题:把大家共有的特征,抽离出来放到一个“父类”中。子类只需要继承父类,就能直接拥有这些共有的东西,自己再补充独有的功能即可。

2. 代码实战:extends 关键字

在 Java 中,我们使用 extends(扩展/继承)关键字来实现继承。

// 1. 提取共有特征,创建一个父类 (Parent/Superclass)
public class Animal {
    String name;
    int age;

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

// 2. 创建子类,使用 extends 继承父类
public class Dog extends Animal {
    // 补充子类独有的方法
    public void bark() {
        System.out.println(name + " 在汪汪叫!");
    }
}

// 3. 测试一下
public class Test {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        // name 属性明明在 Dog 类里没写,但因为继承了 Animal,可以直接用!
        myDog.name = "大黄"; 
        
        myDog.eat();  // 输出: 大黄 正在大口吃东西... (继承来的方法)
        myDog.bark(); // 输出: 大黄 在汪汪叫! (自己的独有方法)
    }
}

3. 继承的三大“铁律”

在使用继承时,新手一定要记住以下几条核心规则(面试必考):

  1. 单继承原则:一个子类只能有一个直接的父类。就像现实中一个人只能有一个亲生父亲。class Dog extends Animal, Pet 这种想同时认两个爹的写法,在 Java 里是绝对会报错的!
  2. 私有财产神圣不可侵犯:父类中被 private 修饰的属性和方法,子类虽然名义上也继承过来了,但不能直接访问(必须通过父类提供的 public getter/setter 方法去间接获取)。
  3. 构造器不能被继承:父类的构造方法(Constructor)子类是继承不了的。但是,子类在被 new 出来创建对象时,默认会先悄悄调用父类的无参构造方法(底层通过隐式的 super() 实现),正所谓“先有父,后有子”。

总结

  • 核心作用:代码复用,减少冗余,建立类与类之间的层级关系。
  • 关键字extends(子类 extends 父类)。
  • 口诀:子类拥有父类非 private 的所有属性和方法,并且可以尽情地扩展自己独有的属性和方法。

二、Java 权限修饰符秒懂:你的数据到底谁能看?

在 Java 的代码里,我们每天都在跟 publicprivate 这些词打交道。它们叫作权限修饰符(Access Modifiers)

想象一下你的生活:有些秘密只能自己知道(日记本),有些东西可以和家人共享(电视机),有些家产只有亲儿子能继承(祖传秘方),而有些则是完全对外开放的(公园)。

Java 的权限修饰符就是用来控制类、属性、方法到底能被谁访问的。今天我们就用一张表、几个比喻,把这 4 种修饰符彻底搞懂。

1. 四大修饰符详解(从小到大)

① private(私有:只有我自己能看)

  • 可见范围只能在当前类内部访问
  • 生活比喻:你的带锁日记本。除了你自己,哪怕是亲儿子(子类)或者同住一个屋檐下的人(同包类)都绝对看不了。
  • 最常用途:修饰实体类的成员变量,保护数据不被外部随意篡改(这就是“封装”的核心)。

② default(缺省/默认:家里人随便用)

  • 可见范围只能在同一个包(Package)内访问。注意,如果你什么修饰符都不写,默认就是它!
  • 生活比喻:你家客厅的电视机。只要是和你住在同一个屋檐下(同一个包里)的类,都能随便看。但隔壁老王(其他包的类)是绝对看不了的。

③ protected(受保护:家里人和外地亲儿子能用)

  • 可见范围同一个包内 + 不同包的子类
  • 生活比喻:家里的祖传秘方。同村的人(同包类)可以看,就算你的亲儿子分家搬到了外地(不同包的子类),他也有权继承和使用。但是外地的陌生人(不同包且非子类)是拿不到的。

④ public(公开:全天下随便看)

  • 可见范围整个项目的所有地方,任何包、任何类
  • 生活比喻:城市里的街心公园。无论你是谁,住在哪里,只要你顺着地址找过来,就能随便进出。
  • 最常用途:修饰供外部调用的核心方法、类的入口(如 main 方法)或对外暴露的 Getter/Setter 方法。

2. 一张表总结(面试必考,建议收藏)

修饰符 当前类内部 同一个包 (同村) 不同包的子类 (外地儿子) 其他任何地方 (外地陌生人)
private
(default)
protected
public

(注:✅ 代表可以访问,❌ 代表禁止访问)

3. 开发中的“潜规则”(最佳实践)

  • 属性尽量私有(private:除非有极其特殊的理由,否则所有类的成员变量都应该用 private 锁起来。如果别人想看或者想改,必须通过你提供的 public 级别 get/set 方法。这就像银行柜员机,钱(数据)藏在铁皮箱里,只能通过按键(方法)来操作。
  • 方法按需开放:专门写给别人调用的核心业务逻辑定为 public;如果是仅在类内部自己使用的“辅助计算小方法”,定为 private,不要暴露出去污染别人的视野。

三、Java 方法重写 (Override) 秒懂:青出于蓝的 4 条铁律

在 Java 的继承关系中,子类不仅可以继承父类的财产,还可以对父类原有的方法进行“改造升级”,这就叫做方法重写(Override)

但是,重写不是你想怎么改就怎么改的,Java 编译器对此有严格的“家规”。今天我们就来盘点一下重写时必须牢记的规则,以及极易踩坑的注意事项。

1. 核心铁律:“两同、两小、一大”

为了方便记忆,你可以把重写最核心的规则总结为这句经典的口诀:两同、两小、一大

① 两同:方法名和参数列表必须一模一样

这是重写的最基本要求。如果你把方法名拼错了,或者参数的类型、数量改了,那就不叫重写了,而是变成了“重载(Overload)”或者干脆是一个全新的方法。

② 两小:返回值类型和抛出的异常,只能更小(或相等)

  • 返回值类型:如果父类方法返回的是 Animal,子类重写时可以返回 Animal,也可以返回更具体的 Dog(子类),但绝对不能返回一个更宽泛的 Object
  • 抛出异常:子类重写的方法,不能抛出比父类更多、更宽泛的编译时异常。比如老爸的方法可能抛出“轻微感冒”,儿子重写的方法绝不能抛出“绝症”。

③ 一大:访问权限只能更大(或相等)

  • 子类重写方法的访问权限,绝对不能比父类更严格。
  • 生活比喻:老爸把一个绝活(protected)公开给了全村,儿子继承后可以继续在村里公开(protected),或者直接开个网店向全网公开(public),但绝不能偷偷藏起来只给自己看(private)。

2. 哪些方法是“重写禁区”?

并不是所有的父类方法都能被子类改造,以下 3 种情况坚决不能重写:

  1. private 私有方法:父类的私有方法对子类本来就不可见,连看都看不到,谈何重写?
  2. static 静态方法:静态方法属于“类”,不参与多态。如果子类写了一个一模一样的静态方法,那叫“隐藏”,不叫重写。
  3. final 方法:被 final 修饰的方法意味着它是“最终版本”,也就是明确下达了禁令:“任何人都不准再改了”。

3. 最佳实践:永远加上 @Override 注解

在实际开发中,只要你是想重写父类方法,请务必在方法上面加上 @Override 注解

public class Dog extends Animal {
    @Override // 强烈建议加上这个注解
    public void eat() {
        System.out.println("小狗正在快乐地啃骨头");
    }
}

为什么要加?
这不仅是为了告诉阅读代码的同事“这是一个重写的方法”,更重要的是让编译器帮你把关
如果你不小心拼错了方法名(比如手抖写成了 eatting()),如果有 @Override,编译器立马就会标红报错提醒你;如果不加,编译器会以为你单纯想写个新方法,从而引发极其隐蔽的 Bug。

四、Java 子类构造器秒懂:先有老爸,后有儿子

在 Java 继承体系中,当我们 new 一个子类对象时,底层其实发生了一件非常重要的事情:它会先去调用父类的构造器,然后再执行子类自己的构造器。

这就是 Java 宇宙中最朴素的真理:“先有父,后有子”。今天我们来彻底搞懂子类构造器背后的运行机制和常踩的坑。

1. 隐形的 super()

很多时候我们在写子类时,并没有觉得父类的构造器被调用了。这是因为 Java 编译器在背后默默帮你做了一件事。

如果你在子类的构造器里什么都没写,编译器会自动在第一行插入一个隐形的 super();,专门用来调用父类的无参构造器

class Father {
    public Father() {
        System.out.println("1. 老爸诞生了");
    }
}

class Son extends Father {
    public Son() {
        // 编译器其实在这里悄悄加了一行:super();
        System.out.println("2. 儿子诞生了");
    }
}

// 当我们在 main 方法里 new Son() 时,输出结果会是:
// 1. 老爸诞生了
// 2. 儿子诞生了

2. 手动调用 super(参数)

如果父类的属性需要赋值,我们就可以在子类构造器中,手动使用 super(参数) 来调用父类的有参构造器

⚠️ 铁律:super() 必须放在子类构造器的第一行! (老爸都没安顿好,儿子不能先干活)。

class Animal {
    String name;
    
    // 父类的有参构造
    public Animal(String name) {
        this.name = name;
    }
}

class Dog extends Animal {
    int age;
    
    public Dog(String name, int age) {
        // 第一行:先把 name 传给父类,让父类完成初始化
        super(name); 
        // 第二行:再初始化子类自己独有的属性
        this.age = age; 
    }
}

3. 新手连环坑:父类没有无参构造器怎么办?

这是初学者写继承时最容易报错的地方!
还记得构造器的规则吗:“如果写了有参构造,系统默认赠送的无参构造就会消失”。

如果父类只有有参构造器,那么子类中那个隐形默认的 super(); 就找不到东西调用了,直接编译报错

破解之法有两种:

  1. (强烈推荐) 老老实实给父类补上一个无参构造器。这能省去子类 99% 的麻烦。
  2. 在子类的所有构造器中,第一行手动写明 super(具体参数),明确告诉编译器去调用父类的哪个有参构造器。

总结

  • 执行顺序:永远是先执行父类构造器,再执行子类构造器。
  • 关键字super 用于调用父类构造器,且必须写在子类构造器的第一行
  • 避坑口诀:写父类时,尽量养成随手写一个无参构造器的习惯。

五、 Java 多态 (Polymorphism) 秒懂:同一个指令,不同的表现

在了解了封装和继承之后,我们终于迎来了 Java 面向对象的三大特性之首——多态(Polymorphism)

很多初学者觉得多态很抽象。其实用一句话就能解释:“同一个行为,不同的对象去执行,会有完全不同的结果。”

举个生活中的例子:同样是“按下 F 键”这个指令,如果在浏览器里,可能是全屏;如果在 Word 里,可能是打出一个字母 f;如果在某款游戏里,可能是“Pay Respects”。指令是一样的,但因为对象不同,表现就不同,这就是多态!

1. 触发多态的 3 大前提条件

在 Java 中,想要实现多态,必须同时满足以下三个条件,缺一不可:

  1. 必须有继承(或实现)关系:这是多态的基础(比如 Dog 继承 Animal)。
  2. 必须有方法重写(Override):子类必须对父类的方法进行改造。
  3. 父类引用指向子类对象:这是多态最核心的语法形式,即 父类类型 变量名 = new 子类对象();

2. 代码实战:感受多态的魅力

我们用经典的“动物叫声”来演示:

// 1. 父类
class Animal {
    public void makeSound() {
        System.out.println("动物发出叫声...");
    }
}

// 2. 子类狗 (继承 + 重写)
class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("汪汪汪!");
    }
}

// 2. 子类猫 (继承 + 重写)
class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("喵喵喵~");
    }
}

public class Test {
    public static void main(String[] args) {
        // 3. 父类引用指向子类对象 (向上转型)
        Animal a1 = new Dog(); 
        Animal a2 = new Cat(); 

        // 发出同样的指令,表现完全不同!
        a1.makeSound(); // 输出: 汪汪汪!
        a2.makeSound(); // 输出: 喵喵喵~
    }
}

3. 黄金法则:“编译看左边,运行看右边”

当你写出 Animal a1 = new Dog(); 时,这个 a1 到底算 Animal 还是算 Dog?

记住多态中最著名的一句口诀:对于成员方法来说,“编译看左边,运行看右边”

  • 编译看左边:Java 编译器在检查代码时,只看等号左边的类型(Animal)。如果 Animal 类里没有 makeSound() 这个方法,代码直接报错,根本不给你运行的机会。
  • 运行看右边:一旦编译通过,真正跑到内存里执行的时候,Java 会去看等号右边到底是个什么实体对象(Dog)。发现是条狗,就坚决执行狗重写过的方法。

4. 我们为什么非要用多态?

很多新手会问:“我直接 Dog d = new Dog(); 不香吗?非要搞个 Animal a = new Dog(); 假装自己是普通动物干嘛?”

多态真正的威力在于扩展性和解耦

想象一下你要写一个宠物医院的看病系统,里面有一个医生类给动物治病:

  • 没有多态:你需要写 treat(Dog d)treat(Cat c)treat(Pig p)... 以后每加一种动物,你都要修改医生类的代码。
  • 有了多态:你只需要写一个方法:
    public void treat(Animal animal) {
        System.out.println("开始检查...");
        animal.makeSound(); // 传进来什么动物,就会发出什么声音
    }
    
    不管以后系统增加了多少种新动物,医生类一行代码都不用改!这就是设计模式中常常追求的“开闭原则”(对扩展开放,对修改关闭)。

总结

多态的本质就是屏蔽子类之间的差异,统一把它们当成父类来处理,从而写出极其灵活、可扩展的代码。记住三大前提和“编译看左,运行看右”,多态对你来说就再也没有秘密了。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容