Java 八股

Java语法基础

面向对象的三大特征

  • 封装
  • 继承
  • 多态

JVM JDK JRE

JVM

JVM是指运行 Java 字节码的一个虚拟机,对不同的平台系统有特定的实现,程序员只需要编写相同的 Java 代码,交由 JVM 运行字节码,实现跨平台, 字节码和不同系统的 JVM 实现是 Java 语言 一次编译,随处可以运行 的关键所在

最常用的 JVM 是 Hotspot JVM

JRE

Java 运行时环境,它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)

JRE 仅包含 Java 应用程序的运行时环境和必要的类库

JDK

包含一系列开发功能的 JAVA SDK 提供给开发者用的,用于创建和编译 Java 程序

JDK 包含了 JRE,同时还包含了编译 java 源码的编译器 javac 以及一些其他工具比如 javadoc(文档注释工具)、jdb(调试器)、jconsole(基于 JMX 的可视化监控⼯具)、javap(反编译工具)等等

此外,对于某些需要使用 Java 特性的应用程序,如 JSP 转换为 Java Servlet、使用反射等,也需要 JDK 来编译和运行 Java 代码。因此,即使不打算进行 Java 应用程序的开发工作,也有可能需要安装 JDK

三者的区别

如何理解 Java 的编译与解释并存

编译型编译型语言 会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低。常见的编译性语言有 C、C++、Go、Rust 等等。

解释型解释型语言会通过解释器一句一句的将代码解释(interpret)为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢。常见的解释性语言有 Python、JavaScript、PHP 等等。

至于为什么说 Java 的编译和解释共存,主要是 Java 先经过编译生成 字节码 文件,之后 JVM 类加载器首先加载字节码文件,再由解释器逐行解释运行(针对热点代码有 JIT 编译器)

采用字节码的好处

在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机

Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点

所以, Java 程序运行时相对来说还是高效的(不过,和 C、 C++,Rust,Go 等语言还是有一定差距的),而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行

我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(Just in Time Compilation) 编译器,而 JIT 属于运行时编译

当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,直接获得机器码的运行效率肯定是高于再去走 Java 解释器的

这也解释了我们为什么经常会说 Java 是编译与解释共存的语言

标识符和关键字的区别

标识符 Identifier 其实就是我们编码的过程中的名字,马甲,称呼

关键字是特殊含义的标识符

hashcode()

本身是 Object 类的方法,带有 native 关键字是用 C/CPP 来实现的

那为什么 JDK 还要同时提供这两个方法呢?

这是因为在一些容器(比如 HashMapHashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet的过程)!

我们在前面也提到了添加元素进HashSet的过程,如果 HashSet 在对比的时候,同样的 hashCode 有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。

那为什么不只提供 hashCode() 方法呢?

这是因为两个对象的hashCode 值相等并不代表两个对象就相等。

那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的?

因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode )。

总结下来就是:

  • 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
  • 如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。
  • 如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。

String为什么不可变

String 真正不可变有下面几点原因:

  1. 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
  2. String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

String参数传递

字符串在 Java 中是通过引用传递的,但由于字符串的不可变性,对字符串的任何修改都会创建一个新的字符串对象。因此,原始的字符串对象不会受到方法内部修改的影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StringParameterExample {

public static void main(String[] args) {
String original = "Hello";
System.out.println("Before method call: " + original);
modifyString(original);
System.out.println("After method call: " + original);
}

private static void modifyString(String str) {
// 在方法内部修改字符串,实际上是创建了一个新的字符串对象
str = str + " World";
System.out.println("Inside method: " + str);
}
}

Object类的方法有哪些

1.clone方法

保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出 CloneNotSupportedException 异常。

主要是JAVA里除了 8 种基本类型 (byte short int long float double char boolean ) + String + 包装数据类型 传参数是值传递,其他的类对象传参数都是引用传递,我们有时候不希望在方法里讲参数改变,这是就需要在类中复写clone方法。

2.getClass方法

final方法,获得运行时类型。

3.toString方法

该方法用得比较多,一般子类都有覆盖。

4.finalize方法

该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。

5.equals方法

该方法是非常重要的一个方法。一般equals和==是不一样的,但是在Object中两者是一样的。子类一般都要重写这个方法。

6.hashCode方法

该方法用于哈希查找,可以减少在查找中使用equals的次数,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。

一般必须满足obj1.equals(obj2)==true。可以推出obj1.hash- Code()==obj2.hashCode(),但是hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价。

如果不重写hashcode(),在HashSet中添加两个equals的对象,会将两个对象都加入进去。

7.wait方法

wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。

调用该方法后当前线程进入睡眠状态,直到以下事件发生。

(1)其他线程调用了该对象的 notify 方法。

(2)其他线程调用了该对象的 notifyAll 方法。

