目录
StringBuilder 与 StringBuffer (可变长字符串):
StringBuffer vs StringBuffer vs String 的执行效率 test:
关于通过直接赋值和通过 new String(String original)先初始化value数组再通过value属性指向池中字符串对象(字符串常量)的创建字符串对象的方式的结论:
(面试常问)String和StringBuilder和StringBuffer 有什么区别?:
String、StringBuilder、StringBuffer 的使用原则:
String类(不可变长字符串):
- String 对象用于保存字符串,也就一组字符序列。字符串常量对象使用双引号 括起 的字符序列。称之为字符串。字符串的本质就是一个 字符数组。
- 字符串的 字符 (内码) 是使用Unicode编码方案的 UTF-16 编码,(开发人员 基本上使用的是UTF-8,所以当内码转成外码的时候默认会以 UTF-8 的存储格式来转换(如有疑问,具体文章可参考我的另外一篇博文。Java 入门之6:Java中的char类型是怎么存储的以及常见的编码字符集 )),一个字符(不管是字母还是汉字和其他文字)都是占用两个字节。
- String类的常用构造器:,byte数组的String 构造器,在IO操作的时候使用得比较多。
- String类继承|实现UML类图:,String实现了Serializable接口,说明String 的对象可以串行化转成二进制在网络上进行传输。实现了Comparable接口说明String对象之间可以相互比较。实现了CharSequence接口说明String类型是 char 类型的序列表示。
- String类被final关键所修饰,证明String类不能有子类,不能被继承。(包括八种基本类型的包装类型也是final修饰的,不能被继承!)
- String类型的字符串是不可变的,一个字符串对象一旦被分配,其内容不可变的。
String类创建字符串对象的内存浅析:
- 常用的两种创建String 对象的方式 区别:
- 通过直接赋值的方式:String china = "中国";
- 这种创建字符串的创建方式,在编译 运行 程序的时候,运行器会先从字符串常量池中 查看是否有 "中国" 的存储空间(就是看常量池中有没有这个字符串)。如果有,则直接 指向字符串的在池中的内存地址并使用;如果没有则重新创建,然后再指向字符串池中内存地址并使用。 china 最终指向的 是字符串常量池的内存地址。
- 调用构造器创建对象:String china1 = new String("中国");
- 使用String类 构造器 初始化 value数组 创建对象 得到字符串的方式,在 编译 运行 程序的时候 运行器会先在堆中开辟一块空间。 里面维护了value属性,指向字符串常量池中的 "中国" 的内存地址。(String 里面有个名为 value 的字符数组,,如果字符串常量池没有 "中国" ,则在字符串常量池中重新创建字符串对象再指向。如果有,直接通过 value属性 指向字符串常量池中的内存地址,最终china1 指向的是堆中 创建的字符串对象的空间地址。(说明:String类 value属性是被 final 所修饰的,不可以修改!不可以修改指的是value指向的地址不可以修改,而不是value中的元素不可以修改。)
- 两种方式的内存浅析示意图:
- 说明:无论是直接赋值还是通过 new 关键字 调用构造器创建对象的方式,它们之间最大的区别是在于 Java提供了 直接赋值的方式省略了new 关键字 加载.class字节码文件,在一定程度上避免了内存开销。它是在加载所属类的时候,就已经在字符串常量池中创建完成并 存在了的。而new关键字创建对象的方式,只是多了一步加载String类的 .class字节码文件,初始化value数组的操作。它们的不同之处在于, 直接赋值的方式 的变量 指向的是 字符串常量池中的地址。而new 关键字创建对象的方式 是指向的 是创建的对象的空间地址。先在堆中开辟空间,再初始化value数组,然后再通过 value数组 指向字符串常量池中的地址。多了一个步骤。不管是 直接赋值还是 通过new关键字创建对象的方式,其实都是在给 value这个char 数组 赋值添加字符元素。只是 直接赋值的方式是隐式的,而new 关键字的方式是显式的!实际上操作 字符串 就等同于 操作 value这个 char数组!不管是操作直接赋值的方式还是操作 new 关键字创建对象的方式得到的字符串。都是在操作 value 这个 char数组。,实际上不管是直接赋值的方式,还是通过new 关键字创建对象的方式,只要它们的内容完全一样。它们所操作的char数组是同一个char数组,只是直接赋值和通过new 关键字创建对象它们的 所属变量引用 指向的内存地址不一样而已。但是它们的内容是完全一样的!
- 通过直接赋值的方式:String china = "中国";
String类对象的各种比较:
-
public static void strEquals1(){ String a = "zxc"; String b = "zxc"; // true (两者内容完全相同) System.out.println(a.equals(b)); // true (a 和 b 都是指向的 字符串常量池的 zxc 的内存地址,比较地址完全相等) System.out.println((a == b) + "\n") ; String c = new String("qwe"); String d = new String("qwe"); // true (两者内容完全相同) System.out.println(c.equals(d)); // false (c 和 d 指向的 是不同的 对象空间地址) System.out.println(c == d); // false ( a 指向的是字符串常量池的地址,而 d 指向的是堆中的对象空间地址) System.out.println( a == d); }
result:
-
public static void strEquals2(){ String e = "java"; String f = new String("java"); // true (两者内容完全相同) System.out.println(e.equals(f)); // false (e 指向的是字符串常量池中的地址,而 f 指向的是堆中的对象空间地址) System.out.println(e == f); /* *intern() 方法的作用: 当调用 intern 方法时,如果字符串常量池中已经包含了一个 等于 此 String对象的字符串 * (用 equals(Object) 方法确定)则返回字符串常量池中的字符串(地址), * 否则,将此String对象的字符串 添加到字符串常量池中后,再返回 String 对象的 字符串在字符串常量池中的地址 * 一句话概括就是:intern()方法最终返回的是主调对象的 字符串 在 字符串常量池中字符串的内存地址! 所以这里为 true */ // true (e 指向的是 字符串常量池中地址,而 f.intern()方法 返回是 f 对象的字符串 在字符串和常量池的内存地址 ) System.out.println(e == f.intern()); // false (f 指向的是 堆中 对象空间地址,而 f.intern() 方法返回是 f 对象的字符串在字符串常量池中的内存地址!) System.out.println(f == f.intern()); // true (两者内容完全相同) System.out.println(f.equals(f.intern())); /* * hashCode() 方法的作用:String类已经重写了Object根基父类的hashCode方法,它得到的是 该字符串的 hashCode 。 * 它的实现算法是: * public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; } * 从这个算法中可以看出 "java" 字符串 的hashCode码是: * 31 * 0 + j(的Unicode码 106 )= 106 * + 31 * j(的Unicode码 106 )+ a (的Unicode码 97 )= 3383 * + 31 * (31 * j(的Unicode码)+ a (的Unicode码)) + v(的Unicode码 118) = 104991 * + 31 * (31 * (31 * j(的Unicode码)+ a (的Unicode码)) + v(的Unicode码)) + a (的Unicode码) = 3254818 */ // true (两者 hashCode码完全相同) System.out.println(e.hashCode() == f.hashCode()); System.out.println(e.hashCode()); System.out.println(f.hashCode()); }
result:
-
public static void strEquals3(){ String g = "SpringBoot"; String h = "Spring"; String i = "Spring"; String j = new String("Spring"); // true (h 和 i 都是指向的字符串常量池的 "Spring" 的内存地址) System.out.println(h == i); // false (h 指向的是 字符串常量池中的内存地址, j 指向的是 堆中的对象空间地址 ) System.out.println(h == j); // true (两者内容完全相同) System.out.println(h.equals(j)); // false (g 指向的是 字符串常量池中 "SpringBoot" 的内存地址, // h 指向的是 字符串常量池中的 "Spring" 的内存地址) System.out.println(g == h); People people = new People(); people.name = "憨憨"; People people1 = new People(); people1.name = "憨憨"; // true(两者内容完全相同) System.out.println(people.name.equals(people1.name)); // true(两个对象中的name属性同时指向的是 "憨憨" 这一个字符串在字符串常量池中的地址) System.out.println(people.name == people1.name); // true(people.name 指向的 字符串常量池中的 "憨憨" 的内存地址, // 而 "憨憨" 这个字符串对象在字符串常量池中内存地址和 people.name 的相等!) System.out.println(people.name == "憨憨"); } class People{ public String name; }
result:
-
public static void strEquals4() { String str1 = "a" + "b" + "c"; String str2 = "ab" + "c"; String str3 = "a" + "bc"; String str4 = "abc" + ""; // true System.out.println(str1 == str2); // true System.out.println(str1 == str3); // true System.out.println(str1 == str4); // true System.out.println(str2 == str3); // true System.out.println(str2 == str4); // true System.out.println(str3 == str4); }
result:,字符串拼接字符串,如果是相同的字符串,如果字符串常量值中已经存在了该字符串。编译器会进行优化,它们指向的都是同一个字符串 在字符串常量池中的内存地址!反编译后代码:
String类常用的API:
-
public static void strCommonMethodTest(){ /*equals() : 区分字符串中字母大小写,判断内容是否相等。*/ /*startsWith(): 查看字符串是否以查看的字符串的前缀开头,是则true,否则false*/ /*endsWith: 查看字符串是否以查看的字符串的后缀结尾,是则true,否则false*/ String a = "java"; String b = "Java"; // true System.out.println(b.startsWith("Ja")); // true System.out.println(b.endsWith("va")); // false System.out.println(a.equals(b)); /* equalsIgnoreCase(): 忽略字符串中字母大小写,判断内容是否相等*/ // true System.out.println(a.equalsIgnoreCase(b)); /* length(): 获取字符的个数,字符串的长度(实际上就是获取的String对象的value数组的长度)*/ String c = "mysql"; // 5 System.out.println(c.length()); /* indexOf(): 获取字符在字符串中第一次出现的索引,索引从0开始,如果找不到,则返回 -1 */ String d = "elasticsearch"; // 2 System.out.println(d.indexOf("a")); /* lastIndexOf(): 获取字符在字符串中最右边出现的索引,索引从0开始,如果找不到,则返回 -1 */ // 7 System.out.println(d.lastIndexOf("s")); /* trim(): 去掉字符串首尾空格 */ String e = " Spring Summer Autumn Winter "; // Spring Summer Autumn Winter System.out.println(e.trim()); /*subString(int beginIndex): 从开始索引 往后截取所有的字串 */ /*subString(int beginIndex, int endIndex): 从开始索引 到 结束索引,截取这一指定范围的子串。*/ String f = "dubbo的底层实现,是基于netty的。"; // bo的底层实现,是基于netty的。 System.out.println(f.substring(3)); // [2,7) 包含开始索引字符,但不包含结束索引的字符! bbo的底 System.out.println(f.substring(2, 7)); /*charAt(): 获取字符串某索引上的字符,不能直接使用 Str[index] 方式! * String 类本身是对象,不是数组,只是字符串存储时是按照 char类型数组 进行存储的!*/ String g = "浮点数是按照IEEE754标准进行存储的!S - E - M"; // 标 System.out.println(g.charAt(13)); /*toUpperCase():将字符串中的字母全部转成大写*/ String h = "Spring家族系列有SpringBoot、SpringCloud、等等很多框架。yes!"; // SPRING家族系列有SPRINGBOOT、SPRINGCLOUD、等等很多框架。YES! System.out.println(h.toUpperCase()); /*toLowerCase(): 将字符串中的字母全部转成小写*/ // spring家族系列有springboot、springcloud、等等很多框架。yes! System.out.println(h.toLowerCase()); /*concat(): 字符串的拼接*/ String i = "12"; String j = "23"; String k = "34"; String concat = i.concat(j).concat(k); // 122334 System.out.println(concat); /*replace(): 替换字符串中需要替换的字符(or 子串), 将原来的字符串中旧的字符(子串)替换为新的字符(or 子串) replaceAll(): 替换字符串中需要替换的子串,将原来的字符串中每个旧的子串替换为新的子串 replaceFirst():将第一次在字符串中出现的字符串替换为新的子串 */ String l = "有个乌龟王八蛋有个王八蛋"; // 有个乌龟有个王*蛋 System.out.println(l.replace('八', '*')); // ****有个王八蛋 System.out.println(l.replace("有个乌龟", "****")); // 有个乌龟有个*** System.out.println(l.replaceAll("王八蛋", "***")); // 有个乌龟tortoise蛋有个王八蛋 System.out.println(l.replaceFirst("王八", "tortoise")); /*split(String regex):按照指定的分割子串 分割字符串。 * split(String regex, int limit):按照指定的分割子串 分割字符串,从左往右,只分割 limit-1 个 匹配的子串*/ String m = "a,b,c,d,e,f,g,h,i,j,k,l,m"; String disk = "C:\\Users\\administrator\\Desktop\\APIs\\jdk-apis"; /* abcdefghijklm */ for (String s : m.split(",")) { System.out.print(s); } /* a b c d,e,f,g,h,i,j,k,l,m */ System.out.println(); for (String s : m.split(",",4)) { System.out.println(s); } // 转义字符需要特殊处理,进行转义才可分割。 /* C: Users administrator Desktop APIs jdk-apis */ for (String s : disk.split("\\\\")) { System.out.println(s); } /* toCharArray():将一个字符串转成字符数组*/ char[] chars = m.toCharArray(); /*compareTo():用于比较两个字符串的大小 compareToIgnoreCase():忽略两个字符串的字母的大小写并且比较字符串大小。 * * (1):如果两个字符串的长度相同,并且字符串的每个字符相同,就返回 0 * (2):如果两个字符长长度相同,但是在比较的时候,compareTo方法区分大小写, * 就返回 主调字符串第一个 不同的那个字符的Unicode码 - 被调字符串第一个不同的那个字符的Unicode码的差值 * (3):如果短的字符串与长的字符串的前面的每个字符都相同,则返回 主调字符串长度 - 被调字符串长度 */ String n = "javaABC"; String o = "java"; String p = "Java"; // 0 System.out.println(o.compareToIgnoreCase(p)); // 3 System.out.println(n.compareTo(o)); o = "Java"; // 32 System.out.println(n.compareTo(o)); // -27917 o = "海市蜃楼"; System.out.println(n.compareTo(o)); /*format(): 格式化字符串:(位置必须一一对应,不然替换不了占位符) * 占位符: * (1)%s -> 字符串的占位符 * (2)%c -> 字符的占位符 * (3)%d -> 整数类型的占位符 * (4)%.2f -> 浮点数的占位符 * (说明:%.2f后面的2表示保留几位小数点, * 并且最后一位没有保留的小数点的数会四舍五入。 * 所以如果想保留3位小数可以用%.3f * 保留4位小数可以用%.4f,等 ……) * */ String name = "michael"; int age = 18; final double PI = 3.1415926; char gender = '男'; // 原本的拼接的方式: String information = "姓名是:" + name + ",年龄是:" + age +",算出来的圆周率是:" + PI +",性别是:" + gender + ",excellence人才"; System.out.println(information); // 使用format()方法后: System.out.println(String.format("姓名是:%s,年龄是:%d,算出来的圆周率是:%.4f,性别是:%c,excellence人才",name,age,PI,gender)); String mark = "他的姓名是:%s,他的年龄是:%d,他算出来的圆周率是:%.5f,他的性别是:%c,excellence人才"; System.out.println(String.format(mark,name,age,PI,gender)); /*regionMatches():当某个字符串调用该方法时,表示从当前字符串的firstStart位置开始,取一个长度为len的子串 然后从另一个字符串other的otherStart位置开始也取一个长度为len的字串,然后比较这两个字串是否相同。相同返回true, 否则返回false, 这个方法还有一个重载的方法,第一个参数如果为true则 忽略大小写比较。 如果为false,则 区分大小写比较 * */ int number = 0; String str = "fdaFDAFdafdafsdAfDa"; for (int y = 0; y < str.length(); y++) { if (str.regionMatches(true,y, "Da", 0, 2)) { number++; } } // Da 不区分大小写一共在 str 字符串那种出现了 6次 System.out.println(number); // true (在str字符串中从 2索引开始取两个长度的字串和 aF66 字符串的从 0索引开始取两个长度的子串比较是否相等。) System.out.println(str.regionMatches( 2, "aF66", 0, 2)); /*contains(): 查找 str字符串中是否包含了 fdaF这个子串,是则true,否则false*/ // true System.out.println(str.contains("fdaF")); /*String.valueOf() : 把指定的 数据类型转换为 字符串 */ // 字符串的 true System.out.println(String.valueOf(true)); }
result:
StringBuilder 与 StringBuffer (可变长字符串):
- StringBuilder 与 StringBuffer 类是对 String 类的增强。可以理解为它们是操作String 的 一个容器。
- 如果有变量参与到字符串的拼接中,编译器并不知道 这个变量指的是什么,所以编译器不会进行优化,不会直接合并。只有在运行程序的时候,运行器才会发现这个变量是引用着什么什么的。这时底层默认会 自动调用StringBuilder 进行字符串的可变操作,也就是在字符串后面追加字符串。如:
- 反编译代码解读:
- StringBuilder和StirngBuffer类 是对 String 类 的加强。StringBuilder和StringBuffer类属于兄弟类,它们同属于 AbstractStringBuilder 抽象类的子类,它们被final修饰,不可被其他类继承!它们的 API 都是一样的。只是在性能上一定的差别,使用场景不同!
- StringBuilder 和 StringBuilder 类的 UML图:
- StringBuilder的构造器:。
- AbstractStringBuilder抽象类中有两个特别重要的属性:
- value数组:用于字符的存储,这value数组 可扩容,数组元素可改变。
- count:用于记录数组有多少空间被使用。
- StringBuilder(); :空构造器,创建一个StringBuilder对象,默认初始化 char 数组长度为 16;(常用)
- StringBuilder(CharSequence seq); :创建一个StringBuilder对象,传入 CharSequence 接口的实现类对象,比如 可以传入 StringBuilder 类的实例,StringBuffer的实例。默认初始化 char 数组长度为传入的 可变字符串对象的长度 + 16;(不常用)
- StringBuilder(int capacity); 创建一个StringBuilder对象,并且可以 指定value数组默认初始化 容量是多少。(常用)
- StirngBuilder(String str);创建一个StringBuilder对象,传入的是 String类的对象,默认初始化 value数组长度为传入的字符串长度 + 16;(常用)
- StringBuilder(String str);和StringBuilder(CharSequence seq),的使用注意事项:
StringBuilder类的字符串可变机制浅析:
以StirngBuilder(String str);构造器为例:
跟踪源码:
- 获取字符串对象长度,调用父类构造器 初始化 value 数组(value数组的默认初始化长度为20):
- 调用本类append方法,在本类append方法中调用父类的append方法:
- 进入父类的append方法:
- 字符串对象,是否为null?
- String类中获取字符串长度的方法。
- 确保内部的容量够用:((count + 字符串的长度 4) - sb的value数组长度 20 )是否 大于 0
- 进入:字符串对象的 getChars方法:
- 进行数组的拷贝:
- value(源数组):指的是当前的 字符串对象(abcd),也就是AbstractStringBuilder中的str对象。
- srcBegin(源数组起始索引):从什么索引处开始 复制。 str.getChars(传入进来的是 0)。
- dst(目标数组):指的就是StringBuilder类中的 value数组,就是AbstractStringBuilder中value数组,指的就是 sb 长度为 20的初始化数组。
- dstBegin(目标数组中的元素起始索引):从目标数组的什么位置开始复制元素。str.getChars(传入进来是count)。第一次调用该方法,count的值为0。
- srcEnd - srcBegin (要从源数组中复制的元素个数): str.getChars(传入进来的是len,即字符串对象的长度),srcEnd(4) - srcBegin(0) ,要从源数组中复制 4个元素,换言之就是全部都复制到 dst 目标数组中。
- System.arraycopy 方法是个本地方法,由Java以外的语言 (C 或者 C++)实现:
- count += len;(执行完这一次 数组复制之后,count的值为 当前的len + count,第一次复制完 count 的值为 4)
- return this:返回当前对象,也就是 sb:,可以看到在StringBuilder 的append方法中没有做任何接收,但是它这里又 return this。返回到构造器调用 append 的地方。但是这里又没有做任何接收。。如果是使用 StringBuilder 的对象调用append方法,retrun this返回给调用者,返回的就是当前 的 sb对象。,调用者可以接收也可以不用接收,(需要说明的:此时sb的对象中的 value数组中 已经有个4个元素了。),不用接收当前sb对象 继续调用 StringBuilder 的append方法,依然是当前对象在调用,这就形成了 链式编程。
- 说明:如果使用当前对象 反复调用 append方法追加字符串,实际上在底层内部操作的都是 StringBuilder中 value 数组。,这个StringBuilder的value 数组(在 AbstractStringBuilder 抽象父类中)的权限修饰符是 缺省的。 说明(StringBuilder的 value数组的 地址和元素 都是可以发生改变的!Java也提供了其他的API让 调用者 针对StringBuilder中value数组中元素进行操作!而StringBuilder中value数组的地址可变是 StringBuilder中针对value数组扩容而发生改变。比如现在使用的 append方法,当追加字符串超过了vlaue数组的初始化长度后,就会进行value数组的扩容。那么value数组的地址也就发生了改变!),而String 内部的value是 prvate final修饰。,说明(String的 value数组的地址不可发生改变!原则上 String的 value数组中 元素 可以发生改变,但实际上 因为被private 修饰,由于封装的特性,Java也并没有提供能够让调用者改变 String 的value数组中的数组的元素的添加,删除 的API,所以String类中的value数组只是用来存储 字符串,并不能够被改变。称之为不可变字符串。)(但是Java提供了让调用者 修改(替换)String的value数组中的元素的API,比如replace(),replaceAll(),replaceFirst(),但是 这修改后的value数组是一个新的 字符串,并不是原来的字符串,这对原来的字符串没有任何影响!如果是拿替换前的变量来接收替换后的字符串时。只是 变量引用指向了其他的 字符串对象的地址了而已。 )
- 当 ,sb.append("efgijklmno"); 第二次向 StringBuilder中value数组中追加元素,的操作的时候,此时也并没有进行数组的扩容,当追加完元素后,StringBuilder底层的中count 的值也才 4 + "efghijklmno".length()。也才 15个,说明 StringBuilder中 value数组中的初始化空间 20个长度 被使用了 15个长度(StringBuilder类中的value数组已经有15个元素了)。还剩余5个空间长度未使用。
- 当 sb.append("pqrstuvwxyz");第三次向StringBuilder中 value 数组中追加元素,的操作的时候,此时进行了数组的扩容。会走以下逻辑:
- 执行(确保内部容量) ensureCapacityInternal(count + str.length());方法的逻辑:(这个方法内部的逻辑只有在 count + 要追加的字符串的长度大于 当前 value.length 时才会进行value数组的扩容!)
- 满足条件执行 Arrays.copyOf(value,new Capacity(mininumCapacity)),方法。
- value(要复制的数组) :就是当前StringBuilder正在操作的 value 数组
- new Capacity(minimunCapacity)(新数组的长度):这个方法就是针对 StringBuilder 类中的 value数组进行扩容的(此为append方法追加字符串针对value数组扩容最为核心的逻辑);
-
- 第一句代码就是 对数组进行扩容的操作 StringBuilder的 旧 value数组长度 左移 1位 + 2: 那就是 (20 << 1) +2 = 42 ,(42 是 即将扩容的新数组的长度)。
- if 语句是 ,newCapacity(42) - minCapacity(26) < 0 ; false里面的语句不执行。(满足这个条件 只有当 之前的 value数组中使用过的空间个数(StringBuilder中使用count进行计数 ) + 要追加的字符串长度 大于 value数组长度 左移 1位 + 2)的时候,那么新数组的长度就是 之前value数组中使用过的空间个数(StringBuilder中使用count进行计数 ) + 要追加的字符串长度 的 数。
- return (nexCapacity <= 0 || MAX_ARRAY_SIZE - newCapactity < 0)? hugnCapacity(minCapacity) : newCapacity;
- (nexCapacity <= 0 || MAX_ARRAY_SIZE - newCapactity < 0)
- 只有当新数组的长度 小数等于 0 时 或者 Integer的最大值((2^31-1)-8)- 新数组的长度 小于0时:如果满足了这个条件,说明新数组的长度已经是 巨大的数组容量了 。执行 hugnCapacity(minCapacity) :
- 第一个if:如果新数组的长度大于 Integer的最大表示范围,则抛出 内存溢出。
- 第二个retrun,即新数组的长度大于 Integer.MAX_VALUE - 8 (满足这个条件就只有中间这八种情况);如果不是则返回最大数组长度,是则返回 中间这八种情况 的数 为新数组长度。
- 执行完 new Capacity(minimunCapacity)方法后执行,Arrays.copyOf(value,42)
-
- char[] copy = new char[newLength];创建了一个长度为 42 的数组。
- System.arraycopy():
- original(源数组):指的是当前的 value数组,也就是AbstractStringBuilder中的value数组。
- srcPos(源数组起始索引):从什么索引处开始 复制。0
- copy(目标数组):即创建好的新数组。长度为 42 。
- destPos(目标数组中的元素起始索引):即从新数组的 0索引开始复制元素。
- Math.min (要从源数组中复制的元素个数): Math.min(源数组的长度(20),新数组的长度(42)),即求出两者之间的最小值(20)。即要从源数组中复制 20个元素,换言之就是全部都复制到 copy 新数组中(如果说除开了旧的 value数组中的默认值的话,实际上 复制的 元素也就才15个)。 新数组长度为 42 个长度,实际使用了 20 个长度,剩余 22 个长度 未使用。(如果说 除开复制的 旧的value数组中的默认值的话,实际上使用了 15个长度,剩余27个长度未使用)。
- 当执行完数组扩容的拷贝后:又回到 value = Arrays.copyOf(value,new Capacity(mininumCapycity));
- 这句代码就是一个赋值操作。此时 value指向的之前的 旧的 value数组就不再指向了!!,而是指向了扩容后的 新数组(长度为42)并且新数组中已经扩容 并拷贝完 旧的value数组中所有的元素!
- 数组扩容完之后,再次复制 "pqrsuvwxyz"字符串到 扩容后的新数组中。
- 进行数组的拷贝:
- value(源数组):指的是当前的 字符串对象(pqrstuvwxyz),也就是AbstractStringBuilder中的str对象。
- srcBegin(源数组起始索引):从什么索引处开始 复制。 str.getChars(传入进来的是 0)。
- dst(目标数组):指的就是StringBuilder类中的 扩容后的 value新数组,(AbstractStringBuilder父类中value数组),指的就是 sb 长度为42的扩容后的value新数组 。
- dstBegin(目标数组中的元素起始索引):从目标数组的什么位置开始复制元素。str.getChars(传入进来是count)。第三次调用该方法,count的值为4 + 11 。从第15个索引开始复制元素。
- srcEnd - srcBegin (要从源数组中复制的元素个数): str.getChars(传入进来的是len,即字符串对象的长度),srcEnd(11) - srcBegin(0) ,要从源数组中复制 11 个元素,换言之就是全部都复制到 dst 目标数组中。
- System.arraycopy 方法是个本地方法,由Java以外的语言 (C 或者 C++)实现:
- count += len;(执行完第三次 数组复制之后,count的值为 当前的len + count,第三次复制完 count 的值为 15 + 11):
- return this:返回当前对象,也就是 sb:,可以看到在StringBuilder 的append方法中没有做任何接收,但是它这里又 return this。如果是使用的是StringBuilder(String str)的方式创建StringBuilder的对象的时候,第一次返回到构造器调用 append 的地方。但是这里又没有做任何接收。。如果是使用 StringBuilder 的对象调用append方法,retrun this返回给调用者,返回的就是当前 的 sb对象。,调用者可以接收也可以不用接收,(需要说明的:此时sb的对象中的 value数组已经有了26个元素。),不用接收继续调用 StringBuilder 的append方法,依然是当前对象再调用,这就形成了 链式调用(编程)。
- 当完成了 字符串的可变操作后,转为 String 类型的时候。调用 toString方法。即可:
- StringBuilder的重写后AbstractStringBuilder抽闲父类的toString方法:
- 这里StringBuilder重写后的toString方法调用了 String类构造器。
- value:指的就是 StringBuilder里面的 扩容后的新数组 (长度为 42)
- offset:偏移量为 0
- count:指的就是StringBuilder里面的扩容后的新数组 有多个长度被使用了。显而易见,当追加完所有的字符串的时候,count的实际计数值为 26。
- 这里StringBuilder重写后的toString方法调用了 String类构造器。
- String构造器源码解读:
-
public String(char value[], int offset, int count) { // 如果偏移量小于0 时 if (offset < 0) { throw new StringIndexOutOfBoundsException(offset); } // 如果count(value数组中使用的长度)小于 等于0 时 if (count <= 0) { if (count < 0) { throw new StringIndexOutOfBoundsException(count); } if (offset <= value.length) { this.value = "".value; return; } } // Note: offset or count might be near -1>>>1. if (offset > value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); } this.value = Arrays.copyOfRange(value, offset, offset+count); }
-
public static char[] copyOfRange(char[] original, int from, int to) { // newLength = 26 - 0 int newLength = to - from; if (newLength < 0) throw new IllegalArgumentException(from + " > " + to); // copyu = new char[26] char[] copy = new char[newLength]; // 拷贝数组 /* orignal(源数组): 当前StringBuilder传入进来的 value数组 from(源数组起始索引):从什么索引处开始复制元素 copy(目标数组):长度为 26 0(从目标数组的什么索引开始复制元素):0索引 Math.min(original.length - from, newLength): (从源数组中复制多少个元素): original.lenth(42) - from(0) newLeagth(26) 从源数组中复制 26个元素到目标数组中 换言之就是把value数组中已经使用了长度 存在了的全部元素(26个) 复制到目标数组中! */ System.arraycopy(original, from, copy, 0, Math.min(original.length - from, newLength)); return copy; }
-
当执行完 Arrays.copyOfRange 后,就已经初始化完 String 的 value 数组了。使用String的 对象引用 接收 使用即可。
-
StringBuilder转为String的其他方式,其实也就是使用String类的其他构造器:
- 满足条件执行 Arrays.copyOf(value,new Capacity(mininumCapacity)),方法。
链式调用(链式编程)的理解:
- 第一种情况:像StringBuilder类或者是StringBuffer类 它们返回的就是对象本身,于是可以一直链式的调用本类中的实例方法一直调用下去。
- 第二种情况:也称之为匿名对象的链式编程。即不接收方法的返回值(这个返回值必须是引用类型的对象!),然后继续使用方法的返回值调用返回值对象的类中的实例方法一直调用下去。只是为了得到最终的结果(就好比 坐地铁一样,会经过很多地铁站,但是没有达到自己想去的地铁站时就不会下地铁。):
- 比如:
class A{ public static void main(String[] args) { // 链式调用 (只是需要最终的结果) 1: A a = new A(); String replace = a.aInvoke().bInvoke().cInvoke().dInvoke().eInvoke().replace(",aaa", ",yyy"); System.out.println(replace); // 链式调用 (只是需要最终的结果) 2: ArrayList<String> list = a.aInvoke().bInvoke().cInvoke().getList(); list.add("ele1"); list.add("ele2"); list.forEach(System.out::println); // 当方法的返回值是 八种基本数据类型 + void 的时候,就不能再链式调用下去,结束了链式调用 a.aInvoke().bInvoke().cInvoke().dInvoke().eInvoke1(); } public B aInvoke(){ return new B(); } } class B{ public C bInvoke(){ return new C(); } } class C{ public D cInvoke(){ return new D(); } } class D{ private ArrayList<String> list = new ArrayList<>(); public E dInvoke(){ return new E(); } public ArrayList<String> getList(){ return this.list; } } class E{ public String eInvoke(){ return "链式调用,aaa"; } public void eInvoke1(){ System.out.println("当方法的返回值是基本类型 + void 的时候就不能继续的链式编程下去了!"); } }
test:
链式编程的优点:代码简洁,不臃肿。执行效率高。
- 链式编程的缺点:可读性差(这个因人而异吧,我个人觉得可读性就很好),链式调用中间链到的对象只能使用一次。故一次链式只能调用对象中的一个实例方法。不能一次链式调用多个实例方法。除非 数据类型 对象名 接收对象就可多次调用对象的类中的实例方法及实例变量 。
- 匿名对象(即不使用 数据类型 对象名 接收对象的引用地址的 对象 )和链式编程和三元运算符和匿名内部类(即不创建类就实现 接口 or 继承 类的 局部内部类(匿名内部类既是类同时也是对象!)) 的优点都是代码简洁,不臃肿,执行效率高。缺点是都只能使用一次,不能够重复使用。
StringBuilder类中常用的API:
StringBuilder stringBuilder = new StringBuilder("我向你奔赴而来,你就是星辰大海。");
// StringBuilder append(): 新增(在原有的字符串的基础上追加字符串)
stringBuilder.append("哦呜呜呜呜").append("~~~~~~").append(123456).append(true).append(3.1415926F);
System.out.println(stringBuilder); // 我向你奔赴而来,你就是星辰大海。哦呜呜呜呜~~~~~~123456true3.1415925
// int length(): 获取字符串长度
System.out.println(stringBuilder.length()); // 46
//StringBuilder delete(int start,int end):删除
// (删除索引位置在 [start,end) 上的子串(字符)。[start,end) (左闭右开,即左边包含,右边不包含))。
stringBuilder.delete(27, 37);
System.out.println(stringBuilder); // 我向你奔赴而来,你就是星辰大海。哦呜呜呜呜~~~~~~3.1415925
//StringBuilder deleteCharAt(int index): (删除指定索引上的子串(字符))
stringBuilder.deleteCharAt(27);
System.out.println(stringBuilder); // 我向你奔赴而来,你就是星辰大海。哦呜呜呜呜~~~~~~.1415925
// StringBuilder insert(int offset,CharSequence seq):修改(插入),
// insert方法有很多重载的方法!
// 在指定索引处 插入一个字符或多个字符串
StringBuffer sbf = new StringBuffer("会不会我们的爱,(insert new String)!!!");
stringBuilder.insert(27, sbf);
System.out.println(stringBuilder); // 我向你奔赴而来,你就是星辰大海。哦呜呜呜呜~~~~~~会不会我们的爱,(insert new String)!!!.1415925
// StringBuilder replace(int start,int end,String str): 修改(替换),
// 从索引位置start [start,end) 到end索引位置的子串替换为新的子串。
stringBuilder.replace(35, stringBuilder.length(), "像星辰守护大海");
System.out.println(stringBuilder); // 我向你奔赴而来,你就是星辰大海。哦呜呜呜呜~~~~~~会不会我们的爱,像星辰守护大海
// vold setLength(int newLength):设置StringBuilder 字符串的新长度。
stringBuilder.setLength(stringBuilder.length() + 1); // stringBuilder.length() + 1
// void setCharAt(int index): 修改(替换),替换指定索引上的一个子串(字符)
stringBuilder.setCharAt(stringBuilder.length() - 1, '!');
System.out.println(stringBuilder); // 我向你奔赴而来,你就是星辰大海。哦呜呜呜呜~~~~~~会不会我们的爱,像星辰守护大海!
// char charAt(int index):查询(返回指定索引上面的一个子串(字符))
int count1 = 0;
int count2 = 0;
int count3 = 0;
int count4 = 0;
for (int i = 0; i < stringBuilder.length(); i++) {
Character character = stringBuilder.charAt(i);
switch (character) {
case '星':
++count1;
break;
case '辰':
++count2;
break;
case '大':
++count3;
break;
case '海':
++count4;
break;
default:
System.out.print(character + "\t");
if (i == stringBuilder.length() -1){
System.out.print('\n');
}
break;
}
}
String mark = "\"星\" 出现了 %d,次。\"辰\" 出现了 %d,次。\"大\" 出现了 %d,次。" +
"\"海\" 出现了 %d,次。";
// "星" 出现了 2,次。"辰" 出现了 2,次。"大" 出现了 2,次。"海" 出现了 2,次。
System.out.println(String.format(mark, count1, count2, count3, count4));
// int indexOf(String str):返回指定子串第一次在字符串中的索引
System.out.println(stringBuilder.indexOf("星辰")); // 11
// int indexOf(String str,int fromIndex):返回指定子串第一次在字符串中的索引,从指定的索引开始
System.out.println(stringBuilder.indexOf("星辰",20)); // 36
// lastIndexOf(String str): 返回指定子串在字符串中最右边出现的索引
System.out.println(stringBuilder.lastIndexOf("大海")); // 40
// lastIndexOf(String str,int fromIndex): 返回指定子串在字符串中最右边出现的索引,从指定的索引开始
System.out.println(stringBuilder.lastIndexOf("大海",30)); // 13
// CharSequence subSequence(int start,int end):
// 返回从开始索引到 [start,end) 结束索引的一个新的字符串,该新的字符串属于该字符串的子串
CharSequence charSequence = stringBuilder.subSequence(0, 16);
System.out.println(charSequence); // 我向你奔赴而来,你就是星辰大海。
// String subString(int start):
// 返回一个从开始索引到[start,sb.length())最后的新的字符串,该新的字符串属于该字符串的子串
System.out.println(stringBuilder.substring(8)); // 你就是星辰大海。哦呜呜呜呜~~~~~~会不会我们的爱,像星辰守护大海!
// String sbuString(int start,int end):
// 返回从开始索引到 [start,end) 结束索引的一个新的字符串,该新的字符串属于该字符串的子串
System.out.println(stringBuilder.substring(16, 28)); // 哦呜呜呜呜~~~~~~会
//StringBuilder reverse():反转:将StringBuilder中的字符串以相反的顺序替换
stringBuilder.reverse();
System.out.println(stringBuilder); // !海大护守辰星像,爱的们我会不会~~~~~~呜呜呜呜哦。海大辰星是就你,来而赴奔你向我
// int capacity(): 返回当前数组容量
int capacity = stringBuilder.capacity();
System.out.println(capacity);// 66
test:
(面试常问)字符串的可变与不可变的区别是什么:
- 不可变字符串:String
- String 保存的是字符串常量。里面的值不可修改。每次String类的更新 实际上就是更新了字符串的地址:这是由 String类中的 value属性决定的!,private 私有的 final 不可更改的 char value[] 数组,用于字符序列的存储。说明(String的 value数组的地址不可发生改变!原则上 String的 value数组中 元素 可以发生改变,但实际上 因为被private 修饰,由于封装的特性,Java也并没有提供能够让调用者改变 String 的value数组中的数组的元素的添加,删除 的API,所以String类中value数组只是用来存储 字符串,并不能够被改变。称之为不可变字符串。)(但是Java提供了让调用者 修改(替换)String的的value数组中的元素的API,比如replace(),replaceAll(),replaceFirst(),但是 这修改后的value数组是一个新的 字符串,并不是原来的字符串,这对原来的字符串没有任何影响!如果拿替换前的变量来接收替换后的字符串对象时。只是 变量引用指向了其他的 字符串的地址了而已,此时,字符串常量池中有替换前和替换后的两个字符串 )
- 说明:String 的字符串是存在于 字符串常量池中的,字符串常量池中有个特点就是,如果字符串常量池中已经有了要使用的 字符串,在运行程序时 就直接引用使用即可。如果字符串常量池中没有要使用的字符串,那么运行程序时 会先创建字符串对象到字符串常量池中,然后再引用 使用。
- 不管是 直接赋值的方式还是通过new String构造器的方式初始化字符串对象,只要字符串常量池中有要使用的字符串,就都会直接使用字符串常量池中的字符串。没有则先创建再引用使用。
- 直接赋值 和 通过 new String构造器方法初始化字符串对象的方式不同之处在于,直接赋值的方式的 对象引用名 直接引用的是 字符串在常量池中的地址,省略了加载String.class 字节码文件的操作。
- 而通过new String构造器初始化字符串对象的方式则是 加载了String.class字节码文件,初始化了String类中value数组,而再通过value数组指向了字符串常量池中的字符串地址。通过 new String构造器初始化字符串对象的方式的 对象引用名 指向的是 对象 在堆中的内存地址!,如果字符串常量池中有要使用的字符串,那么对象中的value数组则可以直接指向字符串常量池的字符串的地址。如果字符串常量池中没有要使用的字符串,则是先创建字符串放入到字符串常量池中,然后value数组再指向字符串常量池中的地址。对象引用名并没有直接指向字符串常量池中字符串的地址!!这就是直接赋值和通过new String构造器它们之间的最大区别!字符串常量池是个线程共享的区域,也可以被多个对象所引用。
- 其实不管是直接赋值还是通过new String构造器的方式初始化字符串,它们都是在给 String类中的value数组初始化字符元素。只是直接赋值的方式初始化字符串的方式是隐式的,省略了加载String.class字节码文件。而new Stirng构造器的方式则是显式的,加载了String.class字节码文件。在 debug 的时候就会发现 用直接赋值的方式想进入源码也不会进入到String 类的源码中去,String类中value数组的值也已经初始化完了。这就说明直接赋值的方式没有加载String.class字节码文件,并且字符串是已经在字符串常量池中创建完毕了!。而new String构造器的方式初始化字符串。则是加载了String.class字节码文件。debug的时候也会进入到String类的源码中去!
- 其实不管是直接赋值还是通过 new String构造器创建对象的方式,实际上操作字符串 就等同于 在操作 value 这个char数组!只是两者 有些微的区别,对象引用名指向的地址不一样而已。 不管是直接赋值还是通过 new Stirng构造器创建对象的方式,只要它们操作的是 同一个字符串,那么这个字符串在字符串常量池就只有一个,也就是说 操作的同一个字符串!不会因为 创建对象 的 方式不同而导致字符串常量池中有两个一模一样的字符串!
- String类的不可变的表现形式在于:如上图所示:显而易见,Stirng类的不可变指的是 在对象引用的 地址不变的情况下,如果想把 abcdef 字符串改变成 abcdefgjik字符串 是不可能的,需要说明的是:就算用String类的 replace()、replaceAll()、replaceFirst()。这三个API来操作的话,也是不可能实现 在 String对象引用的地址 不变的情况下改变字符串的内容的!因为这三个API的底层都使用了StringBuilder类,而StringBuilder类的toString方法底层 就是 通过 new String构造器的方式才转换为 String类型的。!所以字符串常量池中只有 abcdef 这一个字符串,另外一个通过 replace()、replaceAll()、replaceFirst() 改变为 abcdefghijk 的字符串 在 String中的 value数组中。所以才 为什么说 操作字符串就等同于操作 String类中value 数组。
- 可变字符串:StringBuilder 、StringBuffer
- StringBuilder 和 StringBuffer 里面保存的是字符串变量(添加的每个字符串都是一个字符串对象)。value里面的值是可以更改的。每次StringBuilder 和 StringBuffer 的更新 实际上就是更新的value数组里面的内容。不用每次都更新 对象引用的地址。对比于String来说,这两个效率都比String高 !这个也是由 StringBuilder 和 StringBuffer 的抽象父类 AbstractStringBuilder 类中的 value属性决定的!,权限修饰符为 缺省,说明子类和父类在同一个包下的情况下可以直接继承 value 属性。而StringBuilder 和 StringBuffer 和 它们的抽象父类 AbstractStringBuilder 都在 java.lang包下。value数组 没有使用final修饰,说明 value 数组的地址和元素都可以改变!
- StringBuilder 和 StringBuffer 的可变表现形式在于,比如: ,如上图所示:如果想把 StringBuilder 的可变 字符串 abcdef 改变成 abcdefghijk 是可以的,直接使用StringBuilder 的 append 方法即可追加字符串。StringBuilder 的可变指的是 在 StringBuilder sb = new StringBuilder(); sb 这个 对象引用的 对象地址不变的情况下它的字符串内容是可变的。其根本原因就是StringBuilder 类 内部提供了往 value 数组中添加 元素的 append方法。value数组的地址是可变的(StringBuilder是为了value数组的扩容)元素是可变的(StringBuilder提供了 增,删,改 的API。)
- 说明:StringBuilder和StringBuffer的append方法在追加、insert方法、replace方法 修改(插入)。字符串的时候,首先要先在字符串常量池中创建字符串后再追加。因为append方法的和insert方法和replace方法,底层核心就是 将String的vlaue数组中的字符 拷贝到 StringBuilder 或者StringBuffer 的value数组中去,所以必须要先创建字符串对象到字符串常量池之后才能追加,修改 字符串。 StringBuilder和StringBuffer的 append方法、insert方法、replace方法 底层就是实现了 它们的 value 数组的扩容机制。所以它们的长度是可变的,元素是可变的。(而delete()、deleteCharAt()也只是删除StringBuilder和StringBuffer中value数组的字符元素后再toString调用String类的构造器 转成String类型后返回。)
- insert方法核心源码:(以其中一个为例),实现字符串的插入的本质依然还是 通过 System.arraycopy 方法拷贝数组来实现的。
- replace方法核心源码:,字符串的子串替换的本质依然是 通过 System.arraycopy 方法拷贝数组来实现的
- delete方法核心源码:,字符串的删除子串的本质依然是通过 System.arraycopy 方法拷贝数组来实现的。本质是StringBuilder中的value数组发生了改变(但是vlaue数组的实际长度没变!!!)。-> StringBuilder内部再toString 实际上就是调用了 String类的构造器,重新创建了一个字符串对象。 如下模拟代码所示:
-
char[] chars = {'a','b','c','d','e','f','g'}; int start = 1; int end = 5; int len = end - start; int count = chars.length; if (len > 0) { // 参数1:源数组 chars // 参数2:从源数组起始索引开始复制 5 // 参数3:目标数组 chars // 参数4:从目标数组的起始索引开始复制 1 // 参数4:从源数组中复制多少个 2 System.arraycopy(chars,start + len,chars,start,count - end); count -= len; } // chars的数组的元素发生了变化: // 会发现原本数组中的[start,end) 索引处的元素不见了, // afgdefg System.out.print("拷贝后数组:"); for (char aChar : chars) { System.out.print( aChar); } System.out.println(); String string = new String(chars, 0, count); System.out.println("删除 chars 数组的 [1,5) 索引的元素后的 新数组 : " + string);
test:
-
说明:(insert方法、replace方法、delete方法它们的实现 都与 value 属性和 count 属性有关,而count这个属性是最为关键的一个属性!它并不是value数组真实的长度,而是记录着数组中有多个长度的空间被使用!,也可理解为 count 就是 value数组的一个虚拟长度,而StringBuilder的length()方法,(这个方法在StringBuilder 的 AbstractStringBuilder父类中 ,)其这个方法 返回的就是 count 的 值)
StringBuffer vs StringBuffer vs String 的执行效率 test:
public class StringAndStringBuilderAndStringBufferExecuteTimeTest {
public static void main(String[] args) {
AbstractCalcTimeTemplate sub = new SubCalcTimeTemp();
int loopTime = 80000;
String str = "";
StringBuilder builder = new StringBuilder(str);
StringBuffer buffer = new StringBuffer(str);
sub.calculateExecuteTime(loopTime, builder);
sub.calculateExecuteTime(loopTime, buffer);
sub.calculateExecuteTime(loopTime, str);
sub.calculateExecuteTime(loopTime,new CharArray(null,0,0,false));
}
}
public abstract class AbstractCalcTimeTemplate {
protected abstract void stringExecute(int loopTime, String str);
protected abstract void stringBuilderExecute(int loopTime, StringBuilder builder);
protected abstract void stringBufferExecute(int loopTime, StringBuffer buffer);
public void calculateExecuteTime(int loopTime, CharSequence seq) {
long startTime = System.currentTimeMillis();
if (seq instanceof String) {
this.stringExecute(loopTime, (String) seq);
} else if (seq instanceof StringBuilder) {
this.stringBuilderExecute(loopTime, (StringBuilder) seq);
} else if (seq instanceof StringBuffer) {
this.stringBufferExecute(loopTime, (StringBuffer) seq);
} else {
throw new RuntimeException("please afferent " +
"String or StringBuilder or StringBuffer" +
" type instance");
}
long endTime = System.currentTimeMillis();
printExecuteMillis(seq,endTime - startTime);
}
private void printExecuteMillis(CharSequence seq,long millis){
System.out.println(seq.getClass().getSimpleName() + ", 的执行时间是 :" + millis+ " ms ");
}
}
public class SubCalcTimeTemp extends AbstractCalcTimeTemplate {
@Override
protected void stringExecute(int loopTime, String str) {
for (int i = 0; i < loopTime; i++) {
str += i;
}
}
@Override
protected void stringBuilderExecute(int loopTime, StringBuilder builder) {
for (int i = 0; i < loopTime; i++) {
builder.append(i);
}
}
@Override
protected void stringBufferExecute(int loopTime, StringBuffer buffer) {
for (int i = 0; i < loopTime; i++) {
buffer.append(i);
}
}
}
test:
由此可见:
StringBuilder的执行效率是最快的。StringBuffer 次之 ,(需要说明的是:StringBuilder 并不是绝对比 StringBuffer 的效率快,只是在一般情况下都比StringBuffer快,这和电脑的内存开销,硬件配置 是否是单线程的程序 是有关系的!)String最慢。
执行效率:
StringBuilder 快于 StringBuffer 快于 String
为什么StringBuilder和StringBuffer都要比 String 的执行效率快?
因为 StringBuilder 和 StringBuffer 都是可变长的字符串,它们实际上操作的是 AbstractStringBuilder 父类中的value数组。而AbstractStringBuilder父类中的 两个属性 是使 字符串可变长的主要原因:,vlaue数组 访问修饰符 是缺省的,说明和AbstractStringBuilder类在同一个包中的子类可以直接继承使用。而StringBuilder和StringBuffer和AbstractStringBuilder父类都在java.lang 包下。 value 没有使用final修饰,说明value的地址可以改变,也就是底层实现了 value数组的扩容 。而且StringBuilder和StringBuffer提供了对 value数组中的元素 增、改 的API,所以它是可变长的字符串,所以执行效率高。并且每次往 StringBuilder和StringBuffer中的value数组 增、改 字符串对象的时候,都会先在字符串常量池中 创建字符串。然后再在StringBuilder和StringBuffer的 vlaue 数组中 增(append()),改(insert()、replace())字符串(实际上操作的就是String类中的value数组)。
(往往在追加或者修改StringBuilder 和 StringBuffer中的字符串的时候 都是以 直接声明的方式 赋值 得到字符串对象,好处在于不用加载String.class字节码文件,在运行程序的时候加载运行类的时候,字符串就已经在池中创建好了!执行效率高效。不用 对象引用 的字符串就是 匿名字符串对象。
(很少 有用new String构造器的方式去追加、修改字符串。这种方式是要在执行new String构造器这句代码的时候,还要加载String.class,先初始化value数组再通过 value 属性指向字符串常量池中已经有了的字符串(如果没有则先在常量池中创建字符串,然后再通过value数组指向字符串),相对于直接赋值的方式来说执行效率低下。)(而delete()、deleteCharAt()也只是删除StringBuilder和StringBuffer中value数组的字符元素后再toString调用String类的构造器 转成String类型后返回。)
(需要说明的是:不管是 增,改、删 StirngBuilder 和StringBuffer 中的字符串时。其本质都是对value数组进行数组的拷贝,在对value数组中的元素进行字符的赋值操作。其核心就是 通过 System.arraycopy 这个 AP I调用本地方法 来实现的!它们的源码已经说明一切 ),count这个极其重要的属性则是决定了StirngBuilder 和StringBuffer中的 value数组在什么时候扩容,StirngBuilder 和StringBuffer中的 value数组中的怎么进行字符的赋值操作。转成字符串时, StringBuilder和StringBuffer中value数组中的字符元素 拷贝到 String的 value新数组 拷贝中的时候 拷贝到 StringBuilder和StringBuffer中value数组中的那个索引位置上的元素结束!。所以StringBuilder和StringBuffer的执行效率高。它们的 对象引用 引用的是 StringBuilder和StringBuffer对象 在堆中的空间地址。它们实际上操作的是value数组,并不是像 String一样 直接操作的是 字符串常量池中的字符串!
为什么String执行效率这么慢?
因为String 是不可变长的字符串。,它的value数组是 private(私有的) final(不可再更改的)所修饰 ,说明 String 的value数组不能指向其他 char数组的,也就是说明,String的value数组的长度是 根据字符串 字符的个数来定死了的。也就意味着 value数组不能扩容,也就是长度不可变。原则上 value 数组中的字符元素是可以改变的,但是String底层并没有提供 改变vlaue数组中元素的API。也就字符元素不可变!哪些 String 类的 replace() 、replaceAll()、 replaceFirst(),看似好像是可以改变字符串的API,实际上底层都有一句代码: 就是调用 new String构造器的 方式加载String.class 在堆中开辟一块创建的对象的空间 初始化String类的 value数组 初始化字符串对象 ,(而接收对象的 对象引用名 指向的是 对象在堆中的空间地址,并不是字符串在池中的地址!(只有直接赋值在字符串常量池中创建字符串的方式,这个对象引用名 指向的才是 池中的 地址))使用 replace() 、replaceAll()、 replaceFirst() 方法 产生后的字符串和原本的字符串是两个字符串。在内存中 存在两个不同的字符串,一个在字符串常量池中,一个在value数组中(value数组在堆中)!!
比如:String s = "a"; // 在池中创建了一个字符串并且 s对象引用名 直接指向的是 a 字符串在 池中的地址 ,比如 "a" 的地址是 0x100 ,即 s -> 0x100
s += "b"; // 此时 原来的 "a" 字符串对象的在池中的引用 s 这个对象引用名 已经断掉 不再引用 "a" 字符串 了。 现在又产生了一个字符串 s + "b" (也就是 "ab")(需要说明的是, 只要有变量参与到 字符串的拼接运算 中的时候,运行器会默认初始化StringBuilder类,调用StringBuilder类的append方法在 s 之前引用的字符串之上 追加了一个 "b" 字符串 ,然后再调用StringBuilder的toString方法(toString方法底层是调用 String的构造器完成的字符串的转换),将 “ab” 转换成为 String 类型的对象,此时"ab"这个字符串在 当前这个对象的 value数组中,并没有存在与字符串常量池中!)。此时, s 指向的 就是 新的对象的在堆中的地址 ,此时 s 指向的是 字符串对象在堆中的地址。比如 字符串对象在堆中的地址是0xa234 , 即 s -> 0xa234 -> value 数组对象 引用。(需要说明的是,不管是直接赋值的方式 创建字符串对象(字符串常量) 对象引用名 指向池中地址 还是通过 new String构造器的方式初始化value数组初始化 字符串对象的方式 对象引用 指向堆中的 对象地址也好 ,它们在操作字符串的时候,都是操作的 value数组,换句话就是要通过 vlaue数组 才能操作 内存中的 的字符串对象(字符串常量),而使用 == 比较两个字符串的时候,就比较的是 对象引用名 的 直接引用地址,直接赋值的方式引用的池中地址,new String构造器的方式是 引用的堆中对象地址!)如果多次执行这些改变字符串内容的操作,会导致大量的 副本字符串对象 存留在内存中,降低效率,如果这样的操作放到循环中的话,会极大影响程序的性能。所以 ,当对String 做大量修改时,不要使用String。应该使用 StringBuilder 或者 StringBuffer。
关于通过直接赋值和通过 new String(String original)先初始化value数组再通过value属性指向池中字符串对象(字符串常量)的创建字符串对象的方式的结论:
- 直接赋值的方式:
- 是当 运行程序的时候,加载运行类的时候,此时 字符串对象(字符串常量),就已经在字符串常量池中创建好了。并且这种直接赋值的方式没有加载String.class字节码文件。在使用这种方式的创建的 字符串对象的时候,即执行到直接赋值的这句代码的时候,首先 会先(加载String.class,.class字节码文件只会加载一次!)初始化String类中的value数组并且 将字符串对象(字符串常量) 装载到 String 类的value数组中去,然后再使用。只是这个过程是隐式的,看不见而已。(操作字符串,就等同于 操作 String 类中的value数组!)同时,直接赋值的方式的 对象引用名 指向的是 池中 字符串对象(字符串常量)的地址。即 对象引用名 -> 字符串对象(字符串常量)池中地址。
- new String(String original) 先初始化vlaue数组在通过value属性指向池中字符串对象(字符串常量)创建字符串对象的方式:
- 是当运行程序的时候,加载完运行类的时候,执行代码到 new String(String orginal)这句代码时。 new String(String original)先初始化value数组(其实此时这个对象的 value 的地址就是 字符串对象装载的 value 的地址!)再通过value属性指向池中字符串对象(字符串常量),首先会 先(加载String.class,.class字节码文件只会加载一次!)初始化String类中value数组并且再 通过vlaue属性指向池中 字符串对象(字符串常量)的 池中的地址,此时value数组中已经装载好了字符串对象(字符串常量)这个过程是显式的。同时,new String(String original)这种方式的对象引用名 指向的是 对象在堆中开辟的空间地址。即 对象引用名 -> 对象堆中地址 -> 对象的value属性 -> 字符串对象(字符串常量)池中地址。
-
注意事项:
- 只有new String(String original) 构造器中的实参 传入的是 用 "" 引起来的或者是基本类型的字面常量拼接的(比如:123+"" )的字符串对象(常量)的引用才是:对象引用名 -> 对象堆中地址 -> 对象的value属性 -> 字符串对象(字符串常量)池中地址。其他非 "" 引起来的 字符串对象 的字符串的引用则是:对象引用名 -> 对象堆中地址 -> 对象的value属性地址(数组对象地址)
- 在使用字符串时 实际上操作的就是String 类中的 value 数组!字符串常量池中的相同的字符串对象(字符串常量) 的value数组的地址是一样的。因为在 字符串常量池中的 相同的字符串对象(字符串常量) 只会装载到String的 vlaue数组中一次!,换言之就是字符串常量池中不会出现 重复的相同字符串对象(字符串常量),一个字符串对象(字符串常量)在池中 有且 仅有一个 字符串对象(字符串常量)这是由 字符串常量池的设计决定好了的!。
- 在debug时 如果是直接赋值的方式 想进入到源码中去的话,并不会进入到String类的源码中去。证明 直接赋值的方式没有加载String.class,如果是new Stirng构造器的方式,想要进入到源码去中的话,则会进入到String类的源码中,并且会走完super(); 初始化value数组 创建字符串对象的全过程。
- == 运算符比较的是 对象引用名 引用的 内存地址,String 重写父类后的 equals方法比较的是字符串具体的内容!
-
匿名字符串对象的使用:
- new StringBuilder("abc");
- 其中StringBuilder构造器中的 这个 "abc" 这个字符串对象(字符串常量) 指向的是 池中的地址。"abc" 并没有使用 数据类型 对象引用名 来接收。这个 "abc" 就是所谓的匿名字符串对象。
- new StringBuilder("abc");
- 这个初始化出来的StringBuilder 的对象 并没有用 数据类型 对象引用名 接收,所以new StringBuilder("abc");也是一个匿名对象。它指向的是 当前初始化 的 这个对象在 堆中的地址。可以调用StringBuilder类中的实例方法。
- new StringBuilder(s) 和 new StringBuilder("abc");
- 其中 abc 和 s 指向的地址 同为 池中的地址。所以它们不管是 使用 == 比较还是使用 equals(); 方法来比较都是相等的!它们的 value 数组的地址也是相同的!
- new StringBuilder(s);
- 这个初始化出来的 StringBuilder 的对象,并没有用 数据类型 对象引用名 接收,所以 new StringBuilder(s);也是一个匿名对象,它指向的是 当前 初始化 的这个 对象在 堆中的地址。可以直接调用StringBuilder 类中实例方法。
- .append("efg");
- 其中 append("efg"); 中的"efg" 这个字符串对象(字符串常量)指向的也是池中的地址。"efg" 这个字符串对象(字符串常量)也是属于所谓的 匿名字符串对象。这里 "efg" 会装载到 String 的value数组中,然后再给 append 方法使用。(说明:"efg" 在加载运行类的时候就已经在字符串常量池中创建完毕了的! )
- new String("abc");
- 其中 String 构造器中的 "abc"匿名字符串对象 也是指向池中的地址。abc 和 s 指向的地址 同为 池中的地址。它们装载后的value数组的地址也是相同的!
- new String("abc") ;
- 这个初始化出来的 String 的对象 也没有 用 数据类型 对象引用名 来接收,所以 new String("abc") ; 也是一个匿名对象,它指向的 当前 初始化的这个 对象在 堆中的地址。而这个匿名对象的value属性指向的是 "abc" 在字符串常量池中的地址!这个匿名对象也可以直接调用 String 类中的实例方法。
- new String(new char[]{'a','b','c'});
- 其中 构造器中的 new char[]{'a','b','c'} 这个匿名数组对象的地址 和 "abc" 字符串装载后的 value 数组的地址是不一样的,,这个匿名数组对象 是 通过 new 关键字在堆中开辟空间创建出来的。虽然它们的地址 是不一样的,但是它们的当前初始化的对象的 value 数组和 new char[]{'a','b','c'} 匿名数组对象 中的每个字符元素 是相同的。
- "abc" 和 new char[]{'a','b','c'} 之间不能使用 == 比较,不可比较的类型,就算使用 equals方法 比较也是 false!这种方式创建的 字符串对象 并不会把 new char[]{'a','b','c'} 复制到 value数组中的字符 转换成字符串 放入到字符串常量池中,它直接操作的 就是 String类中的 value 数组。
- 为了证明 并不会把 new char[]{'a','b','c'} 复制到 value数组中的字符 转换成字符串 放入到字符串常量池中, 为了避免歧义,下面的比较需要说明(可能存在争议):
- ,这里之所以返回true,
- 先说说 intern方法的作用:当调用 intern 方法时,如果字符串常量池中已经包含了一个 等于 此 String对象的字符串
* (用 equals(Object) 方法确定)则返回字符串常量池中的字符串(地址),
* (否则,将此String对象的字符串 添加到字符串常量池中后,再返回 String 对象的 字符串在字符串常量池中的地址,这句话针对的是 new String("字符串常量"); 这种方式创建的字符串对象。) - 是因为字符串常量池中确实 存在 abc 这个字符串在字符串常量池中。所以返回的是abc在池中的地址,如果池中不存在 abc 这个字符串则返回的是 null,比如:,因为池中并不存在 zaq 这个字符串,所以并不会 把 zaq 这个字符串放入到字符串 池中。返回的是 null!(我认为的理解就是如此,可能存在争议!!!)。
- 先说说 intern方法的作用:当调用 intern 方法时,如果字符串常量池中已经包含了一个 等于 此 String对象的字符串
- ,这里之所以返回true,
- 创建 到字符串常量池中的 字符串对象(常量) 只有 用 "" 号引起来的字符串对象(常量) 才放入到 字符串常量池中!!!。
- 不管是以什么样的方式创建的字符串对象。其本质都是操作的是 String 类中的value 数组!
- new String(new char[]{'a','b','c'});
- 这个初始化出来的 String的对象 没有用 数据类型 对象引用名 来接收。new String(new char[]{'a','b','c'}); 也是一个匿名对象,它指向的 当前 初始化的这个 对象在 堆中的地址。也可以直接调用 String 类中的实例方法。
- new String("yhn");
- 其中String构造器中的"yhn" 匿名字符串对象也是指向 池中的地址,在初始化这个字符串对象的时候, 会先去池中查找是否 含有 "yhn" 的字符串对象(常量),如果有则直接通过 把这个 "yhn"字符串 装载的value数组 装载到new String("yhn");对象的 value 数组中,再通过 value 属性指向 池中的地址。如果没有则先在池中创建 字符串对象(常量),再装载再让 value 指向池中的地址 (通俗易懂的说也就是 new String("yhn")这个对象 的value数组 使用的就是 "yhn" 字符串装载后的value 数组(yhn装载的value数组是一样的!)
- new String("yhn");
- 这个初始化出来的 String 的对象 也没有 用 数据类型 对象引用名 来接收,所以 new String("yhn"); 也是一个匿名对象,它指向的 当前 初始化的这个 对象在 堆中的地址。也可以直接调用 String 类中的实例方法。
- new String();
- 这个初始化出来的String 的对象也没有用 数据类型 对象引用名 来接收, 所以 new String();也是一个匿名对象,它指向的 是当前初始化的这个对象在堆中的地址。也可以直接调用 String 类中的实例方法 , new String();匿名对象里面的 value 数组是什么 如下所示:String s1 = new String();
- String s1 = new String();
- 这个初始化出来的 String 的对象 用 对象引用名 来接收了 对象。所以它不是一个匿名对象,s1 就是一个对象名。s1 指向的是 堆中的对象的地址,s1 可以多次调用 String类中的实例方法,这个对象的 value 数组 虽然是初始化了,但是没有长度,也没有元素,如果 池中 有一个 "" 这样的字符串的话(这个 "" 字符串就相当于啥也没有,就开辟了一个空间),这个对象的value数组和 "" 这个字符串装载后的 value数组的地址是一样的:但是每次 构造器中的 "".value数组,都是不一样的。 ,最终会以池中是否已经有 "" 字符串装载的value数组为准。
- s1 = "abc";
- 这个赋值操作,就已经把 s1 的 原本的引用给 断掉 了。本来s1 是指向的是 new String();这个对象 在堆中的 空间地址。但是这里的引用发生了 改变。此时的 s1 直接指向的是 "abc" 字符串对象(常量) 在池中的地址。不再指向 new String();这个对象在堆中的空间地址了。!此时的 s1 如果 和 s 、"abc"、使用 == 来比较 它们的指向地址是相同的。使用 equals比较 内容 也是相同的!
- new StringBuilder("abc");
(面试常问)String和StringBuilder和StringBuffer 有什么区别?:
- StringBuilder 和 StringBuffer 非常的相似。它们均代表可变的字符序列。它们属于同一个抽象父类的子类,都共同的 继承了AbstractStringBuilder 抽象类。实现了相同的父接口。而且它们的API 基本上也是一模一样的。
- StringBuffer 类 和 StringBuffer 类 代表 一个字符序列 可变的字符串(AbstractStringBuilder父类中的 char[] value 属性 和 int count 已经决定了),可以通过 append,insert,reverse,setChartAt,setLength,delete,replace等方法改变其字符串内容。一旦生成了最终的字符串,调用 toString 方法将其转变为 String 类型不可变长的字符串。
- String类 代表一个 字符序列的 不可变字符串(String类中private final char value[] 属性 已经决定了)。即一旦一个 String对象被创建后,这个String对象中的字符序列就不可再改变。直至这个对象被GC回收销毁。
- StringBuilder 于 JDK 1.5 开始才发布的技术,相对于StringBuffer 有 4个版本的更新 。
- StringBuilder:可变字符序列,效率最高,线程不安全。
- (StringBuilder 比 StringBuffer 的效率高的不是绝对的,这和机器的内存开销,硬件配置、是否单线程 有关。StringBuilder 比 String 的效率高 是绝对的!)
- StringBuffer 与 JDK 1.0 开始就已经发布的技术。
- StringBuffer:可变字符序列,效率较高(增、删),线程安全。
- StringBuffer中的API 几乎全部都使用了 synchronized 关键字 实现了线程同步。故使用StringBuffer的API线程是安全的。
- String 于 JDK 1.0 开始就已经发布的技术。
- String:不可变字符序列,效率低下。但是String 的复用率高。线程不安全。
-
String、StringBuilder、StringBuffer 的使用原则:
- 如果字符串需要可变性、即字符串存在大量的增、删、改操作时,一般使用StringBuilder 或者 StringBuffer。
- 如果字符串需要可变性、即字符串存在大量的增、删、改操作时,并在单线程的情况下,使用StringBuilder。
- 如果字符串需要可变性、即字符串存在大量的增、删、改操作时,并在多线程的情况下,使用StringBuffer。
- 如果字符串需要不可变性、即字符串不需要或者说很少增、删、改操作时,并且字符串对象被多个对象引用。不管是在单线程还是在多线程的情况下,使用String。比如配置 mysql 的 URI,微服务的 URL,mysql的用户名、密码、类中 需要String类型的静态常量 等。
下一篇:面向对象之14:开发中的常用类之一: Math、Random、System、Arrays、BigInteger、BigDecimal、Scanner 的使用总结: