续前篇:编程语言Java入门——基础知识篇(二)-CSDN博客
目录
5. 类和对象
5.1 面向对象概述
Java是一种纯粹的面向对象编程语言,其核心思想是面向对象编程(OOP)。
5.1.1 类和对象
1. 类和对象的概念
类(Class) | 对象(Object) |
---|---|
对象的模板或蓝图 | 类的具体实例 |
定义属性和方法 | 存储实际数据并执行方法 |
编译时概念 | 运行时概念 |
不占用内存 | 占用内存 |
例如:class Car | 例如:Car myCar = new Car() |
2. 类与对象的本质关系
(1) 类:设计蓝图
类就像现实世界中的设计蓝图或配方,它定义了:
-
结构特征(成员变量):如房子的房间数量、汽车的发动机型号
-
行为规范(方法):如房子的开门方法、汽车的加速方法
-
创建规则(构造方法):如建房需要哪些材料、造车需要哪些零件
关键特点:
-
类本身不占用物理空间(就像设计图不占用建筑用地)
-
类定义了创建对象的规则(就像图纸规定了房子的建造标准)
-
类可以重复使用创建多个对象(一张图纸可以建多栋房子)
(2) 对象:具体实例
对象是根据类创建的具体实体,就像:
-
根据设计图实际建造的房子
-
按照配方烘焙出的蛋糕
-
使用模具生产的塑料玩具
关键特点:
-
每个对象都是独立的内存实体
-
对象具有唯一标识(内存地址)
-
对象可以修改状态(成员变量值可变)
-
不同对象之间互不影响(修改一个房子不会影响其他房子)
3. 深入类比分析
(1)汽车制造厂类比
概念 | 汽车制造厂类比 | Java对应 |
---|---|---|
类 | 汽车设计图纸 |
|
对象 | 生产出的汽车 |
|
成员变量 | 汽车配置参数 |
|
方法 | 汽车功能操作 |
|
构造方法 | 生产线装配流程 |
|
static成员 | 工厂公共设施 |
|
运作流程:
-
工程师设计汽车图纸(定义类)
-
生产线按图纸装配(调用构造方法)
-
生产出具体汽车(创建对象)
-
每辆汽车可独立使用(对象调用方法)
-
所有汽车共享工厂数据(static变量)
存储方式为:
(2) 餐厅运营类比
概念 | 餐厅运营类比 | Java对应 |
---|---|---|
类 | 餐厅运营手册 |
|
对象 | 具体分店 |
|
成员变量 | 分店特色 |
|
方法 | 服务流程 |
|
构造方法 | 开店准备 |
|
static成员 | 总部管理系统 |
|
典型场景:
-
每家分店有自己的特色菜(对象独有属性)
-
但都遵循相同的服务标准(类定义的方法)
-
总部知道所有分店数量(static变量)
-
新开分店不影响其他分店(对象独立性)
(3)学校管理系统(代码示例)
class Student {
private String studentId; // 学号
private String name; // 姓名
public static String schoolName = "XX大学"; // 学校名称
public Student(String id, String name) {
this.studentId = id;
this.name = name;
}
public void attendClass() {
System.out.println(name + "正在上课");
}
}
对于Student类中学校和学号对于每一个Student对象来说都是共享访问;至于封装在类中的不同方法则可以看作是不同的实例。
5.1.2 封装
定义:封装是指将数据(属性)和操作数据的方法(行为)绑定在一起,形成一个独立的单元(类),并对外隐藏内部实现细节,仅暴露必要的访问接口。
封装的作用:数据保护:防止外部代码直接修改对象内部状态,保证数据安全性;代码模块化:提高代码的可维护性和可读性;降低耦合:外部代码只需通过公开的方法访问对象,无需关心内部实现。
实现方式:使用 private
修饰属性,防止外部直接访问;提供 public
的 getter
和 setter
方法控制访问;可以在 setter
方法中添加数据校验逻辑。
这里我们举个例子:
public class Person {
private String name; // 私有属性,外部无法直接访问
private int age;
// Getter方法:提供对私有属性的读取
public String getName() {
return name;
}
// Setter方法:提供对私有属性的修改(可添加校验逻辑)
public void setName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name cannot be empty");
}
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age < 0 || age > 120) {
throw new IllegalArgumentException("Invalid age");
}
this.age = age;
}
// 构造器
public Person(String str,int age) {
this.name = str;
this.age = age;
}
}
我们不妨这么理解,对于我们创建的一个对象Person,我们把他的一些私有属性例如年龄和名字之类的封装在Person类中仅供内部调用,所以当一个对象存在多个属性时我们可以用封装的方法放到一个类中。上述示例中Getter是对传入参数的提取,Setter方法是对私有属性的修改和判断,比如当我们的名字一栏为空,则报异常IllegalArgumentException,当年龄过大或过小时同样报异常IllegalArgumentException。另外,我们还需要一个构造器,这样就可以通过new 的方式创建Person对象了。
public static void main(String[] args) {
Person person1 = new Person("zhangsan",20);
System.out.println(person1.getName());
System.out.println(person1.getAge());
}
通过Person person1 = new Person("zhangsan",20);语句,我们就创建了一个名字叫zhangsan,年龄为20的对象了。再把name和age打印出来,运行结果:
zhangsan
20
5.1.3 继承
定义:继承允许一个类(子类)继承另一个类(父类)的属性和方法,并可以扩展或修改它们。
作用:代码复用:子类可以直接使用父类的属性和方法,减少重复代码;扩展功能:子类可以新增方法或重写父类方法;多态的基础:继承是实现多态的前提。
实现方式:使用 extends
关键字;Java 是单继承,一个类只能继承一个父类(但可以实现多个接口);子类可以访问父类的 public
和 protected
成员,但不能访问 private
成员。
public class Animal {
protected String name; // protected 允许子类访问
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + " is eating.");
}
}
public class Dog extends Animal {
protected String name; // protected 允许子类访问
public Dog(String name) {
super(name);
this.name = name;
}
// 新增方法
public void bark() {
System.out.println(name + " is barking.");
}
// 方法重写(Override)
@Override
public void eat() {
System.out.println(name + " is eating dog food.");
}
}
public class test {
public static void main(String[] args) {
Animal animal = new Animal("afu");
System.out.println(animal.name);
animal.eat();
Dog dog = new Dog("puppy");
System.out.println(dog.name);
dog.bark();
}
}
可见,对于对象Dog,继承了父类Animal的方法,同时可以对父类的方法重写。最后的输出结果为:
afu
afu is eating.
puppy
puppy is barking.
5.1.4 多态
定义:多态是指同一个方法在不同情况下有不同的行为,主要分为:
-
编译时多态(方法重载)
-
运行时多态(方法重写 + 动态绑定)
作用:提高代码灵活性:同一方法可以适应不同的对象;降低耦合:程序可以基于接口或父类编程,而不依赖具体实现。
实现方式:
1. 方法重载(Overload)
同一个类中,方法名相同,参数列表不同(个数、类型、顺序)。
返回类型可以不同(但不能仅靠返回类型区分)。
示例:
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
2. 方法重写(Override)
子类重写父类方法,方法名、参数列表、返回类型必须相同。
访问权限不能比父类更严格(如父类是 public
,子类不能是 private
)。
示例:
public class Animal {
public void makeSound() {
System.out.println("Animal makes sound.");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks.");
}
}
3. 动态绑定(运行时多态)
父类引用指向子类对象,调用方法时执行子类的方法。
示例:
Animal myDog = new Dog(); // 父类引用指向子类对象
myDog.makeSound(); // 输出 "Dog barks."
5.2 类
5.2.1 类的基本结构
[访问修饰符] class 类名 {
// 成员变量(属性)
// 构造方法
// 成员方法
// 代码块
// 内部类
}
5.2.2 类的组成部分
1. 成员变量(属性)
-
描述对象的状态
-
可以是基本类型或引用类型
-
通常使用
private
封装
public class Book {
private String title; // 实例变量(每个对象独立)
private double price;
public static String publisher = "ABC Press"; // 静态变量(所有对象共享)
}
2. 成员方法
-
定义对象的行为
-
可以访问成员变量和其他方法
public class Calculator {
// 实例方法
public int add(int a, int b) {
return a + b;
}
// 静态方法
public static double squareRoot(double num) {
return Math.sqrt(num);
}
}
3. 构造方法
-
用于初始化对象
-
特点:
-
方法名与类名相同
-
无返回类型(包括
void
) -
可以重载(多个构造方法)
-
public class Student {
private String id;
// 无参构造
public Student() {
this.id = "UNKNOWN";
}
// 带参构造
public Student(String id) {
this.id = id;
}
}
4. 代码块
-
静态代码块:类加载时执行(仅一次)
-
实例代码块:每次创建对象时执行(在构造方法之前)
public class Demo {
// 静态代码块
static {
System.out.println("类已加载");
}
// 实例代码块
{
System.out.println("对象初始化中...");
}
}
5. 内部类
-
定义在类内部的类
-
分为:成员内部类、静态内部类、局部内部类、匿名内部类
public class Outer {
private String outerField = "Outer";
// 成员内部类
class Inner {
void show() {
System.out.println(outerField); // 访问外部类成员
}
}
}
5.2.3 类的特性
1. 访问控制
修饰符 | 类内 | 同包 | 子类 | 任意位置 |
---|---|---|---|---|
| ✔ | ✖ | ✖ | ✖ |
| ✔ | ✔ | ✖ | ✖ |
| ✔ | ✔ | ✔ | ✖ |
| ✔ | ✔ | ✔ | ✔ |
示例:
public class AccessDemo {
private String secret = "confidential"; // 仅本类可访问
protected String familyData = "shared"; // 子类可访问
public String globalData = "open"; // 全部可访问
}
5.2.4 static 关键字
static
用于修饰类的成员(变量、方法、代码块、内部类),使其成为类级别的成员,而非对象级别。
特点
-
静态成员属于类本身,而非某个对象实例
-
在类加载时初始化,所有对象共享同一份静态成员
-
静态方法只能访问静态成员,不能直接访问实例成员
使用场景:
类型 | 示例 | 说明 |
---|---|---|
静态变量 |
| 所有对象共享的计数器 |
静态方法 |
| 工具类方法(如 |
静态代码块 |
| 类加载时执行初始化 |
静态内部类 |
| 不依赖外部类实例 |
示例代码:
public class Counter {
// 静态变量
public static int count = 0;
public Counter() {
count++; // 所有对象共享同一个count
}
// 静态方法
public static void showCount() {
System.out.println("Total: " + count);
}
// 静态代码块
static {
System.out.println("Counter类已加载");
}
}
// 使用
Counter c1 = new Counter();
Counter c2 = new Counter();
Counter.showCount(); // 输出: Total: 2
5.2.5 this和super关键字
1. this 关键字作用:
this
表示当前对象的引用,用于:
-
区分成员变量与局部变量(同名时)
-
调用当前类的其他构造方法(
this()
) -
作为参数传递当前对象
使用场景
场景 | 示例 | 说明 |
---|---|---|
解决命名冲突 |
| 区分成员变量和参数 |
构造方法调用 |
| 调用本类其他构造方法 |
返回当前对象 |
| 链式调用支持 |
2. super关键字作用:
super
表示父类对象的引用,用于:
-
调用父类的构造方法(
super()
) -
访问父类被覆盖的成员(变量/方法)
-
解决子父类成员命名冲突
使用场景
场景 | 示例 | 说明 |
---|---|---|
调用父类构造方法 |
| 必须放在子类构造方法第一行 |
访问父类方法 |
| 调用被重写的父类方法 |
访问父类变量 |
| 访问被隐藏的父类变量 |
5.2.6 类的主方法
在Java中,主方法(main method)是程序的入口点,当Java程序启动时,JVM会寻找并执行这个主方法。其基本语法为:
public class MainClass {
public static void main(String[] args) {
// 程序代码从这里开始执行
System.out.println("Hello, World!");
}
}
主方法的组成部分:
-
public - 访问修饰符,表示该方法可以从任何地方访问
-
static - 表示该方法属于类而不是类的实例,可以在不创建类对象的情况下调用
-
void - 返回类型,表示该方法不返回任何值
-
main - 方法名,必须是"main"
-
String[] args - 参数,是一个字符串数组,用于接收命令行参数
注意:
-
主方法必须严格遵循上述签名格式,否则JVM将无法识别它作为程序的入口点
-
一个Java程序可以有多个类包含主方法,但运行时只能指定一个作为入口
-
从Java 1.7开始,主方法也可以使用可变参数形式:
main(String... args)
主方法是每个独立Java应用程序的必要组成部分,它定义了程序的起点和初始执行流程。
5.3 对象
Java对象是面向对象编程的核心概念,理解Java对象的结构、创建过程、内存布局以及相关特性对于编写高效、健壮的Java程序至关重要。本文将全面解析Java对象的各个方面。
5.3.1. 对象的基本概念
Java对象是类的实例,每个对象都包含状态(属性)和行为(方法)。在Java中,所有对象都继承自java.lang.Object
类。
对象的基本特征:
-
封装性:隐藏内部实现细节,通过公共方法暴露功能
-
继承性:子类可以继承父类的属性和方法
-
多态性:同一方法在不同子类中有不同实现
5.3.2 对象的结构
Java对象在内存中的结构分为三部分:对象头、实例数据和对齐填充。
(1)对象头(Header)
对象头包含以下信息:
-
Mark Word:存储对象运行时数据,包括:
-
哈希码(HashCode)
-
GC分代年龄
-
锁状态标志(无锁、偏向锁、轻量级锁、重量级锁)
-
线程持有的锁
-
偏向线程ID
-
偏向时间戳16
-
-
Class Pointer:类型指针,指向方法区中的类元数据,用于确定对象属于哪个类
-
Array Length(仅数组对象有):记录数组长度
(2)实例数据(Instance Data)
包含对象的所有成员变量,包括从父类继承的和自身定义的。这部分是对象真正存储的有效信息。
(3)对齐填充(Padding)
HotSpot VM要求对象起始地址必须是8字节的整数倍。当实例数据部分不是8字节的倍数时,需要填充数据以达到对齐要求。
5.5.3 Java对象的创建过程
Java对象的实例化流程可以分为以下步骤:
-
类加载:ClassLoader加载class到方法区
-
内存分配:在堆中为对象分配内存空间
-
初始化默认值:按照class初始化对象,实例数据设为默认值
-
设置对象头:填充Mark Word、Class Pointer等信息
-
执行构造方法:调用构造方法完成对象的初始化
public static void main(String[] args) {
Object obj = new Object();
System.out.println(obj.toString());
}
5.5.4 对象的锁状态与Mark Word
Java对象的锁状态存储在Mark Word中,共有4种状态:
-
无锁状态:对象未被任何线程锁定
-
偏向锁:优化单线程重复获取锁的场景
-
轻量级锁:多线程竞争不激烈时使用
-
重量级锁:传统的内置锁,涉及操作系统互斥量
锁状态只能升级不能降级,这种设计是为了提高锁的性能。
6. 对象的设计原则
良好的对象设计应遵循面向对象设计原则4:
-
单一职责原则:一个类只负责一个功能领域
-
开闭原则:对扩展开放,对修改关闭
-
里氏替换原则:子类可以替换父类
-
接口隔离原则:客户端不应依赖不需要的接口
-
依赖倒置原则:依赖抽象而非具体实现
-
组合聚合复用原则:优先使用对象组合(has-a关系)而非继承(is-a关系)实现代码复用。例如,在汽车类中嵌入引擎对象,而非继承引擎类,避免继承链的复杂性。
-
迪米特法则:减少对象间的直接交互,通过中间类(如“经纪人”)间接通信。例如,订单系统通过服务层调用库存模块,而非直接访问库存类的内部方法。
对于对象的创建,结构还有各个组成部分可以参考这两篇文章,可以说对于对象的描述是非常到位了:
5.5.5 对象与引用
在 Java 中,对象是通过引用(Reference)来访问的,而不是直接操作对象本身。理解对象引用对掌握 Java 内存管理、垃圾回收(GC)和多线程编程至关重要。
-
引用(Reference) 是一个指向堆内存中对象的指针(地址)。
-
变量存储的是引用,而不是对象本身。
以下面的例子为例:p1就是引用,而new Person()是对象。
Person p1 = new Person(); // p1引用指向新创建的Person对象
Person p2 = p1; // p2引用指向同一个对象
p2.setName("John"); // 通过任一引用修改对象
System.out.println(p1.getName()); // 输出"John"
内存模型:
栈(Stack) | 堆(Heap) |
---|---|
存储基本类型变量(int , double 等) | 存储对象实例(new 创建的) |
存储对象引用(变量名,如 p ) | 存储对象的属性和方法 |
自动管理(方法结束即释放) | 由 GC(垃圾回收器) 管理 |
5.5.6 对象的销毁
在 Java 中,对象的销毁完全由 垃圾回收器(Garbage Collector, GC) 自动管理,开发者无法直接销毁对象。
1. 对象何时被销毁?
对象在满足以下条件时会被 GC 回收:
(1)无引用指向:对象不再被任何 GC Roots 引用(强引用、软引用、弱引用、虚引用)。
(2)GC 触发:JVM 根据内存情况自动运行垃圾回收。
示例:对象变为垃圾
Person p = new Person("张三"); // 对象被 p 引用
p = null; // 断开引用,对象变为"可回收状态"
System.gc(); // 建议 JVM 执行 GC(不保证立即执行)
2. 对象销毁的过程
(1) 标记阶段(Marking)
-
GC 从 GC Roots(如栈帧中的局部变量、静态变量)出发,标记所有可达对象。
-
未被标记的对象判定为垃圾。
(2) 回收阶段(Reclamation)
根据不同的 GC 算法,回收方式不同:
-
标记-清除:直接回收内存(可能产生碎片)。
-
复制算法:将存活对象复制到新区域(新生代常用)。
-
标记-整理:移动存活对象压缩内存(老年代常用)。
3. 影响对象销毁的关键因素
(1) 引用类型
引用类型 | GC 行为 | 示例 |
---|---|---|
强引用 | 不回收(除非引用断开) |
|
软引用 | 内存不足时回收 |
|
弱引用 | 下次 GC 必定回收 |
|
虚引用 | 无法通过它访问对象,用于跟踪回收状态 |
|
(2) Finalizer 机制(不推荐使用)
-
如果类重写了
finalize()
方法,对象在回收前会调用该方法。 -
问题:
-
执行时机不确定(可能永远不调用)。
-
性能差(影响 GC 效率)。
-
-
替代方案:使用
Cleaner
(Java 9+)或try-with-resources
。
@Override
protected void finalize() throws Throwable {
System.out.println("对象即将被回收"); // 不推荐依赖此方法!
}
(3) 内存泄漏(阻止对象销毁)
常见场景:
-
静态集合长期持有对象:
static List<Object> cache = new ArrayList<>(); cache.add(new Object()); // 对象永远不会被回收
-
未关闭资源(如文件流、数据库连接)。
-
监听器未注销(如事件监听器)。
4. 如何主动促进对象销毁?
虽然不能强制销毁对象,但可以通过以下方式减少对象存活时间:
-
及时置空引用:
List<String> data = loadLargeData(); data = null; // 不再需要时手动断开引用
-
使用弱引用(如
WeakHashMap
):Map<Key, Value> cache = new WeakHashMap<>(); // 内存不足时自动清理
-
关闭资源:
try (FileInputStream fis = new FileInputStream("file.txt")) { // 自动调用 fis.close() }
5. 对象销毁的监控与验证
(1) 查看对象回收
使用 finalize()
(仅用于实验,生产环境勿用):
class MyObject {
@Override
protected void finalize() {
System.out.println("对象被回收");
}
}
public static void main(String[] args) {
new MyObject(); // 匿名对象,无引用
System.gc(); // 建议触发 GC
}
(2) 使用工具分析
-
VisualVM:监控堆内存和 GC 活动。
-
Eclipse MAT:分析内存泄漏。
-
JConsole:查看实时内存使用。