零基础学Java|第九篇:面向对象编程的类与对象(进阶)

开篇:从基础到进阶,构建面向对象的思维大厦

上一篇我们学习了类与对象的基础概念,理解了对象在内存中的存在形式,掌握了方法调用机制和构造器的使用。如果把面向对象编程比作建造一座大厦,那么上一篇我们打下了地基------学会了如何创建砖块(对象)并理解它们的基本属性。

而今天,我们将开始搭建这座大厦的主体结构。我们会深入探讨Java面向对象的三大核心特征:封装、继承和多态 ,学习如何通过包来组织代码,如何用访问修饰符控制可见性,以及理解Java灵魂般的动态绑定机制。这些知识将真正带你进入Java编程的核心殿堂。


1. 包:代码的"文件夹"与命名空间

1.1 包的本质是什么?

想象一下,你的电脑里有成千上万个文件,如果没有文件夹来分类管理,找文件将是一场噩梦。 包(Package) 就是Java中用于组织代码的"文件夹"。

从本质上讲,包解决了两个核心问题:

  • 组织代码:将相关的类放在一起,形成逻辑模块
  • 命名空间管理 :防止类名冲突。比如你和同事都定义了一个 User 类,但只要放在不同的包里,就能和平共处

1.2 包的命名规范

为了保证包名的全球唯一性 ,Java采用了逆域名命名法

go 复制代码
// 域名是 example.com → 包名是 com.example.项目名.模块名
package com.example.shop.user;
package org.apache.commons.lang;

命名规则

  • 只能包含数字、字母、下划线、小圆点.,但不能用数字开头、不能是关键字或保留字
  • 用点号分隔层级
  • 不能以点号开头或结尾
  • 通常以组织域名的倒序开头

1.3 Java常用包一览

Java提供了丰富的标准库,都以包的形式组织:

包名 作用 使用频率
java.lang 语言基础类(String、System、Object) ⭐⭐⭐⭐⭐ 自动导入
java.util 工具类(集合、日期、随机数) ⭐⭐⭐⭐⭐
java.io 输入输出流 ⭐⭐⭐⭐
java.net 网络编程 ⭐⭐⭐
java.sql 数据库操作 ⭐⭐⭐
java.awt/javax.swing 图形界面、GUI ⭐⭐

特别注意:java.lang包是自动导入 的,不需要写 import 语句。

1.4 包的导入

要在代码中使用其他包的类,有两种方式:

java 复制代码
// 方式一:导入具体类(推荐)
import java.util.ArrayList;
import java.util.Scanner;

// 方式二:导入包下所有类(不推荐,降低可读性)
import java.util.*;

1.5 包的使用细节

关键规则package 声明必须在文件第一行(注释除外),且一个文件最多只能有一个包声明。import语句放在package声明后面, 在类定义前面,可以有多条且无顺序要求。

java 复制代码
// 正确的顺序
package com.example.demo;  // 第1行:包声明

import java.util.List;      // 之后:导入语句
import java.util.ArrayList;

public class MyClass {       // 最后:类定义
    // ...
}

同名类的处理:如果同时用到两个不同包中的同名类,必须使用全限定名来区分:

java 复制代码
import java.util.Date;      // java.util.Date

// 如果想同时使用 java.sql.Date,不能用import,必须全限定名
public class Test {
    Date utilDate = new Date();                          // java.util.Date
    java.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis());  // java.sql.Date
}

2. 访问修饰符:控制可见性的"权限锁"

2.1 四种访问级别

修饰符可以用来修饰类中的属性,成员方法以及类。 访问修饰符决定了类、方法、变量能被哪些地方访问。 Java提供了四种访问级别,从严格到宽松依次是:

修饰符 同类中 同包中 子类中 任何地方
private
默认(无修饰符)
protected
public

2.2 生动的记忆法

想象你在写遗嘱,要把自己的遗产分给不同的人:

  • private(私人物品):只有你自己能看(个人日记)
  • 默认(家人共享):只有家人在场才能看(家庭相册)
  • protected(留给后代):家人和后代都能继承(家族房产)
  • public(公之于众):所有人都能看(回忆录)

2.3 访问修饰符的细节

类的访问修饰符 :外部类只能用 public 或默认,不能是 privateprotected。成员方法的访问规则和属性完全一样。

成员变量的最佳实践 :通常将成员变量设为 private,通过 publicgetter/setter 访问,这正是封装思想的体现。

java 复制代码
public class Student {
    private String name;  // 隐藏细节
    private int age;
    
