三、面向对象

面向对象概述

面向过程

关注点在实现功能的步骤上

面向对象

关注点在实现功能需要哪些对象的参与,可以把问题拆分成几个对象,对象协作起来解决问题。

面向对象开发方式耦合度低,扩展能力强。

面向对象的三大特征

  • 封装
  • 继承
  • 多态

对象的创建

1
2
3
4
[修饰符列表] class 类名{
//属性
/
}

JVM内存分析

元空间 metaspace

元空间中存储的是类的元信息,字节码等。

元空间是java8之后引入的。是JVM规范中方法区的实现。

方法区:JVM规范 的叫法,各个厂商根据这个规范去实现具体的java虚拟机。

总结:方法区是规范,元空间是实现。java8之前使用永久代实现的。

堆内存

所有new的对象,都存储在堆内存中。

方法被调用时会给该方法分配空间,在VM Stack中压栈。

image-20250414144722832

JVM有自动垃圾回收机制,主要针对堆区。

实例变量和实例方法

通常描述一个对象的行为动作时,不加static——称为实例方法

实例方法不能通过类访问,必须通过对象访问。

方法调用传递引用数据类型

传的是地址!!!

!this关键字

  • this本质是一个引用
  • this中保存的是当前对象的内存地址
  • this.大部分情况可以省略,默认是访问当前的类的实例变量。当需要区分局部变量和实例变量时,不能省略。
  • this存储在栈帧的局部变量表的第0个槽位上。
  • this不能出现在静态方法中

this 实参

  • 通过这种语法,可以在构造方法中调用本类的其他构造方法
  • 作用:代码复用
  • this实参只能出现在构造方法的第一行!!!

image-20250416141454757

封装

通过限制外部对对象内部的直接访问和修改,保证数据的安全性,并提高了代码的可维护性和可复用性。

  1. 属性私有化:使用private修饰
  2. 对外提供接口

*快速创建getter,setter方法

  1. alt + insert
  2. 选择getter and setter
  3. 选择要创建的内容

构造器(构造方法)

  1. 对象的创建
  2. 对象的初始化(默认有super();,先对父类的变量进行初始化)

这两个阶段不能颠倒,也不能分割。

  • 构造方法名需与类名一致。

  • 不需要写return,不需要写返回值类型。

  • 如果没有显式定义构造方法,系统会提供一个无参数的构造方法,并且会给属性赋默认值。

  • 定义有参数的构造方法后,可以手动再写一个无参数的构造方法。【方法重载】

如何调用构造方法

new 构造方法名(实参)

写了构造方法,为什么还要单独写set方法?

构造方法是对象第一次创建时用于初始化的。set方法可以在后期修改属性值

构造代码块

语法格式
1
2
3
{ } //直接写在class体里面
//每次new都会运行一次构造代码块中的内容,运行前对象已经创建好,并且完成了初始值的赋值。
//!!!构造代码块是在构造方法执行之前执行的!!!
作用

如果所有的构造方法在最开始的时候有相同的一部分代码,可以将公共的代码放在构造代码块中,达到复用的效果。

流程
  1. new的时候在堆内存中开辟空间,给所有属性赋默认值
  2. 执行构造代码块进行初始化
  3. 执行构造方法体进行初始化
  4. 构造方法执行结束,对象初始化完毕

static 关键字

  • static修饰静态变量,当所有对象的某个属性的值是相同的,建议将该属性定义为静态变量,来节省内存。
  • JDK8后,静态变量存储在堆内存中。在类加载时进行初始化。
  • 静态变量可以通过“引用.”来访问,实际运行时和对象无关(不会出现空指针异常),但不建议。会让程序员造成误解。

image-20250416143214835

静态代码块

  • 语法格式: static{ }
  • 在类加载的时候执行,并且只执行一次。
  • 可以有多个静态代码块,自上而下依次执行。
  • 作用:在类加载的时候运行一段代码,可能是进行一些准备工作。

java虚拟机规范

运行时数据区的六个内容

  • PC Register, PC计数器:是一块较小的内存空间,用于存储下一条要执行的字节码指令地址。
  • java Virtual Machine Stacks, java虚拟机栈:用于存储栈帧,栈帧存储局部变量表、操作数栈、动态链接、方法出口等信息。
  • Heap, 堆:java虚拟机所管理的最大的一块内存,用于存储java对象实例以及数组。堆是垃圾回收器主要使用区域。
  • Method Area, 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量(hotspot把这个内容存到堆里去了)、即时编译器编译后的代码等数据。
  • Run-Time Constant Pool, 常量池:方法区的一部分,用于存放编译期生成的各种字面量与符号引用(类名、方法名、属性名)。
  • method stacks, 本地方法栈:在本地方法的执行过程中,会使用本地方法栈。

