八股!!!!!!!

集合

List,Set,Map 三者结构

list:有序,可重复

set:不可重复

map:键值对存储,key是无序的、不可重复;value是无序的,可重复

ArrayList的原理

动态数组

基于数组

动态数组的底层就是一个Object[]数组

数组在内存中是 连续 存储的,这意味着通过索引访问元素非常快

快速随机访问 : 通过索引 i 访问元素的时间复杂度为 O(1)。

扩容

数组的长度是固定的,但 ArrayList 可以存储任意数量的元素,有动态扩容机制。

触发:调用add方法的时候,先进行容量检查,只有当列表中的实际元素个数 等于 内部数组的长度 (elementData.length) 时,才会触发扩容流程。

计算新容量:旧容量的1.5倍

为什么:太少会频繁扩容,太多会浪费,1.5倍是折中方案

增删改查

查询,修改是O1。直接新增在尾部添加O1,触发扩容是On。

指定位置add是On,整体向后移动。删除同指定位置add。

不是线程安全

并发问题: 在多线程环境下,如果一个线程正在进行扩容或修改 size 变量,而另一个线程同时访问或修改,可能导致数据不一致或抛出 ArrayIndexOutOfBoundsException

一般不解决,因为实际个人使用的时候都涉及不到ArrayList需要高并发的场景,一般使用在:

局部变量 : 列表作为方法内部的临时变量,只在当前线程内部可见和使用,根本不存在线程共享问题。

单线程应用: 传统的桌面应用、工具类或某些后端任务的某个阶段是严格单线程执行的,自然不需要同步。

HashMap的原理

底层实现是什么样的

JDK1.7之前:数组+链表

JDK1.8及之后:数组+链表+红黑树

红黑树的作用
红黑树是自平衡二叉搜索树,查询效率是O(logn),链表是O(n),链表较短相差不大,链表越长链表性能下降

链表的作用

解决哈希冲突

HashMap 的底层是 数组+链表(+红黑树),这里面数组存储的是一个对象引用也就是内存地址,内存地址指向链表的Node对象。

我们知道数组对象创建在堆内存中,那对象引用也在堆内存中,链表也在堆内存中

核心的PUT方法的实现

首先是计算哈希值:首先调用 key.hashCode() 计算原始哈希值,然后通过扰动函数对高16位和低16位进行异或运算。目的是为了增加低位的随机性,减少哈希冲突

计算数组下标:使用 (n - 1) & hashn是数组长度)来计算元素应该存放在哪个桶里。这本质上是一个取模运算,但因为使用了位运算,效率更高。

1.目标桶为空,直接新建一个Node节点放入。

2.目标桶不为空,那说明发生了哈希冲突,那就遍历该桶上的链表或树。

2.1**如果是链表**:依次比较每个节点的 `key`(先比`hash`,再用`equals`)。如果找到相同的`key`,则覆盖其`value`;如果没找到,则将新节点插入到**链表尾部**(JDK 1.7是头插法,JDK 1.8改为尾插法,避免成环)。

2.2**如果是红黑树**:则按照红黑树的方式插入节点。

怎么树化和扩容

  • 判断是否树化:插入链表后,如果链表的长度大于等于8,并且整个数组的长度大于等于64,则将这个链表转换为红黑树。如果数组长度小于64,则只是进行扩容,而不树化。
  • 判断是否扩容:插入成功后,判断当前元素总数是否超过 容量 * 负载因子(默认负载因子0.75,默认初始容量16)。如果超过,则进行扩容

为什么优先扩容?

数组扩容能减少哈希冲突的发生概率(即将元素重新分散到新的、更大的数组中),这在多数情况下比直接转换为红黑树更高效。红黑树需要保持自平衡,维护成本较高。并且,过早引入红黑树反而会增加复杂度。

是不是线程安全的?怎么解决

1.问题

a.数据覆盖(JDK 1.7和1.8都存在)

就是两个线程同时执行put操作,且计算放到的位置相同,结果数据覆盖了

b.在JDK 1.7中并发扩容可能导致死循环(JDK 1.7)

JDK 1.7:采用头插法转移链表节点。在并发扩容时,重新计算位置并移动节点可能会导致链表形成环形结构。这时候要是来遍历,就会死循环

image-20251026114403264
image-20251026114403264

JDK 1.8的改进:改为尾插法,保证了链表节点在扩容时的顺序不变,从而解决了死循环问题。

