Java面试题
- 基础部分
-
- 1 单例模式
- 2 排序算法思想
- 3 阶乘递归思想
- 4 LinkedList 对比 ArrayList 的区别
- 5 线程池的 7 大核心参数
- 6 掌握 Java 线程六种状态
- 7 wait vs sleep
- 8 lock vs synchronized
- 9 悲观锁 vs 乐观锁
- 10 Hashtable vs ConcurrentHashMap
- 11 ThreadLocal
- 12 JVM内存结构
- 13 三种垃圾回收算法
- 14 GC 与分代回收算法
- 15 十进制转二进制
- 16 基本数据类型:4大类8种
- 17 表达式的自动类型转换
- 18 break和continue
- 19 数组使用常见问题
- 20 Java的参数传递机制
- 21 类和对象
- 22 构造器
- 23 this关键字
- 24 封装
- 25 成员变量和局部变量
- 26 String是不可变字符串的原因
- 27 数组和集合
- 28 static
- 29 继承
- 30 方法重写
- 31 final
- 32 抽象类
- 33 多态
- 34 接口
- 35 匿名内部类
- 36 StringBuilder
- 37 包装类
- 38 Lambda表达式
- 39 Collection接口。
- 40 红黑树
- 41 常见数据结构特点
- 42 List系列集合
- 43 Set系列集合
- 44 哈希表
- 45 Map集合
- 47 HashMap是线程安全的吗? 不是
- 48 MySQL 内部结构了解过吗??
- 49 索引了解吗?SQL优化?
- 50 三次握手和四次挥手?
- 51 HTTP和HTTPS的区别
- 54 线程池 自己封装 还是 用jdk里提供的类
- 55 Docker
- 56 浏览器输入一个URL发生了什么?
- 57 Linux中777代表什么?
- 58 MySQL中存储引擎最大的区别?
- 框架部分
- Redis缓存
- MQ消息队列
- 其他
基础部分
1 单例模式
饿汉式:
/** a、定义一个单例类 */
public class SingleInstance {
/** c.定义一个静态变量存储一个对象即可 :属于类,与类一起加载一次 */
public static SingleInstance instance = new SingleInstance ();
/** b.单例必须私有构造器*/
private SingleInstance (){}
}
懒汉式:
/** 定义一个单例类 */
class SingleInstance{
/** 定义一个静态变量存储一个对象即可 :属于类,与类一起加载一次 */
public static SingleInstance instance ; // null
/** 单例必须私有构造器*/
private SingleInstance(){}
/** 必须提供一个方法返回一个单例对象 */
public static SingleInstance getInstance(){
...
return ...;
}
}
2 排序算法思想
快排:
1. 每一轮排序选择一个基准点(pivot)进行分区 2. 让小于基准点的元素的进入一个分区,大于基准点的元素的进入另一个分区 3. 当分区完成时,基准点元素的位置就是其最终位置 4. 在子分区内重复以上过程,直至子分区元素个数少于等于 1,这体现的是分而治之的思想 ([divide-and-conquer])
冒泡:
- 依次比较数组中相邻两个元素大小,若 a[j] > a[j+1],则交换两个元素,两两都比较一遍称为一轮冒泡,结果是让最大的元素排至最后
- 重复以上步骤,直到整个数组有序
选择:
- 将数组分为两个子集,排序的和未排序的,每一轮从未排序的子集中选出最小的元素,放入排序子集
- 重复以上步骤,直到整个数组有序
插入:
- 将数组分为两个区域,排序区域和未排序区域,每一轮从未排序区域中取出第一个元素,插入到排序区域(需保证顺序)
- 重复以上步骤,直到整个数组有序
3 阶乘递归思想
假设我们求的是10的阶乘【10!】,那么就必须要知道9的阶乘【9!】,由此得出【10! = 9! * 10】
每个阶层都以此类推,当到达1时,我们都知道:1的阶乘的值为1,那么直接返回1就好了!
用函数f()表示阶乘【n!】:f(n) = f(n-1) * n,当n到达1时,返回1的阶乘【1! = 1】【return 1】,否则继续调用自身,返回前一个数的阶乘的值
递归的公式: f(n) = f(n-1) * n;
递归的终结点:f(1)
递归的方向必须走向终结点:
4 LinkedList 对比 ArrayList 的区别
LinkedList
- 基于双向链表,无需连续内存
- 随机访问慢(要沿着链表遍历)
- 头尾插入删除性能高
- 占用内存多
ArrayList
- 基于数组,需要连续内存
- 随机访问快(指根据下标访问)
- 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低
- 可以利用 cpu 缓存,局部性原理
5 线程池的 7 大核心参数
- corePoolSize 核心线程数目 - 池中会保留的最多线程数
- maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目
- keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
- unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
- workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
- threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
- handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
6 掌握 Java 线程六种状态
- 新建
- 当一个线程对象被创建,但还未调用 start 方法时处于新建状态
- 此时未与操作系统底层线程关联
- 可运行
- 调用了 start 方法,就会由新建进入可运行
- 此时与底层线程关联,由操作系统调度执行
- 终结
- 线程内代码已经执行完毕,由可运行进入终结
- 此时会取消与底层线程关联
- 阻塞
- 当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间
- 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态
- 等待
- 当获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间
- 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态
- 有时限等待
- 当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法,此时从可运行状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu 时间
- 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁
- 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁
- 还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自然恢复为可运行状态
7 wait vs sleep
- 共同点
- wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
- 不同点
- 方法归属不同
- sleep(long) 是 Thread 的静态方法
- 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
- 醒来时机不同
- 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
- wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
- 它们都可以被打断唤醒
- 锁特性不同(重点)
- wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
- wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
- 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
8 lock vs synchronized
三个层面
- 不同点
- 语法层面
- synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
- Lock 是接口,源码由 jdk 提供,用 java 语言实现
- 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
- 功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
- Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
- Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock
- 性能层面
- 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
- 在竞争激烈时,Lock 的实现通常会提供更好的性能
9 悲观锁 vs 乐观锁
- 悲观锁的代表是 synchronized 和 Lock 锁
- 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】
- 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
- 实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会
- 乐观锁的代表是 AtomicInteger,使用 cas 来保证原子性
- 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】
- 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
- 它需要多核 cpu 支持,且线程数不应超过 cpu 核数
10 Hashtable vs ConcurrentHashMap
- Hashtable 与 ConcurrentHashMap 都是线程安全的 Map 集合
- Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它
- ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突
11 ThreadLocal
作用
- ThreadLocal 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题
- ThreadLocal 同时实现了线程内的资源共享
原理
- 每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象
- 调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
- 调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
- 调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
12 JVM内存结构
- 执行 javac 命令编译源代码为字节码
- 执行 java 命令
- 创建 JVM,调用类加载子系统加载 class,将类的信息存入方法区
- 创建 main 线程,使用的内存区域是 JVM 虚拟机栈,开始执行 main 方法代码
- 如果遇到了未见过的类,会继续触发类加载过程,同样会存入方法区
- 需要创建对象,会使用堆内存来存储对象
- 不再使用的对象,会由垃圾回收器在内存不足时回收其内存
- 调用方法时,方法内的局部变量、方法参数所使用的是 JVM 虚拟机栈中的栈帧内存
- 调用方法时,先要到方法区获得到该方法的字节码指令,由解释器将字节码指令解释为机器码执行
- 调用方法时,会将要执行的指令行号读到程序计数器,这样当发生了线程切换,恢复时就可以从中断的位置继续
- 对于非 java 实现的方法调用,使用内存称为本地方法栈
- 对于热点方法调用,或者频繁的循环代码,由 JIT 即时编译器将这些代码编译成机器码缓存,提高执行性能
13 三种垃圾回收算法
标记清除法
解释:
- 找到 GC Root 对象,即那些一定不会被回收的对象,如正执行方法内局部变量引用的对象、静态变量引用的对象
- 标记阶段:沿着 GC Root 对象的引用链找,直接或间接引用到的对象加上标记
- 清除阶段:释放未加标记的对象占用的内存
标记整理法
解释:
- 前面的标记阶段、清理阶段与标记清除法类似
- 多了一步整理的动作,将存活对象向一端移动,可以避免内存碎片产生
标记复制法
解释:
- 将整个内存分成两个大小相等的区域,from 和 to,其中 to 总是处于空闲,from 存储新创建的对象
- 标记阶段与前面的算法类似
- 在找出存活对象后,会将它们从 from 复制到 to 区域,复制的过程中自然完成了碎片整理
- 复制完成后,交换 from 和 to 的位置即可
14 GC 与分代回收算法
GC 的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度
GC 要点:
- 回收区域是堆内存,不包括虚拟机栈
- 判断无用对象,使用可达性分析算法,三色标记法标记存活对象,回收未标记对象
- GC 具体的实现称为垃圾回收器
- GC 大都采用了分代回收思想
- 理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收
- 根据这两类对象的特性将回收区域分为新生代和老年代,新生代采用标记复制法、老年代一般采用标记整理法
- 根据 GC 的规模可以分成 Minor GC,Mixed GC,Full GC
15 十进制转二进制
- 除二取余法
16 基本数据类型:4大类8种
整数(byte:1字节 -128~127,short:2字节,int:4字节,long:8字节)
浮点数(float:4字节,double:8字节)
字符 char:2字节
布尔:boolean:1字节
17 表达式的自动类型转换
- 表达式的最终结果类型由表达式中的最高类型决定
18 break和continue
break : 跳出并结束当前所在循环的执行。只能用于结束所在循环, 或者结束所在switch分支的执行
continue: 用于跳出当前循环的当次执行,进入下一次循环。只能在循环中进行使用
19 数组使用常见问题
问题1:如果访问的元素位置超过最大索引,执行时会出现ArrayIndexOutOfBoundsException(数组索引越界异常)
问题2:如果数组变量中没有存储数组的地址,而是null, 在访问数组信息时会出现NullPointerException(空指针异常)
20 Java的参数传递机制
值传递,传输的是实参存储的值。
- 基本类型的参数传输存储的数据值。
- 引用类型的参数传输存储的地址值
21 类和对象
- 类:是共同特征的描述(设计图);
- 对象:是真实存在的具体实例
22 构造器
- 作用:初始化类的对象,并返回对象的地址
23 this关键字
(是什么)出现在构造器和成员方法中,代表当前对象的地址
(作用) 可以用于指定访问当前对象的成员
24 封装
告诉我们,如何正确设计对象的属性和方法。
原则:对象代表什么,就得封装对应的数据,并提供数据对应的行为
25 成员变量和局部变量
- 成员变量(类中,方法外。有默认值,无需初始化。堆内存。随着对象的创建而存在,随着对象的消失而消失。)
- 局部变量(常见于方法中。没有默认值,使用之前需要完成赋值。栈内存。随着方法的调用而存在,随着方法的运行结束而消失。在所归属的大括号中)
26 String是不可变字符串的原因
String变量每次的修改其实都是产生并指向了新的字符串对象。
原来的字符串对象都是没有改变的,所以称不可变字符串。
27 数组和集合
- 数组定义后类型确定,长度固定,适合做数据个数和类型确定的场景。可以存储基本类型和引用类型的数据
- 集合类型可以不固定,大小是可变的,适合做数据个数不确定,且要做增删元素的场景。只能存储引用数据类型的数据。
28 static
(是什么)静态的意思,可以修饰成员变量、成员方法
- 静态成员变量(有static修饰,属于类、加载一次,内存中只有一份),访问格式:类名.静态成员变量。需要被类的所有对象共享信息时定义
- 实例成员变量(无static修饰,属于对象),访问格式:对象.实例成员变量。属于每个对象,且每个对象的该信息不同时定义
- 注意事项:
- 静态方法只能访问静态的成员,不可以直接访问实例成员。
- 实例方法可以访问静态的成员,也可以访问实例成员。
- 静态方法中是不可以出现this关键字的。
29 继承
- (设计规范)
- 子类们相同特征(共性属性,共性方法)放在父类中定义。
- 子类独有的的属性和行为应该定义在子类自己里面。
- (特点)
- 子类可以继承父类的属性和行为,但是子类不能继承父类的构造器。
- Java是单继承模式:一个类只能继承一个直接父类。
- Java不支持多继承、但是支持多层继承。
- Java中所有的类都是Object类的子类。
30 方法重写
- 重写方法的名称和形参列表应该与被重写方法一致。
- 私有方法不能被重写。
- 子类重写父类方法时,访问权限必须大于或者等于父类被重写的方法的权限
31 final
- final 关键字是最终的意思,可以修饰(类、方法、变量)
- 修饰类:表明该类是最终类,不能被继承。
- 修饰方法:表明该方法是最终方法,不能被重写。
- 修饰变量:表示该变量第一次赋值后,不能再次被赋值(有且仅能被赋值一次)。
- final修饰的变量是基本类型:那么变量存储的数据值不能发生改变。
- final修饰的变量是引用类型:那么变量存储的地址值不能发生改变,但是地址指向的对象内容是可以发生变化的。
32 抽象类
- 一个类如果继承了抽象类,那么这个类必须重写完抽象类的全部抽象方法,否则这个类也必须定义成抽象类。
- (是什么)都是用abstract修饰的;抽象方法只有方法签名,不能写方法体
- 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类
- 不能用abstract修饰变量、代码块、构造器。
- 最重要的特征:得到了抽象方法,失去了创建对象的能力(有得有失)
33 多态
- 父类类型 对象名称 = new 子类构造器
- 方法调用:编译看左边,运行看右边。(成员访问特点)
- 变量调用:编译看左边,运行也看左边有继承/实现关系;
- (前提)有父类引用指向子类对象;有方法重写(多态侧重行为多态)
34 接口
- 类和类的关系:单继承。
- 类和接口的关系:多实现。
- 接口和接口的关系:多继承,一个接口可以同时继承多个接口。
- (接口多继承作用)规范合并,整合多个接口为同一个接口,便于子类实现。
- (注意事项)
- 1、接口不能创建对象
- 2、一个类实现多个接口,多个接口的规范不能冲突
- 3、一个类实现多个接口,多个接口中有同样的静态方法不冲突。
- 4、一个类继承了父类,同时又实现了接口,父类中和接口中有同名方法,默认用父类的。
- 5、一个类实现了多个接口,多个接口中存在同名的默认方法,可以不冲突,这个类重写该方法即可。
- 6、一个接口继承多个接口,是没有问题的,如果多个接口中存在规范冲突则不能多继承。
35 匿名内部类
- 作用)方便创建子类对象,最终目的是为了简化代码编写
- (特点)匿名内部类是一个没有名字的内部类,同时也代表一个对象。
- 匿名内部类产生的对象类型,相当于是当前new的那个的类型的子类类型。
36 StringBuilder
- StringBuilder:内容是可变的、拼接字符串性能好、代码优雅。拼接、修改等操作字符串使用StringBuilder
- String :内容是不可变的、拼接字符串性能差。定义字符串使用String
37 包装类
- (是什么)基本数据类型对应的引用类型,实现了一切皆对象。
- (作用)后期集合和泛型不支持基本类型,只能使用包装类
- (特殊功能)可以把字符串类型的数值转换成真实的数据类型(真的很有用)
38 Lambda表达式
- (作用)简化函数式接口的匿名内部类的写法
- (使用前提)必须是接口的匿名内部类,接口中只能有一个抽象方法
39 Collection接口。
- (2大集合体系)List系列集合:添加的元素是有序、可重复、有索引。
- Set系列集合:添加的元素是无序、不重复、无索引。
40 红黑树
- (规则)
- 每一个节点或是红色的,或者是黑色的,根节点必须是黑色
- 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点(Nil)是黑色的;
- 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况)
- 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。
- 红黑树增删改查的性能都很好
41 常见数据结构特点
- 队列:先进先出,后进后出。
- 栈:后进先出,先进后出。
- 数组:内存连续区域,查询快,增删慢。
- 链表:元素是游离的,查询慢,首尾操作极快。
- 二叉树:永远只有一个根节点, 每个结点不超过2个子节点的树。
- 查找二叉树:小的左边,大的右边,但是可能树很高,查询性能变差。
- 平衡查找二叉树:让树的高度差不大于1,增删改查都提高了。
- 红黑树(就是基于红黑规则实现了自平衡的排序二叉树)
42 List系列集合
- (特点)ArrayList、LinekdList :有序,可重复,有索引
- (List实现类的底层原理)ArrayList底层是基于数组实现的,根据查询元素快,增删相对慢
- LinkedList底层基于双链表实现的,查询元素慢,增删首尾元素是非常快的
43 Set系列集合
- (特点)无序、不重复、无索引。
- (实现类特点)
- HashSet无序、不重复、无索引。
- LinkedHashSet 有序、不重复、无索引。
- TreeSet 可排序、不重复、无索引。底层基于红黑树实现排序,增删改查性能较好
- LinkedHashSet有序、不重复、无索引。底层基于哈希表,使用双链表记录添加顺序。
44 哈希表
- (流程)
- 创建一个默认长度16,默认加载因为0.75的数组,数组名table
- 根据元素的哈希值跟数组的长度计算出应存入的位置
- 判断当前位置是否为null,如果是null直接存入,如果位置不为null,表示有元素, 则调用equals方法比较属性值,如果一样,则不存,如果不一样,则存入数组。
- 当数组存满到16*0.75=12时,就自动扩容,每次扩容原先的两倍
45 Map集合
- HashMap:元素按照键是无序,不重复,无索引,值不做要求。(与Map体系一致)
- 由键决定:无序、不重复、无索引。
- HashMap底层是哈希表结构的。
- 依赖hashCode方法和equals方法保证键的唯一。
- 如果键要存储的是自定义对象,需要重写hashCode和equals方法。
- 基于哈希表。增删改查的性能都较好。
- LinkedHashMap:元素按照键是有序,不重复,无索引,值不做要求。
- TreeMap:元素按照建是排序,不重复,无索引的,值不做要求。
47 HashMap是线程安全的吗? 不是
参考: 图解为什么HashMap
48 MySQL 内部结构了解过吗??
参考: MySQL的内部结构
49 索引了解吗?SQL优化?
参考: 索引、约束、视图、索引失效
参考: MySQL优化
50 三次握手和四次挥手?
参考: 简述三次握手和四次挥手
51 HTTP和HTTPS的区别
参考: HTTP和HTTPS的区别
54 线程池 自己封装 还是 用jdk里提供的类
55 Docker
参考: Docker基本使用教程
56 浏览器输入一个URL发生了什么?
参考: 浏览器输入一个url全过程详解
57 Linux中777代表什么?
参考: Linux777权限代表?
58 MySQL中存储引擎最大的区别?
MyISAM:表锁(操作一条数据锁整张表),高并发下就会出现严重的锁问题!
InnoDB :行锁
参考: MySQL存储引擎种类和区别
框架部分
参考: Spring常见面试题
参考: SpringBoot常见面试题
参考: SpringMVC常见面试题
参考: MyBatis常见面试题
参考: SpringCloud常见面试题
Redis缓存
参考: Redis从入门到精通
MQ消息队列
参考 : MQ消息队列详解
其他
DDoS攻击
是什么?
- 拒绝服务(Denial of Service,简称DoS)攻击也称洪水攻击,是一种网络攻击手法,其目的在于使目标电脑的网络或系统资源耗尽,服务暂时中断或停止,导致合法用户不能够访问正常网络服务的行为。当攻击者使用网络上多个被攻陷的电脑作为攻击机器向特定的目标发动DoS攻击时,称为分布式拒绝服务攻击(Distributed Denial of Service Attack,简称DDoS)
攻击类型:
- 网络层攻击
- 通过大流量拥塞被攻击者的网络带宽,导致被攻击者的业务无法正常响应客户访问
- NTP Flood攻击
- 传输层攻击
- 通过占用服务器的连接池资源,达到拒绝服务的目的
- SYN Flood攻击
- ACK Flood攻击
- ICMP Flood攻击
- 会话层攻击
- 通过占用服务器的SSL会话资源,达到拒绝服务的目的
- SSL连接攻击
- 应用层攻击
- 通过占用服务器的应用处理资源,极大消耗服务器处理性能,达到拒绝服务的目的
- HTTP Get Flood攻击
- HTTP Post Flood攻击 、
参考: 华为云开发者社区
入职准备
参考: vim基本使用
参考: nodeJS安装及环境配置
参考: nodeJS安装配置详细版
参考: Maven安装和配置
参考: Idea导入项目
参考: SpringBoot快速入门
参考: 连接池
参考: E-R图
参考: Idea调整代码大小快捷键
参考: Java工程师初进公司的准备工作