八股打卡三
- 保证数据库和缓存一致性(回顾复习)
- String, StringBuilder, StringBuffer区别
- 接口和抽象类的区别
- Java常见的异常类
- Java三大特性
- 继承
- 封装
- 多态
- Java多态
- 重写和重载的区别
- final关键字的作用
- == 和 equals区别
- Java中的集合类
- 线程不安全的集合类
- 线程安全的集合类
- ArrayList vs. Array
- ArrayList vs. LinkedList
- ArrayList的扩容机制
- HashMap的底层实现
- 解决hash冲突的方法
- HashMap的put流程
保证数据库和缓存一致性(回顾复习)
- Cache Aside:先从缓存中取数据,如果命中则返回,不命中则访问数据库,并将该数据放入缓存中。对于修改的数据,先将数据持久化到数据库中,再使缓存失效。
- Read / Write Through:将更新数据库的操作给缓存代理。
- Read Through:查询时更新缓存。当缓存失效时,缓存自己加载数据库中的数据,而Cache Aside需要调用方加载数据到缓存。
- Write Through:更新数据时,若缓存未命中,则直接更新数据库中的数据,若命中,则先更新缓存,再更新数据库。
- Write Behind:在更新数据时,只更新缓存,不更新数据库,缓存会异步批量更新数据库。好处是数据IO很快,缺点是数据无法保证强一致性。
String, StringBuilder, StringBuffer区别
三者都是Java中处理字符串的类。
- 可变性:
- String内部的字符数据是用final关键字修饰的,所以在定义后这个String是不可变的。当对该String对象进行修改时,实际上是新创建了一个String对象,旧的String会被JVM回收。
- StringBuilder和StringBuffer都是可变的。这意味着它们都可以修改字符串,且不会创建新的对象。
- 线程安全:
- String是不可变的,线程安全。
- StringBuffer的方法是用synchronized关键字修饰的,线程安全。
- StringBuilder是线程不安全的。
- 性能
- 对于复杂的字符串操作(拼接,插入,删除)StringBuffer和StringBuilder的效率要比String高,因为它们可变。
- 使用场景
- String 用于字符串很少变化的场景,如常量的定义和少量的变量运算。
- StringBuffer用于频繁的字符串操作(拼接,替换,删除),且运行在多线程,例如XML文件解析,HTTP参数解析和封装。
- StringBuilder用于频繁的字符串操作,且运行在单线程,例如SQL语句的拼接,JSON的封装。
接口和抽象类的区别
- 定义
- 接口是一个抽象类型,定义了一组抽象方法,它里面只能包含常量(static final)和抽象方法。
- 抽象类是一个类,它既能包含抽象方法也能包含具体方法,既能包含常量也能包含成员变量。抽象类不能直接实例化,通常需要子类继承并实现抽象类的抽象方法。
- 继承
- 接口是多继承,一个类可以继承多个接口。
- 抽象类是单继承,一个类只能继承一个抽象类。
- 构造器
- 接口没有构造器,不能被实例化,一个类继承接口必须实现接口中所有方法。
- 抽象类有构造器,用于初始化抽象类的实例,子类继承抽象类后,子类实例化会调用父类的构造方法。
- 访问修饰符
- 接口:接口中变量默认public static final修饰,方法默认public abstract修饰。
- 抽象类:其中的抽象方法默认是protected,具体方法public, private, protected都可以。
- 实现限制
- 接口:一个类可以继承多个接口,但必须实现接口中所有方法。
- 抽象类:一个类只能继承一个抽象类,必须实现抽象类中所有抽象方法。
- 设计目的
- 接口:提供一种规范。
- 抽象类:用于代码复用,提供基础功能和通用实现。
Java常见的异常类
Java中的异常类都是派生于Throwable类的实例,继承于Throwable。
Java中的异常分为两种:RunTimeException(运行时异常)和其他异常。运行时异常是由程序错误引起的,其他异常程序本身没有问题,外部导致。
- 运行时异常
编译器不会处理,如数组索引越界(ArrayIndexOutOfBoundsException),访问对象为空(NullPointException),强制类型转换错误,除0等等,程序内部引起的。 - 其他异常
又称为编译时异常,编译器要求必须处理这些异常。一般是由外部环境导致的,如打开一个不存在的文件,会抛出文件不存在异常。编译器要求java程序要捕获或声明所有的编译时异常,并对可能出现的异常做准备工作。
Java三大特性
继承
一个类继承另一个类的方法和属性,子类能够重用父类的代码,并且新增方法或者修改已有方法实现功能的拓展。一个类只能继承一个父类。
封装
将对象的属性和方法结合在一起,隐藏内部的实现细节,对外暴露一个可以访问的接口,通常使用public, private, protected关键字来设置访问权限,以实现封装。
多态
方法的重载和重写。
Java多态
当把一个子类对象赋值给父类引用变量,调用该引用变量的方法,其方法表现出子类方法的行为特征,而不是父类方法的行为特征。也就是收,对于相同的变量。调用同一个方法会表现出不同的特征,这就是多态。
- 编译时多态:编译器在编译阶段就会决定调用哪个方法。通过方法的重载来实现,根据参数的类型、数量和顺序决定调用哪个方法。
- 运行时多态:程序运行阶段会根据实际对象类型来选择调用的方法,通过方法的重写来实现,运行时多态依赖实际的对象类型而非引用类型。
重写和重载的区别
- 重载:编译时多态,在一个类中允许有多个同名的方法,但是它们的参数列表不同(类型,个数,顺序),可以有不同的返回值和访问修饰符,通过静态绑定实现。
- 重写:运行时多态,子类可以重新定义父类中已有的方法,方法名,参数列表和返回值都必须相同。重写的方法访问级别不能低于被重写的父类方法。虚拟机在运行时会根据实际对象的类型决定调用哪个方法。
总而言之,重载关注方法多样性,重写关注方法一致性,子类有其独特的实现。
final关键字的作用
不可变,可以用来修饰类,方法和变量。
- 类:被final修饰的类不能被继承。
- 方法:被final修饰的方法不能被子类覆盖。
- 变量:
(1)基本数据类型:被final修饰表示定义好后不可修改
(2)引用数据类型:被final修饰后不能指向其他对象,即地址不可变,但是对象的内容可变。
== 和 equals区别
- ==运算符对于基本数据类型是比较值,对于引用类型比较在内存中的位置。
- equals()方法在Object类中被定义,默认与==类似。但是在子类中通常被重写,如String, Integer中重写equals()用于比较引用类型的内容。
- == 一般用于比较对象的地址,equals比较对象的内容。
- 在重写equals方法时也要重写hashCode方法,保证两者一致性。
Java中的集合类
Java中的集合类分为Collection和Map。Colleaction又派生出了List, Set, Queue三个类。Java中的集合类都是由这四个类派生的。
- List:有序元素的集合,允许重复值。有ArrayList, LinkedList。
- Set:无序元素的集合,不允许重复值,有HashSet, LinkedHashSet, TreeSet。
- Queue:用于队列的数据结构,有LinkedList, PriorityQueue。
- Map:键值对集合,有HashMap, LinkedHashMap和TreeMap。
线程不安全的集合类
以上都是线程不安全的,在多线程场景下,可能会出现不确定的结果。
线程安全的集合类
- Vector:类似于ArrayList,方法都是同步的来实现线程安全。
- HashTable:类似于HashMap,通过同步对象实现线程安全。
- ConcurrentHashMap:提高并发性能,通过锁分离技术实现线程安全。
- Collections.synchronizedList, Collections.synchronizedMap, Collections.synchronizedSet:将非线程安全的集合类包装成线程安全的集合类。
ArrayList vs. Array
- ArrayList底层是动态数组,Array是固定数组。
- ArrayList提供了强大的功能,如自动扩容、增加、删除,而Array没有。
- Array能够存放基本数据类型和对象,而ArrayList只能存放对象。对于基本数据类型,ArrayList只能存放它们的包装类(Integer, Double)。
- 在随机访问中,Array由于连续的内存存储,访问性能优于ArrayList。
ArrayList vs. LinkedList
- ArrayList底层是动态数组,LinkedList底层是双向链表。
- 随机访问:ArrayLsit随机访问的效率比LinkedLsit高,因为LinkedList要从头或尾部遍历链表,时间复杂度为O(n)。
- 插入删除操作:向ArrayList尾部插入元素很快,但是向中间或者头部插入元素,需要移动后续的元素,复杂度为O(n),而LinkedList只需要修改节点的引用。
- 扩容:当元素数量到达容量时会自动扩容,扩容设计新数组的建立和将旧数组内容复制到新数组,有一定的性能开销。
- 使用场景:随机访问操作多的用ArrayList,插入删除多的用LinkedList。
ArrayList的扩容机制
指的是计算出新扩容的数据的大小,并实例化,将旧数组中的数据复制到新数组。
- 创建一个ArrayLsit对象,系统会分配给它初始容量,默认为10,以节约数组空间。
- 当元素数量到达当前容量时,会自动扩容。调用其中的grow()函数,该方法会尝试将容量扩大为原来的1.5倍。
- 若新容量满足需求,那么调用Arrays.copyOf函数将旧数组的内容复制到新数组中;若不满足,则新容量大小为当前所需容量+1。
HashMap的底层实现
JDK1.8之前,HashMap是由数组和链表组成的,当发生哈希冲突的时候,多个元素会以链表的形式存储在同一个数组位置;JDK1.8以后引入红黑树,当链表长度超出默认长度(8)时会自动转换成红黑树,以提高查询效率。
- 为什么链表长度>=8成为红黑树,<=6成为链表?
根据泊松分布,当负载因子默认为0.75时,单个hash槽的元素个数等于8的概率小于百万分之一,所以以7作为分水岭,等于7不转换,大于等于8转为红黑树,小于等于6转为链表。 - 为什么引入红黑树?
因为红黑树具有以下性质:
- 不追求绝对的平衡,允许一定的局部不平衡。相比于AVL(平衡二叉搜索树)树追求绝对的平衡,减少了很多性能开销;
- 红黑树是一种自平衡的二叉搜索树,插入和删除的时间复杂度都是O(logn)。
- HashMap的读写时间复杂度?
- 读:最好情况直接通过数组下标读取O(1);最坏情况发生哈希冲突,链表O(n),红黑树O(logn)。
- 写:O(1),发生冲突O(n)。
解决hash冲突的方法
- 链地址法:在数组的每一个位置维护一个链表,当发生哈希冲突时,新的元素会被添加到链表的尾部。
- 开放寻址法:发生哈希冲突时,通过某种检测算法在哈希表中寻找下一个空闲的存储位置。
HashMap采用链地址法。
HashMap的put流程
- 判断键值对数组是否为空,为空则扩容;
- 不为空,根据key计算hash值找到插入数组的索引i,判断table[i]是否为空,为空说明map里面这个位置没有键值对,那么新建节点插入,判断是否需要扩容,结束;
- 若table[i]不为空,说明这个位置有键值对节点了,判断它的首个元素的hashCode和equals是否与key相等,相等,那么直接覆盖掉value;
- 若不相等,发生了哈希冲突,首个元素也不是这个键值对,那么判断table[i]是否为TreeNode,是则说明形成了红黑树,直接插入树中;
- 如果不是TreeNode。说明table[i]现在还是个链表,遍历判断链表长度是否大于等于8,是则先转为红黑树,不是那就在链表中插入。注意遍历链表的过程中如果找到key,那么覆盖它的value。