Java 字符串与常用对象
本篇是 2017 年 Java 学习笔记系列的第十二天,主角是开发中使用频率最高的类——String。先建立”不可变性 + 字符串常量池”的入门认知,再把几十个常用方法按判断、获取、转换、替换四大类整理清楚,顺带聊聊字符串拼接的性能意识和 Scanner 键盘录入的换行陷阱。
本篇目标
- 理解 String 不可变的含义与字符串常量池的作用
- 按功能分类掌握 String 的常用方法
- 知道为什么循环里不该用
+拼字符串 - 会用 Scanner 接收键盘输入,并避开 nextInt 与 nextLine 混用的坑
String 的不可变性:从常量池说起
String s = "abc"; 这行代码里,字面量 "abc" 存放在字符串常量池中,s 只是一个指向它的引用。再写 String s2 = "abc";,JVM 发现池中已有相同内容,直接复用同一个对象——所以 s == s2 是 true。常量池本质上是一块”字符串缓存区”,目的是让内容相同的字符串在内存里只存一份,避免浪费。
除了字面量赋值,String 也提供了构造方法:new String() 创建空串、new String(byte[]) 把字节数组按平台默认编码转成字符串、new String(char[]) 把字符数组合成字符串、new String("abc") 基于已有字符串再建一个新对象。日常开发几乎只用字面量,构造方法主要在处理字节、字符数组的场景登场。
所谓不可变,是指字符串对象一旦创建,其内容就不能再改。所有看似”修改”的方法(toUpperCase、replace、substring……)都是返回一个全新的 String 对象,原对象纹丝不动。这样设计至少带来三个好处:常量池可以安全地被多处共享,而不用担心被某个引用改掉;hashCode 只需计算一次并可以缓存,这也是 String 能成为 HashMap 最常用的 key 类型的原因之一;对象天然线程安全,多线程共享无需加锁。至于 intern()、JDK 9 之后底层改用 byte 数组压缩存储等进阶话题,先知道有这回事即可。
新手最常犯的错误是忘记接收返回值:
String s = "abc"; |
由此引出几个经典面试辨析:
String s1 = "abc"; |
分水岭就一句话:常量拼接发生在编译期,变量拼接发生在运行期;== 比地址,equals 比内容。
常用方法分类速查
String 的方法很多,按用途记四类就不会乱。
判断类
equals(区分大小写)、equalsIgnoreCase(忽略大小写)、contains、startsWith、endsWith、isEmpty。注意 equals 是实例方法,调用者为 null 会直接空指针,所以推荐 "admin".equals(input) 这种常量在前、变量在后的写法。
获取类
length() 返回字符个数(注意数组是 length 属性,String 是 length() 方法,别混);charAt(index) 取指定位置字符;indexOf / lastIndexOf 查找字符或子串位置,找不到返回 -1;substring(start, end) 截取子串,包左不包右。这里有两个高频错误:一是把 indexOf 的返回值直接当布尔用,正确姿势是判断 != -1;二是 charAt 和 substring 的索引越界会抛 StringIndexOutOfBoundsException,访问前先确认 length。
转换类
toCharArray() 转字符数组、getBytes() 转字节数组、静态方法 String.valueOf(...) 把任意类型转字符串、toUpperCase() / toLowerCase() 大小写转换、concat 拼接。这些方法都返回新对象,因此可以链式调用。比如把首字母转大写、其余转小写:
String s = "hELLO"; |
替换与去除类
replace(old, new) 替换所有匹配内容;replaceAll 支持正则表达式,后面正则篇再展开;trim() 只去除首尾的空白,中间的空格不动;compareTo 按字典序比较大小,返回值是两者的差值。
拼接的性能意识
String 不可变意味着每次 + 拼接都要创建新对象并把旧内容复制过去。写一次无所谓,放进循环里就是灾难:
String s = ""; |
一万次循环背后是上亿次字符拷贝,时间和内存都在被白白消耗——这就是典型的平方级开销。结论:循环中或大量拼接,一律使用 StringBuilder——它内部维护可变的字符数组,追加只是往数组里写,均摊下来每次拼接接近常数时间。编译器虽然能把单行 + 优化成 StringBuilder,但跨循环的拼接它救不了你:每一轮 s += i 都会被翻译成一次”新建 StringBuilder、append、toString”,对象照样满天飞。StringBuilder 是下一篇的主角,这里先立个意识。
Scanner:键盘录入与换行陷阱
Scanner 是对输入流做解析的工具类,new Scanner(System.in) 接收键盘录入。常用两类方法:hasNextXxx() 判断是否还有下一个某种类型的输入项,nextXxx() 真正取走它——nextInt() 读整数,nextLine() 读一整行。
经典陷阱:先 nextInt() 再 nextLine(),第二个方法会”秒过”不等你输入。原因是 nextInt 只读到数字为止,你敲的回车符还留在缓冲区,紧随其后的 nextLine 立刻把这个残留的换行读走,拿到一个空串。
Scanner sc = new Scanner(System.in); |
最稳的解决方案是全部用 nextLine 接收,再按需转换类型:
Scanner sc = new Scanner(System.in); |
经典练习
模拟登录,给三次机会
Scanner sc = new Scanner(System.in); |
注意 "admin".equals(user) 的写法:常量在前,即使 user 是 null 也不会抛异常,这是值得养成的习惯。
统计字符串中各类字符的个数
String s = "ABCDEabcd123456!@#$%^"; |
char 可以直接和字符字面量比大小,因为 char 本质上是数字(Unicode 码点)——这是遍历字符串最常用的套路。
统计大串中小串出现的次数
思路:用 indexOf(小串, 起始位置) 找一次、计数加一,然后把起始位置推到”本次命中位置 + 小串长度”,继续找,直到返回 -1:
String big = "woaijavawozhenaijavawodeaijava"; |
这个练习把 indexOf 的重载形式、循环条件和”指针推进”三件事串在了一起,值得动手写一遍。
小结
- String 不可变:字面量驻留常量池,任何”修改”都产生新对象;
==比地址,equals比内容。 - 常量拼接编译期折叠进常量池,变量拼接运行期在堆里生成新对象——这是面试题的分水岭。
- 常用方法按判断、获取、转换、替换四类记忆;链式调用利用的正是”返回新对象”。
- 循环拼接用 StringBuilder,别用
+;Scanner 混用 nextInt/nextLine 会吞换行,统一用 nextLine 再转换最稳。

