Java常用API实战指南:从String、集合到Stream的避坑手册
做Java这些年面试过不少人也带过不少新人。有个现象特别有意思很多人简历上写着“熟练掌握Java基础”但一问到String、Object、集合这些最常用的API往往答得支支吾吾。不是说他们不会写代码——CRUD谁都会写——而是真正理解这些API怎么设计、为什么这么设计的人太少。这篇东西不打算讲什么高深原理就是把我日常开发里真正高频使用的Java常用API按“是什么、怎么用、坑在哪”三个维度梳理一遍。既适合刚入门的朋友系统扫盲也适合工作了两三年、想查漏补缺的同学。里面不少细节是面试常客也是线上事故的高发区建议收藏反复看。1. 先搞清楚什么是Java常用API别再用死记硬背的方式学1.1 为什么同样的功能老手两三行就写完你要写二十行API的全称是Application Programming Interface翻译过来叫“应用程序编程接口”。说白了就是别人帮你写好的现成代码你只需要知道怎么调用不需要关心内部实现。JDK本身就带了几千个类这些类里提供的方法就是Java官方API。新手和老手的差距很多时候就体现在对API的熟悉程度上。举个最简单的例子把列表里的字符串按长度排序。新手可能先想着写个冒泡排序再来个双层循环比较字符串长度——不是不行但没必要。老手拿Stream加一行搞定。再比如日期计算新手可能自己写工具类处理月份进位、闰年判断老手直接用LocalDate.plusDays()底层早就处理好了。这就是熟悉常用API的价值你不是在写代码你是在指挥别人写好的高质量代码。JDK团队把边界条件、性能优化、并发安全都处理好了你只需要站在他们的肩膀上。1.2 学习API的正确打开方式别背要“用”出感觉很多初学者学API的方式是背方法名今天背了String的substring、indexOf明天又忘了。我自己的经验是学一个API要掌握三个维度第一这个API解决什么问题适用什么场景。比如StringBuilder它的出现就是为了解决频繁字符串拼接的性能问题。第二核心方法有哪些参数是什么意思。第三也是最重要的有哪些坑。比如SimpleDateFormat线程不安全、Arrays.asList返回的list不能增删这些都是血泪教训。另外强烈建议大家看官方文档英文看不懂就用翻译工具但一定要养成看文档的习惯。IDEA里按住Ctrl点方法名能看到JDK源码和注释这也是很好的学习途径。比在网上搜二手教程靠谱得多。2. Object与String几乎每个类都绕不开的底层API2.1 Object类把equals和hashCode搞明白面试就赢了一半Object是所有类的父类意味着你创建的每一个对象天生就继承了Object的九个方法。其中最常被问到也最容易出错的就是equals和hashCode这俩在面试里属于“八股文”级别的经典问题但真到了写代码的时候很多人照样踩坑。先说equals。Object默认的equals比较的是内存地址也就是两个引用是否指向同一个对象。但业务上我们经常需要“两个对象内容相等”这种判断比如两个User对象的id相等就认为是同一个人。这时候就需要重写equals。重写equals有个铁律必须同时重写hashCode否则在HashMap、HashSet这类基于哈希的集合里会出大问题。原因是哈希集合的查找流程是先算hashCode定位到某个桶再通过equals比较桶里的元素。如果两个对象equals相等但hashCode不同它们会被放进不同的桶导致集合里出现“明明相等却存了两份”的荒谬现象。反过来hashCode相同但equals不同只会导致同一个桶里链表变长影响性能但不会出逻辑错误。这就是规范里“equals相等的两个对象hashCode必须相等”的真正原因。实际操作中我建议直接用Objects工具类public class User { private String id; private String name; Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; User user (User) o; return Objects.equals(id, user.id) Objects.equals(name, user.name); } Override public int hashCode() { return Objects.hash(id, name); } }这样写的好处是自动处理了null值判断不会因为某个字段为null就抛空指针。2.2 String类不可变性的好处比你想象的更大String绝对是Java里使用频率最高的类没有之一。它的核心特征是“不可变”也就是说一个String对象创建之后它的值就永远不变了。很多人不理解为什么要设计成不可变其实好处非常多线程安全、可以安全地被多个集合共享、hashCode可以缓存起来不用重复计算——HashMap之所以敢用String做key依赖的就是String的不可变性。String类提供的常用方法可以分成几类字符串操作类有substring、concat、replace、trim查找类有indexOf、contains、startsWith、endsWith转换类有toCharArray、toLowerCase、toUpperCase还有判断类的isEmpty、isBlank、equalsIgnoreCase等。这些方法都返回值或者新的String不会修改原字符串。这里说几个实战细节。第一判断字符串是否为空Java 11之后推荐用isBlank()而不是isEmpty()因为isBlank还会把全空格字符串判断为空。第二split()方法接收的是一个正则表达式如果用英文句号“.”切割字符串一定要写成“\.”不然你会发现怎么切都切不开。第三尽量不要在循环里用“”拼接字符串因为每次拼接都会创建一个新的String对象循环次数多了性能会很难看。2.3 StringBuilder和StringBuffer拼字符串的正确姿势说到字符串拼接就不得不提StringBuilder。它的内部维护了一个可变数组append()方法会直接往数组里追加内容避免了频繁创建新对象的开销。StringBuffer和StringBuilder的功能几乎一样只是StringBuffer的方法加了synchronized线程安全但性能稍差。在单线程环境下StringBuilder是绝对的首选。注意一点如果你知道最终字符串的大致长度可以提前指定StringBuilder的初始容量这样能减少扩容次数。我见过有人写代码不关心这个结果几百万次循环拼接时反复扩容性能差距能到好几倍。// 推荐写法循环外创建指定容量 StringBuilder sb new StringBuilder(1024); for (int i 0; i 1000; i) { sb.append(item:).append(i).append(,); } String result sb.toString();3. 集合框架开发中最高频的一坨API3.1 集合家族的脉络先有整体认知再记细节Java集合框架可以用一句话概括两大派系一个Collection存单个元素一个Map存键值对。Collection下面又分List有序可重复、Set无序不可重复、Queue队列。这是整个集合框架的基础脉络其他全是细节。List里最常用的是ArrayList和LinkedList。ArrayList底层是数组随机访问快通过下标直接定位但插入和删除需要移动元素LinkedList底层是双向链表插入和删除只需要修改指针但随机访问要遍历。实际开发中ArrayList用得远远比LinkedList多因为大多数场景都是“先收集数据再遍历”随机访问的需求远高于中间插入删除。Set这边HashSet底层是HashMap存的时候用元素做keyvalue随便放个占位对象。TreeSet底层是红黑树元素天然有序但要求元素实现Comparable接口或者传入比较器。使用HashSet时放入的元素一定记得重写equals和hashCode否则“去重”的效果就变成了“去地址”很容易埋雷。3.2 面试高频说一下HashMap的底层实现HashMap是集合框架里的重点中的重点面试基本必问。它的大致结构是“数组加链表加红黑树”通过key的hashCode经过扰动函数处理后定位到数组的某个桶如果多个key哈希冲突就挂在链表上当链表长度超过8且数组长度超过64时链表会转成红黑树把查询时间从O(n)降到O(log n)。这里我给一个实用建议如果能预估HashMap的容量构造时一定要指定。因为HashMap默认容量是16当元素数量超过容量乘以负载因子默认0.75时会触发扩容扩容是一次代价极高的操作——需要重新计算所有元素的哈希并搬移位置。比如你确定要存1000个元素直接new HashMap(2048)或new HashMap(1000 / 0.75 1)能省掉不必要的扩容开销。LinkedHashMap也值得一提它额外维护了插入顺序或者访问顺序。如果你需要一个“保持放入顺序”的Map用它如果你想实现一个简单的LRU缓存把accessOrder设为true再加一层removeEldestEntry即可这在我之前的项目中用过很多次非常实用。3.3 遍历和操作最容易踩的几个坑集合遍历看起来简单但坑特别多。最经典的是遍历过程中删除元素会抛出ConcurrentModificationException。原因是ArrayList的迭代器内部维护了一个modCount修改次数校验你一边遍历一边调list.remove()modCount变了迭代器发现跟自己的预期不一致直接崩溃。正确删除方式是使用迭代器自己的remove方法或者Java 8之后的removeIfListString list new ArrayList(Arrays.asList(a, b, c, b)); // 正确使用removeIf list.removeIf(s - b.equals(s));还有一个高频坑Arrays.asList()返回的ArrayList不是java.util.ArrayList而是Arrays内部的一个私有类底层还是固定长度的数组所以不能调用add和remove否则抛UnsupportedOperationException。如果你需要一个真正可变长度的列表应该写成new ArrayList(Arrays.asList(...))。集合转数组也有讲究。toArray()无参方法返回Object[]强转有风险正确做法是传一个指定类型的空数组list.toArray(new String[0])。Java 11之后甚至可以写成list.toArray(String[]::new)看起来更优雅。4. Stream与Lambda现代Java写起来最爽的API组合4.1 Lambda表达式把代码当数据传递Lambda表达式是Java 8引入的本质上是一种匿名函数的简写。它的作用是让你把一段行为作为参数传递给方法而不是只能传递值。比如Collections.sort需要传入一个Comparator有Lambda之前你得new一个匿名内部类有了Lambda之后一行搞定ListString names Arrays.asList(tom, jerry, alice); names.sort((a, b) - a.compareTo(b));理解Lambda的关键在于函数式接口——只有一个抽象方法的接口。Runnable、Comparator、Callable都是典型的函数式接口。平时写代码时看到接口只有一个方法就可以考虑用Lambda简化。方法引用是Lambda的进一步简写比如list.forEach(System.out::println)如果Lambda只是调用某个现成方法就可以这么写。4.2 Stream流水线filter、map、collect三板斧Stream是建立在Lambda基础上的数据处理API它让我们可以用声明式的方式操作集合而不是写一堆for循环。Stream的核心思想是流水线先创建流经过零个或多个中间操作最后用一个终止操作产出结果。中间操作常用的有filter过滤、map转换、distinct去重、sorted排序、limit截取。终止操作常用的有collect收集为集合、forEach遍历、count计数、reduce聚合。举个例子从人员列表里筛出年龄大于18的姓名并按照姓名排序ListPerson people ...; ListString adultNames people.stream() .filter(p - p.getAge() 18) .map(Person::getName) .distinct() .sorted() .collect(Collectors.toList());这段代码可读性极强每一行都是在描述“我要干什么”而不是“怎么干”。Stream还有一个特性是惰性求值中间操作不会立即执行只有遇到终止操作才真正开始处理。这意味着你可以先组装好一整条流水线再决定要不要执行中间操作不会产生无谓的遍历开销。4.3 分组、分区与汇总Collectors的进阶玩法Collectors工具类提供了一系列强大的收集器。最简单的toList、toSet、toMap之外groupingBy可以做分组partitioningBy可以做分区按条件分成true/false两组还有summarizingInt可以一次性统计数量、总和、平均值、最大值、最小值。// 按部门分组 MapString, ListEmployee byDept employees.stream() .collect(Collectors.groupingBy(Employee::getDept)); // 按是否在职分区 MapBoolean, ListEmployee partitioned employees.stream() .collect(Collectors.partitioningBy(Employee::isActive)); // 统计工资 IntSummaryStatistics stats employees.stream() .collect(Collectors.summarizingInt(Employee::getSalary));我用groupingBy有个小技巧如果分组后你不需要保存完整的对象列表而是只需要某些字段可以提前map一下比如Collectors.groupingBy(Employee::getDept, Collectors.mapping(Employee::getName, Collectors.toList()))这样分完组直接就是名字列表省一次遍历。5. 时间日期APIjava.time才是正统别再碰Date和SimpleDateFormat5.1 SimpleDateFormat为什么不安全用什么替代老项目里很多人还在用SimpleDateFormat格式化日期但你知道吗它是线程不安全的。SimpleDateFormat内部维护了一个Calendar对象format和parse操作会修改它的状态多线程并发操作同一个实例时计算结果会错乱甚至直接抛NumberFormatException。很多人因此在线上踩过坑。正确姿势是使用java.time包下的DateTimeFormatter它是线程安全的可以放心地定义为静态常量复用private static final DateTimeFormatter FORMATTER DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); // 使用 String formatted LocalDateTime.now().format(FORMATTER); LocalDateTime parsed LocalDateTime.parse(2024-01-15 10:30:00, FORMATTER);5.2 LocalDate、LocalDateTime的常用操作java.time包是Java 8引入的全新时间日期API设计得非常友好。它的核心类有LocalDate日期、LocalTime时间、LocalDateTime日期时间、Instant时间戳、Duration时长、Period日期间隔。这些类都是不可变的所有操作都返回新对象天然线程安全。日常开发中经常用的操作包括获取当前时间LocalDateTime.now()字符串解析LocalDate.parse(2024-01-15)日期加减localDate.plusDays(7)、localDate.minusMonths(1)比较两个日期a.compareTo(b)或a.isAfter(b)获取两个日期相差的天数ChronoUnit.DAYS.between(start, end)。特别提醒一个坑LocalDateTime本身不带时区信息如果你做的是跨时区的国际化应用直接用LocalDateTime获取“当前时间”得到的是服务器时区的时间可能跟用户预期不一致。这时候应该用ZonedDateTime或者先明确时区再转换。还有后端传给前端的时间建议统一用字符串或时间戳避免时区歧义。6. 高频问题与实战避坑把八股文变成肌肉记忆6.1 面试常考的API细节快问快答把近年面试中被问烂的几个API细节整理成一个速查表建议逐条对照自己能不能答上来问题参考答案踩坑点Integer的缓存范围是多少-128到127之间会自动装箱缓存超过范围用比较会false要用equalsArrays.asList能增删元素吗不能底层是固定长度数组调用add/remove抛UnsupportedOperationExceptionHashSet怎么实现去重底层是HashMap靠hashCode和equals判断元素必须同时重写这两个方法HashMap初始容量设多少计算预估容量/0.751频繁扩容严重影响性能SimpleDateFormat线程安全吗不安全多线程需加锁或用DateTimeFormatterString和char[]怎么转toCharArray() / new String(char[])密码等敏感数据用char[]更安全Math.round(-1.5)结果是什么-1因为四舍五入取floor(x0.5)负数取整别想当然6.2 实战中那些让我头皮发麻的报错最后分享几个真实开发中遇到的报错和排查思路希望能帮大家少走弯路。第一个是OutOfMemoryError。Java程序运行时内存不够会直接抛这个错通常分为堆内存不足和直接内存不足。如果是堆不足先看一下是不是哪个集合类无限往里面加数据或者大对象没有及时置空。用jmap -heap看堆使用情况再用jmap -dump导出堆快照用MAT分析是哪块内存撑爆的。如果是循环里不断创建对象可以考虑复用对象或者调整JVM启动参数比如-Xmx设大一些。第二个是字符串拼接导致的性能问题这个不像报错那么显眼但影响可能很大。我接手过一个项目某个接口响应特别慢查了半天发现是循环里用拼接SQL语句几万条数据拼下来产生了海量中间对象GC频繁触发。改成StringBuilder之后接口耗时直接降了一个量级。这种问题不报错但比报错更可怕。第三个是API版本带来的坑。不同JDK版本的API有差异比如Java 8的String没有isBlank方法Java 11才有Java 8的Optional没有ifPresentOrElse方法Java 9才有。如果项目中混用了多个JDK版本编译和运行很容易出现NoSuchMethodError。解决方法是统一项目JDK版本同时在pom.xml中设置maven.compiler.source和target与运行环境保持一致。6.3 我的一点个人习惯讲了这么多最后说一个我自己的小习惯每学一个新API我都会在代码里写一个最小demo跑一遍哪怕只有十行也亲自看它的输入输出。花不了几分钟但比看十遍文档都管用。尤其是那些有坑的方法比如split的正则、HashMap的扩容时机、Stream的惰性求值自己亲手踩一遍坑比背一百条面试题都记得牢。Java常用API这个东西说到底是熟能生巧用得多了它的设计思路自然就理解了面试题也就能答到点子上了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →