点击 Mr.绵羊的知识星球 解锁更多优质文章。
目录
一、介绍
1. 概念
面向对象是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物。
2. 特征
(1) 封装
a. 在面向对象程式设计当中,封装(Encapsulation)是指一种将抽象性函式接口实作细节部分包装、隐藏起来的方法。
b. 封装可以认为是一种保护屏障,防止该类的代码和数据被外部定义的代码随机访问。
c. 要访问该类的代码和数据,必须通过严格的接口控制。
d. 封装最主要的功能在于我们能修改自己实现的代码,而不用修改那些调用我们代码的程序片段。
e. 适当的封装可以让程式更容易维护和理解,加强了代码的安全性。
(2) 继承
多个类有相同特征和行为的时候就可以将相同的内容提取出来组成一个新类,让原来的类继承自新类就可以了从而实现原来这些类吸收新类中成员的效果,此时在原来这些类中需要编写自己独有的成员即可。(提高了代码的可复用性、可维护以及可拓展的机制)
注意事项:
a. 子类可以继承父类的成员变量.包括:私有成员变量,但不能直接访问(子类不可以继承父类中的构造方法以及私有的成员方法)
b. 构造子类对象时会自动调用父类中的无参构造方法,用来初始化从父类中继承下来的成员变量信息,相当于在子类构造方法中增加super();
c. Java只支持单继承,也就是一个子类只能有一个父类,但是一个父类可以有多个子类;
d. 只有满足:子类 is a 父类的逻辑关系才能用继承
(3) 多态
a. 语法格式:
父类类型 引用 = new 子类类型();
引用.方法();
b. 解析:编译时父类类型,调用的是父类方法。在运行阶段,指向的是子类对象,调用子类的方法
注意事项:在使用多态调用方法时,首先会检查父类中是否有该方法,如果没有会编译报错。如果有再去调用子类的同名方法(注意:静态static方法属于特殊情况,静态方法只能继承,不能写@override,如果子类中定义了同名同形式的静态方法,他对父类方法只能起到隐藏作用,调用的时候用谁的引用则调用的就调用谁的版本)
二、对象创建过程
1. 父类创建过程
(1) 将xxx.class文件中相关子类信息读取到内存空间的方法区,这个过程叫类的加载。
(2) 当程序开始运行时找到main方法去执行方法体中的语句,使用new来创建对象
(3) 若没有执行初始值采用默认初始化,否则采用执行的数值来作为初始化
(4) 可以通过构造块来更改来更改成员变量的数值
(5) 执行构造方法体中的语句可以再次修改成员变量的数值
(6) 此时对象创建完毕,继续执行后续的语句
2. 子类创建过程
(1) 先加载父类再去加载主类,先执行父类的静态语句块在执行子类的静态语句块
(2) 执行父类的构造块,在执行父类的构造方法体,此时父类部分构造完毕
(3) 执行子类的构造块,在执行子类的构造方法体,此时子类对象构造完毕
流程:父类静态语句块->子类静态语句块->父类构造方法->父类普通方法->子类构造方法->子类普通方法
如果您想了解父类和子类加载流程:Java类的加载及父类子类加载顺序
三、实际应用
1. 案例一
(1) 场景:
多种动物继承了Animal的属性和方法,在调用eat方法时,展示出不同动作,帮助更直观的了解面向对象的特征。
(2) 代码:git地址
a. 动物父类
/**
* Animal: 描述动物的类
*
* @author wxy
* @since 2023-01-01
*/
public class Animal {
/**
* 名称: 所有动物都有名称, 所以将name提取出来,
* 如此一来所有继承Animal的动物都有name这个属性
*/
private String name;
/**
* 动物(阿猫、阿狗...)都要吃东西, 所以提取出来(主要是为了演示多态的特性)
* 当然如果不需要吃东西, 或者这个eat方法满足你的需求, 那就不用重写这个方法
*/
public void eat() {
System.out.println("animal eat food");
}
/**
* setName
*
* @param name 写入的名称
*/
public void setName(String name) {
this.name = name;
}
/**
* getName
*
* @return 返回写入的名称
*/
public String getName() {
return name;
}
}
b. 三个动物子类
/**
* Dog: 描述狗的类
* 继承了动物父类, 所以他拥有Animal的属性和方法(不包括私有和静态)
*
* @author wxy
* @since 2023-01-01
*/
public class Dog extends Animal {
/**
* 重写了父类的方法, 描述了狗吃肉
*/
@Override
public void eat() {
System.out.println("dog eat meat");
}
}
/**
* Cat: 描述猫的类
* 继承了动物父类, 所以他拥有Animal的属性和方法(不包括私有和静态)
*
* @author wxy
* @since 2023-01-01
*/
public class Cat extends Animal {
/**
* 重写了父类的方法, 描述了猫吃鱼
*/
@Override
public void eat() {
System.out.println("cat eat fish");
}
}
/**
* Monkey: 描述猴子的类
* 继承了动物父类, 所以他拥有Animal的属性和方法(不包括私有和静态)
*
* @author wxy
* @since 2023-01-01
*/
public class Monkey extends Animal {
}
c. 测试方法
/**
* 运行方法
*
* @author wxy
* @since 2023-01-01
*/
public class AnimalCase1Main {
public static void main(String[] args) {
action();
attribute();
}
/**
* 描述继承或重写的方法
*/
private static void action() {
// 调用dog.eat方法
Dog dog = new Dog();
// 输出: dog eat meat
dog.eat();
// 来个狗多态试试
Animal animalDog = new Dog();
// 输出: dog eat meat
animalDog.eat();
// 调用cat.eat方法
Cat cat = new Cat();
// 输出: cat eat fish
cat.eat();
// 来个猫多态试试
Animal animalCat = new Cat();
// 输出: cat eat fish
animalCat.eat();
/*
通过上述Dog and Cat, 无论使用父类的引用, 还是子类的引用, 调用的都是子类的eat方法
那么如何调用父类的方法呢?
*/
// 调用monkey.eat方法
Monkey monkey = new Monkey();
// 输出: animal eat food
monkey.eat();
// 来个猴多态试试
Animal animalMonkey = new Monkey();
// 输出: animal eat food
animalMonkey.eat();
/*
通过上述描述可知, 如果子类没重写父类的方法, 当调用eat方法时会自动调用父类的方法
当然, 你还可以通过在子类方法中通过super关键字, 例如: super.eat();
*/
}
/**
* 描述属性使用的方法
*/
private static void attribute() {
/*
下面描述了父类属性的使用, 无论猫、狗、猴子, 人类都给他们起了名字
如果创建Dog、Cat、Monkey等100种动物, 我们总不能在每一个类都写一个String name属性吧
这样代码重复, 而且浪费时间, 能少写就少写。
*/
Dog dog = new Dog();
dog.setName("狗子");
// 输出: 狗子
System.out.println(dog.getName());
// 狗多态试试
Animal animalDog = new Dog();
animalDog.setName("狗子");
// 输出: 狗子
System.out.println(animalDog.getName());
}
}
2. 案例二
(1) 场景
通过案例一的分享,我们发现我们给所有动物都搞了一个吃的动作,那么我们现在需要定义一个方法,只要你传给我动物,我就可以执行他的eat方法,如果不使用面向对象编程,将会是怎样的结果呢?使用之后又是怎样呢?
(2) 代码:git地址
a. 不使用父类作为参数
/**
* NotUsedOop: 不使用面向对象编程
*
* @author wxy
* @since 2023-01-04
*/
public class NotUsedOop {
/**
* 执行狗吃的动作
*
* @param dog 狗
*/
static void dogAction(Dog dog) {
dog.eat();
}
/**
* 执行猫吃的动作
*
* @param cat 猫
*/
static void catAction(Cat cat) {
cat.eat();
}
}
b. 使用父类作为参数
/**
* UsedOop: 使用面向对象编程
*
* @author wxy
* @since 2023-01-04
*/
public class UsedOop {
/**
* 执行动物吃的动作
*
* @param animal 某种动物
* @param <T> 泛型: 规范参数必须继承Animal
*/
static <T extends Animal> void animalAction(T animal) {
animal.eat();
}
}
c. 测试方法
/**
* 运行方法
*
* @author wxy
* @since 2023-01-01
*/
public class ActionExecuteCase2Main {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Monkey monkey = new Monkey();
/*---不将父类作为参数---*/
// 执行dog吃的动作, 输出: dog eat meat
NotUsedOop.dogAction(dog);
// 执行cat吃的动作, 输出: cat eat fish
NotUsedOop.catAction(cat);
/*---将父类作为参数---*/
// 执行dog吃的动作, 输出: dog eat meat
UsedOop.eatAction(dog);
// 执行cat吃的动作, 输出: cat eat fish
UsedOop.eatAction(cat);
// 执行monkey吃的动作, 输出: animal eat food
UsedOop.eatAction(monkey);
/*
通过上述代码可以看出,
1.不提取公共父类: 如果有n种动物需要我们执行吃这个动作, 我们需要写100个方法
2.提取公共父类: 一个方法满足n种动物执行吃的动作
*/
}
}
3. 案例三
(1) 场景
假如我们请求接口,以删除接口为例:如果删除成功,仅响应成功结果,如果失败仅响应失败编码和失败信息。这样如何编写代码呢?当然您也可以选择这种方式:Jackson去除响应JSON中为null的参数
(2) 代码:git地址
a. 三个响应结果类
/**
* Response 响应父类
*
* @author wxy
* @since 2023-01-07
*/
public class Rsp {
private static final Rsp EMPTY_RSP = new Rsp();
public static Rsp empty() {
return EMPTY_RSP;
}
}
/**
* SucceedRsp 成功响应
*
* @author wxy
* @since 2023-01-07
*/
public class SucceedRsp<T> extends Rsp {
/**
* 成功响应结果
*/
private T result;
public static <T> SucceedRsp<T> ok(T result) {
SucceedRsp<T> rsp = new SucceedRsp<>();
rsp.result = result;
return rsp;
}
public T getResult() {
return result;
}
}
/**
* FailedRsp 失败响应
*
* @author wxy
* @since 2023-01-07
*/
public class FailedRsp<T> extends Rsp {
/**
* code: 响应编码
*/
private T code;
/**
* 响应信息
*/
private String message;
public static <T> FailedRsp<T> failed(T code, String message) {
FailedRsp<T> rsp = new FailedRsp<>();
rsp.code = code;
rsp.message = message;
return rsp;
}
public T getCode() {
return code;
}
public String getMessage() {
return message;
}
}
b. Controller逻辑
import com.wxy.common.exception.BusinessException;
import com.wxy.common.rsp.FailedRsp;
import com.wxy.common.rsp.Rsp;
import com.wxy.common.rsp.SucceedRsp;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* ResponseController 响应体测试控制器
*
* @author wxy
* @since 2023-01-07
*/
@RestController
public class ResponseController {
/**
* 删除账号接口
*
* @param flag true: 删除成功 false: 删除失败
* @return Rsp
*/
@DeleteMapping("/delete")
public Rsp delete(@RequestParam(value = "flag") Boolean flag) {
try {
// 返回成功的响应结果:
/*
{
"result": "delete succeed"
}
*/
return SucceedRsp.ok(deleteAccount(flag));
} catch (Exception e) {
String code = "unknown exception";
if (e instanceof BusinessException) {
// 如果抛出的异常属于我们自定义的业务异常, 获取自定义的错误编码
code = ((BusinessException) e).getCode();
}
// 返回失败的响应结果:
/*
{
"code": "DeleteFailed",
"message": "delete failed because runtimeException"
}
*/
return FailedRsp.failed(code, e.getMessage());
}
}
private String deleteAccount(boolean flag) {
if (flag) {
// 如果flag为true的时候, 返回delete succeed
return "delete succeed";
}
// 如果flag为false的时候, 返回错误编码和错误信息
throw new BusinessException("DeleteFailed",
"delete failed because runtimeException");
}
}
c. 测试方式
postman请求接口
四、总结
通过上述介绍以及案例可以看出,面向对象让编程变得更加简洁,我们可以通过简单的代码来实现我们的需求,所以若想掌握面向对象,还需要在学习和工作中多多运用,如果有哪些概念或者代码需要调整和优化,欢迎提出建议。