(3)其他线程调用了 interrupt 中断该线程。

(4)时间间隔到了。

此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。

8.notify方法

该方法唤醒在该对象上等待的某个线程。

9.notifyAll方法

该方法唤醒在该对象上等待的所有线程。

如何比较两个对象

两种方法:

  • equals
  • compareTo

equals()

在Java中,每个类都继承自 Object 类,而 Object 类中有一个equals()方法。默认情况下,equals()方法实现是比较对象的引用是否相等,即比较两个对象是否是同一个对象

如果你想在自定义类中比较对象的内容而非引用,你需要覆盖equals()方法,并在其中实现你自己的比较逻辑。通常,你需要比较对象的属性来确定它们是否相等。

compareTo()

如果你的类实现了Comparable接口,你可以使用compareTo()方法来比较两个对象的大小。这通常用于排序操作。

函数签名三要素

方法名,参数列表,返回值

通过这三点来确定一个函数

重载和重写的区别

重载

发生在编译期,同一个类中(或者父类和子类之间也可以),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。

重写

重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。

  1. 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类(两同两小一大)
  2. 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。
  3. 构造方法无法被重写

深拷贝和浅拷贝

浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象

深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象

反射

几种实现

一种是基于JDK实现的,另一种是 CGLIB ,还有ASM的实现

优缺点

优点:提供了在程序运行时获取和执行任意一个类中的方法,代码更加灵活

缺点:安全问题:无视泛型的类型检查(因为泛型的类型检查发生在编译期间)

IO流

输入输出(I/O)流是Java中用于处理输入和输出的一组机制。

I/O流以字节或字符为单位传输数据,用于与文件、网络、设备等进行数据交互。在Java中,I/O流主要分为字节流和字符流两种类型。

Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。

  • InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
  • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

在 Java 中一个字符一般认为相当于 2 个字节

序列化

ObjectInputStream 用于从输入流中读取 Java 对象(反序列化),ObjectOutputStream 用于将对象写入到输出流(序列化)

另外,用于序列化和反序列化的类必须实现 Serializable 接口

对象中如果有属性不想被序列化,使用 transient 修饰

有了字节流为什么还需要字符流

  • 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时。
  • 如果我们不知道编码类型就很容易出现乱码问题。

对于文本类型包含字符数据的,直接用字节流会出现编码格式导致的乱码问题,因此I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。

如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。

字符流默认采用的是 Unicode 编码,我们可以通过构造方法自定义编码

顺便分享一下之前遇到的笔试题:常用字符编码所占字节数?

utf8 :英文占 1 字节,中文占 3 字节,unicode:任何字符都占 2 个字节,gbk:英文占 1 字节,中文占 2 字节。

缓冲流

IO 操作是很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率。

字节缓冲流这里采用了装饰器模式来增强 InputStreamOutputStream子类对象的功能。

1
2
// 新建一个 BufferedInputStream 对象
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt"));

字节流和字节缓冲流的性能差别主要体现在我们使用两者的时候都是调用 write(int b)read() 这两个一次只读取一个字节的方法的时候。由于字节缓冲流内部有缓冲区(字节数组),因此,字节缓冲流会先将读取到的字节存放在缓存区,大幅减少 IO 次数,提高读取效率。

Spring

什么是Spring

Spring支持的事务类型有哪些

  • 声明式
  • 编程式
  • 基于注解
  • 注解驱动
  • 全局事务

AOP的应用

  • 日志
  • 事务
  • 异常处理
  • 性能监控

SpringBoot

SpringBoot核心入口

在Spring Boot应用中,核心的入口点是SpringApplication类。具体来说,Spring Boot应用的启动是通过public static void main(String[] args)方法实现的,该方法通常位于一个包含@SpringBootApplication注解的主类中。这个主类是Spring Boot应用的启动类,同时也是SpringApplication的一个参数。

以下是一个简单的Spring Boot应用启动类示例:

1
2
3
4
5
6
7
8
9
10
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}

在这个例子中,MyApplication类上使用了@SpringBootApplication注解,它包含了多个元注解,其中之一是@SpringBootConfiguration,它表明这是一个Spring Boot配置类。main方法使用SpringApplication.run启动了Spring Boot应用,传递了MyApplication.class作为主要的启动类,以及args数组作为命令行参数。

SpringBoot常见注解

@SpringBootApplication

  • 包含**@SpringBootConfiguration**
    • 包含**@Configuration**

@RestController

SpringMVC

简单介绍一下是什么

Spring MVC(Model-View-Controller)是Spring框架的一个模块,用于支持构建基于模型-视图-控制器设计模式的Web应用程序

它提供了一个灵活的、基于注解的Web框架,用于开发和部署Web应用程序

Spring MVC被设计为与Spring框架的其他模块(如Spring Core和Spring Data等)无缝集成,提供了一种结构化和模块化的方式来构建Web应用程序

