
专栏简介 :java语法
创作目标:从不一样的角度,用通俗易懂的方式,总结归纳java语法知识.
希望在提升自己的同时,帮助他人,与大家一起共同进步,互相成长.
学历代表过去,能力代表现在,学习能力代表未来!
目录
Hello!大家好!今天给大家带来的是,javaSE中的字符串语法讲解,旨在从更深入更底层的视角去剖析字符串在内存中的存储结构,便于大家更深入的理解.在学习字符串底层逻辑的同时还能了解到更多关联知识,开拓视野并完善知识体系.
观察String类的源码我们可以发现,String为一个引用类型指向一个由value和hash组成的对象(JDK1.8),value又指向一个char类型的数组.但JDK1.9中String为一个引用类型指向一个由value,hash,coder,hashIsZero组成的对象,value指向一个byte类型的数组.改动的目的是coder为byte编码更加节省空间.由于改动不影响理解,所以之后的内容都按JDK1.8来讲解.


常见的构造字符串的方式:
- //方法一:
- String str = "Hello World";
-
- //方法二:
- 2.String str = new String("Hello World");
-
- //方法三:
- 3.char[ ] array = {'a','b','c'};
- String str = new String(array);
每种创建字符串的方式所创建的对象数量都不相同,例如方法一创建2个对象,方法二创建3个对象,方法三创建4个对象.
注意:String为引用类型,String str = "Hello World";的内存布局如下:(以下是为了方便理解的简图,更底层的讲解在字符串常量池部分)

"池""是编程中一中常见的,提升效率的方式,在java程序中类似1,2,3,true,false,."hello" 这些常见的字面值常量会频繁的使用,为了提升程序效率节省内存,java为8种基本数据类型和String类都提供了常量池.
例如:小美没有男朋友的时候想吃瓜子必须从瓜子池中取出一个瓜子嗑瓜子,小美觉得这样太麻烦,于是她找了一个男朋友小明,小明把嗑好的瓜子收集在一起等待小美食用,从此以后小美吃瓜子的效率大大提升了.
JDK1.6时StringTable存放在永久代,由于永久代使用JVM内存默认空间较小且垃圾回收评率较低,于是JDK1.7时存放在堆区,JDK1.8时存放在元空间(本质还是堆区),这样做的好处是和其他对象一样需要调优应用时调整堆的大小即可.
| JDK版本 | 字符串常量池位置 | 大小设置 |
| 1.6 | 永久代 | 固定大小:1009 |
| 1.7 | 堆 | 可设置:默认60013 |
| 1.8 | 元空间(堆) | 可设置:最小1009 |
通过以下代码剖析StringTable在内存中的存储方式.
- public static void main(String[] args) {
- String str1 = "Hello";
- String str2 = "Hello";
- String str3 = new String("world");
- String str4 = new String("world");
- }
注意:像"hello","world"这类常见字符串,编译时期已创建好并存放于字符串池.字符串常量池会返回一个String对象引用,当创建String str1 = "hello"时,先在字符串常量池中找,找到了直接将字符串引用赋值给str1.
通过以上案例可以看出:1.只要new都会产生一个唯一的对象,2.常量串创建String对象更高效.

intern()方法是一个Native方法(底层由C++实现无法观察到源码),其作用是将String创建的对象收到入池.如:
- char[] arr = new char[]{'h','e','l','l','o'};
- String str = new String(arr).intern();
- String str2 = "hello";
- System.out.println(str==str2);//输出true


调用intern()方法来创建字符串时,先到字符串常量池中寻找有没有与要创建的字符串equals相等的字符串.
- 如果有则返回字符串常量池中的地址.
- 如果没有则创建str到字符串常量池中并返回str的地址.
如果上面例子中的String str2先创建,此时字符串常量池已有"hello",那么str.intern()就不会入池,并且创建一个新的对象.最后结果一定是false.
注意:char创建的字符数组的引用赋值给字符串时,并不会把本身引用直接赋值给字符串,因为如果这样做的话一但改变字符数组那么字符串也会跟着改变,所以需要拷贝(copyof)一份对象再赋值给字符串

该图可以清晰的看出:
- String类中的字符实际保存在,内部维护的value数组中.
- String被final所修饰只能表示该类不能被继承.
- value被final所修饰表明vale自身的值不能改变,即value引用的指向不能改变,但引用空间中的值可以被修改.
- private才是字符串真正不能被修改的原因,被private修饰且没有提供可修改的公共接口.如setvalue().
由于博主使用JDK1.9不方便展示源码细节,感兴趣的朋友使用JDK1.8可以按住ctrl+鼠标左键在IDEAL中查看.
所有涉及到字符串修改的操作和方法都会新建一个对象效率非常低下,如果要修改建议使用StringBuffer或StringBuilder.
- public static void main(String[] args) {
- long start = System.currentTimeMillis();
- String str = "";
- for (int i = 0; i < 10000; i++) {
- str+="w";
- }
- long end = System.currentTimeMillis();
- System.out.println(end-start);
-
- StringBuilder str1 = new StringBuilder("");
- start = System.currentTimeMillis();
- for (int i = 0; i < 10000; i++) {
- str1.append("w");
- }
- end = System.currentTimeMillis();
- System.out.println(end-start);
-
- StringBuffer str2 = new StringBuffer("");
- start = System.currentTimeMillis();
- for (int i = 0; i < 10000; i++) {
- str2.append("w");
- }
- end = System.currentTimeMillis();
- System.out.println(end-start);
- }
- //str+="w" 花费19s
- //str1.append("w") 花费1s以内
- //str2.append("w") 花费1s以内
- public static void test(){
- String str1 = "1";
- String str2 = "2";
- String str3 = "12";
- String str4 = str1+str2;
- System.out.println(str3==str4);//false
- }
str3在字符串常量区,str4在堆上. 二者地址必然不同.
str1+str2的微观操作解释:
- 2 astore_1 //存放在局部变量表索引1位置处(当前方法为非静态,0处存放的是this)
- 3 ldc #15 //字符串常量池中的"b"
- 5 astore_2 //存放在局部变量表索引2位置处
- 6 ldc #16
//字符串常量池中的"ab" - 8 astore_3 //存放在局部变量表索引3位置处
- 9 new #9
//堆上创建StringBuilder - 12 dup //将堆上对象的地址复制到局部变量表中
- 13 invokespecial #10
> //init构造器初始化 - 16 aload_1 //取出局部变量表中的"a"
- 17 invokevirtual #11
//调用StringBuilder的append方法,将"a"添加apperd("a") - 20 aload_2 //取出局部变量表中的"b"
- 21 invokevirtual #11
//调用StringBuilder的append方法,将"b"添加apperd("b") - 24 invokevirtual #12
//toString()转为字符串,"ab" - 27 astore 4 //"ab"存放在局部变量表索引4位置处
- 29 return //返回
由此可观察到字符串创建的三步骤:
- 9 new #9
//堆上创建StringBuilder - 12 dup //将堆上对象的地址复制到局部变量表中
- 13 invokespecial #10
> //init构造器初始化
间接说明new关键字不是原子性的.
str1+str2的宏观操作解释:
[拓展]
字符串拼接操作不一定必须使用StringBuilder.
如果拼接符号左右均是字符常量或常量引用(被final修饰),则仍然使用编译器优化而非StringBuilder.
- public static void test1(){
- final String str1 = "a";
- final String str2 = "b";
- String str3 = "ab";
- String str4 = str1+str2;
- System.out.println(str3==str4);//true
- }

总得来说字符串拼接需要注意四点:
常量或者被final修饰的变量,在编译器可以确定所以防止常量池中.
变量由于其引用地址不确定所以不能放入常量池只能在堆上创建.
[常见面试题测试]:
- public static void test2() {
- String str1 = "a"+"b"+"c";
- String str2 = "abc";
- System.out.println(str1==str2);
- System.out.println(str1.equals(str2));
- }
- //true
- //true
str1中拼接的都是字符串常量编译期就已确定.
- public static void test3(){
- String str1 = "hello";
- String str2 = "world";
-
- String str3 = "helloworld";
- String str4 = "hello"+"world";
- String str5 = str1+"world";
- String str6 = "hello"+str2;
- String str7 = str1+str2;
- System.out.println(str3==str4);//true
- System.out.println(str3==str5);//false
- System.out.println(str3==str6);//false
- System.out.println(str3==str7);//false
- System.out.println(str5==str6);//false
- System.out.println(str5==str7);//false
- System.out.println(str6==str7);//false
- String str8 = str6.intern();
- System.out.println(str3==str8);//true
- }
String str5 = str1+"world";//此时str1为一个引用变量
拼接前后出现变量则存放在堆空间中,与字符串常量池中的地址必然不一样.
- String str3 = "helloworld";
- String str8 = str6.intern();
- System.out.println(str3==str8);//true
此时字符串常量池中已有"helloworld",调用intern()会返回字符串常量池中的地址,被str8接收那么二者地址必然一样,
[讨论一]
String s = new String("ab");
new String("ab")为什么会创建两个对象?

- 堆上创建了一个.
- 字符串常量池中创建了一个,如果之前没有的话.
- public static void test4(){
- String s = new String("a")+new String("b");//创建了几个对象?
- System.out.println(s);
- }
StringBuilder.toString().剖析:
该操作相当于把字符串缓冲区中的字符串对象返回,但不会返回到字符串常量池,只会在堆上创建一个对象 new String("ab").

我们发现没有idc指令,其实就是拼接完没有往常量池中放一份.
[讨论二]
- public static void test5(){
- String str1 = new String("1");
- str1.intern();//字符串常量池中已有"1"
- String str2 = "1";
- System.out.println(str1==str2);//false
-
- String str3 = new String("1")+new String("1");
- str3.intern();//字符串常量池中没有"11"
- String str4 = "11";
- System.out.println(str3==str4);//true
- }
str1在先在堆上创建一个对象,再到字符串常量池中创建一个对象,str1指向的是堆上的对象.因为常量池中已有"1",所以str1.intern();没有任何效果.st1与str2地址不一致.
str3拼接的对象创建在堆上,且不会在字符串常量池中创建(原理上文已阐述),因此 str3.intern();这项操作会把str3所指对象置于常量池中.str3与str4地址一致.
字符串比较是比较常见的操作之一,java提供了四种比较方式.
注意:对于内置类型==比较的是变量中的值,对于引用类型则比较的是地址.
- public static void test7(){
- int a = 10;
- int b = 3;
- int c = 10;
- System.out.println(a==b);//false
- System.out.println(a==c);//true
-
- String s1 = "hello";
- String s2 = new String("hello");
- String s3 = "hello";
- System.out.println(s1==s2);//false
- System.out.println(s2==s3);//true
- }
父类中的equals()方法同样是比较地址,只不过String重写了该方法.

