在C语言中若要表示字符串只能使用字符数组或者字符指针,Java语言则专门提供了 String 类,在面向对象编程中具有重要地位。在开发和校招笔试中,字符串也是常客。
目录
一、字符串的构造
二、常用方法
2.1 字符串的拼接
2.2 字符串之间的比较
2.3 计算长度
2.4 查找
2.5 转化
2.6 替换
2.7 拆分
2.8 截取
2.9 删除左右两边的空白字符
2.10 不可变性
2.11 修改
三、StringBuilder 和 StringBuffer
append 方法的比较
四、String 和 StringBuilder 之间的转换原则
五、面试题
六、练习题
6.1 找出第一个只出现一次的字符
6.2 最后一个单词的长度
6.3 检测字符串是否是回文
6.4 手搓 toLowerCase 方法
6.5 字符串中的单词数
一、字符串的构造
String 类提供的构造方法非常多,常用的有以下3种。三种方法的区别只是语法上的精简程度不一
String str1 = "hello";String str2 = new String("world"); // 相当于创建数组char[] chars = {'a','b','c'};
String str3 = new String(chars);
String 是引用类型,内部存储的不上字符串本身,通过调试查看 String 类种的成员变量如下(jdk1.8环境下):
字符串常量值是哈希表,涉及数据结构的知识。本文不涉及数据结构的知识。
(jdk1.8 中 String 类只有2个成员变量,而在 jdk9 之后有4个成员变量,加上 coder = 0 和 hashisZero = false。在 jdk1.8 中,字符串实际存放在 char 类型的数组中,而 value 存放的是数组的地址。)
二、常用方法
2.1 字符串的拼接
使用 “+” 可以对字符串进行拼接,注意字符串与整型进行“相加”的情况。
String str1 = "hello";
String str2 = "world";
System.out.println(str1 + str2); // 输出 helloworld
int a = 10;
int b = 20;
System.out.println("a = " + a); // 输出 a = 10
这里的10是字符串,因为如果是字符串放在 + 号前面,后面的整型都将会隐式转化成字符串,因此使用 “+” 也是将整型转化成字符串的一种方法。如下,
int a = 10;
int b = 20;
System.out.println("a = " + a + b); // 输出 a = 1020
如果想要得到 a = 30 的结果,就得将 a + b 用括号括起来,因为 () 的优先级比 + 高。但如果是System.out.println(a + b + "= a + b");
则输出30 = a + b,此时
第一个 + 是求和,第二个 + 是拼接。
2.2 字符串之间的比较
对于引用类型,== 比较的是引用中的地址;String 中重写了 Object 类的 .equals 方法按照字符大小的顺序一个字符一个字符地进行比较,返回值是 Boolean 类型。
public class test {public static void main(String[] args) {String str1 = new String("hello");String str2 = new String("hello");String str3 = str1;System.out.println("====比较地址====");System.out.println(str1 == str2); // falseSystem.out.println(str1 == str3); // trueSystem.out.println(str2 == str3); // falseSystem.out.println("====比较内容====");System.out.println(str1.equals(str2)); // trueSystem.out.println(str1.equals(str3)); // trueSystem.out.println(str2.equals(str3)); // true}
}
比较大小用的是 compareTo 方法,返回 int 类型,比较规则如下:
- 先按照字典次序大小比较,如果出现不等的字符,直接返回这两个字符的大小差值;
- 如果前k个字符相等(k为两个字符长度最小值),返回值两个字符串长度差值。
若想忽略大小写则调用 compareToIgnoreCase() 方法,该方法常用于验证码当中。
public class test {public static void main(String[] args) {// 比较字符串的大小String str1 = new String("abc");String str2 = new String("abc");String str3 = new String("acd");String str4 = new String("abcdef");String str5 = new String("AbC");System.out.println(str1.compareTo(str2)); // 相同输出0System.out.println(str1.compareTo(str3)); // 不同输出字符十进制差值-1System.out.println(str1.compareTo(str4)); // 前k=3个字符完全相同,输出长度差值-3System.out.println(str1.compareToIgnoreCase(str5)); // 相同输出0}
}
2.3 计算长度
使用 .length() 方法获取字符串长度,注意是有 () 的。
String str = new String("ohana");
System.out.println(str.length()); // 输出5System.out.println("ohana".length()); // 用""引起来的也是 String 类型对象
int[] array = {1,2,3,4,5};
System.out.println(array.length); // 而计算基本数据类型数组的长度没有 ()
2.4 查找
方法 | 功能 |
---|---|
char chaAt(int index) | 返回 index 位置上的字符,如果 index 为负数或者越界,抛出 IndexOutOfBoundsException 异常 |
int indexOf(char ch) | 返回字符 ch 第一次出现的位置,没有则返回 -1 |
int indexOf(char ch, int fromIndex) | 从 fromIndex 位置开始找 ch 第一次出现的位置,没有则返回 -1 |
int indexOf(String str) | 返回 str 第一次出现的位置,没有则返回 -1 |
int indexOf(String str, int fromIndex) | 从 fromIndex 位置开始找 str 第一次出现的位置,没有则返回 -1 |
int lastIndexOf(char ch) | 从后往前找,返回 ch 第一次出现的位置,没有则返回 -1 |
int lastIndexOf(char ch, int fromIndex) | 从 fromIndex 位置往前找 ch 第一次出现的位置,没有则返回 -1 |
int lastIndexOf(String str) | 从后往前找,返回 str 第一次出现的位置,没有则返回 -1 |
int lastIndexOf(String str, int fromIndex) | 从 fromIndex 位置往前找 str 第一次出现的位置,没有则返回 -1 |
public class test {public static void main(String[] args) {// 1、charAt(int index) 根据下标查找相应的字符String str = "hello";char ch = str.charAt(2);System.out.println(ch);// charAt 通常用for循环打印字符串内容:for (int i = 0; i < str.length(); i++) {char c = str.charAt(i);System.out.print(c + " ");}// 2、indexOf() 返回要查找的字符或者字符串第一次出现的位置,没有则返回-1// indexOf(int ch)int index = str.indexOf('l');System.out.println(index);// indexOf(String str)int index2 = str.indexOf("ll");System.out.println(index2);// 3、indexOf(char ch, int fromIndex) 从fromIndex位置开始找ch第一次出现的位置,没有返回-1int index3 = str.indexOf('l',3);System.out.println(index3);// 4、从后往前找,返回ch/str第一次出现的位置,没有返回-1// lastIndexOf(char ch)String str2 = "ababcabcd";int index4 = str2.lastIndexOf('a');System.out.println(index4);// lastIndexOf(String str)int index5 = str2.lastIndexOf("abc");System.out.println(index5);// 5、从fromIndex开始从右往左找int index6 = str2.lastIndexOf('a',4);System.out.println(index6);int index7 = str2.lastIndexOf("bc",7);System.out.println(index7);}
}
2.5 转化
包括数值和字符串之间的转化、大小写字母转化、字符串转化成数组、格式化。
1、数值转化成字符串,调用 String.valueOf() 方法
class Student{public String name;public int age;public Student(String name, int age) {this.name = name;this.age = age;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +'}';}
}
public static void main(String[] args) {int i = 4901;String s1 = String.valueOf(i);String s2 = String.valueOf(49.01);String s3 = String.valueOf(true);String s4 = String.valueOf(new Student("张三",16));System.out.println(s1);System.out.println(s2);System.out.println(s3);System.out.println(s4);
}
2、字符串转化为数值,使用 包装类型.parseXxx()方法(in 的包装类型是 Integer、char 的包装类型是 Character)
public static void main(String[] args) {int a = Integer.parseInt("409");double b = Double.parseDouble("40.901");System.out.println(a);System.out.println(b);
}
3、大小写转化,只能把字母进行大小写转化,对汉字或者阿拉伯数字无效
public static void main(String[] args){String str1 = "hello";System.out.println(str1.toUpperCase()); // 输出 HELLOString str2 = "IcE-creAM好吃";System.out.println(str2.toLowerCase()); // 输出 ice-cream好吃
}
4、字符串转数组,使用 toCharArray() 并用字符数组进行接收
public static void main(String[] args){String str3 = "iceCream";char[] ch = str3.toCharArray();for (int j = 0; j < ch.length; j++) {System.out.println(ch[j]);}
}
5、数组转字符串,前面讲字符串的构造已有提及
char[] ch2 = {'i','c','e'};
String str4 = new String(ch2);
6、 格式化输出
public static void main(String[] args){String str5 = String.format("%d-%d-%d",2025,11,1);System.out.println(str5); // 2025-11-1String str6 = String.format("%f@.com",4.09);System.out.println(str6); // 4.090000@.com
}
2.6 替换
使用一个特定的新的字符串替换掉已有的字符串数据。
方法 | 功能 |
---|---|
String replaceAll(String regex, String replacement) | 替换所有的指定内容 |
String replaceFirst(String regex, String replacement) | 替换首个内容 |
public class test {public static void main(String[] args) {String s = "ababcabcd";String tmp = s.replace('a','A'); // 因为不是修改原来的字符串,而是返回新字符串因此要接收System.out.println(tmp);tmp = s.replace("ab","uu");System.out.println(tmp);tmp = s.replaceFirst("ab","uu");System.out.println(tmp);tmp = s.replaceAll("ab","UU");System.out.println(tmp);}
}
【注意:涉及到字符串的修改的(如转化和替换),并不是在原本的字符串上进行修改,而是返回一个新的字符串。】
2.7 拆分
将完整的字符串按照指定的分隔符划分成若干个子字符串
方法 | 功能 |
---|---|
String[] split(String regex) | 将字符串全部拆分 |
String[] split(String regex, int limit) | 将字符串以指定的格式,拆分为 limit 个组 |
1、按照一个字符来拆分
public static void main(String[] args) {String s = "brand=Haagen-Dazs&price=699";String[] tmp = s.split("&");for (int i = 0; i < tmp.length; i++) {System.out.println(tmp[i]);}System.out.println();
}
2、按照两个字符来拆分,有2种方法
public class test_7_27 {public static void main(String[] args) {String s = "brand=Haagen-Dazs&price=699";String[] tmp = s.split("&");// 多次拆分——嵌套for (int i = 0; i < tmp.length; i++) {String[] ss = tmp[i].split("=");for (int j = 0; j < ss.length; j++) {System.out.print(ss[j] + " ");}System.out.println();}System.out.println("-----------------------");tmp = s.split("=|&"); // 用字符“|”来间隔开两个字符for (int i = 0; i < tmp.length; i++) {System.out.print(tmp[i]+" ");}}
}
3、按特殊字符来拆分
注意事项:
- 字符"|","*","+"都得加上转义字符,前面加上"\";
- 而如果是"\",那么就得写成"\\";
- 如果一个字符串中有多个分隔符,可以用"|"作为连字符。
public class test {public static void main(String[] args) {// 拆分IP地址String s = "192.168.1.0";String[] tmp = s.split("\\.");for (int i = 0; i < tmp.length; i++) {System.out.println(tmp[i]);}System.out.println("-------------");String s2 = "4+9+101";tmp = s2.split("\\+");for (int i = 0; i < tmp.length; i++) {System.out.println(tmp[i]);}System.out.println("-------------");String s3 = "ab\\cab\\cd"; // 实际上是字符串:ab\cab\cdtmp = s3.split("\\\\"); // 实际上是两个\for (int i = 0; i < tmp.length; i++) {System.out.println(tmp[i]);}}
}
2.8 截取
public static void main(String[] args) {String s = "icecream";String ss = s.substring(3);System.out.println(ss);ss = s.substring(3,7); // 区间是左闭右开System.out.println(ss);
}
2.9 删除左右两边的空白字符
trim() 方法删除字符串左右两边空白字符(空格、换行、制表符等)
public static void main(String[] args) {String s = " ni hao saoa \t";System.out.println("[" + s + "]");String ss = s.trim(); // 该方法只能去除字符串左右两边的空白字符System.out.println("[" + ss + "]");
}
了解到那么多方法后,不知道你们有没有发现有一些方法可以直接通过类名来调用,但有一些方法需要引用变量才能调用。那些直接通过类名调用的,如 String.valueOf() 方法,说明它们是静态方法,又比如最常见的 System.out 中的 out 方法也是静态方法。
2.10 不可变性
在对 String 这个类创建的原对象进行操作的时候,我们发现原来的字符串并没有被修改,而是接收到一个新的字符串,所以需要新建引用变量来接收“修改”之后的对象。这是为什么?
String 被 final 修饰,表示该类不能被继承,而不是不能被修改的原因;验证 value 数组被 final 修饰是不是不能被修改:
由上图可见,被 final 修饰的数组可以修改原本的数组元素,但不能创建新对象,与 String 的实现相反。因此并不是这个原因令字符串不能被修改。
实际上是因为 value 被 private 修饰,只能在当前 String 类中使用,因此我们无法获取 value 并对其进行操作。
为什么 String 要设计成不可变的?(不可变对象的好处是什么?)
1. 方便实现字符串对象池。如果 String 可变,那么对象池就需要考虑写时拷贝的问题了。
2. 不可变对象是线程安全的。
3. 不可变对象更方便缓存 hash code,作为 key 时可以更高效的保存到 HashMap 中。
2.11 修改
注意:尽量避免直接对String类型对象进行修改,因为String类是不能修改的,所有的修改都会创建新对象,效率非常低下。原因如下:
public static void main(String[] args) {String s = "hello";s += " world";System.out.println(s);
}
上面代码将字符串 s 拼接成 helloworld,是某种意义上的“修改”。将这些代码反汇编之后实际上的底层逻辑是下面的代码:
图片对应的执行代码如下:
public static void main(String[] args) {StringBuilder stringBuilder = new StringBuilder(); // 对应 <init>stringBuilder.append("hello");stringBuilder.append(" world");String ret = stringBuilder.toString();System.out.println(ret);
}
两个代码运行结果一致,但使用 “+” 进行拼接与调用 StringBuilder 的效率不同,因为 StringBuilder 不会创建一个对象之后销毁再创建一个对象再销毁……而是只创建了一个对象并对其进行操作之后最后返回同一个对象。以下是通过统计计算机当前以毫秒为单位的时间戳来分别计算 使用 “+” 进行拼接与调用 StringBuffer 和 StringBuilder 的效率:
public static void main(String[] args) {long start = System.currentTimeMillis();String s = "";for(int i = 0; i < 10000; ++i){s += i;}long end = System.currentTimeMillis();System.out.println(end - start);start = System.currentTimeMillis();StringBuffer sbf = new StringBuffer("");for(int i = 0; i < 10000; ++i){sbf.append(i);}end = System.currentTimeMillis();System.out.println(end - start);start = System.currentTimeMillis();StringBuilder sbd = new StringBuilder();for(int i = 0; i < 10000; ++i){sbd.append(i);}end = System.currentTimeMillis();System.out.println(end - start);
}
输出结果如:
328
1
1
因此以后尽量不使用 “+” 进行拼接,而是借助 StringBuffer 或 StringBuilder 这两个类。
三、StringBuilder 和 StringBuffer
由于 String 的不可更改特性,为了方便字符串的修改,Java 中又提供 StringBuilder 和 StringBuffer 类,String 类有的方法,它们同样也有。这两个类大部分功能是相同的,这里介绍 StringBuilder 常用的一些方法,其它需要用到的时候可参阅Java Platform SE 8(在网页中按Ctrl+F,可输入要查找的类)。
方法 | 说明 |
---|---|
append() | 拼接字符串 |
void setCharAt(int index, char ch) | 将index位置的字符设置为ch |
StringBuilder deleteCharAt(int index) | 删除 index 位置字符 |
int capacity() | 获取底层保存字符串空间总的大小 |
void ensureCapacity(int mininmumCapacity) | 扩容 |
StringBuilder insert(int offset, String str) | 在 offset 位置插入:八种基类类型 & String类型 & Object类型数据 |
StringBuilder replace(int start, int end, String str) | 将 [start, end) 位置的字符替换为 str |
StringBuilder delete(int start, int end) | 删除 [start, end) 区间内的字符 |
String substring(int start) | 从start开始一直到末尾的字符以String的方式返回 |
String substring(int start,int end) | 将 [start, end) 范围内的字符以String的方式返回 |
String toString() | 将所有字符按照String的方式返回 |
StringBuilder reverse() | 反转字符串 |
public static void main(String[] args) {// 逆转字符串StringBuilder stringBuilder = new StringBuilder("abcdef");stringBuilder.reverse(); // fedcbaSystem.out.println(stringBuilder);
}public static void main(String[] args) {// 插入字符串StringBuilder stringBuilder = new StringBuilder("hello,,nice");stringBuilder.insert(5,"world"); // helloworld,,niceSystem.out.println(stringBuilder);
}
append 方法的比较
@Override
public StringBuilder append(String str) {super.append(str);return this;
} // 主要用在单线程情况下@Override
public synchronized StringBuffer append(String str) {toStringCache = null;super.append(String.valueOf(obj));return this;
} // 主要用在多线程情况下,可以保证线程安全
synchronized “同步的”,相当于一把锁,可以频繁开锁和关锁,也就是说字符串如果被频繁修改会占用资源,且一旦被占用就会锁上,以保安全。因此如果要频繁修改字符串 ,应该使用 StringBuilder 类处理。
四、String 和 StringBuilder 之间的转换原则
注意:String 和 StringBuilder 类不能直接转换。
如果要想互相转换,可以采用如下原则:
· String 变为 StringBuilder:利用 StringBuilder 的构造方法或 append() 方法
· StringBuilder 变为 String:调用 toString() 方法。
五、面试题
1、String、StringBuffer、StringBuilder的区别
答:
① String 的内容不可修改,StringBuffer 与 StringBuilder 的内容可以修改;
② StringBuffer 与 StringBuilder 大部分功能是相似的;
③ StringBuffer 采用同步处理,属于线程安全操作;而 StringBuilder 未采用同步处理,属于线程不安全操作。
2、 字符串相加
给定两个字符串形式的非负整数 num1
和num2
,计算它们的和并同样以字符串形式返回。
不能使用任何内建的用于处理大整数的库(比如 BigInteger
), 也不能直接将输入的字符串转换为整数形式。
六、练习题
6.1 找出第一个只出现一次的字符
给定一个字符串 s
,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1
。
解题思想:创建一个很长的整型数组 count[] ,其中 ‘a’ 字符对应的是97下标,‘b’字符对应的是98下标,因此字符也可以当作数组下标。如果遇到了这个字符 count 就加1,统计这个字符出现的次数得到存放每个字符在字符串中总出现次数的数组。例如 "baccdd" 得到数组是"1122"。
简单来说,就是数组的下标是字符,内容是出现次数。
但我们发现前面和后面有大部分空间都被浪费了,所以我们可以重写申请一个字符'a'开头的数组,需要用 字符- 'a' ,若该字符正好是'a' ,则对应的就是下标0,以此类推。
但是不能直接遍历数组,认为第1个出现次数是1的就是只出现一次的字母,因为 "baccdd" 中第一个只出现一次的字符是 ‘b’。
class Solution {public int firstUniqChar(String s) {int[] count = new int[26];// 统计每个字符出现的次数for(int i = 0; i < s.length(); i++){char ch = s.charAt(i);count[ch - 'a']++;}// 找到第一个只出现一次的字符for(int i = 0; i < s.length; i++){char ch = s.charAt(i);if(count[ch - 'a'] == 1){return i;}}return -1;}
}
6.2 最后一个单词的长度
对于给定的若干个单词组成的句子,每个单词均由大小写字母混合构成,单词间使用单个空格分隔。输出最后一个单词的长度。
两种方法:
1、从后往前查找第一个空格的下标,使用截取获得该位置+1之后的字符串,然后计算其长度;
2、将字符串根据空格进行分割获得字符串数组,拿下标为数组长度-1的元素出来计算其长度。
import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner in = new Scanner(System.in);String s = in.nextLine();// 第1种方法int index = s.lastIndexOf(" ");int len = s.substring(index + 1).length();System.out.println(len);// 第2种方法/*String[] ss = s.split(" ");int len = ss[ss.length-1].length();System.out.println(len);*/}
}
6.3 检测字符串是否是回文
如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符(字母和数字都属于字母数字字符)之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。
给你一个字符串 s
,如果它是 回文串 ,返回 true
;否则,返回 false
。
class Solution {public boolean isDigitOrCharacter(char ch) {if (Character.isDigit(ch) || Character.isLetter(ch)) {return true;}return false;}public boolean isPalindrome(String s) {s = s.toLowerCase();int left = 0;int right = s.length() - 1;while (left < right) {while (left < right && !isDigitOrCharacter(s.charAt(left))) {left++;}while (left < right && !isDigitOrCharacter(s.charAt(right))) {right--;}if (s.charAt(left) == s.charAt(right)) {left++;right--;}else {return false;}}return true;}
}public class Palindromic_String {public static void main(String[] args) {String s = "A man, a plan, a canal: Panama";Solution solution = new Solution();System.out.println(solution.isPalindrome(s));}
}
6.4 手搓 toLowerCase 方法
因为大小写字母之间的ASCII码值相差32,因此大小写的转化可以是大写字母+32,或者小写字母-32,记得要强制类型转换。
class Solution2{public String toLowerCase(String s) {StringBuilder stringBuilder = new StringBuilder();for (int i = 0; i < s.length(); i++) {char ch = s.charAt(i);// 判断当前字符是否是字母if (Character.isLetter(ch)){// 判断当前是不是大写字母if (Character.isUpperCase(ch)){ch = (char)(ch + 32);}}stringBuilder.append(ch); // 将字符拼接起来}return stringBuilder.toString();}
}public class toLowerLetter {public static void main(String[] args) {Solution2 solution2 = new Solution2();String ret = solution2.toLowerCase("HEllo");System.out.println(ret);}
}
6.5 字符串中的单词数
统计字符串中的单词个数,这里的单词指的是连续的不是空格的字符。请注意,你可以假定字符串里不包括任何不可打印的字符。
示例:
输入:"Hello, my name is John"
输出:5
解释:这里的单词是指连续的不是空格的字符,所以 "Hello," 算作 1 个单词。
这道题目其实并没有想象中的简单,因为如果是"",应该返回0而不是1,这个比较好解决:
class Solution{public int countSegments(String s){int len = s.length;if(len == 0){return 0;}String[] ss = s.split(" ");return ss.length;
}
又或者是", , , , a, eaefa",应该返回6,但是上述的代码返回的是13。调试得到数组存放具体的13个字符是什么:
由图可见,空格之间的间隙——空字符也被当作字符了。对此我们需要新增条件:
public class CountWords {public static int countSegments(String s) {int len = s.length();if(len == 0){return 0;}int count = 0;String[] ss = s.split(" ");for (String r: ss) {if (r.length() != 0){ // 长度如果是0的不算count++;}}return count;}public static void main(String[] args) {int count = countSegments(", , , , a, eaefa");System.out.println(count);}
}
还有第2种调用 isEmpty() 的判断方法:
public class CountWords {public static int countSegments(String s) {int len = s.length();if(len == 0){return 0;}int count = 0;String[] ss = s.split(" ");for (String r: ss) {if (!r.isEmpty()){ // 不为空则+1count++;}}return count;}public static void main(String[] args) {int count = countSegments(", , , , a, eaefa");System.out.println(count);}
}