首页 / 资讯中心 / 文章详情

Java基础类库通关指南:Object、包装类、String与Random核心考点解析

Java基础类库通关指南:Object、包装类、String与Random核心考点解析 ★ FEATURED ARTICLE
最近带的学生项目里有个小A同学被Educoder平台上一套Java练习搞得有点缩手缩脚跑过来跟我吐槽Object类、包装类、String那三个类、还有Random类上课都听懂了一到平台在线评测就红一片。我扒着题目看了一圈发现问题不是语法没背熟而是这几个知识点正好卡在“看得懂”和“写得出”之间属于典型的“你以为你会了但其实你只是看懂了”。这四关练习说白了是在帮你补齐Java对象体系里相对底层的一块拼图Object类告诉你所有类的共同行为长什么样包装类告诉你基本类型如何混进对象的世界String三兄弟教你处理字符串时怎么做取舍Random类则是你写工具方法、做模拟数据时经常要碰的随机数来源。这篇文章我打算站在刷题者加复盘者的角度把这四关的核心考点、底层原理以及我在实际调试中踩过的坑全部拆开讲一遍适合刚学完Java语法正准备刷题巩固、或者面试前想快速过一遍基础类库的人。1. 这套练习到底想考什么四关之间的逻辑线别看只是一个平台练习关卡的列表这四关的排列顺序其实藏了一条线从“一切皆对象”的根源到基本类型如何与对象互通再到最常用的字符串对象家族最后落到生成随机数据的工具类。四个话题看似独立实际上是在帮你打通“类、对象、常用API”这条基本功链条。1.1 为什么第一关是Object类在Java里任何一个类不写extends编译器也会让它默认继承Object。所以Object是所有类的祖宗类。平台把Object类放在第一关目的很直白先让你意识到你写的任何一个自定义类从诞生那天起就自带一堆可以重写的方法比如toString、equals、hashCode。这三兄弟在你后面学集合框架时几乎是绕不开的。我在实际开发里见过不少同事在实体类上只重写toString不重写equals结果对象丢进Set里出现一堆“重复元素”排查半天才意识到是hashCode没跟上。1.2 第二关到第四关在补充什么能力第二关的包装类解决的是一个对称问题Java把基本类型和引用类型刻意分开但实际编码中你又不得不把int当成Object用比如放进集合、泛型、判空。于是包装类应运而生。第三关的String家族则是日常编码里出现频率最高的对象群字符串拼接、截取、比较这些操作几乎每天都在写。第四关的Random类相对独立但它属于“你不知道底层机制也能跑知道底层机制才能跑得准”的典型。比如生成验证码时你如果用new Random()每次在循环里调用很容易出现重复序列理解了种子机制之后这个问题就能从根上避免。1.3 配合平台评测模式来理解做题思路在线实训平台的评测方式和在公司写业务代码有个明显的差异点平台只认输出格式差一个空格、多一个换行都可能被判错。所以刷这类练习时第一优先级不是把代码写“好看”而是把题目要求的输出格式看清。我的习惯是先在本地把题目给的示例输入跑一遍确认输出与自己预想一致再粘到评测区。这一点在后面第六节我会单独展开。2. 第1关 Object类所有类的根上最容易错的三件事Object类真正需要你动手重写的方法没几个但每一个都自带经典的坑。平台这一关的题目十有八九会让你定义一个自定义类然后实现toString、equals、hashCode中的一个或多个。2.1 重写toString时要看清输出模板很多初学者愣是分不清默认情况和重写情况下的打印结果。直接System.out.println(对象)时如果没有重写toString打印的是“全限定类名散列码”这种形式重写之后才会按你定义的格式输出。平台题目的评判逻辑通常是对比字符串。假设题目定义一个Person类里面有name和age两个字段要求输出“姓名:xxx 年龄:xx”这样的格式那么你重写toString时就必须一字不差地把冒号、空格、顿号都对上。我见过有人明明逻辑正确结果中文冒号写成了英文冒号整题判错。2.2 equals与hashCode的约定比你想的更严格重写equals的时候有一个规范是所有Java程序员都该刻在脑子里的如果两个对象通过equals方法比较是相等的那么它们的hashCode值也必须相等。反过来不成立hashCode相等equals不一定相等。为什么要有这个约束因为HashMap、HashSet这类散列集合的第一步是看hashCode决定桶的位置第二步才是用equals确认桶里有没有相同元素。如果你重写了equals却不改hashCode两个语义上相等的对象会被分到不同桶里集合里就会出现“内容重复但检查不出来”的脏数据。平台练习一般会用一个测试用例来验证这个约定比如创建两个内容相同的对象把它们都放进一个HashSet检查集合大小是不是1。顺带说一句equals重写时判断类型用instanceof还是getClass()也有讲究。在实体类里我习惯用getClass()比较严格类型避免子类继承后被误判为相等如果业务上允许子类对象与父类对象相等再考虑instanceof配合字段比较。2.3 实训题目示例一个典型自定义类的重写public class Person { private String name; private int age; public Person(String name, int age) { this.name name; this.age age; } Override public String toString() { return 姓名: name 年龄: age; } Override public boolean equals(Object obj) { if (this obj) { return true; } if (obj null || getClass() ! obj.getClass()) { return false; } Person person (Person) obj; return age person.age name.equals(person.name); } Override public int hashCode() { int result name.hashCode(); result 31 * result age; return result; } }这段代码里最重要的一行是return age person.age name.equals(person.name)它先用基本类型比较age再调用String的equals去比较name。有人会手滑写成name person.name这一写就直接出bug了——因为在引用类型上比较的是地址不是内容。平台评测时只要两个Person对象name相同但来自不同引用你的equals就会返回false题目直接扣分。3. 第2关 包装类自动装箱与“”陷阱包装类是八个小兄弟的“对象化变身”目的很简单让基本类型也能在需要Object的地方出现。但包装类带来的最大迷惑点是自动装箱和自动拆箱似乎让基本类型和包装类型“长得一样”了结果一个判断就把多少人打得措手不及。3.1 自动装箱与拆箱的底层手法自动装箱其实是你写了Integer i 100时编译器帮你悄悄变成了Integer i Integer.valueOf(100)。自动拆箱则是你写了int j i 1时编译器帮你调用i.intValue()得到int值再进行运算。所以装箱拆箱不是魔法而是语法糖。平台练习大概率会让你回答“下列代码输出什么”Integer a 127; Integer b 127; System.out.println(a b); Integer c 128; Integer d 128; System.out.println(c d);输出结果会非常反直觉第一个打印true第二个打印false。原因是Integer类内部维护了一个缓存池范围是-128到127。在这个范围内valueOf不会新建对象而是直接返回缓存里的同一个对象超出范围才会new一个新的Integer。所以a和b指向同一个缓存对象而c和d是两个不同对象自然就false了。3.2 缓存机制不是所有包装类都有需要注意Byte、Short、Integer、Long这四种整数型包装类都有缓存机制范围也是-128到127Character缓存的是0到127Boolean直接缓存了true和false两个实例。但是Float和Double没有缓存因为它们能表示的数值密度太大缓存没有意义。所以Float f1 1.0f; Float f2 1.0f; f1 f2的结果是false这一点在练习里也是个隐蔽考点。3.3 比较包装类值什么时候用什么时候用equals我的建议是无脑用equals除了一个特例当你确保比较的值的范围在缓存区间内、且你非常清楚底层是同一批对象的时候可以用。但实际写业务代码时你真的没必要去赌缓存范围直接用equals或者包装类的compareTo方法更稳。另外还有一个高频考点三元运算符会造成包装类的自动拆箱和类型提升。Integer num null; boolean flag false; int result flag ? 0 : num;这段代码运行时你会拿到NullPointerException。因为三元运算符要求第二和第三操作数统一类型flag为false时会走num这个分支然后需要把Integer拆箱成int于是对null调用了intValue()直接NPE。很多人在练习里栽跟头就是只看到了“0”和“Integer”没意识到拆箱步骤的存在。没过脑子就想当然地认为result会等于0但实际上代码在取num的值时就崩溃了。这一关的实训题里如果涉及null的安全判断务必先把装箱对象判空再做数值运算。4. 第3关 三个字符串类不可变、可变与线程安全的取舍String、StringBuilder、StringBuffer是Java里被问烂的一个“三连”。很多人背住了结论String不可变、StringBuilder线程不安全但快、StringBuffer线程安全但慢。但实训题不会只问结论它会拿具体场景让你选型或者让你判断某段代码的结果。4.1 三个类的本质区别特性StringStringBuilderStringBuffer可否修改不可变任何修改都生成新对象可变直接修改内部字符数组可变直接修改内部字符数组线程安全不可变天然线程安全非线程安全方法级synchronized线程安全性能大量拼接时最差拼接性能最好比StringBuilder略慢典型场景字符串常量、少量拼接、作为HashMap的key单线程环境下的字符串拼接多线程共享同一个可变字符串为什么String不可变却适合做HashMap的key因为不可变意味着对象哈希值稳定。如果key可变放进HashMap后再被改掉hashCode就变了而桶的位置是基于原来的hashCode算的后面就再也找不到这个key了。4.2 String的拼接陷阱与编译器优化很多人以为字符串用“”拼接是最简单的实际上编译器对“”的处理分两种情况如果拼接的双方都是字符串常量编译器会直接合并成一个常量比如a b直接变成ab如果拼接里出现了变量编译器就会自动转成类似new StringBuilder().append(...).append(...).toString()的形式。所以短代码里“”并不会慢得离谱但如果你在循环里反复拼接问题就大了。看下面这段代码String result ; for (int i 0; i 10000; i) { result result i; }每次循环都会创建一个新的StringBuilder然后append再toString生成的临时对象数量非常可观。平台练习里如果让你比较两种拼接方式的性能差异大概率就是考这个点。正确的做法是StringBuilder sb new StringBuilder(); for (int i 0; i 10000; i) { sb.append(i); } String result sb.toString();4.3 StringBuilder的扩容机制与题点StringBuilder构造时默认容量是16个字符如果你append的内容超过了当前容量它会自动扩容。扩容的大致策略是取“原容量乘以2再加2”和“实际所需最小容量”的较大值然后调用Arrays.copyOf把旧数据搬进新数组。这个机制你在写业务代码时一般不用深究但实训题如果考到“底层是字面量数组/StringBuilder扩容方向”这种判断题你需要知道它并不是每次append都新建底层数组而是有预分配和增量扩容的。4.4 字符串常见API的考点这一关练习往往会混合考察String的常用方法比如length、charAt、substring、indexOf、equals、replace、split等。有两个细节特别容易翻车第一substring的参数是左闭右开即substring(0, 3)取的是索引0、1、2三个字符不包含3。第二String的equals比较的是内容而比较的是引用。如果你用去比较两个内容相同的String可能true也可能false取决于它们是否都指向常量池中的同一个对象。new String(abc)产生的是一个新对象和字面量abc用比较结果是false。这个点在String类练习里属于必考级别几乎每个版本的题目都会出现。5. 第4关 Random类伪随机数的原理与边界控制Random类表面上看就是一个“能生成随机数的工具”但要真正把这关练习做明白你得理解一个概念它不是真正的随机而是伪随机。所谓伪随机就是它内部维护着一个“种子数”每次生成新数都基于当前种子进行一系列数学运算算出一个结果再把种子更新。只要种子确定生成的序列就完全可复现。5.1 种子机制决定一切new Random()不传参数的时候种子用的是当前系统时间的纳秒值。因为每次运行时间不一样所以序列看起来是随机的。但如果你显式指定种子比如new Random(42L)那么每次运行生成的序列都一样。这在实际开发里有个好处复用性。比如做测试数据你想让每次跑测试都生成相同的数据集就指定固定种子。平台题目很可能让你判断“两个相同种子的Random对象各自nextInt()结果是否相同”答案是相同。5.2 常用方法与区间控制方法返回值范围说明nextInt()-2^31 到 2^31-1完整int范围nextInt(n)0 到 n-1左闭右开n必须为正nextLong()完整long范围无参nextDouble()0.0 到 1.0不含1.0左闭右开nextBoolean()true或false按概率各半你如果想生成一个[min, max)区间的随机整数有一个常用公式int randomNum random.nextInt(max - min) min;比如生成5到10之间的随机数包含5不包含10就是random.nextInt(5) 5。这个公式在实训题里大概率会出现因为平台经常用“猜数字”“随机点名”之类的场景来包装这个知识点。注意nextInt(n)的n必须是正数如果你传0或负数会抛出IllegalArgumentException。所以写完区间公式后一定要确认max - min大于0。5.3 多线程环境下为什么推荐ThreadLocalRandomRandom类是线程安全的因为它内部用CAS更新种子。但正因为线程安全在高并发下会有比较明显的竞争开销。如果每个线程都共享同一个Random实例性能并不理想。Java并发包里提供了ThreadLocalRandom属于多线程进阶话题它让每个线程维护独立的Random实例从而避免竞争。虽然平台这一关通常不考线程安全但你在做扩展练习题或者面试聊到Random时千万别张嘴就说“Random线程不安全”这个说法是错的。另外还要注意一个常见的错误写法在循环里反复new Random()。由于无参构造取的是当前时间如果循环速度极快两个Random实例的种子可能一样于是生成的随机数序列一模一样。我在实际项目里见过有人写循环生成验证码结果连续两次生成了一模一样的数字后来排查发现就是循环里new Random()导致的。正确做法应该是复用同一个Random对象或者直接用ThreadLocalRandom.current().nextInt(...)。6. 刷题实录易错点、报错信息与排查思路这一节是纯实战向的内容我把自己当年刷这类练习时最有代表性的几个翻车现场整理成速查表也把排查思路讲清楚。哪怕你不刷这套题换个平台换套题这些坑大概率也长一样。6.1 高频错误速查表错误现象常见原因排查建议编译失败Cannot override toString方法签名写成了toString()带了参数或者返回类型不是String检查方法名、参数列表、返回值保证和Object签名一致equals比较结果与预期不符直接用了引用类型或者没有判空全部改用equals并在equals开头判this obj和obj null输出格式不符被判错冒号、空格、换行、全半角字符不一致先把示例输出原样复制到文本编辑器里逐字符对再改代码包装类比较结果反直觉Integer值超过127或使用了Float/Double用equals比较不要用拼接性能有明显卡顿循环里用String的拼接改成StringBuilder append随机数序列重复循环里重复new Random()把Random对象提到循环外6.2 我在实训过程里的三条避坑心得第一println到底打什么别“猜”去“读”。题目要求输出“姓名:xxx 年龄:xx”那就严格对照这个模板。最后一段用println还是print也要看题目是要求换行还是纯字符串对比平台评测一般对结尾换行不会太苛刻但有时会。保险起见把文档里的输出样例当作正则表达式来精读。第二代码里只要涉及两个对象相等判断先停下来问自己这里是引用比较还是内容比较这个问题值得每次写代码都过一遍因为99%的编程新手在初期都会在和equals上犯晕。特别是字符串和包装类这两个坑在项目里至今最常出现。第三本地调试时如果你看不到平台具体的测试用例就自己写几个边界值的测试比如null、空串、最小值、最大值、缓存边界127/128、随机数的边界值等。实训平台的隐藏用例几乎全部由边界值组成把这些场景测一遍基本能覆盖评测定的大部分坑。最后再分享一个小技巧这套练习我建议配合着IDE的调试功能来过遇到结果不一致就在equals、hashCode、StringBuffer的append这些方法调用处打断点一步步看对象的引用地址和字段值。比对着控制台干想高效得多。刷完这四关再去翻集合框架的源码你会发现HashMap、ArrayList的很多设计都在为这些基础类库做铺垫那时候回头看这套题的价值就不是“过一遍”这么简单了。
阅读完成 · 觉得有帮助?
咨询建站