与equals()不同的是equals返回的是boolean类型的值,compareTo返回的是int类型的值.
- 先按照字典次序比较,如果出现不相等的字符,直接返回两字符大小的差值.
- 如果前k个字符相等(k为最小字符串的长度),返回两字符串长度的差值.
- public static void test8(){
- String s1 = "abc";
- String s2 = "ac";
- String s3 = "abc";
- String s4 = "abcdef";
- System.out.println(s1.compareTo(s2));//字符差值为1
- s1.compareTo(s3);//相同0
- s1.compareTo(s4);//长度差值-3
- }
- public static void test8(){
- String s1 = "abc";
- String s2 = "ac";
- String s3 = "Abc";
- String s4 = "abcdef";
- System.out.println(s1.compareTo(s2));//字符差值为1
- s1.compareTo(s3);//相同0
- s1.compareTo(s4);//长度差值-3
- }
| 方法 | 功能 |
| charAt(char 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 lastIndeOf(String str) | 从后往前寻找str第一次出现的位置,没有返回-1. |
| int lastIndexOF(String str,int fromIndex) | 从Index位置开始从后往前寻找str第一次出现的位置,没有返回-1. |
- public static void test9(){
- String str = "aaabbbcccaabbccdd";
- System.out.println(str.charAt(3));//b
- System.out.println(str.indexOf('b'));//3
- System.out.println(str.indexOf('b',6));//11
- System.out.println(str.indexOf("bbb"));//3
- System.out.println(str.lastIndexOf('c'));//14
- System.out.println(str.lastIndexOf('b',6));//5
- }
注意:上述方法都是实例方法.
- public static void test10(){
- String s1 = String.valueOf(123);//整形转字符串
- String s2 = String.valueOf(12.3);//浮点型转字符串
- String s3 = String.valueOf(true);//布尔型转字符串
- String s4 = String.valueOf(new Student("zhangsan",18));
- System.out.println(s1);
- System.out.println(s2);
- System.out.println(s3);
- System.out.println(s4);
- System.out.println("============================================");
- //字符串转数组
- int x = Integer.parseInt(s1);
- double y = Double.parseDouble(s2);
- System.out.println(x);
- System.out.println(y);
- }
注意:以上为静态方法.
- public static void test10(){
- String str1 = "abcdef";
- String str2 = "ABCDEF";
- System.out.println(str1.toUpperCase().equals(str2));//true
- System.out.println(str2.toLowerCase().equals(str1));//true
- }
- public static void test10(){
- String s = "hello";
- //字符串转数组
- char[] ch = s.toCharArray();
- for (int i = 0; i < ch.length; i++) {
- System.out.print(ch[i]+" ");
- }
- //数组转字符串
- String s1 = new String(ch);
- System.out.println(s1);
- }
- public static void test10(){
- String s = String.format("%d-%d-%d",2019,8,19);
- System.out.println(s);//2019-8-19
- }
| 方法 | 功能 |
| String replaceAll(String regex,String replacement) | 替换所有指定内容 |
| String replaceFirst(String regex,String replacement) | 替换首个指定内容 |
- public static void test10(){
- String str = "helloworld";
- System.out.println(str.replace("l","_"));//he__owor_d
- System.out.println(str.replaceFirst("l","_"));//he_loworld
- }
可以将一个完整的字符串按照指定的分隔符划分为若干个子字符串.
| 方法 | 功能 |
| String[] split(String regex) | 将字符串区别拆分 |
| String[] split(String regex,int limit) | 将字符串以指定形式拆分为limit组. |
- public static void test10(){
- String str = "hello world hello friend";
- String[] str2 = str.split(" ");
- for (String str1:str2) {
- System.out.print(str1);
- }
- }
- //hello
- //world
- //hello
- //friend
- public static void test10(){
- String str = "hello world hello friend";
- String[] str2 = str.split(" ",2);
- for (String str1:str2) {
- System.out.print(str1);
- }
- }
- //hello
- //world hello friend
- public static void test10(){
- String str = "192.168.1.1" ;
- String[] result = str.split("\\.") ;
- for(String s: result) {
- System.out.println(s);
- }
- }
注意:
- 如果是"|","+","."等有含义字符需要加上转义字符 "\\".
- 如果是"\"就得写成"\\\\".
- 如果一个字符串有多个分隔符可以用"|"做连字符.
- public static void test10(){
- String str = "name|hansan&age=18";
- String[] str2 = str.split("&|=|\\|");
- for (String str1:str2) {
- System.out.println(str1);
- }
- }
- //name
- //hansan
- //age
- //18
- public static void test10(){
- String str = "name=zhansan&age=18";
- String[] str2 = str.split("&");//[name=zhangsan,age=18]
- for (int i = 0; i < str2.length; i++) {
- String[] temp = str2[i].split("=");
- for(String stemp:temp){
- System.out.println(stemp);
- }
- }
- }
- //name
- //zhangsan
- //age
- //18
| 方法 | 功能 |
| String substring(int beginIndex) | 从指定索引截取到末尾 |
| String substring(int beginIndex,int endIndex) | 截取部分内容 |
- public static void test10(){
- String str = "HelloWold";
- System.out.println(str.substring(5));//World
- System.out.println(str.substring(0,5));//Hello
- }
| 方法 | 功能 |
| String trim() | 去掉字符串左右两边空格,保留中间空格 |
- public static void test10(){
- String str = " Hello Wold ";
- System.out.println(str.trim());
- }
trim()方法会去掉左右两边的空格,制表符,回车键等.....
由于String类的不可变性,为了方便字符串的修,java提供了两种类StringBuffer和StringBuilder.接下来介绍常见的操作方法.
| 方法 | 功能 | |
| StringBuilder append(String str) | 尾部追加,相当于字符串的+=,可以追加:
| |
| char charAt(int index) | 获取index位置的字符 | |
| int length() | 获取字符串的长度 | |
| int capaticy | 获取底层底层保存字符串空间的总的大小 | |
| void ensureCapaticy(int mininmumCapacity) | 扩容 | |
| int indexOf(String str) | 返回str第一次出现的位置 | |
| int indexOf(String str,int fromIndex) | 从下标fromIndex开始返回第一次出现str的位置 | |
| int lastIndexOf(String str) | 返回最后一次出现str的位置 | |
| int lastIndexOf(String str,int fromIndex) | 从下标fromIndex开始返回最后一次出现strf的位置. | |
| StringBuilder insert(int offset,String str) | 在offset位置插入:八种基本数据类型,或String类型或Object类型 | |
| StringBuilder deleteCharAt(int index) | 删除下标为index位置的元素 | |
| StringBuilder delete(int start,int end) | 删除区间[strat,end)之间的元素 | |
| StringBuilder replace(int start,int end,String str) | 将区间[start,end)之间的元素替换为str | |
| String subString(int start) | 从start开始一直到末尾的字符,以String类型返回 | |
| String subString(int start,int end) | 将[start,end)区间的字符,以String类型返回. | |
| StringBuilder reverse() | 反转字符串 | |
| String toStrng() | 将字符串按String的方式返回 | |
| void setCharAt(int index,char ch) | 将index位置的字符替换为ch |
- public static void test10(){
- StringBuilder sb1 = new StringBuilder("hello");
- StringBuilder sb2 = sb1;
- sb1.append("world");//helloworld
- sb1.append(123);//hellloworld123
- System.out.println(sb1==sb2);//true
- System.out.println(sb1.charAt(0));//获取0号位上的字符
- System.out.println(sb1.length());//获取字符串的长度
- System.out.println(sb1.capacity());//
- sb1.setCharAt(0,'H');//Helloworld123
- sb1.insert(0,"Helloworld!!!");//Helloworld!!!Helloworld123
- System.out.println(sb1.indexOf("Hello"));//获取Hello第一次出现的位置
- System.out.println(sb1.lastIndexOf("Hello"));//获取Hello最后一次出现的位置
- sb1.deleteCharAt(0);//elloworld!!!Helloworld123
- sb1.delete(0,5);//orld!!!Helloworld123
- String str = sb1.substring(0,5);//orld!
- System.out.println(str);
- sb1.reverse();//321dlrowolleH!!!dlro
- str = sb1.toString();
- System.out.println(str);
- }
注意:String 与 StringBuilder不能直接转换
- String转StringBuilder:利用StringBuilder的构造方法,或append()方法.
- StringBuilder转String:利用toString方法.
[面试题 ]
1.String,StringBuilder与StringBuffer的区别?
2.以下共创建几个对象?[前提不考虑字符串常量池中存在]
- String str1 = new String("hello");//2
- String str2 = new String("world")+new String("hehe");//6

由于只考虑小写字母,所以我们可以定义一个大小为26的整形数组,每个字符-'a'就会得到它数组中对于的位置, 该字符出现一次就给数组该位置的元素+1,最后遍历字符串,如果字符在count数组中对应的元素为一就返回该字符.
- public static 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;
- }
方法一:
用字符串的内置方法String[] split()方法,以空格分隔字符串并保存与字符串数组中,返回数组最后一个元素即可.
- import java.io.InputStream;
- import java.util.Scanner;
- public class Main{
- public static void main(String [] args) throws Exception{
- Scanner scanner = new Scanner(System.in);
- String str = scanner.nextLine();
- String[] str1 = str.split(" ");
- String str2 = str1[str1.length-1];
- System.out.println(str2.length());
- }
- }
方法二:
更底层的做法较为推荐,将字符串转换为字符数组,从后往前遍历数组,遇到空格返回数组长度-1与空格下标的差值即可.
- import java.io.InputStream;
- import java.util.Scanner;
- public class Main{
- public static void main(String [] args) throws Exception{
- Scanner scanner = new Scanner(System.in);
- String str = scanner.nextLine();
- char[] arr = str.toCharArray();
- int index = -1;
- for(int i = arr.length-1;i>=0;i--){
- if(arr[i]==' '){
- index = i;
- break;
- }
- }
- System.out.println(arr.length-1-index);
- }
- }