image-20250416160312404

GoF设计模式

什么是设计模式

可以重复利用的一套方案

GoF设计模式的分类

  1. 创建型:主要解决对象的创建问题
  2. 结构型:通过设计和构建对象之间的关系,以达到更好的重用性、扩展性和灵活性
  3. 行为型:主要用于处理对象之间的算法和责任分配

单例模式

属于创造型设计模式,确保一个类只有一个实例,并提供一个全局访问点来访问该实例。

饿汉式单例模式

类加载时对象就创建好了,不管这个对象用还是不用

  1. 构造方法私有化
  2. 定义一个静态变量,在类加载的时候初始化静态变量(只初始化一次)
  3. 对外提供一个公开的静态方法,用这个方法获取单个实例

image-20250416162537904

懒汉式单例模式

用到这个对象的时候再创建对象,别在类加载的时候创建对象

  1. 构造方法私有化
  2. 提供一个静态变量,但这个变量的值为NULL
  3. 对外提供一个静态方法,通过这个方法可以获取对象

image-20250416162601772

继承

作用

  • 代码复用
  • 有了继承,才有了方法覆盖和多态机制

实现

1
2
3
[修饰符列表] class 类名 extends 父类名 {

}

特性

  • 只支持单继承,一个类只能继承一个类
  • 不支持多继承,但支持多重继承(多层继承)
  • 子类继承父类的除私有的、构造方法以外的所有内容
  • 一个类没有显式继承任何类时,默认继承java.lang.Object类

方法覆盖(重写) overwrite

什么时候使用?

当从父类继承过来的方法,无法满足子类的业务需求时。

特性

  • 当子类将父类方法覆盖之后,将来子类对象调用方法的时候,一定会执行重写之后的方法。
  • 注解:@override,在方法前写这个注解,在编译阶段会检查这个方法是否重写了父类的方法。
  • 如果返回值类型是引用数据类型,那么这个返回值类型可以是原类型的子类型。
  • 访问权限不能变低,可以变高。public最高
  • 抛出异常不能变多,可以变少。
  • 方法覆盖针对的是实例方法,和静态方法无关。

多态

向上转型和向下转型的基本概念

引用数据类型进行类型转换。

向上转型:子–>父 (可以等同看做自动类型转换)

父类型引用指向子类型对象,这是多态机制最核心的语法。

如果父类中没有某个方法,而子类中有,那么就需要向下转型。

image-20250416211915263

向下转型:父–>子(可以等同看做强制类型转换)

当调用的方法是子类特有的方法,需要向下转型,进行强制转换。

如果两个子类不是同一类,会出现ClassCastException异常

image-20250416212604349

如何避免ClassCastException异常?

使用运算符 instanceof

语法格式:

引用 instanceof 类型

在进行向下转型之前,用if语句判断一下是否是要向下转型的类型,不是就不要转换了。

静态方法和多态没有关系,因此静态方法和方法覆盖无关系。

软件开发七大原则

image-20250417144316692

多态在开发中的作用

  1. 降低程序耦合度,提高程序的扩展力
  2. 尽量使用多态,面向抽象编程,不要面向具体编程。

实例变量无法覆盖,根据声明的类型进行赋值

抽象类和抽象方法

1
2
3
4
5
6
public abstract class Name{ //父类:所有子类的公共属性+公共方法的集合体
public abstract void functionName();
}
// 抽象方法必须在抽象类中
//public 和 abstract的顺序没有要求
// !!!继承该抽象类的子类必须覆盖这个抽象方法

存在的意义:强制子类重写抽象方法,编译器会报错。如果类中有一些方法无法实现或者没有意义,就可以将方法定义为抽象方法。

  • abstract 关键字不能和private、final、static关键字共存

super 关键字

  • 当子类和父类有名称相同的属性/方法,此时调用父类中继承而来的属性/方法需要使用super.属性/方法
  • super不能在静态方法中使用
  • this可以单独输出(本质是引用,内容是地址),super不能单独输出(本质不是引用,只是代表了对象父类型特征的那部分)

如何在子类中在使用父类方法的基础上进行方法覆盖?

按正常方法覆盖,但是方法体中先写一个super.方法名()调用一下父类的方法,再写需要添加的内容。

在子类中调用父类的构造方法

在子类的构造方法中使用super(参数);

通过此方法可以给继承过来的父类特征进行初始化,达到代码复用。

final 关键字

  • final修饰的类不可以被继承
  • final修饰的方法无法被覆盖
  • final修饰的变量一旦赋值,不能重新赋值
  • final修饰的实例变量必须在构造方法执行完之前手动赋值。这种变量一般和static联用,得到常量(单词全部大写,每个单词用_连接)
  • final修饰的引用一旦指向某个对象,不能再指向其他对象。但指向的对象内部的数据可以修改。

