Day9|类、对象与封装全解析
在学习 Java 的过程中,你是否曾被「类」「对象」「Class 对象」「接口」「抽象类」「Object」这些术语搞得晕头转向?
是否又在“面向对象”和“面向过程”之间模糊不清,搞不明白它们到底是语言特性还是编程思维?
今天我们通过这篇文章,用最通俗的语言、最贴近现实的类比、最扎实的代码案例,一起来梳理 Java 面向对象世界的核心概念。
从类与对象的本质,到接口与抽象类的选择,再到封装、继承、多态的实践,一步步了解Java的OOP。
一、搞清楚各种术语
类(Class)
类是对一类现实事物的抽象描述。
定义了数据结构(字段)和行为方式(方法)。
Java中类是用户自定义的引用类型,属于"模版"性质,不会单独占用运行内存。
public class Person {
String name;
int age;
void speak() {
System.out.println("我叫" + name);
System.out.println("我" + age + "岁");
}
void walk() {
System.out.println("我能走路");
}
}
Person就是自定义的一个类,是对人的抽象。人都有什么?有名字,有年龄(字段);人能干什么,能走路,能说话。
人这个现实的物种,你把他们的共性抽取出来,就形成了类。
对象
对象就是类的实例,通过 new 操作符创建。
堆内存中分配空间,拥有自己的字段副本。
Person p = new Person(); // 创建对象
p.name = "懒惰蜗牛";
p.speak();
p指向的就是一个具体的人,这个具体的人叫懒惰蜗牛,他做自我介绍的时候就会说"我叫懒惰蜗牛"。
TIP
类是图纸,对象就是根据这个图纸造出来的房子 类是汽车设计稿,对象就是你开的那辆车
Class对象
Java里所有类在运行时都是一个java.lang.Class实例。
是不是有点晕?捋一捋。
类(Class)是语法结构,就是你自己定义的那个文件(类文件)。
类编译之后是.class文件(字节码文件),运行的时候会被JVM加载到内存,这时候就产生了一个Class对象(java.lang.Class)。
然后如果碰到了new操作符,就会产生具体的对象。
Class<?> clazz = Person.class;
System.out.println(clazz.getName()); // 输出 Person
这就是拿到Class对象的其中一种方式。
Object类
这是一个具体的类,java.lang.Object
它是所有类的祖先,所有类都默认继承这个类。
Day9Demo是一个自定义的类,没有定义任何东西,也没有显示的继承。
当你创建了一个实例之后,你会发现,这个实例天然具有很多行为(方法),比如:clone、equals、hashCode等。
这些方法其实就是从Object继承而来,具体的定义都在Object中。
任何类都可以看成是这样:
public class Day9Demo extends Object{
}
只是不需要我们显示进行继承。
接口
接口是什么呢?接口是行为的抽象。
在JDK8之前,接口里只能定义方法签名,JDK8之后可以有default方法了
接口只定义应该做什么,然后被类通过implements关键字实现。
package com.lazy.snail.day9;
/**
* @ClassName Flyable
* @Description TODO
* @Author lazysnail
* @Date 2025/5/13 15:26
* @Version 1.0
*/
public interface Flyable {
void fly();
}
Flyable就是自定义的接口,跟类有什么区别,没错,就是类名前的关键字从class变成了interface。
然后定义的方法没有具体的实现。(大括号都没有)。
然后来个鸟类:
package com.lazy.snail.day9;
/**
* @ClassName Bird
* @Description TODO
* @Author lazysnail
* @Date 2025/5/13 15:26
* @Version 1.0
*/
public class Bird implements Flyable {
@Override
public void fly() {
System.out.println("有只笨鸟正在扑哧翅膀飞");
}
}
通过implements实现接口Flyable。然后在fly方法里写具体的逻辑。
接口里的方法其实可以看成是行为规范(飞),然后具体怎么飞就由实现类去确定。
抽象类
抽象类可以理解成共性模版和部分实现。
这种类里面可以有抽象方法(没有方法体的),也可以有普通方法(有实现逻辑)。
这种类不能直接实例化,只能被子类继承。
强调"是一种什么(is-a)"关系。
来一个动物的抽象类:
package com.lazy.snail.day9;
/**
* @ClassName Animal
* @Description TODO
* @Author lazysnail
* @Date 2025/5/13 15:37
* @Version 1.0
*/
public abstract class Animal {
abstract void speak();
void eat() {
System.out.println("干饭");
}
}
抽象类跟普通类相比,多了一个abstract修饰符。
abstract修饰的方法就是抽象方法,没有具体的实现,由子类实现。
eat方法就是普通的方法。
然后来一个狗类(狗是动物的一种):
package com.lazy.snail.day9;
/**
* @ClassName Dog
* @Description TODO
* @Author lazysnail
* @Date 2025/5/13 15:38
* @Version 1.0
*/
public class Dog extends Animal {
@Override
void speak() {
System.out.println("汪汪汪");
}
}
Dog通过extends关键字继承了Animal这个抽象类。
Dog就必须实现speak方法。
抽象类是不是跟接口有点类似,来看看他们的对比:
特点 | 接口 Interface | 抽象类 Abstract Class |
---|---|---|
方法实现 | 默认无实现(可以有default方法) | 可部分实现 |
成员类型 | 常量(public static final) | 字段+方法都可以 |
多继承 | 支持多接口实现 | 只能单继承 |
语义 | 能力(行为) | 类型(共性) |
二、面向对象
三大特性
封装
可以把Java的封装特性想象成智能家电的使用方式:
就像你使用微波炉的时候,不需要知道内部的磁控管怎么产生微波,只要通过面板按钮(对外接口)设定时间和火力就能加热食物。
Java的封装把数据和相关操作打包成一个"黑箱子",外部只能通过指定按钮(方法)来操作。
每个封装好的类就像乐高积木:
- 积木内部结构被外壳包裹(数据隐藏)
- 积木表面有标准凸起和凹槽(对外接口)
- 拼装的时候只需要对准接口,不用破坏积木内部
有什么好处?
你想升级,只要改进某个积木内部结构,不影响其他积木的使用。
你可以单独测试某个积木的承重能力(性能极限)。
设计好的积木可以在不同的模型里面使用,复用性好。
就算整个模型没完工,单个积木也能正常使用,不会有那么高的系统风险。
前文提到的Person类再来一次,随便加深点印象:
package com.lazy.snail.day9;
/**
* @ClassName Person
* @Description TODO
* @Author lazysnail
* @Date 2025/5/13 15:58
* @Version 1.0
*/
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
void speak() {
System.out.println("我叫" + name);
System.out.println("我" + age + "岁");
}
void walk() {
System.out.println("我能走路");
}
}
Person类里面封装了name、age等属性,外界只能通过get()方法获取一个Person对象的name属性,但是没办法拿到age属性。但是age属性又可以提供给speak方法使用。
注意到 name 属性使用 String 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 name 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。
继承
继承实现了 is -a 关系,例如 Dog 和 Animal 就是一种 is -a 关系,因此 Dog 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法。
继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。
Dog 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Dog 对象。父类引用指向子类对象称为向上转型 。
Animal animal = new Dog();
多态
多态的类型
编译时多态(方法重载):就像同一个名字的多个工具,根据不同的情况自动选择合适的工具。
家里的多功能刀,根据你按的按钮不同,可以是小刀、开瓶器或剪刀。在代码中,比如一个计算器类有多个add方法,分别处理整数相加和浮点数相加。
运行时多态(继承+覆盖):就像同一个遥控器可以控制不同品牌的电视,按下“开机”键时,不同电视会有不同的反应。
用“乐器遥控器”(父类引用)控制不同的乐器(子类对象),按下“演奏”键时,如果是钢琴就弹琴,如果是鼓就打鼓。
运行时多态的三要素
- 继承:子类继承父类,比如唢呐和鼓都是乐器的子类。
- 覆盖:子类重写父类的方法,比如两种乐器都重写了play()方法,实现自己的演奏方式。
- 向上转型:用父类的“遥控器”操作子类的对象,比如:
Instrument myInstrument = new Sona(); // 父类引用指向子类对象
文字看着迷糊,就看代码吧:
package com.lazy.snail.day9;
/**
* @ClassName MusicShow
* @Description TODO
* @Author lazysnail
* @Date 2025/5/13 16:11
* @Version 1.0
*/
public class MusicShow {
public static void main(String[] args) {
Instrument instrument1 = new Suona();
Instrument instrument2 = new Drum();
instrument1.play();
instrument2.play();
}
}
// 乐器
class Instrument {
public void play() {
System.out.println("随便弹点声音...");
}
}
// 唢呐
class Suona extends Instrument {
@Override
public void play() {
System.out.println("吹唢呐,滴滴滴滴");
}
}
// 鼓
class Drum extends Instrument {
@Override
public void play() {
System.out.println("敲个鼓,dungdungdung");
}
}
TIP
这里把几个类都写在了一个文件里,是为了看着方便,实际开发的时候一般不会这么写。 这不是什么内部类,只是一个源文件包含了多个类而已!
上面的代码中,Instrument(乐器)有两个子类:Suona(唢呐)和Drum(鼓),它们都覆盖了父类的 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Suona和 Drum对象。
在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。
三、面向对象 vs 面向过程
你可能听过C语言是面向过程的语言,然后Java是面向对象的语言(OOP)。
其实是面向对象还是面向过程,不是语言本身决定的,而是开发者设计思想和具体业务场景主导的。
说人话:你可以把C语言写成面向对象的,你也可以把Java写成面向过程的。
编程范式不是语言的标签,而是一种解题方式。
虽然我们常说“C 是面向过程的”“Java 是面向对象的”,但这其实是基于主流用法与历史发展习惯的简化归类。
C 语言虽然没有内建类机制,但通过函数指针、结构体模拟对象,也能实现面向对象的封装、继承与多态。
Java 尽管鼓励 OOP,也常在工具类、Lambda 表达式、Stream API 中体现出过程式/函数式风格。
面向过程是以“解决问题的步骤”为核心,强调“从头到尾如何做”。它关注的是过程逻辑的执行顺序,将数据与操作分离,代码以函数为单位组织。
面向对象是以现实世界建模为核心,关注对象之间的协作。强调的是数据和行为的封装、继承与重用,将数据与操作统一为成对象。
总结
- 类是对现实建模的基本单元,对象是其具体表现
- Java 是典型的面向对象语言,但依然可承载过程式逻辑
- 接口强调能力,抽象类强调类型
- Object 是所有类的祖宗,Class 是反射的载体
- 面向过程适合短期任务、嵌入式;面向对象适合大型系统建模
- 掌握这些术语与本质区分,是写出可读性强、结构合理 Java 程序的前提