题中明确说明只考虑数字字符和字母,所以我们可以构造一个方法判断字符串是否合法,涉及到包装类Character的内置方法isDigit()和isLetter().题中强调不考虑字符的大小写,所以我们首先将字符串全部转换为小写或大写,然后定义首尾指针left和right比较字符串即可,注意比较时首先要满足left
再考虑是否合法,比较后一定要记得移动指针.
- public boolean isTrue(char ch){
- if(Character.isDigit(ch)||Character.isLetter(ch)){
- return true;
- }
- return false;
- }
- public boolean isPalindrome(String s) {
- String str = s.toLowerCase();
- int left = 0;
- int rigth = str.length()-1;
- while (left
- while (left
- left++;
- }
- while (left
- rigth--;
- }
- if (str.charAt(left)!=str.charAt(rigth)){
- return false;
- }
- left++;
- rigth--;
- }
- return true;
-
- }
4)字符集合

创建一个布尔数组大小为所有字母的编码之和(122-65),由于布尔数组默认初始化为false,所以我们可以判断如果字符串对应的数组下标为false,我们就将它拼接给StringBuilder对象,并将该下标对应元素置为true.最后以toString()返回StringBuilde对象并打印即可.
- public static String test(String s){
- boolean[] arr = new boolean[58];
- StringBuilder str = new StringBuilder();
- for(int i = 0;i
- char ch = s.charAt(i);
- if(arr[ch-'A']==false){
- str.append(ch);
- arr[ch-'A'']=true;
- }
- }
- return str.toString();
- }
- public static void main(String[] args) {
- Scanner scanner = new Scanner(System.in);
- while (scanner.hasNextLine()) { // 注意 while 处理多个 case
- String s = scanner.nextLine();
- System.out.println(test(s));
- }
- }
总结
以上就是字符串深入剖析的全部内容,从字符串最底层的常量池到基本操作方法和面试题1万5千多字,码子字易如果对你有亿点点帮助和启发麻烦不要吝啬三连哦!

-
相关阅读:
自动驾驶感知算法实战15——纯视觉感知和传感器融合方案对比,特斯拉九头蛇的进化
华为常用命令
人生就是一个醒悟的过程(深度好文)
Conditional DETR(ICCV 21)
SAP移动端解决方案参考
【当LINUX系统出现网络问题时该如何排查】
【Linux】环境变量
Python实战小项目分享
八、Nacos配置管理(统一配置管理、配置热更新、配置共享)
深度解析自然语言处理之篇章分析
-
原文地址:https://blog.csdn.net/liu_xuixui/article/details/126022607