尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Java测验2:六大核心考点深度解析与高频错误复盘

Java测验2:六大核心考点深度解析与高频错误复盘 我们团队最近在Java技术内部分享会上用了一份内部整理的“Java测验2”试卷做了次摸底考核。说真的这套题做下来很多工作了三五年的老伙计都栽了跟头反而几个刚毕业的应届生表现亮眼。我把它重新梳理了一遍把每道题背后的知识点、出题意图、错因分析都整理出来希望对正在学Java或者准备面试的朋友有帮助。这套测验不是什么竞赛级难题就是对Java基础到中高级知识的一次全景扫描。覆盖了语法细节、面向对象、集合框架、Lambda表达式、异常处理、JVM内存这几个高频考察方向。特别适合这几类人准备Java实习或校招的在校生、刚转行Java想自查基础的自学者、以及想验证团队新人基本功的技术leader。1. 测验整体设计与思路拆解1.1 为什么以“测验”而不是“面试题”的形式整理起初我只是想整理一份Java面试题合集方便团队内部互相考查。但后来发现纯面试题容易陷入“背八股”的误区——大家看到题目就知道在考什么答案背得滚瓜烂熟但换个问法就懵了。测验就不一样。它把知识点打散混在真实的代码场景里需要你现场分析输出结果、判断代码能否编译、找出隐藏的逻辑错误。这种形式更能暴露真实水平。我做了个简单对比考察形式考察能力易伪装程度面试口述题记忆能力、表达能力高可背题笔试题选择/填空记忆能力、判断能力中代码阅读题输出结果代码推演能力、底层原理掌握度低代码改错题实战经验、调试能力低这份试卷大部分是代码阅读和结果预测题。1.2 知识点分布与出题逻辑整套卷子按Java知识体系的六大板块出题每个板块的题量和权重不是随便定的对应了实际开发中踩坑率最高的地方。Java基础语法与运算符20%类型转换、运算符优先级、标识符命名规则面向对象核心特性25%继承、封装、多态、枚举集合框架与泛型20%ArrayList、HashMap的底层行为Lambda与函数式编程15%Comparator、Stream常见用法异常处理机制10%受检异常与非受检异常、数组越界JVM与工具链10%内存溢出、Lombok、JDK版本问题这套权重不是凭空拍的。我自己带过几次新人发现刚入行的开发者最薄弱的不是框架而是基础语法和集合类的底层行为。很多人会用HashMap但不知道它在JDK 8之后引入了红黑树会用ArrayList但不清楚扩容机制。这些东西平时写业务代码不碍事一到性能优化或者排查线上问题就露馅。2. 六大核心考点深度解析2.1 基本语法运算符、类型转换和标识符最爱挖坑Java基础语法部分最容易出错的集中在三块。第一是整数除法。int a 5 / 2;的结果是2而不是2.5这个绝大多数人都知道。但换成double c 5 / 2;结果依然是2.0因为右侧的整数除法先执行了。想得到2.5必须写成5 / 2.0或者(double) 5 / 2。这个知识点在试卷里是必考题考的不是除法本身而是类型转换的优先级。第二是运算符优先级。int a 1; int b a a;很多人会算错。a先返回1此时a变成2a先把a变成3再返回3所以结果是4。这种题看起来无聊但实际写代码时在复杂表达式中混用自增自减和赋值运算很容易产生难以察觉的bug。我的建议是实际开发中不要写这种一行完成多件事的代码但考试必须知道结果。第三是标识符命名规则。汉字可以作为Java标识符开头这是合法的。int 变量 10;能编译通过。同时$和_也是合法标识符字符。但数字不能开头123abc非法。关键字不能做标识符不过class1这种是合法的因为后面跟了数字。2.2 面向对象三大特性继承和多态是重灾区面向对象部分我认为是整个Java学习路线中最核心也最容易被误解的部分。封装考的是访问修饰符的控制范围。protected修饰的成员不仅同包可见子类也可以访问哪怕子类在不同的包。这个概念太多人混淆了以为protected就是“同包可见”忘了“继承可见”这一层。继承考的是初始化顺序。有一个经典题目创建子类对象时父类静态代码块、子类静态代码块、父类实例代码块、父类构造器、子类实例代码块、子类构造器的执行顺序是什么正确答案是父类静态代码块 → 子类静态代码块 → 父类实例代码块 → 父类构造器 → 子类实例代码块 → 子类构造器。静态代码块只在类加载时执行一次实例代码块在每次创建对象时执行且先于构造器。多态考的是方法重写与方法重载的区分以及动态绑定。核心规则只有一条编译看左边运行看右边。引用类型决定编译期能调用哪些方法实际对象类型决定运行时调用哪个版本的方法。如果子类重写了父类的方法并且通过父类引用调用执行的是子类的方法体。枚举类型在Java中不是简单的常量集合它本质上是继承了java.lang.Enum的类。枚举可以定义构造器、字段和方法。enum Color { RED, GREEN, BLUE }背后是三个Color类型的静态常量实例。枚举还可以实现接口、定义抽象方法。Java枚举还提供了values()、valueOf()等内置方法。试卷里出了一道题Color.valueOf(red)会怎样答案是抛IllegalArgumentException因为枚举常量名区分大小写必须是RED。2.3 集合框架ArrayList扩容和HashMap底层机制集合框架是Java开发中使用频率最高的工具类也是面试问得最细的地方。ArrayList底层是Object数组。默认容量是10当元素个数超过当前容量时会扩容为原来的1.5倍JDK 8之后。具体计算int newCapacity oldCapacity (oldCapacity 1);右移一位相当于除以2。扩容时会创建新数组用Arrays.copyOf把旧数组元素复制过去。看下面这道典型题ListString list new ArrayList(); list.add(a); // 只触发一次add list.addAll(Arrays.asList(b, c, d, e, f, g, h, i, j, k));第一次add时容量从0变为10后续批量添加11个元素时需要容量至少11于是触发扩容。从10扩容到15再把11个元素放进去数组长度15。很多人以为会扩容到20实际上1.5倍是右移一位再加原值10的1.5倍是15不是20。HashMap在JDK 8之后的底层结构是数组链表红黑树。默认初始容量16负载因子0.75当元素个数超过16 * 0.75 12时触发扩容扩容为原来的两倍。当链表长度大于等于8且数组长度大于等于64时链表转为红黑树当树节点数小于等于6时红黑树转回链表。HashMap允许一个null键和多个null值而Hashtable不允许任何null。ConcurrentHashMap在JDK 8之后抛弃了分段锁改用CAS synchronized锁住数组的单个桶节点细粒度更高。这些细节在开发中也许用不到但排查性能问题时非常关键。2.4 Lambda与函数式接口从Comparator开始Lambda表达式本质上是对匿名内部类的一种简化但它不是语法糖那么简单背后依赖于函数式接口——只有一个抽象方法的接口。Java 8开始Comparator接口增加了大量默认方法和静态方法。比如Comparator.comparing(Function keyExtractor)可以按对象的某个属性排序thenComparing可以追加次级排序条件reversed()可以反转排序顺序。热词里提到了“Java Comparator.comparing 将某元素值放第一个”这是一个非常常见的排序需求。实现方式如下ListUser users new ArrayList(); // 期望status为0的排最前面其余按年龄降序 users.sort(Comparator .comparingInt((User u) - u.getStatus() 0 ? 0 : 1) .thenComparing(User::getAge, Comparator.reverseOrder()));这种写法比传统的匿名内部类简洁得多而且可读性更好。lambda函数式编程在Java中的地位越来越重要Stream API配合Lambda可以做非常复杂的集合操作ListString names users.stream() .filter(u - u.getAge() 18) .map(User::getName) .sorted() .collect(Collectors.toList());注意Lambda表达式捕获外部变量时变量必须是 effectively final——也就是说变量被声明后不再重新赋值。如果非要修改值可以用AtomicInteger或者数组包装。2.5 异常体系受检与非受检、数组越界Java的异常体系是分层的。Throwable是所有异常和错误的父类下面分为Error和Exception。Error表示JVM层面的严重问题比如OutOfMemoryError、StackOverflowError程序无法恢复不应捕获。Exception分为受检异常checked exception和非受检异常runtime exception。受检异常编译期强制处理必须用try-catch或throws声明。比如IOException、SQLException。非受检异常是RuntimeException的子类编译期不强制处理。比如NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException、IllegalArgumentException。热词中“java中数组越界异常”指的就是ArrayIndexOutOfBoundsException这是访问数组不存在的索引时抛出的非受检异常。代码如下int[] arr new int[5]; System.out.println(arr[5]); // 运行时报ArrayIndexOutOfBoundsException注意数组长度是5有效索引是0到4arr[5]越界了。这段代码能编译过但运行时会抛异常。所以数组越界是运行时异常不是编译错误。2.6 JVM与工具链内存溢出、Lombok与JDK版本热词中有好几个关于编译环境的问题这些恰恰是新手入门最容易卡住的地方。java: outofmemoryerror: insufficient memory是JVM启动时无法分配足够的堆内存导致的。常见于-Xmx设置过大但物理内存不足或者系统内存本身不够用。解决方法是调整JVM参数比如-Xms256m -Xmx1024m确保设置值不超过物理内存。java: you arent using a compiler supported by lombok, so lombok will not work是Lombok的经典报错。Lombok通过注解处理器在编译期生成getter/setter、构造器等方法。当IDE内置的编译器版本和Lombok版本不兼容时就会报这个警告。解决思路是升级Lombok版本或者换用支持当前JDK的Lombok版本。JDK 17配Lombok 1.18.30及以上版本比较稳定。java: 警告: 源发行版 17 需要目标发行版 17是Maven或Gradle编译时源代码编译级别和Java目标版本不匹配导致的。通常在pom.xml里检查maven.compiler.source和maven.compiler.target确保两者一致并且本机JDK版本不低于这个版本。java: internal error in the mapping processor: java.lang.nullpointerexception是MapStruct或类似注解处理器内部报错常见原因是Java版本与MapStruct版本不兼容或者使用了无效的映射配置。升级MapStruct版本、清理缓存后重启IDE通常能解决。3. 实战模拟一套可自测的Java题目与解析3.1 基础语法与类型转换这里是我整理的一份自测题建议先自己写出答案再对照解析。题目1以下代码输出什么int a 5; double b 2; System.out.println(a / b); System.out.println(a / (int) b);解析a / b中a是intb是doubleint自动提升为double结果是2.5。a / (int) b中b先被强转为int得到2a也是int整数除法结果是2。输出为2.5和2。题目2以下代码能否编译通过如果能输出什么String s Java; int[] arr new int[3]; System.out.println(s instanceof String); System.out.println(s instanceof Object); // System.out.println(arr instanceof String);解析s是String类型的引用s instanceof String为true。String是Object的子类s instanceof Object也是true。第三行编译报错因为int数组类型和String类型之间没有任何继承关系编译器直接判定不可能。题目3判断下列标识符哪些合法int _abc 1; // 合法下划线开头 int $abc 2; // 合法$开头 int 变量 3; // 合法Unicode字符 int 123abc 4; // 非法数字不能开头 int class 5; // 非法关键字不能做标识符3.2 面向对象与继承题目4输出什么class Animal { public String name Animal; public void speak() { System.out.println(Animal speaks); } } class Dog extends Animal { public String name Dog; Override public void speak() { System.out.println(Dog barks); } } Animal a new Dog(); System.out.println(a.name); a.speak();解析a.name输出的是Animal因为字段没有多态性访问字段看引用类型。a.speak()输出的是“Dog barks”因为方法有动态绑定看实际对象类型。这是最经典的“字段看左边方法看右边”的验证题。题目5下列代码哪里有问题enum Level { LOW, MEDIUM, HIGH } Level level Level.MEDIUM; switch (level) { case LOW: System.out.println(Low); case MEDIUM: System.out.println(Medium); }解析switch分支中缺少break会导致case穿透。当level为MEDIUM时会先输出“Medium”然后穿透到下面继续执行虽然下面没有代码了如果还有会继续执行。在枚举switch中case标签必须写常量名不能写成Level.LOW。题目6代码输出顺序是什么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 ); } } new Child(); System.out.println(); new Child();解析第一次创建时输出P-static C-static P-instance P-constructor C-instance C-constructor。第二次创建时静态代码块不再执行输出P-instance P-constructor C-instance C-constructor。3.3 集合框架与泛型题目7下面代码输出什么ListInteger list new ArrayList(); list.add(1); list.add(2); list.add(3); list.remove(1); System.out.println(list);解析输出[1, 3]。list.remove(1)删除的是索引为1的元素也就是第二个元素2而不是删除值为1的元素。如果想删除值为1的元素需要用list.remove(Integer.valueOf(1))。题目8HashMap的Key如果是一个可变对象会发生什么MapListInteger, String map new HashMap(); ListInteger key new ArrayList(); key.add(1); map.put(key, value); key.add(2); // 修改了key System.out.println(map.get(key));解析HashMap的hashCode是key在放入时计算的。修改key之后它的hashCode变了但HashMap链表数组中存储的位置还是根据原来的hashCode算的所以get(key)时重新计算hashCode定位到不同的桶返回null。这是一个很难排查的坑所以在实际开发中作为HashMap的Key的对象必须是不可变的或者至少在放入后不能被修改。题目9下列代码输出什么MapString, Integer map new HashMap(); map.put(null, 1); map.put(a, 2); System.out.println(map.get(null)); System.out.println(map.containsKey(null)); System.out.println(map.size());解析HashMap允许null键输出1、true、2。但是注意Hashtable和ConcurrentHashMap不允许null键会抛NullPointerException。3.4 Lambda与Stream题目10使用Lambda将列表按字符串长度排序再按字母顺序排序。ListString words Arrays.asList(banana, apple, cherry, date); words.sort(Comparator.comparingInt(String::length) .thenComparing(Comparator.naturalOrder())); System.out.println(words);解析先按长度排date4个字符在前apple和banana都是5个字符按字母序排apple在banana前最后是cherry。输出[date, apple, banana, cherry]。题目11Stream的中间操作是惰性的终端操作触发执行。以下代码输出什么StreamInteger stream Stream.of(1, 2, 3, 4, 5); stream .filter(n - { System.out.println(filter n); return n % 2 0; }) .map(n - { System.out.println(map n); return n * 10; }) .limit(1) .forEach(System.out::println);解析stream是惰性求值limit(1)会短路。执行过程为filter 1false舍弃、filter 2true、map 2、输出20。然后停止因为limit(1)已经满足。所以结果只输出filter 1、filter 2、map 2、20后面的元素不会继续处理。这个特性对大数据量的性能优化很有价值。题目12Lambda捕获变量int num 10; Runnable r () - System.out.println(num); num 20; r.run();解析这段代码编译报错。因为Lambda捕获的变量必须是effectively finalnum被重新赋值为20不再满足条件。如果删除num 20;这一行代码就能编译运行输出10。3.5 异常处理题目13以下代码输出什么try { int[] arr new int[5]; System.out.println(arr[10]); } catch (ArrayIndexOutOfBoundsException e) { System.out.println(Array index out of bounds); } catch (Exception e) { System.out.println(Exception); } finally { System.out.println(Finally); }解析访问arr[10]越界抛出ArrayIndexOutOfBoundsException被第一个catch捕获输出“Array index out of bounds”finally块始终执行输出“Finally”。注意多个catch时子类异常必须放在父类异常前面否则编译报错。所以ArrayIndexOutOfBoundsException必须放在Exception前面。题目14受检异常的处理public static void main(String[] args) { Thread.sleep(1000); }解析编译报错因为Thread.sleep声明抛出InterruptedException这是受检异常必须在方法签名上声明throws或try-catch捕获。正确写法是public static void main(String[] args) throws InterruptedException { Thread.sleep(1000); }3.6 JVM内存与工具链题目15以下代码会不会抛出OutOfMemoryErrorListbyte[] list new ArrayList(); while (true) { list.add(new byte[10 * 1024 * 1024]); }解析会抛出OutOfMemoryError堆内存不足。默认堆大小是物理内存的1/4但持续分配10MB的byte数组最终会耗尽堆内存。这个题目还考察了内存溢出和内存泄漏的区别内存溢出是分配不到足够内存内存泄漏是对象无法被GC回收导致占用内存不能释放。上例中list持有引用导致byte数组无法被回收既是内存泄漏也是内存溢出。题目16关于Lombok警告java: you arent using a compiler supported by lombok, so lombok will not work解析这是Lombok版本与JDK版本的兼容性问题。Lombok依赖内部编译器API进行注解处理JDK升级后API变化会导致Lombok失效。解决方案升级Lombok到支持当前JDK的版本。在Maven中可以用maven-compiler-plugin的annotationProcessorPaths显式指定Lombok版本。4. 高频错误与排查技巧实录4.1 数组越界和空指针总是结对出现数组越界异常ArrayIndexOutOfBoundsException和空指针异常NullPointerException是Java新手最常遇到的两个运行时异常。数组越界的排查思路第一步是看异常堆栈中打印的行号直接定位到出错的代码行。第二步是检查循环边界条件常见错误是写成或者索引从1开始而不是0。第三步是检查是不是用了动态计算的索引比如根据另一个数组的长度去索引当前数组两个数组长度不一致时最容易越界。空指针的排查思路是时刻确认对象是否可能为null。我习惯在代码里对可能为null的值做防御性判断或者用Objects.requireNonNull快速失败。Java 8之后可以用Optional来包装可能为null的返回值但它本身不解决null判断只是让调用方更清楚地知道值可能不存在。4.2 HashMap死循环与JDK版本演进在JDK 7及之前HashMap在并发扩容时可能形成环形链表导致get操作死循环CPU飙到100%。当时很多人在多线程环境下使用HashMap没有加锁踩过这个坑。JDK 8中修改了扩容方式扩容时保持原链表顺序或者通过高低位拆分避免了环形链表问题但并发下HashMap依然不是线程安全的因为多个线程同时put可能导致数据丢失。这个案例给了我一个很重要的教训不要靠侥幸写并发代码该用ConcurrentHashMap就用ConcurrentHashMap。它内部通过CAS synchronized实现线程安全读操作完全无锁写操作只锁桶的头节点并发性能远优于给整个HashMap加锁。4.3 Lombok警告与版本兼容Lombok是Java开发中非常常用的库用注解自动生成样板代码。但版本兼容问题确实烦人。我在JDK 17环境下遇到过you arent using a compiler supported by lombok的警告查了半天发现是Lombok版本太老1.16.x。解决方案很简单dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.30/version scopeprovided/scope /dependency升级后警告消失。这个问题的本质是Lombok通过JSR 269注解处理器在编译期修改抽象语法树不同的JDK版本的内部API有差异老版本Lombok无法适配新JDK。4.4 源发行版17需要目标发行版17这个警告在Maven项目中非常常见。根因是pom.xml里的compiler插件配置的source和target版本不同或者IDE的Java版本与项目的Java版本不一致。检查以下三处第一pom.xml中的属性定义properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target /properties第二maven-compiler-plugin的配置。第三IDE的Project Structure中Project SDK和Project language level是否一致。4.5 OutOfMemoryError的不同场景java.lang.OutOfMemoryError: Java heap space是最常见的堆内存溢出。排查步骤是先确定堆大小-Xmx参数。然后用jmap或MAT分析堆转储文件找出占用内存最大的对象。很多时候是某个List或Map无限制增长。java.lang.OutOfMemoryError: GC overhead limit exceeded是GC频繁回收但回收效果不明显垃圾回收线程占用大量CPU时间但堆内存依然不足。通常的解决方向是调整堆大小或者检查代码中是否有大对象没有释放。热词里还有个insufficient memory的场景这其实是JVM启动时分配内存失败一般是因为-Xmx设置的太大了超过了物理内存的限制。比如机器只有4GB内存但你设置了-Xmx4096m加上元空间和线程栈可能就超了。以下是一个常见的堆参数建议表场景堆大小建议说明本地开发调试-Xms256m -Xmx512m够用启动快普通Spring Boot应用-Xms512m -Xmx1g满足日常需求内存密集型服务-Xms2g -Xmx4g需确认物理内存足够大流量高并发服务-Xms4g -Xmx8g根据压测结果调整4.6 接口自动化测试框架的Java要素热词里出现了“java接口自动化测试框架”这个话题与测验中集合和Lambda的知识点关联很大。接口自动化测试框架用Java实现时核心是RestAssured或HttpClient加上TestNG或JUnit做断言和用例管理再用Java的集合和Stream做数据驱动。Test(dataProvider userData) public void testCreateUser(String name, int age) { User user new User(name, age); Response response RestAssured.given() .contentType(ContentType.JSON) .body(user) .post(/api/users); Assert.assertEquals(response.getStatusCode(), 200); }Lambda和Stream在接口测试中常用来自动校验一组响应数据是否符合预期比如所有订单金额都大于0、所有用户都有合法的手机号格式。5. 备考路线与学习经验5.1 Java学习路线怎么规划更高效很多人问Java学习路线怎么走我的建议是按“基础语法 → 面向对象 → 集合与泛型 → 异常处理 → IO与网络 → 并发编程 → JVM基础 → 框架”这个顺序来。基础语法和面向对象是一切的根基学不扎实后面全是空中楼阁。集合和泛型是日常开发使用频率最高的工具必须熟练掌握。异常处理决定了代码的健壮性很多线上事故都是异常处理不当导致的。并发编程和JVM基础是区分初级和高级开发者的分水岭。我的经验是不要一口气把所有理论都看完再动手。学完一个知识点就立刻写代码验证写个小demo跑一跑断点观察到实际行为后再学下一个。比如学HashMap就去debug看一下put方法怎么计算hash、怎么判断扩容学多态就写一个父类引用指向子类对象的例子观察调用结果。5.2 从“八股文”到真正的理解热词里“java面试八股文”反复出现可见这个概念的普及度。我的看法是八股文不是完全没价值但只背八股文肯定不行。八股文的题目本身是对知识点的提炼。比如“HashMap底层原理”这个问题提炼了HashMap的关键机制。但如果只背答案而不理解背后的设计思想面试官换个问法就暴露了。比如问“HashMap为什么负载因子是0.75”如果你只知道0.75不知道原因就说明没掌握。0.75是时间和空间成本的一个折中。负载因子太高比如1.0空间利用率高但发生hash冲突的概率增加查询效率下降。负载因子太低比如0.5冲突减少但空间浪费严重频繁扩容也更耗性能。0.75是综合统计后比较均衡的选择。所以背八股文的同时一定要追问自己“为什么”。为什么HashMap用红黑树而不用普通的平衡二叉树为什么ArrayList扩容是1.5倍而HashMap是2倍答得出这些为什么才算真正掌握。5.3 动手实践是最好的测验这份卷子做完之后我发现一个规律平时写代码多、经常调试的人做代码阅读题的正确率明显更高。原因很简单代码在脑子里跑一遍和真的在IDE里跑一遍虽然结果一样但对代码行为和运行时状态的感知是完全不一样的。所以我建议每个Java学习者都养成debug的习惯遇到不懂的写法就打断点一步步看变量的变化。特别是对象创建、方法调用、集合扩容这类过程用IDE的debug模式看到的画面远比书本上的文字描述直观。我个人的习惯是建一个demo工程专门用来验证各种“不确定”的Java语法行为。遇到一个疑问就写一个main方法跑一下跑完就删。这个习惯帮我解决了非常多的知识盲区也让我对Java的底层机制有了更深的体感。测验中涉及到的所有代码我在写这篇整理时也是一个一个跑过验证过没有任何凭感觉写的答案。另外再分享一个小技巧如果在一个类中定义多个main方法IDE只能选择其中一个启动。我通常建一个独立的Demo类每个类一个入口用类名区分这样验证起来更清晰。Java不需要你等到面试前才刷题平时遇到一个小疑问就顺手验证掉积累下来就是非常大的优势。
返回列表