设计模式

单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class EagerSingleton {

// 在类加载时就创建单例实例
private static final EagerSingleton instance = new EagerSingleton();

// 私有构造方法,防止外部实例化
private EagerSingleton() {
}

// 全局访问点
public static EagerSingleton getInstance() {
return instance;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SimpleLazySingleton {

private static SimpleLazySingleton instance;

private SimpleLazySingleton() {
}

public static SimpleLazySingleton getInstance() {
// 懒汉式,在第一次请求时创建实例
if (instance == null) {
instance = new SimpleLazySingleton();
}
return instance;
}
}

线程安全问题?饿汉式是线程安全的,懒汉式不安全

可以通过双重锁定+同步块来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DoubleCheckedLazySingleton {

private static volatile DoubleCheckedLazySingleton instance;

private DoubleCheckedLazySingleton() {
}

public static DoubleCheckedLazySingleton getInstance() {
// 双重检查锁定
if (instance == null) {
synchronized (DoubleCheckedLazySingleton.class) {
if (instance == null) {
instance = new DoubleCheckedLazySingleton();
}
}
}
return instance;
}
}

装饰器设计模式

缓冲流是I/O流中的一种装饰器,它提供了对底层字节流(或字符流)的缓冲功能,以提高I/O性能。在Java中,缓冲流通常通过 BufferedInputStreamBufferedOutputStream(用于字节流)以及 BufferedReaderBufferedWriter(用于字符流)来实现。这些类都是装饰器类,采用了装饰器设计模式。

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
import java.io.*;

public class BufferedStreamsExample {

public static void main(String[] args) {
// 创建文件输入流
try (FileInputStream fileInputStream = new FileInputStream("input.txt");
// 将文件输入流包装在缓冲输入流中
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
// 创建文件输出流
FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
// 将文件输出流包装在缓冲输出流中
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream)) {

// 读取数据并写入到输出流
int byteRead;
while ((byteRead = bufferedInputStream.read()) != -1) {
bufferedOutputStream.write(byteRead);
}

System.out.println("File copied successfully.");

} catch (IOException e) {
e.printStackTrace();
}
}
}

在这个示例中,BufferedInputStreamBufferedOutputStream 分别包装了 FileInputStreamFileOutputStream,提供了缓冲的功能。这样的嵌套使用正是装饰器设计模式的体现,通过一系列装饰器来增强原始的功能。

字符流的缓冲流用法类似,可以使用 BufferedReaderBufferedWriter 来提高字符流的性能。

集合相关

说说 List Set Queue Map 的几个区别

List 有序可重复

Set 不可重复,具体有序无序要看实现

Queue FIFO的实现,有序可重复

Map 键值对存储映射

  • key是无序不可重复
  • value是无序可重复的
  • 一个key最多对应一个value

常见的 List Set Queue Map

List:

  • ArrayList 线程不安全
  • Vector 线程安全,古早
  • LinkedList 双向链表,1.6之前还是循环的,但是在1.7之后取消了循环

Set:

都是线程不安全的

  • HashSet 无序(底层是HashMap)
  • LinkedHashSet(底层是LinkedHashMap)
  • TreeSet(有序)

Queue:

  • PriorityQueue:优先级队列
  • DelayQueue
  • ArrayQueue 可扩容的动态双向数组

Map:

  • HashMap 在1.8之前是数组+链表,数组是主体,链表则是为了解决哈希冲突,拉链法实现的;在1.8以后当一个点上的链表长度大于8的时候就会转化为红黑树(前提也要是数组长度大于64的时候才会转化,小于64则会先进行数组扩容)
  • LinkedHashMap 继承自 HashMap 同时增加了一条双向链表,使得可以按插入顺序访问各个 bucket 的节点
  • HashTable 线程安全,不允许 null 的 key 和 value
  • TreeMap 红黑树(自平衡的二叉排序树)

线程不安全的解决

我们都知道ArrayList是线程不安全的,多个线程对同一个List进行写或者是删除的操作可能会导致数据冲突

为了确保线程安全,可以使用 Collections.synchronizedList 或者 CopyOnWriteArrayList 等线程安全的替代品,或者在访问 ArrayList 时手动进行同步

CopyOnWriteArrayList

它通过在写操作时创建原始数据的拷贝来实现线程安全性,这使得读操作可以在不受写操作干扰的情况下进行。适用于读多写少的场景,对于迭代操作也是安全的。

ArrayList

加入操作默认是最后一位加入,操作是O(1),删除中间指定位置的就是O(n)因为要移动

以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10。

线程相关

Java中创建线程的方法

主要是两个

继承Thread类重写run方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyThread extends Thread {
public void run() {
// 线程的执行逻辑
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getId() + " Value " + i);
}
}
}

