2024.11.20周三
今天猛猛刷了下八股,恶补了Java和计算机的一些基础知识,计划是明天做Day9的苍穹外卖项目的实战。
八股
Java基础
为什么重写 equals 还要重写 hashcode ?
在Java中,hashCode()
方法是 Object
类的一部分,每个类都继承自 Object
类,因此每个对象都有一个 hashCode()
方法。该方法返回对象的哈希码值,它是该对象的一个整数值,通常用于哈希表(如 HashMap
、HashSet
等)中快速查找对象
当然,如果我们不把自定义对象当成HashMap
的键来使用,那么自定义对象不重写equals
和hashCode
也是可以的,该规定主要使HashMap
正常工作。
假设我们有一个简单的类 Person
,它有两个属性:name
和 age
。我们想要根据这两个属性来判断两个Person
对象是否相等。
如果我们使用错误的 hashCode
实现,那么即使两个Person
对象的 name
和 age
相同,它们也会被视为不同的对象,因为它们的哈希码是基于内存地址计算的。
示例代码
import java.util.Objects;
public class Person {
//成员变量
private String name;
private int age;
//构造器
public Person(String name, int age) {
this.name = name;
this.age = age;
}
//equals方法重写
@Override
public boolean equals(Object o){
//若当前对象与比较对象的 内存地址 相同,返回true
if (this == o) return true;
//若比较对象为空或当前对象与比较对象的 类 不同,返回false
//此处this.getClass()和getClass()效果相同
if (o == null || getClass() != o.getClass()) return false;
//将比较对象强转成当前类的对象存储用于比较
Person person = (Person) o;
//int类通过==比较值是否相同
//String类通过调用java.util.Objects类的静态方法比较
//若String类直接使用 == 比较可能会抛出 NullPointerException
return age == person.age && Objects.equals(name, person.name);
}
//默认的hashCode实现
public int hashCode(){
//调用父类(Object类)的hashCode()方法
//返回的是对象的内存地址的哈希码
return super.hashCode();
}
//hashcode方法重写
@Override
public int hashCode() {
/**Objects.hash()是一个静态方法,能接受多个参数
并返回这些参数的哈希码的组合*/
return Objects.hash(name, age);
}
}
== 和 equals 比较的区别?
对于字符串变量来说,使用 ==
和 equals
比较字符串时,其比较方法不同。 ==
比较两个变量本身的值,即两个对象在内存的首地址,equals
比较字符串包含内容是否相同。
对于非字符串变量来说,如果没有对equals()
进行重写的话,==
和equals
方法的作用是相同的,都是用来比较对象在堆内存中的首地址,即比较两个引用变量是否指向同一个对象。
==
:比较的是两个字符串内存地址(堆内存)的数值是否相等,属于数值比较;euqals()
:比较的是两个字符串的内容,属于内容比较。
为什么有时会出现 4.0 - 3.6 = 0.40000001这种现象?
不是所有小数都可以用「完整」的二进制来表示的,比如十进制 0.1 在转换成二进制小数的时候,是一串无限循环的二进制数,计算机是无法表达无限循环的二进制数的,毕竟计算机的资源是有限。
因此,计算机只能用「近似值」来表示该二进制,那么意味着计算机存放的小数可能不是一个真实值。
现在基本都是用 IEEE 754 规范的「单精度浮点类型」(float)或「双精度浮点类型」(double)来存储小数的,根据精度的不同,近似值也会不同。
- 符号位:表示数字是正数还是负数,为 0 表示正数,为 1 表示负数;
- 指数位:指定了小数点在数据中的位置,指数可以是负数,也可以是正数,指数位的长度越长则数值的表达范围就越大;
- 尾数位:小数点右侧的数字,也就是小数部分,比如二进制 1.0011 x 2^(-2),尾数部分就是 0011,而且尾数的长度决定了这个数的精度,因此如果要表示精度更高的小数,则就要提高尾数位的长度;
final关键字的作用
final
关键字是一个用于修饰类、方法和变量的关键字。
- 修饰类: 当final关键字用于修饰一个类时,表示这个类不能被继承。这意味着该类是最终的,其设计和实现不能被任何子类修改或扩展。
- 修饰方法: 当final关键字用于修饰一个方法时,表示这个方法不能被子类覆盖(重写)。这意味着该方法的行为在所有使用它的地方都是一致的,子类不能改变这个行为。
- 修饰变量:
- 成员变量(实例变量):当final关键字用于修饰类的成员变量时,表示这个变量的值一旦被初始化之后就不能再更改。也就是说,它是一个常量。如果成员变量是基本数据类型,其值不能更改;如果成员变量是引用类型,其引用不能指向另一个对象,但是对象本身的内容可以更改。
- 静态变量(类变量):final修饰后即是常量。
- 局部变量:当final关键字用于修饰局部变量时,表示这个变量的值在初始化之后不能更改。局部final变量必须在声明时或构造器中初始化。
抽象类能加final修饰吗?
不能,Java中的抽象类是用来被继承的,而final修饰符用于禁止类被继承或方法被重写,因此抽象类和final修饰符是互斥的,不能同时使用。
介绍Java的集合类
Java集合框架是一组接口和类,它们提供了一种用来存储和操作对象组的统一架构。这个框架主要位于java.util
包中,它包含了许多接口和类,用于表示不同类型的集合,例如列表、集合、队列和映射等。
集合类的特点:
- 泛型:Java集合框架广泛使用泛型,允许集合类在定义时指定它们可以包含的对象类型。
- 迭代器:所有集合类都实现了Iterator接口,提供了一种统一的方式来遍历集合中的元素。
- 线程安全性:Java集合框架中的一些类是线程安全的(如
Vector
和Hashtable
),而另一些则不是(如ArrayList
和HashMap
)。
Java集合框架中一些主要的接口和类:
Collection
接口:
- 是集合层次中的根接口,定义了集合的基本操作,如添加、删除、清空、遍历等。
- 它有两个子接口:
List
和Set
。
List
接口:
- 继承自
Collection
接口,表示一个有序集合,可以包含重复的元素。- 常用实现类有
ArrayList
、LinkedList
和Vector
。
Set
接口:
- 继承自
Collection
接口,表示一个不包含重复元素的集合。- 常用实现类有
HashSet
、LinkedHashSet
和TreeSet
。
Deque
接口:
- 继承自
Queue
接口,代表双端队列,允许元素从两端被添加或移除。- 常用实现类有
ArrayDeque
和LinkedList
。
Map
接口:
- 表示键值对的集合,其中键是唯一的。
- 常用实现类有
HashMap
、LinkedHashMap
、TreeMap
和Hashtable
。
常用的Java集合实现类:
ArrayList
: 动态数组,实现了List
接口,支持动态增长。LinkedList
: 双向链表,也实现了List
接口,支持快速的插入和删除操作。HashMap
: 基于哈希表的Map
实现,存储键值对,通过键快速查找值。HashSet
: 基于HashMap
实现的Set
集合,用于存储唯一元素。TreeMap
: 基于红黑树实现的有序Map
集合,可以按照键的顺序进行排序。LinkedHashMap
: 基于哈希表和双向链表实现的Map
集合,保持插入顺序或访问顺序。PriorityQueue
: 优先队列,可以按照比较器或元素的自然顺序进行排序。
ArrayList和LinkedList的区别
ArrayList
和LinkedList
都是Java中常见的集合类,它们都实现了List
接口。
- 底层数据结构不同:
ArrayList
使用数组实现,通过索引进行快速访问元素;LinkedList
使用链表实现,通过节点之间的指针进行元素的访问和操作。 - 插入和删除的操作的效率不同:
ArrayList
在尾部的插入和删除操作效率较高,但在中间或开头的插入和删除操作效率较低,需要移动元素。LinkedList
在任意位置的插入和删除操作效率都比较高,因为只需要调整节点之间的指针,但是LinkedList
是不支持随机访问的,所以除了头节点外插入和删除的时间复杂度都是O(n)。 - 随机访问的效率不同:
ArrayList
支持通过索引进行快速随机访问,时间复杂度为O(1);LinkedList
需要从头到尾开始遍历链表,时间复杂度为O(n)。 - 空间占用:
ArrayList
在创建时需要分配一段连续的内存空间,因此会占用较大的空间。LinkedList
每个节点只需要存储元素和指针,因此相对较小。 - 使用场景:
ArrayList
适用于频繁随机访问和尾部的插入删除操作,而LinkedList
适用于频繁的中间插入删除操作和不需要的随机访问的场景。 - 线程安全:两个集合都不是线程安全的。
操作系统
什么是死锁?死锁产生的条件?
在多线程编程中,我们为了防止多线程竞争共享资源而导致数据错乱,都会在操作共享资源之前加上互斥锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放。
那么,当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁。
举个例子,小林拿了小美房间的钥匙,而小林在自己的房间里,小美拿了小林房间的钥匙,而小美也在自己的房间里。如果小林要从自己的房间里出去,必须拿到小美手中的钥匙,但是小美要出去,又必须拿到小林手中的钥匙,这就形成了死锁。
死锁只有同时满足以下四个条件才会发生:
- 互斥条件;
- 持有并等待条件;
- 不可剥夺条件;
- 环路等待条件;
互斥条件
互斥条件是指多个线程不能同时使用同一个资源。
上图中,如果线程A已经持有的资源,不能再同时被线程B持有,如果线程B请求获取线程A已经占用的资源,那线程B只能等待,直到线程A释放该资源。
持有并等待条件
持有并等待条件是指,当线程A已经持有了资源1,又想申请资源2,而资源2已经被线程C持有了,所以线程A就会处于等待状态,但是线程A在等待资源2的同时并不会释放自己已经持有的资源1。
不可剥夺条件
不可剥夺条件是指,当线程已经持有了资源,在自己使用完之前不能被其他线程获取,线程B如果也想使用此资源,则只能在线程A使用完并释放完才能获取。
环路等待条件
环路等待条件指的是,在死锁发生的时候,两个线程获取资源的顺序构成了环形链。
比如,线程A已经持有资源2,而想请求资源1,线程B已经获取了资源1,而想请求资源2,这就形成资源请求等待的环形图。
线程有哪几种状态?
java.lang.Thread.State
枚举类中定义了六种线程的状态,可以调用线程Thread
中的getState()
方法获取当前线程的状态。
线程状态 | 解释 |
---|---|
NEW | 尚未启动的线程状态,即线程创建,还未调用start方法 |
RUNNABLE | 就绪状态(调用start,等待调度)+正在运行 |
BLOCKED | 等待监视器锁时,陷入阻塞状态 |
WAITING | 等待状态的线程正在等待另一线程执行特定的操作(如notify) |
TIMED_WAITING | 具有指定等待时间(比如等待?秒钟)的等待状态 |
TERMINATED | 线程完成执行,终止状态 |
有哪些进程调度算法?
进程调度算法也称 CPU 调度算法,毕竟进程是由 CPU 调度的。
当 CPU 空闲时,操作系统就选择内存中的某个「就绪状态」的进程,并给其分配 CPU。
什么时候会发生 CPU 调度呢?通常有以下情况:
「非抢占式调度」
非抢占式的意思就是,当进程正在运行时,它就会一直运行,直到该进程完成或发生某个事件而被阻塞时,才会把 CPU 让给其他进程。
- 当进程从运行状态转到等待状态;
- 当进程从运行状态转到终止状态;
「抢占式调度」
抢占式调度,顾名思义就是进程正在运行的时,可以被打断,使其把 CPU 让给其他进程。抢占原则一般有三种:时间片原则、优先权原则、短作业优先原则。
- 当进程从等待状态转到就绪状态;
- 当进程从运行状态转到终止状态;
先来先服务调度算法
非抢占式的先来先服务(First Come First Severd, FCFS)算法。
顾名思义,先来后到,每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。
FCFS 对长作业有利(短作业等待的时间长),适用于 CPU 繁忙型作业的系统,而不适用于 I/O 繁忙型作业的系统。
最短作业优先调度算法
最短作业优先(Shortest Job First, SJF)调度算法同样也是顾名思义,它会优先选择运行时间最短的进程来运行,这有助于提高系统的吞吐量。
显然对长作业不利,很容易造成一种极端现象:比如一个长作业在就绪队列等待运行,而这个就绪队列有非常多的短作业,那么就会使得长作业不断的往后推,周转时间变长,致使长作业长期不会被运行。
高响应比优先调度算法
高响应比优先 (Highest Response Ratio Next, HRRN)调度算法主要是权衡了短作业和长作业。
每次进行进程调度时,先计算「响应比优先级」,然后把「响应比优先级」最高的进程投入运行,「响应比优先级」的计算公式:
- 「等待时间」相同时,依据糖水公式,「要求的服务时间」(水)越短,「响应比」(浓度)就越高,因此短作业的进程容易被选中运行。
- 「要求的服务时间」(水)相同时,「等待时间」(糖分)越长,「响应比」(浓度)就越高,这就兼顾到了长作业进程,因为进程的响应比可以随时间等待的增加而提高,当其等待时间足够长时,其响应比便可以升到很高,从而获得运行的机会;
时间片轮转调度算法
最古老、最简单、最公平且使用最广的算法就是时间片轮转(Round Robin, RR)调度算法。
每个进程被分配一个时间段,称为时间片(Quantum),即允许该进程在该时间段中运行。
- 如果时间片用完,进程还在运行,那么将会把此进程从 CPU 释放出来,并把 CPU 分配另外一个进程;
- 如果该进程在时间片结束前阻塞或结束,则 CPU 立即进行切换;
另外,时间片的长度就是一个很关键的点:
- 如果时间片设得太短会导致过多的进程上下文切换,降低了 CPU 效率;
- 如果设得太长又可能引起对短作业进程的响应时间变长。
通常时间片设为 20ms~50ms
通常是一个比较合理的折中值。
最高优先级调度算法(抢占式/非抢占式)
对于多用户计算机系统就有不同的看法了,它们希望调度是有优先级的,即希望调度程序能从就绪队列中选择最高优先级的进程进行运行,这称为最高优先级(Highest Priority First,HPF)调度算法。
进程的优先级可以分为,静态优先级或动态优先级:
- 静态优先级:创建进程时候,已经确定过优先级,之后整个运行时间优先级都不会变化。
- 动态优先级:根据进程的动态变化调整优先级,比如,如果进程运行时间增加,则降低其优先度;如果进程等待时间(就绪队列的等待时间)增加,则升高其优先级,也就是随着时间的推移增加等待进程的优先级。
依然有缺点,可能会导致低优先级的进程永远不会运行。
多级反馈队列调度算法
多级反馈队列(Multilevel Feedback Queue)调度算法是「时间片轮转算法」和「最高优先级算法」的综合和发展。
- 「多级」表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。
- 「反馈」表示如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列。
工作原理:
- 设置多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短。
- 新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待调度,如果在第一级队列规定的时间片没有运行完成,则转入第二级队列的末尾,以此类推,直至完成。
- 当较高优先级的队列为空,才调度较低优先级的队列。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行。
对于短作业可能可以在第一级队列很快被处理完。对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待的时间变长了,但是运行时间也会更长了,所以该算法很好的兼顾了长短作业,同时有较好的响应时间。
算法
今日暂无该内容学习。
项目
苍穹外卖项目回顾
全局异常处理器的实现
com.sky.handler.GlobalExceptionHandler
/**
* 业务异常
*/
// BaseException 属于 Exception class
public class BaseException extends RuntimeException {
public BaseException() {
}
public BaseException(String msg) {
super(msg);
}
}
/**
* 全局异常处理器,处理项目中抛出的业务异常
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获业务异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(BaseException ex){
log.error("异常信息:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
/**
* 处理SQL异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
//违反了约束条件的SQL错误处理(如此处username设置成唯一约束,此时重复添加就会出现该异常)
String message = ex.getMessage();
if (message.contains("Duplicate entry")){
String[] split = message.split(" ");
String username = split[2];
String msg = username + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
}else{
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
}
总结:在方法中添加@ExceptionHandler
注解,将Exception class
的类作为参数传入方法中,再针对这些异常做异常处理。