Java之字符串
来源:互联网 发布:java 短信验证码 缓存 编辑:程序博客网 时间:2024/06/06 09:28
原文地址:http://blog.csdn.net/zhangerqing/article/details/8093919
Java中的字符串处理主要有下面三个类来处理的:String、StringBuffer、StringBuilder。
1、String
一、常量池、字符串常量池、运行时常量池
常量池一般就是指字符串常量池,是用来做字符串缓存的一种机制,当我们在程序中写了形如String s = “qqq”这样的语句后,JVM会在栈上为我们分配空间,存放变量s和对象”qqq“,当我们再次需要abc对象时,如果我们写下:String s1 = “qqq”的语句时,JVM会先去常量池中找,如果不存在,则新创建一个对象。如果存在,则直接将s1指向之前的对象”qqq“,此时,如果我们用==来判断的话,返回的true。这样做的好处就是节省内存,系统响应的速度加快,(因为省去了对象的创建时间)这也是缓存系统存在的原因。常量池是针对在编译期间就确定下来的常量而言的。但是,当类被加载后,常量池会被搬到方法区的运行时常量池,此时就不再是静态的了,那么是不是就不能向常量池中添加新的内容了呢(因为我们刚刚说过,常量池是在编译期确定好的)?答案是否定的,我们依然可以在运行时向常量池添加内容!String类有个方法叫intern(),它可以在运行时将新的常量放于常量池。
二、初始化
一般由String声明的字符串,长度是不可变的,这也是它与StringBuffer和StringBuilder最直观的一个区别。一般初始化方式:String s = “hello world”;经过这条语句,JVM的栈内存中产生一个s变量,堆内存中产生hello world字符串对象。s指向了hello world的地址。像上面这种方式产生的字符串属于直接量字符串对象,JVM在处理这类字符串的时候,会进行缓存,产生时放入字符串池,当程序需要再次使用的时候,无需重新创建一个新的字符串,而是直接指向已存在的字符串。看下面程序:
@Test public void stringTest0() { String s = "hello world"; String s2 = "hello world"; System.out.println(s == s2); //true }
该程序输出:true 因为s和s2都指向了hello world字符串,他们的地址是同一个。 我们常说,String的一个很大的特点,就是它是一个“不可变的字符串”,就是说,当一个String对象完成创建后,该对象的内容就固定下来了,但是为什么还会有下面这种情况呢?
@Test public void stringTest1() { String str = "I like";// ---------1-------- System.out.println(System.identityHashCode(str)); str = str + "java";// --------2--------- System.out.println(System.identityHashCode(str)); }
该程序输出:
31427481
21375057
说明:str似乎是变了,这是为什么呢?其实是这样的:str只是一个引用变量,当程序执行完1后,str指向“I like”。当程序执行完2之后,连接运算符会将两个字符串连在一起,并且让str指向新的串:”I like java”,所以,从这里应该可以看得出来,最初的对象确实没有改变,只是str所指向的对象在不断改变。
三、String类的一些常用方法
(1)public int length(){}
该方法用于获取字符串的长度。
public int length() { return count; }
(2)public boolean equals(Object anObject){}
该方法用于比较给定对象是否与String相等。
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = count; if (n == anotherString.count) { char v1[] = value; //---------1--------- char v2[] = anotherString.value;//-------2---------- int i = offset; int j = anotherString.offset; while (n-- != 0) { if (v1[i++] != v2[j++]) return false; } return true; } } return false; }
例子1:
String s1 = new String("hello world"); String s2 = new String("hello world"); String s3 = new String("hello"); System.out.println(s1.equals(s2));; System.out.println(s1.equals(s3));
结果输出:
true
false
例子2:
String s = "hello world"; String s1 = new String("hello world"); String s2 = new String("hello world"); String s3 = new String("hello"); String s4 = "hello world"; System.out.println(s.equals(s1));; System.out.println(s1.equals(s2)); System.out.println(s1.equals(s3)); System.out.println("\------------------"); System.out.println(s == s1); System.out.println(s == s3); System.out.println(s == s4);
输出:
true
true
false
\——————
false
false
true
结论:如上述代码中,s、s1、s2、s4他们四个String对象的内容都是”hello world”,所以,用equals()比较他们,返回的都是true。但是,当s和s1用==比较时,却返回false,因为二者在堆中开辟的地址不一样,所以,返回的肯定是false。而为什么s和s4用==比较时,返回的是true呢,因为上文中提到过,直接量的字符串会产生缓存池,所以,当声明s4的时候,编译器检测到缓存池中存在相同的字符串,所以就直接使用,只要将s4指向s所指向的字符串就行了,二者指向同一字符串,所以地址当然相等!
(3)public int compareTo(String anotherString){}
compareTo()可实现比较两个字符串的大小,源码如下:
public int compareTo(String anotherString) { int len1 = count; int len2 = anotherString.count; int n = Math.min(len1, len2); char v1[] = value; char v2[] = anotherString.value; int i = offset; int j = anotherString.offset; if (i == j) { int k = i; int lim = n + i; while (k < lim) { char c1 = v1[k]; char c2 = v2[k]; if (c1 != c2) { return c1 - c2; } k++; } } else { while (n-- != 0) { char c1 = v1[i++]; char c2 = v2[j++]; if (c1 != c2) { return c1 - c2; } } } return len1 - len2; }
compareTo是怎么实现的呢?
首先,会对两个字符串左对齐,然后从左到右一次比较,如果相同,继续,如果不同,则计算不同的两个字符的ASCII值的差,返回就行了。与后面的其他字符没关系。
例子:
@Test public void stringTest4() { String s = "hallo"; String s2 = "ha"; String s3 = "haeeo"; int a = s.compareTo(s2); System.out.println("a:" + a); int b = s.compareTo(s3); System.out.println("b:" + b); int c = s2.compareTo(s3); System.out.println("c:" + c); }
程序输出:
a:3
b:7
c:-3
s和s2相比,前两个相同,如果是这种情况,则直接返回length1-length2
s和s3相比,前两个相同,不用管,直接用第三个字符的ASCII码做差就行了。所以’l’-‘a’=7
(4)public char charAt(int index){}
获取指定位置的字符,比较容易理解,源码为:
public char charAt(int index) { if ((index < 0) || (index >= count)) { throw new StringIndexOutOfBoundsException(index); } return value[index + offset]; }
String s = “hallo”;
char a = s.charAt(2);
System.out.println(a);
输出:l
注意:参数index的值从0到字符串的长度-1,所以,如果值不在这个范围内,如下:
String s = “hallo”;
char a = s.charAt(8);
System.out.println(a);
则报错:
Exception in thread “main” java.lang.StringIndexOutOfBoundsException: String index out of range: 8
at java.lang.String.charAt(String.java:686)
at com.xtfggef.string.CompareToTest.main(CompareToTest.java:20)
与charAt()相对应的是indexOf():根据给定的字符串,返回他的位置。
indexOf()有多个参数:
public int indexOf(int ch)
public int indexOf(int ch, int fromIndex)
public int indexOf(String str)
public int indexOf(String str, int fromIndex)
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex)
有兴趣的自己去试试,这儿就不多阐述了。
(5)public String substring(int beginIndex){}
源码:
public String substring(int beginIndex) { return substring(beginIndex, count); }
用于截取字符串,此处与另一个方法对比:
public String substring(int beginIndex, int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > count) { throw new StringIndexOutOfBoundsException(endIndex); } if (beginIndex > endIndex) { throw new StringIndexOutOfBoundsException(endIndex - beginIndex); } return ((beginIndex == 0) && (endIndex == count)) ? this : new String(offset + beginIndex, endIndex - beginIndex, value); }
前者调用后者来实现,前者截取从指定位置到字符串结束的子字符串,后者截取从指定位置开始,到endIndex-1位置的子字符串。与数学中的 [beginIndex,endIndex)类似。
String s = "helloworld"; String s1 = s.substring(2); String s2 = s.substring(2, 7); String s3 = (String) s.subSequence(2, 7); System.out.print("s1:"+s1+"\n"+"s2:"+s2+"\n"+"s3:"+s3);
输出:
s1:lloworld
s2:llowo
s3:llowo
细心的读者应该看出来,该类里面包含一个subSequence(),而且该方法与substring(int,int)返回的结果一样,观察下源码,发现区别:
public CharSequence subSequence(int beginIndex, int endIndex) { return this.substring(beginIndex, endIndex); } }
其实subSequence()内部就是调用的substring(beginIndex, endIndex),只是返回值不同。
subString返回的是String,subSequence返回的是实现了CharSequence接口的类,也就是说使用subSequence得到的结果,只能使用CharSequence接口中的方法。不过在String类中已经重写了subSequence,调用subSequence方法,可以直接转为String对象,如我们例子中的做法。
(6)public String replace(char oldChar, char newChar){}和public String replaceAll(String regex, String replacement){}
public String replace(char oldChar, char newChar) { if (oldChar != newChar) { int len = count; int i = -1; char[] val = value; /* avoid getfield opcode */ int off = offset; /* avoid getfield opcode */ while (++i < len) { if (val[off + i] == oldChar) { break; } } if (i < len) { char buf[] = new char[len]; for (int j = 0 ; j < i ; j++) { buf[j] = val[off+j]; } while (i < len) { char c = val[off + i]; buf[i] = (c == oldChar) ? newChar : c; i++; } return new String(0, len, buf); } } return this; }
public String replaceAll(String regex, String replacement) { return Pattern.compile(regex).matcher(this).replaceAll(replacement); }
前者参数为两个字符串,用newChar替换原串里的所有oldChar。
后者从第一个参数可以看出,需要替换的东西可以用正则表达式描述。例子:
String s = "hello world"; String s1 = s.replace("l", "d"); System.out.println(s1); String s2 = "a78e5opx587"; String s3 = s2.replaceAll("[0-9]", "");//用空串替换原串里所有的0-9的数字 System.out.println(s3);
输出:
heddo wordd
aeopx
(7)public String[] split(String regex){}
该方法用于分割字符串,得到一个String类型的数组,根据regex可知,参数是个正则表达式。请看下面的例子:
String s = "hello world"; String s1 = "hello.worldd"; String[] s2 = s.split(" "); String[] s3 = s1.split("\\."); for(int i=0; i<s2.length; i++){ System.out.print(s2[i]+" "); } System.out.println(); for(int j=0; j<s3.length; j++){ System.out.print(s3[j]+" "); }
输出:
hello world
hello worldd
关于spilt()的其他重载方法,可参见JDK的String类的实现。
spilt()需要注意的事项,就是当分隔符为 . 的话,处理起来不一样,必须写成\.因为.是正则表达式里的一个特殊符号,必须进行转义。
(8)public native String intern(){}
intern()方法和前面说的equals()方法关系密切,从public native String intern()看出,它是Java的本地方法,我们先来看看Java文档里的描述:
Returns a canonical representation for the string object. A pool of strings, initially empty, is maintained privately by the class String.When the intern method is invoked, if the pool already contains a string equal to this String object as determined by theequals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned. It follows that for any two strings s and t, s.intern()==t.intern() is true if and only if s.equals(t) is true. All literal strings and string-valued constant expressions are interned. @return a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.
意思就是说,返回字符串一个规范的表示。进一步解释:有两个字符串s和t,s.equals(t),则s.intern()==t.intern().举个例子:
String s = new String("abc"); String s1 = "abc"; String s2 = "abc"; String s3 = s.intern(); System.out.println(s == s1);//false System.out.println(s == s2);//false System.out.println(s == s3);//false System.out.println(s1 == s3);//true
输出结果如注释所示,前两个结果前面已经说的很清楚了,现在拿最后一个说明,首先看看s3 = s.intern()这句,当调用s.intern()这句的时候,先去字符串常量池中找,是否有abc这个串,如果没有,则新增,同时返回引用,如果有,则返回已经存在的引用,此处s1和s2都指向常量池中的abc对象,所以此处是存在的,调用s.intern()后,s3和s1、s2指向同一个对象,所以s1==s3返回的是true。
intern()做到了一个很不寻常的行为:在运行期动态的在方法区创建对象,一般只有像new关键字可以在运行期在堆上面创建对象,所以此处比较特殊。属于及时编译的概念。
四、一些常见的问题,处理结果
在我们日常的开发中,总会遇到一些问题:
String s = “123” + “456”内存中产生几个字符串对象?
这是个比较有争议的问题,面试的时候,老师还挺喜欢问,论坛上大家说几个的也有,我给大家分析一下,因为我们前面有提到Java字符串的缓存机制,编译器在编译的时候会进行优化,所以在编译的过程中123和456被合成了一个字符串”123456”,因此,如果缓存池中目前没有123456这个对象,那么会产生一个,即”“123456”,且栈中产生一个引用s指向它,如果缓存池中已经存在”123456”,那么将产生0个对象,直接用s指向它。
如果spilt()函数的参数在要分割的字符串中没有怎么办?如String s = “helloworld” ,我现在调用String[] s2 = s.spilt(“abc”),返回什么?
做一个简单的测试,就可以看得出来:
public class StringSpilt { public static void main(String[] args) { String s = "helloworld"; String[] s2 = s.split("abc"); for (int i = 0; i < s2.length; i++) { System.out.println(s2[i] + " " + i); } } }
输出:helloworld 0
说明当遇到源字符串中没有的字符时,会把它整个串放入到数组中。
String s = "ab"; String s1 = "a"; String s2 = s1 + "b"; String s3 = "ab"; System.out.println(s == s2);//false System.out.println(s2 == s3);//false //为什么?System.out.println(s2.hashCode() == s3.hashCode()); //trueString s4 = "ad"; String s5 = "a" + "d"; String s6 = "ad"; System.out.println(s4 == s5);//true System.out.println(s4 == s6);//true
调用s2.hashCode() == s3.hashCode()返回true。解释:
==比较的是他们的地址,s1+”b”会产生一个新的串,所以和s和s2用==比,返回false,如果用equals的话,返回肯定是true,因为equals()比较的是对象的内容(String类是这样的)。至于hashCode,是这样的:如果没有重写Object的hashCode(),那么如果对象调用equals()放回true,则这两个对象调用hashCode()后返回的整数一定相等。此处继续补充:对于Object类而言,原生的equals()方法,必须两个对象的地址和内容都一样才返回true,同时Object类原生的hashCode()是参照对象的地址和内容根据一定的算法生产的。所以原生的hashCode()只有调用equals()返回true才相等。而String类不同,String类重写了Object的equals(),放松了条件,只要对象地址或者内容相等就返回true。
同时,String类重写了hashCode()方法,只要内容相等,则调用hashCode返回的整数值也相等,所以此处:s3和s2虽然地址不等,但是内容相等,所以会有:s2.hashCode() == s3.hashCode()返回true。但是这句话反过来讲就不一定成立了,因为毕竟hashCode()只是一种算法。继续补充:刚刚说了Object类和String类,此处补充下Integer类:Integer类,返回的哈希码就是Integer对象里所包含的那个整数的数值,例如Integer a=new Integer(50),则a.hashCode的值就是50 。由此可见,2个一样大小的Integer对象,返回的哈希码也一样。
System.out.println(s1.hashCode()); System.out.println(s2.hashCode()); System.out.println(s3.hashCode()); System.out.println(s4.hashCode()); System.out.println(s5.hashCode());
输出:
97
3105
3105
3107
3107
说明s1+”b”的过程创建了新的对象,所以地址不一样了。所以用==比较的话,返回的是false。
此处继续补充:为什么s1+”b”会产生新的对象?而没有去常量池查找是否已经存在ab对象,以致于s==s2返回false。因为我们说过常量池(下文会讲常量池)是在编译期确定好的,所以如果我们的语句时String s5 = “ab”的话,这个是在编译期确定的,会去常量池查找,而此处我们的语句时s2 = s1+”b”,s2的值只有在运行期才能确定,所以不会去常量池查找,也就是产生新串。再次提问:那么这里s2的值是在哪儿分配的呢?堆、JVM栈还是运行时常量池?正确回答:s2在堆上分配,因为+的内部实现是用StringBuilder来实现的。String s2 = s1+”b” 内部是这样实现的:String s2 = new StringBuilder(s1).append(“b”).toString();所以是在堆上来分配的
2.StringBuffer、StringBuilder
一、初始化
StringBuffer和StringBuilder 共四个构造方法:
StringBuffer()
public StringBuffer(int paramInt)
public StringBuffer(String paramString)
public StringBuffer(CharSequence paramCharSequence)
观察其源码发现,使用StringBuffer()时,默认开辟16个字符的长度的空间,使用public StringBuffer(int paramInt)时开辟指定大小的空间,使用public StringBuffer(String paramString)时,开辟paramString.length+16大小的空间。都是调用父类的构造器super()来开辟内存。这方面StringBuffer和StringBuilder都一样,且都实现AbstractStringBuilder。
二、主要方法
二者几乎没什么区别,基本都是在调用父类的各个方法,一个重要的区别就是StringBuffer是线程安全的,内部的大多数方法前面都有关键字synchronized,这样就会有一定的性能消耗,StringBuilder是非线程安全的,所以效率要高些。
public static void main(String[] args) throws Exception { String string = "0"; int n = 10000; long begin = System.currentTimeMillis(); for (int i = 1; i < n; i++) { string += i; } long end = System.currentTimeMillis(); long between = end - begin; System.out.println("使用String类耗时:" + between+"ms"); int n1 = 10000; StringBuffer sb = new StringBuffer("0"); long begin1 = System.currentTimeMillis(); for (int j = 1; j < n1; j++) { sb.append(j); } long end1 = System.currentTimeMillis(); long between1 = end1 - begin1; System.out.println("使用StringBuffer类耗时:" + between1+"ms"); int n2 = 10000; StringBuilder sb2 = new StringBuilder("0"); long begin2 = System.currentTimeMillis(); for (int k = 1; k < n2; k++) { sb2.append(k); } long end2 = System.currentTimeMillis(); long between2 = end2 - begin2; System.out.println("使用StringBuilder类耗时:" + between2+"ms"); }
输出:
使用String类耗时:982ms
使用StringBuffer类耗时:2ms
使用StringBuilder类耗时:1ms
虽然这个数字每次执行都不一样,而且每个机子的情况也不一样,但是有几点是确定的,String类消耗的明显比另外两个多得多。还有一点就是,StringBuffer要比StringBuilder消耗的多,尽管相差不明显。
(2)public synchronized int length(){}和public synchronized int capacity(){}
二者都是获取字符串的长度,length()获取的是当前字符串的长度,capacity()获取的是当前缓冲区的大小。举个简单的例子:
StringBuffer sb = new StringBuffer(); System.out.println(sb.length());; System.out.println(sb.capacity());
输出:
0
16
StringBuffer sb = new StringBuffer("hello"); System.out.println(sb.length());; System.out.println(sb.capacity());
输出:
5
21
因为默认分配16个字符大小的空间,所以不难解释上面的结果。
(3)public boolean equals(Object paramObject){}
StringBuffer sb = new StringBuffer("hello"); StringBuffer sb2 = new StringBuffer("hello"); System.out.println(sb.equals(sb2));
以上程序输出false,是不是有点惊讶?记得之前我们的文章说过,equals()比较的是字符串的内容,按理说此处应该输出的是true才对。
究其原因,String类重写了Object的equals(),所以只需要看内容是否相等即可,但是StringBuffer没有重写equals(),此处的equals()仍然是调用的Object类的,所以,调用StringBuffer类的equals(),只有地址和内容都相等的字符串,结果才会返回true。
(4)另外StringBuffer有一系列追加、插入、删除字符串的方法,首先append(),就是在原来的字符串后面直接追加一个新的串,和String类相比有明显的好处:
String类在追加的时候,源字符串不变(这就是为什么说String是不可变的字符串类型),和新串连接后,重新开辟一个内存。这样就会造成每次连接一个新串后,都会让之前的串报废,因此也造成了不可避免的内存泄露。
StringBuffer sb = new StringBuffer("helloworld, "); sb.append("I'm ").append("erqing ").append("who ").append("are you ?"); System.out.println(sb); //public synchronized StringBuffer insert(int paramInt, Object paramObject) sb.insert(12, /*9*/"nice! "); System.out.println(sb); //public synchronized StringBuffer reverse() sb.reverse(); System.out.println(sb); sb.reverse(); System.out.println(sb); //public synchronized StringBuffer delete(int paramInt1, int paramInt2) //public synchronized StringBuffer deleteCharAt(int paramInt) sb.delete(12, 18); System.out.println(sb); sb.deleteCharAt(5); System.out.println(sb);
输出:
helloworld, I’m erqing who are you ?
helloworld, nice! I’m erqing who are you ?
? uoy era ohw gniqre m’I !ecin ,dlrowolleh
helloworld, nice! I’m erqing who are you ?
helloworld, I’m erqing who are you ?
helloorld, I’m erqing who are you ?
(5)public synchronized void trimToSize(){}
该方法用于将多余的缓冲区空间释放出来。
StringBuffer sb = new StringBuffer("hello erqing"); System.out.println("length:"+sb.length()); System.out.println("capacity:"+sb.capacity()); sb.trimToSize(); System.out.println("trimTosize:"+sb.capacity());
输出:
length:12
capacity:28
trimTosize:12
3.字符串处理类StringTokenizer
StringTokenizer是java.util包下的一个类,用来对字符串做简单的处理。
举个简单的例子:
String s = "Tonight is the answer !"; StringTokenizer st = new StringTokenizer(s," "); int count = st.countTokens(); System.out.println("个数为:"+count); while (st.hasMoreTokens()) { String token = st.nextToken(); System.out.println(token); }
输出:
个数为:5
Tonight
is
the
answer
!
- Java 数据类型之字符串
- Java笔记之字符串
- Java之字符串
- java学习之字符串
- Java之字符串
- java实例之字符串
- java之字符串组合
- Java之String 字符串
- Java之字符串
- Java之字符串处理
- java之字符串
- java 之 字符串
- java难点之字符串
- Java 之字符串
- Java 之字符串
- JAVA之字符串
- JAVA之定位字符串
- java之字符串
- 既要宽广,又要深邃,这也行
- npm太慢, 淘宝npm镜像使用方法
- 利用Chart.JS柱形统计表并且绑定数据库的数据
- 欢迎使用CSDN-markdown编辑器
- C#梳理【结构体Struct】
- Java之字符串
- 最详细的安装一个vue项目,利用了vue-cli脚手架
- Python 之 Selenium(1)Hello World
- hadoop初识之十二:wordcount 处理过程和mapreduce的数据类型
- [bzoj3522][bzoj4543][POI2014]HOTEL
- POJ
- C语言之如何输出uint32_t和uint64_t和16进制
- 开始了
- jstree插件的demo和相关信息