十、面向对象
1. 面向对象概述
面向对象(Object-Oriented-Programming,简称 OOP) 是软件开发中的一类编程思想、开发范式。它将现实世界中的事物抽象为 “对象”,通过对象之间的交互来实现功能。其核心是以 “对象” 为中心,强调事物的属性和行为,并通过封装、继承、多态三大特性提高代码的可维护性、复用性和灵活性。
封装(Encapsulation)
- 定义:将对象的属性和行为隐藏在类内部,仅通过公共接口(如方法)对外暴露必要的功能,限制外部直接访问内部细节;
- 作用:保护数据安全性,避免外部随意修改,同时降低代码耦合度。
- 实现:通过访问修饰符(如
private、public)控制访问权限。
注:耦合度是指程序中,某一模块与另一模块之间信息或参数依赖的程度。
继承(Inheritance)
- 定义:子类可以继承父类的属性和方法,并可以新增自己的属性或重写父类的方法;
- 作用:代码复用,减少重复代码(如“大学生类”继承“学生类”,“高中生类”也可以继承 “学生类”);
- 实现:通过
extends关键字,在 Java 中一个子类只能继承一个父类(单继承)。
多态(Polymorphism)
- 定义:同一方法在不同对象上有不同的实现方式,即 “一个接口,多种实现”;
- 作用:提高代码灵活性和扩展性,允许用父类引用指向子类对象,简化调用逻辑;
- 实现:通过方法重写
Overrite(子类重写父类方法)和接口实现implements(不同类实现同一接口)实现。
除了面向对象,还有面向过程、指令式编程和函数式编程。在所有的编程范式中,我们接触最多的还是面向过程和面向对象两种。
| 维度 | 面向过程(Procedure-Oriented-Programming) | 面向对象(Object-Oriented-Programming) |
|---|---|---|
| 核心 | 以 “过程” 为中心,强调步骤和函数 | 以 “对象” 为中心,强调事物的属性和行为 |
| 特点 | 代码模块化(函数),数据与操作分离 | 代码封装为类,数据与操作绑定 |
| 适用场景 | 简单程序(如计算器、脚本) | 复杂程序(如电商系统、游戏) |
| 扩展性 | 修改难度大,牵一发而动全身 | 易扩展,通过继承、多态灵活扩展功能 |
注:面向对象与面向过程二者相辅相成,并非是对立的关系!
2. Java语言的基本元素(类、对象)
2.1 核心概念
类(Class)
- 定义:对一类具有相同属性和行为的对象的抽象描述,是对象的模板;
- 方便理解:人类,灵长类等;
- 举例:“学生类” 定义了所有学生共有的属性(姓名、年龄等)和行为(上课、考试等);
对象(Object)
- 定义:具体的事物或抽象的概念,具有属性和行为;
- 方便理解:一个名叫张三的人,一只5岁的黑猩猩等;
- 举例:“学生对象 张三” 是一个具体的学生,是学生类的实例化对象;
2.2 类的概述
类(class)是一组相关属性和行为的集合,这也是类最基本的两个成员;
属性:是类中定义的变量,用于存储对象的状态。也称为 “成员变量”。
- 属性 <=> 成员变量 <=> Field
行为:是类中定义的方法,用于描述对象的行为。也称为 “成员方法”。
- 行为 <=> 成员方法 <=> Method
类的声明:
[权限修饰符] class 类名 {
属性声明;
方法声明;
}
-- 举例 --
public class Person {
String name;// 属性(成员变量)name
int age;// 属性(成员变量)age
// 行为(成员方法)showName
public void showName() {
System.out.println("name =" + name);
}
// 行为(成员方法)showAge
public void showAge() {
System.out.println("age =" + age);
}
}
2.3 对象的概述
对象(Object)是类的实例化结果,可以将理解为现实世界中具体存在的事物在程序中的抽象表示。
特点:
- 封装性:对象将数据(属性)和操作数据的方法封装在一起,对外隐藏内部实现细节。
- 独立性:每个对象都是独立的个体,拥有自己的属性值,不同对象之间互不干扰。
- 交互性:对象之间可以通过方法调用进行交互,协作完成复杂功能。
对象的创建:
在 Java 中,通过 new 关键字创建对象,语法如下:
类名 对象名 = new 类名();
// 举例:创建Person类的实例对象
Person person = new Person();
对象的使用:
- 访问属性:通过
对象名.属性名访问或修改对象的状态 - 调用方法:通过
对象名.方法名()让对象执行特定操作
// 举例:创建Person类的实例对象
Person person = new Person();
// 获取person对象的name属性
String name = person.name;
// 将person对象的name属性修改为“张三”
person.name = "张三";
// 调用person对象的showName方法
person.showName();
3. 对象的内存解析
3.1 JVM内存结构划分
HotSpot JVM(Java虚拟机)的架构图如下:
其中我们主要关心的是运行时数据区部分(Runtime Data Area)。
-
栈(Stack):是指虚拟机栈,用于存储局部变量等。局部变量表存放了编译期可知长度的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不是对象本身,而是对象在堆内存的首地址)。方法执行完成后,自动释放。
-
堆(Heap):此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
-
方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
3.2 代码内存解析
示例代码:
// 定义一个“人”类
class Person {
String name;
int age;
}
// 定义一个测试类,在测试类中使用 main 方法执行具体代码
public void Test {
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "TheShy";
p1.age = 25;
Person p2 = new Person();
p2.name = "clearlove7";
p2.name = 32;
P2 = P1;
Person p3 = new Person();
}
}
内存解析流程图:
Tips:
- 新创建一个类的多个对象时(比如p1、p2),则每个对象都拥有当前类的一个“副本”,多个对象之间互不影响,当修改其中一个对象的属性时,不会影响其它对象此属性的值。
- 当一个变量指向另一个变量进行赋值时(比如p2 = p1),此时并没有在堆空间中创建新的对象,而是两个变量共同指向了堆空间中同一个对象。此时,通过其中一个变量修改指向对象的属性时,将会影响所有指向该对象的变量的调用。
3.3 对象数组
数组的元素可以是 基本数据类型,也可以是 引用数据类型
当元素是引用数据类型中的类时,我们称为 对象数组
示例:
public class Person() {
int number; //编号
int age; //年龄
}
public class Test() {
Person[] personArr = new Person[8];
for (int i = 0; i < personArr.length; i++) {
personArr[i] = new Person();
personArr[i].number = i;
personArr[i].age = i+20;
}
}
内存解析:
Tips:
- 定义对象数组时,必须确定数组长度,否则对象数组为null
4. 类的成员之一:变量
前面我们提到 “属性,即是类中定义的变量”,在Java中,可分为成员变量和局部变量两种变量。
变量的声明格式
[修饰符] 数据类型 变量名 [= 初始化值];
4.1 成员变量和局部变量
位置在类中,方法体外 的变量,即为 成员变量;
位置在类中,方法体内 的变量,即为 局部变量。
示例:
public class Person {
String name; // 成员变量1
int age; // 成员变量2
public void sayHello(String yourName) { // 局部变量1
String info = "hello!"; // 局部变量2
System.out.println(info + yourName);
}
}
4.2 成员变量 VS 局部变量
相同点:
- 声明格式相同,所有变量的声明格式都是移植的
- 所有变量都仅在其自己的作用域内有效
不同点:
-
位置不同
- 成员变量的位置是,在类中,
方法体外的变量 - 局部变量的位置是,在类中,
方法体内的变量(代码块中也属于局部变量)
- 成员变量的位置是,在类中,
-
内存中存储位置不同
- 成员变量在内存中存储于
堆内存 - 局部变量在内存在存储于
栈内存
- 成员变量在内存中存储于
-
生命周期不同
- 成员变量与对象的生命周期一致,跟随对象的创建而产生,跟随对象的销毁而销毁;即使多个对象的数据类型相同,但每个对象的成员变量独立存在
- 局部变量与方法的执行周期一致,跟随方法的调用而产生,跟随方法的结束而销毁。即使多次方法的调用,但每次方法调用的局部变量独立存在
-
作用域不同
- 成员变量在本类中可直接调用,其他类中获取方式有
对象.变量名或get方法等 - 局部变量仅在本方法中可调用
- 成员变量在本类中可直接调用,其他类中获取方式有
-
可用修饰符不同
- 成员变量:
public,privite,final,static,volatile等 - 局部变量:
final
- 成员变量:
-
默认值不同
- 成员变量有默认值,引用数据类型的默认值为null
- 局部变量无默认值,只有在方法被调用时才有意义,其中形参的依靠实参进行初始化
5. 类的成员之一:方法
前面我们提到 “行为,即是是类中定义的方法”,在其他语言中也常称为函数或过程。
5.1 什么是方法?
- 方法是一个类行为特征的抽象,用来完成某个功能或一组操作
- 方法存在的目的是为了
实现代码重用,减少冗余 - 在Java中,方法无法独立存在,必须定义在类中
方法声明的格式
权限修饰符 [其他修饰符] 返回值类型 方法名(形参列表) [throws 异常列表] {
// 方法体
[return xxx;] //当返回值类型不为void时,方法体中必须使用return结束方法进行返回
}
注:[]表示非必填
权限修饰符:public、protected、缺省、private
其他修饰符:static、final等
返回值类型:基本数据类型、引用数据类型、void表示无返回值
方法名:该方法的标识符
形参列表:调用该方法需要传递的参数,可无
异常列表:后续讲解
方法体:整个方法的核心,方法体中的内容决定该方法可以干什么
return:
1. 作用是结束方法的执行,同时将方法的结果返回
2. 当返回值类型为void时,该方法可以没有return或直接写 "return;"
3. 当返回值类型为基本数据类型或引用数据类型时,该方法必须有"return xxx;",其中"xxx"的数据类型必须与返回值类型一致
4. return语句后面的"所有代码将不会执行!且不可再编写代码!"
方法的举例:
- Math.random()的random()方法
- Math.sqrt(x)的sqrt(x)方法
- Arrays类中的sort()、equals()方法
- 类中常见的get、set方法
5.2 方法的调用
方法调用格式:对象.方面名(参数);
示例:
/**
* 方法调用案例演示
*/
public class MethodInvokeDemo {
public static void main(String[] args) {
System.out.println("-----------------------方法调用演示-------------------------");
//调用Demo类中无参无返回值的方法sayHello,调用一次,执行一次,不调用不执行
Demo md = new Demo();
md.sayHello();
//调用Demo类中有参无返回值的方法sayHello,调用一次,执行一次,不调用不执行
String name = "刘旭涛";
int age = 25;
md.sayHello(name,age);
//调用Demo类中有参无返回值的方法sayHello,调用一次,执行一次,不调用不执行
String name = "刘旭涛";
int age = 25;
String info = md.sayHello(name,age);
//举例 info 的内容为:大家好,我是刘旭涛,我今年25岁
}
}
5.3 方法相关的注意事项
- 必须先声明后使用,且方法必须定义在类的内部
- 调用一次就执行一次,不调用不执行
- 方法中可以调用类中的方法或属性,不可以在方法内部定义方法
5.4 方法中的 return 语句
- 作用: 结束一个方法,同时返回一个结果
- 当返回值类型为void时,该方法可以没有return或直接写 "return;"
- 当返回值类型为基本数据类型或引用数据类型时,该方法必须有"return xxx;",其中"xxx"的数据类型必须与返回值类型一致
- return语句后面的"所有代码将不会执行!且不可再编写代码!"
5.5 方法调用的内存解析
- 方法在未被调用的时候,都在
方法区中的字节码(.class)文件中存放 - 方法在被调用的时候,需要进入
栈内存中执行,称之为入栈,即在栈内存中开辟独立的内存供该方法执行,该内存中将存储该方法执行过程中产生的局部变量等 - 方法在被调用结束的时候,会释放刚才分配的内存,称之为
出栈。当该方法有返回值时,会将返回值返回至调用处;当没有返回值时,则继续执行调用处的后续代码 - 栈结果的特点:
先进后出,后进先出
示例代码:
public class Person {
String name;
/**
* 调用eat方法时,会先调用sleep方法,再执行打印睡醒了
*/
public void eat() {
sleep();
System.out.println(name+"睡醒了,可以吃饭了!");
}
/**
* 调用sleep方法时,会先调用sport方法,再执行打印运动结束
*/
public void sleep() {
sport();
System.out.println(name+"运动结束,可以睡觉了!");
}
/**
* 调用sport方法时,直接打印开始运动
*/
public void sport() {
System.out.println(name+"开始运动!");
}
}
public class Test {
public static void main() {
Person p1 = new Person();
p1.eat();
}
}
内存解析图:
5.6 方法进阶
5.6.1 方法的重载
概念: 在一个类中,允许存在相同方法名的方法,前提是他们的形参列表不相同
意味着,当一个类中的多个方法的方法名相同,但形参列表各不相同时,我们称之为方法的 重载
在方法被调用时,JVM 通过形参列表调用最匹配的方法,先找形参个数、类型最匹配的,其次找个数和类型可兼容的,多个兼容时会报错。例如:形参为int、double、long 等类型时。
5.6.2 可变个数形参
在JDK5版本中,新增了 “Varargs(variable number of arguments)” 机制,即当定义一个方法时,形参列表的个数可动态不确定,通俗讲就是 可变个数形参
格式:
方法名(参数类型... 参数名)
举例1:
add(int... numbers) {
//方法体
}
举例2:
add(String name, int... numbers) {
//方法体
}
Tips:
- 可变个数形参等同于数组参数,二者不可同时存在
- 可变个数形参必须声明在参数列表的最后
- 一个方法中只能存在一个可变个数形参