JDK和JRE
JDK:开发环境和运行环境。
JRE:运行环境。
静态变量
运行时,Java 虚拟机只为静态变量分配一次内存,加载类过程中完成静态变量的内存分
配。
在类的内部,可以在任何方法内直接访问静态变量。
在其他类中,可以通过类名访问该类中的静态变量。
==和equals
-
equals
默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。
-
==
基本类型:比较的是值是否相同。
引用类型:比较的是引用是否相同。
-
代码解读:因为 x 和 y 指向的是同一个引用,所以 == 也是 true,而 new String()方法则重写开辟了内存空
间,所以 = = 结果为 false,而 equals 比较的一直是值,所以结果都为 true
String x = "string";
String y = "string";
String z = new String("string");
System.out.println(x==y); // true
System.out.println(x==z); // false
System.out.println(x.equals(y)); // true
System.out.println(x.equals(z)); // true
因为内存的分配方式不一样。String x = "string"的方式,java 虚拟机会将其分配到常量池中;而 String z =
new String(“string”)则会被分到堆内存中。
hashCode
- 在散列表中,hashCode()相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。
final
- final 修饰的类叫最终类,该类不能被继承。
- final 修饰的方法不能被重写。
- final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改
Math.round
- (四舍五入)在数轴上取值时,中间值(0.5)向右取整,所以正 0.5 是往上取整,负 0.5 是直接舍弃。
数据类型
-
基本数据类型:(整型)int、long、short、byte;(浮点型)float,double;(字符型)char;(布尔型)Boolean
-
引用数据类型:类、 接口类型、 数组类型、 枚举类型、 注解类型、 字符串型等
-
区别
-
储存位置
基本变量类型:在方法中定义的非全局基本数据类型变量的具体内容是存储在栈中的。
引用变量类型:只要是引用数据类型变量,其具体内容都是存放在堆中的,而栈中存放的是其具体内容所
在内存的地址。
-
传递方式
基本变量类型:在方法中定义的非全局基本数据类型变量,调用方法时作为参数是按数值传递的
引用变量类型:引用数据类型变量,调用方法时作为参数是按引用传递的
-
字符串类
-
String、StringBuffer、StringBuilder
-
区别
-
String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新
的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基
础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
-
StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程
安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多
线程环境下推荐使用 StringBuffer。
-
String 类常用方法
- indexOf():返回指定字符的索引。
- charAt():返回指定索引处的字符。
- replace():字符串替换。
- trim():去除字符串两端空白。
- split():分割字符串,返回一个分割后的字符串数组。
- getBytes():返回字符串的 byte 类型数组。
- length():返回字符串长度。
- toLowerCase():将字符串转成小写字母。
- toUpperCase():将字符串转成大写字符。
- substring():截取字符串。
- equals():字符串比较。
类
- 抽象类不一定非要抽象方法
- 普通类和抽象类的区别
- 普通类不能包含抽象方法,抽象类可以包含抽象方法。
- 抽象类不能直接实例化,普通类可以直接实例化。
- 抽象类不能使用final修饰
- 接口和抽象类的区别
- 实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
- 构造函数:抽象类可以有构造函数;接口不能有。
- main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。
- 实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
- 访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。
IO流
-
按功能来分:输入流(input)、输出流(output)。
-
按类型来分:字节流和字符流。
-
字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位
输入输出数据。
-
BIO、NIO、AIO的区别
- BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
- NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
- AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
Files的常用方法
- Files.exists():检测文件路径是否存在。
- Files.createFile():创建文件。
- Files.createDirectory():创建文件夹。
- Files.delete():删除一个文件或目录。
- Files.copy():复制文件。
- Files.move():移动文件。
- Files.size():查看文件个数。
- Files.read():读取文件。
- Files.write():写入文件。
树
演示过程网址:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
table1 | table2 |
---|---|
1 | 11 |
2 | 3 |
3 | 22 |
4 | 23 |
5 | 29 |
1.二叉树
-
每个节点最多含有两个子树的树称为二叉树
-
原理:二叉树把第一个插入的节点做为根节点,后面插入的数据从根节点开始比较,小于比较节点值的都
走右边,大于或等于都走左边。图中插入三个数都走左边。如果插入的数据都是有序的那么也就不会分叉
就是一条线,如果需要查找一个数,而这个数正好在叶子节点那么就需要比较所以的节点值。
2.平衡二叉树
-
一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
-
原理:插入三个数在没有平衡之前和二叉树一样。二叉树的平衡因子的绝对值不能大于1所以左边0个节点
减右边2个节点等于-2。绝对值大于了1所以进行了旋转。
3.红黑树
-
红黑树是一种近似平衡的二叉查找树,它能够确保任何一个节点的左右子树的高度差不会超过二者中较低
那个的一陪。具体来说,红黑树是满足如下条件的二叉查找树(binary search tree):
-
每个节点要么是红色,要么是黑色。
-
根节点必须是黑色
-
红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色)。
-
对于每个节点,从该点至null(树尾端)的任何路径,都含有相同个数的黑色节点。
-
在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件3或条件4,需要通过调整使得查
找树重新满足红黑树的条件。
-
4.B-tree
-
B树也称B-树,它是一颗多路平衡查找树。B树和B+树都是从最简单的二叉树变换而来的。描述一颗B树时
需要指定它的阶数,阶数表示了一个节点最多有多少个孩子节点,一般用字母m表示阶数。B树是一种自
平衡树,是AVL树的一般化,它维护有序数据并允许以对数时间进行搜索,顺序访问,插入和删除。与
AVL树不同的是,B树非常适合读取和写入相对较大的数据块(如光盘)的存储系统。它通常用于数据库
和文件系统。
-
树中每个结点最多含有m棵子树。(table1是3阶树,最多含有3棵子树)
-
若根结点不是叶子结点,则至少有2个子树。(table1的根结点是0002 000),下面有3个子树)
-
除根结点之外的所有非终端结点至少有⌈m/2⌉棵子树。
-
如果一个结点有n-1个关键字,则该结点有n个分支,且这n-1个关键字按照递增顺序排列。(table1根结点0002 0004有2个关键字,则有3个分支)
-
每个非终端结点中包含信息:(N,A0,K1,A1,K2,A2,…,KN,一)其中:
- Ki(1≤i≤n)为关键字,且关键字按升序排序。
- 指针Ai(0≤i≤n)指向子树的根结点,Ai-1指向子树中所有结点的关键字均小于Ki,且大于Ki-1;
- 关键字的个数n必须满足:⌈m/2⌉-1≤n≤m-1。
- 结点内关键字各不相等且按从小到大排列。
5.B+tree
- B+树是应文件系统所需而产生的B树的变形树
- 一颗m阶的B+树满足如下条件:
- 每个节点最多只有m个子节点。
- 除根节点外,每个非叶子节点具有至少有 m/2(向下取整)个子节点。
- 非叶子节点的根节点至少有两个子节点。
- 有k颗子树的非叶节点有k个键,键按照递增顺序排列。
- 叶节点都在同一层中。
- B+树与B树的差异
B树 | B+树 |
---|---|
有m颗子树的节点中含有 m-1 个关键码 | 有m颗子树的节点中含有 m 个关键码 |
B树中非叶子节点的关键码与叶子结点的关键码均不重复,它们共同构成全部的索引信息 | 所有的叶子结点中包含了完整的索引信息,包括指向含有这些关键字记录的指针,中间节点每个元素不保存数据,只用来索引 |
B 树的非叶子节点包含需要查找的有效信息 | 所有的非叶子节点可以看成是高层索引, 结点中仅含有其子树根结点中最大(或最小)关键字 |
容器
-
常用容器分类
-
Collection 和 Collections
-
Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通
用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集
合提供了最大化的统一操作方式,其直接继承接口有List与Set。
-
Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排
序、搜索以及线程安全等各种操作。
-
-
List、Set、Map 之间的区别
比较 List Set Map 接口继承 Collection Collection 常见的实现类 ArraryList、LinkedList、Vector Collection HashSet、LinkedHashSet、TreeSet 常见方法 add()、remove()、clear()、get()、contains()、size() add()、remove()、clear()、contains()、size() put()、get()、remove()、clear()、containsKey()、keySey()、containsValue()、values()、size() 元素 可重复 不可重复(用equals()判断) 不可重复 顺序 有序 无需(实际上由HashCode决定) 线程安全 Vector线程安全 HashTable线程安全 -
HashMap 、Hashtable区别
- hashMap去掉了HashTable 的contains方法,但是加上了containsValue()和containsKey()方法
- hashTable同步的,而HashMap是非同步的,效率上比hashTable要高。
- hashMap允许空键值,而hashTable不允许。
-
LinkedHashMap
-
LinkedHashMap继承于HashMap,底层基于HashMap和双向链表来实现的。
-
HashMap无序,LinkedHashMap有序,可分为插入顺序和访问顺序两种。
-
如果是访问顺序,那put和get操作已经存在的Entry时都会把Entry移动到双向链表的标为(先删除再插
入)。遍历时按照插入顺序排序,原因在于LinkedHashMap的内部类LinkedHashIterator,执行
Iterator.next访问链表的下一个元素,所以可以按照插入顺序的输出。
-
LinkedHashMap存储数据,还是跟HashMap一样使用Entry的方式,双向链表只是为了保证顺。
-
HashMap的遍历速度和他的容量有关,而LinkedHashMap只跟实际数量有关。
-
LinkedHashMap按照插入顺序排序,HashMap基于哈希表乱序。
-
-
如何决定使用 HashMap 还是 TreeMap
对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key
集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将
map换为TreeMap进行有序key的遍历。
-
map中的默认初始容量是16,16×0.75=12也就是说在键值对小于12时,不会进行扩容,大于等于12时会进行
扩容;另外map的初始容量并不是根据我们传进去的值来作为初始容量,它会经过计算,把第一个大于等于传
入值的2的幂作为初始容量。例如:传一个7,那初始容量就是2的3次方8,传一个9,初始容量就是2的四次方16。
为啥要设置初始容量:设置初始容量是为了提高性能,因为当"键值对数量" > 0.75 × initialCapacity会进行扩容,每次扩容都要重建hash表,是非常影响性能;初始容量设置过大,又会浪费内存,所以非常有必要设置一个合适的初始容量。
-
ArrayList 和 LinkedList 的区别是什么?
ArrrayList底层的数据结构是数组,支持随机访问,而 LinkedList 的底层数据结构是链表,不支持随
机访问.查询用ArrrayList,插入用LinkedList 。
ArrayList 的初始容量是10,当ArrayList中的元素超过10个以后,会重新分配内存空间公式:((旧容量 * 3) / 2) + 1
-
哪些集合类是线程安全的?
vector、enumeration(枚举)
多线程
1.process和thread
程序:指令和数据的有效集合,其本身没有任何的运行含义,是一个静态的概念。
进程:执行程序的一次执行过程,是一个动态的概念,是系统分配的单位。
线程:CPU调度和执行的单位。(在一个CPU的情况下,在同一时间,只能执行一个代码,因为切换很快所以有同步的感觉)
每个任务都好像有自己的CPU一样,而其底层的机制就是切分CPU的时间,也就是CPU将轮流给每个任务分配其占用时间。每个任务都觉得自己在一直占用CPU,而事实上是将CPU时间划分成片段分配给所有的任务。在多个CPU的环境下,多线程的运作,可以极大的提供程序的运行速度。
如:边吃饭边玩手机
2.核心概念
-
线程就是独立的执行路径。
-
在程序运行时,即使自己没有创建线程,后台也有多个线程,如主线程,gc线程。
-
main()称之为主线程,为系统的入口,用于执行整个程序。
-
在一个进程中如果开辟了多个线程,线程的执行由调度器(CPU)安排调度,调度器是系统精密相关的,人为
不能控制。
-
对同一份资源操作时,会存在抢夺资源问题,需要加入并发控制。
-
线程会带来额外的开销,如CPU调度的时间,并发控制开销。
-
每个线程在自己的工作内存交互,内存控制不当会导致数据不一致。
3.创建线程的方式
Thread类
- 自定义线程类,继承Thread类
- 重写run方法,编写线程执行体
- 创建线程对象,调用start方法启动线程
线程不一定立即执行,靠CPU调度
/**
* @author by LiuFAN
* @date 2022/11/19 0010.
*/
//自定义线程方法,继承thread类
public class ThreadTest extends Thread {
//重写run方法
@Override
public void run() {
//线程体
for (int i = 1; i < 20; i++) {
System.out.println("线程体");
}
}
public static void main(String[] args) {
//创建线程对象
ThreadTest dodo = new ThreadTest();
//start方法启动线程
dodo.start();
for (int i = 1; i < 20; i++) {
System.out.println("主线程");
}
}
}
Thread类底层是实现runnable接口,runnable接口里只有一个run方法所以需要重写run方法
runnable接口
- 自定义线程类,实现runnable接口
- 重写run方法,编写线程执行体
- 创建线程对象,传入对象加Thread类调用start方法启动。
//自定义线程方法,实现Runnable接口
public class ThreadTest implements Runnable {
//重写run方法
@Override
public void run() {
//线程体
for (int i = 1; i < 20; i++) {
System.out.println("线程体");
}
}
public static void main(String[] args) {
//创建线程对象
ThreadTest dodo = new ThreadTest();
//创建一个thread对象
Thread thread=new Thread(dodo);
//start方法启动线程
thread.start();
for (int i = 1; i < 20; i++) {
System.out.println("主线程");
}
}
}
callable接口
- 自定义线程类实现callable接口,需要返回值类型
- 重写call方法,需要异常抛出
- 创建目标对象
- 创建执行服务ExecutorService
- 提交结果
- 获取结果
- 关闭服务
//自定义线程类实现callable接口,需要返回值类型
public class ThreadTest implements Callable<Integer> {
// 重写call方法,需要异常抛出
@Override
public Integer call() {
int i;
for (i=1;i<20;i++){
System.out.println("线程体");
}
return i;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建目标对象
ThreadTest dodo=new ThreadTest();
//创建执行服务ExecutorService
ExecutorService service= Executors.newFixedThreadPool(1);
// 提交结果
Future<Integer> future=service.submit(dodo);
//获取结果
Integer integer = future.get();
System.out.println(integer);
//关闭服务
service.shutdown();
for (int i=1;i<20;i++){
System.out.println("主线程");
}
}
}
4.线程有哪些状态
-
创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态。
-
就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状
态。
-
运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。
-
阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
-
死亡状态。如果一个线程的run方法执行结束或者调用stop方法后,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪
线程停止
-
不推荐使用jdk提供的stop,destory方法
-
推荐线程自己停止
-
建议使用一个标志位,终止变量
public class ThreadTest implements Runnable { //设置一个标志去切换 private boolean flag = true; //重写run方法 @Override public void run() { //线程体 int i = 0; while (flag) { System.out.println("线程体" + i++); } } //切换的方法 public void stopThread() { this.flag = false; } public static void main(String[] args) { //创建线程对象 ThreadTest dodo = new ThreadTest(); //start方法启动线程 new Thread(dodo).start(); for (int i = 1; i < 20; i++) { if (i == 10) { dodo.stopThread(); } System.out.println("主线程" + i); } } }
线程休眠(sleep)
- sleep(时间)指当前线程阻塞的毫秒数
- sleep时间到达后,线程处于就绪状态
- 每个对象都有一把锁。sleep不会释放锁
线程礼让(yield)
- 礼让线程,让当前的线程停止但不阻塞
- 让线程从执行状态,变为就绪状态
- cpu重新调度,但是不一定成功
线程强制执行(join)
- join合并线程,待此线程执行完后,再执行其他线程,其他线程阻塞
- 可以想成插队
5.线程的优先级
-
java提供一个线程的调度器(CPU)来监控启动后就绪的线程,按照优先级去执行线程
-
线程的优先级由数字表示,范围1~10
-
改变或者获取优先级(getPriority,setPriority)
//自定义线程方法,实现Runnable接口 public class ThreadTest implements Runnable { //重写run方法 @Override public void run() { //线程体 System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority()); } //主线程默认优先级 public static void main(String[] args) { //创建线程对象 ThreadTest dodo = new ThreadTest(); Thread t1=new Thread(dodo); Thread t2=new Thread(dodo); Thread t3=new Thread(dodo); Thread t4=new Thread(dodo); t1.start(); //先设置优先级在启动 t2.setPriority(10); t2.start(); t3.setPriority(5); t3.start(); t4.setPriority(1); t4.start(); } }
优先级只是级别,不是固定的。如:买彩票,买1000和买1的概率是不一样的
6.守护线程
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完成
- 虚拟机不需要等待守护线程执行完毕
- 如,后台的日志,监控,垃圾回收
public class ThreadTest {
//主线程默认优先级
public static void main(String[] args) {
//创建线程对象
//用户线程
ThreadTests threadTest = new ThreadTests();
new Thread(threadTest).start();
//守护线程
OtherThread otherThread = new OtherThread();
Thread thread = new Thread(otherThread);
thread.setDaemon(true);//默认为false
thread.start();
}
}
class OtherThread implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("守护线程");
}
}
}
class ThreadTests implements Runnable {
//重写run方法
@Override
public void run() {
//线程体
for (int i = 0; i < 100; i++) {
System.out.println("用户线程活着");
}
System.out.println("用户线程死了");
}
}
7.线程同步机制
多个线程操作同一个资源
并发:同一个对象被多个线程操作
8.线程同步
- 多个线程访问同一个对象,并且某些线程还要修改这个对象,这时就需要线程同步。
- 线程同步其实就是等待机制,多个线程同时访问这个对象时,就会进入这个对象的等待池,形成队列,等待前面线程使用完毕,下一个线程再使用。
- 队列加锁才能保证我们线程同步的安全性
9.锁机制
由于多个线程共享同一个储存空间,带来了访问冲突的问题,为了保证数据的正确性,在访问时会加入锁机制synchronized。
- 当一个线程获得对象的锁,就会独占资源,其他线程必须等待,使用后释放锁即可
缺点
- 一个线程持有锁,会使其他需要此锁的线程挂起。
- 在多个线程的竞争下,会引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,就会出现优先级的问题,引起性能的问题
10.线程安全
-
在 java 程序中怎么保证多线程的运行安全?
-
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
-
可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
-
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序
-
11.多线程锁的升级原理
在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会
随着竞争情况逐渐升级。锁可以升级但不能降级。
无锁
没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的
线程会不断重试直到修改成功。
偏向锁
偏向锁的核心思想就是锁会偏向第一个获取它的线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争
偏向锁才会被释放。在接下来的执行过程中该锁没有其他的线程获取,则持有偏向锁的线程永远不需要再进行步。
轻量级锁
轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自
旋的形式尝试获取锁,线程不会阻塞,从而提高性能。而此时又有第三个线程来访时,轻量级锁也会升级为重量级
锁。
重量级锁
指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。重量级锁将程序运行交出控制权,
将线程挂起,由操作系统来负责线程间的调度,负责线程的阻塞和执行。这样会出现频繁地对线程运行状态的切
换,线程的挂起和唤醒,消耗大量的系统资源,导致性能低下。
12.死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外
力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
13.怎么防止死锁?
死锁的四个必要条件:
-
互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该
资源的进程使用完成后释放该资源
-
请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事
请求阻塞,但又对自己获得的资源保持不放
-
不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
-
环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发
生死锁。对资源的分配要给予合理的规划,不让这四个必要条件成立
14.synchronized 底层实现原理
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证
共享变量的内存可见性。
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象
设计模式
1.什么是设计模式
- 设计模式是对面向对象设计中反复出现的问题的解决方案**
也可:
- 设计模式就是为了解决某类重复出现的问题而出现的一套成功或有效的解决方案
2.设计模式的作用
- 重用设计和代码(例:软件公司做定制OA系统,做的每个OA都可以重用设计和其中的代码)
- 提高了扩展性(大量使用面向接口编程,新的功能和特性很容易加到系统中来)
- 提高开发效率(正确的使用设计模式,大大节约了开发的时间)
3.设计模式的分类
按设计模式解决问题的类型可以分为3大类:
- 创建型模式:对象实例化的模式,用于解耦创建实例化的过程。
- 结构型模式:把类或对象结合在一起形成更大的结构。
- 行为型模式:类和对象互相交互,及划分责任和算法。
简单的来说,一个类有3个成员,构造方法,成员变量,方法**。创建型模式就解决了构造方法的对象创建的问题,**
结构型模式就解决了成员变量的结合的问题,行为型模式就解决了方法调用的问题。
3.1创建型模式
3.1.1工厂模式
简单工厂模式
-
什么是简单工厂模式?
简单工厂模式属于类的创建型模式,又叫做静态工厂方法模式。通过专门定义一个类来负责创建其他类的实
例,被创建的实例通常都具有共同的父类。
-
组成
-
工厂(Creator)角色:简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类可以被外界直接调用,创建所需的产品对象。
public class Factory{ //getClass 产生Sample 一般可使用动态类装载装入类。 public static Product creator(int which){ if (which==1) return new Product1(); else if (which==2) return new Product2(); } }
-
抽象(Product)角色:简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。
产品: public interface Product { void show (); }
-
具体产品(Concrete Product)角色:简单工厂模式所创建的具体实例对象
产品1: class Product1 implements Product { public void show () { System.out.println ("创建产品1"); } } 产品2: class Product2 implements Product { public void show () { System.out.println ("创建产品2"); } }
-
-
优点
创建对象由工厂类统一创建,实现了创建和调用分离。
-
缺点
如果需要增加或者减少的时候就会去修改工厂类,违背了开闭原则。
如果子类多过,导致工厂类会很庞大。不利于后期的维护(子类全部由工厂类创建)。
-
适用场景
产品子类较少,创建较简单的时候可以采用此类模式。
工厂方法模式
-
什么是工厂方法模式
工厂方法模式被称为多态工厂模式 。工厂方法模式的意义是定义一个创建产品对象的工厂接口,将实际创建
工作推迟到子类当中。核心工厂类不再负责产品的创建,这样核心类成为一个抽象工厂角色,仅负责具体工厂
子类必须实现的接口。
-
组成
-
抽象工厂(Creator)角色:工厂方法模式的核心,任何工厂类都必须实现这个接口。
public interface FruitFactory { public Fruit getFruit(); }
-
具体工厂( Concrete Creator)角色:具体工厂类是抽象工厂的一个实现,负责实例化产品对象。
public class AppleFactory implements FruitFactory { *//苹果工厂实现抽象工厂接口,只负责实例化苹果* public Fruit getFruit() { return new Apple(); } } public class BananaFactory implements FruitFactory { *//香蕉工厂实现抽象工厂接口,只负责实例化香蕉* public Fruit getFruit() { return new Banana(); } }
-
抽象产品(Product)角色:工厂方法模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接
口。
public interface Fruit { public void get(); }
-
具体产品(Concrete Product)角色:工厂方法模式所创建的具体实例对象。
public class Apple implements Fruit{ public void get(){ System.out.println("采集苹果"); } } public class Banana implements Fruit{ public void get(){ System.out.println("采集香蕉"); } } test.class public class MainClass { public static void main(String[] args) { *//获得AppleFactory* FruitFactory ff = new AppleFactory(); *//通过AppleFactory来获得Apple实例对象* Fruit apple = ff.getFruit(); apple.get(); *//采集苹果* *//获得BananaFactory* FruitFactory ff2 = new BananaFactory(); Fruit banana = ff2.getFruit(); banana.get(); } }
-
-
优点
对象的创建由专门的工厂完成,也实现了创建和调用分离,也符合开闭原则,即使产品子类多,也不会导致工
厂类庞大,利于后期维护。
-
缺点
需要额外的编写代码,增加工作量
-
适用场景
产品子类比较多,创建操作比较复杂
-
拓展
抽象工厂方法模式:一个具体工厂创建一系列相关联的产品。
建造者模式:在抽象工厂方法模式的基础上,提供一个组装类去完成。
3.1.2单例模式
-
什么是单例模式
指一个类只有一个实例,且该类能自行创建这个实例的一种模式。
饿汉式单例模式
-
什么是饿汉式单例模式
类一旦加载就创建一个单例,保证在调用公共静态方法之前单例已经存在了。
-
组成
public class HungrySingleton { //定义一个自身类型的静态成员变量 private static final HungrySingleton instance=new HungrySingleton(); //构造私有的方法(禁止在类的外部直接创建) private HungrySingleton(){} //提供一个公共的静态方法,返回已经创建好的实例 public static HungrySingleton getInstance() { return instance; } }
饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可
以直接用于多线程而不会出现问题。
-
优点
绝对保证只有一个实例
-
缺点
没有获取实例时,实例已经创建好了
懒汉式单例模式
-
什么是懒汉式单例模式
类加载时没有生成单例,只有当第一次调用 方法时才去创建这个单例。
-
组成
public class LazySingleton { //保证 instance 在所有线程中同步 private static volatile LazySingleton instance=null; //private 避免类在外部被实例化 private LazySingleton(){} public static synchronized LazySingleton getInstance() { //getInstance 方法前加同步 if(instance==null) { instance=new LazySingleton(); } return instance; } }
-
优点
资料利用高,不去调用不会去创建实例
-
缺点
多线程情况下会出现多例的情况,违背了单例模式
双重检测懒汉式单例模式
-
什么是双重检测懒汉式单例模式
为了解决多线程的问题,需使用此种类型的模式
-
组成
class Cat { private String name; //volatile 能够防止指令重排造成的问题 private volatile static Cat cat; //声明唯一实例 private Cat(String name) { this.name = name; } public static Cat getInstance() { if (cat == null) { //保证加锁之前只有一个对象 synchronized (Cat.class) { //获取锁 if (cat == null) { cat = new Cat("DCL懒汉猫"); } } } return cat; } }
详解:
-
两次判断对象是否为 null 的目的分别是什么?
-
最外层的 if 判断很好理解嘛,如果对象已经不为空了,已经创建过了,直接拿着实例返回。
-
两个线程 A 和 B 来获取对象,此时对象还没被创建,A 和 B 都通过了第一层判断,两个线程开始竞
争锁,A 抢到了锁,因此 B 进入同步队列阻塞等待。当 A 创建完对象并且释放了锁,B 拿到了锁,又
**去执行 new Cat(“DCL懒汉猫”),此时就创建了两个对象。**因此,必须在加锁之后再来一次判断,才
能保证只创建一次对象。
-
-
为什么要给实例对象添加 volatile 关键字呢?
这是因为编译器在编译时会进行指令重排,而 volatile 可以禁止指令重排。
对象的创建大概有这么三个步骤(从指令层面来看啊,从JVM来讲具体还有很多步骤):
-
分配内存空间
-
执行构造方法,初始化对象
-
把这个对象指向这个空间
正常是按 123 的顺序执行,但由于指令重排的存在,可能会存在 A 线程按照132 执行,当执行到 3
时,线程 B 来取对象,就会得到空值。因此必须给单例对象加上 volatile 关键字。
-
-
-
优点
资料利用高,不去调用不会去创建实例
-
缺点
由于java内存模型的原因,偶尔会失败。所以需要加volatile 关键字,可以确保完全的线程安全。
3.1.2.4静态内部类单例模式
public final class StaticInnerSingleton {
//private 避免类在外部被实例化
private StaticInnerSingleton() {
}
//静态内部类创建实例
private static class SingletonHolder {
private static StaticInnerSingleton instance = new StaticInnerSingleton();
}
//公共静态方法里直接使用静态内部类创建的对象
public static StaticInnerSingleton getInstance() {
return SingletonHolder.instance;
}
}
-
私有的静态内部类,来存储外部类的单例
-
优点
兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(不会被反射入侵)。
-
缺点
需要多加载一个类。
3.1.3原型模式
-
什么是原型模式
创建重复的对象,同时又能保证性能。
-
实现方式
在实体类里实现cloneable接口,并重写clone方法把作用域改为public
-
拷贝方式
-
深拷贝
容器,数组之类的都会去拷贝。
-
浅拷贝
只拷贝对象里的基本数据类型,容器,数组之类的不会去拷贝。
-
3.2结构型模式
3.2.1外观模式
-
什么是外观模式
外观模式,又叫做门面模式。外观模式将一个系统中的每一项称为一个子系统,为这一组子系统提供一个高层
接口,这个接口可以使得这一子系统更加容易使用。
-
组成
//外观类。门面里结合了所有的子系统 public class CompleteMachine { private Cpu cpu = new Cpu("酷睿i5"); private Ram ram = new Ram("金士顿16G"); private Rom rom = new Rom("金士顿1T"); public void install() { cpu.assembly(); ram.assembly(); rom.assembly(); } } /** * 测试 * * @author ZhongJing </p> */ public class MainTest { public static void main(String[] args) { //外观模式:只需要使用整合好的门面就能直接把全部的子系统调用到 CompleteMachine completeMachine = new CompleteMachine(); completeMachine.install(); //普通情况 Cpu cpu = new Cpu("酷睿i7"); Ram ram = new Ram("金士顿32G"); Rom rom = new Rom("金士顿2T SSD"); cpu.assembly(); ram.assembly(); rom.assembly(); } }
-
适用场景
常见开发中,引入的业务层就是外观模式。(service层)
-
优点
层次分明,子系统能统一,使用比较方便
-
缺点
增加和修改子系统时需要去改动外观类,违背了开闭原则。
3.2.2装饰模式
-
什么是装饰模式
向一个现有的对象添加新的功能,同时又不改变其结构。这种模式创建了一个装饰类,用来包装原有的类,并
在保持类方法完整性的前提下,提供了额外的功能。
-
适用场景
- 需要增加一个类的对象。
- 动态的为一个对象增加功能,且不影响原有的功能。
3.2.3代理模式
-
什么是代理模式
代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们
生活中常见的中介。
-
为什么要使用代理模式
-
**中介隔离作用:**在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客
户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。
-
**开闭原则,增加功能:**代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功
能来扩展委托类的功能。
-
静态代理
-
什么是静态代理
两个对象之间的操作不直接进行,通过第三方进行代理来实现。
在编译期需要程序员通过程序自定义代理类和创建代理对象
-
组成
-
一个公共的接口,代理方和被代理方进行继承
//结婚的接口,接口中定义一个happyMarry的方法 interface Marry{ void happyMarry();//愉快的结婚 }
-
一个代理的类
class WeddingCompany implements Marry{ private Marry target; public WeddingCompany(Marry target) { this.target = target; } @Override public void happyMarry() { ready(); this.target.happyMarry(); after(); } private void ready(){ System.out.println("婚礼策划"); } private void after(){ System.out.println("收尾工作"); } }
-
一个被代理的类
class You implements Marry{ @Override public void happyMarry() { System.out.println("下个月我要去和我的爱人结婚了。。。哈哈哈"); } }
test.class
public class test { public static void main(Srting[] args){ new WeddingCompany(new You()).happyMarry() } }
-
-
优点
可以做到符合开闭原则的情况下,扩展更多的功能
-
缺点
一个代理类只能代理一个接口,代理类的工作量太大。
动态代理
-
什么是动态代理
两个对象之间的操作不直接进行,通过第三方进行代理来实现。
动态代理的代理类是动态生成的,不是我们自己写好的,动态代理是由jdk api帮我们动态实现代理类的。
-
组成
-
一个公共的接口提供方法
public interface Rent { public void rent(); }
-
被代理方
public class Host implements Rent{ public void rent() { System.out.println("我要出租房子"); } }
-
实现InvocationHandler 类,用这个类能够帮我们动态生成代理
public class ProxyInvocationHandler implements InvocationHandler { private Object target; public void setTarget(Object target) { this.target = target; } //生成代理类 public Object getProxy(){ return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } //处理代理类的实例,返回结果 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result= method.invoke(target,args); return result; } }
test.class
public class Client { public static void main(String[] args) { //被代理的类 Host host=new Host(); //ProxyInvocationHandler类并不是代理类,它只是一个能够动态生成代理类的工具 ProxyInvocationHandler proxyInvocationHandler=new ProxyInvocationHandler(); //通过调用程序处理角色来处理我们要调用的接口 proxyInvocationHandler.setTarget(host); //返回代理类,这里getProxy()才是真正的创建出代理类 Rent proxy =(Rent) proxyInvocationHandler.getProxy(); proxy.rent(); } }
-
-
适用场景
扩展一个新的功能,不能改变原来的业务代码,就得用代理
3.3行为型设计模式
-
什么是行为型设计模式
对象或者类之间的沟通和交流.
3.3.1责任链模式
-
每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个
接收者,依此类推
SpringBoot
什么是springBoot
内置tomcat、提供自动配置,搭建spring应用的框架
特点:
- 创建独立的Spring应用程序
- 直接嵌入Tomcat、Jetty或Undertow(无需部署WAR文件)
- 提供自以为是的“启动者”依赖关系,以简化构建配置
- 尽可能自动配置Spring和第三方库
- 提供生产就绪功能,如度量、健康检查和外部化配置
- 绝对没有代码生成,也不需要XML配置
SpringBoot自动配置原理
启动类@SpringbootApplication注解下,有三个关键注解
-
@springbootConfiguration:表示启动类是一个自动配置类
-
@CompontScan:扫描启动类所在包外的组件到容器中
-
@EnableConfigutarion:最关键的一个注解,他拥有两个子注解,其中@AutoConfigurationpackageu会将启动类所在包下的所有组件到容器中,@Import会导入一个自动配置文件选择器,他会去加载META_INF目录下的spring.factories文件,这个文件中存放很大自动配置类的全类名,这些类会根据元注解的装配条件生效,生效的类就会被实例化,加载到ioc容器中
Spring
什么是Spring
Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架。
IOC(控制反转)
spring的核心思想,java开发领域对象的创建和个管理的问题。
开发方式:不通过new关键字去创建对象,而是通过IOC容器来帮助我们实例化对象。
将bean的创建和管理权力交给spring框架,我们只需要注入。
AOP(面向切面编程)
解决的问题:
- 代码重复的问题
- 横切逻辑代码(重复代码)和业务代码混在一起,不便维护
业务逻辑不动,只操作横切逻辑代码
spring 常用的注入方式有哪些?
Spring通过DI(依赖注入)实现IOC(控制反转),常用的注入方式主要有三种:
- 构造方法注入
- setter注入
- 基于注解的注入
spring 的事务隔离?
事务隔离级别指的是一个事务对数据的修改与另一个并行的事务的隔离程度,当多个事务同时访问相同数据时,如果没有采取必要的隔离机制,就可能发生以下问题:
-
脏读:一个事务读到另一个事务未提交的更新数据。
-
幻读:例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二
个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的
用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样。
-
不可重复读:比方说在同一个事务中先后执行两条一模一样的select语句,期间在此次事务中没有执行过任何
DDL语句,但先后得到的结果不一致,这就是不可重复读。
SpringMvc
spring mvc 运行流程
-
用户向服务器发送请求,请求被Spring 前端控制Servelt DispatcherServlet捕获;
-
DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI)。然后根据该URI,调用
HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截
器),最后以HandlerExecutionChain对象的形式返回;
-
DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter;(附注:如果成功获得
HandlerAdapter后,此时将开始执行拦截器的preHandler(…)方法)
-
提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。 在填充Handler的入参过
程中,根据你的配置,Spring将帮你做一些额外的工作:
mybatis
什么是Mybatis
-
Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不
需要花费精力去处理加载驱动、创建连接、创建statement 等繁杂的过程。程序员直接编写原生态 sql,可以
严格控制 sql 执行性能,灵活度高。
-
MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映射成数据库中的记录。
执行 sql 到返回 result 的过程
-
通过 xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java 对象和 statement 中 sql 的动
态参数进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并返回。
mybatis的优缺点
优点
- 基于sql语句编程,非常的灵活。
- 是独立的XML文件,不会影响应用程序和数据库的设计,支持动态sql。
- 很好的与各种数据库兼容(因为mybatis使用JDBC连接的,所以JDBC支持的数据库Mybatis都支持)
- 提供映射标签,支持对象与数据库字段的映射
缺点
- 当表字段多,关联表多的时候,编写工作量较大
- sql语句依赖数据库,不能随意更换数据库
#{}和${}的区别是什么?
- #{}是预编译,可以防止sql注入,提高系统安全
- ${}是字符串替换,会导致sql注入
当实体类中的属性名和表中的字段名不一样 ,怎么办 ?
-
通过在查询的 sql 语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
-
<resultMap type="me.gacl.domain.order" id="orderresultmap"> <!--用 id 属性来映射主键字段--> <id property="id" column="order_id"> <!–用 result 属性来映射非主键字段,property 为实体类属性名,column 为数据表中的属性–> <result property = “orderno” column =”order_no”/> </resluMap>
模糊查询 like 语句该怎么写?
-
<select id=”selectlike”> select * from foo where bar like "%"#{value}"%" </select>
Dao/Mapper的原理
规则:
接口的全限名,就是映射文件中的 namespace 的值;
接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值;
接口方法内的参数,就是传递给 sql 的参数。
Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符\串作为 key 值,可唯一定位一个
MapperStatement。在 Mybatis 中,每一个标签,都会被解析为一个MapperStatement 对象。
工作原理:
工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK动态代理为 Mapper 接口生成代理对象 proxy,代理对象会
拦截接口方法,转而执行 MapperStatement 所代表的 sql,然后将 sql 执行结果返回。