数据覆盖等问题依然存在,所以JDK 1.8的HashMap仍然是线程不安全的。

2.解决

其实就是问并发环境下的Map使用问题

a. 使用Collections.synchronizedMap(Map m)(X)

原理:它返回一个包装后的Map,所有方法都用 synchronized 关键字加锁(对象锁)。

缺点:锁的粒度粗,整个Map对象一把锁,并发效率很低。

b. 使用 ConcurrentHashMap(√)

ConcurrentHashMap。在JDK 1.7中它采用分段锁,而在1.8中它优化为使用 synchronized 和 CAS 来锁住单个桶,锁粒度更细,性能非常高。

concurrenthashmap

直接说对比

JDK1.7

数据结构:hashmap是数组+链表结构;concurrenthashmap是分段锁机制,内部是一个Segment数组,每个Segment都类似一个小的hashmap

线程安全:hashmap是非线程安全的,ConcurrentHashMap这个时候已经是线程安全的了,每个Segment都有自己的锁,所以不同的Segment可以被不同的线程访问

性能:hashmap没有锁的开销,单线程好,但是多线程情况下要用其他的同步机制影响性能;ConcurrentHashMap分段锁多线程更优

JDK1.8

数据结构:hashmap是数组+链表+红黑树结构;concurrenthashmap不用分段锁了,改成用CAS+synchronized,也是数组+链表+红黑树结构。

线程安全:hashmap还是不安全的,有数据覆盖问题,concurrenthashmap通过CAS+synchronized保证了线程安全。在put 的时候,首先尝试CAS操作更新这个节点,失败了就用synchronized锁住当前节点,再put。

性能:单线程hashmap引入了红黑树性能高,多线程concurrenthashmap进一步降低了锁的粒度

1.7:分段锁:每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。

1.8:采用 Node + CAS + synchronized 来保证并发安全。数据结构跟 HashMap 1.8 的结构类似,数组+链表/红黑二叉树。
这种模式,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点,不发生hash冲突,就不产生并发,效率好

IO

首先说NIO

image-20251016212853121
image-20251016212853121

BIO:也就是传统的 IO,基于字节流或字符流进行文件读写,基于 Socket 和 ServerSocket 进行网络通信。对于每个连接,都需要创建一个独立的线程来处理读写操作。

NIO 的魅力主要体现在网络编程中,服务器可以用一个线程处理多个客户端连接,通过 Selector 监听多个 Channel 来实现多路复用,极大地提高了网络编程的性能。

AIO:它引入了异步通道的概念,使得 I/O 操作可以异步进行。这意味着线程发起一个读写操作后不必等待其完成,可以立即进行其他任务,并且当读写操作真正完成时,线程会被异步地通知。

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

反射

反射是什么?

“反射就是在程序运行期间,能够直接获取和操作任意一个类的所有信息(比如它的属性、方法、构造器等)的一种能力。

在Java中,一切皆对象,连“类”本身也是一种对象,它就是 Class 类的对象。

反射的优缺点

优点:

  • 灵活性高,动态调整:程序不需要在编译的时候写死类,可以根据外部配置、用户输入等动态加载和操作类
// 通过反射动态加载类并创建实例
        // 编译时完全不知道是什么
        Class<?> clazz = Class.forName(className);
        MessageService service = (MessageService) clazz.getDeclaredConstructor().newInstance();
        
        // 使用服务
        service.sendMessage("Hello World!");
        // 输出:发送短信: Hello World!
        
        // 如果明天想改成邮件服务,只需修改配置文件为:
        // message.service.class=com.example.EmailService
        // 代码一行都不用改,这就是动态性的威力。
  • DI、AOP、注解处理等都是反射为核心
  • 解耦合和通⽤性:通过反射,可以编写更通⽤、可重⽤和⾼度解耦的代码,降低模块之间的依 赖。例如,可以通过反射实现通⽤的对象拷⻉、序列化、Bean ⼯具等。

缺点:

  • 性能开销:反射比直接代码调用慢,因为涉及动态解析、还有验证访问权限等。
  • 安全性问题:因为反射是绕过访问控制的,像private这种字段,让程序破坏了封装性,还能绕过泛型的检查。
  • 代码可读性和维护:过多的使用反射会让代码很复杂很难读懂和调试。