接口

要想解耦合,就是多态+接口

接口在Java中表示一种规范或契约,它定义了一组抽象方法和常量,用来描述一些实现这个接口的类应该具有哪些行为和属性。接口和类一样,也是一种引用数据类型。

分类

  1. 普通接口
  2. 起标志的作用

如何定义

[修饰符列表] interface 接口名{}

接口是完全抽象的

抽象类是半抽象的(可以定义抽象的方法,也可以定义非抽象的方法)

接口是完全抽象的,没有构造方法,也无法实例化。

JDK8之前的语法规则

接口中只能定义:常量+抽象方法

接口中的常量的static final可以省略,抽象方法的abstract可以省略。

所有方法和变量都是public的

接口与接口之间可以多继承

类和接口的关系——实现

这里的实现可以等同看做继承。(接口是父,类是子) 这个说法仅供理解

使用implements关键字进行接口的实现。

一个非抽象的类实现接口必须将接口中所有抽象方法全部实现(否则编译器报错)

一个类可以实现多个接口

class 类名 implements 接口A,接口B{ }

使用了接口之后,为了降低程序的耦合度,一定要让接口和多态联合起来使用

父类型的引用指向子类型的对象。

JDK8后,接口中允许出现默认方法和静态方法

默认方法

引入默认方法是为了演变接口演变问题。

接口可以定义抽象方法,但不能实现这些方法。所有实现接口的类都必须实现这些抽象方法,这会导致接口升级问题——当我们向接口添加或删除一个抽象方法时,这会破坏该接口的所有实现,所有与它有关的类都需要修改代码。

覆盖会使用静态方法

只能通过接口名去调用

通常将接口作为工具使用时,会使用静态方法

JDK9之后允许定义私有实例方法(为默认方法服务)和私有静态方法(为静态方法服务)

私有静态方法便于将静态方法拆分为多个方法,免得一个方法中有几千行。便于代码复用

所有接口隐式继承object,因此接口也可以调用object类的相关方法

接口的作用

  • 调用者和实现者通过接口达到了解耦合。调用者不需要关心具体的实现者,实现者也不许要关心具体的调用者,双方只要遵循规范,面向接口进行开发。
  • 面向抽象编程,面向接口编程,可以降低程序的耦合度,提高程序的扩展力。

接口和抽象类的选择

  • 抽象类主要用于公共代码的提取。多个类有共同的属性和方法时,提取出一个父类。
  • 接口主要用于功能的扩展。有一些类需要实现某个方法,另一些类不需要,那就将这个方法定义到接口中,需要这个方法的就去实现这个接口。

一个类单继承父类,多实现接口

extends在前,implements在后

UML 统一建模语言

image-20250423171427415

类之间的六种关系

image-20250423180648373

聚合关系:整体和部分各自有自己的生命周期

组合关系:整体和部分有相同的生命周期。eg.人死了,四肢也没了。

三个比较重要的关系

l

image-20250423174945893

其他的关系

聚合关系

image-20250423175943893

组合关系

image-20250423180306838

依赖关系

image-20250423180526190

image-20250423180542254

访问控制权限

image-20250423202738814

  • 类的访问权限只有两种:public和缺省
  • 访问权限控制符不能修饰局部变量

Object类

toString :将java对象转换成字符串型

但默认的方法输出的是地址,因此需要自己覆盖方法。

调用print()打印时,会自动调用类的toString()(和自己调用toString方法有区别,因为它会先判断是否是NULL,再使用toString)

equal :判断两个对象是否相等

默认方法是判断地址是否相等,也需要自己重写方法。

hashCode :返回一个对象的哈希值

通常用来在哈希表中查找该对象的键值。

默认实现是根据对象的内存地址生成一个哈希码(将对象的内存地址转换为整数作为哈希值)。

该方法是为了HashMap、Hashtable、HashSet等集合类进行优化而设置的,便于更快地查找和存储对象。

clone

  • 实现对象拷贝。通常在开发中需要保护原对象数据结构,于是克隆出一份新对象,对新对象进行操作。
  • 默认实现:是protected类型,专门给子类使用的。(本地方法,调用C++程序实现的)【浅克隆】
  • 怎么解决克隆方法调用问题?—— 在子类中重写clone方法,并且为了保证clone方法在任何位置都可以调用,建议将其修饰符修改为public
  • 凡是参加克隆的对象,必须实现一个标志接口:java.lang.Cloneable

需要重写变成【深克隆】

就是先完成浅克隆,再单独克隆其中包含的类,然后赋值给克隆出来的东西。

eg.

image-20250425160124895

内部类

什么是内部类

定义在一个类中的类

什么时候使用内部类

image-20250425161313968

四种内部类

image-20250425161340502

匿名内部类:

image-20250425171754060