    public String getName() {  // 提供公共访问接口
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
}

3. 面向对象三大特征之一:封装

3.1 什么是封装?

封装就是将对象的状态(属性)和行为(方法)绑定在一起,并对外隐藏内部实现细节,仅公开有限的访问接口。

现实中的例子:电视机。你只需要用遥控器(公开接口)来操作,而不需要知道内部的电路如何工作(隐藏细节)。

3.2 封装的实现步骤

  1. 属性私有化 :使用 private 修饰成员变量
  2. 提供公共访问方法 :为每个属性编写 gettersetter 方法
  3. 在方法中添加逻辑控制 :可以在 setter 中加入验证逻辑
java 复制代码
public class BankAccount {
    private String accountNumber;
    private double balance;
    
    // 构造器
    public BankAccount(String accountNumber, double balance) {
        this.accountNumber = accountNumber;
        this.balance = balance;
    }
    
    // 公共接口方法 - 存款
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("成功存入:" + amount);
        } else {
            System.out.println("存款金额必须大于0");
        }
    }
    
    // 公共接口方法 - 取款(带控制逻辑)
    public boolean withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            return true;
        }
        return false;
    }
    
    // 只提供getter,不提供setter,余额只能通过存款取款改变
    public double getBalance() {
        return balance;
    }
}

3.3 封装的好处

  • 安全性:防止外部直接修改内部数据,可以加入验证逻辑
  • 隔离变化:内部实现改变不影响外部调用
  • 可复用性:封装好的类可以在多处使用
  • 简化调用:使用者只需关注公开接口,无需理解内部实现

3.4 构造器与 setter 结合

在构造器中直接调用 setter 方法,可以复用验证逻辑:

java 复制代码
public class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        setName(name);      // 调用setter,复用验证逻辑
        setAge(age);
    }
    
    public void setName(String name) {
        if (name != null && name.length() >= 2) {
            this.name = name;
        } else {
            throw new IllegalArgumentException("姓名至少2个字符");
        }
    }
    
    public void setAge(int age) {
        if (age >= 0 && age <= 150) {
            this.age = age;
        } else {
            throw new IllegalArgumentException("年龄不合法");
        }
    }
}

4. 面向对象三大特征之二:继承

4.1 为什么需要继承?

现实世界中,事物之间存在着"is-a"的关系。比如:"学生是人"、"猫是动物"。这种关系在编程中通过继承来体现。

继承允许我们基于一个已有的类创建新类,新类可以复用父类的属性和方法,并在此基础上进行扩展。

4.2 继承的基本语法

java 复制代码
// 父类(基类、超类)
class Animal {
    protected String name;  // protected 让子类可以访问
    
    public Animal(String name) {
        this.name = name;
    }
    
    public void eat() {
        System.out.println(name + "正在吃东西");
    }
    
    public void sleep() {
        System.out.println(name + "正在睡觉");
    }
}

// 子类(派生类)
class Dog extends Animal {
    public Dog(String name) {
        super(name);  // 调用父类构造器,必须放在第一行
    }
    
    // 新增方法
    public void bark() {
        System.out.println(name + "汪汪叫");
    }
    
    // 重写(Override)父类方法
    @Override
    public void eat() {
        System.out.println(name + "正在啃骨头");
    }
}

4.3 继承的本质与内存图

当我们执行 Dog dog = new Dog("旺财"); 时,内存中发生了什么?

lua 复制代码
堆内存中的 Dog 对象
+----------------------------------+
| 从 Animal 继承的部分              |
|   name = "旺财" (引用指向字符串常量池) |
|  Dog 自己的部分                    |
|   (没有新增属性)                    |
+----------------------------------+

栈内存
+------------------+
| dog 引用 (0x1234) | -----> 指向堆中的 Dog 对象
+------------------+

继承的本质:子类对象包含一个完整的父类对象子对象。可以理解为子类对象在堆内存中为父类的所有属性都分配了空间。

4.4 继承的细节与规则

1. 单继承限制:Java 只支持单继承,一个类只能有一个直接父类。

2. 传递性 :继承具有传递性,class C extends Bclass B extends A,那么 C 拥有 A 和 B 的所有非私有成员。

3. 构造器的调用链:创建子类对象时,一定会调用父类的构造器,最终会调用到 Object 类的构造器。

java 复制代码
class A {
    public A() {
        System.out.println("A构造器");
    }
}

class B extends A {
    public B() {
        // 这里隐含了 super()
        System.out.println("B构造器");
    }
}

class C extends B {
    public C() {
        // 隐含 super()
        System.out.println("C构造器");
    }
}

// 执行 new C(); 输出:
// A构造器
// B构造器
// C构造器