反射的使用场景

  1. IOC:Spring的动态加载管理bean
  2. AOP:动态代理就是实现AOP的手段,动态代理本身就离不开反射,代理的对象在调用真实方法的对象的时候,就是通过反射完成的

    动态代理:拦截方法调用并加入额外逻辑
  3. ORM:对象关系映射,绕过权限,反射实现找属性对应的SQL,反过来也是一样

并发

Java怎么创建多线程

1、继承Thread类,重写run方法,start启动

2、实现Runnable接口,重写run方法,创建thread,start启动

3、实现callable接口,重写call方法,创建future,创建thread,start启动,future获取返回值

4、线程池:........

本质只有一种,就是构造thread类!

JAVA怎么保证线程安全

三要素:原子性,可见性,有序性

乐观锁和悲观锁

为了保证线程安全,可以使用 synchronized 关键字对方法加锁,对代码块加锁。线程在执行同步方法、同步代码块时,会获取类锁或者对象锁,其他线程就会阻塞并等待锁。原子+可见+有序

如果需要更细粒度的锁,可以使用 ReentrantLock 并发重入锁等。原子+可见+有序

如果需要保证变量的内存可见性,可以使用 volatile 关键字。可见+有序

对于简单的原子变量操作,还可以使用 Atomic 原子类。原子+可见+有序

对于线程独立的数据,可以使用 ThreadLocal 来为每个线程提供专属的变量副本。封闭性

对于需要并发容器的地方,可以使用 ConcurrentHashMap等。原子+可见+有序

悲观锁实现方式

每次在获取资源操作的时候都会上锁,其它线程阻塞。

synchronized和ReentrantLock等独占锁就是悲观锁思想

乐观锁实现方式

线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源。

版本号机制

简而言之,就是在数据库表中加入一个新字段 version,代表数据被修改的次数。修改一次就加一。

所以在线程要读取数据的同时也会读取 version 值,提交更新的时候,如果刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

CAS

Compare And Swap比较交换

例子:LongAdder,AtomicInteger

他的思想很简单:就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。同时CAS是一个原子操作。

在Java中,它主要通过 sun.misc.Unsafe 类提供的方法,并由 java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)封装后供我们使用。

CAS的理念非常直观,就是‘我认为值应该是A,如果是的话就把它改成B;如果不是,说明有人动过了,那我就不修改了’。它涉及三个核心操作数:

  • V(Variable): 要更新的内存值。
  • E(Expected): 线程预期的原始值。
  • N(New): 线程希望更新成的新值。

在执行更新前,先读取主内存中的值V。

在进行更新时,将V与之前读到的预期值E进行比较。

  • 如果相等,说明在这期间没有其他线程修改过V,那么就可以安全地将N值写入V,操作成功。
  • 如果不相等,说明值已经被其他线程修改了,当前线程的预期值E已经过时,那么本次更新操作失败,不会写入。但是是允许再次尝试的。

ABA问题

就是说如果V原本是A,直到要给V赋值的时候他还是V,不能说他没被修改过,因为可能在这段时间他已经被修改过然后又修改成A了。

有什么危害呢?----- 值看起来没变,但背后的状态或依赖关系已经变了。CAS只检查值是否相等,而不检查值是否“被修改过又改回来”。

ABA怎么解决的?

在变量前面追加上版本号或者时间戳

compareAndSet() 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

开销大问题

就是CAS会经常自旋的重试,不成功就一直循环执行直到成功,如果长时间不成功,CPU开销很大。

如果 JVM 能够支持处理器提供的pause指令,那么自旋操作的效率将有所提升。pause指令有两个重要作用:

  1. 延迟流水线执行指令pause指令可以延迟指令的执行,从而减少 CPU 的资源消耗。具体的延迟时间取决于处理器的实现版本,在某些处理器上,延迟时间可能为零。
  2. 避免内存顺序冲突:在退出循环时,pause指令可以避免由于内存顺序冲突而导致的 CPU 流水线被清空,从而提高 CPU 的执行效率。

对比

缺点场景为什么
乐观失败并重试,回滚数据多读场景,竞争较少乐观锁在读取和处理数据阶段不加锁不占用资源,不频繁加锁
悲观死锁,阻塞事务多写场景,竞争激烈排队成本小于不断重试的成本,不用一直回滚

ThreadLcoal

是什么

ThreadLocal是一种用于实现线程局部变量的工具类。

