Skip to content

Latest commit

 

History

History
609 lines (446 loc) · 29.7 KB

File metadata and controls

609 lines (446 loc) · 29.7 KB

1.Java 基础

[TOC]

《Java编程思想》、《疯狂Java:突破程序员基本功的16课.修订版》,这里省略了一些非常基础的知识点

请你解释为什么会出现 4.0 - 3.6=0.40000001 这种现象

二进制小数无法精确表达十进制小数,计算机在计算十进制小数的过程中要先转换为二进制进行计算,这个过程中出现了误差

请你讲讲一个十进制的数在内存中是怎么存的?

以二进制补码形式存储,最高位是符号位,正数的补码是它的原码,负数的补码是它的反码加1,在求反码时符号位不变,其他取反,1表示负数,0正数

接口和抽象类的区别是什么 ?

接口中的所有方法隐含的都是抽象的,而抽象类则可以同时包含抽象和非抽象方法

类可以实现很多个接口,但只能继承一个抽象类

类可以不实现抽象类和接口声明的所有方法,但这种情况下,该类必须声明成抽象的

接口中的成员函数默认是public的。抽象类的成员函数可以是private,protected或者是public。

JDK8后,接口中可以包含default方法,抽象类中不可以

面向对象开发的六个基本原则,在项目中用过哪些原则

  • 六个基本原则

    • 单一原则 一个类只做它该做的事情(高内聚)。在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一原则

    • 开闭原则 软件实体应当对扩展开发,对修改关闭,要做到开闭有两个要点:

      1. 抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点
      2. 封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变的复杂而繁乱
    • 里氏替换原则 任何时候都可以用子类型替换掉父类型,子类一定是增加父类的能力而不是减少父类的能力

    • 依赖倒置原则 面向接口编程。高层模块不应该依赖底层模块,两者都应该依赖其抽象,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代。

    • 接口隔离原则 类间的依赖关系应该建立在最小的接口上,不能大而全,接口表示能力,一个接口只应该描述一种能力,接口也应该高度内聚

    • 迪米特法则 又叫最少知识原则,一个对象应该对其他对象有尽可能少的了解

  • 根据自己的项目来说 详细的可以看这里:https://www.cnblogs.com/qifengshi/p/5709594.html

关于网络方面的问题,面试官应该只会问一题,不会多问

HTTP请求的GET与POST方式的区别

  1. GET在浏览器回退是无害的,而POST会再次提交请求
  2. GET请求会被浏览器主动cache,而POST不会,除非手动设置
  3. GET请求只能进行URL编码,而POST支持多种编码
  4. GET请求参数会被完整保留在浏览器历史记录中,而POST中的参数不会被保留
  5. GET请求在URL中传送参数是有大小限制的,不能大于2KB,而POST可以说没有
  6. GET只接受ASCII字符,而POST没有限制
  7. GET参数直接暴露在URL上,而POST将数据放在request body中

TCP 三次握手,四次挥手

  • SYN(Synchronize):同步标志位,用于发起连接、请求同步序列号。
  • ACK(Acknowledgment):确认标志位,用于确认收到对方数据,ACK=1表示确认有效。
  • 序列号(Seq):随机生成的初始值(ISN,Initial Sequence Number),后续数据按 “Seq + 数据长度” 递增,确保数据有序。
  • 确认号(Ack):表示 “期望收到对方的下一个序列号”,值为 “对方已发 Seq + 已接收数据长度”。
  • 三次握手详细流程(以 “客户端→服务器” 建立连接为例)
步骤 发起方 标志位 关键字段 目的
1 客户端 SYN=1 Seq = x(随机初始值) 向服务器 “请求建立连接”,并告知服务器:“我后续发送数据的序列号从 x 开始”。
2 服务器 SYN=1
ACK=1
Seq = y(服务器随机值)
Ack = x+1
1. 用ACK=1Ack=x+1确认 “已收到客户端的连接请求”;
2. 用SYN=1Seq=y向客户端发起 “反向连接请求”,告知客户端:“我后续发送数据的序列号从 y 开始”。
3 客户端 ACK=1 Seq = x+1
Ack = y+1
ACK=1Ack=y+1确认 “已收到服务器的反向连接请求”,至此双方确认 “收发能力正常”,连接建立。
  • 四次挥手详细流程(以 “客户端→服务器” 主动断开连接为例)