public class ThreadExample {
public static void main(String args[]) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();

// 启动线程
thread1.start();
thread2.start();
}
}

实现 Runnable 接口,把这个类的实例传递作为 Thread 构造函数的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyRunnable implements Runnable {
public void run() {
// 线程的执行逻辑
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getId() + " Value " + i);
}
}
}

public class RunnableExample {
public static void main(String args[]) {
Thread thread1 = new Thread(new MyRunnable());
Thread thread2 = new Thread(new MyRunnable());

// 启动线程
thread1.start();
thread2.start();
}
}

可以直接调用 Thread 类的 run 方法吗?

这是另一个非常经典的 Java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。

但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结:调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

Volatile 关键字

保证多线程环境下变量的可见性,但是不保证针对变量的操作是原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class VolatoleAtomicityDemo {
public volatile static int inc = 0;

public void increase() {
inc++;
}

public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
VolatoleAtomicityDemo volatoleAtomicityDemo = new VolatoleAtomicityDemo();
for (int i = 0; i < 5; i++) {
threadPool.execute(() -> {
for (int j = 0; j < 500; j++) {
volatoleAtomicityDemo.increase();
}
});
}
// 等待1.5秒,保证上面程序执行完成
Thread.sleep(1500);
System.out.println(inc);
threadPool.shutdown();
}
}

ReentrantLock

ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能

公平锁和非公平锁的区别

公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。

非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。

synchronized 默认是非公平,ReentrantLock 可以自定义公平和非公平

可重入锁指的是什么

可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。

JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入的。

在下面的代码中,method1()method2()都被 synchronized 关键字修饰,method1()调用了method2()

1
2
3
4
5
6
7
8
9
10
public class SynchronizedDemo {
public synchronized void method1() {
System.out.println("方法1");
method2();
}

public synchronized void method2() {
System.out.println("方法2");
}
}

由于 synchronized锁是可重入的,同一个线程在调用method1() 时可以直接获得当前对象的锁,执行 method2() 的时候可以再次获取这个对象的锁,不会产生死锁问题。假如synchronized是不可重入锁的话,由于该对象的锁已被当前线程所持有且无法释放,这就导致线程在执行 method2()时获取锁失败,会出现死锁问题。

可中断锁和不可中断锁有什么区别?

  • 可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。ReentrantLock 就属于是可中断锁。
  • 不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 synchronized 就属于是不可中断锁

计算机网络

OSI参考模型

什么时候用 TCP 什么时候用 UDP

UDP 一般用于即时通信,比如:语音、 视频、直播等等。这些场景对传输数据的准确性要求不是特别高,比如你看视频即使少个一两帧,实际给人的感觉区别也不大。

TCP 用于对传输准确性要求特别高的场景,比如文件传输、发送和接收邮件、远程登录等等

为什么需要三次握手

为什么要三次握手?

三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。

  1. 第一次握手:Client 什么都不能确认;Server 确认了对方发送正常,自己接收正常
  2. 第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:对方发送正常,自己接收正常
  3. 第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常

三次握手就能确认双方收发功能都正常,缺一不可

奇偶校验机制

奇偶校验机制是在数据链路层中一种常见的用于检测bit数据在传输过程中是否发生位错误的一种技术

Redis

基本数据类型

5 种基础数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)

3 种特殊数据类型:HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)

String 存储的是序列化后的对象数据,存放的是整个对象。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。

应用场景

缓存、分布式锁

具体数据类型以及对应的应用场景如下:

String

可以缓存网页内容,存储简单的数据对象例如验证码等kv键值对,就是最简单的 kv 存储

1
2
SET page:/home "<html>...</html>" #被频繁访问的网页内容本体可以缓存在 Redis 中
GET page:/home

List

有序的字符串列表(说是列表其实是一个队列)

可以用 list 数据结构实现最简单的一个消息队列模型

1
2
3
LPUSH queue:tasks "task1"
LPUSH queue:tasks "task2"
RPOP queue:tasks

可以使用 Redis 列表实现简单的消息队列,LPUSH 用于添加任务,RPOP 用于消费任务

Set

Set 集合数据结构可以实现简单的用户标签系统

同时还可以进行交集并集运算

1
2
3
4
SADD user:1:tags "redis"
SADD user:1:tags "database"
SADD user:2:tags "cache"
SINTER user:1:tags user:2:tags #查询两个用户的兴趣交集

在社交网络应用中,可以使用 Set 集合存储用户的标签,并进行交集计算,找出共同的兴趣点

Hash

哈希是键值对集合,我们可以实现对象的存储

相较于 String 只能存储简单的字符串,Hash可以存储多个字段以及他们的值

1
2
3
HSET user:1000 name "John Doe"
HSET user:1000 email "john.doe@example.com"
HGETALL user:1000

Hash 数据结构可以对单个字段进行操作。例如,只修改或获取哈希中的一个字段