每个线程访问的变量副本都是独立的,避免了共享变量引起的线程安全问题。由于 ThreadLocal 实现了变量的线程独占,使得变量不需要同步处理,因此能够避免资源竞争。

ThreadLocal 可用于跨方法、跨类时传递上下文数据,不需要在方法间传递参数。

源码

ThreadLocalMap 虽然被叫做 Map,但它并没有实现 Map 接口,是一个简单的线性探测哈希表。

底层的数据结构也是数组,数组中的每个元素是一个 Entry 对象 ,key 是 ThreadLocal 对象,value 是线程的局部变量。

底层原理

每个Thread线程对象内部,都维护了一个私有的、特殊的Map——ThreadLocalMap。

最终的变量是放在了当前线程的ThreadLocalMap 中,并不是存在Threadoca1上.

ThreadLocalMap的key是ThreadLocal对象本身(使用弱引用,这是导致内存泄漏的根源!),value是你存储的变量副本。

类中可以通过 ThreaThrealLocald.currentThread()获取到当前线程对象后,直接通过 getMap(Threadt)可以访问到该线程的 ThreadLocalMap 对象。

ThreadLocalMap使用线性探测法解决哈希冲突,而不是像HashMap那样使用链表或红黑树。如果计算出的位置已被占用,就顺序向后查找下一个空槽。

内存泄露

是什么

就是如果有一个对象在内容空间里,你不需要他了,但是从代码层面无法释放他,而垃圾回收器还以为这个对象不能回收,就是内存泄露。

为什么出现

先聊:引用的差异(GC:垃圾回收)

强引用:我们能看到的显式的声明基本都是强引用,只要强引用链存在,垃圾回收器永远不会回收该对象。 **只有当所有指向该对象的强引用都断开,对象才会在下一次垃圾回收时被标记并回收。**

**任何new出来的对象或者赋值操作都是强引用!**比如`String str = new String("Hello");`、`class Person { private final Car myCar = new Car(); }`

弱引用:需要我们继承weakReference。而被弱引用的对象,如果没有其他的强引用或软引用,**下一次GC必然会被回收。**

Thread有三个角色:

  1. Thread对象
  2. ThreadLocalMap 对象
  3. Entry 对象,其中KeyThreadLocal 对象本身,Value通过set方法创建

1、ThreadThreadLocalMap是强引用,只要Thread存活,ThreadLocalMap就一直存活

ThreadLocalMapEntry是强引用

2、EntryKey也就是ThreadLocal是弱引用

3、EntryValue是强引用。

4、业务方法对ThreadLocal是强引用

正常线程场景下:线程执行完销毁之后,Thread对象会被回收,整个引用链都不存在,没有内存泄露的问题

线程池场景下:

SpringBoot的默认容器可就是tomcat,而tomcat底层是基于线程池实现的,每一条请求都对应一条线程。所以只要使用主流框架开发使用Thread就是线程池场景下

线程池场景下一个线程会不断重复使用,不会销毁Thread。

业务代码→ThreadLocal(key)强引用

Entry→ThreadLocal(key)弱引用

强引用消失,然后弱引用消失,Key被回收了

现在:Thread强引用Map强引用Entry(key=null)强引用Value

内存泄露了,因为Key是null永远都处理不了这个Value了。

JAVA中的锁

synchronized

是什么?干什么?

Java 早期版本中,synchronized 属于 重量级锁,效率低下

Java 6 之后, synchronized 引入了大量的优化如自旋锁、自旋锁、偏向锁、轻量级锁等技术来减少锁操作的开销--->现如今JDK 源码、很多开源框架都大量使用了 synchronized 。

底层原理

(JVM级别)synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令--

monitorenter 指向同步代码块的开始monitorexit 指向同步代码块的结束。

synchronized 是可重入锁吗?

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

使用方式

修饰实例方法(锁当前实例方法)

synchronized void method() {
    //业务代码
}

修饰静态方法(锁当前类,因为静态方法属于整个类)

synchronized static void method() {
    //业务代码
}

修饰代码块

ReentrantLock

是什么?干什么?

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

ReentrantLock、synchronized 区别

可重入锁方面

都是可重入锁,Lock的所有实现类都是可重入的

实现方式

synchronized是依赖于JVM实现的,1.6之后为synchronized在虚拟机层面做了很多优化

ReentrantLock是依赖于JDK实现的,用API实现的---lock、unlock、try/catch

更多