步骤 发起方 标志位 关键字段 目的
1 客户端 FIN=1
ACK=1
Seq = u(客户端当前 Seq)
Ack = v(服务器当前 Ack)
客户端告知服务器:“我没有数据要发了,请求关闭我的发送通道(单向)”,但仍能接收服务器的数据。
2 服务器 ACK=1 Seq = v
Ack = u+1
服务器确认 “已收到客户端关闭发送通道的请求”,此时客户端→服务器的发送通道关闭,但服务器→客户端的发送通道仍可传输数据(服务器可能还有未发完的数据)。
3 服务器 FIN=1
ACK=1
Seq = w(服务器剩余数据后的 Seq)
Ack = u+1
服务器发送完所有剩余数据后,告知客户端:“我也没有数据要发了,请求关闭我的发送通道(单向)”。
4 客户端 ACK=1 Seq = u+1
Ack = w+1
客户端确认 “已收到服务器关闭发送通道的请求”,此时服务器→客户端的发送通道关闭。客户端会等待2MSL(Maximum Segment Lifetime,报文最大生存时间) 后释放连接(确保服务器能收到该确认,避免服务器重发 FIN)。

TCP 和 UDP区别

  • 区别: UDP是无连接的,即发送数据之前不需要建立连接 UDP使用尽最大努力交付,即不保证可靠交付,同时也不使用拥塞控制 UDP是面向报文的,没有拥塞控制,适合多媒体通信要求 UDP支持一对一,一对多,多对一和多对多的交互通信 UDP首部开销小,只有8个字节 TCP是面向连接的运输层协议 TCP只能一对一连接 TCP提供可靠的交付服务,提供全双工通信 TCP 面向字节流,头部最低20个字节

从输入网址到获取页面的过程

  • 查询DNS, 获取域名对应的IP地址
    • 浏览器搜索自身的DNS缓存
    • 搜索操作系统的DNS缓存
    • 读取本地的HOST文件
    • 发起一个DNS系统调用(宽带运营服务器查看本身缓存,运营服务器发起一个迭代DNS解析请求)
  • 浏览器获得域名对应的IP地址后,发起HTTP三次握手
  • TCP/IP建立连接后,浏览器可以向服务器发送HTTP请求了
  • 服务器接收到请求后,根据路径参数,经过后端处理将页面返回给浏览器
  • 浏览器渲染页面,和外部资源,最终将完整的页面呈现给用户

Session与Cookie区别

Session:

服务器端会为每个访问服务端的请求分配一个会话Session,其数据存储在服务器端,不依赖浏览器端环境,因此高效安全

Cookie:

数据以文件形式存在用户浏览器端,用户可以通过浏览器禁用Cookie,用户可以对Cookie进行查看,修改,和删除

列出自己常用的JDK包

常用的包:

java.lang 包装类,线程等都在该包

java.math 有BigDecimal 精确数字类型

java.util 并发,集合等都在该包内

equals与==的区别

  1. equals 比较两个实体值是否相同,可以被覆盖,但需要遵循几个约定: 自反性:对于任何非null的引用值x, x.equals(x)必须返回true 对称性:对于任何非null的引用值x和y,当y.equals(x)返回true时,x.equlas(y)必须返回true 传递性:对于任何非null的引用值x、y、z,如果x.equals(y)返回true,并且y.equals(x)也返回true,那么x.equals(z)也必须返回true 一致性:对于任何非null的引用值x和y,只要比较对象中的所有信息没有被修改,多次调用equals一致返回true,或者false

  2. == 比较两个实体的引用地址是否相等,不能覆盖,如果引用地址相等,那认为两个实体为同一个实体

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);

hashCode和equals方法的区别与联系

对于覆盖了equals方法的类中,同样也要覆盖hashCode方法。这是JDK规定的结果。

比较两个对象是否相同,hashCode比equals效率更高,所以优先会根据hashCode来比较,但如果不重写hashCode,原本两个对象可以认为是相等,但由于hashCode默认返回表示对象地址的整数,必然不相等,所以需要重写hashCode。

由于hashCode有个问题,可能两个不同的对象会有相同的hashCode,这样还需要通过equals来比较

比如HashMap中,计算key的索引位置,会用到key.hashCode,在确定是否为同一个元素时通过equals比较

什么是Java序列化和反序列化,如何实现Java序列化?或者请解释Serializable 接口的作用

序列化是一种用来处理对象流的机制,也就是将对象的内容转化成二进制流,可以将对象持久化或者网络传输

反序列化是将二进制流还原为对象的过程

实现Java序列化,通过实现Serializable即可