1
2
HSET user:1000 name "Jane Doe"
HGET user:1000 name #只获取 user:1000 key(集合)的name这一个字段

ZSet/Sorted Set

相较于集合而言多了一个权重,可以进行排序

例如在标签的基础上增加一个 weight 权重的设计,或者是设计一个排行榜

1
2
3
ZADD leaderboard 1000 "user1"
ZADD leaderboard 2000 "user2"
ZREVRANGE leaderboard 0 1 WITHSCORES

Bitmap

可以用来实现某一天的签到业务,或者是统计海量用户在某一天的活跃度

一个网站可以使用 Bitmap 来记录用户在某一天是否活跃。假设网站有 1 亿用户,可以使用一个大小为 1 亿位的 Bitmap,其中每一位代表一个用户的活跃状态(0 表示不活跃,1 表示活跃)

1
2
SETBIT active_users:2024-05-14 12345 1  # 用户 ID 为 12345 的用户在 2024-05-14 活跃
GETBIT active_users:2024-05-14 12345 # 检查用户 ID 为 12345 的用户在 2024-05-14 是否活跃

采用这个数据结构可以节省大量存储空间,比使用布尔数组更高效

HyperLogLog

HyperLogLog 是一种基于概率的数据结构,用于估算大规模数据集合的基数(即不同元素的数量)

例如可以实现独立(不同)访客统计,商品去重的访问量

1
2
3
4
5
6
7
PFADD unique_visitors:2024-05-14 user12345  # 记录用户 ID 为 user12345 的用户在 2024-05-14 访问
PFADD unique_visitors:2024-05-14 user67890 # 记录用户 ID 为 user67890 的用户在 2024-05-14 访问
PFCOUNT unique_visitors:2024-05-14 # 估算 2024-05-14 的独立访客数量

PFADD product:page:123 user12345 # 记录用户 ID 为 user12345 访问了商品页面 123
PFADD product:page:123 user67890 # 记录用户 ID 为 user67890 访问了商品页面 123
PFCOUNT product:page:123 # 估算商品页面 123 的独立访问用户数量

Geospatial

可以借助 Geospatial 来存储和操作地理空间数据,可以进行半径查询、距离计算等操作

1
2
3
GEOADD stores 13.361389 38.115556 "Palermo"
GEOADD stores 15.087269 37.502669 "Catania"
GEORADIUS stores 15 37 100 km

上面是查询了以 15 37 这一经纬度坐标点为中心,100km为半径的所有商店

持久化机制

RDB

Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

AOF

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式( fsync策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。

只有同步到磁盘中才算持久化保存了,否则依然存在数据丢失的风险,比如说:系统内核缓存区的数据还未同步,磁盘机器就宕机了,那这部分数据就算丢失了。

AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof

缓存穿透 缓存击穿 缓存雪崩

缓存穿透:说简单点就是大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。

缓存击穿: 大量请求的 key 不在缓存中但是在数据库里(一般都是缓存的时间到了就过期)

缓存雪崩:缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。

Redis常见的删除策略

定时删除,TTL 结束就触发删除,对 CPU 不友好

惰性删除:使用的时候发现过期了才删除,但是会浪费内存空间

定期删除:每隔一段时间「随机」取出一定数量的 key 进行检查和删除

描述用 Redis 实现验证码的存储和过期时间

用户请求验证码的时候进行验证码的生成,之后存储到 Redis 中,通过一个 Set 方法就可以同时实现过期时间的设置

之后用户携带验证码的时候从 Redis 中获取验证码,如果验证码正确且未过期,则验证成功;否则验证失败。

MySQL

分页

LIMIT 子句来实现

分页通常需要两个参数:偏移量(offset)和行数(limit)。

基本的分页查询语法如下:

1
2
SELECT * FROM your_table
LIMIT offset, limit;

其中,offset表示开始的行数,limit表示要选择的行数。

例如,如果你想获取从第 11 行开始的 10 条记录,查询语句如下:

1
2
SELECT * FROM your_table
LIMIT 10, 10;

这将返回从第 11 行到第 20 行的记录。

索引

在 MySQL 中,索引是一种用于提高数据库查询性能的重要机制。索引是一种数据结构,类似于书籍目录,它提供了一种快速查找数据的方式,而不必扫描整个数据表。例如我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。下面是关于 MySQL 索引的一些基本概念

基本概念

  1. 索引的作用:

    • 提高检索速度: 索引可以加快数据检索的速度,特别是在大型数据表中。
    • 加速排序: 对于排序和分组操作,索引也可以提高性能。
    • 唯一性约束: 索引可以强制表中的每一行数据的唯一性。
  2. 索引的缺点

    • 创建和维护索引需要一定的性能开销
    • 如果是批量插入上万行的数据,则需要针对这些插入语句关闭索引,否则会很慢很慢

常见类型的索引:按照底层数据结构划分

  • B树
  • B+树
  • Hash表
  • 红黑树

无论是 InnoDB 还是 MyISam,采用的底层数据结构都是 B+ 树

Hash表数据结构

其实就是哈希函数映射+拉链法

和JDK1.8以前的实现方式差不多

哈希表虽然快,但是不支持顺序和范围查询

1
SELECT * FROM tb1 WHERE id < 500;

像是这样的 SQL 就无法通过哈希表来实现

(TODO)

常见类型的索引:按照应用维度划分

  • 主键索引(Primary Key Index): 数据表的主键列使用的就是主键索引,是一种唯一性索引,用于唯一标识表中的每一行记录。一个表只能有一个主键索引。
  • 唯一索引(Unique Index): 唯一索引确保索引列中的所有值都是唯一的,但允许空值。
  • 普通索引(Normal Index): 普通索引是最基本的索引类型,没有唯一性限制。
  • 全文索引(Full-Text Index): 用于全文搜索,适用于对文本数据进行搜索的场景。
  • 唯一索引、普通索引、全文索引,都属于二级索引,他们的叶子节点存储的数据是主键的值

InnoDB存储引擎使用的是B树索引结构。B树(或B+树)是一种平衡树结构,它在插入、删除和查找操作上具有较好的性能表现,适用于范围查询和排序操作。InnoDB的主键索引和普通索引都是基于B树的。

事务带来的问题

脏读 幻读 不可重复读

SQL定义的四个隔离级别

SQL 标准定义了四个隔离级别:

  • READ-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交) :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读) :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化) :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过SELECT @@tx_isolation;命令来查看,MySQL 8.0 该命令改为SELECT @@transaction_isolation;