4. 父类私有成员 :子类拥有 父类的私有成员,但不能直接访问,需要通过公共的 getter/setter。

5. 创建子类对象时,内存中只有一个对象,而不是多个对象叠加。


5. super 关键字:指向父类的引用

5.1 super 是什么?

super 是一个关键字,代表当前对象的父类部分的引用 。它和 this 类似,但 this 指向当前对象本身,而 super 指向当前对象中的父类部分。

5.2 super 的三种用法

1. 调用父类的属性 :当子类有同名属性时,用 super.属性 访问父类的属性

java 复制代码
class Parent {
    String name = "Parent";
}

class Child extends Parent {
    String name = "Child";
    
    public void printName() {
        System.out.println(name);        // 输出 Child
        System.out.println(super.name);  // 输出 Parent
    }
}

2. 调用父类的方法 :当子类重写了父类方法,用 super.方法() 调用父类被重写的方法

java 复制代码
class Parent {
    public void show() {
        System.out.println("Parent show");
    }
}

class Child extends Parent {
    @Override
    public void show() {
        super.show();  // 先调用父类的show
        System.out.println("Child show");  // 再执行自己的逻辑
    }
}

3. 调用父类的构造器 :用 super(参数) 调用父类指定的构造器,必须放在子类构造器的第一行

5.3 super 与 this 的对比

对比项 this super
指向 当前对象的引用 当前对象中父类部分的引用
查找范围 先找本类,找不到再找父类 直接找父类
特殊要求 调用构造器时必须放在第一行
能否同时使用 不能与 super 同时出现在构造器第一行 不能与 this 同时出现在构造器第一行

核心规则 :在构造器中,this(...)super(...) 只能二选一,且必须放在第一行。


6. 方法重写

6.1 什么是方法重写?

子类对父类中允许访问的方法重新实现 ,方法的签名(名称+参数列表)保持不变,称为方法重写(Override)。

6.2 重写的规则

必须满足的条件

  • 方法名、参数列表必须完全相同
  • 返回值类型:如果是基本类型,必须相同;如果是引用类型,可以是原返回类型的子类型 (称为协变返回类型
  • 访问权限:不能比父类更严格(可以相同或更宽松)
  • 不能重写 privatestaticfinal 方法
java 复制代码
class Animal {
    protected Animal getAnimal() {
        return new Animal();
    }
}

class Dog extends Animal {
    // 返回值类型可以是 Animal 的子类 Dog
    @Override
    public Dog getAnimal() {  // 访问权限从 protected 提升为 public
        return new Dog();
    }
}

6.3 重写 vs 重载

对比项 重写 重载
发生范围 父子类之间 同一个类中
方法名 必须相同 必须相同
参数列表 必须相同 必须不同
返回值 相同或子类型 无关
访问权限 不能更严格 无关
目的 改变行为,实现多态 提供更多调用方式

7. 面向对象三大特征之三:多态

7.1 什么是多态?

多态 (Polymorphism)是指"同一个接口,不同的实现 "。通俗地说,就是父类的引用指向子类的对象,调用同一个方法时,表现出不同的行为。

7.2 多态的实现条件

在Java中,实现多态需要满足三个条件:

  1. 继承:存在父子类关系
  2. 重写:子类重写父类的方法
  3. 父类引用指向子类对象Animal a = new Dog();

7.3 多态的具体体现

java 复制代码
class Animal {
    public void speak() {
        System.out.println("动物发出声音");
    }
}

class Dog extends Animal {
    @Override
    public void speak() {
        System.out.println("汪汪汪");
    }
    
    public void wagTail() {
        System.out.println("摇尾巴");
    }
}

class Cat extends Animal {
    @Override
    public void speak() {
        System.out.println("喵喵喵");
    }
}

public class Test {
    public static void main(String[] args) {
        // 多态:父类引用指向子类对象
        Animal a1 = new Dog();
        Animal a2 = new Cat();
        
        a1.speak();  // 输出:汪汪汪
        a2.speak();  // 输出:喵喵喵
        
        // a1.wagTail(); // 编译错误!Animal类型没有wagTail方法
    }
}

7.4 多态的细节

1. 编译时看左边,运行时看右边

  • 编译时,编译器检查左边引用的类型是否有该方法
  • 运行时,JVM实际调用右边对象的方法

2. 不能调用子类特有的方法:父类引用只能调用父类中声明的方法,不能调用子类特有的方法。

3. 属性的访问属性没有多态性!访问属性时,看左边引用的类型。

java 复制代码
class Parent {
    String name = "Parent";
}

class Child extends Parent {
    String name = "Child";
}

public class Test {
    public static void main(String[] args) {
        Parent p = new Child();
        System.out.println(p.name);  // 输出 Parent,不是 Child!
    }
}

7.5 动态绑定机制(核心原理)

这是Java多态的灵魂所在!

动态绑定 :在运行时,根据对象的实际类型来确定调用哪个方法,而不是根据引用变量的类型。

原理揭秘

  1. 每个类在方法区中都有一个方法表,存储了该类的所有方法入口地址
  2. 当调用虚方法(非private、static、final方法)时,JVM通过对象的实际类型找到对应的方法表
  3. 从方法表中获取方法的实际入口地址进行调用

为什么属性没有多态? 因为属性在编译期就已经确定了访问哪个,不需要动态绑定。

7.6 多态的应用

1. 方法参数的多态:编写一个方法,接收父类类型,实际可以传入任意子类对象

java 复制代码
public void animalSpeak(Animal a) {  // 多态参数
    a.speak();  // 根据实际传入的对象,调用不同的speak
}

animalSpeak(new Dog());  // 汪汪汪
animalSpeak(new Cat());  // 喵喵喵

2. 数组/集合的多态:可以创建父类类型的数组,存放各种子类对象

java 复制代码
Animal[] animals = new Animal[3];
animals[0] = new Dog();
animals[1] = new Cat();
animals[2] = new Dog();

for (Animal a : animals) {
    a.speak();  // 各自发出不同的声音
}

3. 强制类型转换(向下转型):当需要调用子类特有方法时,可以强制转换

java 复制代码
Animal a = new Dog();
if (a instanceof Dog) {        // 先判断类型,避免 ClassCastException
    Dog d = (Dog) a;
    d.wagTail();               // 现在可以调用子类特有方法
}

8. Object 类详解

8.1 Object 是什么?

Object 类是Java中所有类的根父类。每个类都直接或间接继承自 Object。

8.2 equals() 方法

默认实现 :比较两个对象的内存地址 ,相当于 ==

java 复制代码
// Object 类中的默认实现
public boolean equals(Object obj) {
    return (this == obj);
}

重写原则 :当我们需要根据对象的内容判断是否相等时(比如两个Person对象的id相同就算相等),就需要重写 equals。

java 复制代码
public class Person {
    private String id;
    private String name;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                 // 同一对象
        if (o == null || getClass() != o.getClass()) return false;  // 类型检查
        
        Person person = (Person) o;
        return Objects.equals(id, person.id);       // 根据id判断相等
    }
}

equals 方法的契约

  • 自反性:x.equals(x) 必须为 true
  • 对称性:x.equals(y) 与 y.equals(x) 结果相同
  • 传递性:x.equals(y) 且 y.equals(z),则 x.equals(z)
  • 一致性:多次调用结果一致(前提是参与比较的内容没变)
  • 非空性:x.equals(null) 必须为 false

8.3 hashCode() 方法

hashCode 返回对象的哈希码,是一个整数,主要用于哈希表(如 HashMap、HashSet)。

重要契约 :如果两个对象通过 equals 比较相等,那么它们的 hashCode 必须相等

java 复制代码
@Override
public int hashCode() {
    return Objects.hash(id);  // 和 equals 用相同的字段
}

常见错误:重写了 equals 但不重写 hashCode,会导致对象无法在 HashSet/HashMap 中正常工作!

8.4 toString() 方法

默认实现类名@十六进制哈希码,例如 Person@4eec7777

重写目的:提供更有意义的对象描述信息,方便调试和日志输出。

java 复制代码
@Override
public String toString() {
    return "Person{id='" + id + "', name='" + name + "'}";
}

8.5 finalize() 方法(已过时)

注意 :从 Java 9 开始,finalize() 已被标记为过时 (deprecated),不建议使用

淘汰原因

  • 执行时机不确定(依赖 GC)
  • 性能影响严重
  • 可能导致资源泄漏

替代方案 :使用 try-with-resources 或显式编写 close() 方法。


9. 断点调试:定位问题的"显微镜"

9.1 为什么需要断点调试?

当程序运行结果不符合预期时,我们需要"看"到程序执行的过程------变量如何变化,代码走哪个分支,方法如何调用。断点调试就是让我们暂停程序,逐行执行,观察内部状态的利器。

9.2 基本调试操作(以 IntelliJ IDEA 为例)

1. 设置断点:点击代码行号左侧的空白区域,出现红色圆点

2. 启动调试 :点击"Debug"按钮(小虫子图标)或按 Shift+F9

