✨博客主页: https://blog.csdn.net/m0_63815035?type=blog
💗《博客内容》:大数据、AI开发、Java、测试开发、Python、Android、Go、Node、Android前端小程序等相关领域知识
📢博客专栏:https://blog.csdn.net/m0_63815035/category_11954877.html
📢欢迎点赞 👍 收藏 ⭐留言 📝
📢本文为学习笔记资料,如有侵权,请联系我删除,疏漏之处还请指正🙉
📢大厦之成,非一木之材也;大海之阔,非一流之归也✨
目录
- 一、语言发展主线:为什么需要对象?
- 二、类与对象
- 1. 类的定义
- 2. 对象的创建和使用
- 3. 构造器(Constructor)
- 4. this 关键字
- 5. 内存分析(简略)
- 三、封装
- 1. 为什么要封装?
- 2. 访问控制符
- 3. getter / setter
- 4. JavaBean 规范
- 四、继承
- 1. 继承语法和特点
- 2. 方法重写(Override)
- 3. super 关键字
- 4. 构造器调用顺序
- 5. Object 类常用方法
- 五、多态
- 1. 多态的定义
- 2. 注意点
- 3. 向下转型
- 4. 动态绑定机制
- 六、抽象类与接口
- 1. 抽象类
- 2. 接口
- 3. 接口 vs 抽象类(Java 8 以前)
- 4. Java 8 之后的接口变化
- 5. 面向接口编程
- 七、内部类
- 1. 成员内部类
- 2. 静态内部类
- 3. 局部内部类
- 4. 匿名内部类
- 八、类和类之间的关系
- 九、设计原则(SOLID 简版)
- 十、综合练习
本篇是面向对象的完整讲解,从类和对象开始,到封装、继承、多态、接口、内部类,最后涉及设计原则。每个概念都有例子和常见注意事项。
一、语言发展主线:为什么需要对象?
计算机语言发展本质上是为了更自然地表达人的思维,同时管理越来越复杂的数据和操作。
- 只有基本变量→ 能存单个数字、字符,但数据一多就乱。
- 数组→ 把同一类型的数据连续存放,可以批量处理。但数组要求所有元素类型相同,且只能存数据,没有操作。
- 结构体(struct)→ 允许把不同类型的数据(如人的姓名、年龄、身高)组合成一个整体。但数据和方法还是分开的。
- 类和对象→ 把数据(属性)和操作(方法)绑在一起。一个对象就像现实中的一个实体,有自己的状态和行为。
面向过程:数据和方法分离,程序 = 数据结构 + 算法,以函数为中心。
面向对象:数据和方法合一,程序 = 对象 + 对象之间的消息传递,以类/对象为中心。
对于复杂系统(比如一个电商平台),宏观上需要用面向对象分析业务,拆成用户、商品、订单等类;但每个方法内部的具体实现(如计算价格、校验库存)仍然是面向过程的。
二、类与对象
1. 类的定义
类是模板,描述了一类事物共有的属性和方法。
classStudent{// 属性(成员变量,实例变量)intid;Stringname;intage;// 方法voidstudy(){System.out.println(name+"正在学习");}}- 属性可以设默认值,不设则系统给默认值:
0、0.0、false、\u0000、null。 - 方法定义和之前的函数一样,只不过它属于类。
2. 对象的创建和使用
Students1=newStudent();s1.id=1001;s1.name="张三";s1.study();new Student()在堆内存中开辟空间,并返回地址给栈中的变量s1。- 每个对象有自己的属性副本,互相独立。
- 方法代码只存一份,所有对象共享。
3. 构造器(Constructor)
构造器是创建对象时自动调用的特殊方法,用来初始化对象。
特点:
- 名字和类名完全相同
- 没有返回值(也不能写
void) - 不能被
static、final、abstract修饰 - 可以重载
classStudent{intid;Stringname;// 无参构造Student(){System.out.println("调用无参构造");}// 有参构造Student(intid,Stringname){this.id=id;// this 区分成员变量和局部变量this.name=name;}}重要:
- 如果你不写任何构造器,编译器会自动生成一个无参构造(空实现)。
- 只要你写了任何带参构造,编译器就不再自动生成无参构造。为了避免子类继承时出错,建议手动把无参构造也写上。
构造器之间可以用this(...)互相调用,但必须放在第一行。
Student(){this(0,"无名");// 调用有参构造}4. this 关键字
- 在构造器中,表示正在初始化的对象。
- 在实例方法中,表示调用该方法的对象。
- 不能出现在静态方法中(因为静态方法不属于任何对象)。
常用场景:
- 区分成员变量和参数。
- 调用另一个构造器(必须第一行)。
- 返回当前对象(链式调用)。
classCalculator{Calculatoradd(intx){// ... 运算returnthis;// 返回自身}}Calculatorc=newCalculator().add(5).add(3);5. 内存分析(简略)
- 栈:存放局部变量(包括基本类型和对象引用),方法调用时会压栈。线程私有,速度较快。
- 堆:存放所有
new出来的对象(包括数组)。线程共享,速度稍慢。 - 方法区:存放类信息(字节码)、静态变量、字符串常量等。线程共享。
Students=newStudent();- 加载
Student.class到方法区。 - 栈中创建
s变量(引用)。 - 堆中开辟一块空间存 Student 对象,成员变量取默认值。
- 执行构造器代码(如果存在)。
- 将堆中对象的地址赋给
s。
三、封装
1. 为什么要封装?
- 防止外部代码随意修改内部状态(例如年龄不能为负数)。
- 隐藏实现细节,外部只需知道调用什么方法,不必关心内部如何实现。
- 提高代码的可维护性:修改内部逻辑不影响调用方。
2. 访问控制符
| 修饰符 | 同类 | 同包 | 子类(不同包) | 任何地方 |
|---|---|---|---|---|
private | √ | |||
| (default) | √ | √ | ||
protected | √ | √ | √ | |
public | √ | √ | √ | √ |
- 属性通常用
private隐藏。 - 方法一般用
public向外提供服务。 - 仅本类内部调用的辅助方法可以用
private。
3. getter / setter
publicclassPerson{privateStringname;privateintage;privatebooleanmarried;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}publicintgetAge(){returnage;}publicvoidsetAge(intage){if(age>=0&&age<=150){this.age=age;}else{thrownewIllegalArgumentException("年龄非法");}}// 注意:boolean 的 getter 习惯用 isXxxpublicbooleanisMarried(){returnmarried;}}4. JavaBean 规范
一个标准的 JavaBean 需要:
- 类公开,有无参构造
- 属性私有
- 提供 getter/setter
- 实现
Serializable(可序列化,后面再学)
封装不是简单的私有化,而是提供合理的访问接口。
四、继承
1. 继承语法和特点
classAnimal{Stringname;voideat(){System.out.println("吃东西");}}classDogextendsAnimal{voidbark(){System.out.println("汪汪");}}- Java 只支持单继承(一个类只能有一个直接父类),但可以实现多接口。
- 所有类(除了
Object)都直接或间接继承Object。 - 子类拥有父类所有的成员,但
private成员不能直接访问(可以通过public或protected方法间接访问)。
2. 方法重写(Override)
子类重新实现父类的方法。
规则:
- 方法名、参数列表必须完全相同。
- 返回值类型:子类返回值类型可以是父类返回值类型的子类(协变返回类型,了解即可)。
- 访问权限不能更严格(可以扩大,不能缩小)。
private方法不能被重写,static方法不能被重写(但是可以重新声明)。- 可以用
@Override注解,让编译器帮你检查。
classAnimal{protectedvoidshout(){System.out.println("动物叫");}}classCatextendsAnimal{@Overridepublicvoidshout(){// protected -> public 允许System.out.println("喵喵");}}3. super 关键字
- 访问父类的属性(当子类隐藏了父类同名属性时):
super.name - 调用父类的方法:
super.shout() - 调用父类的构造器:
super(...),必须出现在子类构造器的第一行。
4. 构造器调用顺序
创建子类对象时,会先创建父类部分(递归直到Object),然后再执行子类构造器体的代码。
- 子类构造器默认第一行有
super()(调用父类无参构造)。 - 如果父类没有无参构造,子类构造器必须显式调用父类的有参构造。
classFather{Father(intx){System.out.println("Father "+x);}}classSonextendsFather{Son(){super(100);// 必须写,否则编译错误System.out.println("Son");}}5. Object 类常用方法
toString():返回对象的字符串表示,通常重写。equals(Object obj):判断对象是否相等,默认比较地址,通常需要重写。hashCode():返回对象的哈希码,重写equals时必须重写hashCode。getClass():返回运行时类信息。
@OverridepublicStringtoString(){return"Person{name="+name+", age="+age+"}";}五、多态
1. 多态的定义
同一个类型的变量,调用同一个方法,实际执行的行为取决于运行时具体的对象类型。
必要条件:
- 继承
- 方法重写
- 父类引用指向子类对象
Animala=newDog();// 向上转型(自动)a.shout();// 调用的是 Dog 的 shout()2. 注意点
- 属性没有多态:访问属性看编译时类型,不是运行时类型。
Animala=newDog();System.out.println(a.age);// 还是 Animal 的 age- 静态方法没有多态:静态方法属于类,调用时看编译时类型。
Animala=newDog();a.staticMethod();// 调用的是 Animal 的静态方法3. 向下转型
当你想调用子类特有的方法时,需要把父类引用转回子类类型。
Animala=newDog();if(ainstanceofDog){// 安全检查,避免 ClassCastExceptionDogd=(Dog)a;d.bark();// Dog 独有的方法}instanceof关键字:判断对象是否是某个类(或其子类)的实例。
4. 动态绑定机制
- 编译时,编译器只检查引用变量的类型中是否有该方法。
- 运行时,JVM 会找到对象实际类型的方法表,执行真正的方法。
- 这就是多态的核心原理。
六、抽象类与接口
1. 抽象类
当某个方法无法在父类中给出合理实现时,可以声明为抽象方法,要求子类必须实现。
abstractclassShape{abstractdoublearea();// 抽象方法,无方法体voidshow(){System.out.println("这是一个形状");}}- 有抽象方法的类必须声明为
abstract。 - 抽象类不能实例化(不能
new)。 - 抽象类可以有构造器(供子类调用)、普通属性、普通方法。
- 子类必须实现所有抽象方法,除非子类也是抽象类。
2. 接口
接口是完全抽象的规范,强调“能做什么”。
interfaceFlyable{intMAX_HEIGHT=1000;// public static finalvoidfly();// public abstract}- 接口中的变量默认是
public static final,方法默认是public abstract。 - 接口不能有构造器,不能有实例属性(但可以有静态常量)。
- 一个类可以实现多个接口(多实现)。
- 实现类必须重写所有接口中的抽象方法,且方法必须是
public。
classBirdimplementsFlyable{@Overridepublicvoidfly(){System.out.println("鸟飞");}}3. 接口 vs 抽象类(Java 8 以前)
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 多继承 | 不支持 | 支持(类可多实现) |
| 实例属性 | 可以有 | 只能有静态常量 |
| 构造器 | 可以有 | 不能有 |
| 访问权限 | 可以任意 | 方法默认 public |
| 使用场景 | 表示“是什么”(is-a) | 表示“能做什么”(can-do) |
4. Java 8 之后的接口变化
- 默认方法:用
default修饰,可以有方法体,实现类可以继承或重写。 - 静态方法:用
static修饰,属于接口,通过接口名调用。
interfaceVehicle{defaultvoidrun(){System.out.println("交通工具在跑");}staticvoidhonk(){System.out.println("嘀嘀");}}这些变化允许接口在不破坏已有实现类的情况下增加方法。
5. 面向接口编程
优先使用接口类型声明变量,而不是具体类。这样更换实现不影响调用代码。
List<String>list=newArrayList<>();// 好// ArrayList<String> list = new ArrayList<>(); // 不好,耦合太强七、内部类
内部类定义在另一个类内部,可以更好地封装,也能访问外部类的所有成员(包括私有)。
1. 成员内部类
classOuter{privateintx=10;classInner{voidprint(){System.out.println(x);// 可以访问外部类私有成员}}}// 创建方法Outerout=newOuter();Outer.Innerin=out.newInner();- 成员内部类不能有静态成员(除了静态常量)。
- 内部类中可以使用
Outer.this访问外部类当前对象。
2. 静态内部类
classOuter{staticclassInner{}}// 创建Outer.Innerin=newOuter.Inner();- 不持有外部类对象的引用,只能访问外部类的静态成员。
- 可以像普通类一样有静态成员。
3. 局部内部类
定义在方法内部,作用域仅限该方法。
voidmethod(){classLocal{voidwork(){}}Locall=newLocal();}- 局部内部类可以访问外部类的成员,也可以访问方法中的局部变量,但局部变量必须是
final或“实际上 final”(JDK 8 后不需要显式写 final,但不能修改)。
4. 匿名内部类
最常用,一次性使用,无需命名。
Runnabler=newRunnable(){@Overridepublicvoidrun(){System.out.println("run");}};- 匿名内部类继承某个类或实现某个接口,不能同时做两件事。
- 不能定义构造器。
- 常用于事件监听、线程、回调等场景。
button.addActionListener(newActionListener(){publicvoidactionPerformed(ActionEvente){// 处理点击}});八、类和类之间的关系
| 关系 | 描述 | UML 表示 | 代码示例 |
|---|---|---|---|
| 依赖 | 一个类临时使用另一个类(方法参数) | 虚线箭头 | 方法参数、局部变量 |
| 关联 | 一个类长期持有另一个类的引用 | 实线箭头 | 成员变量 |
| 聚合 | 整体与部分,部分可独立存在 | 空心菱形+实线箭头 | 成员变量(弱) |
| 组合 | 整体与部分,同生共死 | 实心菱形+实线箭头 | 成员变量(强) |
| 继承 | is-a | 空心三角+实线(子类→父类) | extends |
| 实现 | 类实现接口 | 空心三角+虚线(类→接口) | implements |
- 聚合:汽车和轮胎,轮胎可以拆下来装在另一辆车上。
- 组合:人和心脏,人没了心脏也就没了意义。
九、设计原则(SOLID 简版)
- 单一职责:一个类只负责一项职责。
- 开闭原则:对扩展开放,对修改关闭。增加功能尽量新增代码,而不是改原有代码。
- 里氏替换:子类必须能替换父类并且程序行为正确。
- 接口隔离:接口应该小而专,不要做“胖接口”。
- 依赖倒置:依赖抽象(接口/抽象类),不依赖具体实现。
- 迪米特法则:一个对象应尽可能少地了解其他对象。
这些原则不是死的,但遵循它们能让代码更容易维护和扩展。
十、综合练习
- 定义一个
Point3D类:三个坐标x,y,z,提供构造器,计算到原点的距离平方,计算到另一个Point3D对象的距离平方。 - 定义一个
Circle类:包含圆心(Point)和半径,提供面积方法,以及判断一个Point是否在圆内的方法。 - 动物多态:
Animal抽象类,shout()抽象方法。Dog、Cat继承并重写。用Animal引用调用shout()。 - 接口练习:定义
USB接口,含work()方法。实现Mouse和Keyboard类,模拟将 USB 设备插入计算机。
学习面向对象时,多思考现实中的事物如何抽象成类,类之间有哪些关系。代码写多了,自然就会明白为什么需要封装、继承和多态。遇到不确定的就多翻 API 或写个小例子验证。
今天这篇文章就到这里了,大厦之成,非一木之材也;大海之阔,非一流之归也。感谢大家观看本文