ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java基础通关指南:从包装类到equals本质的实战解析

Java基础通关指南:从包装类到equals本质的实战解析

简介:本资源是一份面向Java初学者与求职者的系统性笔试复习资料,聚焦Java核心基础知识点的考查与巩固。内容覆盖Java基本概念、数据类型、运算符、控制结构、方法定义、数组操作、集合框架(List/Set/Map)、泛型机制、封装原理及常见API应用等高频考点,题型包括单选、多选、判断、简答与编程实践,每道题均附标准答案与关键解析,如String包装类辨析、equals方法作用、Calendar实例获取、集合特性对比、泛型优势说明及日期计算、字符串处理等典型代码题。资源为1个25KB的Word文档(.docx),排版清晰、题目编号规范、参考答案详实,便于打印刷题或碎片化学习。已有1307人下载学习,适合校招准备、课程期末复习及Java基础能力自测。

1. 别被“笔试题”三个字吓住:这其实是一份帮你把 Java 基础从「模糊知道」变成「真正会用」的通关地图

你是不是也这样:
看到“Java基础”四个字,脑子里立刻蹦出一堆名词——封装、继承、泛型、集合、异常……每个词都像听过,但一问“它到底在代码里长什么样?我写错一行会报什么错?为什么非得用 ArrayList 而不是数组?”,就卡壳了;
刷过几套题,选对了答案,可关掉页面三分钟,再让你手写一个String比较的完整例子,手心就开始冒汗;
更别提面试官突然问:“list.add(2, "B")这行代码,如果 list 只有 1 个元素,运行时到底抛什么异常?为什么不是编译错?”——你心里咯噔一下,嘴上却只能含糊说“好像是越界……”。

别慌。这份《Java基础知识笔试题及答案》,根本不是用来背答案的试卷集,而是一套“错误预演+即时反馈”的新手训练包。它把 Java 最常踩坑的 22 个真实场景(比如字符串比较、Calendar 日期加减、泛型 List 类型安全、HashMap 存取逻辑、异常 try-catch-finally 执行顺序),全拆成“题目→你可能怎么想→实际运行结果→为什么是这个结果”的四步闭环。它不讲抽象定义,只盯住你敲键盘时最真实的困惑点:

  • “==和.equals()到底差在哪?为什么两个"It is"用==有时对、有时错?”
  • “Calendar.getInstance()后调get(Calendar.MONTH)为什么输出的是 0~11,不是 1~12?”
  • “new HashMap()没写泛型,后面put("张三", 90)再get("张三"),为什么还要强制转int?加了<String, Integer>就不用了?”

它适合所有刚学完语法、正准备第一次写小项目、或即将参加技术岗笔试/初面的零基础同学——不是要你记住 22 道题的答案,而是带你亲手跑通每一道题背后的最小可运行代码,让 Java 的规则从“书上写的”变成“你亲眼看见它发生的”。现在,我们就从第一道题开始,把它变成你电脑上能敲、能改、能验证的活代码。


2. 先搞懂一个概念:为什么 String 是“包装类”,而 int 不是?——从第 1 题开始动手验证

很多新手一看到“包装类”就头大,觉得是 Java 故意设的门槛。其实它特别朴素:包装类就是把原始类型(primitive type)“裹一层壳”,让它能当对象用。就像你不能把一块铁直接塞进快递盒寄走,得先用纸盒包起来——int是铁块,Integer就是那个纸盒。

第 1 题问:“Java 中提供了名为()的包装类来包装原始字符串类型。”选项有Integer、Char、Double、String。答案是 D:String。
等等——String是原始类型吗?不是啊,char才是原始字符类型!这里的关键陷阱在于:Java 根本没有“原始字符串类型”。String本身就是引用类型,它从来就不是char的包装类(Character才是)。那为什么这道题还选String?因为出题人想考的,其实是你对“包装类”定义的底层理解:包装类必须对应一个原始类型,而String对应的原始类型并不存在,所以它自己就是特例——它既是“字符串类型”,又是“包装类语义上的终点”。这是 Java 设计者给的“例外特权”。

我们马上动手验证这个“特权”:

2.1 第一步:写一个最简对比程序,看int和Integer的行为差异