3. 调试控制按钮

按钮 快捷键 作用
Step Over F8 执行当前行,不进入方法内部
Step Into F7 进入当前行调用的方法内部
Step Out Shift+F8 跳出当前方法,返回调用处
Resume Program F9 继续执行到下一个断点或结束
Evaluate Expression Alt+F8 计算表达式,临时查看或修改变量

9.3 调试实战:观察动态绑定

创建一个调试场景,观察多态的动态绑定过程:

java 复制代码
public class DebugDemo {
    public static void main(String[] args) {
        Animal a = new Dog();    // 在这里设置断点
        a.speak();                // 观察实际调用哪个方法
    }
}

调试步骤

  1. a.speak(); 行设置断点
  2. Debug 运行,程序停在该行
  3. 按 F7(Step Into),观察进入哪个类的 speak 方法
  4. 查看 Variables 窗口,观察 a 的实际类型是 Dog

动手实践

练习一:包与访问修饰符

目标 :创建 com.study.bank 包,在其中定义 Account 类(属性:账户号、密码、余额,全部 private)。提供公共的存款、取款方法。在另一个包 com.study.test 中创建测试类,尝试访问 Account 的私有属性(应该失败),通过公共方法操作账户。

练习二:封装实践

目标 :设计 Employee 类,包含私有属性 name、salary、bonus。提供公共的 getter/setter,在 setSalary 中加入验证(必须大于0)。编写一个方法 calculateYearlyIncome() 计算年收入(工资+奖金)。使用构造器初始化对象。

练习三:继承与 super

目标 :创建 Vehicle(交通工具)父类,属性 brand、speed,方法 run() 输出"正在行驶"。创建子类 Car,增加属性 fuelType,重写 run() 方法,先调用父类的 run,再输出"使用燃油:xx"。使用 super 调用父类构造器。

练习四:多态与动态绑定

目标 :创建接口 Shape,包含方法 double area()。实现三个类 CircleRectangleTriangle,分别实现 area 方法。编写一个方法 printArea(Shape s),打印面积。在 main 中创建 Shape 数组,存放不同图形对象,遍历调用 printArea。

练习五:Object 方法重写

目标 :创建 Book 类,属性 isbn(唯一编号)、titleprice。重写 equals(根据 isbn 判断相等)、hashCode(使用 isbn 生成)、toString(返回完整信息)。创建 HashSet 存放 Book 对象,验证重复的 isbn 不会被加入。

练习六:断点调试练习

目标:故意编写一个有逻辑错误的递归方法(比如求斐波那契数列但递归条件写错),使用断点调试观察递归调用过程,找出问题所在。练习使用 Step Into 进入递归,观察栈帧变化。


总结

本篇我们深入学习了Java面向对象编程的核心知识:

知识点 核心要点
组织代码、命名空间、import 规则
访问修饰符 private → 默认 → protected → public,控制可见性
封装 属性私有、方法公开、隐藏实现细节
继承 is-a 关系、代码复用、构造器调用链
super 调用父类成员,必须放在构造器第一行
重写 子类重新实现父类方法,遵循规则
多态 父类引用指向子类对象、动态绑定、instanceof
Object类 所有类的根,equals、hashCode、toString 需成对重写
断点调试 观察程序执行过程,定位问题

面向对象编程不仅仅是一种语法,更是一种思维方式。掌握好这些基础知识,你就能用Java构建出结构清晰、易于维护的软件系统。 下一篇文章我们将学习抽象类、接口和内部类,继续深入面向对象的世界!

相关推荐
咚为2 小时前
Rust 跨平台编译实战:从手动配置到 Cross 容器化
开发语言·后端·rust
秦艽2 小时前
openclaw使用Claude Code 实现 10 倍效率提升&Token 消耗减少了 50%
后端
L0CK2 小时前
实战篇 10. 好友关注 - 实现 Feed 流滚动分页查询学习文档
后端
用户272017999752 小时前
Skill和MCP到底有什么区别?它们越多,效率就越高吗?
后端
PFinal社区_南丞2 小时前
将 Golang 接口的 JSON 响应改为 MessagePack,性能提升实战记录
后端·go
Soofjan3 小时前
Go 关键字:select / defer / panic & recover / make & new
后端
野犬寒鸦3 小时前
从零起步学习计算机操作系统:进程篇(基础知识夯实)
java·服务器·后端·学习·面试
开心就好20253 小时前
移动应用上架到应用商店的完整指南:原理与详细步骤
后端·ios
Moment4 小时前
从爆红到被嫌弃,MCP 为什么开始失宠了
前端·后端·面试