第五章:面向对象编程
1. 面向对象概述
软件开发方法:面向过程和面向对象
面向过程:关注点在实现功能的步骤上
- PO:Procedure Oriented。代表语言:C语言
- 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
- 例如开汽车:启动、踩离合、挂挡、松离合、踩油门、车走了。
- 对于简单的流程是适合使用面向过程的方式进行的。复杂的流程不适合使用面向过程的开发方式。
面向对象:关注点在实现功能需要哪些对象的参与
- OO:Object Oriented 面向对象。包括OOA,OOD,OOP。OOA:Object Oriented Analysis 面向对象分析。OOD:Object Oriented Design 面向对象设计。OOP:Object Oriented Programming 面向对象编程。代表语言:Java、C#、Python等。
- 面向对象就是分析出解决这个问题都需要哪些对象的参加,然后让对象与对象之间协作起来形成一个系统。
- 例如开汽车:汽车对象、司机对象。司机对象有一个驾驶的行为。司机对象驾驶汽车对象。
- 面向对象开发方式耦合度低,扩展能力强。例如采用面向过程生产一台电脑,不会分CPU、内存和硬盘,它会按照电脑的工作流程一次成型。采用面向对象生产一台电脑,CPU是一个对象,内存条是一个对象,硬盘是一个对象,如果觉得硬盘容量小,后期是很容易更换的,这就是扩展性。
面向对象三大特征
- 封装(Encapsulation)
- 继承(Inheritance)
- 多态(Polymorphism)
2. 类与对象
类:
- 现实生活中事物与事物之间具有共同特征
- 类实际上是一个模板 ,是一个抽象的概念
- 状态在程序中对应属性。属性通常用变量来表示
- 行为在程序中对应方法。用方法来描述行为动作。
- 类 = 属性 + 方法
对象:
- 实际存在的个体
- 对象又称实例(instance)
- 通过类可以实例化对象
类的定义:
定义类的语法格式:
1
2
3
4
5
6
7[修饰符列表] class 类名 {
类体 = 属性 + 方法;
// 属性 (实例变量) , 描述的是状态
// 方法 ,描述的是行为动作
}例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14package com.north.oop01;
/**
* @author Stone
* @date 2024/1/25$
*/
public class Student {
String name;
int age;
boolean gender;
}为什么要定义类?
- 因为要通过类实例化对象。有了对象,让对象之间写作起来形成系统
- 一个类可以实例化多java对象(通过一个类可以造出多个java对象)
- 实例变量是一个对象一份,比如创建3个学生对象,每个学生对象中应该有name变量
实例变量属于成员变量 ,成员变量如果没有手动赋值,系统会赋默认值
1
2
3
4
5
6
7
8
9
10
11数据类型 默认值
----------------------
byte 0
short 0
int 0
long 0L
float 0.0F
double 0.0
boolean false
char \u0000
引用数据类型 null
对象的创建和使用:
以下列程序为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30package com.north.oop01;
/**
* @author Stone
* @date 2024/1/25$
*/
public class StudentTest01 {
public static void main(String[] args) {
// 创建对象
Student s1 = new Student();
// 访问对象的属性
System.out.println("姓名:" + s1.name);
System.out.println("年龄:" + s1.age);
System.out.println("性别:" + (s1.gender ? "男" : "女"));
Student s2 = new Student();
// 对属性进行操作
s2.name = "陈平安";
s2.age = 19;
s2.gender = true;
System.out.println("-----------------------------------------");
System.out.println("姓名:" + s2.name);
System.out.println("年龄:" + s2.age);
System.out.println("性别:" + (s2.gender ? "男" : "女"));
}
}对象的创建
- Student s1 ; 是什么?s1是变量名。Student 是一种数据类型名。属于引用数据类型
- s1 也是局部变量
- s1 变量中保存的是堆内存中Student 对象的内存地址
- 其中 , s1有一个特殊的称呼:引用
- 什么是引用?引用的本质是一个变量 ,这个变量中保存了java对象的内存地址
- 引用和对象要区分开。对象在JVM堆中。引用是保存对象地址的变量
对象的使用
- 读取属性值:s2.name
- 修改属性值:s2.name = “徐凤年”;
通过一个类可以实例化多个对象 , 如:Student类
1
2Student s1 = new Student();
Student s2 = new Student();上述代码中的 name 和 age 为什么不能使用
类名.去访问- 实例变量要想访问 ,必须先new 对象。通过引用来访问实例变量
- 实例变量是不能通过类名直接访问的
3. 对象的内存分析(对象与引用)
- new运算符会在JVM的堆内存中分配空间用来存储实例变量。new分配的空间就是Java对象。
- 在JVM中对象创建后会有对应的内存地址,将内存地址赋值给一个变量,这个变量被称为引用。
- Java中的GC主要针对的是JVM的堆内存。
空指针异常是如何发生的?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class PetTest02 {
public static void main(String[] args) {
// 创建宠物对象
Pet dog = new Pet();
// 给属性赋值
dog.name = "小黑";
dog.birth = "2012-10-11";
dog.sex = '雄';
// 读取属性的值
System.out.println("狗狗的名字:" + dog.name);
System.out.println("狗狗的生日:" + dog.birth);
System.out.println("狗狗的性别:" + dog.sex);
dog = null;
// 注意:引用一旦为null,表示引用不再指向对象了。但是通过引用访问name属性,编译可以通过。
// 运行时会出现异常:空指针异常。NullPointerException。这是一个非常著名的异常。
// 为什么会出现空指针异常?因为运行的时候会找真正的对象,如果对象不存在了,就会出现这个异常。
//System.out.println("狗狗的名字:" + dog.name);
// 会出现空指针异常。
dog.eat();
// 会出现空指针异常。
//dog.run();
}
}- 方法调用时参数是如何传递的?将变量中保存的值复制一份传递过去。
- 初次认识this关键字:出现在实例方法中,代表当前对象。“this.”大部分情况下可以省略。this存储在实例方法栈帧的局部变量表的0号槽位上。
4. 封装
什么是封装
- 封装是将一个对象所有==状态(属性)== , 以及 ==行为(方法)== 统一封装到一个类中 ,从而隐藏了对象内部的具体实现细节,向外界提供了有限的访问接口 ,以实现对对象的保护和隔离
封装的好处
- 封装通过限制外部对对象内部的直接访问和修改,保证了数据的安全性 ,并提高了代码的可维护性和可复用性
在代码上如何实现封装
- 属性私有化, 对外提供getter和setter方法
实现封装的步骤:
第一步:属性私有化(什么是私有化?使用private 进行修饰)
- 属性私有化的作用是:禁止外部程序对该属性进行随意访问
- 所有被private修饰的 ,都是私有的 ,私有的只能在本类中访问
第二步:对外提供setter和getter方法
访问一般包括两种:
- 读:读属性的值 , 读的方法格式:getter ,
getter方法是绝对安全的。因为这个方法是读取属性的值,不会涉及修改操作。 改:修改属性的值 , 改的方法格式:setter ,
setter方法当中就需要编写拦截过滤代码,来保证属性的安全。1
2
3
4
5
6
7
8
9public void setAge(int age){
if(age < 0 || age > 100) {
System.out.println("对不起,您的年龄值不合法!");
return;
}
// this. 大部分情况下可以省略。
// this. 什么时候不能省略?用来区分局部变量和实例变量的时候。
this.age = age;
}
- 读:读属性的值 , 读的方法格式:getter ,
5. 构造方法Constructor(构造器)
构造方法有什么作用
- 作用1:对象的创建(通过调用构造方法可以完成对象的创建)
- 作用2:对象的初始化(给对象的所有属性赋值就是对象的初始化)
怎么定义构造方法呢?
1 | [修饰符列表] 构造方法名(形参列表) { |
注意:
- 构造方法名必须和类名一致。
- 构造方法不需要提供返回值类型。
- 如果提供了返回值类型,那么这个方法就不是构造方法了 ,就变成普通方法了。
构造方法怎么调用呢?
- 使用new 运算符来调用
- 语法:new 构造方法名(实参)
- 注意:构造方法最终执行结束之后,会自动将创建的对象内存地址返回。但构造方法体中不需要提供 =="return 值"== ;这样的语句
缺省构造器
- 在java语言中,如果一个类没有显示的去定义构造方法 , 系统会默认提供一个无参的构造方法。(通常把这个构造方法叫做缺省构造器)
构造方法中给属性赋值了,为什么还需要单独定义set方法给属性赋值呢?
- 在构造方法中赋值是对象第一次创建时属性赋的值。set方法可以在后期的时候调用,来完成属性值的修改
构造方法的执行原理
构造方法执行包括两个重要的阶段
- 第一阶段:对象的创建
- 第二阶段:对象的初始化
对象在什么时候创建的?
- new 的时候 ,会直接在堆内存中开辟空间。然后给所有属性赋默认值,完成对象的创建。(==这个过程是在构造方法体执行之前就完成了==)
对象初始化在什么时候完成的?
- 构造方法体开始执行,标志着开始进行对象初始化。构造方法体执行完毕,表示对象初始化完成。
构造代码块?
语法格式:
{ }代码演示:
1
2
3
4
5
6
7
8
9// 构造代码块
{
//System.out.println("构造代码块执行!");
// 这里能够使用this,这说明,构造代码块执行之前对象已经创建好了,并且系统也完成了默认赋值。
//System.out.println(this.name);
for(int i = 0; i < 10; i++){
System.out.println("iiiiiiiiiii = " + i);
}
}构造代码块什么时候执行,执行几次?
- 每一次在new的时候,都会先执行一次构造代码块
- 构造代码块是在构造方法之前执行的
6. this 关键字
this关键字的简单介绍
- this 是一个关键字
- this 出现在实例方法中,代表当前对象。语法是:this
- 通过this 可以访问实例变量 ,可以调用实例方法
- this 大部分情况下可以省略 ,用于区分局部变量和实例变量时不能省略
- this 不能出现在静态方法中
this(实参) 语法:
只能出现在构造方法的第一行- 通过当前构造方法调用本类中其他的构造方法
- 作用是:代码复用
- this 本质上是一个引用
- this 中保存的也是对象的内存地址
- this 中保存的是当前对象的内存地址
7. static 关键字
static 关键字简单介绍
- static 翻译为静态的
- static 修饰的变量 ,静态变量
- static 修饰的方法 ,静态方法
- 所有static修饰的,访问的时候,直接采用”类名.” , 不需要new 对象
什么情况下把成员变量定义为静态成员变量?
- 当一个属性是对象级别的 ,这个属性通常定义为实例变量。(实例变量时一个对象一份。100个对象就应该有100个空间)
- 当一个属性是类级别的(所有对象都有这个属性) ,并且这个属性的值是一样的 ,建议将其定义为静态变量 ,在内存空间上只有一份 ,节省内存开始,这种类级别的属性不需要new对象,直接通过类名访问
静态变量存储在哪里?静态变量在什么时候初始化?(什么时候开辟空间)
- JDK8 之后 ,静态变量存储在堆内存中
- 类加载时初始化
静态变量可以采用"引用." 来访问吗?
- 可以(但不建议:会给程序员造成困惑,程序员会认为country是一个实例变量。)
- 建议还是使用“类名.”来访问。这是正规的。
什么时候会出现空指针异常
- 一个空引用访问实例相关的,都会出现空指针异常。
静态代码块
语法格式:
1
2
3static{
}静态代码块什么时候执行?执行几次?
- 静态代码块在类加载时执行 ,并且只执行一次
- 静态代码块可以编写多个 ,并且遵循自上而下的顺序依次执行
静态代码块什么时候使用?
- 本质上,静态代码块就是为程序员预留的一个特殊的时间点:类加载时刻
- 如果你需要再类加载时刻执行一段程序的话,这段代码就可以写到静态代码块当中。
- 例如,有这样一个需求:请在类加载时,记录日志。那么记录日志的代码就可以编写到静态代码块当中。
8. JVM体系结构
JVM对应了一套规范(Java虚拟机规范),它可以有不同的实现
JVM规范是一种抽象的概念,它可以有多种不同的实现。例如:
- HotSpot:HotSpot 由 Oracle 公司开发,是目前最常用的虚拟机实现,也是默认的 Java 虚拟机,默认包含在 Oracle JDK 和 OpenJDK 中
- JRockit:JRockit 也是由 Oracle 公司开发。它是一款针对生产环境优化的 JVM 实现,能够提供高性能和可伸缩性
- IBM JDK:IBM JDK 是 IBM 公司开发的 Java 环境,采用了与 HotSpot 不同的 J9 VM,能够提供更小的内存占用和更迅速的启动时间
- Azul Zing:Azul Zing 是针对生产环境优化的虚拟机实现,能够提供高性能和实时处理能力,适合于高负载的企业应用和实时分析等场景
- OpenJ9:OpenJ9 是由 IBM 开发的优化的 Java 虚拟机实现,支持高度轻量级、低时延的 GC、优化的 JIT 编译器和用于健康度测试的可观察性仪表板
右图是从oracle官网上截取的Java虚拟机规范中的一部分。(大家也可以找一下oracle官方文档)
我们主要研究运行时数据区。运行时数据区包括6部分:
- The pc Register(程序计数器)
- Java Virtual Machine Stacks(Java虚拟机栈)
- Heap(堆)
- Method Area(方法区)
- Run-Time Constant Pool(运行时常量池)
- Native Method Stacks(本地方法栈)
JVM规范中的运行时数据区
- The pc Register(程序计数器):是一块较小的内存空间,此计数器记录的是正在执行的虚拟机字节码指令的地址;
- Java Virtual Machine Stacks(Java虚拟机栈):Java虚拟机栈用于存储栈帧。栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- Heap(堆):是Java虚拟机所管理的最大的一块内存。堆内存用于存放Java对象实例以及数组。堆是垃圾收集器收集垃圾的主要区域。
- Method Area(方法区):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- Run-Time Constant Pool(运行时常量池):是方法区的一部分,用于存放编译期生成的各种字面量与符号引用。
- Native Method Stacks(本地方法栈):在本地方法的执行过程中,会使用到本地方法栈。和 Java 虚拟机栈十分相似。
- ==总结:这些运行时数据区虽然在功能上有所区别,但在整个 Java 虚拟机启动时都需要被创建,并且在虚拟机运行期间始终存在,直到虚拟机停止运行时被销毁。同时,不同的 JVM 实现对运行时数据区的分配和管理方式也可能不同,会对性能和功能产生影响。==
JVM体系结构图(该图属于JVM规范,不是具体的实现)
JVM规范的实现:HotSpot(Oracle JDK/Open JDK内部使用的JVM就是HotSpot)
以下是JDK6的HotSpot
- 年轻代:刚new出来的对象放在这里。
- 老年代:经过垃圾回收之后仍然存活的对象。
- 符号引用:类全名,字段全名,方法全名等。
- 这个时期的永久代和堆是相邻的,使用连续的物理内存,但是内存空间是隔离的。
- 永久代的垃圾收集是和老年代捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。
以下是JDK7的HotSpot,这是一个过渡的版本,该版本相对于JDK6来说,变化如下:
- 类的静态变量转移到堆中了
- 字符串常量池转移到堆中了
- 运行时常量池中的符号引用转移到本地内存了
以下是JDK8及更高版本的HotSpot,相对于JDK7来说发生了如下变化:
- 彻底删除永久代(为了避免OOM错误的发生)
- 将方法区的实现转移到本地内存
- 将符号引用重新放回运行时常量池
9. 单例模式
设计模式概述
什么是设计模式?
- 设计模式(Design Pattern)是一套被广泛接受的、经过试验验证的、可反复使用的基于面向对象的软件设计经验总结,它是软件开发人员在软件设计中,对常见问题的解决方案的总结和抽象。设计模式是针对软件开发中常见问题和模式的通用解决方案
设计模式有哪些?
- GoF设计模式:《Design Patterns: Elements of Reusable Object-Oriented Software》(即后述《设计模式》一书),由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为四人组(Gang of Four)。
- 架构设计模式(Architectural Pattern):主要用于软件系统的整体架构设计,包括多层架构、MVC架构、微服务架构、REST架构和大数据架构等。
- 企业级设计模式(Enterprise Pattern):主要用于企业级应用程序设计,包括基于服务的架构(SOA)、企业集成模式(EIP)、业务流程建模(BPM)和企业规则引擎(BRE)等。
- 领域驱动设计模式(Domain Driven Design Pattern):主要用于领域建模和开发,包括聚合、实体、值对象、领域事件和领域服务等。
- 并发设计模式(Concurrency Pattern):主要用于处理并发性问题,包括互斥、线程池、管道、多线程算法和Actor模型等。
- 数据访问模式(Data Access Pattern):主要用于处理数据访问层次结构,包括数据访问对象(DAO)、仓库模式和活动记录模式等。
GoF设计模式的分类?
- 创建型:主要解决对象的创建问题
- 结构型:通过设计和构建对象之间的关系,以达到更好的重用性、扩展性和灵活性
- 行为型:主要用于处理对象之间的算法和责任分配
单例模式:
饿汉式:类加载时对象就创建好了。不管这个对象用还是不用。提前先把对象创建好
实现步骤:
- 第一步:构造方法私有化
- 第二步:对外提供一个公开的静态的方法 ,用这个方法获取单个实例
- 第三步:定义一个静态变量 ,在类加载的时候 ,初始化静态变量(只初始化一次)
代码演示:
1
2
3
4
5
6
7
8
9
10
11public class Singleton {
private static Singleton s = new Singleton();
private Singleton(){}
// 实例方法
public static Singleton get(){
return s;
}
}
懒汉式:用到这个对象的时候再创建对象 ,别在类加载的时候创建对象
实现步骤:
- 第一步:构造方法私有化
- 第二步:对外提供一个静态方法 ,通过这个方法可以获取到 Singleton 对象
- 第三步:提供一个静态变量 ,但是这个变量值为 null
代码演示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Singleton {
private static Singleton s;
private Singleton(){}
public static Singleton get(){
if (s == null) {
s = new Singleton();
System.out.println("对象创建了");
}
return s;
}
}
10. 继承
继承的简单介绍:
- 面向对象的三大特征之一:继承
- java只支持单继承 ,一个类只能直接继承一个类
- java不支持多继承 ,但支持多重继承(多层继承)
- 子类继承父类后,除私有的不支持继承 ,构造方法不支持继承 , 其他的全部会继承
- 一个类没有显示继承任何类时 ,默认继承==java.lang.Object==
继承的作用
- 基本作用:代码复用
- 重要作用:有了继承 ,才有了方法覆盖(方法重写)和多态机制
继承在java中如何实现
- [修饰符列表] class 类名 extends 父类名() { }
- extends 翻译为扩展 ,表示子类继承父类后 ,子类是对父类的扩展
继承相关的术语:当B类继承A类时
- A类称为:父类 ,超类 ,基类 , superclass
- B类称为:子类 ,派生类 ,subclass
11. 方法覆盖
回顾方法重载 overload
什么时候考虑使用方法重载
- 在一个类中,如果功能相似,可以考虑使用方法重载。
- 这样做的目的是:代码美观,方便编程。
当满足什么条件的时候构成方法重载
- 条件1:在同一个类中。
- 条件2:相同的方法名。
- 条件3:不同的参数列表:类型,个数,顺序
方法重载机制属于编译阶段的功能
- 方法重载机制是给编译器看的。
方法覆盖/override/方法重写/overwrite
什么时候考虑使用方法重写?
- 当从父类中继承过来的方法,无法满足子类的业务需求时。
当满足什么条件的时候 ,构成方法重写?
- 条件1:方法覆盖发生在具有继承关系的父子类之间。
- 条件2:具有相同的方法名(必须严格一样)
- 条件3:具有相同的形参列表(必须严格一样)
- 条件4:具有相同的返回值类型(可以是子类型)
关于方法覆盖的细节
- 当子类将父类方法覆盖之后,将来子类对象调用方法的时候,一定会执行重写之后的方法。
在java语言中,有一个注解,这个注解可以在编译阶段检查这个方法是否是重写了父类的方法。
- @Override注解是JDK5引入,用来标注方法,被标注的方法必须是重写父类的方法,如果不是重写的方法,编译器会报错。
- @Override注解只在编译阶段有用,和运行期无关。
- 如果返回值类型是引用数据类型,那么这个返回值类型可以是原类型的子类型
- 访问权限不能变低,可以变高。
- 抛出异常不能变多,可以变少。(后面学习异常的时候再说。)
- 私有的方法,以及构造方法不能继承,因此他们不存在方法覆盖。
- 方法覆盖针对的是实例方法。和静态方法无关。(讲完多态再说。)
- 方法覆盖针对的是实例方法。和实例变量没有关系。
代码演示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class Bird extends Animal{
/**
* Bird对继承过来的move()方法不满意。
* Bird类有权利将move()方法进行重写/覆盖。
*/
public void move(){
System.out.println("鸟儿在飞翔!");
}
public String getObj(long a, String b){
return "";
}
}
12. 多态
关于基本数据类型之间的类型转换
第一种:小容量转换成大容量,叫做自动类型转换。
如:
1
2int i = 100;
long x = i;
第二种:大容量转换成小容量,不能自动转换,必须添加强制类型转换符才行。叫做强制类型转换。
1
int y = (int)x;
除了基本数据类型之间的类型转换之外,对于引用数据类型来说,也可以进行类型转换。
只不过不叫做自动类型转换和强制类型转换。我们一般称为向上转型和向下转型。
关于Java语言中的向上转型和向下转型:
- 向上转型(upcasting):子 —-> 父 (可以等同看做自动类型转换。)
- 向下转型(downcasting):父 —-> 子 (可以等同看做强制类型转换。)
- 注意:
不管是向上还是向下转型,两种类型之间必须要有继承关系,编译器才能编译通过。这是最基本的大前提。
向上转型和向下转型
- 多态的向上转型 : 编译看左边 ,运行看右边
其中在多态的向上转型中使用对象去调用方法时只能调用子类从父类那里继承的方法或者时重写的方法 ,而不能调用子类自己特有的方法。比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 在这个程序中 ,所创建的对象可以去调用从父类那里继承下来的方法,而不能调用scratch()方法。除非使用向下转型
public class Cat extends Animal{
// 写一下Cat自己的行为
public void move() {
System.out.println("猫在爬树");
}
public void eat() {
System.out.println("猫在吃饭");
}
public void scratch() {
System.out.println("猫抓老鼠");
}
}向下转型的使用前提是要先存在向上转型 ,利用强转使向上转换时穿件的对象进项创建新的对象来调用子类特有的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class Application {
public static void main(String[] args) {
Cat cat = new Cat();
cat.run();
System.out.println("---------------------------------");
// 向上转型
Animal tom = new Cat();
tom.eat();
tom.move();
tom.run();
System.out.println("---------------------------------");
// 多态 —— 向下转型
Cat jerry = (Cat) tom;
jerry.scratch();
}
}
向上转型(upcasting):
- 子 —> 父
- 也可以等同看做自动类型转换
- 前提:两种类型之间要有继承关系
- 父类型引用指向子类型对象。这个就是多态机制最核心的语法。
java程序包括两个重要的阶段:
第一阶段:编译阶段
- 在编译的时候,编译器只知道a2的类型是Animal类型。因此在编译的时候就会去Animal类中找move()方法。找到之后,绑定上去,此时发生静态绑定。能够绑定成功,表示编译通过。
第二阶段:运行阶段
- 在运行的时候,堆内存中真实的java对象是Cat类型。所以move()的行为一定是Cat对象发生的。因此运行的时候就会自动调用Cat对象的move()方法。这种绑定称为运行期绑定/动态绑定。
因为编译阶段是一种形态,运行的时候是另一种形态。因此得名:多态。
instanceof 运算符的语法规则:
- instanceof运算符的结果一定是:true/false
- 语法格式:==(引用 instanceof 类型)==
例如:
1
2
3
4
5(a instanceof Cat)
true表示什么?
a引用指向的对象是Cat类型。
false表示什么?
a引用指向的对象不是Cat类型。注意:==做向下转型之前,为了避免ClassCastException的发生,一般建议使用instanceof进行判==
软件开发七大原则
软件开发原则旨在引导软件行业的从业者在代码设计和开发过程中,遵循一些基本原则,以达到高质量、易维护、易扩展、安全性强等目标。软件开发原则与具体的编程语言无关的,属于软件设计方面的知识。
软件开发七大原则?
- ==开闭原则 (Open-Closed Principle,OCP):一个软件实体应该对扩展开放,对修改关闭。即在不修改原有代码的基础上,通过添加新的代码来扩展功能。(最基本的原则,其它原则都是为这个原则服务的。)==
- 单一职责原则:一个类只负责单一的职责,也就是一个类只有一个引起它变化的原因。
- 里氏替换原则:子类对象可以替换其基类对象出现的任何地方,并且保证原有程序的正确性。
- 接口隔离原则:客户端不应该依赖它不需要的接口。
- 依赖倒置原则:高层模块不应该依赖底层模块,它们都应该依赖于抽象接口。换言之,面向接口编程。
- 迪米特法则:一个对象应该对其它对象保持最少的了解。即一个类应该对自己需要耦合或调用的类知道得最少。
- 合成复用原则:尽量使用对象组合和聚合,而不是继承来达到复用的目的。组合和聚合可以在获取外部对象的方法中被调用,是一种运行时关联,而继承则是一种编译时关联。
多态在开发中的作用
- 降低程序的耦合度 ,提高程序的扩展能力
- 尽量使用多态 ,面向抽象编程 ,不要面向具体编程
代码演示:
应用类
1
2
3
4
5
6
7
8
9
10
11
12/**
* @Author North
* @Date 2024/2/4
*/
public class Application {
public static void main(String[] args) {
Master lisi = new Master();
lisi.feed(new Cat());
}
}宠物类
1
2
3
4
5
6
7
8
9
10/**
* @Author North
* @Date 2024/2/4
*/
public class Pet {
public void eat() {
}
}猫类
1
2
3
4
5
6
7
8
9
10
11/**
* @Author North
* @Date 2024/2/4
*/
public class Cat extends Pet{
public void eat() {
System.out.println("猫吃鱼");
}
}狗类
1
2
3
4
5
6
7
8
9
10
11/**
* @Author North
* @Date 2024/2/4
*/
public class Dog extends Pet{
public void eat() {
System.out.println("狗啃骨头");
}
}主人类
1
2
3
4
5
6
7
8
9
10/**
* @Author North
* @Date 2024/2/4
*/
public class Master {
public void feed(Pet pet) {
pet.eat();
}
}
13. super关键字
super关键字介绍
- this代表的是当前对象 ,super代表的是当前对象中的父类型特征
- super 不能使用在静态上下文中 ,同时this也是
super大部分情况下是可以省略的,==其中不能省略的情况为:==
- 当父类和子类中定义了相同的属性(实例变量)或者相同方法(实例方法)时,如果需要在子类中访问父类的属性或方法时,super.不能省略。
- this可以单独输出,super不能单独输出。
- super(实参); 通过子类的构造方法调用父类的构造方法,==目的是为了完成父类型特征的初始化。==
- 当一个构造方法第一行没有显示的调用“super(实参);”,也没有显示的调用“this(实参)”,系统会自动调用super()。因此一个类中的无参数构造方法建议显示的定义出来。
super(实参); 这个语法只能出现在构造方法第一行。- 在Java语言中只要new对象,Object的无参数构造方法一定会执行。
部分代码演示:
1 |
|
14. final关键字
final关键字介绍
- final表示最终的,不可变的。
- final修饰的类无法被继承。
- final修饰的变量一旦赋值,不能重新赋值。
- final修饰的方法无法覆盖。
- final修饰的实例变量。必须在构造方法执行完之前手动赋上值。(不允许采用系统默认值) 一般不存在这种情况。
- final修饰的实例变量一般和static联合使用,这就是著名的:常量。
- final修饰的引用:一旦指向了某个对象,则不能再指向其它对象。但指向的对象内部的数据是可以修改的。
常量的命名:
1 | public static final double MATH_PAI = 3.1415926; |
- 常量的定义:==public static final 数据类型 常量名 = 常量值;==
- 常量名的命名规范:全部单词大写,每个单词采用“_”衔接。
15. 抽象类
什么时候考虑将类定义为抽象类?
如果类中有些方法无法实现或者没有意义,可以将方法定义为抽象方法。类定义为抽象类。这样在抽象类中只提供公共代码,具体的实现强行交给子类去做。- 比如一个Person类有一个问候的方法greet(),但是不同国家的人问候的方式不同,因此greet()方法具体实现应该交给子类。再比如主人喂养宠物的例子中的宠物Pet,Pet中的eat()方法的方法体就是没有意义的。
抽象类如何定义?
1 | abstract class 类名{ |
抽象类有构造方法,但无法实例化。抽象类的构造方法是给子类使用的。
抽象方法如何定义?
1 | abstract 方法返回值类型 方法名(形参); |
抽象类中不一定有抽象方法,但如果有抽象方法那么类要求必须是抽象类。
一个非抽象的类继承抽象类,要求必须将抽象方法进行实现/重写。
abstract关键字不能和private,final,static关键字共存。
16. 接口
接口的简单介绍
- 接口(interface)在Java中表示一种规范或契约,它定义了一组抽象方法和常量,用来描述一些实现这个接口的类应该具有哪些行为和属性。
- 接口和类一样,也是一种引用数据类型
- 接口怎么定义?[修饰符列表] interface 接口名{}
- 抽象类是半抽象的,接口是完全抽象的。接口没有构造方法,也无法实例化
- (JDK8之前的语法规则) 接口中只能定义:常量+抽象方法。接口中的常量的static final可以省略。接口中的抽象方法的abstract可以省略。接口中所有的方法和变量都是public修饰的
- 接口和接口之间可以多继承
- 类和接口的关系我们叫做实现(这里的实现也可以等同看做继承)。使用implements关键字进行接口的实现。
- 一个非抽象的类实现接口必须将接口中所有的抽象方法全部实现(强制要求的,必须的,要不然编译器报错。)
- 一个类可以实现多个接口。语法是:class 类 implements 接口A,接口B{}
Java8之后,接口中允许出现默认方法和静态方法(JDK8新特性)
默认方法:
- 引入默认方式是为了解决接口演变问题:接口可以定义抽象方法,但是不能实现这些方法。
- 所有实现接口的类都必须实现这些抽象方法。这会导致接口升级的问题:当我们向接口添加或删除一个抽象方法时,
- 这会破坏该接口的所有实现,并且所有该接口的用户都必须修改其代码才能适应更改。这就是所谓的”接口演变”问题
静态方法:
- 注意:java中规定,在JDK8之后,接口中可以一定静态方法,但是这个静态方法,只能通过“该接口名”去调用的。别的都无法调用。
- 在JDK8之后引入接口可以定义静态方法,实际上想表达一个意思:接口也可以作为工具来使用了。
- JDK9之后允许接口中定义私有的实例方法(为默认方法服务的)和私有的静态方法(为静态方法服务的)
- 所有的接口隐式的继承Object。因此接口也可以调用Object类的相关方法
接口的作用
- 面向接口调用的称为:接口调用者
- 面向接口实现的称为:接口实现者
- 调用者和实现者通过接口达到了解耦合。也就是说调用者不需要关心具体的实现者,实现者也不需要关心具体的调用者,双方都遵循规范,面向接口进行开发。
- ==面向抽象编程,面向接口编程,可以降低程序的耦合度,提高程序的扩展力。==
例如定义一个Usb接口,提供read()和write()方法,通过read()方法读,通过write()方法写:
- 定义一个电脑类Computer,它是调用者,面向Usb接口来调用。
Usb接口的实现可以有很多,例如:打印机(Printer),硬盘(HardDrive)。
1
2
3
4
5
6
7
8public class Computer{
public void conn(Usb usb){
usb.read();
usb.write();
}
}
再想想,我们平时去饭店吃饭,这个场景中有没有接口呢?食谱菜单就是接口。顾客是调用者。厨师是实现者。
接口与抽象类如何选择
抽象类和接口虽然在代码角度都能达到同样的效果,但适用场景不同:
- 抽象类主要适用于公共代码的提取。当多个类中有共同的属性和方法时,为了达到代码的复用,建议为这几个类提取出来一个父类,在该父类中编写公共的代码。如果有一些方法无法在该类中实现,可以延迟到子类中实现。这样的类就应该使用抽象类。
- 接口主要用于功能的扩展。例如有很多类,一些类需要这个方法,另外一些类不需要这个方法时,可以将该方法定义到接口中。需要这个方法的类就去实现这个接口,不需要这个方法的就可以不实现这个接口。接口主要规定的是行为。
17. UML
18. 访问控制权限
- private:私有的,只能在本类中访问。
- 缺省:默认的,同一个包下可以访问。
- protected:受保护的,子类中可以访问。(受保护的通常就是给子孙用的。)
public:公共的,在任何位置都可以访问。
- 类中的属性和方法访问权限共有四种:private、缺省、protected和public。
- 类的访问权限只有两种:public和 缺省。
- 访问权限控制符不能修饰局部变量。
19. Object 类
java.lang.Object是所有类的超类。java中所有类都实现了这个类中的方法。
现阶段Object类中需要掌握的方法:
- toString:将java对象转换成字符串。
- equals:判断两个对象是否相等。
现阶段Object类中需要了解的方法:
- hashCode:返回一个对象的哈希值,通常作为在哈希表中查找该对象的键值。Object类的默认实现是根据对象的内存地址生成一个哈希码(即将对象的内存地址转换为整数作为哈希值)。hashCode()方法是为了HashMap、Hashtable、HashSet等集合类进行优化而设置的,以便更快地查找和存储对象。
- finalize:当java对象被回收时,由GC自动调用被回收对象的finalize方法,通常在该方法中完成销毁前的准备。
clone:对象的拷贝。(浅拷贝,深拷贝)
- protected修饰的只能在同一个包下或者子类中访问。
- 只有实现了Cloneable接口的对象才能被克隆。
Object类中的toString()方法
Object类设计toString()方法的目的是什么?
- 这个方法的作用是:将java对象转换成字符串的表示形式。
Object类中toString()方法的默认实现是怎样的?
- 默认实现是:完整类名 + @ + 十六进制的数字
这个输出结果可以等同看做一个java对象的内存地址
1
2
3public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
Object类中的equals方法:
Object类设计equals方法的作用是什么?目的是什么?
- equals方法的作用是:判断两个对象是否相等。
equals方法的返回值是true/false
- true代表两个对象相等。
- false代表两个对象不相等。
Object类中对equals方法的默认实现是怎样的?
a.equals(b) 表面是a和b的比较。实际上方法体当中是:this和obj的比较。
1
2
3public boolean equals(Object obj) {
return (this == obj);
}
关于 == 运算符的运算规则:
== 永远只有一个运算规则,永远比较的是变量中保存的值之间的比较。- 只不过有的时候这个值是基本数据类型。有的时候这个值是对象的内存地址。
equals方法为什么要重写?
- 因为Object类中的equals方法在进行比较的时候,比较的是两个java对象的内存地址。
- 我们希望比较的是对象的内容。只要对象的内容相等,则认为是相同的。
- 字符串的比较不能使用
==, 必须使用equals方法进行比较 - 字符串String类型已经重写了equals方法
关于Object类的hashCode()方法:
- hashCode:返回一个对象的哈希值,通常作为在哈希表中查找该对象的键值。
- Object类的默认实现是根据对象的内存地址生成一个哈希码(即将对象的内存地址转换为整数作为哈希值)。
- hashCode()方法是为了HashMap、Hashtable、HashSet等集合类进行优化而设置的,以便更快地查找和存储对象
hashCode()方法在Object类中的默认实现:
1
public native int hashCode();
- 这是一个本地方法,底层调用了C++写的动态链接库程序:xxx.dll
关于Object类中的clone()方法:
- clone方法作用:对象拷贝。通常在开发中需要保护原对象数据结构。通常复制一份,生成一个新对象,对新对象进行操作。
Object类中的默认实现:
1
2
3
4
5protected native Object clone() throws CloneNotSupportedException;
* 受保护的方法,专门给子类使用的。
* 本地方法。
* 底层调用C++程序已经可以完成对象的创建了。
* 我们现在要解决的问题是:怎么调用这个方法。怎么解决clone()方法的调用问题?
- 在子类中重写该clone()方法。
- 为了保证clone()方法在任何位置都可以调用,建议将其修饰符修改为:public
凡事参加克隆的对象,必须实现一个标志接口:java.lang.Cloneable
java中接口包括两大类:
- 一类是:起到标志的作用,标志型接口。
- 另一类是:普通接口。
内部类
什么是内部类?
- 定义在一个类中的类。
什么时候使用内部类?
- 一个类用到了另外一个类,而这两个类的联系比较密切,但是如果把这两个类定义为独立的类,不但增加了类的数量,也不利于代码的阅读和维护。
- 内部类可以访问外部类的私有成员,这样可以将相关的类和接口隐藏在外部类的内部,从而提高封装性。
- 匿名内部类是指没有名字的内部类,通常用于定义一个只使用一次的类,比如在事件处理中。
内部类包括哪几种?
静态内部类:和静态变量一个级别
- 静态内部类如何实例化:OuterClass.StaticInnerClass staticInnerClass = new OuterClass.StaticInnerClass();
- 无法直接访问外部类中实例变量和实例方法。
实例内部类:和实例变量一个级别
- 实例内部类如何实例化:OuterClass.InnerClass innerClass = new OuterClass().new InnerClass();
- 可以直接访问外部类中所有的实例变量,实例方法,静态变量,静态方法。
局部内部类:和局部变量一个级别
- 局部内部类方外类外部的局部变量时,局部变量需要被final修饰。
- 从JDK8开始,不需要手动添加final了,但JVM会自动添加。
匿名内部类:特殊的局部内部类,没有名字,只能用一次。
- 匿名内部类:特殊的局部内部类,没有名字,只能用一次。










