八股!!!!!!!
集合
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) & hash (n是数组长度)来计算元素应该存放在哪个桶里。这本质上是一个取模运算,但因为使用了位运算,效率更高。
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:采用头插法转移链表节点。在并发扩容时,重新计算位置并移动节点可能会导致链表形成环形结构。这时候要是来遍历,就会死循环
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
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这种字段,让程序破坏了封装性,还能绕过泛型的检查。
- 代码可读性和维护:过多的使用反射会让代码很复杂很难读懂和调试。
反射的使用场景
- IOC:Spring的动态加载管理bean
AOP:动态代理就是实现AOP的手段,动态代理本身就离不开反射,代理的对象在调用真实方法的对象的时候,就是通过反射完成的
动态代理:拦截方法调用并加入额外逻辑
- 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指令有两个重要作用:
- 延迟流水线执行指令:
pause指令可以延迟指令的执行,从而减少 CPU 的资源消耗。具体的延迟时间取决于处理器的实现版本,在某些处理器上,延迟时间可能为零。- 避免内存顺序冲突:在退出循环时,
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有三个角色:
Thread对象ThreadLocalMap对象Entry对象,其中Key是ThreadLocal对象本身,Value通过set方法创建
1、Thread对ThreadLocalMap是强引用,只要Thread存活,ThreadLocalMap就一直存活
ThreadLocalMap对Entry是强引用
2、Entry对Key也就是ThreadLocal是弱引用
3、Entry对Value是强引用。
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+) | 访问方式 |
|---|---|---|---|
| 静态变量 | 属于类,所有实例共享一个副本,在类加载时初始化。 | 存储在堆上。 | 类名.变量名 或 对象名.变量名 (不推荐) |
| 静态方法 | 属于类,可以直接通过类名调用。不能使用 this 和 super,也不能直接访问非静态成员。 | 存储在方法区,执行时依赖栈帧。 | 类名.方法名 |
| 静态代码块 | 在类加载时执行,且只执行一次。优先级高于构造方法。 | 在类加载的初始化阶段执行。 | 自动执行 |
| 静态内部类 | 也称为嵌套类。不持有外部类的引用,因此可以独立于外部类实例而存在。 | 独立于外部类,属于类的静态成员。 | 外部类名.静态内部类名 |
定义常量必须用 static 的原因:
- 必须性:常量不一定必须使用
static,但若要实现全局共享、唯一性,并且不依赖任何对象实例就可以访问,则必须使用static。 - 最佳实践:结合
public(可见性)、static(类级别) 和final(不可变性),实现标准的全局编译期常量。
this
this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this 的用法在 Java 中大体可以分为 3 种:
- 普通的直接引用,this 相当于是指向当前对象本身
形参与成员变量名字重名,用 this 来区分:
public Person(String name,int age){ this.name=name; this.age=age; }
- 引用本类的构造方法
final
①当 final 修饰一个类时,表明这个类不能被继承。比如,String 类、Integer 类和其他包装类都是用 final 修饰的。
②当 final 修饰一个方法时,表明这个方法不能被重写(Override)。也就是说,如果一个类继承了某个类,并且想要改变父类中被 final 修饰的方法的行为,是不被允许的。
③当 final 修饰一个变量时,表明这个变量的值一旦被初始化就不能被修改。
如果是基本数据类型的变量,其数值一旦在初始化之后就不能更改;如果是引用类型的变量,在对其初始化之后就不能再让其指向另一个对象。
==、eq、Hash
==
- 对于基本类型来说,是比较他们的数值
- 对于引用类型来说,是比较对象的内存地址,也就是是否指向同一个实例
equals
- 不能判断基本数据类型,只能判断引用数据类型(对象)是否相等
- equals存在于Object类中,所以所有的类都有equals方法
不被重写:比内存地址;被重写,一般自己写都是比较里面的属性值
hashCode
1、作为第一层过滤
2、快速定位存储位置:
当你向 HashMap 或 HashSet 中存储或查找一个对象时,集合不会遍历整个列表去查找,而是依赖 hashCode()。
那为什么要同时提供这俩个方法?
在⼀些容器(⽐如 HashMap 、 HashSet )中效率更高
那为什么不只提供hashCode?
因为hashCode值相等并不代表对象相等
总结:
如果hashCode值相等,不一定相等(哈希碰撞)。
如果hashCode值相等,同时equals方法也返回true,一定相等。
如果hashCode值不相等,一定不相等
String
String 为什么是不可变的?
误区:String并非是因为 final 关键字修饰字符数组导致的不可变
实际:1.保存字符串的数组被 private final修饰 2.String 类本身也被 final 修饰,防止被继承后通过子类破坏不可变性。
String、StringBuffer、StringBuilder 的区别?
可变性
String是不可变的。StringBuffer和StringBuilder 都继承⾃ 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 修饰属性,防止直接访问。提供 public 的 Getter/Setter 方法来读取和修改属性。
继承
定义:允许一个类(子类)继承另一个类(父类/基类)的属性和方法。子类可以使用父类的非私有成员,并可以在此基础上进行扩展。
实践体现:代码复用,extends
多态
定义:指允许同一个行为(方法)作用于不同的对象时,产生不同的执行结果。
条件:有继承或实现关系,子类重写父类,父类引用指向子类对象: Parent p = new Child();调用的是子类方法。
重载重写区别
如果一个类有多个名字相同但参数个数不同的方法,我们通常称这些方法为方法重载。
如果子类具有和父类一样的方法(参数相同、返回类型相同、方法名相同,但方法体不同),我们称之为方法重写。
抽象类接口区别
| 类型 | 抽象 | 接口 |
|---|---|---|
| 方法实现 | 普通+抽象,可以部分实现 | 只能抽象,规范行为,必须实现 |
| 理念 | 是什么,继承 | 能做什么,实现 |
| 参数 | 都可 | 只能定义常量 |
内存
Java内存区域
大块有:栈和堆
| 类型 | 栈 | 堆 |
|---|---|---|
| 线程范围 | 线程私有 | 线程共享 |
| 存储内容 | 栈帧 | 存放所有的对象实例和数组 |
| 生命周期 | 随线程生灭,随方法调用而进出栈 | JVM启动时创建,GC决定何时销毁对象 |
| 分配和回收 | 随方法进出栈,固定,不需要GC | GC自动管理 |
栈帧:局部变量表:存放方法参数、方法内部定义的局部变量(主要是基本数据类型和对象的引用地址)。
深拷贝和浅拷贝了解吗?引用拷贝?
浅拷贝:在堆上新建一个对象,但是共用一个内部对象.
深拷贝:完全复制一个对象,包括内部对象.
引用拷贝:两个不同的引用指向同⼀个对象.
设计模式
是一个蓝图或者说模版,描述了如何组织类和对象解决设计难题。
原则
单一职责原则
核心:一个类或模块应该只有一个原因会导致变化
理解:一个类只做一个事情,如果一个类做多个事情,那当一个事情变化的时候,有可能会影响其他事情的实现
开闭原则
核心:软件实体应该对扩展开放,对修改关闭
理解:当需要添加一个新的功能的时候,应该增加新的代码来实现,而不是修改已经有的并且工作良好的代码,这要是面向对象设计的重要原则-->系统复用性、维护性
里氏代换原则
任何父类可以出现的地方,子类也一定可以出现。
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组件成套更换
观察者模式
核心思想是定义一个一对多的关系,被观察者状态发生改变之后,所有依赖于他的对象(观察者)都会自动收到通知并进行更新
四个角色:
- 主题/被观察者:一个接口,维护一个观察者列表,并且提供添加、删除、通知所有观察者三个方法
- 具体主题:实现主题,包含业务逻辑,并且在自身状态发生改变之后,通知所有观察者
- 观察者:一个接口,定义了Update方法,当观察者收到主题的通知之后,update被调用
- 具体观察者:实现观察者,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";
}优点:
- 高度解耦,主题只需要知道有一系列观察者,不需要知道是什么
- 符合开闭原则,添加新的行为,只需要创建新的具体观察者注册就好了
- 广播通知
缺点:
- 性能问题,如果观察者数量巨多或者某个观察者的update方法很复杂,通知的时候就会导致主线程的阻塞(可以考虑异步通知优化)
- 可能导致意外的循环,如果观察者之间还有依赖关系,一个观察者更新可能影响另一个主题的更新,变得越来越复杂,可以会导致循环调用。
责任链模式(项目)
将请求的发送者和接受者解耦,创建一个处理请求的接收者链来处理请求。每一个接收者都对请求的一部分进行处理,自己决定是不是应该把请求传给下一个接收者,还是说中断请求。
两个角色:
- Handler(处理器):是一个接口,定义了处理请求的接口和指向下一个处理器的引用。
- ConcreteHandler(具体处理器):实现Handler接口,处理方法的时候,判断自己能不能处理现在的请求,能处理就处理;不能处理就把请求传给下一个处理器。
例子:订单校验(多种校验形成责任链),OA的审批流(多流程审批),Filter过滤器(HTTP请求的过滤,鉴权),Interceptor拦截器
分布式
引入
服务器就叫做节点
多个一样内容的服务器就是集群
nginx就一个路由,对一个集群进行负载均衡
微服务就是把一个项目里的功能都拆分出来,每一个小模块都是一个微服务,同时数据库也可以这样,这样就实现了 数据隔离,语言无关
在一个节点中想调用另一个节点的微服务,需要远程调用,也就是RPC
在远程调用之前,需要发现对方在哪个节点,这个过程叫 服务发现
如何知道对方在哪个节点,我们要有一个 注册中心,用来记录服务都在哪里
一个微服务挂了,调用它的也跟着等然后挂了,这个就是 服务雪崩
为了防止服务雪崩,引入一个保护机制比如什么时候失败什么时候接着来,叫做 服务熔断
Nacos
作用是实现 服务发现,注册中心,配置中心
OpenFeign
RPC
Sentinel
服务熔断 流量控制
Gateway
网关 路由 负载均衡