1
2
3
4
5
6
mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+

MySQL存储引擎

除了 InnoDB 和 MyISAM 外还有什么

MEMORY

CSV文件

一百万条数据插入MySQL,有什么好的处理方法么

禁用索引

用 preparedStatement 预编译SQL语句批处理插入

JDBC

JDBC(Java Database Connectivity)是Java程序与关系型数据库进行交互的一种标准接口。它提供了一组用于访问和操作数据库的Java API,允许Java应用程序与各种关系型数据库进行通信,如 MySQL、Oracle、SQL Server 等。

JDBC的主要组成部分包括:

  1. DriverManager: 用于管理一组数据库驱动程序,负责建立与数据库的连接。
  2. Driver: JDBC驱动程序是一个实现JDBC接口的类,它充当Java应用程序与数据库之间的桥梁。不同的数据库需要使用相应的JDBC驱动。
  3. Connection: 表示与数据库的连接。通过DriverManager.getConnection方法获取数据库连接。
  4. Statement: 用于执行SQL语句。Statement 接口有三个主要的实现类:StatementPreparedStatementCallableStatement
  5. ResultSet: 表示数据库结果集,是通过执行查询获取的数据集合。

JDBC中的事务是如何管理的?

在JDBC中,事务管理主要通过Connection接口的commit()rollback()方法来实现

当所有SQL语句执行完成后,通过commit()提交事务;如果出现错误或需要回滚,则调用rollback()方法

默认情况下,每个SQL语句都是一个事务,如果需要手动管理事务,可以使用setAutoCommit(false)关闭自动提交。

Mybatis

动态SQL如何实现

MyBatis中的动态SQL允许你在映射文件中编写灵活的SQL语句,根据不同的条件生成不同的SQL片段,以满足动态查询的需求。动态SQL通常使用<if>, <choose>, <when>, <otherwise>, <trim>, <set>, <where>, <foreach>等标签来实现

分页如何实现

MyBatis 提供了 RowBounds 类,通过将 RowBounds 作为方法的参数来实现分页

1
List<User> getUserList(RowBounds rowBounds);
1
2
3
<select id="getUserList" resultType="User">
SELECT * FROM users
</select>
1
2
3
4
int offset = 0;
int limit = 10;
RowBounds rowBounds = new RowBounds(offset, limit);
List<User> userList = userDao.getUserList(rowBounds);

#{}和${}的区别是?

${}是properties文件的变量占位符,可以用在配置文件或者是 SQL 语句中

例如:根据参数按任意字段排序

1
select * from users order by ${orderControls}

orderControls可以是 namename descname,sex asc等,实现灵活的排序

而 #{} 是SQL的参数占位符,Mybatis会将其设置为 ? 并且在执行的过程中由 preparedStatement 来设置参数值

Tomcat & Servlet

什么是Tomcat?它与Web服务器的区别是什么?

Tomcat是一个开源的、轻量级的、用于运行Java Servlet和JSP的Web应用服务器

它与传统的Web服务器(如Apache HTTP Server)的区别在于Tomcat更专注于Java Servlet和JSP技术的支持,而不仅仅是提供静态文件服务。