ReentrantLock还有四个功能

等待可中断:就是说在当前线程等待获取锁的时候,其他线程来中断当前线程,当前线程会抛异常,可以捕捉异常

公平锁:(默认非公)可以指定是公平锁还是非公平锁,synchronized只能是非公平锁。

公平锁就是先等待的线程先获得锁,线程进来直接开排

非公平锁就是线程进来直接申请锁,获取不到再排

基础

特性

代码的运行过程

从源代码到运行,两部分:

编译阶段

源代码到字节码

步骤:.java文件通过javac编译器输出.class字节码文件

javac是JDK自带的工具

.class是一种JVM可以理解的指令集

运行阶段

字节码到机器码

在JVM中操作。

1、类加载:JVM启动,类加载器负责找到磁盘上的.class文件,加载到JVM的内存上

2、字节码执行:JVM采取解释器和JIT编译器(也就是运行时编译)协作

只有解释器的时候:解释器把加载到内存上的字节码逐条翻译成机器码,然后执行。

解释器+JIT编译器:有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),被JVM认定成热点代码之后,就会把整个代码块一次性编译成机器码

3、编译之后的机器码会被缓存起来,再执行代码的时候,JVM直接运行缓存的机器码

机器码的运行效率远远高于解释器逐条翻译的效率

编译与解释共存的语言

为什么要有JVM

1、跨平台:字节码是依托于JVM运行的,然后由各个平台专属的 JVM 来翻译成当地的机器码执行,JVM把底层系统和代码隔离开,做了一个中间件。

2、GC机制:自动垃圾回收,不用像C++手动释放内存

3、JIT解释器:如上,JIT实现了高效运行热点代码,速度接近原生语言。

类型

Java的数据类型

  • byte -> Byte (1字节)
  • short -> Short (2字节)
  • int -> Integer (4字节)
  • long -> Long (8字节)
  • float -> Float (4字节)
  • double -> Double (8字节)
  • char -> Character (2字节)
  • boolean -> Boolean

(占用字节,对应包装)

包装类型和基础类型的区别

默认值:基础是0,包装是null

存储位置:基础是栈,对象是堆,栈存的是引用

比较方式:基础==,包装是equals

包装类型的缓存机制

Boolean就两个状态就不谈了

Byte、Short、Integer、Long是默认创造 -128到127 的缓存数据

Character默认创造 0到127 的缓存数据

Integer i1 = 40;

Integer i2 = new Integer(40);

System.out.println(i1==i2);

答案是false

i1触发装箱,用的缓存中的; i2用的new的新的对象

自动装箱

Integer i = 10;  //装箱
int n = i;   //拆箱

装箱:基本类变成包装类 本质:包装类的valueOf()方法

拆箱:包装类变成基本类 本质:调用了 xxxValue()方法。

从字节码中,我们发现装箱其实就是调用了 ,拆箱其实就是

因此,

●Integer i = 10 等价于 Integer i = Integer.valueOf(10)●int n = i 等价于 int n = i.intValue();

Java类关键字方法

Static

类型作用/特点内存位置(JDK 1.8+)访问方式
静态变量属于类,所有实例共享一个副本,在类加载时初始化。存储在堆上。类名.变量名对象名.变量名 (不推荐)
静态方法属于类,可以直接通过类名调用。不能使用 thissuper,也不能直接访问非静态成员。存储在方法区,执行时依赖栈帧类名.方法名
静态代码块类加载时执行,且只执行一次。优先级高于构造方法。在类加载的初始化阶段执行。自动执行
静态内部类也称为嵌套类。不持有外部类的引用,因此可以独立于外部类实例而存在。独立于外部类,属于类的静态成员。外部类名.静态内部类名

定义常量必须用 static 的原因:

  • 必须性:常量不一定必须使用 static,但若要实现全局共享、唯一性,并且不依赖任何对象实例就可以访问,则必须使用 static
  • 最佳实践:结合 public (可见性)、static (类级别) 和 final (不可变性),实现标准的全局编译期常量

this

this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针

this 的用法在 Java 中大体可以分为 3 种:

  1. 普通的直接引用,this 相当于是指向当前对象本身
  2. 形参与成员变量名字重名,用 this 来区分:

    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
  1. 引用本类的构造方法

final

①当 final 修饰一个类时,表明这个类不能被继承。比如,String 类、Integer 类和其他包装类都是用 final 修饰的。

