《Java编程思想》、《疯狂Java:突破程序员基本功的16课.修订版》,这里省略了一些非常基础的知识点
二进制小数无法精确表达十进制小数,计算机在计算十进制小数的过程中要先转换为二进制进行计算,这个过程中出现了误差
以二进制补码形式存储,最高位是符号位,正数的补码是它的原码,负数的补码是它的反码加1,在求反码时符号位不变,其他取反,1表示负数,0正数
接口中的所有方法隐含的都是抽象的,而抽象类则可以同时包含抽象和非抽象方法
类可以实现很多个接口,但只能继承一个抽象类
类可以不实现抽象类和接口声明的所有方法,但这种情况下,该类必须声明成抽象的
接口中的成员函数默认是public的。抽象类的成员函数可以是private,protected或者是public。
JDK8后,接口中可以包含default方法,抽象类中不可以
关于网络方面的问题,面试官应该只会问一题,不会多问
可见该文:https://blog.csdn.net/qq_38950316/article/details/81087809
Session:
服务器端会为每个访问服务端的请求分配一个会话Session,其数据存储在服务器端,不依赖浏览器端环境,因此高效安全
Cookie:
数据以文件形式存在用户浏览器端,用户可以通过浏览器禁用Cookie,用户可以对Cookie进行查看,修改,和删除
常用的包:
java.lang 包装类,线程等都在该包
java.math 有BigDecimal 精确数字类型
java.util 并发,集合等都在该包内
int a = 1;
Integer b = new Integer(1);
Integer c = new Integer(1);
Integer d = Integer.valueOf(1);
Long e = new Long(1);
Long f = Long.valueOf(1);
assert a == b;
assert b != c;
assert b != d;
assert a == d;
assert a == e;
assert a == f;
assert b.equals(c);
assert !b.equals(e);
对于覆盖了equals方法的类中,同样也要覆盖hashCode方法。这是JDK规定的结果。
比较两个对象是否相同,hashCode比equals效率更高,所以优先会根据hashCode来比较,但如果不重写hashCode,原本两个对象可以认为是相等,但由于hashCode默认返回表示对象地址的整数,必然不相等,所以需要重写hashCode。
由于hashCode有个问题,可能两个不同的对象会有相同的hashCode,这样还需要通过equals来比较
比如HashMap中,计算key的索引位置,会用到key.hashCode,在确定是否为同一个元素时通过equals比较
序列化是一种用来处理对象流的机制,也就是将对象的内容转化成二进制流,可以将对象持久化或者网络传输
反序列化是将二进制流还原为对象的过程
实现Java序列化,通过实现Serializable即可
因为Java提供的锁是对象级的,每个对象都有对象头,用来存储锁
<? extends Fruit> 称为 上界限定符,list只能get,不能add(除了add null值),通常用于读
<? super Apple>称为 下界限定符,list只能add,不能get(只能用Object接收),通过用于写
Object()默认构造方法
clone()创建并返回此对象的一个副本
equals(Object obj) 当前对象是否与obj对象相同
finalize()当垃圾收集器确定该对象可以回收时,由垃圾收集器调用此方法
getClass返回一个对象的运行时类
hashCode()返回该对象的哈希码值
notify()唤醒此对象监视器上等待的单个线程
notifyAll()唤醒在此对象监视器上等待的所有线程
toString()返回该对象的字符串表示
wait()使当前线程等待,直到其他线程调用此对象的notify()或者notifyAll()方法
wait(long timeout)导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。wait(long timeout, int nanos) 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。
ArrayList 是一种顺序存储的线性表,底层使用数组实现
LinkedList是一种链式存储的线性表,本质是一个双向链表,实现了List、Deque接口,可以当成双向链表、队列、栈使用
在往ArrayList add元素的时候,如果ArrayList 已有元素数量+1 大于 ArrayList 存储元素的总长度,就会触发扩容。
首先ArrayList会计算新数组的长度,长度为老数组的1.5倍,如果新数组长度还是小于插入元素需要的最小长度,那么新数组长度赋值为最小长度,如果超过ArrayList允许的最大长度Integer.MAX_VALUE(
),那么新数组长度为Integer.MAX_VALUE,否则为Integer.MAX_VALUE - 8(为什么要-8?Why the maximum array size of ArrayList is Integer.MAX_VALUE - 8?)
最后将原数组元素拷贝到新数组进行扩容
如果在new HashMap的时候,没有指定初始initialCapacity,则初始initialCapacity为16,负载因子为0.75,下次扩容阈值为 16*0.75=12
这个初始容量 不一定等于初始化完成后底层数组实际的容量,因为存在阈值的计算,方法如下;也不是初始容量是多少开始就能存多少个元素,因为存在负载因子,在底层数组还没满的时候就会进行扩容
阈值计算方法为:
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
该方法计算大于等于输入参数并最接近参数的2的整数次幂的数,如10,返回16
cap -1 ,-1是为了在计算的时候能得到大于等于输入参数的值
在HashMap 进行put方法的时候,如果判断已有的元素大于 阈值就会触发扩容计算,扩容步骤
final Node<K,V>[] resize() {
//原table数组赋值
Node<K,V>[] oldTab = table;
//如果原数组为null,那么原数组长度为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//赋值阈值
int oldThr = threshold;
//newCap 新数组长度
//newThr 下次扩容的阈值
int newCap, newThr = 0;
// 1. 如果原数组长度大于0
if (oldCap > 0) {
//如果大于最大长度1 << 30 = 1073741824,那么阈值赋值为Integer.MAX_VALUE后直接返回
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 2. 如果原数组长度的2倍小于最大长度,并且原数组长度大于默认长度16,那么新阈值为原阈值的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 3. 如果原数组长度等于0,但原阈值大于0,那么新的数组长度赋值为原阈值大小
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 4. 如果原数组长度为0,阈值为0,那么新数组长度,新阈值都初始化为默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 5.如果新的阈值等于0
if (newThr == 0) {
//计算临时阈值
float ft = (float)newCap * loadFactor;
//新数组长度小于最大长度,临时阈值也小于最大长度,新阈值为临时阈值,否则是Integer.MAX_VALUE
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//计算出来的新阈值赋值给对象的阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//用新计算的数组长度新建一个Node数组,并赋值给对象的table
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//后面是copy数组和链表数据逻辑
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
总结:
如下3种情况,例子需要自己调式,主要关注数组长度(OldCap,newCap)新老变化,扩容阈值(oldThr,newThr)新老变化
//①
Map<String, String> map = new HashMap<>();
map.put("1", "1");
//②
Map<String, String> map1 = new HashMap<>(2);
map1.put("2", "2");
//③
Map<String, String> map2 = new HashMap<>(2, 0.5f);
map2.put("3", "3");
① 没有设置initialCapacity,也没有设置负载因子,第一次put的时候会触发扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
第一次的时候,数组长度为默认值16,阈值为16*0.75=12,走的代码4
逻辑,等到数组长度超过阈值12后,触发第二次扩容,此时table数组,和threshold都不为0,即oldTab、oldCap、oldThr都不为0,先走代码1
,如果oldCap长度的2倍没有超过最大容量,并且oldCap 长度大于等于 默认容量16,那么下次扩容的阈值 变为oldThr大小的两倍即 12 * 2 = 24,newThr = 24,newCap=32
② 设置了initialCapacity,没有设置负载因子,此时hashMap使用默认负载因子0.75,本实例设置的初始容量为2,通过计算阈值为2,第一次put的时候由于还没初始化table数组,因此触发第一次扩容。此时oldCap为0,oldThr为2,走代码3
,确定这次扩容的新数组大小为2,此时还没有确定newThr 下次扩容的大小,于是进入代码5
确定newThr为 2 * 0.75 = 1.5 取整 1 ,及下次扩容阈值为1。当数组已有元素大于阈值及1时,触发第二次扩容,此时oldCap为1,oldThr为1,走代码1
newCap = oldCap << 1 结果为 4 小于最大容量, 但oldCap 小于hashMap默认大小16,结果为false,跳出判断,此时由于newThr等于0,进入代码5
,确定newThr为 4 * 0.75 = 3,下次扩容阈值为3
③ 设置了initialCapacity=2,负载因子为0.5,通过tableSizeFor计算阈值为2,第一次put的时候,进行扩容,此时oldCap为2,oldThr为2,进入代码1
,同实例②,newCap = oldCap << 1 结果为 4 小于最大容量, 但oldCap 小于hashMap默认大小16,结果为false,跳出判断,进入代码5
,确定newThr为 4 * 0.5 = 2,下次扩容阈值为2
面试回答要素
要点:
红黑树也是一种平衡树,但不是严格平衡,平衡树是左右子树高度差不超过1,红黑树可以是2倍
红黑树在插入、删除的时候旋转的概率比平衡树低很多,效率比平衡树高
查找时间复杂度都维持在O(logN)
关于HashMap的其他文章:https://blog.csdn.net/login_sonata/article/details/76598675 源码解析:https://www.jianshu.com/p/0a70ce2d3b67
Exception,Error有共同的父类Throwable
Error: 表示程序发生错误,是程序无法处理的,不可恢复的,如OutOfMemoryError
Exception: 表示程序可处理的异常,又分为CheckedException(受检异常)、UncheckedException(非受检异常),受检异常发生在编译期,必须要使用try…catch 或者 throws捕获或者抛出异常,否则编译不通过;非受检异常发生在运行期,具有不确定性,主要由程序的逻辑问题引起的,在程序设计的时候要认真考虑,尽量处理异常
可以直接使用LinkedHashMap实现
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private int initialCapacity;
public LRUCache(int initialCapacity) {
//true表示按照访问顺序排序
super(initialCapacity, 0.75f, true);
this.initialCapacity = initialCapacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
if (size() > initialCapacity) {
return true;
}
return false;
}
}
Stream流是Java8中引入的新特性,Stream有几个特点:
不存数据,都是通过管道将源数据元素传递给操作;
对Stream的任何修改都不会修改数据源,都是新产生一个流
流的很多操作如filter、map都是延迟执行的,只有到终点才会将操作顺序执行
对于无限流可以通过“短路”操作访问到有限元素后就返回
流的元素只访问一次,如果需要重新访问,需要重新生成一个新的流
Stream中BaseStream规定了流的基本接口,在Stream中使用Stage来描述一个完整的操作,将具有先后顺序的各个Stage连一起,就构成了整个流水线。
AbstractPipeline是流水线的核心,定义了三个AbstractPipeline类型的变量:sourceStage(源阶段)、previousStage(上游pipeline,上一阶段),nexStage(下一阶段)
ReferencePipeline 继承了AbstractPipeline
Head、StatefulOp、StatelessOp继承了ReferencePipeline,分别表示源,无状态操作,有状态操作
比如Collection.stream()方法得到Head也就是stage0,紧接着调用一系列的中间操作,不断产生新的stage。这些stage对象以双向链表的形式组织在一起,构成整个流水线。 由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就建立起对数据源的所有操作。