Tomcat 运行流程

Tomcat的运行流程可以简单概括为以下几个步骤:

  1. 启动阶段:
    • 当Tomcat启动时,会加载并初始化服务器组件。在这个阶段,Tomcat会读取配置文件(主要是 server.xml 文件)来配置连接器、虚拟主机、Web应用等信息。
  2. 监听端口:
    • 连接器(Connector)负责监听指定的端口,等待客户端的请求。常见的连接器有HTTP/1.1连接器、AJP连接器等。
  3. 接收请求:
    • 当有请求到达时,连接器将请求交给 Catalina 容器进行处理。Catalina是Tomcat的Servlet容器,负责处理Servlet和JSP。
  4. 处理请求:
    • Catalina根据请求的URL和虚拟主机等信息,将请求交给对应的Host(主机)。Host根据Context的配置将请求分发给对应的Web应用。
  5. Web应用处理:
    • Web应用处理阶段涉及多个组件,包括ClassLoader、Context、Wrapper等。ClassLoader加载Web应用的类,Context提供Web应用的运行环境,Wrapper负责实际的Servlet执行。
  6. Servlet执行:
    • 当请求到达目标Servlet时,Wrapper将请求转发给Servlet进行处理。Servlet执行相应的逻辑,生成响应内容。
  7. 生成响应:
    • Servlet生成响应后,将响应返回给Catalina容器,然后由Catalina返回给连接器。
  8. 连接器响应:
    • 连接器将响应发送给客户端,完成请求-响应周期。
  9. 关闭阶段:
    • 当Tomcat关闭时,会执行关闭阶段。在这个阶段,Tomcat会释放资源、关闭连接器等。

简单介绍 Servlet

Servlet(Serverlet)是Java编写的服务器端程序,主要用于扩展服务器功能。Servlet在Web开发中广泛使用,它是Java EE(Enterprise Edition)平台的一部分,用于处理Web请求和响应

K8S

基本组件和概念

你在项目中用到了哪些功能

pod网络

笔试刷题错题

数组声明

左半部分不应该指定大小,下面这种都不是 Java 的

1
2
int[5] a= {1,2,3,4,5};
Integer[2][2] a = {{new Integer(1), new Integer(2)},{new Integer(3),new Interger(4)}};

此外,创建二维数组的时候不能先创建第二层,至少也得先声明第一层的

1
2
Float[][] a = new Float[][5]; //这种就不对
Fload[][] a = new Float[5][]; //这种可以

声明作用域

private: 仅限类的内部

pakage: 包内类可以访问

default: 同一个包可见

protect: 包内类可以访问,另外包外子类也可以访问

抽象类

抽象类可以实现接口,且不必实现接口中的抽象方法

抽象类可以没有抽象方法,也可以有抽象方法+具体方法

抽象类和接口不能实例化

包装类

包装数据类型变量 ++a

核心考点:

  • 数据机器级表示
  • 包装类自动装箱拆箱

核心解法:

  • ++a先是触发拆箱操作Byte.byteValue,得到基本类型的值127,然后执行+1操作,使得值变为-128,最后触发装箱操作Byte.valueOf将value=-128的Byte对象赋值给a
  • 至于为什么是 -128 这是CSAPP和计算机组成原理的知识
  • add方法里那一段其实没有任何作用,实际上这个和自增也没啥关系,就算是你改成 b = ++b 由于包装类作为参数的时候传递的是值而不是引用,因此无论是 ++b 还是 b++ 都不会修改到 test() 声明的 b 的值

多线程

一个新创建的线程并不是自动的开始运行的,必须调用它的start()方法使之将线程放入可运行态(runnable state)

集合

ArrayList 初始化之后第一次扩容的逻辑:通过新建一个数组,再通过 System.arrayCopy 的方式将旧数据移植新数组中

数据库

事务相关的指令

Commit 用于事物的显示提交

savepoint 用于在 sql 语句中设置事物保存点,用法为 savepoint 保存点名称 ,在 rollback 时使用

rollback [work] to [savepoint] 回滚到某个保存点

需要注意的是我们都知道 commit 和 rollback 但是 savepoint 不是很熟


正常执行完DDL语句。包括 create,alter,drop,truncate,rename

正常执行完DCL语句。包括 grant,revoke

正常退出数据库管理软件,没有明确发出 commit 或者 rollback

除了基本的查询语句与增删改的对表的操纵语句外基本都是隐式提交的,使用时要注意。

视图

在定义视图的 SELECT 语句后的字段列表中使用 DISTINCT 、聚合函数 、 GROUP BY 、 HAVING 、UNION 等,视图将不支持INSERT、UPDATE、DELETE

UNION

UNION ALL不去重,UNION去重

别名