②当 final 修饰一个方法时,表明这个方法不能被重写(Override)。也就是说,如果一个类继承了某个类,并且想要改变父类中被 final 修饰的方法的行为,是不被允许的。

③当 final 修饰一个变量时,表明这个变量的值一旦被初始化就不能被修改。

如果是基本数据类型的变量,其数值一旦在初始化之后就不能更改;如果是引用类型的变量,在对其初始化之后就不能再让其指向另一个对象。

==、eq、Hash

==

  • 对于基本类型来说,是比较他们的数值
  • 对于引用类型来说,是比较对象的内存地址,也就是是否指向同一个实例

equals

  • 不能判断基本数据类型,只能判断引用数据类型(对象)是否相等
  • equals存在于Object类中,所以所有的类都有equals方法

不被重写:比内存地址;被重写,一般自己写都是比较里面的属性值

hashCode

1、作为第一层过滤

2、快速定位存储位置:
当你向 HashMapHashSet 中存储或查找一个对象时,集合不会遍历整个列表去查找,而是依赖 hashCode()

那为什么要同时提供这俩个方法?

在⼀些容器(⽐如 HashMap 、 HashSet )中效率更高

那为什么不只提供hashCode?

因为hashCode值相等并不代表对象相等

总结:

如果hashCode值相等,不一定相等(哈希碰撞)。

如果hashCode值相等,同时equals方法也返回true,一定相等。

如果hashCode值不相等,一定不相等

String

String 为什么是不可变的?

误区:String并非是因为 final 关键字修饰字符数组导致的不可变

实际:1.保存字符串的数组被 private final修饰 2.String 类本身也被 final 修饰,防止被继承后通过子类破坏不可变性。

StringStringBufferStringBuilder 的区别?

可变性

String是不可变的。StringBufferStringBuilder 都继承⾃ AbstractStringBuilder ,可变。

线程安全性

String中的对象是不可变的,也就可以理解为常量,线程安全。

StringBuffer 对⽅法加了同步锁或者对调用的方法加了 同步锁,所以是线程安全的。

StringBuilder 并没有对⽅法进⾏加同步锁,所以是非线程安全的

AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共⽗类,定义了⼀些字符串的基本操作,如 、append 、 insert 等公共⽅法。
性能

String每次改变,都得生成一个新的String对象,然后把指针指向新的String对象

StringBuffer每次都会对自己对象本身进行操作,而不是生成新的

StringBuilder性能比StringBuffer高15%,但是冒线程不安全的风险。

总结

操作量很少的时候:String

单线程下操作量多:StrtingBuilder

多线程下操作量多:StrtingBuffer

泛型

泛型是什么?作用?

参数化类型,也就是把数据类型也当做一个参数,传入变量的数据类型如果和泛型不匹配,编译器就直接报错,就不用运行的时候才再发现问题了

1、提高了程序代码的可读性。

2、类型安全检测

泛型的使用方式

泛型类、泛型接⼝、泛型⽅法。

项目里哪里用到了泛型?

统一返回类型Result。beanutils进行copy

面向对象

三大特性

封装

定义:将对象的状态(属性)和行为(方法)组合在一起,把对象的属性私有化,提供一些可以别外界访问的方法

实践体现:使用 private 修饰属性,防止直接访问。提供 publicGetter/Setter 方法来读取和修改属性。

继承

定义:允许一个类(子类)继承另一个类(父类/基类)的属性和方法。子类可以使用父类的非私有成员,并可以在此基础上进行扩展。

实践体现:代码复用,extends

多态

定义:指允许同一个行为(方法)作用于不同的对象时,产生不同的执行结果。

条件:有继承或实现关系,子类重写父类,父类引用指向子类对象: Parent p = new Child();调用的是子类方法。

重载重写区别

如果一个类有多个名字相同但参数个数不同的方法,我们通常称这些方法为方法重载。

如果子类具有和父类一样的方法(参数相同、返回类型相同、方法名相同,但方法体不同),我们称之为方法重写。

抽象类接口区别

类型抽象接口
方法实现普通+抽象,可以部分实现只能抽象,规范行为,必须实现
理念是什么,继承能做什么,实现
参数都可只能定义常量

内存

Java内存区域

大块有:栈和堆

类型
线程范围线程私有线程共享
存储内容栈帧存放所有的对象实例和数组
生命周期随线程生灭,随方法调用而进出栈JVM启动时创建,GC决定何时销毁对象
分配和回收随方法进出栈,固定,不需要GCGC自动管理

