String
关于String类,在API中可以看到有很多很多的方法可以去调用,下面来对重要的部分进行介绍。
String字符串的存储原理
关于Java JDK中内置的一个类:java.lang.String
1、String表示字符串类型,属于引用数据类型,不属于基本数据类型。
2、在java中随便使用双引号括起来的都是String对象。例如:"abc","def","hello world!",这是3个String对象。
3、java中规定,双引号括起来的字符串,是不可变的,也就是说"abc"自出生到最终死亡,不可变,不能变成"abcd",也不能变成"ab"
4、在JDK当中双引号括起来的字符串,例如:"abc" "def"都是直接存储在“方法区”的“字符串常量池”当中的。
为什么SUN公司把字符串存储在一个“字符串常量池”当中呢。
因为字符串在实际的开发中使用太频繁。为了执行效率,所以把字符串放到了方法区的字符串常量池当中。
例如下面的代码:
这两行代码表示底层创建了3个字符串对象,都在字符串常量池当中。
String s1 = "abcdef";
String s2 = "abcdef" + "xy";
如下的内存图去帮助理解:
可以看到,s1和s2是在main方法中的局部变量,所以存储在栈,但是字符串是存储在方法区的字符串常量池之中,所以s1和s2是直接存储的这片的地址。同时可以知道,如果再去通过其他的引用访问相同的字符串时,他们会指向同一个地址!
如果通过String s3 = new String("xy");的方法去创建字符串呢?
String s3 = new String("xy");
分析:这是使用new的方式创建的字符串对象。这个代码中的"xy"是从哪里来的?
凡是双引号括起来的都在字符串常量池中有一份。
new对象的时候一定在堆内存当中开辟空间。
所以如上图的s3所示,首先,因为new了对象,所以现在堆内存开辟一块空间去存储,然后又因为字符串(凡是被“”括起来的东西都是)存储在方法区的字符串常量池当中,所以堆内存中对象有存储着方法区字符串常量池的对应字符串的地址。
我们再去对我们的设想进行验证:
先创建一个字符串,存储着"hello"
String s1 = "hello";
然后再去创建s2字符串,同样存储着"hello"
String s2 = "hello";
我们已经知道,字符串是存储在方法区的字符串常量池
"hello"是存储在方法区的字符串常量池当中
所以这个"hello"不会新建。(因为这个对象已经存在了!)
那么如何去验证呢?
System.out.println(s1 == s2);
已知 == 双等号比较引用的话是变量中保存的内存地址
如果他们都是指向的方法区字符串常量池中的同一个字符串,那么他们保存的内存地址应该是一样的!
也就是说System.out.println(s1 == s2); 应该是true
事实证明的确如此,所以我们的设想得到了验证
当然,这并不意味着我们去判断字符串是否相等就能用"=="了
这样不保险,如下:
通过new对象的方式去创建两个字符串,都是xyz
String x = new String("xyz");
String y = new String("xyz");
然后去用"=="比较他们是否相等。
按道理应该是的,因为他们都是xyz
System.out.println(x == y);
然而结果是false,因为用"=="比较引用的话是变量中保存的内存地址!
然而x和y是new出来的,也就是说他们是指向的堆中的2块不同的地址,然后再堆中的那个对象才是指向的同一块字符串常量池!
如下图解释所示:
所以可以看到,"=="不保险。正确的做法是应该调用String类的equals方法。
String类已经重写了equals方法,以下的equals方法调用的是String重写之后的equals方法。
System.out.println(x.equals(y)); // true
小tips
String k = new String("testString");
"testString"这个字符串可以后面加"."呢?
因为"testString"是一个String字符串对象。只要是对象都能调用方法。
System.out.println("testString".equals(k));
建议使用这种方式,因为这个可以避免空指针异常。
System.out.println(k.equals("testString"));
存在空指针异常的风险。不建议这样写。
String类中的构造方法
先上最终结论:
第一个:String s = new String("");
第二个:String s = ""; 最常用
第三个:String s = new String(char数组);
第四个:String s = new String(char数组,起始下标,长度);
第五个:String s = new String(byte数组);
第六个:String s = new String(byte数组,起始下标,长度)
String与byte数组;
在这个构造方法中可以传进去一个byte数组,然后通过ASCLL码去解析对应的字符。
这里只掌握常用的构造方法。
byte[] bytes = {97, 98, 99};
97是a,98是b,99是c
String s2 = new String(bytes);
前面说过:输出一个引用的时候,会自动调用toString()方法,默认 Object的话,会自动输出对象的内存地址。
通过输出结果我们得出一个结论:String类已经重写了toString()方法。
输出字符串对象的话,输出的不是对象的内存地址,而是字符串本身。
System.out.println(s2);
abc
String(字节数组,数组元素下标的起始位置,长度)
将byte数组中的一部分转换成字符串。
String s3 = new String(bytes, 1, 2);
System.out.println(s3);
bc
String与Char数组
在这个构造方法中可以传进去一个char数组,然后将每个字符转换成字符串。
char[] chars = {'我','是','中','国','人'};
String s4 = new String(chars);
System.out.println(s4);
我是中国人
将char数组的一部分转换成字符串
String s5 = new String(chars, 2, 3);
System.out.println(s5);
中国人
String类当中常用方法
在API中可以查看到,String类中有许多的方法可以调用,如下
下面来学习一些基础的方法。
当然如果记不住也可以去查阅API文档
API链接: JDK11版本.
API链接: JDK8版本.
1(掌握).char charAt(int index)
这个方法是将一个字符串中指定位置的字符提取出来,返回一个char类型字符
char c = "中国人".charAt(1);
"中国人"是一个字符串String对象。只要是对象就能“点.”
System.out.println(c);
国
2(了解).int compareTo(String anotherString)
字符串之间比较大小不能直接使用 > < ,需要使用compareTo方法。
注意,这里是比较大下,而并不是单纯的看是否相等!
如想比较是否相等,仅需调用equals方法即可。
int result = "abc".compareTo("abc");
System.out.println(result);
0(等于0) 前后一致 10 - 10 = 0
int result2 = "abcd".compareTo("abce");
System.out.println(result2);
-1(小于0) 前小后大 8 - 9 = -1
int result3 = "abce".compareTo("abcd");
System.out.println(result3);
1(大于0) 前大后小 9 - 8 = 1
compareTo方法是按照字典序去比较的,比较到第一个不相同的字符就得出结果。
拿着字符串第一个字母和后面字符串的第一个字母比较。能分胜负就不再比较了。
System.out.println("xyz".compareTo("yxz"));
-1
3(掌握).boolean contains(CharSequence s)
该方法是去判断前面的字符串中是否包含后面的子字符串。
然后返回一个boolean值,true代表包含,false代表不包含。
System.out.println("HelloWorld.java".contains(".java"));
true
System.out.println("http://www.baidu.com".contains("https://"));
false
4(掌握). boolean endsWith(String suffix)
该方法是去判断当前字符串是否以某个子字符串结尾。
然后返回一个boolean值,true代表是,false代表否。
System.out.println("test.txt".endsWith(".java"));
false
System.out.println("test.txt".endsWith(".txt"));
true
5(掌握).boolean equals(Object anObject)
比较两个字符串必须使用equals方法,不能使用“==”
equals只能看出相等不相等。
compareTo方法可以看出是否相等,并且同时还可以看出谁大谁小。
System.out.println("abc".equals("abc"));
true
6(掌握).boolean equalsIgnoreCase(String anotherString)
该方法是去判断两个字符串是否相等,并且同时忽略大小写。
然后返回一个boolean值,true代表是,false代表否。
System.out.println("ABc".equalsIgnoreCase("abC"));
true
7(掌握).byte[] getBytes()
该方法是去将字符串对象转换成字节数组
byte[] bytes = "abcdef".getBytes();
for(int i = 0; i < bytes.length; i++){
System.out.println(bytes[i]);
}
可以得到结果:
97
98
99
100
101
102
8(掌握).int indexOf(String str)
该方法是去判断某个子字符串在当前字符串中第一次出现处的索引(下标)。
从0开始
System.out.println("oraclejavac++.netc#phppythonjavaoraclec++".indexOf("java"));
6
9(掌握).boolean isEmpty()
该方法是去判断某个字符串是否为“空字符串”。底层源代码调用的应该是字符串的length()方法。
然后返回一个boolean值,true代表是,false代表否。
String s = "";
System.out.println(s.isEmpty());
true
String b = "a";
System.out.println(s.isEmpty());
false
10(掌握). int length()
该方法是去判断数组长度是length属性,判断字符串长度是length()方法。
注意!判断数组长度和判断字符串长度不一样!
判断数组长度是length属性,判断字符串长度是length()方法。
System.out.println("abc".length());
3
System.out.println("".length());
0
11(掌握).int lastIndexOf(String str)
该方法是去判断某个子字符串在当前字符串中最后一次出现的索引(下标)
从0开始
System.out.println("oraclejavac++javac#phpjavapython".lastIndexOf("java"));
22
12(掌握). String replace(CharSequence target, CharSequence replacement)
该方法是替换字符串,将target所表示的字符串换成replacement代表的字符串。
然后返回新的字符串。
String的父接口就是:CharSequence
把 http:// 换成 https://
String newString = "http://www.baidu.com".replace("http://", "https://");
System.out.println(newString);
https://www.baidu.com
13(掌握).String[] split(String regex)
该方法是拆分字符串,然后将每个拆分的字符串存储到一个String[]数组当中。
返回值就是String[] String数组。
"1980-10-11"以"-"分隔符进行拆分。
String[] ymd = "1980-10-11".split("-");
for(int i = 0; i < ymd.length; i++){
System.out.println(ymd[i]);
}
输出如下:
1980
10
11
14(掌握).boolean startsWith(String prefix)
该方法是去判断某个字符串是否以某个子字符串开始。
然后返回一个boolean值,true代表是,false代表否。
System.out.println("http://www.baidu.com".startsWith("http"));
true
15(掌握). String substring(int beginIndex) 参数是起始下标。
该方法是截取字符串,从起始下标开始向后截取完。
然后返回新的字符串。
System.out.println("http://www.baidu.com".substring(7));
www.baidu.com
16(掌握).String substring(int beginIndex, int endIndex)
该方法是截取字符串,从起始下标开始向后到结束位置(不包括)截取完。
beginIndex起始位置(包括)
endIndex结束位置(不包括)
然后返回新的字符串。
System.out.println("http://www.baidu.com".substring(7, 10));
www
17(掌握) .char[] toCharArray()
该方法是将字符串转换成char数组。
然后返回一个char数组
char[] chars = "我是中国人".toCharArray();
for(int i = 0; i < chars.length; i++){
System.out.println(chars[i]);
}
输出如下:
我
是
中
国
人
18(掌握).String toLowerCase()
该方法是转换为小写。
然后返回新的字符串。
System.out.println("ABCDefKXyz".toLowerCase());
abcdefkxyz
19(掌握).String toUpperCase();
该方法是转换为小写。
然后返回新的字符串。
System.out.println("ABCDefKXyz".toUpperCase());
ABCDEFKXYZ
20(掌握). String trim();
该方法是去除字符串前后空白
注意仅仅是字符串前后的空白!中间的无法去除!
然后返回新的字符串。
System.out.println(" hello world ".trim());
hello world
21(掌握). String valueOf()
String中只有一个方法是静态的,不需要new对象
这个方法叫做valueOf
作用:将“非字符串”转换成“字符串”
String s1 = String.valueOf(123);
s1就是指向的字符串"123"
这个静态的valueOf()方法,参数是一个对象的时候,会自动调用该对象的toString()方法!
String s1 = String.valueOf(new Customer());
如果将Customer类中的toString方法重写,就会得到重写的结果。
不再演示,前面自行查阅。
在此基础上,再去研究JAVA自带的打印方法,println()方法的源代码
本质上System.out.println()这个方法在输出任何数据的时候都是先转换成字符串,再输出。
StringBuffer和StringBuilder
思考:我们在实际的开发中,如果需要进行字符串的频繁拼接,会有什么问题?
因为java中的字符串是不可变的,每一次拼接都会产生新字符串。
这样会占用大量的方法区内存。造成内存空间的浪费。
String s = "abc";
s += "hello";
就以上两行代码,就导致在方法区字符串常量池当中创建了3个对象:
"abc"
"hello"
"abchello"
如果以后需要进行大量字符串的拼接操作,建议使用JDK中自带的:
java.lang.StringBuffer
java.lang.StringBuilder
StringBuffer和StringBuilder的用法
StringBuffer stringBuffer = new StringBuffer();
StringBuffer是默认给一个容量为16的byte[] 数组的。(字符串缓冲区对象)
当进行拼接字符串时,只需调用append方法即可
stringBuffer.append("a");
append方法底层在进行追加的时候,如果byte数组满了,会自动扩容。
也可以指定初始化容量的StringBuffer对象(字符串缓冲区对象)
StringBuffer sb = new StringBuffer(100);
如何优化StringBuffer的性能?
- 在创建StringBuffer的时候尽可能给定一个初始化容量。
- 最好减少底层数组的扩容次数。预估计一下,给一个大一些初始化容量。
- 关键点:给一个合适的初始化容量。可以提高程序的执行效率。
StringBuilder用法同StringBuffer
StringBuilder sb = new StringBuilder();
sb.append(100);
StringBuffer和StringBuilder的区别
StringBuffer中的方法都有:synchronized关键字修饰。表示StringBuffer在多线程环境下运行是安全的。
StringBuilder中的方法都没有:synchronized关键字修饰,表示StringBuilder在多线程环境下运行是不安全的。
StringBuffer是线程安全的。
StringBuilder是非线程安全的。
小题目
String为什么是不可变的?
在String源代码中,String类中有一个byte[]数组,这个byte[]数组采用了final修饰。
因为数组一旦创建长度不可变。并且被final修饰的引用一旦指向某个对象之后,不可再指向其它对象,所以String是不可变的!
"abc" 无法变成 "abcd"
StringBuilder/StringBuffer为什么是可变的呢?
在源代码中,StringBuffer/StringBuilder内部实际上是一个byte[]数组,这个byte[]数组没有被final修饰。
StringBuffer/StringBuilder的初始化容量是16,当存满之后会进行扩容,底层调用了数组拷贝的方法System.arraycopy()。
所以StringBuilder/StringBuffer适合于使用字符串的频繁拼接操作。