6.面向对象2
讲师:库瑞
博客园csdn博主:小蜗牛zww
抖音搜:库瑞爱音乐(程序员导师),B站搜:高级程序员歌手-库瑞,大学生想要报名系统学习就业课程,请在这三个平台任意一个私信报名
一.封装
封装(Encapsulation)的本质是将对象的属性(数据)和操作属性的方法(行为)捆绑在一起,并通过访问控制限制外部对内部数据的直接访问。简单来说,就是“把该藏的藏起来,只暴露必要的接口”。–>通过限制外部对内部封装的数据直接访问意思比如一个类的属性name定义为private String name,由于private 修饰的属性,外界不能直接拿到这个类的name属性值,是而是通过公共的方法get()进行访问,防止外界修改该类的属性或方法结果
类比现实世界:一台手机的内部电路、芯片等复杂结构被外壳包裹,用户无需关心内部如何实现通话或上网,只需通过屏幕、按键等“接口”操作即可。封装的目的正是如此——隐藏实现细节,仅对外提供安全、简洁的操作入口。
为了保证数据的安全性和完整性,将不必要暴露的外界的属性和方法私有化(使用private修饰)
但是为了让外界可以在一定情况下获得或修改相关的属性的值,需要提供一些公共的get和set方法,让外界可以在一定情况下对私有属性和行为进行访问
(1)访问修饰符

private :仅当前类可见(最严格)。
default(默认,无修饰):同包可见。
protected:同包+不同包的子类可见。
pubilc:全局可见(最宽松)。
类的属性应优先使用private修饰,避免外部直接修改;方法根据需求选择修饰符(如工具方法可用public,内部辅助方法用private)。
(2)getter/setter方法
当属性被private修饰后,外部无法直接读写,需通过公共的getter(读)和setter(写)方法间接操作。这两个方法不仅能控制访问,还能在方法中添加校验逻辑,确保数据合法性。
(3)代码实例
以学生类(Student)为例,我们需要保证学生的年龄(age)不能为负数,姓名(name)不能为空。通过封装可以实现这一目标:
package packaging;
public class Student {
// 私有属性:隐藏细节,外部无法直接访问
private String name;
private int age;
// 无参构造方法(按需添加)
public Student() {
}
// 全参构造方法(按需添加)
public Student(String name, int age) {
this.name = name;
setAge(age); // 复用setter的校验逻辑,避免重复代码
}
// getter:获取name(只读)
public String getName() {
return name;
}
// setter:设置name(带非空校验)
public void setName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("姓名不能为空");
}
this.name = name.trim(); // 去除首尾空格
}
// getter:获取age(只读)
public int getAge() {
return age;
}
// setter:设置age(带范围校验)
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄必须在0~150之间");
}
this.age = age;
}
}
测试类:
package packaging;
public class Main {
public static void main(String[] args) {
Student student = new Student();
student.setName(" 张三 "); // 自动trim为"张三"
student.setAge(20);
System.out.println(student.getName()); // 输出:张三
System.out.println(student.getAge()); // 输出:20
// 尝试设置非法值
student.setAge(-5); // 抛出IllegalArgumentException:年龄必须在0~150之间
}
}
(4)封装的优势
数据安全 :防止外部随意修改属性(如年龄设为-10),通过校验逻辑保证数据合法性;
降低耦合:外部仅需关注getter/setter的功能,无需了解内部存储结构(如name可能实际存在数组中,但外部无需知道);
便于维护:若属性存储方式变更(如从int age改为String birthDate),只需修改getter/setter,不影响外部调用代码。
二.继承
1.概念:
继承是指子类可以复用父类的非私有属性和方法,并可在此基础上扩展新功能或重写父类方法
即:将具有相同属性和行为的多个类的相同的属性和行为抽取出来形成一个新的类作为父类,
这些具有相同属性和行为的类就不再需要去定义这些相同的属性和行为,只需要继承这个新类就会获得这些相同的属性和行为
这个新类称之为父类或者超类,这多个类称之为子类
2.实现继承:
Java通过extends关键字实现单继承(一个子类只能有一个直接父类,但可多层继承,如“学生→人→生物”),语法如下:

继承的语法是怎样的?
[修饰符] class 子类名 extends 父类名{
类体//类体包括,属性,行为,代码块,构造器,内部类等
}
编写代码时,是先写父类,后写子类–》先把父类创建出来,再创建子类,代码里面写继承父类
注意:
1,子类不能继承父类的私有属性和行为
2,子类不能继承父类的构造器
3,子类可以扩展自己的特有的属性和行为
4,不要因为想要获得某种功能而去继承一个类,继承一定要有从属关系 即 狗是动物的一种,哈巴狗是狗的一种
5,java中Object是所有类的直接或间接父类 –》Object类是顶级类
6,java中子类只能有一个直接父类,称之为单继承
(1)父类的“私有成员”能否被子类继承?
不能。父类中使用private修饰的属性或方法,子类无法直接访问(编译报错)。但需注意:子类可通过父类提供的public/protected的getter/setter间接访问父类的私有属性。
(2)构造方法:子类必须调用父类构造
父子父子,先有父再有子,即:子类对象构造时,需要先调用基类构造方法,然后执行子类的构造方法。
子类的构造方法中,第一行默认隐式调用父类的无参构造(super ());若父类没有无参构造,子类必须显式调用父类的有参构造(super(参数)),否则编译报错。
代码示例:
package Inheritance;
class Person {
private String name;
// 父类只有带参构造,无默认无参构造
public Person(String name) {
this.name = name;
}
// 为了能获取name,添加getter方法
public String getName() {
return this.name;
}
}
class Student extends Person {
private int id;
public Student(String name, int id) {
super(name); // 必须显式调用父类有参构造,否则编译报错
this.id = id;
}
// 为了能获取id,添加getter方法
public int getId() {
return this.id;
}
}
public class Test {
public static void main(String[] args) {
// 创建Student对象
Student student = new Student("张三", 2025001);
// 测试获取属性
System.out.println("学生姓名:" + student.getName());
System.out.println("学生学号:" + student.getId());
}
}
(3)方法的重写
概念1:
当子类从父类继承过来的方法,不足以满足子类的需求时,子类可以重写该方法
子类在重写方法时,不能改变其方法名,参数列表,返回值的数据类型,访问权限修饰符的权限必须大于或等于父类方法的权限
概念2:
子类可对父类的非私有方法进行重写,即子类方法与父类方法名、参数列表、返回值类型(协变返回类型除外)完全一致,且访问权限不能更严格(如父类方法是public,子类不能是protected或private)。
重写的标志:可用@Override注解标记(编译器 会检查是否符合重写规则,避免拼写错误)。–》推荐加上@Override注解,让编译器进行检查
示例:Animal父类有eat()方法,Cat子类重写为自己的实现:
package Inheritance;
// 父类:Animal
class Animal {
public void eat() {
System.out.println("动物吃东西");
}
// 补充一个带参数的方法,演示更贴近实际的重写场景
public void sleep(int hour) {
System.out.println("动物每天睡" + hour + "小时");
}
}
// 子类:Cat,继承Animal并重写方法
class Cat extends Animal {
// 重写无参的eat()方法
@Override // 显式标记重写,编译器会校验规则(如方法名/参数是否匹配)
public void eat() {
System.out.println("猫吃鱼"); // 子类特有逻辑
}
// 重写带参数的sleep()方法
@Override
public void sleep(int hour) {
System.out.println("猫每天睡" + hour + "小时,还会打呼"); // 子类扩展逻辑
}
}
// 测试类:验证方法重写效果
public class Test1 {
public static void main(String[] args) {
// 父类对象调用父类方法
Animal animal = new Animal();
animal.eat(); // 输出:动物吃东西
animal.sleep(8); // 输出:动物每天睡8小时
System.out.println("——————-");
// 子类对象调用重写后的方法
Cat cat = new Cat();
cat.eat(); // 输出:猫吃鱼(重写后的逻辑)
cat.sleep(8); // 输出:猫每天睡8小时,还会打呼(重写后的逻辑)
// 多态场景:父类引用指向子类对象,依然调用重写后的方法
Animal animalCat = new Cat();
animalCat.eat(); // 输出:猫吃鱼
}
}
(4)子类对象的创建流程
在创建子类对象时,会先创建其父类对象,如果父类还有父类,那么会先创建其体系中最顶层的对象,如果父类对象创建失败,那么子类对象也会创建失败
如何在创建子类对象时,先创建父类对象?
在子类构造器中,如果没有显式的调用父类的构造器,那么java会默认在子类构造器的第一行隐式的调用父类的无参构造器,如果父类没有提供无参构造器,
那么就需要显式的调用父类的其他构造器
super用于在子类中指代父类,主要场景:
super():调用父类构造方法(必须位于子类构造方法第一行)。
super.属性:访问父类非私有属性(当子类与父类有同名属性时,区分父子类属性)。
super.方法():调用父类被重写的方法(避免子类方法递归调用自身)。
示例:子类与父类有同名属性type ,通过super区分如下代码:
package Inheritance;
// 父类:Animal
class Animal {
int age; // 父类成员变量
String type; // 父类成员变量
// 父类方法A(无参)
public void eat() {
System.out.println("Animal类:动物进食");
}
// 父类方法B(无参)
public void sleep() {
System.out.println("Animal类:动物睡觉");
}
}
// 子类:Dog(继承Animal)
class Dog extends Animal {
int age; // 与父类同名同类型变量
char type; // 与父类同名不同类型变量
// 与父类eat()构成重载(参数列表不同)
public void eat(int foodAmount) {
System.out.println("Dog类:小狗吃" + foodAmount + "份食物");
}
// 与父类sleep()构成重写(原型一致)
public void sleep() {
System.out.println("Dog类:小狗趴着睡");
}
// 测试同名变量、方法重载/重写的访问
public void test() {
// 访问子类同名变量(默认this.)
age = 2;
type = 'A';
System.out.println("子类age:" + age);
System.out.println("子类type:" + type);
// 访问父类同名变量(用super.)
super.age = 3;
super.type = "犬科";
System.out.println("父类age:" + super.age);
System.out.println("父类type:" + super.type);
// 访问重载方法:通过参数区分
eat(); // 无参 → 父类eat()
eat(5); // 有参 → 子类eat(int)
// 访问重写方法:通过super区分
sleep(); // 直接调用 → 子类sleep()
super.sleep(); // super.调用 → 父类sleep()
}
}
public class Test2{
public static void main(String[]args){
new Dog().test();
//子类age:2
//子类type:A
//父类age:3
//父类type:犬科
//Animal类:动物进食
//Dog类:小狗吃5份食物
//Dog类:小狗趴着睡
//Animal类:动物睡觉
}
}
–总结:
1,在子类构造器中显式的调用父类的构造器
super(参数值1,参数值2,….);
注意:super(参数值1,参数值2,….); 必须写在子类构造器方法体的第一行
提问:在显式调用了父类的构造器时,是否还能调用本类的其他构造器?
不能,因为super(参数值1,参数值2,….); 和 this(参数值1,参数值2,….);都必须位于方法体的第一行
2,当子类变量和父类成员变量发生重名时,可以使用super.变量名来表示父类成员变量
3,在方法被子类重写后仍然需要使用父类的同名的方法时,可以使用super.方法名(参数值1,参数值2,….);来调用父类同名的方法
练习:定义一个父类Father,有属性共有 姓名,性别,汽车
有功能 赚钱 在该方法中打印输出 个体户
只有一个有参构造器,用于初始化,姓名,性别,汽车
定义一个子类Son,有扩展属性汽车
重写赚钱的方法 在该方法中要求保留父类的赚钱的功能,并在此基础上扩展 java工程师的功能
有一个测试方法,要求分别打印出 父类对象的汽车品牌,和 当前类的对象的汽车品牌
在测试类中去创建子类对象,并使用debug功能查看子类对象的创建流程 ,调用子类对象的赚钱的方法和测试的方法
(6)final与继承的限制
1.final类:不能被继承(如String类被final修饰,无法创建子类)。
final public class Fruit { // final类
String taste = "有味道";
}
// 尝试继承final类
public class Apple extends Fruit {
// Error: 无法从最终类Fruit进行继承
}
2.final方法:不能被重写(如父类不希望子类修改某方法逻辑,可标记为final)。
public class Plant {
// final方法:子类不能重写
final public void grow() {
System.out.println("植物生长");
}
}
public class Flower extends Plant {
// 尝试重写final方法
@Override
public void grow() {
// Error: 方法Does not override method from its superclass
System.out.println("花朵绽放");
}
}
3.final属性:常量(值不可变,需在声明时或构造方法中初始化)。
final double num = 3.1415
num = 3.14 //final变量不能重新赋值
三.多态
1. 什么是多态
多态是指同一行为(方法调用)在不同对象上具有不同表现形式。简单说:“父类引用指向子类对象,调用方法时执行子类的实现”。
多态的核心是“动态绑定”(运行时确定调用哪个方法)。
简单概括:
一种事物多种形态称之为多态
2.java中多态
1.方法的多态
方法的重载
方法的重写
2.对象的多态
向上转型
向下转型
向上转型(类似于基本数据类型中的自动转型)
父类变量可以直接保存子类对象的引用
//1,创建子类对象
Dog dog = new Dog();
//2,使用父类变量来保存子类对象的引用
Animal animal = dog;
注意事项: 向上转型之后,子类扩展的功能和属性对外不公开,但是是存在的,只能访问从父类继承过来的属性和方法
如果方法被重写,那么访问的实际就是子类重写后的方法
向下转型(类似于基本数据类型中的强制转型)
注意:在向下转型之前,一定要先向上转型,否则就会出现类型转换异常 ClassCastException
//1,创建子类对象
Dog dog = new Dog();
//2,向上转型
Animal animal = dog;
//3,向下转型
Dog dog01 = (Dog)animal;
多态的好处:可以降低类与子类的耦合性
(1)向上转型:父类引用指向子类对象
语法:

此时,变量只能调用父类中定义的属性和方法,子类特有的方法无法直接调用,除非强制向下转型。
package Polymorphism;
class Animal {
// 父类方法
public void eat() {
System.out.println("动物吃东西");
}
}
// 子类:Cat(继承Animal)
class Cat extends Animal {
// 重写父类eat方法
@Override
public void eat() {
System.out.println("猫吃鱼");
}
// 子类特有方法(父类没有)
public void catchMouse() {
System.out.println("猫抓老鼠");
}
}
public class Test1 {
public static void main(String[] args) {
// 1. 向上转型:父类引用指向子类对象
Animal animal = new Cat();
// 2. 调用重写的方法:执行子类的实现(多态特性)
animal.eat(); // 输出:猫吃鱼
// 3. 无法直接调用子类特有方法(父类引用“看不到”子类新增方法)
//animal.catchMouse(); //Animal类没有该方法,会报错
}
}
(2)向下转型:子类引用指向父类对象(需强制转换)
向上转型后,若需调用子类特有的方法(父类未定义),需将父类引用强制转换为子类类型,即向下转型。
package Polymorphism;
class Animal {
// 父类
public void eat() {
System.out.println("动物吃东西");
}
}
class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫吃鱼");
}
// 子类特有方法
public void catchMouse() {
System.out.println("猫抓老鼠");
}
}
public class Test2 {
public static void main(String[] args) {
// 1. 向上转型
Animal animal = new Cat();
// 2. 直接向下转型(无instanceof判断)
Cat cat = (Cat) animal;
// 3. 调用子类特有方法
cat.catchMouse();
cat.eat();
}
}
//猫抓老鼠
//猫吃鱼
(3)方法调用的动态绑定
编译时,编译器仅检查父类是否有该方法(若不存在则编译报错),运行时,JVM 会根据对象的实际类型(而非引用类型)调用对应的方法——这就是动态绑定。
package Polymorphism;
class Animal {
public void eat() {
System.out.println("动物吃东西");
}
}
class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗吃骨头");
}
}
public class Test3 {
public static void main(String[] args) {
Animal animal1 = new Cat(); // 实际类型是Cat
Animal animal2 = new Dog(); // 实际类型是Dog
animal1.eat(); // 运行时调用Cat.eat(),输出猫吃鱼
animal2.eat(); // 运行时调用Dog.eat(),输出狗吃骨头
}
}
以下作为了解:
当然还有别的运用方式。例如:(1)方法参数多态:提高方法通用性。(2)数组/集合 多态:统一管理不同子类对象。(3)接口多态:解耦“定义与实现”。
四、三大特征的作用
封装是基础:通过隐藏细节,让类和对象的状态更安全,为继承和复用提供可靠的“模块”。
继承是手段:通过复用父类代码,减少冗余,为多态提供“父子类/接口-实现类”的关系基础。
多态是目标:通过动态绑定,让继承的复用更具灵活性,最终实现代码的“高内聚、低耦合”。
五、总结
封装:通过private+getter/setter隐藏细节,保护数据。
继承:通过extends复用父类逻辑,支持方法重写(@Override)和super访问。
多态:通过向上转型+动态绑定,实现“同一行为不同表现”,提升代码灵活性和扩展性。