Object类中常见的方法,为什么wait notify会放在Object里边?

因为Java提供的锁是对象级的,每个对象都有对象头,用来存储锁

解释一下extends 和super泛型限定符

<? extends Fruit> 称为 上界限定符,list只能get,不能add(除了add null值),通常用于读

<? super Apple>称为 下界限定符,list只能add,不能get(只能用Object接收),通过用于写

请列举你所知道的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() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。

创建线程的方式

  • 继承Thread类创建线程,并重写run方法,调用实例对象的start()方法启动线程
public class ThreadDemo extends Thread {
    @Override
    public void run() {
        // 线程执行逻辑
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }

    public static void main(String[] args) {
        ThreadDemo thread = new ThreadDemo();
        thread.setName("子线程1");
        thread.start(); // 启动线程,JVM会调用run()
        
        // 主线程逻辑
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}
  • 实现Runnable接口,并实现run方法,将实现Runnable的类传入Thread构造函数中,并调用Thread实例对象的start方法启动线程
public class RunnableDemo implements Runnable {
    @Override
    public void run() {
        // 线程执行逻辑
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }

    public static void main(String[] args) {
        // 创建任务对象
        RunnableDemo task = new RunnableDemo();
        
        // 创建线程并关联任务
        Thread thread = new Thread(task, "子线程1");
        thread.start();
        
        // 主线程逻辑
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}
  • 实现Callable接口,并实现call方法,创建Callable实现类的实例,使用FutureTask包装Callable对象,使用FutureTask对象传入Thread中,调用start方法启动线程,使用FutureTask对象的get方法获取线程的返回值
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableDemo implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 线程执行逻辑,返回计算结果
        int sum = 0;
        for (int i = 1; i <= 10; i++) {
            sum += i;
        }
        return sum;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建任务对象
        CallableDemo task = new CallableDemo();
        
        // 包装任务,用于获取结果
        FutureTask<Integer> futureTask = new FutureTask<>(task);
        
        // 启动线程
        new Thread(futureTask, "计算线程").start();
        
        // 获取结果(会阻塞,直到子线程执行完毕)
        System.out.println("1-10的和为:" + futureTask.get());
    }
}

ArrayList 与 LinkedList 区别

ArrayList 是一种顺序存储的线性表,底层使用数组实现

LinkedList是一种链式存储的线性表,本质是一个双向链表,实现了List、Deque接口,可以当成双向链表、队列、栈使用

自定义注解

  1. 声明注解的保留期限类型 @Retention(RetentionPolicy.RUNTIME)表示该注解可以在运行期保留 保留期限类型:java.lang.annotation.Retention SOURCE: 注解信息仅保留在目标类源代码文件中,对应的字节码文件不会保留 CLASS: 注解信息存在于源代码、字节码文件中,但运行期JVM不能获得该注解信息 RUNTIME: 注解信息存在于源代码、字节码文件、运行期JVM中,能够通过反射机制获取注解类信息

  2. 声明注解可以使用的目标类型 @Target(ElementType.METHOD) 表示这个注解只能在方法上使用 目标类型:java.lang.annotation.ElementType TYPE: 类、接口、注解类、Enum FIELD: 类成员变量或常量 METHOD: 方法 PARAMETER: 参数 CONSTRUCTOR: 构造器 LOCAL_VARIABLE: 局部变量 ANNOTATION_TYPE: 注解 PACKAGE: 包

  3. 使用@interface 修饰类

  4. 声明注解成员 成员无入参、不能抛出异常; 可以通过default成员指定默认值 成员类型只能使用基本数据类型、String、Class、enums、注解类型,及上述类型的数组类型。如ForumService value()是非法的 如果注解只有一个成员,则成员名必须取名为value(),再使用时可以忽略成员名和赋值号,如果注解类拥有多个成员时, 对value成员赋值,可以省略value和赋值号,如果是多个成员赋值,必须使用赋值号

ArrayList扩容机制是怎么样的? 详细说一下。

在往ArrayList add元素的时候,如果ArrayList 已有元素数量+1 大于 ArrayList 存储元素的总长度,就会触发扩容。

