理解字符串不可变
字符串是一种不可变对象. 它的内容不可改变.
String 类的内部实现也是基于 char[] 来实现的, 但是这个char[]数组是私有的且被final所修饰的数组,String 类并没有提供 set 方法之类的来修改这个char类型的字符数组.
所以原则上来说字符串是一种不可变的对象,每创造一个新的字符串常量都要在字符串常量池当中重新开辟内存存储.
感受下形如这样的代码:
String str = "hello" ; str = str + " world" ; str += "!!!" ; System.out.println(str); // 执行结果 hello world!!!
形如 += 这样的操作, 表面上好像是在原地址修改了字符串, 其实不是. 内存变化如下:
+= 之后 str 打印的结果的确是变了, 但并不是在str引用第一次指向的对象“hello”的地址上原地发生拼接, 而是每次拼接完成后就要在常量池中开辟临时内存去存储新拼接的临时变量.因为每个字符串都是不可变的,相当于如果按照上述方法的话每次拼接完成后实际上都是一个新的对象,新的对象每次挨个存储在常量池中,str引用最终指向最后一个临时变量.
那么假如我们要拼接100次,例如下面的代码,那么就会在堆内存中的字符串常量池产生99个临时变量,这种方法是不可取的。这样的代码以后尽量不要出现在项目当中
1.public static void main(String[] args) { 2. String str1 = "abc"; 3. for(int i = 0;i <= 100;i++) { 4. //要创造99个临时变量 5. str1 += i; 6. } 7. System.out.println(str1); 7.}
那么对于在循环中进行拼接产生大量临时变量,降低效率的情况,我们改怎样做呢?
此时需要用到StringBuffer和StringBuilder可以来处理在循环过程中拼接的这样一个过程(单线程使用StringBuilder,多线程使用StringBuffer),使用两者共有的append方法进行拼接,append方法的拼接是不会产生临时变量的,后续我们在String与StringBuilder的 区别一栏中会给出详细解答,大家可以直接通过目录进行跳转,观看解答.
到了这里,我们可以了解到,字符串常量因为底部源码实现的问题,它是不可变的,每次所创建的新的字符串原则上是不能在原字符串上进行修改变动的,必须在堆内存上的字符串常量池中创建新的内存来存储变动的字符串,但是java中的反射打破了这一规则,当然后续我们也会仔细去讲解反射,现在我们就来大致了解下反射,以及如何通过反射来打破字符串不可变这一规则:
反射打破字符串不可变
还是来看之前的一段代码:
之前我们想将str1的“Hello”改成"hello"怎么做的呢?
常见办法:借助原字符串, 创建新的字符串
String str = "Hello"; str = "h" + str.substring(1); System.out.println(str); // 执行结果 hello
那么利用反射该怎么做呢?
使用 "反射" 这样的操作可以破坏封装, 访问一个类内部的 private 成员
IDEA 中 ctrl + 左键 跳转到 String 类的定义, 可以看到内部包含了一个 char[] , 保存了字符串的内容.被private所修饰,但是此时String类中并没有提供对这个数组的set方法.
代码如下:
public static void main(String[] args) { String str1 = "abc"; //Class对象 Class c1 = String.class; //getDeclaredField方法可能会抛出NoSuchFieldException异常,需要被捕获 try { // 获取 String 类中的 value 字段. 这个 value 和 String 源码中的 value 是匹配的. Field field = c1.getDeclaredField("value"); // 将这个字段的访问属性设为 true field.setAccessible(true); //get方法可能会抛出IllegalAccessException异常,需要捕获. try { // 把 str1 中的 value 属性获取到. char[] value = (char[]) field.get(str1); //这块打印下获取到的value属性发现是【a,b,c】 System.out.println(Arrays.toString(value)); //打印下修改前的str1的值,为abc System.out.println(str1); // 修改 value 的值 value[0]='G'; //打印修改后的str1的值,为Gbc System.out.println(str1); } catch (IllegalAccessException e) { e.printStackTrace(); } } catch (NoSuchFieldException e) { e.printStackTrace(); } }
字符与字符串
字符串内部包含一个字符数组,String 可以和 char[] 相互转换.
代码示例1:获取指定位置的字符
String str = "hello" ; System.out.println(str.charAt(0)); // 下标从 0 开始 // 执行结果 h System.out.println(str.charAt(10)); // 执行结果如果超出下标范围,产生 StringIndexOutOfBoundsException 异常
代码示例2:将字符数组所有内容变为字符串进行输出
1.char[] value = {'a', 'b', 'c', 'd'}; 2.String str = new String(value); //输出结果为abcd 3.System.out.println(str);
代码示例3: 将字符数组部分内容变为字符串进行输出
1.char[] val = {'a', 'b', 'c', 'd'}; 2.String str1 = new String(value, 1, 3); 3.//输出结果为bcd 4.System.out.println(str1);
offet为偏移量,是计算从哪个下标开始(下标从0开始奇数),例如为1就是从第二个数组元素开始,count为往后要的个数
假如此时个数超过了数组元素或者offset超过了数组长度-1,那么会发生StringIndexOutOfBoundsException异常
代码示例4: 字符串与字符数组的转换
String str = "helloworld" ; // 将字符串变为字符数组 char[] data = str.toCharArray() ; for (int i = 0; i < data.length; i++) { System.out.print(data[i]+" "); }
小练习:字符串的逆置
方法:先使用toCharArray方法将字符串转变为字符数组,然后再将char类型数组转变为字符串返回(共有三种方法返回).
public static String reverse(String string) { //字符串转为数组 char[] chars = string.toCharArray(); int i = 0; int j = chars.length-1; while (i < j) { char tmp = chars[i]; chars[i] = chars[j]; chars[j] = tmp; i++; j--; } //数组转化为字符串(三种方法) //return new String(chars); //return String.copyValueOf(chars); return String.valueOf(chars); }
字节与字符串
字节常用于数据传输以及编码转换的处理之中,String 也能方便的和 byte[] 相互转换.
代码示例1: 将整个字节数组转变为字符串
注意将字节数组转变为字符串时,是按照unicode编码进行转换的,例如97这个数字在unicode表中对应a这个字母
1.byte[] bytes={97,98,99,100,101,102}; 2.String str=new String(bytes); 3.//输出结果为abcdef 4.System.out.println(str);
代码示例2: 将部分字节数组内容变为字符串
1.byte[] bytes1={97,98,99,100,101,102}; 2.String str2=new String(bytes1,1,3); 3.//输出结果为bcd 4.System.out.println(str2);
代码示例3: 将字符串以字节数组的方式返回
1.String string="abcde"; 2.byte[] bytes2=string.getBytes(); 3.//输出结果为[97, 98, 99, 100, 101] 4.System.out.println(Arrays.toString(bytes2));
代码示例4:编码转换处理
情况1 当字符串为英文
String str3 = "gaobo"; try { byte[] bytes3 = str3.getBytes("gbk"); System.out.println(Arrays.toString(bytes3)); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } //输出结果 [103, 97, 111, 98, 111]
当字符串转变为字节数组的时候,此时我们字符串为英文,我们设置编码格式不管是gbk或者是utf8,最终的输出结果都是一样的
情况2 当字符串为中文(分编码格式不同的情况)
String str3 = "高博"; try { byte[] bytes3 = str3.getBytes("gbk"); System.out.println(Arrays.toString(bytes3)); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } //输出结果 [-72, -33, -78, -87]
当字符串转变为字节数组的时候,此时我们字符串为中文,我们设置编码格式不同,最终的输出结果都是不一样的,例如针对上述代码,gbk的输出结果为[-72, -33, -78, -87].
String str3 = "高博"; try { byte[] bytes3 = str3.getBytes("utf8"); System.out.println(Arrays.toString(bytes3)); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } //输出结果 [-23, -85, -104, -27, -11, -102]
当编码格式为utf8的时候,输出结果为[-23, -85, -104, -27, -115, -102]
总结
那么何时使用 byte[], 何时使用 char[] 呢?
byte[] 是把 String 按照一个字节一个字节的方式处理, 这种适合在网络传输, 数据存储这样的场景下使用. 更适合针对二进制数据来操作.
char[] 是把 String 按照一个字符一个字符的方式处理, 更适合针对文本数据来操作, 尤其是包含中文的时候.
回忆概念: 文本数据 vs 二进制数据
一个简单粗暴的区分方式就是用记事本打开能不能看懂里面的内容.
如果看的懂, 就是文本数据(例如 .java 文件), 如果看不懂, 就是二进制数据(例如 .class 文件).
字符串常规操作
字符串比较
上面使用过String类提供的equals()方法,该方法本身是可以进行区分大小写的相等判断。除了这个方法之外,String 类还提供有如下的比较操作:
区分大小写比较
String str1 = "hello" ; String str2 = "Hello" ; System.out.println(str1.equals(str2)); // 输出结果 false
不区分大小写比较
String str1 = "hello" ; String str2 = "Hello" ; System.out.println(str1.equalsIgnoreCase(str2)); // 输出结果 true
比较两个字符串大小关系(conpareTo方法)
在String类中compareTo()方法是一个非常重要的方法,该方法返回一个整型,该数据会根据大小关系返回三类内容:
1.相等:返回0.
2.小于:返回内容小于0.
3.大于:返回内容大于0。
代码:观察conpareTo比较
System.out.println("A".compareTo("a")); // -32 System.out.println("a".compareTo("A")); // 32 System.out.println("A".compareTo("A")); // 0 System.out.println("AB".compareTo("AC")); // -1 System.out.println("刘".compareTo("杨")); //-5456
compareTo()是一个可以区分大小关系的方法,是String方法里是一个非常重要的方法。
它的比较规律如下:
字符串的比较大小规则:总结成三个字 “字典序” 相当于判定两个字符串在一本词典的前面还是后面. 先比较第一个字符的大小(根据 unicode 的值来判定), 如果不分胜负, 就依次比较后面的内容例如AB和AC中,一开始先比较A和A,发现两个相同则为0,继续往下比,B和C在unicode表中对应的数字分别为66,67,可以看出来B比C要小,所以返回负数,这个负数的数字为66-67=-1.
字符串查找
从一个完整的字符串之中可以判断指定内容是否存在,对于查找方法有如下定义
代码示例1:contains方法
String str = "helloworld" ; System.out.println(str.contains("world")); // true
contains方法的判断形式是从JDK1.5之后开始追加的,在JDK1.5以前要想实现与之类似的功能,就必须借助、indexOf()方法完成。
来看contains方法的源码:
底层其实还是index方法
代码示例2:indexOf(String str)方法
String str = "helloworld"; System.out.println(str.indexOf("world")); // 结果为5,w开始的索引 System.out.println(str.indexOf("bit")); // 结果为-1,没有查到 if (str.indexOf("hello") != -1) { System.out.println("可以查到指定字符串!"); }
代码示例3:indexOf(String str,int fromIndex)方法
fromIndex是从前往后确定的位置,例如5就是从前往后数下标为5的字母,意思就是从这个字母开始查找是否存在str这个字符串,有返回这个字符串的第一个字母,没有返回-1.
String str = "helloworld" ; //结果为5 System.out.println(str.indexOf("world",5)); //结果为-1 System.out.println(str.indexOf("world",6));
代码示例4:lastIndexOf(String str)方法
lastIndexOf方法虽然是从后面开始往前数有没有world这个单词,如果有,返回数字的时候还是返回world这个单词中w所在的下标,没有返回-1
String str = "helloworld" ; //结果为5 System.out.println(str.lastIndexOf("world"));
代码示例5:lastIndexOf(String str,int fromIndex)方法
String str = "ababcfacd" ; //结果为2 System.out.println(str.lastIndexOf("ab",4));
此段代码相当于从下标为4处的字母开始从后往前寻找是否存在ab,如果有,直接返回2,没有返回-1.此时从c开始往前寻找ab,找到了以后返回第一次出现ab中a字母的下标
代码示例6:startsWith(String prefix)方法
String str = "ababcfacd" ; //结果为true System.out.println(str.startsWith("ab"));
代码示例7:startsWith(String prefix,int toffset)方法
String str = "ababcfacd" ; //结果为false System.out.println(str.startsWith("ab",4));
代码示例8:endsWith方法
String str = "ababcfacd" ; //结果为false System.out.println(str.endsWith("ab"));