别名不可以用’’引起来
别名可以用””引起来
别名前面用不用as都可以

索引

索引是记录文件位置的特殊文件结构,他是保存在磁盘上的,所以不能直接被sql引用

纯概念

数据库的独立性分为逻辑独立性和物理存储独立性。

逻辑独立性是指应用程序独立与数据库的逻辑结构,当模式发生改变时,外模式不变从而使应用程序不变。

物理独立性是指应用程序独立于数据的物理存储结构,当内模式发生改变时,模式不会发生改变,从而外模式也不会发生改变,从而使得应用程序不发生改变。

模式/外模式映射保证数据库的逻辑独立性

内模式/模式映射保证数据库的物理独立性。

数据库范式

1NF:指每一列是原子不可分割

2NF:消除非主属性对主属性的部分依赖,也就是说每列都依赖主键,而不能与主键的部分相关,也就是不存在依赖与联合主键的情况

3NF:关系中既不存在部分依赖,也不存在传递依赖的关系

操作系统

空间计算

7000H = 0111 0000 0000 0000

8K=2^13 = 0010 0000 0000 0000

初始地址为7000H,则其8K长度的地址的终点为

7000H + 8K - 1=&nbsp;0111 0000 0000 0000 +&nbsp;0010 0000 0000 0000 - 1

=0111 0000 0000 0000&nbsp;

+0001 1111 1111&nbsp;1111
=1000 1111 1111 1111=8FFFF(十六进制)

计算机网络

TCP重传

TCP使用超时事件冗余ACK来检测数据包的丢失。

  • 当超时事件发生时,TCP会执行重传
  • 此外,TCP还使用冗余ACK(冗余确认)来快速检测数据包的丢失,并在收到冗余ACK时触发快速重传

在TCP中,超时时间是动态调整的,根据网络的延迟和拥塞情况进行调整,而不是固定的

TCP接收方通常需要对乱序到达的分组进行确认。TCP协议使用累积确认机制,接收方会发送一个确认号表示已成功接收到这个序号之前的所有数据。如果接收方收到乱序的分组,它仍然会确认已经接收到的最后一个按序到达的分组,以指示发送方不需要重传已经成功接收的数据,但仍然需要重传丢失的数据。

JVM

垃圾回收策略

新生代收集器: Serial、 ParNeW、Parallel Scavenge

老年代收集器: Serial Old、 Parallel Old、 CMS

整堆收集器: G1 (统管新生代和老年代)

释放指定内存空间

在Java中,释放掉一个指定占据的内存空间的方法是:

A:调用system.gc()方法

B:调用free()方法

C:赋值给该对象的引用为null

D:程序员无法明确强制垃圾回收器运行

在Java中,释放内存的具体操作是由垃圾回收器(Garbage Collector)负责的,而不是由程序员手动释放。因此,程序员不能直接明确强制垃圾回收器运行

A:System.gc()方法调用是建议垃圾回收器运行,但并不能保证立即执行。

B:free()方法在Java中通常不是程序员用来释放内存的方法,而是在C语言中使用。

C:赋值给该对象的引用为null并不能直接释放内存。垃圾回收器会在适当的时候发现这个对象没有被引用,然后将其回收

D:程序员无法明确强制垃圾回收器运行,这是Java设计的一部分,以避免程序员过多地涉及内存管理。

数据结构

可以判断有向图是否带环的算法

  • DFS
  • 拓扑排序

OLK

SPI 机制

SPI和API

简单来说,API是接口和实现都放在实现方,实现对调用者透明,是实现方定义了接口的规则调用者调用实现方提供的接口

而SPI是接口由调用方来定义规则,实现方基于调用方接口的定义,来实现调用方定义的接口

核心实现

外部函数注册&下推

继承抽象类

注绑定字符串,数字运算,日期相关的函数

MariaClientModule绑定配置

注册绑定 Client Module 的时候针对配置文件中的 URL 处理,MySQL 和 MariaDB 实现不一样

MariaDB 由驱动提供的 UrlParser类来实现

此外获取数据库的时候一些判断也根据不同的 Driver 进行上层的实现

编写流程

注册 Module,配置一些连接器元数据信息,包括 catalog schema 名称等

绑定 Client 对象,外部函数以及配置文件的信息(实体化为Config类)

MariaClient 代表一个 JDBC 客户端对象,基于JDBC来操作数据源,是在BaseJdbcClient基础上针对MairaDB的一套适配,一些异常ErrorCode进行了适配

最难的问题

在集成测试的时候,社区要求的引入的 airlift 包里通过代码创建了 mysqld 进程,但是默认是root身份启动,不允许,因此学习修改源码修改启动的 mysqld 的相关配置,允许 root 启动


Java 八股
http://example.com/2024/01/05/Java 面试/
作者
Noctis64
发布于
2024年1月5日
许可协议