String的创建方式
可以使用下面两种方式创建String类型的对象:
String s3 = "java";String s4 = new String("javaWeb");
这两种方式的区别:
s3:系统会在常量池里面创建一个hello的字符串对象。
s4:系统会在常量池里面创建一个javaWeb字符串对象,然后在堆内存里面再创建一个javaWeb字符串对象。
请看下面程序的打印结果:
public static void main(String[] args) { String s3 = new String("java"); String s4 = new String("java"); String s5 = "hello"; String s6 = "hello"; System.out.println(s3 == s4); //false System.out.println(s3.equals(s4));//true System.out.println(s5== s6); //true System.out.println(s5.equals(s6)); //true}
开发中建议使用String s = "java";这种方式创建字符串对象,可以减少堆内存的使用。
注意:比较两个字符串是否一致最好使用equals方法
String类常用构造方法
String()初始化新创建的 String对象,它代表了一个空字符序列。 String(byte[] bytes)通过使用平台的默认字符集解码指定的字节数组构造了一个新的 String。 String(byte[] bytes, int offset, int length)构建了一种新的 String通过解码指定的字节数组使用平台的默认字符集。 String(byte[] bytes, int offset, int length, String charsetName)构建了一种新的 String通过解码指定的字节数组使用指定的字符集。 String(byte[] bytes, String charsetName)通过使用指定的 charset解码指定的字节数组构造了一个新的 String。 String(char[] value)分配一个新的 String,它代表了目前包含在字符数组参数字符序列。 String(char[] value, int offset, int count)分配一个包含字符与字符数组数组参数的新 String。
String类常用方法
- char charAt(int index)
- 获取index位置的字符
- boolean contains(CharSequence s)
- 判断字符串中是否包含某个字符串
- boolean endsWith(String endStr)
- 判断是否是以某个字符串结尾
- boolean equalsIgnoreCase(String anotherString)
- 忽略大小写比较两个字符串是否相等
- byte[] getBytes()
- 转换成byte数组
- int indexOf(String str);
- 取得指定字符在字符串的位置
- int indexOf(String str, int fromIndex)
- 从指定的下标开始取得指定字符在字符串的位置
- int lastIndexOf(String str)
- 从后面开始取得指定字符在字符串最后出现的的位置
- int lastIndexOf(String str, int fromIndex)
- 从后面开始指定的下标开始取得指定字符在字符串的位置
- int length()
- 获取字符串的长度
- String replaceAll(String s1,String s2)
- 替换字符串中的内容
- String[] split(String s)
- 根据指定的表达式拆分字符串
- boolean startsWith(String s)
- 判断是否是以某个字符串开始
- String substring(int begin)
- 根据传入的索引位置截子串
- String substring(int beginIndex, int endIndex)
- 根据传入的起始和结束位置截子串
- char[] toCharArray()
- 将字符串转换为char数组
- void toUpperCase()
- 转换为大写
- void toLowerCase()
- 转换为小写
- String trim()
- 去除首尾空格
- String valueOf(Object obj)
- 将其他类型转换为字符串类型
public static void main(String[] args) { String s1 = "javajavajava"; // char charAt(int index);获取index位置的字符 System.out.println(s1.charAt(5)); // boolean contains(CharSequence s);判断字符串中是否包含某个字符串 System.out.println(s1.contains("key")); // boolean endsWith(String endStr);判断是否是以某个字符串结尾 System.out.println(s1.endsWith("a")); // boolean equalsIgnoreCase(String anotherString);忽略大小写比较两个字符串是否相等 System.out.println(s1.equalsIgnoreCase("java")); // byte[] getBytes();转换成byte数组 String s2 = "abc"; byte[] b1 = s2.getBytes(); for(int i=0; i<b1.length; i++){ System.out.print(b1[i] + " "); } System.out.println(); // int indexOf(String str);取得指定字符在字符串的位置 System.out.println(s1.indexOf("a")); // int indexOf(String str, int fromIndex);从指定的下标开始取得指定字符在字符串的位置 String s3 = "javajavaasdxx"; System.out.println(s3.indexOf("a", 2)); // int lastIndexOf(String str);从后面开始取得指定字符在字符串最后出现的的位置 System.out.println(s3.lastIndexOf("a")); // int lastIndexOf(String str, int fromIndex);从后面开始指定的下标开始取得指定字符在字符串的位置 System.out.println(s3.lastIndexOf("a", 5)); // int length();获取字符串的长度 System.out.println(s3.length()); // String replaceAll(String s1,String s2);替换字符串中的内容 String s4 = "javajavajavakey"; System.out.println(s4.replaceAll("key", "YYY")); // String[] split(String s);根据指定的表达式拆分字符串 String s5 = "a,b,c,d"; String[] array1 = s5.split(","); for(int i=0; i<array1.length; i++){ System.out.print(array1[i] + " "); } System.out.println(); // boolean startsWith(String s);判断是否是以某个字符串开始 System.out.println(s3.startsWith("ja")); // String substring(int begin);根据传入的索引位置截子串 System.out.println(s3.substring(5)); // String substring(int beginIndex, int endIndex);根据传入的起始和结束位置截子串 System.out.println(s3.substring(6, 10)); // char[] toCharArray();将字符串转换为char数组 char[] array2 = s5.toCharArray(); for(int i=0; i<array2.length; i++){ System.out.print(array2[i] + " "); } System.out.println(); // void toUpperCase();转换为大写 System.out.println(s5.toUpperCase()); // void toLowerCase();转换为小写 System.out.println(s5.toLowerCase()); // String trim();去除首尾空格 String s6 = " java good ok "; System.out.println(s6.trim()); // String valueOf(Object obj);将其他类型转换为字符串类型 Object o = new Object(); o = null; System.out.println(String.valueOf(o));//建议使用这种方法转换字符串 //System.out.println(o.toString());//报出空指针错误}
String源码分析
String的不可变性
咱们先来看一下String源码里面的一则注解内容
这句话说明咱们java里面的字符串当创建成功后是不可变的。
对于Java初学者, 对于String是不可变对象总是存有疑惑。看下面代码:
public static void main(String[] args) { String s = "Hello Java"; System.out.println("s = " + s); s = "Hello String"; System.out.println("s = " + s);}
其输出结果为:
这里首先我们先创建了一个String对象,其值为“Hello Java”,然后有把s的对象重新赋值成“Hello String”,咱们从打印的结果也能发现s对象对应的值确实改变了。那为什么还要说String对象是不可变了。其实在这里咱们存在着一个误区,这个误区就是:在这里s只是String的一个对象引用。s并不是String对象本身(String是引用数据类型)。
其内存结构图是:
咱们再来看看String的源码
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0}
value[]是用来存储字符串内容的字符数组
hash:用来保存字符串对应的hash值
这两个属性都是被private和final修饰,说明外部是不能直接访问,并且一旦赋值成功以后是不能修改的。
类上被final修饰类不能被其它类所继承
在jdk1.8及其之前String存储使用用字符数组来存储内容,而jdk9之后字符串改为byte字节数组来存储这个是为了避免浪费空间
常量池
字符串常量池设计初衷:咱们使用的每个字符串都是一个String对象,并且开发中将会频繁使用字符串,如果像其他对像那样创建销毁将极大影响程序的性能。虚拟机(JVM)为了提高性能和减少内存开销,在实例化字符串的时候进行了优化,为字符串开辟了一个字符串常量池,类似于缓存区创建字符串常量时,首先判断字符串常量池是否存在该字符串,存在该字符串返回引用实例,不存在,实例化字符串,放入池中。
情景一
public static void main(String[] args) { String s1 = "abc"; String s2 = "abc"; System.out.println("s1 == s2 : "+(s1==s2)); System.out.println("s1.equals(s2) : " + (s1.equals(s2)));}
首先在字符串常量池中创建了一个字符串对象“abc”,第二次又想去创建一个字符串“abc”,但是由于字符串“abc”在常量池中存在,所以直接使用不会再去创建新的字符串对象
情景二
public static void main(String[] args) { String s1 = "Hello Java"; String s2 = "Hello Java"; String s3 = new String("Hello Java"); String s4 = new String("Hello Java"); System.out.println("s3 == s4 : "+(s3==s4)); System.out.println("s3.equals(s4) : "+(s3.equals(s4))); System.out.println("s1 == s3 : "+(s1==s3)); System.out.println("s1.equals(s3) : "+(s1.equals(s3)));}
其运行结果为:
情景三
字符串使用“+”进行字符串的拼接
public static void main(String[] args) { String s1 = "Hello" + "Java"; String s2 = "HelloJava"; System.out.println("s1 = s2 : "+ (s1 == s2));}
从上面两张图可以看出s1,s2同时指向的是常量池中的“HelloJava”,此时在常量池中的“Hello”和“Java”如果没有被其他对象引用这个时候这两个对象就成为垃圾,可能会被垃圾回收机制(GC)回收内存。
情景四
public static void main(String[] args) { String str1 = "Hello"; String str2 = "Java"; String str3 = str1 + str2; String str4 = "HelloJava"; System.out.println("str3 = str4 : " + (str3 ==str4));}
咱们先来看一下String str3 = str1 + str2;这段代码的运行原理:
运行期JVM首先会在堆中创建一个StringBuilder类,同时用str1指向的拘留字符串对象完成初始化,然后调用append方法完成对str2所指向的拘留字符串的合并, 接着调用StringBuilder的toString()方法在堆中创建一个String对象,最后将刚生成的String对象的堆地址存放在局部变量str4中
情景五
public static void main(String[] args) { String str1 = "b"; String str2 = "a" + str1; String str12 = "ab"; System.out.println("str2 = str12 : "+ (str2 == str12)); final String str3 = "b"; String str4 = "a" + str3; String str34 = "ab"; System.out.println("str4 = str34 : "+ (str4 == str34));}
JAVA编译器对字符串给拼接的都是基本类型或者常量 是当成常量表达式直接求值来优化的。也就是在编译期间就会被优化 对于通过对象名拼接,会产生新的对象的,存储在堆(heap)中。
在编译期间优化不会产生新对象,在运行期间优化会产生新对象
String相关的+
String中的 + 常用于字符串的连接。看下面一个简单的例子:
public static void main(String[] args) { String a = "aa"; String b = "bb"; String c = "xx" + "yy" + a + "zz" + b; System.out.println(c);}
javap -c
String的不可变性导致字符串变量使用+号的代价
public static void main(String[] args) { String s = "a" + "b" + "c"; String s1 = "a"; String s2 = "b"; String s3 = "c"; String s4 = s1 + s2 + s3;}
变量s的创建等价于 String s = "abc"; 由上面例子可知编译器进行了优化,这里只创建了一个对象。由上面的例子也可以知道s4不能在编译期进行优化,其对象创建相当于
StringBuilder temp = new StringBuilder();temp.append(s1).append(s2).append(s3);temp.toString();
由上面的分析结果,可就不难推断出String 采用连接运算符(+)效率低下原因分析,形如这样的代码:
public static void main(String[] args) { String s = ""; for (int i = 0; i < 10; i++) { s += "a"; } System.out.println(s);}
每做一次 + 就产生个StringBuilder对象,然后append后就扔掉。下次循环再到达时重新产生个StringBuilder对象,然后 append 字符串,如此循环直至结束。如果我们直接采用 StringBuilder 对象进行 append 的话,我们可以节省 N - 1 次创建和销毁对象的时间。所以对于在循环中要进行字符串连接的应用,一般都是用StringBuffer或StringBulider对象来进行append操作。