写这份笔记之前我先说个背景。我的JavaSE学习笔记已经更到第四篇前几篇分别梳理过环境搭建和基础语法、面向对象的核心思想、继承多态和接口这些抽象概念。到这期聊常用类性质突然就变了——不再是学概念而是在学工具本身。昨天还在为多态和抽象类头疼今天要面对的是String、Integer、LocalDate这类每天都在写、时刻都在用的类。之所以把这批类单独拎出来写一篇是因为它们在JavaSE阶段出现的频率实在太高了字符串处理几乎贯穿每个业务方法包装类在集合框架里处处可见时间日期类更是任何系统都绕不开的数据类型。如果你正处于初学JavaSE、准备期末考试或者第一次刷面试题这个阶段这一篇笔记应该能帮你把这些高频类一次理清。1. 这期笔记的边界哪些类配得上常用两个字1.1 为什么把一堆看似无关的类放进同一个笔记我翻了下自己的学习目录Java基础阶段前前后后会接触到接近二十个类但真正值得单独开一篇来写的其实就那么几组String、包装类、时间日期类再加上Math、Arrays、System这几个工具型选手。它们之间从继承关系上看毫无血缘却有一个共同的身份——都定义在java.lang或java.util这种JavaSE默认加载的核心包里不用导包或极少导包就能直接用。把常用类合成一篇笔记还有一层实用考虑。初学者在写第一个标准Java程序时就会碰到System.out.println()这里的System就是常用类跟着敲键盘输入示例时碰到Scanner也是常用类。这些类不需要你理解多么复杂的底层原理只要掌握能用它做什么、方法怎么调、返回什么就能顺畅地写代码所以放在一起集中过一遍效率最高。1.2 学这些类之前先记住三条底层认知第一Java几乎所有常用类都继承自Object。这意味着每个对象都自带toString()、equals()、hashCode()这些方法理解常用类时遇到这个方法哪来的的疑问大概率能在Object里找到答案。第二常用类里相当一部分是final修饰的。String是final包装类是final所以它们不能被继承、不能被重写设计成这样是为了保证行为的一致性。你后面对比String和StringBuilder的时候会发现可变性不一样用途完全就变了。第三别把精力浪费在背方法列表上。我见过不少人学String的时候把JDK文档里几十个方法一个个抄下来背这种做法效率极低。正确的思路是记住核心特性的同时学会查文档理解每个方法背后的设计意图。这一篇笔记也会贯彻这个思路我把高频方法用法写出来但更重要的是告诉你它们解决的是什么问题。2. String类拆开了看不可变性、常量池和那些高频方法2.1 为什么String要设计成不可变我在笔记中不止一次强调这个观点想要理解一个类先去搞清楚它为什么被设计成这个样子。String的设计者把它做成不可变immutable确实是深思熟虑的。不可变意味着一旦创建了String对象它的值就不能再被修改。比如执行String s hello;这个对象就是固定的后续不管做什么操作——拼接、截取、替换——都不会改动原对象而是重新生成一个新对象。这种设计带来的最直接好处是线程安全。多个线程同时去读同一个字符串完全不用加锁因为没人能改它。另一个好处是安全性密码、文件路径、类名这些敏感信息如果传进方法里却被随意修改后果不堪设想不可变就排除了这个隐患。从性能角度看不可变性配合字符串常量池还能实现复用。相同内容的不同变量引用同一个对象省内存省到极致。当然代价也有频繁修改字符串时会不断创建新对象性能损耗明显这就是后面引入StringBuilder的根本原因。所以面试官爱问String为什么是不可变的绝不是为了刁难人而是考察你是否理解这种设计的取舍。2.2 字符串常量池到底new String和直接赋值差在哪这里有个经典对比值得单独记一笔。看这两行代码String s1 hello; String s2 new String(hello);两者在内存层面的行为完全不同。直接赋值时JVM会先去字符串常量池String Constant Pool里查找有没有内容为hello的对象如果池里有直接复用如果没有先在池里创建一个再返回引用。所以执行两次String s hello;两个变量指向的是同一个对象。但是用new String(hello)则是强制在堆内存里创建了一个全新的String对象不管常量池里是不是已经存在相同内容。因此哪怕内容是同一个hellos1 s2这个判断结果永远是false因为比较的是对象地址而不是内容。这个知识点在笔试里反复出现正确率却一直不高。学到这里一定要延伸一个点字符串的拼接优化。在编译器的常量优化机制下he llo这种纯常量拼接会在编译期直接折叠成hello进的是常量池。但he str这种涉及变量的拼接编译期算不出来运行时就得创建一个新对象。JDK 9之后这类拼接底层会走invokedynamic指令但原则不变能提前确定的才进常量池。String str llo; String s3 he llo; // 编译期优化等价于 hello String s4 he str; // 运行时拼接生成新对象2.3 String的常用方法分组记忆比逐个背效率高String类有几十个方法全部背下来不现实。我给自己做了个简单分类把功能相近的归成一组这样记得快、查的时候也有方向。第一组比较和判断。equals(Object)比较内容是否一致equalsIgnoreCase(String)忽略大小写比较startsWith(String)和endsWith(String)判断字符串是否以某内容开头或结尾isEmpty()判断是否为空字符串。注意isEmpty()检查的是长度为0不是判断null想同时处理null和空串得自己写判断条件。第二组查找和提取。charAt(int index)按索引拿字符indexOf(String)从前向后找某个子串第一次出现的位置找不到返回-1lastIndexOf(String)从后向前找。substring(int beginIndex, int endIndex)截取子串时记住左闭右开endIndex不包括想取整串就只传beginIndex。第三组转换和替换。toLowerCase()和toUpperCase()转大小写trim()去掉首尾空格replace(char oldChar, char newChar)替换字符split(String regex)按正则分割成数组。这里有三个坑要单独标一下第一trim()只能去掉普通空格全角空格之外的空白字符需要靠strip()JDK 11起第二replace和replaceAll的区别在于后者走的是正则性能更贵第三split的参数是正则表达式遇到.或|这些有特殊含义的字符时必须用转义。第四组类型转换。valueOf()静态方法把基本类型转成字符串toCharArray()转字符数组getBytes()按指定字符集转字节数组。尤其是把数字转字符串这一点String.valueOf(num)比num 可读性更好也更能避免一些隐性的字符串拼接歧义。常用方法这部分写代码时不需要全记住但重点方法必须形成肌肉记忆。因为后面学习StringBuilder、正则表达式、IO流时这组方法的出镜率实在太高了。3. 包装类为什么Java要搞出八个名字大写的基本类型3.1 基本类型和包装类的关系其实是容器需求逼出来的Java是面向对象语言但int、double、boolean这些基本类型并不是对象它们没有方法可以调用、不能放进集合框架里。ArrayList、HashMap这些容器存的是Object类型的东西你要往里面放一个数字竟然放不进去这就是包装类存在的根本原因。Java为八个基本类型各配置了一个对应的包装类Byte、Short、Integer、Long、Float、Double、Character、Boolean。除了Character是拼写全称其他几个基本就是把首字母大写。每个包装类都提供了基本类型和对象之间互相转换的方法自从JDK 5引入了自动装箱和自动拆箱之后这种转换已经不需要手动调方法了Integer num 10; // 自动装箱基本类型 int - 包装类 Integer int n num; // 自动拆箱包装类 Integer - 基本类型 int写起来很顺但底层依旧是调用了Integer.valueOf(10)和num.intValue()。这个机制给编码带来了巨大的便利也让不少初学者因此忘记了一个潜在问题包装类是对象对象默认值就是null而基本类型默认值是0或false。在写实体类的时候Integer和int的选择不只是写法不同还决定了数据库字段为空时程序怎么表现。3.2 Integer的缓存机制为什么两个100相等两个200不相等包装类里最值得深挖的就是缓存机制因为它的面试出现率和踩坑率都相当高。直接看这段代码的输出结果会颠覆很多人的直觉Integer a 100; Integer b 100; System.out.println(a b); // true Integer c 200; Integer d 200; System.out.println(c d); // false同样的比较结果却不同原因隐藏在valueOf()的实现里。Integer内部维护了一个缓存数组默认缓存了-128到127这个范围内的Integer对象。当你通过自动装箱或者直接调用valueOf()得到一个这个范围内的值时JVM不会新建对象而是直接返回缓存数组里已经存在的那个对象。所以a和b指向同一个引用比较自然为true而200超出了默认缓存范围每次装箱都会在堆里新建对象c和d是两个不同的实例结果就是false。这里要单独说清楚Integer缓存上限127默认是可以通过-XX:AutoBoxCacheMax参数调大的但下限-128不能调。我在自己做实验时试过改上限确实能影响比较结果但这只是性能优化手段千万不要在业务代码里依赖比较包装类的值老老实实用equals()才是正确的写法。其他包装类也有类似的缓存机制Byte、Short、Long缓存范围一致都是-128到127Character缓存的是0到127范围内的字符Float和Double是浮点数没有缓存。这个机制学一次就够了后面用Long做数据库主键ID时你会碰到一模一样的坑。3.3 自动装箱拆箱的几个坑尽早知道能少写一堆Bug第一个坑在比较运算上。Integer和int混合运算时Integer会自动拆箱成int再来比所以Integer a 127; int b 127; a b;结果是true这是正常逻辑。但如果是两个Integer用比较就要小心缓存范围和对象引用的干扰。第二个坑在boolean的条件判断里。包装类Boolean的自动拆箱在if条件中很自然但如果Boolean对象本身是null拆箱就会触发NullPointerExceptionBoolean flag null; if (flag) { // 编译没问题运行时直接NPE System.out.println(true); }第三个坑在前面的赋值场景。写实体类的时候如果数据库允许某个字段为null用基本类型int反而是有风险的——它拿不到null这个语义只能用0来兜底。这种问题看起来不起眼等到排查线上数据异常时可就要费老大劲了。踩过这些坑之后我的体会是包装类是Java为了在基本类型和面向对象之间搭桥而做的妥协理解了它的定位再遇到Integer和int混用场景心里就有底了。4. 时间日期类从Date到LocalDate一套完整的演进史4.1 老牌的Date和Calendar为什么被新手嫌弃又被老手吐槽刚开始学Java基础最先接触的时间日期类是java.util.Date。它最简单、最直白new Date()拿当前时间getTime()拿到从1970年1月1日零点到现在的毫秒数。但它有两个让人崩溃的设计第一是getYear()返回的是当前年份减去1900比如2026年返回的是126完全不直观第二是toString()输出的是Sat Mar 14 09:00:00 CST 2026这种格式看着就直接劝退初学者。然后Java 1.1引入了Calendar类来补救但同样是吐槽重灾区。月份从0开始计数也就是说Calendar.OCTOBER对应的是9月谁写谁错Calendar.getInstance()每次拿到的是可变对象线程共享时容易出问题。更别提什么设置时间、做加减运算时那又臭又长的方法调用链。我自己写聊天室小项目时用Calendar算日期差算错了好几次才搞明白问题出在月份和星期的索引上。所以很多老项目里日期处理都是Date拿毫秒值、SimpleDateFormat格式化这种朴素的组合。可是SimpleDateFormat也不是省油的灯它线程不安全多线程共享同一个实例去parse或format时会抛出各种古怪异常生产环境踩过这个坑的人应该不在少数。4.2 新日期时间API的优势不可变和清晰的逻辑Java 8带着java.time包正式结束了这场混乱核心类就是LocalDate、LocalTime、LocalDateTime。这套API的设计思路非常直白日期和时间彻底分开不同需求用不同的类别一锅炖。LocalDate只关心年月日适合存生日、发布日期这类场景LocalTime只关心时分秒纳秒适合存上下班打卡时间LocalDateTime日期和时间都要适合完整的业务时间戳这三个类都可以通过now()直接获取当前值也可以通过of()方法传入具体值构造。有了它过去那些绕来绕去的日期操作变得异常顺手LocalDate today LocalDate.now(); LocalDate tomorrow today.plusDays(1); LocalDate yesterday today.minusDays(1); LocalDate firstDayOfMonth today.withDayOfMonth(1);plusXxx()和minusXxx()系列方法支持天数、周数、月数、年数的增减withXxx()系列方法用于调整指定字段。因为LocalDate本身不可变每次操作返回的都是新对象原对象不受影响这样写起来不需要担心方法调用会偷偷改掉数据理解成本和排错成本都大幅下降。4.3 DateTimeFormatter接管格式化摆脱SimpleDateFormat阴影新API里格式化这块做得尤其好。DateTimeFormatter是线程安全的可以定义为静态常量复用单凭这一点就比SimpleDateFormat高出一截。用法也很模式化先定义格式再通过parse或format和字符串互转。DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); String text LocalDateTime.now().format(formatter); LocalDateTime parsed LocalDateTime.parse(2026-03-14 09:30:00, formatter);注意格式模式里的yyyy代表年份MM是月份MM要大写小写mm是分钟dd是月份中的某天HH是24小时制12小时制用hh。这些模式和SimpleDateFormat保持一致但体验完全不同清爽、无状态、不易出错。时区这块需要单独提一句。LocalDateTime不带时区信息适合表示与具体时区无关的时间ZonedDateTime才是有时区的时间类型配合ZoneId使用。做国际化系统时存数据库要么统一存UTC时间要么存带时区类型如果都用LocalDateTime会导致多个时区的用户看到各自不同的同一时刻。4.4 一个学习实例计算两个日期之间隔了多少天笔记写到这用一个综合小例子做收尾把上面的方法串起来。假设要算两个日期相差多少天LocalDate start LocalDate.of(2026, 3, 1); LocalDate end LocalDate.of(2026, 3, 14); long daysBetween ChronoUnit.DAYS.between(start, end); System.out.println(daysBetween); // 输出13如果用老牌的Date和Calendar来做光是拿到两个日期的时间戳再除以一天的毫秒数就得绕一大圈还涉及时间分量和时区的干扰。而ChronoUnit直接提供各个时间单位之间的差值计算一行代码拿下。这并不是说老API垃圾而是新API在语义清晰度上确实碾压式的更好新人直接学新API是完全正确的路线。5. Math、Arrays、System藏在java.lang和java.util里的工具箱5.1 Math类纯静态方法没什么好说的但必须知道java.lang.Math大概是常用类里最没设计感的一个——没有对象、没有多态全类由静态方法和两个常量组成。它的定位就是数学工具库做数值计算时直接用类名调方法就行。常用的方法几乎不需要经过大脑Math.abs(-5)绝对值返回5Math.max(3, 7)/Math.min(3, 7)最大值和最小值Math.pow(2, 10)幂运算2的10次方返回1024.0double类型Math.ceil(4.2)向上取整返回5.0Math.floor(4.8)向下取整返回4.0Math.round(4.6)四舍五入返回5返回值类型是longMath.random()生成一个[0.0, 1.0)区间的随机小数这里有个被忽略的冷知识Math.random()的底层用的是Random.nextDouble()而且每次调用都会创建一个新的Random实例性能其实一般。在循环里大量生成随机数时更推荐自己创建Random对象然后反复调用nextInt()这点我实测过高并发测试的循环场景里差距很明显。还有一个值得聊的是Math.round()的返回类型。Math.round(4.5)返回long类型的5因为 float 重载返回 int、double 重载返回 long需要大数值时不会丢精度。如果只要整数部分用(int) Math.floor(x)小心处理负数斜向的问题就行。5.2 Arrays类数组和集合之间的螺栓刀java.util.Arrays是操作数组的工具类。操作数组这件事本身在业务代码里不算高频但一旦需要它它提供的方法就是及时雨尤其是它连接数组和List的桥接能力。Arrays.toString()把一维数组的内容转成人类可读的字符串打印数组时直接System.out.println(Arrays.toString(arr))。Arrays.sort()对数组排序直接改原数组默认升序。支持传比较器实现自定义排序规则比如降序。Arrays.binarySearch()二分查找前提是数组已排序。注意找不到时返回的是负数的插入点减一千万别把返回值当索引直接用。Arrays.copyOf()复制数组并指定新长度扩容缩容都好用。Arrays.equals()/Arrays.deepEquals()比较数组内容是否相等一眼看穿两个数组是否一致。Arrays.asList()数组转List这个用得最多坑也最多。Arrays.asList()的坑值得单独展开。它返回的List是一个定长列表底层仍然指向原数组所以不能调用add()和remove()一调就抛UnsupportedOperationException。想得到一个真正独立可增删的List要这样做ListInteger list new ArrayList(Arrays.asList(1, 2, 3));还有一个细节asList()接收的是泛型参数如果传一个int[]进去它会把整个数组当成单个元素得到的是Listint[]长度是1而不是数组长度。要转换成元素类型的List得先把基本类型数组转成包装类型数组或者用IntStream配合boxed()来处理。这个细节在代码评审时经常出现值得记一下。5.3 System类原来日常几行代码背后都藏着它System类同学应该最不陌生因为第一行Java代码里就有System.out.println。但很多人对这个类的理解就停在输出这句话上了。实际上System类做的是对接操作系统能力这件事常用的就三块第一块是标准输入输出流。System.out是标准输出流System.err是标准错误流System.in是标准输入流。Scanner读取键盘输入时传的就是System.in这个流对象。第二块是获取系统属性。System.getProperty(os.name)拿到操作系统名System.getProperty(user.home)拿到用户主目录路径。在做跨平台路径拼接、判断当前系统环境时很常用。String osName System.getProperty(os.name); System.out.println(osName.contains(Windows) ? Windows环境 : 非Windows环境);第三块是System.arraycopy()。它是在两个数组之间复制一段数据的底层方法效率比for循环手写复制高很多。比如集合扩容、手动实现队列时底层都在调用它。JDK里大量源码都用它搬数据说明它确实是高性能通道值得在简历上的技术词里占个位置。还有一个常见用途是System.currentTimeMillis()获取当前时间的毫秒值做性能计时的时候特别方便long start System.currentTimeMillis(); // 执行一段代码 long end System.currentTimeMillis(); System.out.println(耗时 (end - start) ms);注意System.gc()这里提醒一句就够了别调用垃圾回收不需要你手动去请求调用它往往只会增加系统负担。6. 这几组常用类的横向对比和学习顺序的建议6.1 用一张对比表理清各组类的定位笔记写到后段我习惯把相似的东西拉出来横向对比。这里花点篇幅把String、StringBuilder、StringBuffer放一起讲因为它们三个是初学阶段最混淆的三兄弟。String是不可变类StringBuffer是JDK 1.0时期提供的可变字符串方法加了synchronized保证线程安全StringBuilder是JDK 5之后提供的可变字符串不保证线程安全但单线程环境下性能更好。实际写业务代码局部变量不存在线程竞争直接用StringBuilder就行如果字符串对象真的可能被多线程共享才需要StringBuffer。时间日期那边也可以排个队DateSimpleDateFormat是历史遗留代码可维护性差Calendar更是能不碰就不碰新代码一律用java.time包。这不是情怀问题是跨时区处理、线程安全、格式化能力全面碾压后的理性选择。包装类之间则不用纠结选型基本类型都有对应的唯一包装类按需使用即可重点是把自动装箱和缓存机制搞懂别让比较运算出现诡异的false就行。6.2 学习方法建议工程导向而不是文档导向结合我自己做这套学习笔记的经验学常用类最容易掉进的陷阱就是看文档像看说明书看完就忘。我的建议是边学边动手改写代码看到String.split()就自己写一个按逗号拆解姓名的示例学完LocalDate就立刻去算今年到月底还剩多少天学完Arrays.sort()就写一个学生对象按年龄排序的Demo。代码不用多每个方法对应一个小练习就够关键是让手指记住方法签名而不是让眼睛假装记住示例。在这个基础上我会建议闲下来时直接去读源码。比如Integer.valueOf()的缓存实现并不长String.equals()的代码量也就三四十行这些源码的逻辑比任何博客讲得都透彻。刚开始读不懂没关系先看懂流程再抠细节读到十来个核心类的实现之后再去看别的类就会形成一种哦这套路我见过了的感觉。6.3 常用类里最容易踩但又不被重视的几个坑最后集中火力把前面几章零零散散提到的坑汇总一遍方便以后写代码时回头翻字符串类的坑比较字符串是否相等的坑只要有一个字符串是new出来的实例就是false。一律用equals()。split()遇到空字符串时末尾的空串会被丢弃a,b,,.split(,)得到的数组只到第2位。replace和replaceAll别混用前者是纯字符替换后者内部走正则非正则需求用replace更高效。包装类的坑Integer缓存范围内的引用比较陷阱不要用用equals()。拆箱导致NPE的问题尤其是数据库查询返回null给Boolean、Integer时先判空再参与运算。日期时间的坑Calendar的月份从0开始LocalDate则从1开始迁移老代码时千万别直接套公式。LocalDate.parse()和format()用的模式串大小写含义完全不同HH是24小时hh是12小时写错直接解析失败。数据库driver低版本支持不了LocalDateTime需要额外启用参数或改用时间戳字段。工具类的坑Arrays.asList()返回定长列表增删必崩。System.arraycopy()复制的是引用基本类型数组没问题对象数组要注意浅拷贝的特性。这些年看着不同阶段的同学在同样几个坑里反复栽跟头我越来越深刻地觉得常用类学习最大的价值不是认识一堆API而是理解每个类被设计成现在这个样子背后都有一组明确的取舍和约束。你把String的不可变性和缓存机制想透了后面看StringBuilder的效率设计就顺理成章你把包装类的自动装箱流程理清了再去看泛型和集合的操作就有基础。所以这一篇笔记的落点不在于把所有方法都翻一遍而是帮你把这些类的骨架搭好以后遇到实际问题能第一时间在心里定位到该用哪一类、它的能力边界在哪里这比记住一百个方法签名都管用。