栈帧:局部变量表:存放方法参数、方法内部定义的局部变量(主要是基本数据类型和对象的引用地址)。

深拷贝和浅拷贝了解吗?引用拷贝?

浅拷贝:在堆上新建一个对象,但是共用一个内部对象.

深拷贝:完全复制一个对象,包括内部对象.

引用拷贝:两个不同的引用指向同⼀个对象.

设计模式

是一个蓝图或者说模版,描述了如何组织类和对象解决设计难题。

原则

单一职责原则

核心:一个类或模块应该只有一个原因会导致变化

理解:一个类只做一个事情,如果一个类做多个事情,那当一个事情变化的时候,有可能会影响其他事情的实现

开闭原则

核心:软件实体应该对扩展开放,对修改关闭

理解:当需要添加一个新的功能的时候,应该增加新的代码来实现,而不是修改已经有的并且工作良好的代码,这要是面向对象设计的重要原则-->系统复用性、维护性

里氏代换原则

任何父类可以出现的地方,子类也一定可以出现。

LSP 是继承复用的基石,只有当子类可以替换掉父类,并且单位功能不受到影响时,父类才能真正被复用,而子类也能够在父类的基础上增加新的行为。

接口隔离原则

依赖倒置原则

高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。这意味着设计时应该尽量依赖接口或抽象类,而不是实现类。IOC

单例模式

单例模式属于创建型模式,⼀个单例类在任何情况下都只存在⼀个实例,构造方法必须是私有的、 由自己创建⼀个静态变量存储实例,对外提供⼀个静态公有方法获取实例

饿汉式--线程安全

顾名思义,类一加载就创建对象

如何保证线程安全?基于类加载机制避免了多线程的同步问题,如果被不同的类加载器加载就会创建不同的实例。

优点:实现简单,天生线程安全

缺点:不是懒加载,如果实例一直没被用过,就是内存浪费

懒加载 (lazy loading):使用的时候再创建对象

懒汉式--线程不安全

意思就是只有在被第一次使用时才会创建实例

优点:实现懒加载

缺点:线程不安全

