Java基础面试知识点02 1. hashCode和equalshashCode介绍hashCode()的作用是获取哈希码也称为散列码它实际上是返回一个int整数这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在JDK的Object.java中Java中的任何类都包含了hashCode()函数。散列表存储的是键值对(key-value)特点是能根据key快速检索出对应的value这其中就利用到了散列码可以快速找到需要的对象。为什么要有hashCode以“HashSet 如何检查重复”为例对象加入 HashSet 时HashSet 会先计算对象的 hashcode 值来判断对象加入的位置看该位置是否有值如果没有HashSet 会假设对象没有重复出现。但是如果发现该索引位置已有元素这时会调用 equals() 方法来检查两个对象是否真的相同。如果两者相同HashSet 就不会让其加入操作成功。如果不同的话就会发生哈希冲突将新对象挂载到链表或红黑树上。这样就大大减少了 equals 的调用次数相应就大大提高了执行速度。如果两个对象相等则hashCode一定相同两个对象相等对这两个对象分别调用equals方法都返回true两个对象有相同的hashCode值他们也不一定是相等的因此equals方法被重写过则hashCode方法也必须被重写hashCode()的默认行为是对堆上的对象生成哈希值。如果没有重写hashCode()即便两个对象成员变量完全一致该类的两个对象哈希码大概率不同equals默认比较地址也不会判定相等2. ConcurrentHashMap原理jdk7和jdk8版本的区别JDK7底层数据结构ReentrantLock Segment HashEntry外层是Segment数组单个Segment内部封装HashEntry数组数组每个下标位置挂载单向HashEntry链表。寻址查询采用两次哈希定位第一次哈希锁定目标Segment第二次哈希在该Segment内定位链表头节点。加锁机制Segment直接继承ReentrantLock实现分段锁仅锁定当前操作的Segment分段其余Segment完全不受锁影响并发上限等于Segment分段数量可在构造方法手动指定单个Segment内部数组扩容不会干扰其他分段。get读操作查询全程不需要加锁依靠volatile修饰变量保证数据可见性。JDK8底层数据结构synchronized CAS Node 链表红黑树废弃Segment分层结构底层直接是Node数组链表长度超过阈值会转为红黑树优化查询效率Node节点里val值与next指针均用volatile修饰保障多线程可见。基础修改操作查找、替换、新增赋值优先使用CAS无锁操作。锁策略不再锁整个分段仅对当前哈希桶链表头head节点加synchronized锁锁粒度大幅缩小不会阻塞其他桶位元素读写并发性能更高扩容支持多线程并发协助迁移数据不会阻塞全局读写。读操作无锁设计Node的val、next被volatile修饰读写之间数据互相可见哈希数组table本身由volatile修饰扩容时读线程可以及时感知数组变化。对比总结JDK7依靠Segment分段锁ReentrantLock锁定整个分段两次hash寻址各分段独立扩容JDK8移除Segment改用CAS对桶头加synchronized锁链表过长转为红黑树支持多线程并发扩容锁粒度更细、并发能力更强两个版本get操作均不加锁依靠volatile关键字保证读取数据可见性。3. 如何实现一个IOC容器简要回答配置文件配置包扫描路径递归包扫描获取.class文件利用反射机制筛选需要交给IOC容器管理的类对需要注入依赖的属性完成依赖注入详细回答在配置文件中指定需要扫描的包路径自定义注解分别标记控制层、业务层、持久层、依赖注入、读取配置等标识注解读取配置文件中的扫描包路径遍历路径下文件与文件夹将所有后缀为.class的文件存入Set集合去重存储遍历Set集合通过反射获取类对象筛选带有指定组件注解的类使用线程安全Map存储实例对象完成Bean注册遍历IOC容器中所有Bean实例遍历成员属性识别注入注解递归获取对应Bean完成属性依赖注入4. 什么是字节码采用字节码的好处是什么Java中引入了虚拟机的概念这台虚拟机在任意平台都为编译程序提供统一接口。编译程序只需要面向虚拟机生成虚拟机能够识别的代码再由JVM解释器将虚拟机代码转换为当前操作系统的机器码执行。在Java中这种专供JVM解析执行的中间代码叫做字节码即扩展名为.class的文件它不面向任何特定CPU处理器只面向Java虚拟机。执行流程.java源代码 → javac编译 →.class字节码文件 → JVM解析翻译为机器码 → CPU执行好处实现跨平台源码仅需编译一次生成字节码不同操作系统只要安装对应平台版本JVM即可运行该字节码文件实现一次编译到处运行。兼顾运行效率与跨平台可移植性弥补传统纯解释型语言逐行解释执行速度慢的缺陷。5. Java类加载器Java源码编译后生成class字节码文件存储在磁盘类加载器ClassLoader是JVM中专负责查找、读取class字节码文件将类加载至JVM方法区并在堆中生成对应Class对象程序后续依靠该Class对象创建实例、访问类成员。JVM内置三种类加载器自上而下双亲委派层级关系如下启动类加载器Bootstrap ClassLoader最顶层加载器由C编写本身并非Java类负责加载JDK核心基础类库例如java.lang包下Object、String等核心类加载路径为jre/lib下核心rt.jar等包。扩展类加载器Extension ClassLoader父加载器为启动类加载器用于加载jre/lib/ext扩展目录下的jar包用来拓展JDK原生基础功能。应用程序/系统类加载器Application ClassLoader开发默认使用的加载器父加载器是扩展类加载器加载项目classpath下自定义编写代码以及Maven/Gradle引入的第三方依赖Jar包。6. 双亲委派模型加载流程应用类加载器收到类加载请求不会自身直接加载优先委托父加载器扩展类加载器尝试加载扩展类加载器继续向上委托给顶层启动类加载器启动类加载器在自身加载路径范围内查找目标类查找成功则直接加载若查找失败请求逐层向下回退由下级加载器在自身路径中完成类加载。作用安全防护防止开发者自定义java.lang.String这类核心类覆盖JDK原生基础类避免恶意代码篡改底层API保障程序运行安全。避免重复加载同一个类只会被某一个加载器加载一次防止JVM方法区中存在多份完全相同的Class对象节约内存空间。7. Java中的异常体系Java中所有异常与错误的顶层父类为Throwable。Throwable下分为两大子类Exception与Error。Error属于程序无法手动处理的严重系统级错误一旦触发通常直接终止程序进程。Exception不会直接导致程序整体崩溃又划分为运行时异常RuntimeException与受检异常CheckedException。RuntimeException运行时异常程序运行期间抛出编译阶段不会强制校验捕获会终止当前线程执行。CheckedException受检异常编译期强制校验必须使用try-catch捕获或者throws向上抛出否则代码编译报错。8. GC如何判断对象可以被回收1. 引用计数法给每个对象维护一个引用计数器新增引用计数1引用失效释放计数-1计数器为0时标记可回收。缺陷若A对象持有B引用、B同时持有A引用二者外部无其他引用计数器仍为1对象永远无法被GC回收出现内存泄漏因此Java未采用该算法。2. 可达性分析法Java正式采用以GC Roots作为起始节点向下遍历搜索遍历经过的链路称为引用链。若一个对象没有任何引用链与GC Roots相连则判定该对象不可达标记为可回收对象。可作为GC Roots的对象虚拟机栈栈帧本地变量表中引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象本地方法栈中Native方法JNI引用的对象对象回收补充说明被标记为不可达不等于立即销毁对象拥有一次自救机会依靠finalize()方法。对象第一次被标记不可达时放入F-Queue队列由低优先级Finalizer守护线程执行对象finalize()方法若在finalize方法内重新将该对象赋值给GC Roots可达引用对象自救成功跳过本次GC回收若自救失败第二次标记后该对象会被直接回收。9. 线程的生命周期/线程有哪些状态线程共五种核心状态新建、就绪、运行、阻塞、终止。阻塞状态细分三类阻塞分类等待阻塞运行线程执行wait()方法释放持有的所有锁资源线程移入等待池。无法自动唤醒必须由其他线程调用notify()或notifyAll()唤醒wait()是Object类方法。同步阻塞线程尝试获取synchronized同步锁时锁已被其他线程占用线程进入锁池阻塞排队竞争锁。其他阻塞线程执行sleep()、join()方法或是发起IO请求时进入阻塞。sleep超时、join等待线程结束、IO操作完成后线程重新转为就绪状态sleep()是Thread类静态方法。各状态详解新建状态New仅new创建Thread线程对象未调用start()未开启线程调度。就绪状态Runnable调用start()后线程进入就绪线程池具备运行资格等待操作系统分配CPU时间片。运行状态Running就绪线程获取CPU时间片执行run()方法内业务代码。阻塞状态Blocked线程主动/被动放弃CPU使用权暂停执行解除阻塞后只能先回到就绪队列。终止状态Deadrun()方法正常执行完毕或运行过程抛出未捕获异常终止线程线程生命周期结束。10. sleep()、wait()、join()、yield()的区别锁池与等待池概念锁池所有竞争同一把synchronized同步锁的线程存入锁池锁被占用时其余线程在锁池等待锁释放后锁池内线程争抢锁抢到锁后进入就绪队列等待CPU调度。等待池调用wait()后线程进入等待池等待池内线程不会参与锁竞争必须被notify/notifyAll唤醒后线程才会转入锁池参与锁争夺。notify随机选取一条等待池线程移入锁池notifyAll将等待池全部线程移入锁池。方法区别归属类sleep是Thread类静态本地方法wait是Object类本地方法。锁释放sleep不会释放持有的同步锁wait会释放当前对象锁并进入等待队列。同步依赖sleep无需配合synchronized同步代码块wait必须写在synchronized修饰的代码内。唤醒机制sleep休眠时间结束自动解除阻塞无参wait()必须被其他线程唤醒带超时时间wait()超时后自动唤醒。使用场景sleep多用于线程定时休眠、轮询间隔暂停wait用于多线程之间生产者消费者等线程间通信场景。CPU调度sleep让出CPU时间片强制上下文切换wait被唤醒后需要重新竞争锁不一定立刻获取CPU执行权。yield()执行yield()后线程直接回到就绪状态主动让出本次CPU执行权但依旧保留CPU调度资格操作系统下一次调度仍有可能再次选中该线程执行。join()在主线程B中调用线程A.join()线程B进入阻塞状态直到线程A执行完毕结束或是调用interrupt中断等待B才会解除阻塞继续执行。