首先ArrayList会计算新数组的长度,长度为老数组的1.5倍,如果新数组长度还是小于插入元素需要的最小长度,那么新数组长度赋值为最小长度,如果超过ArrayList允许的最大长度Integer.MAX_VALUE($2^{31} - 1$),那么新数组长度为Integer.MAX_VALUE,否则为Integer.MAX_VALUE - 8(为什么要-8?Why the maximum array size of ArrayList is Integer.MAX_VALUE - 8?

最后将原数组元素拷贝到新数组进行扩容

为什么要预留8个字节

数组除了存储元素的数组体外,还需要额外的元数据来记录数组的长度、类型信息等,本质是 为数组元数据预留内存空间,避免因数组容量达到理论最大值时,元数据无内存可用而导致的分配失败

HashMap 1.7 和 1.8 的区别

  • 1.7,在发生hash冲突的时候,数据结构只有链表;

  • 1.8,数据结构有链表和红黑树,使用红黑树是为了能够提高查询效率。在链表长度达到7时(bingCount >= TREEIFY_THRESHOLD - 1),并且hash tab[]数组长度大于等于64时,将链表转换成红黑树,如果数组长度小于64,只是对数组进行扩容 https://blog.csdn.net/qq_21251983/article/details/90056067

HashMap中的key可以是任何对象或数据类型吗

  • 可以是null,但不能是可变对象,如果是可变对象,对象中的属性改变,则对象的HashCode也相应改变,导致下次无法查找到已存在Map中的数据
  • 如果要可变对象当着键,必须保证其HashCode在成员属性改变的时候保持不变

HashMap 初始容量 计算方法

如果在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,走代码1newCap = 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. 以后什么情况下会再次扩容,这次是怎么计算新数组大小,及阈值大小的

HashMap、ConcurrentHashMap初始化阈值为什么要是8,才转为红黑树?

  1. 当初始阈值为8时,链表的长度达到8的概率变的很小,如果再大概率减小的并不明显
  2. 树结构查找的时间复杂度是O(log(n)),而链表的时间复杂度是O(n),当阈值为8时,long8 = 3,相比链表更快,但树结构比链表占用的空间更多,所以这是一种时间和空间的平衡

手写HashMap

要点:

  1. 底层是数组结构
  2. hash冲突的时候,转换为链表
  3. 考虑扩容处理

HashMap在并发下会产生什么问题?有什么替代方案?(HashTable, ConcurrentHashMap)。它们两者的实现原理。

  • HashMap并发下产生问题:由于在发生hash冲突,插入链表的时候,多线程会造成环链,再get的时候变成死循环,Map.size()不准确,数据丢失
    https://www.iteye.com/blog/hwl-sz-1897468

  • HashTable: 通过synchronized来修饰,效率低,多线程put的时候,只能有一个线程成功,其他线程都处于阻塞状态

  • ConcurrentHashMap: 1.7 采用锁分段技术提高并发访问率
    1.8 数据依旧是分段存储,但锁采用了synchronized,内部采用Node数组+链表+红黑树的结构存储,当单个链表存储数量达到红黑树阈值8时(此时链表已有元素7),并且数组长度大于64时,存储结构转换为红黑树来存储,否则只进行数组的扩容 https://www.cnblogs.com/banjinbaijiu/p/9147434.html

HashMap 为什么不用平衡树,而用红黑树

红黑树也是一种平衡树,但不是严格平衡,平衡树是左右子树高度差不超过1,红黑树可以是2倍

红黑树在插入、删除的时候旋转的概率比平衡树低很多,效率比平衡树高

查找时间复杂度都维持在O(logN)

关于HashMap的其他文章:https://blog.csdn.net/login_sonata/article/details/76598675

源码解析:https://www.jianshu.com/p/0a70ce2d3b67

常见异常分为哪两种(Exception,Error),区别是什么,了解受检异常和非受检异常吗

Exception,Error有共同的父类Throwable

Error: 表示程序发生错误,是程序无法处理的,不可恢复的,如OutOfMemoryError

Exception: 表示程序可处理的异常,又分为CheckedException(受检异常)、UncheckedException(非受检异常),受检异常发生在编译期,必须要使用try...catch 或者 throws捕获或者抛出异常,否则编译不通过;非受检异常发生在运行期,具有不确定性,主要由程序的逻辑问题引起的,在程序设计的时候要认真考虑,尽量处理异常

实现一个LRU(最近最少使用)

可以直接使用LinkedHashMap实现

《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;
    }
}

获取一个Class对象的方式

  1. 通过类对象的getClass方法
User user = new User();
Class<?> clazz = user.getClass();
  1. 通过类的静态成员,每个类都有隐含的静态成员class
Class<?> clazz = User.class;
  1. 通过Class类的静态方法forName方法获取
Class<?> clazz = Class.forName("com.itsaysay.User");