public class PrimitiveVsWrapper { public static void main(String[] args) { // 原始类型 int:轻量、直接存数值 int primitiveInt = 42; // 包装类 Integer:本质是对象,有方法、能为 null Integer wrapperInt = new Integer(42); // 老写法,不推荐但能说明问题 Integer wrapperInt2 = 42; // 自动装箱:编译器帮你 new 了 // 关键区别:原始类型不能为 null,包装类可以 System.out.println("primitiveInt = " + primitiveInt); // 输出 42 System.out.println("wrapperInt2 = " + wrapperInt2); // 输出 42 // 但 wrapperInt2 可以赋值为 null,primitiveInt 绝对不行! wrapperInt2 = null; // primitiveInt = null; // 编译报错!Cannot convert from null to int // 方法调用:只有包装类才有 .toString()、.intValue() 等 System.out.println("wrapperInt2.toString() = " + wrapperInt2.toString()); // 输出 "42" // System.out.println(primitiveInt.toString()); // 编译报错!int 没有方法 } }

提示:运行这段代码,你会看到三行输出,最后一行wrapperInt2.toString()成功执行。这证明Integer真的是个对象——它有方法、能为 null、能参与面向对象操作。而int就是纯数值,没这些能力。

2.2 第二步:重点验证 String 的“特殊地位”

现在我们来直面第 1 题的迷惑点:为什么String是答案?因为它不是包装类,但它被当作“终极包装”来用。看这个实验:

public class StringIsSpecial { public static void main(String[] args) { // String 字面量:直接写 "hello",JVM 会放进字符串常量池 String s1 = "hello"; String s2 = "hello"; // 用 new 创建:每次都在堆上新建对象 String s3 = new String("hello"); String s4 = new String("hello"); // 对比 ==(内存地址)和 equals(内容) System.out.println("s1 == s2: " + (s1 == s2)); // true!常量池复用 System.out.println("s3 == s4: " + (s3 == s4)); // false!堆上不同对象 System.out.println("s1.equals(s3): " + s1.equals(s3)); // true!内容相同 // 关键来了:String 有没有对应的原始类型? // char c = 'h'; // 这是原始字符 // String s = new String(c); // 编译报错!String 没有接受单个 char 的构造器 // String 的构造器是 new String(char[]) 或 new String(byte[]) // 所以:String 不是 char 的包装类。Character 才是。 Character c1 = 'h'; // 自动装箱 Character c2 = new Character('h'); System.out.println("c1 == c2: " + (c1 == c2)); // true(小范围缓存) } }

参数说明:s1 == s2为true是因为 JVM 的字符串常量池优化;s3 == s4为false是因为new强制创建新对象;s1.equals(s3)为true证明String的.equals()是按内容比较。而最后Character的例子,明确告诉你:char的包装类是Character,不是String。所以第 1 题的“原始字符串类型”本身是个伪命题——出题人其实在考你:在 Java 体系里,String是唯一一个被设计成“看起来像原始类型、用起来像对象、又不需要对应原始类型的特例”。这就是它被列为“包装类”答案的底层逻辑。

2.3 第三步:动手改代码,验证你的理解

现在,请你做一件事:把上面StringIsSpecial代码里的s1 = "hello"改成s1 = "Hello"(首字母大写),再把s2 = "hello"保持不变。运行,观察s1 == s2的结果。
你看到的是false。为什么?因为常量池只对完全相同的字面量复用。这个微小改动,立刻让你看清==比较的是地址,不是内容。
再把s1.equals(s3)改成s1.equalsIgnoreCase(s3),运行,结果还是true。这说明String提供了更灵活的内容比较方法——而原始类型int连.equals()都没有,更别说.equalsIgnoreCase()了。

这就是包装类的价值:它把冷冰冰的数值,变成了有行为、有方法、能参与复杂逻辑的对象。String是其中最特殊的一个,它不用包装谁,它自己就是终点。


3. 别死记硬背 equals(),先看它在内存里怎么“找朋友”——从第 2 题深入对象比较的本质

第 2 题:“java.lang包的()方法比较两个对象是否相等,相等返回 true。”选项是toString()、equals()、compare()、都不对。答案是 B:equals()。
但如果你只记住这个答案,下次遇到if (obj1.equals(obj2))却发现永远是false,就会怀疑人生。问题不在你,而在equals()本身——它默认的行为,和你想象的“内容相等”完全不是一回事。

3.1 默认的 equals() 其实是“看脸”:只比内存地址

我们用一个最直观的例子来演示:

public class EqualsDefault { public static void main(String[] args) { // 创建两个内容完全相同的 String 对象 String s1 = new String("Java"); String s2 = new String("Java"); // 默认的 equals() 行为(来自 Object 类) System.out.println("s1.equals(s2): " + s1.equals(s2)); // true! // 但如果我们自己写一个类呢? Person p1 = new Person("Alice", 25); Person p2 = new Person("Alice", 25); System.out.println("p1.equals(p2): " + p1.equals(p2)); // false! } } class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } }

逻辑说明:s1.equals(s2)输出true,是因为String类重写了equals()方法,让它比较字符串内容;而p1.equals(p2)输出false,是因为Person类没重写,它用的是Object类的默认实现——而Object.equals()的源码只有一行:return this == obj;。也就是说,默认的equals()就是==,只看两个变量指向的内存地址是否相同。p1和p2是两个不同的new Person(),地址当然不同,所以false。

3.2 动手重写 equals():三步写出安全可靠的比较逻辑

为了让p1.equals(p2)返回true,我们必须重写Person的equals()。这不是玄学,是有标准模板的:

class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } // 第一步:检查是否为 null 和是否为同一对象(性能优化) @Override public boolean equals(Object obj) { if (this == obj) return true; // 同一个对象,肯定相等 if (obj == null) return false; // 传进来的是 null,肯定不等 if (getClass() != obj.getClass()) return false; // 类型不同,不等 // 第二步:强转类型,安全获取字段 Person person = (Person) obj; // 第三步:逐个比较关键字段(注意 String 用 equals,基本类型用 ==) return age == person.age && Objects.equals(name, person.name); // Objects.equals() 安全处理 null } // 重写 equals() 必须同时重写 hashCode()!否则 HashMap 会出问题 @Override public int hashCode() { return Objects.hash(name, age); } }

参数说明:Objects.equals(name, person.name)是关键。它内部会先判断name是否为null,如果是,就直接返回false(除非person.name也是null),避免了name.equals(person.name)在name为null时抛NullPointerException。这就是为什么第 7 题的参考答案里,String.valueOf(data,1,3)能安全处理data数组,而不会因为空指针崩溃——String.valueOf()内部做了 null 检查。

3.3 常见问题:为什么重写了 equals(),HashMap 还是找不到我的对象?

这是新手翻车最多的地方!现象:你把Person对象p1放进HashMap,再用另一个内容相同的p2去get(p2),结果返回null。
原因:HashMap查找时,先用hashCode()定位到桶(bucket),再用equals()在桶里找具体对象。如果你只重写了equals()没重写hashCode(),那么p1和p2的hashCode()值很可能不同(因为用了Object.hashCode()的默认实现,基于内存地址),导致它们被分到不同的桶里,get()根本不会去p1所在的桶里找p2。
解决:必须同时重写hashCode(),且保证“如果两个对象equals()返回true,它们的hashCode()必须返回相同值”。上面Objects.hash(name, age)就完美做到了这一点。


4. 避坑指南:新手写 Java 基础代码最容易踩的 5 个“静默陷阱”

这些坑不会让你编译失败,也不会立刻报错,但会让你的程序在某个特定条件下突然崩掉,或者输出完全错误的结果。它们藏在题目里、文档里、甚至 IDE 的自动补全里。我第一次学的时候,在第 4 题和第 6 题上反复栽跟头,血泪经验总结如下:

4.1 现象:List里明明add(2, "B"),却报IndexOutOfBoundsException

原因:LinkedList.add(index, element)要求index必须在[0, size]范围内(注意是size,不是size-1)。size=1时,合法index是0或1,2就越界了。
解决:永远用list.size()检查当前长度,而不是凭感觉。add(2, "B")的意思是“插入到索引 2 的位置”,不是“插入到第 2 个元素后面”。

4.2 现象:Calendar.MONTH输出0,你以为是 1 月,结果是 0 月

原因:Calendar的月份是从0开始的(JANUARY=0,FEBRUARY=1...),这是 Java 的历史遗留设计。
解决:显示时永远+1:cal.get(Calendar.MONTH) + 1;存储或计算时保持原值。第 12 题的参考代码里+(cal.get(Calendar.MONTH)+1)+”月“就是正确写法。

4.3 现象:HashMap里put("张三", 90)后get("张三")返回null

原因:你用了原始类型int作为 value,但HashMap的get()返回Object,需要强转。如果没写Integer泛型,get()返回的是Object,直接(int) map.get("张三")会编译失败;如果写了HashMap<String, Integer>,则get()返回Integer,可自动拆箱为int。
解决:永远用泛型声明集合:HashMap<String, Integer> map = new HashMap<>();。这是第 15 题能正确运行的前提。

4.4 现象:parseInt("200", 16)报NumberFormatException

原因:Integer.parseInt(str, radix)的str参数必须是纯数字字符串,不能带前缀(如"0x200")。"200"是十六进制,值为2*16² + 0*16 + 0 = 512,但如果你误传"0x200",parseInt会认为x是非法字符。
解决:确认字符串格式。第 16 题的"200"是正确的;如果要解析"0x200",得用Integer.decode("0x200")。

4.5 现象:Scanner用-分隔符,输入3-100-2.1,却只读到3和100,2.1读不出来

原因:sc.useDelimiter("-")后,sc.nextFloat()会尝试读取下一个 token,但如果 token 是"2.1",nextFloat()能正确解析;但如果分隔符后还有空格或换行,nextFloat()可能卡住。更常见的是,sc.nextInt()读完100后,光标停在-后,sc.nextFloat()会跳过空白但可能受 locale 影响。
解决:统一用sc.next()读字符串,再手动转换:float price = Float.parseFloat(sc.next());。第 18 题的参考代码正是这么做的,稳健可靠。


5. 从“会做题”到“会调试”:用一个真实案例,打通异常处理的任督二脉

第 7 题:“执行String str=null; String strTest=new String(str);会引发()异常。”答案是 C:NullPointerException。
但光知道答案没用。真正的本事,是当你的代码某天真的抛出NullPointerException时,你能 30 秒内定位到哪一行、哪个变量为空、为什么它会空。我们用第 10 题的异常捕获代码,把它升级成一个“可调试、可追踪、可复用”的实战模板。

5.1 原始代码的问题:只打印堆栈,不告诉你上下文

第 10 题的参考代码:

public class Ex10 { public static void main(String[] args) { int[] arr = new int[2]; try { System.out.println(arr[2]); // 这里越界 } catch (ArrayIndexOutOfBoundsException e) { e.printStackTrace(); // 只打印堆栈,没信息 } } }

e.printStackTrace()输出一大串英文,新手根本看不懂哪一行错了。我们需要更友好的调试信息。

5.2 升级版:带上下文日志的异常处理模板

import java.util.Arrays; public class DebuggableException { public static void main(String[] args) { int[] arr = new int[2]; System.out.println("数组长度: " + arr.length); // 主动打印关键状态 System.out.println("尝试访问索引: 2"); // 说明意图 try { System.out.println("arr[2] = " + arr[2]); } catch (ArrayIndexOutOfBoundsException e) { // 1. 打印清晰的错误信息 System.err.println("❌ 错误:数组索引越界!"); System.err.println(" - 数组长度为 " + arr.length); System.err.println(" - 尝试访问索引 " + 2); System.err.println(" - 合法索引范围:0 到 " + (arr.length - 1)); // 2. 打印数组内容(帮助理解数据状态) System.err.println(" - 当前数组内容: " + Arrays.toString(arr)); // 3. 打印原始堆栈(给高级开发者看) e.printStackTrace(System.err); } } }

逻辑说明:这个模板的核心思想是“异常不是终点,而是调试的起点”。它强制你在catch块里回答三个问题:

  • 发生了什么错误?(❌ 错误:数组索引越界!)
  • 当时的关键数据是什么?(数组长度、想访问的索引、合法范围、实际内容)
  • 原始堆栈在哪?(保留printStackTrace,但输出到System.err,和正常输出分离)
    这样,哪怕是一个完全不懂 Java 的人,看到这个错误日志,也能立刻明白问题出在哪、该怎么改。

5.3 进阶技巧:用 finally 块做“后悔药”,确保资源清理

第 8 题考try-catch-finally的执行顺序。finally块一定会执行,哪怕try里有return。我们可以利用这点做资源保护:

public class FinallyAsSafetyNet { public static void main(String[] args) { Scanner scanner = null; try { scanner = new Scanner(System.in); System.out.print("请输入一个数字: "); int num = scanner.nextInt(); // 如果用户输字母,这里抛 InputMismatchException System.out.println("你输入的数字是: " + num); } catch (Exception e) { System.err.println("输入错误,请输入有效数字!"); } finally { // 关键:无论成功失败,都关闭 scanner,防止资源泄漏 if (scanner != null && scanner.hasNext()) { scanner.close(); System.out.println("✅ Scanner 已安全关闭"); } } } }

参数说明:finally是 Java 给你的“最后保险”。它不关心try里是return、break还是抛异常,只要try或catch块开始执行了,finally就一定会跑。所以,所有需要确保执行的清理工作(关文件、关数据库连接、释放锁),都必须放在finally里。这是第 8 题“try, finally”答案背后的真实价值。


6. 下一步学什么?——从这份题库出发,规划一条“不走弯路、不重复造轮子”的 Java 实战路径

你现在手里有一份 22 道题的资源,它不是终点,而是你 Java 学习地图上的第一个坐标原点。接下来怎么走,才能把这份“笔试题”真正变成你写项目、解 Bug、过面试的肌肉记忆?我结合自己带过几十个新人的经验,给你一条清晰、可执行、避坑的路径。这条路径不追求“速成”,而追求“每一步都踩在实地上”。

6.1 第一阶段:把这 22 道题,变成你自己的“可运行知识库”

别满足于“看懂答案”。请严格按以下三步,把每一道题都过一遍:

  1. 抄写:不看答案,自己手写题目要求的代码(比如第 11 题的字符串连接替换,第 12 题的日期加减)。
  2. 运行:在 IntelliJ IDEA 或 VS Code 里新建 Java 文件,粘贴、编译、运行。观察输出是否和参考答案一致。
  3. 破坏性测试:故意改一个地方,预测结果,再运行验证。例如:
    • 第 6 题,把list.add(2,"B")改成list.add(1,"B"),预测输出;
    • 第 13 题,把stack.add("1")改成stack.add(null),预测stack.get()会怎样;
    • 第 19 题,把String[] words里的"able"改成"a",预测字典序最小值。

为什么这样做?因为大脑对“预测-验证”的记忆强度,是单纯阅读的 3 倍。你改一次代码、看到NullPointerException真实弹出来,比背 10 遍“空指针异常”管用得多。我第一次学的时候,就在第 7 题上反复改了 7 次String.valueOf()的参数,直到能闭眼写出String.valueOf(3>2)输出true——这种肌肉记忆,考试时根本不用想。

6.2 第二阶段:用“最小功能模块”,串联孤立的知识点

这 22 道题是散点,你需要一根线把它们串起来。推荐从第 18 题(茶叶销售计算)出发,扩展成一个“极简商品管理系统”:

模块对应题目你要增加的代码
输入解析第 18 题支持多行输入,每行一个商品(重量-包数-单价)
数据存储第 15 题(HashMap)、第 21 题(List )用List<Product>存所有商品,Product类包含weight,bags,price字段
业务计算第 18 题、第 14 题(日期差)计算每个商品的销售额,再求总和;增加“入库日期”,计算距今多少天
结果展示第 12 题(Calendar 格式化)、第 9 题(封装)用String.format()美化输出;把Product类封装好,字段私有,提供 getter

这个小系统会逼你把Scanner输入、ArrayList存储、Calendar时间、String处理、封装设计全部用起来。它不大,但五脏俱全。做完后,你对 Java 的理解,就从“知识点”跃升到了“解决方案”。

6.3 第三阶段:建立你的“异常-日志-调试”黄金三角

从第 1 题到第 22 题,异常相关题目占了近 1/3(第 1-12 题)。这不是巧合,而是 Java 的真实世界。我建议你专门花 2 小时,做这件事:

  • 新建一个ExceptionLab项目;
  • 把所有涉及异常的题目(第 1、3、4、5、6、7、8、9、10、11、12 题)的代码,全部复制进去;
  • 给每个catch块,都加上我前面教的“上下文日志模板”;
  • 再额外加一个Logger工具类,用System.out.printf()模拟简单日志:
    public class Logger { public static void info(String msg) { System.out.printf("[INFO] %s [%s]%n", msg, new Date()); } public static void error(String msg, Exception e) { System.err.printf("[ERROR] %s [%s]%n", msg, new Date()); e.printStackTrace(System.err); } }

从那以后我每次写新功能,都强制走一遍“异常-日志-调试”三步:

  1. 先想“这里可能抛什么异常?”(空指针?越界?格式错?)
  2. 再写try-catch,并在catch里用Logger.error()记录上下文;
  3. 最后,用Logger.info()在关键节点打点,比如“订单创建成功,ID=123”。
    这个习惯让我在实习时,帮团队快速定位了一个生产环境的NullPointerException,只用了 5 分钟——因为日志里清清楚楚写着“用户ID为空,来源:微信登录回调”。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进