十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java基础面试核心知识点解析:JVM、集合与多线程实战梳理

Java基础面试核心知识点解析:JVM、集合与多线程实战梳理 1. JDK、JRE、JVM三个字母被问烂了但大多数人都答不透Java面试基础篇十个面试官九个会从JDK、JRE、JVM有什么区别开场。这个问题看着简单但恰恰是最容易暴露水平的地方——能背出字面意思的人很多能把一次编译到处跑背后的原理讲明白的人却不多。更重要的是这道题后面往往跟着一连串追问Java是编译型语言还是解释型语言javac做了什么main方法为什么要写成这样如果前面只是背了定义到这一层基本就卡壳了。1.1 三者区别与一次编译到处跑的实现原理先说结论再讲原理。JVMJava Virtual MachineJava虚拟机是一个运行Java字节码的虚拟计算机规范。它负责把字节码解释/编译成当前操作系统的机器指令。JREJava Runtime EnvironmentJava运行时环境包含JVM和Java核心类库。如果你只是运行Java程序装JRE就够了。JDKJava Development KitJava开发工具包包含JRE以及javac、jar、javadoc等开发工具。你要写代码、编译代码必须装JDK。面试官会追问那为什么同一个.class文件在Windows、Linux、Mac上都能运行关键在于.class文件里装的是字节码bytecode字节码是平台无关的中间指令。而JVM是平台相关的——每个操作系统都有对应版本的JVM实现JVM把字节码翻译成当前平台的机器指令翻译这一步由各自操作系统上的JVM完成。所以一次编译到处跑的前提是目标平台已经安装好了对应的JRE/JVM。冷知识编译器和JVM在底层其实都是平台相关的只有字节码是跨平台的很多新人搞混这个点。再往深一层javac把.java源码编译成.class字节码的过程其实经历了四个阶段词法分析把源码拆成token、语法分析构建抽象语法树、语义分析检查类型、变量使用等是否合法、字节码生成。面试时能主动说出这条链路会明显区别于只会背三句话的候选人。1.2 main方法为什么必须写成public static void main(String[] args)这个送分题翻车率极高。你反着问自己四个关键字能不能换publicJVM在外部调用main方法时需要一个公共入口所以访问权限必须是public否则虚拟机无法从包外访问到它。static如果main不是静态方法JVM需要先创建一个包含main方法的对象实例才能调用它。但这时JVM还没有加载任何业务对象用static可以避免实例化直接通过类名调用。void程序入口本身不向JVM返回结果结果通过System.exit(0/非0)来表达运行状态所以返回值是void。String[] args接收命令行参数。写成String... args也是允许的本质是数组。顺带提醒新人在自己电脑上经常遇到java: 警告: 源发行版 17 需要目标发行版 17这种报错本质是IDE的编译器配置和项目字节码版本不一致。检查一下Project Structure里的SDK版本和Java Compiler的target字节码版本保持统一就行这是个环境问题不是代码问题。1.3 环境变量配置JAVA_HOME、PATH、CLASSPATH的真相我在指导新人时发现环境变量这块被大量过时教程带偏了。现在装JDK不配置CLASSPATH也完全能正常编译运行。JDK 1.5之后javac会自动在当前目录及jar包中搜索类不再需要手动指定CLASSPATH来覆盖默认类路径。真正需要配置的是这两个JAVA_HOME指向JDK安装目录很多中间件比如Tomcat、Maven、IDEA会通过这个变量寻找JDK。PATH把%JAVA_HOME%\binWindows或$JAVA_HOME/binLinux/macOS追加进去。这样你在终端敲java -version、javac -version时操作系统才能在任意目录下找到javac和java执行文件。排查命令也很简单依次执行java -version、javac -version、echo $JAVA_HOME哪个命令找不到就说明PATH或JAVA_HOME没配好。遇到过有人在IDE里能运行但命令行里javac报不存在就是PATH的事。提示教程里让你把.;%JAVA_HOME%\lib这类CLASSPATH配置复制粘贴的时代早就过了。如果你在别人机器上看到这样的配置不用学那是历史遗留。2. 面向对象三特性说会背容易能扛追问才算数面向对象三大特性是什么——封装、继承、多态人人都能答。但面试官往往紧接着问那你说说多态底层的动态绑定是怎么发生的或者为什么有人说组合优先于继承这时候就分出高下了。我建议你把这三个特性当成三组面试题来复习而不是三个名词。2.1 封装不是字段private就完事封装的核心不是把属性设为private这么简单它的本质是信息隐藏和操作接口化。举个最经典的例子一个User类有age字段你直接public修饰调用方随手赋个-1程序不崩但数据全乱了。封装之后外部只能通过setAge(int age)方法赋值在方法内部可以做非空、范围校验这才是封装的真正价值。追问环节经常出现的是Java的四种访问修饰符不少人只知道public和private但讲不清protected和default的区别修饰符同类同包子类不同包任何地方private可以不行不行不行default不写可以可以不行不行protected可以可以可以不行public可以可以可以可以我会专门强调一句protected的设计目标就是继承友好让子类能访问父类的受保护成员但又不向外部开放。2.2 继承组合优先于继承这个结论怎么来的继承表达的是is-a关系Dog is-a Animal。它的好处是代码复用和形成多态。但继承有几个天然缺陷脆弱的基类问题——父类一改子类全崩菱形问题——一个类继承两个父类且两个父类有相同方法Java用单继承避开了一半问题。所以业界有个共识优先使用组合而不是继承。组合表达has-a关系一个类持有另一个类的引用。比如一辆Car有Engine你可以通过组合把引擎的启动逻辑委托给Engine对象而不是继承Engine来复用代码。组合的好处是关系更灵活、耦合更低。如果面试官继续追问方法重写的规则你需要答出这几点方法名和参数列表必须完全一致返回类型可以不同但必须是原返回类型的子类型协变返回类型访问权限不能比父类更严格比如父类是public子类重写不能变成protected或private不能抛出比父类更宽泛的受检异常父类的构造器不能被子类继承子类构造器第一行会隐式调用父类无参构造器super()。如果父类只有带参构造器子类必须显式用super(参数)调用。最后补一句里氏替换原则LSP所有能用到父类对象的地方换成子类对象都应该能正常工作。这一句说出来面试官会觉得你真的理解继承不是为了复用方便就随便extends。2.3 多态运行时才决定的动态绑定机制多态分两种编译期多态方法重载和运行期多态方法重写。面试官问多态绝大多数情况指的是运行期。Animal a new Dog(); a.speak(); // 输出汪汪a的静态类型是Animal但运行时JVM会根据实际对象类型Dog调用Dog重写的speak方法。这个过程叫动态绑定或后期绑定。底层机制是类加载的时候每个类会生成一个虚方法表vtable表中存着方法实际入口地址。方法调用时JVM通过实际类型的方法表找到对应实现而不是直接调用编译期确定的方法地址。我面试时还会加一问向上转型和向下转型了解吗向上转型是安全的Animal a new Dog()完全没问题因为Dog必然有Animal的能力向下转型有风险Dog d (Dog) a之前必须用instanceof判断否则可能ClassCastException。把这条链路捋顺多态这题就稳了。3. String系列Java基础面试的送分题也是送命题在Java面试的题库里哪类题最密集String和集合绝对并列第一。String看似简单但面试官可以从不可变性一路追问到常量池、equals/hashCode、StringBuilder优化、intern机制串起来就是一个小体系。这里我把最常考的集中拆开。3.1 为什么String被设计成不可变先把JDK 8和JDK 9的变化说清楚JDK 8及之前String内部是private final char value[]JDK 9开始为了节省内存改用private final byte[] value配合coder字段区分Latin-1和UTF-16编码。不管哪个版本value数组是final的String类也是final的没有提供任何修改内部字符的public方法所以String对象一旦创建内容不可变。为什么要不可变四个关键原因字符串常量池复用。如果String可变常量池里共享的字符串被一个引用改了所有引用都会受污染。安全性。String被大量用作网络参数、文件路径、反射的类名可变字符串会造成严重安全隐患。线程安全。不可变对象天然线程安全不需要同步。适合做HashMap的key。HashMap依赖key的hashCode缓存String不可变保证hash值稳定。经典追问new String(a)创建了几个对象答案是如果常量池里还没有a会先创建一个常量池对象再在堆中创建一个new对象如果常量池已有a只创建一个堆对象。面试后再补一个String s a b创建了几个对象编译器会直接优化成常量池中的一个ab一个对象都没有创建在堆上。3.2 、equals与hashCode的三角关系这道题是所有Java面试题的母题之一。比较引用是否指向同一个对象也就是内存地址是否相等equals()Object默认实现就是所以String类必须重写equals才能比较内容hashCode()返回对象的哈希值是HashMap/HashSet定位的依据。String重写equals的思路可以记住先判断引用是否相同再判断类型是否是String再逐字符比较。回答时能说出先比较length长度不同直接false避免逐字符扫描这种细节会加分。然后就是那个几乎必问的问题为什么重写equals必须重写hashCode因为对象有两个身份逻辑等值是equals判断的散列定位是hashCode判断的。约定是equals相等hashCode必须相等。反过来hashCode相等equals不一定相等这叫哈希碰撞。如果只重写equals不重写hashCode会怎样举个例子你创建了两个字段值相同的Person对象equals返回true但hashCode是Object默认的不同值。当你把Person放进HashSet时第一个对象根据hash落到桶A第二个对象根据不同的hash落到桶B。HashSet看到两个不同hash直接判定是不同对象于是同一个逻辑对象被存放了两次——这彻底破坏了Set不可重复的语义。提示HashMap用hash定位桶时hashCode是先用来帮助equals快速判断的粗筛条件所以在散列集合里hashCode和equals必须配合。3.3 String、StringBuilder、StringBuffer到底怎么选三者区别是必须背下来的String不可变拼接字符串会生成新对象适合少量操作、内容不变的场景。StringBufferJDK 1.0就有方法加了synchronized线程安全但性能略差。StringBuilderJDK 1.5引入线程不安全单线程下性能最优。但面试官不会满足于背表格他会问字符串拼接的内部是怎么实现的。如果你回答编译器会用StringBuilder优化要留个心眼——这只对单条拼接语句成立比如String s a b c会被编译成abc常量。但是循环里String s ; for (int i 0; i 10000; i) { s i; }每次循环都会new一个StringBuilder对象循环一万次就是一万次对象创建代价极大。所以真正写代码时循环拼接用手动创建StringBuilder。3.4 intern()字符串面试题的高频加餐String.intern()是一个native方法如果常量池中存在该字符串直接返回池中的引用否则把该字符串放入常量池并返回引用。这个方法的坑在于JDK 7之后常量池移到了堆中intern的语义有细微变化。面试基本考一道题String s1 new String(a) bc; String s2 s1.intern();问s1 s2是否为true。这类题太绕基础篇阶段你只要记住intern的作用是手动把字符串加入常量池、返回池中引用就够了不必深挖到版本差异。4. 集合框架HashMap的底层原理和并发安全隐患集合是Java基础面试的绝对大头尤其是HashMap。如果面试时间只有一个小时面试官至少会在HashMap上花十五分钟。从存储结构问到hash算法再问扩容再问线程安全一条龙下来基础是否扎实一目了然。这一节把这几个问题一次说完。4.1 集合框架地图与List的高频对比先在心里建一张图Collection接口派生出List、Set、QueueMap是独立体系。平时最常用的List实现是ArrayList和LinkedList对比维度对比项ArrayListLinkedList底层结构动态数组双向链表随机访问O(1)实现了RandomAccessO(n)插入/删除尾部O(1)均摊中间O(n)需要移动元素头部/尾部O(1)中间需遍历O(n)内存占用连续空间可能预留容量每个节点还有前后指针占用更高适用场景频繁随机访问频繁头尾增删ArrayList的扩容机制也是一个独立考点初始容量10每次扩容为原来的1.5倍新容量 旧容量 旧容量右移一位通过Arrays.copyOf把旧数据拷贝到新数组。追问为什么是1.5倍——太大会浪费内存太小会导致频繁扩容复制数组1.5倍是空间和时间的一个折中。4.2 HashMap底层存储结构数组、链表、红黑树三件套JDK 1.8的HashMap由数组 链表 红黑树组成。数组是主体每个位置叫桶bucket当多个key的哈希碰撞到同一个桶时用链表串起来链表长度超过阈值8且数组容量大于等于64时链表转红黑树把查找从O(n)降到O(logn)。HashMap的hash函数值得单独说一句key.hashCode()结果先做一次扰动计算h ^ (h 16)把高位信息混入低位降低哈希碰撞。索引计算则是(n - 1) hash——因为数组长度是2的幂n-1的二进制全是低位1与hash做位与等价于取模效率更高。put的完整流程要能讲成一段话计算key的hash值通过(n-1)hash得到桶下标如果数组为null或长度0先resize扩容桶为空直接new Node放入桶不为空比较key是否equals相等则覆盖value不相等且当前桶是红黑树节点走红黑树插入否则在链表尾部插入尾插法JDK 1.7是头插法并发下头插容易成环链表长度超过8尝试转红黑树还要看数组长度是否达到64元素个数后超过 threshold 容量 * loadFactor触发扩容。接下来是三个高频追问加载因子为什么是0.75这是空间和时间折中的经典结果。太高如1空间利用率高但碰撞概率大查询变慢太低如0.5频繁扩容浪费空间。0.75是大量实验后Java官方选定的平衡点。为什么链表树化的阈值是8源码注释里给出了统计依据HashMap的哈希函数足够散列时桶中元素个数服从泊松分布链表长度达到8的概率约为千万分之六极低。所以阈值8是为了在极端冲突时才动用红黑树正常情况链表足够用。退化阈值是6避免树和链表频繁切换。HashMap的key有什么要求equals和hashCode要按约定实现不要把可变对象当key否则插入后hashCode变化就再也查不到了。4.3 线程安全三兄弟HashTable、Collections.synchronizedMap、ConcurrentHashMapHashMap本身线程不安全并发下put可能丢数据甚至JDK 1.7扩容时形成循环链表导致死循环JDK 1.8改为尾插后死循环问题从扩容角度大幅缓解但仍有覆盖丢失问题。所以并发场景要换工具HashTable直接在方法上加synchronized简单粗暴。所有线程争抢同一把锁并发度极低基本淘汰。Collections.synchronizedMap包装类同样锁整张表性能一般。ConcurrentHashMap正确方案。JDK 1.7用分段锁每段独立加锁JDK 1.8改用CAS synchronized锁粒度细化到单个桶。读操作大多不加锁符合并发读多写少的实际场景。另一个集合并发经典问题是fail-fast在ArrayList遍历过程中如果调用list.remove()会抛出ConcurrentModificationException。原因是迭代器内部维护modCount每次next都会校验modCount是否改变。Java 8之后也可以用removeIf底层使用迭代器的remove方法不会抛异常。5. 异常体系除了try-catch面试官更想听你怎么设计异常异常这块的面试题看起来比集合少但问起来非常实战。常见的连环问是先问你异常分类再问你受检异常和非受检异常的区别然后让你写try-catch-finally的执行顺序最后问线上代码里catch环节最容易犯什么错。5.1 异常家族关系与受检异常之争异常体系的根是Throwable下面两大分支ErrorJVM层面的严重问题比如OutOfMemoryError、StackOverflowError。程序不该catch也不该处理因为这类错误出现后程序基本无法恢复。Exception程序层面的异常再分两类受检异常Checked Exception如IOException、SQLException。编译器强制要求你在方法上throws或try-catch否则编译不过。非受检异常RuntimeException及其子类如NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException。编译器不强制处理。面试考受检异常好还是非受检好时别站队给判断标准如果调用方能从异常中恢复用受检异常提醒他处理如果调用方无从处理就用非受检异常快速失败。但说实话现在Spring的开发模式下很多团队倾向于用非受检异常因为受检异常会污染方法签名到处throws让人烦躁而且很多受检异常比如IOException在Web应用里并没有真正的恢复场景。5.2 try-catch-finally执行顺序与return的坑经典题try块里有returnfinally块还会执行吗答案会。Java保证了finally一定在执行完try或catch后才返回。执行时机是try中return表达式的值先算好压栈然后执行finally最后返回。如果finally里也有returnfinally的return会覆盖try的return——这是最典型的错误写法绝对不要在finally里return。有三个情况finally不会执行System.exit()调用、JVM崩溃、执行try语句的线程被杀死。Java 7之后我更推荐try-with-resourcestry (FileInputStream fis new FileInputStream(a.txt)) { // 使用fis } catch (IOException e) { log.error(读取文件失败, e); }好处是只要是实现了AutoCloseable的资源使用结束后自动调用close()代码简洁而且关闭异常不会覆盖主异常——try-with-resources会用抑制异常suppressed exception记录关闭阶段的异常这在finally里手动close是做不到的。5.3 自定义异常与线上代码的三个坏习惯自定义异常要回答好关键是区分两个选择继承RuntimeException还是Exception我的建议是绝大多数业务异常继承RuntimeException因为业务异常通常没有调用方能恢复让事务回滚、快速失败更适合。实际写代码时最常见的三个坏习惯空catch块。吞掉异常是最坏的情况线上排查时你根本不知道哪里错了只能靠猜。只log e.getMessage()。这样打印出来往往是一个null pointer之类的空话真正的堆栈完全看不到。正确做法是log.error(xxx操作失败, e)把异常对象整个传进去日志系统会打印堆栈。new异常时不带原始cause。捕获IOException后抛自定义异常时要写throw new BusinessException(xxx, e)把根因传进去。否则到了上层你只知道业务失败不知道为什么失败。6. 泛型与反射基础篇里的两座大山泛型和反射属于那种平时写业务代码用得少但一用就离不开的知识。面试官考这两个主题真实意图是看你有没有理解Java的运行机制和框架底层的原理。Spring的依赖注入、MyBatis的Mapper代理、JDK动态代理都建立在这两座山上。6.1 泛型的本质是类型擦除一句话答核心Java泛型是编译期的语法糖运行时不携带类型参数信息。这就是所谓的类型擦除Type Erasure。ListString stringList new ArrayList(); ListInteger integerList new ArrayList(); System.out.println(stringList.getClass() integerList.getClass()); // true两个list在运行时Class完全相同都是ArrayList.class。String和Integer的类型信息在编译后就被擦除了List内部实际存放的是Object如果指定了上界就擦除到上界类型。那类型安全是怎么保证的编译器在编译期检查你只能往List 里放String并且在读取时自动插入强制类型转换。真正在字节码层面list.add和list.get都变成了Object的操作和强转。进阶考点是桥方法。比如子类实现一个泛型接口ComparatorString编译器会额外生成一个参数为Object的桥方法先强转再调用真正的compare方法目的是保证多态在泛型擦除后依然成立。把这个说出来面试官会觉得你阅读过Java官方文档的泛型章节。注意反射可以绕过泛型检查通过getMethod拿到add方法后invoke往List 里放Integer是能成功的。这个知识点经常和反射是否破坏安全一起考可以主动提一下。6.2 通配符与PECS原则泛型通配符的经典考法List?无界通配符任何类型都可以放但只能读不能写写什么类型都不安全。List? extends Number可以放Number及其子类的List只能读不能写。因为实际类型可能是List 你写个Double进去就类型不安全了。List? super Integer可以放Integer及其父类的List可以写Integer但读出来只能当Object用。记忆法则PECSProducer Extends, Consumer Super。如果参数只从集合中读取生产者用extends如果参数只往集合中写入消费者用super。JDK里Collections.copy(List? super T dest, List? extends T src)就是最经典的应用场景。6.3 反射动态获取类型信息的四个核心操作获取Class对象的三种方式必背Class? clazz1 Person.class; // 方式一类名.class Class? clazz2 person.getClass(); // 方式二实例.getClass() Class? clazz3 Class.forName(com.example.Person); // 方式三全限定名有了Class对象可以继续做四类事情查看类结构getDeclaredFields/getDeclaredMethods、创建实例clazz.getDeclaredConstructor().newInstance()、调用方法method.invoke(obj, args)、访问字段field.set(obj, value)。一个完整的反射调用示例Class? clazz Class.forName(com.example.User); Object user clazz.getDeclaredConstructor().newInstance(); Method setName clazz.getDeclaredMethod(setName, String.class); setName.invoke(user, 张三); Field name clazz.getDeclaredField(name); name.setAccessible(true); // 突破private限制 System.out.println(name.get(user));面试官通常会追问反射的缺点一是性能开销大反射涉及动态解析类型、安全校验比直接调用慢一个数量级所以框架会用缓存或运行时生成字节码如Spring使用CGLIB来规避二是破坏封装setAccessible(true)可以绕过private访问限制给安全带来隐患三是代码可读性差大量的反射代码很难阅读和调试。这里有个判断面试深浅的分水岭初级候选人知道反射能看到私有字段高级候选人知道反射调用私有字段在生产代码里要慎重并且能解释Spring中Autowired为什么借助反射缓存来提高性能。7. 多线程基础入门并发篇的地基在这里很多面试者到了JUCjava.util.concurrent部分开始背锁、背线程池但问到底层基础概念反而含糊。我自己的看法是基础篇的多线程没必要追到AQS那么深但下面四个问题必须烂熟否则并发篇就是空中楼阁。7.1 创建线程的四种方式与线程状态机继承Thread类重写run方法实现Runnable接口传给Thread实现Callable接口能返回结果、能抛异常配合FutureTask使用线程池ExecutorService。面试在聊第四种方式时重点不是怎么new一个线程而是为什么推荐用线程池减少线程创建销毁的开销、控制并发数量防止资源耗尽、方便统一管理定时、调度、监控。线程的6种状态要能说出来并解释状态间怎么流转NEW新建未启动、RUNNABLE可运行/运行中、BLOCKED等待锁、WAITING无限期等待、TIMED_WAITING有期限等待、TERMINATED终止。sleep和wait的区别是必考题sleep不释放锁wait会释放锁sleep是Thread的静态方法wait是Object的方法wait必须要在synchronized块中调用sleep则没有这个限制wait需要被notify/notifyAll唤醒或带超时参数自动醒来。7.2 synchronized与volatile可见性、有序性、原子性三角synchronized的三种用法// 1. 同步实例方法锁的是this对象 public synchronized void method1() { } // 2. 同步静态方法锁的是当前类的Class对象 public static synchronized void method2() { } // 3. 同步代码块锁是指定的对象 public void method3() { synchronized (lock) { } }底层原理每个Java对象头里有一个Monitor锁监视器锁线程进入同步代码块前要获取Monitor执行完释放。被synchronized修饰的代码块编译后会多出monitorenter和monitorexit指令。volatile要讲清楚两件事可见性和有序性。可见性一个线程修改共享变量会被主内存同步其他线程读的时候能立刻看到最新值。原理是volatile变量在读后会强制从主内存刷新写前会强制回写主内存并禁止指令重排序。不保证原子性count在字节码层面是读、加、写三步volatile只保证读和写各自的可见性不保证三步操作作为一个整体不被中断。所以volatile不能替代synchronized解决i并发问题。面试时给一道经典可见性题目一个线程死循环while (!flag)读一个普通boolean flag另一个线程改flag为true程序不会停把flag声明为volatile后程序能正常退出。能讲清楚这个例子volatile的可见性就理解到位了。7.3 从synchronized到ReentrantLockLock不是替代品是补充Lock接口主要是ReentrantLock和synchronized的区别用表格对比更清楚维度synchronizedReentrantLock获取锁方式隐式获取、释放显式lock()/unlock()需在finally中释放可中断性不可中断lockInterruptibly()支持中断等待超时获取不支持tryLock(timeout)支持公平锁非公平构造器可选公平/非公平条件队列wait/notifynewCondition()支持多个等待队列锁绑定每次只有一个条件一个Lock可关联多个Condition注意别答成Lock比synchronized更高级所以应该用Lock——在JDK 6之后synchronized经过锁升级优化无锁-偏向锁-轻量级锁-重量级锁在很多情况下性能并不差而且synchronized语法更简洁、自动释放锁更安全。Lock的价值在于需要超时、可中断、公平性等高级特性时才值得用。最后补充两个协作工具够用即可CountDownLatch让主线程等待一组线程执行完再继续CyclicBarrier让一组线程相互等待全部到达后再同时执行。基础篇能区分各自适用场景就行具体源码分析放到并发篇。8. Java 8新特性从Lambda到Stream高频必问现在面试Java不问Java 8新特性几乎不可能。Lambda、Stream、Optional、接口默认方法已经不只是语法糖而是日常开发的主旋律。不懂Lambda你连公司现成的代码都可能看不懂。这一节把核心考点过一遍。8.1 Lambda表达式与函数式接口Lambda的底层是函数式接口的实例。所谓函数式接口就是只有一个抽象方法的接口通常用FunctionalInterface注解标注。Java 8内置了一批函数式接口最常用的四个接口输入输出典型场景FunctionT,RTR转换类型PredicateTboolean过滤判断ConsumerTvoid消费处理SuppliervoidT延迟生成Lambda语法三要素参数列表、箭头、方法体。几个典型写法// 传统匿名类 Runnable r1 new Runnable() { Override public void run() { System.out.println(hello); } }; // Lambda Runnable r2 () - System.out.println(hello); // 方法引用更简洁的Lambda list.forEach(System.out::println); list.stream().map(String::toUpperCase).collect(Collectors.toList());方法引用四种形式要能区分类名::静态方法如Integer::parseInt、对象::实例方法如System.out::println、类名::实例方法如String::toUpperCase、构造器引用如ArrayList::new。8.2 Stream API中间操作是惰性的Stream的核心理念是管道式操作数据源 - 中间操作 - 终止操作。中间操作包括filter、map、distinct、sorted、limit、skip终止操作包括forEach、collect、reduce、count、anyMatch等。关键结论中间操作是惰性求值的没有终止操作Stream不会执行任何实际计算。比如list.stream().filter(x - x 10).map(x - x * 2); // 这行代码什么都不会做加上collect后整条链路才会真正执行。这个特性的实际价值是Stream可以在一次遍历中完成所有操作而不是每个中间操作都遍历一次集合避免中间集合的创建。分组统计是面试里常见的实操题MapString, Long countByType list.stream() .collect(Collectors.groupingBy(Item::getType, Collectors.counting()));parallelStream并行流也要提一句底层用ForkJoinPool公共线程池数据量小时并行反而更慢而且并行流中如果涉及共享可变状态会有线程安全问题。正确姿势是数据量大、元素处理耗时、且操作无状态时才考虑。8.3 Optional、接口默认方法与接口新能力Optional是用来解决NullPointerException的工具类。问题是很多新人把它用成了包装null其实Optional的核心是强迫调用方处理可能为空的情况而不是让你optional.get()直接取值。常用方法差异orElse(x): 为null时返回x无论是否为空x表达式都会先被求值orElseGet(() - ...): 为null时才执行lambda是惰性的orElseThrow(() - new BusinessException(...)): 为null时抛出异常这是我在业务代码中推荐最多的写法。接口的默认方法default method和新接口静态方法static method是Java 8解决集合库演进问题的关键。举例要给Collection接口新增stream()方法如果直接加抽象方法所有实现Collection的类全部要修改实现类数量巨大且不可控。用default方法提供默认实现已有的ArrayList、LinkedList等实现类不用改一行代码就能继承默认实现这就是默认方法存在的意义。追问时答出这一点就说明你理解了它的设计动机而不是只背了接口里可以写方法体。9. 基础篇复习时最容易翻车的几个细节前面几章把Java基础的大头过了但这些年我面试和带新人时发现有些零散细节特别容易翻车。它们单独拿出来不算一个体系但每一题都能在简历上划一道口子。这一章就给这些边角料收个口。9.1 数组越界与遍历时的坑ArrayIndexOutOfBoundsException是Java程序员第一个见到的异常之一但面试并不止于访问了不存在的下标。常见追问包括增强for循环编译后是什么样其实是基于Iterator的语法糖底层用Iterator遍历。数组版本的增强for会被编译成普通for循环 下标访问。遍历时为什么不能removeArrayList的Iterator在next()时会检查modCount一旦在遍历过程中调用list.remove()而不是iterator.remove()modCount变化就会抛出ConcurrentModificationException。正确做法是使用iterator.remove()或者用Java 8的removeIf。for(int i 0; i list.size(); i)每次循环都调用size()是不是低效ArrayList的size()是直接返回字段值O(1)不用担心但如果你有更好的写法可以先把size()存成局部变量。9.2 运算符和表达式的经典陷阱短路与、短路或|| vs 位与、位或|当左侧false时右侧不执行则两侧都执行。特例是和|在boolean运算中也有非短路语义面试题经常用if (a ! null a.equals(x))来挖npe陷阱。三目运算符的空指针陷阱boolean flag false; Integer num 1; int result flag ? num : 100; // 正常 Integer result2 flag ? null : 100; // 如果flagtrue自动拆箱NPE第二个例子中三目运算符的两个分支类型需要统一null被强制拆箱成int时抛NullPointerException。这是一个非常隐蔽的坑代码评审时我抓到过好几次。i和ii先自增再返回i先返回再自增。在循环里性能差异微乎其微所以这个问题通常只考返回值区别。9.3 标识符、枚举与BigDecimal看起来基础实际是底线标识符命名规则Java标识符以字母、下划线_、美元符$开头后续字符可以是字母、数字、下划线、美元符不能以数字开头不能是Java关键字。关于$这个字符有一个冷面试点编译器允许但规范强烈不建议使用因为内部类生成的类名会包含$。枚举enum很多人以为枚举只是常量列表你说到enum本质上是一个继承java.lang.Enum的final类可以定义字段、方法、构造器就比别人深一层。枚举可以用于switch、单例模式天然防止反射和序列化破坏、状态机建模。面试问枚举如何保证单例安全要能答出反序列化和反射都会破坏普通单例而枚举的序列化是专门处理的反射也无法创建枚举实例。BigDecimal为什么不能替换成double做金额计算double的二进制浮点表示无法精确表达0.1这样的十进制小数会出现0.30000000000000004这种误差。金额计算必须用new BigDecimal(String)而不是new BigDecimal(double)构造器传double仍然有误差正确写法是new BigDecimal(0.1)。这也是线上支付系统里一个真实的血泪教训。回到开头那句话八股不是背完就完事。我带候选人时发现能在大脑中画出HashMap put流程图的人比把源码注释背得滚瓜烂熟的人更靠谱。原因很简单——画图的过程逼着你去理清因果链条。我的建议是这一篇你读完别急着做下一件事先合上屏幕拿一张白纸把九章的标题写下来然后凭记忆在每章下面默写核心问题和答案链条。写不出来的就是你的薄弱点回头再翻一遍。这个方法不需要额外的资料也不用花钱但效率比自己闷头刷十遍题高出太多。基础篇地基打牢了后面的JVM、并发、Spring源码才接得住。
返回列表