public static Singleton getInstance() {
 // 判断为 null 的时候再创建对象
if (instance == null) {
 instance = new Singleton();

懒汉式--同步方法

基于普通版本,加入synchronized

    //定义⼀个静态变量指向⾃⼰类型
    private static Singleton instance; 
public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }

非常影响性能,每次获取实例时需要加锁和释放锁

懒汉式--双重检查锁

可见性:当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取

指令重排:instance = new Singleton();实际上分三步

1、为instance分配内存

2、初始化instance

3、将instance指向分配的内存地址

JVM有指令重排的特性,执行顺序有可能变成1->3->2。单线程下不会有问题,多线程下就会导致一个线程获得还没有初始化的实例。

在13执行完还没执行2 的时候另一个线程发现instance不为空,返回了instance,但是instance还未初始化

    //用volatile修饰,保证可见性和禁止指令重排
    private static volatile Singleton instance; 
public static Singleton getInstance() {
        // 第⼀次检查:避免不必要的同步
        if (instance == null) {
            // 第⼆次检查:在同步块内部确保只有⼀个线程创建实例
            synchronized (Singleton.class) {
                if (instance == null) {
                    // new 操作不是原⼦性的,需要 volatile
                    instance = new Singleton();
                }
            }

外层检查:如果实例已经创建了,直接返回结果就行,不需要进入方法内部了,少了大比开销。

内层检查:当多个线程同时通过外层检查时,同步块保证只有一个线程进入创建逻辑,后续线程都会被拦截。

工厂模式

简单工厂模式

抽象产品、具体产品1、具体产品2、简单工厂——>根据类型创建设备

优点:结构简单,将创建逻辑集中管理

缺点:违反了开闭原则,新增产品,工厂类就得添加新的逻辑

工厂方法模式

抽象产品、具体产品1、具体产品2、抽象工厂、具体工厂1、具体工厂2

优点:完美遵循开闭原则,扩展性好。创建逻辑被分散到各个具体工厂中,符合单一职责原则。

缺点:类数量很多,新增产品,就得添加一个具体工厂类

实例:Collection接口,里面的ArrayList和LinkedList都实现了这个接口,但是各自的iterator() 方法返回的是不同的迭代器实现类

抽象工厂模式

抽象产品A、抽象产品B、具体产品A1、具体产品A2、具体产品B1、具体产品B2、抽象工厂、具体工厂1(A1B1)、具体工厂2(A2B2)

优点:创造一系列相互匹配的产品,切换产品族,只需要更换具体的工厂。

缺点:扩展新的产品类型,全得改

实例:更换软件皮肤 比如Win和Mac的UI组件成套更换

观察者模式

核心思想是定义一个一对多的关系,被观察者状态发生改变之后,所有依赖于他的对象(观察者)都会自动收到通知并进行更新

四个角色:

  1. 主题/被观察者:一个接口,维护一个观察者列表,并且提供添加、删除、通知所有观察者三个方法
  2. 具体主题:实现主题,包含业务逻辑,并且在自身状态发生改变之后,通知所有观察者
  3. 观察者:一个接口,定义了Update方法,当观察者收到主题的通知之后,update被调用
  4. 具体观察者:实现观察者,Update方法中,会根据自己收到的通知,完成具体的逻辑,是更新状态还是执行什么操作

例子:支付成功后,触发一堆独立的后续操作,加积分,减库存,发邮件等

   @Override
    public void notifyObservers() {
        // 遍历列表,调用每个观察者的update方法
        for (PaymentObserver observer : observers) {
            observer.update(this.orderId);
        }
    }

    // 这个就是 setState() 方法!当支付成功时,网关回调会触发它。
    public void setState(String orderId) {
        this.orderId = orderId;
        // 状态改变,立即通知所有观察者
        notifyObservers();
    }
    
// 支付控制器,接收支付网关的回调
public class PaymentCallbackController {

    @Autowired
    private PaymentSuccessSubject paymentSuccessSubject;
    @PostMapping("/payment/callback")
    public String handlePaymentCallback(@RequestParam String orderId) {
        // 支付网关确认支付成功...
        System.out.println("支付网关回调,订单 " + orderId + " 支付成功!");
        // 核心:只需要调用主题的setState方法,所有后续业务自动触发!
        paymentSuccessSubject.setState(orderId);
        return "callback processed";
    }

优点:

  1. 高度解耦,主题只需要知道有一系列观察者,不需要知道是什么
  2. 符合开闭原则,添加新的行为,只需要创建新的具体观察者注册就好了
  3. 广播通知

缺点:

  1. 性能问题,如果观察者数量巨多或者某个观察者的update方法很复杂,通知的时候就会导致主线程的阻塞(可以考虑异步通知优化)
  2. 可能导致意外的循环,如果观察者之间还有依赖关系,一个观察者更新可能影响另一个主题的更新,变得越来越复杂,可以会导致循环调用。

责任链模式(项目)

将请求的发送者和接受者解耦,创建一个处理请求的接收者链来处理请求。每一个接收者都对请求的一部分进行处理,自己决定是不是应该把请求传给下一个接收者,还是说中断请求。

两个角色:

  1. Handler(处理器):是一个接口,定义了处理请求的接口和指向下一个处理器的引用。
  2. ConcreteHandler(具体处理器):实现Handler接口,处理方法的时候,判断自己能不能处理现在的请求,能处理就处理;不能处理就把请求传给下一个处理器。

例子:订单校验(多种校验形成责任链),OA的审批流(多流程审批),Filter过滤器(HTTP请求的过滤,鉴权),Interceptor拦截器

分布式

引入

服务器就叫做节点

多个一样内容的服务器就是集群

nginx就一个路由,对一个集群进行负载均衡

微服务就是把一个项目里的功能都拆分出来,每一个小模块都是一个微服务,同时数据库也可以这样,这样就实现了 数据隔离语言无关

在一个节点中想调用另一个节点的微服务,需要远程调用,也就是RPC

在远程调用之前,需要发现对方在哪个节点,这个过程叫 服务发现

如何知道对方在哪个节点,我们要有一个 注册中心,用来记录服务都在哪里

一个微服务挂了,调用它的也跟着等然后挂了,这个就是 服务雪崩

为了防止服务雪崩,引入一个保护机制比如什么时候失败什么时候接着来,叫做 服务熔断

Nacos

作用是实现 服务发现,注册中心,配置中心

OpenFeign

RPC

Sentinel

服务熔断 流量控制

Gateway

网关 路由 负载均衡

Seata