1. 这套java测验5到底在测什么如果你正在准备Java面试或者学完Java基础后想检验一下自己到底掌握了多少这套java测验5就是给你准备的。我把这些年带新人、面试候选人时反复出现的考点以及日常开发中那些最容易让人翻车的细节整理成了几组自测题。每道题都不是单纯背概念而是会让你动手算、动手想做错的题往往能暴露出你知识体系里的真实短板。为什么要专门强调测验5因为我习惯把Java基础拆成多个专题来检验学习效果前面的测验分别覆盖了语法、流程控制、数组字符串这些入门内容这一套更偏向面向对象编程、集合框架、Lambda表达式以及经典排序算法这些中高频面试点。换句话说这套题做完你对Java基础到底学到什么程度这件事心里会有一杆秤。建议你先准备一个空白的代码编辑器或者草稿纸把下面的题目当作真实的笔试来对待先不看解析做完一组再看一组。如果一组题能答对80%以上说明这一块基本过关如果错的比较多正好可以通过后面的解析把知识点补上——这套测验的核心价值就是帮你精准定位漏洞。2. 第一组自测运算符、表达式与标识符的细节2.1 短路运算符和自增自减笔试常客先看一道看起来简单但错误率很高的题目int a 5; boolean flag (a 3) (a 5); System.out.println(a);问输出是什么很多初学者会按顺序一步步算觉得a 3成立然后a 5此时a变成6但6 5成立所以flag是true输出6。错。是短路与只要左边为false右边就不会执行如果左边为true右边才会执行。这里a 3为true所以右边a 5会执行a变成66 5成立flag为true输出6。等等这题我是不是在误导你重新算一遍a初始5a 3为true短路与需要判断右边a 5是先取a的当前值5去比较5 5为false然后a自增为6所以flag为false输出6。注意这里最关键的坑表达式的结果是false但a的值确实变成了6。再看自增自减的经典组合int i 3; int j i i; System.out.println(i i , j j);i是先使用i的当前值3参与运算然后i变为4i是先自增i变为5然后用5参与运算。所以j 3 5 8此时i的值是5。这类题考的其实就是表达式的值与变量的值分离这一核心概念很多人混淆的点在于自增自减表达式的返回值和自增自减结束后变量的值是两个东西。2.2 三目运算符的类型自动提升三目运算符? :也有一个很容易踩的坑。看这道题Object result true ? 1 : 2.0; System.out.println(result.getClass().getName());很多人会觉得结果是1因为true所以取第一个值类型是Integer。实际上Java的三目运算符有类型统一机制当两个分支的类型不一致且存在隐式转换关系时结果类型会自动提升为较大的类型。int和double会统一为double所以result实际是Double类型输出是java.lang.Double。这个点在日常开发中如果没注意很容易在序列化、反射等场景下产生隐蔽的Bug。2.3 标识符命名规则连老手都会犹豫的边界标识符命名规则其实很简单字母、数字、下划线、美元符号组成数字不能开头不能是Java关键字。但具体到某些边界情况比如$开头合法吗_单独作为变量名合法吗在Java 9之前_是合法的标识符但从Java 9开始_被保留为关键字单独用_做变量名会编译报错。再比如int、class、true、null是否可以作为标识符true、false、null是字面量不能作为标识符。这类题目在笔试里非常基础但出错率并不低因为很多人只记得不能是关键字却忘了字面量和保留字。3. 第二组自测面向对象从构造方法到多态3.1 继承体系下构造方法的执行顺序面向对象编程Java中最重要的就是继承。看这道class Parent { static { System.out.print(P-static ); } { System.out.print(P-instance ); } Parent() { System.out.print(P-constructor ); } } class Child extends Parent { static { System.out.print(C-static ); } { System.out.print(C-instance ); } Child() { System.out.print(C-constructor ); } } public class Demo { public static void main(String[] args) { new Child(); } }输出顺序是什么这个考点几乎每次面试都会出现核心规律是父类静态代码块→子类静态代码块→父类实例代码块→父类构造方法→子类实例代码块→子类构造方法。静态代码块只在类加载时执行一次实例代码块在每次创建对象时执行且先于构造方法。这里还有一个细节如果题目改成创建两个Child对象静态代码块不会再次执行但实例代码块和构造方法会再次执行。理解了执行顺序背后的类加载和对象初始化机制这类题就是送分题。3.2 动态绑定与重写方法调用的底层逻辑再看多态的核心——动态绑定class Animal { void speak() { System.out.println(animal); } } class Dog extends Animal { void speak() { System.out.println(dog); } void fetch() { System.out.println(fetch); } } Animal a new Dog(); a.speak(); a.fetch();a.speak()输出dog因为方法重写后JVM在运行时根据实际对象类型动态绑定到Dog的speak()。但a.fetch()编译就报错因为编译器根据声明类型Animal来判断可调用的方法Animal中没有fetch()。很多初学者在这个地方会混淆编译看左边运行看右边这句话编译期看声明类型决定方法是否存在运行期看实际对象类型决定调用哪个重写版本。3.3 重载与重写的区别面试高频重载是同一个类中方法名相同、参数列表不同的方法重写是子类对父类方法的重写方法签名必须一致。但有一个容易被忽略的点重载方法的返回类型可以不同但只有返回类型不同不足以构成重载重写方法要求返回类型可以协变即子类可以返回父类返回类型的子类型。再比如Override注解它不是必须的但加上后编译器会帮你检查是否真的重写了父类方法写错方法签名时能及时报错。实际开发中我见过太多因为漏写Override导致的意外Bug方法名打错一个字母结果没有重写父类方法程序行为完全不对。3.4 接口与抽象类怎么选才是合理的接口和抽象类的选择也是Java面试八股文里的常客。我的经验是优先考虑接口因为接口更灵活支持多实现抽象类适合模板方法模式把公共代码放在父类把可变部分留给子类实现。从设计角度说接口定义了能做什么能力契约抽象类定义了是什么公共血缘。比如定义一个Flyable接口鸟和飞机都可以实现而抽象类Bird只适合所有鸟类继承。Java 8之后接口可以有default方法这让接口的能力进一步增强但也带来一个经典问题一个类实现多个接口如果两个接口有同名default方法怎么办类必须重写该方法来消除冲突。4. 第三组自测集合框架、常用类与排序逻辑4.1 HashMap的底层实现别再只背数组加链表HashMap是集合框架里最核心的考点几乎每场Java面试都会问。要彻底理解HashMap需要从这几个层次看底层是数组加链表加红黑树默认初始容量16负载因子0.75阈值是容量乘负载因子。put一个键值对时先算key的hash值然后通过(n - 1) hash算出数组下标如果该位置为空直接放入不为空则遍历链表比较key相同则覆盖value否则尾插法插入新节点。链表长度超过8且数组长度超过64时链表转红黑树。扩容时容量翻倍元素重新分布。面试时除了要能说出这个流程还要能回答为什么链表转红黑树的阈值是8——因为泊松分布下链表节点数达到8的概率极低用8作为阈值是为了平衡空间和时间成本。还有一个高频问题hash函数为什么要高16位异或低16位因为(n - 1) hash只会用到低几位如果hash的高位有差异、低位相同会导致大量碰撞高16位参与异或能把高位的信息也混入低位减少碰撞概率。这些细节如果你只是死记硬背面试官一追问就露馅了。4.2 Comparator与Comparable把某个元素排到第一位集合排序相关Comparable是类内部实现自然排序Comparator是外部实现自定义排序。看一个实际开发中很常见的需求使用Comparator.comparing排序时想把某个特定值的元素排到第一位。比如有一个用户列表要按年龄升序但把管理员role admin永远排在最前面list.sort( Comparator .comparing((User u) - u.getRole().equals(admin) ? 0 : 1) .thenComparing(User::getAge) );这里的关键思路是先按是否目标值分组把目标值的权重设为0其余设为1然后再用thenComparing按业务字段排序。注意thenComparing的用法——只有当前比较器返回0时才会执行下一级比较这样就实现了先排到前面再按年龄排。如果不用Comparator手动写排序逻辑很容易写出Bug而且可读性差。这个技巧在日常报表、列表展示场景经常用到建议掌握。4.3 常用类与容器的一些基础但易错的知识点集合框架里其他容易出错的地方ArrayList底层是Object数组默认容量10扩容时新容量是旧容量的1.5倍LinkedList底层是双向链表随机访问性能差但插入删除快HashSet底层就是HashMap只是把value固定为一个常量对象。迭代时使用remove()方法必须用iterator.remove()而不是collection.remove()否则会抛ConcurrentModificationException。还有Arrays.asList()返回的是固定长度的List不支持add和remove很多人一上来就操作结果报错。字符串相关的String是不可变类每次拼接都会产生新对象大量拼接场景要用StringBuilder这些基础容器知识虽然不难但都是高频笔试考点。5. 第四组自测Lambda表达式与函数式接口的实战姿势5.1 Lambda语法从匿名类到函数式写法Java 8引入的Lambda表达式核心价值是把代码块作为参数传递本质上是对匿名内部类的一种简化。比如传统的Runnable写法new Thread(new Runnable() { Override public void run() { System.out.println(run); } }).start();用Lambda简化为new Thread(() - System.out.println(run)).start();Lambda的基本语法是(参数列表) - { 方法体 }如果参数只有一个可以省略括号方法体只有一行可以省略花括号和return。需要注意Lambda表达式只能用于函数式接口——也就是只有一个抽象方法的接口。Runnable、Comparator、Callable都是函数式接口。5.2 常用函数式接口认识它们才能用好Stream实际开发中常用的函数式接口有四个FunctionT, R接收一个参数返回一个结果ConsumerT接收一个参数不返回结果SupplierT不接收参数返回一个结果PredicateT接收一个参数返回boolean。Stream API的map、forEach、generate、filter分别对应这些接口。举个例子有一段用户列表想筛选出年龄大于18的用户并取他们的名字ListString names users.stream() .filter(u - u.getAge() 18) .map(User::getName) .collect(Collectors.toList());这里的u - u.getAge() 18就是PredicateUser::getName是方法引用等价于u - u.getName()。方法引用让代码更简洁但前提是方法签名和函数式接口的抽象方法一致。很多初学者看到方法引用觉得难懂其实只要记住方法引用只是Lambda的一种简写形式本质上还是传了一个函数逻辑进去。5.3 Lambda的变量捕获和实战注意事项Lambda有一个容易踩的坑lambda体内使用的外部局部变量必须是事实final的。什么是事实final就是变量在初始化之后没有再被赋值。比如int threshold 10; list.forEach(x - { if (x threshold) { ... } });这段代码没问题。但如果后面写了threshold 20编译直接报错。原因是Java为了保证线程安全会把捕获的变量做副本传递如果变量可变副本和原值可能不一致。另外不要试图在Lambda表达式中改变外部变量的值比如计数器累计这种场景应该用AtomicInteger或者数组包装。如果遇到需要修改集合元素的需求优先考虑用Stream的map生成新集合而不是在forEach里改外部状态这样代码更清晰也更安全。6. 第五组自测枚举类型、异常处理与那些学了就忘的边角料6.1 Java枚举类型的使用比你想的更强大Java枚举类型从表面看是一种特殊的类每个枚举常量都是该类的一个实例。基本用法很简单enum Color { RED, GREEN, BLUE }Color.RED、Color.valueOf(RED)、Color.values()、Color.valueOf这些常见方法要熟练。但实际开发中枚举的价值远不止于此枚举可以带字段、构造器、方法enum OrderStatus { CREATED(0, 已创建), PAID(1, 已支付), SHIPPED(2, 已发货); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } }这样设计的好处是状态码和描述集中管理不会散落各处配合持久层存储code展示层用desc就不用到处写魔法数字了。枚举还可以实现接口甚至可以用枚举实现单例模式——因为枚举的构造器是私有的且JVM保证枚举实例只会被创建一次天然线程安全。我实际项目中确实用枚举实现过单例配合readResolve()还可以避免反序列化破坏单例非常可靠。6.2 数组越界异常与异常体系运行时异常和受检异常ArrayIndexOutOfBoundsException是Java中最经典的运行时异常之一看这段代码int[] arr new int[3]; for (int i 0; i arr.length; i) { System.out.println(arr[i]); }当i等于3时数组就越界了。这个异常属于RuntimeException编译器不会强制你catch但运行时程序会直接崩溃。遍历数组或集合时注意边界条件的判断i arr.length这种写法是常见的越界来源正确写法是i arr.length。Java异常体系整体分两大类Exception和Error其中Exception又分为运行时异常RuntimeException的子类和受检异常Checked Exception。受检异常是编译器强制要求处理的比如IOException、SQLException运行时异常则不需要强制捕获。一个常见的面试问题是ClassNotFoundException和NoClassDefFoundError有什么区别前者是受检异常通常在类加载时显式用Class.forName()引发后者是Error表示JVM在运行时找不到类定义通常是编译时存在但运行时的类路径缺失导致的。这种问题如果没实际遇到过确实容易卡壳。6.3 字符串拼接、equals和的经典比较还有一个几乎每次面试都会碰到的问题和equals有什么区别 这题的答案其实不难比较的是引用地址equals默认也是比较引用地址但String类重写了equals让它比较内容。难点在于字符串常量池String s1 hello; String s2 hello; String s3 new String(hello); System.out.println(s1 s2); // true都是常量池里的同一个对象 System.out.println(s1 s3); // falsenew出来的在堆区 System.out.println(s1.equals(s3)); // true内容相同注意编译器会把字符串常量的拼接优化掉比如String s4 he llo在编译期就变成hello所以s1 s4是true。但如果用变量拼接如String s5 he new String(llo)运行期才会生成新对象结果就可能是false。这一块和JVM内存模型绑定在一起理解效果最好。7. 排序算法手写题冒泡排序与快速排序的实操对比7.1 冒泡排序基础中的基础但容易写错细节手写冒泡排序几乎是Java笔试题的保留项目。基本思路是相邻元素两两比较大的往后移每轮确定一个最大值的位置。最基础的写法public static void bubbleSort(int[] arr) { int n arr.length; for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { int tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; } } } }这里有一个常见的优化点如果某一轮遍历中没有发生任何交换说明数组已经有序可以提前退出。用一个boolean swapped标记即可。这个优化在笔试中如果主动写出来往往能让人眼前一亮。冒泡排序的时间复杂度最好O(n)最坏O(n^2)平均O(n^2)空间复杂度O(1)是稳定排序。它的主要应用场景是数据量很小、对代码简洁性要求高的地方。7.2 快速排序理解分治思想避开递归陷阱快速排序是考察分治思想的代表性算法也是Java面试中最高频的手写排序算法之一。核心思路是选一个基准值pivot把小于基准的放左边大于基准的放右边然后对左右两边递归排序。快速排序的时间复杂度平均O(n log n)最坏O(n^2)——当数组已经有序且基准值总是取第一个元素时退化为最坏情况。常见的优化是随机选基准或三数取中避免逆序数组导致O(n^2)的效率问题。一个简洁的实现public static void quickSort(int[] arr, int low, int high) { if (low high) return; int i low, j high; int pivot arr[low]; while (i j) { while (i j arr[j] pivot) j--; arr[i] arr[j]; while (i j arr[i] pivot) i; arr[j] arr[i]; } arr[i] pivot; quickSort(arr, low, i - 1); quickSort(arr, i 1, high); }这个实现采用的是挖坑法每次把一侧的坑填到另一侧最后把pivot放回坑里。手写时最容易出错的地方是循环条件的边界尤其是i j的判断不能丢否则数组会越界。另一个容易错的是判等条件如果写成arr[j] pivot而不是遇到重复元素时可能会死循环。快速排序是不稳定排序这一点和冒泡排序不同面试时如果被问到要能说出来。7.3 排序场景怎么选比单纯背代码更重要面试官让你手写排序算法不只是考察你会不会背代码更想看你是否理解每一种排序的适用场景。如果数据量很小比如几十个冒泡排序就够了代码简单不容易出错如果数据量大但基本有序插入排序或者优化后的快速排序会更好如果数据量大且无序快速排序是最常用的选择如果对稳定性有要求比如按多个字段排序时保持原顺序应该选归并排序。在Java里Arrays.sort()对基本类型用的是双轴快速排序对对象类型用的是TimSort归并排序的优化版本因为对象排序通常需要稳定性。这个底层实现细节知道的人不多面试时说出来会加分。8. 加餐环境配置与编译报错这些坑我帮你们踩过了8.1 环境变量配置与源发行版17需要目标发行版17Java环境变量配置是每个Java学习者起步的第一关。JAVA_HOME指向JDK安装目录Path里添加%JAVA_HOME%\binCLASSPATH现在基本不需要手动配置了。但实际开发中环境问题往往不会这么简单。比如启动项目时看到java: 警告: 源发行版 17 需要目标发行版 17这个报错的意思是当前编译器使用的源版本和生成字节码的目标版本不一致。常见于Maven项目里pom.xml设置了maven.compiler.source和maven.compiler.target为17但IDE的Java版本设置还是8或11或者反之。解决办法是检查三处pom.xml的编译插件版本、IDE的Project Structure里的Project SDK和Language Level、Maven的settings.xml里是否有全局编译配置。三处保持一致重启即可。8.2 Lombok的编译警告别直接忽略很多人在新项目里启用Lombok后会看到类似You arent using a compiler supported by Lombok, so Lombok will not work的警告。这是Lombok检测到当前IDE的编译器不在它的支持列表里。常见原因有三个一是IDE内置的编译器版本和Lombok版本不匹配二是项目中同时使用了其他注解处理器如MapStruct导致冲突三是Maven编译时指定的JDK版本与IDE不一致。处理方式不是忽略警告而是升级Lombok版本到和当前JDK兼容的版本并确认IDE的注解处理Annotation Processing已启用。在IntelliJ IDEA中是Settings → Build → Compiler → Annotation Processors → Enable annotation processing。这个开关默认是关闭的很多人启动项目时反射字段全部为null找半天原因最后发现是注解处理没有打开。8.3 VSCode运行Java报错乱码编码问题实战用VSCode写Java时一个很典型的问题是运行输出中文乱码。JDK 17之前的版本默认文件编码是GBK而VSCode默认的源文件编码是UTF-8两者不一致就会乱码。解决办法是在settings.json里设置{ java.debug.settings.consoleEncoding: UTF-8, java.debug.settings.vmArgs: -Dfile.encodingUTF-8, terminal.integrated.profile.windows: { args: [-Dfile.encodingUTF-8] } }或者在运行配置的vmArgs里加上-Dfile.encodingUTF-8。另一种做法是统一源文件的编码为GBK但新项目中一般推荐UTF-8所以我建议从JVM参数入手。这个问题在团队协作中也很容易引发冲突——一个人的环境是Windows默认编码另一个是UTF-8Git提交时diff会变成一坨乱码。最好的习惯是在项目根目录的.editorconfig或pom.xml里显式指定项目编码为UTF-8从源头避免。8.4 内存溢出错误OutOfMemoryError的出现场景与排查标题热词里还有java: OutOfMemoryError: insufficient memory这类运行时报错很常出现在大文件读取、批量数据处理的场景。OutOfMemoryError不是异常而是Error表示JVM内存不足程序无法继续运行。常见的有堆内存不足Java heap space和直接内存不足Direct buffer memory。排查时要先用jmap -heap或可视化工具如VisualVM看堆使用情况定位是哪个对象占用最多内存再回到代码里看有没有大对象、无限增长集合或者忘记关闭的资源。根本解决方案不是盲目调大-Xmx而是找到内存泄漏点。但如果你确认代码没问题、只是数据量确实大那调整JVM启动参数也是合理的比如-Xmx2g。需要特别注意的是OutOfMemoryError和StackOverflowError不一样后者是栈深度超过JVM限制通常是递归没有出口导致的解决办法是修改递归逻辑为循环。9. 这套测验的用法和我刷了几年Java题的一点心得前面这8组题目基本就是我检验Java基础掌握程度的固定套路。我自己的使用习惯是这样的第一遍快速做题不看解析每道题给自己一个确切的答案不会的标记出来第二遍对解析重点不是看题目本身而是看解析里提到的原理是否和自己理解的一致第三遍隔一周再重新做一遍错题检验是否真正吸收了。这个方法看起来笨但比起机械地背知识点效果好得多因为每一道题的解析都在教你为什么。做完这套测验如果你想继续加深可以试试反过来自己出题——把某个知识点改编成题目比如把HashMap的put流程画成一张图把冒泡排序改成降序等等。出题的过程会让你主动思考边界条件这种主动输出的学习效果是被动看答案的好几倍。Java基础就是这样知识点不算多但每个点都有很多细节把这些细节通过题目串起来体系自然就建立起来了。我在实际面试候选人时发现能把基础题答得滴水不漏的往往不是背得最多的而是真正理解原理的人。希望你也能通过这套测验找到自己理解上的盲区补上它们后面的路会顺很多。