字符串
字符串操作是计算机程序设计中最常见的行为。这章让我们来了解一下这个既熟悉又陌生 的编程伙伴。
不可变String
我们都知道String 类是一个final 类,这意味着String 不可被继承。但这不是关键,查看JDK文档你就会发现,String 类中用于存放字符数组的属性private final char value[];
是一个常量。并且String 类中的每一个看起来会修改String 的方法,实际上都是返回一个全新的String 对象,以包含修改后的字符串内容。而最初的String对象丝毫未动。来看下面的代码:
public class Test {
public static void main (String args[]) {
String s = "hello" ;
System.out .println(s.toUpperCase());
System.out .println(s);
}
}
对s 调用toUpperCase() 并不会影响原来s 的值。事实上我们只是需要它的返回值而已,我们并不希望它去修改原来s 的值。比如说我现在给你一个字符串对象。要你输出它的全大写 ,全小写 ,拼接某个字符串 。如果String 任一个方法都会修改原对象的话,那还得多写上三句new String() 的语句,这样其实非常的麻烦。个人觉得String 类的设计者就是考虑到这个原因才会这么去实现String 的方法。
重载 + 与StringBuider
我们都知道对String对象执行”+”操作就是进行字符串拼接,它会返回一个新的字符串对象。我们也知道String对象是不可变的,所以在进行字符串拼接的过程中效率 是一个很大的问题。如果编译器什么都不做。针对代码String s = "a"+"b"+"c"+"d";
我们可能需要创建“a”,”b”,”c”,”d”,”ab”,”abc”,”abcd” 等7个字符串对象。但是我们真正需要的只是“abcd” ,期间产生的“ab”,”abc” 可以说是毫无必要的,但它的确会影响垃圾回收 和拼接操作的所需时间。那么编译器是如何优化的呢?针对下面代码:
public class Test {
public static void main (String args[]) {
String hange = "hange" ;
String s = "abc" + hange + "def" + 47 ;
System.out .println(s);
}
}
我们将其编译成Test.class 文件,然后通过命令javap -c Test
即可生成JVM字节码:
D:\>javap -c Test
Compiled from "Test.java"
public class Test {
public Test();
Code:
0 : aload_0
1 : invokespecial #1
4 : return
public static void main(java.lang.String[]);
Code:
0 : ldc #2
2 : astore_1
3 : new #3
6 : dup
7 : invokespecial #4
10 : ldc #5
12 : invokevirtual #6
15 : aload_1
16 : invokevirtual #6
19 : ldc #7
21 : invokevirtual #6
24 : bipush 47
26 : invokevirtual #8
29 : invokevirtual #9
32 : astore_2
33 : getstatic #10
36 : aload_2
37 : invokevirtual #11
40 : return
}
通过字节码可以看出,编译器自动为我们产生了StringBuilder 对象(Java SE5 引入,较StringBuffer 速度更快。StringBuffer 线程安全开销较大。),并且使用了它的append() 方法,以避免产生多余的无用对象。最后又调用了StringBuilder 的toString() 方法返回了一个字符串。如果我们认为编译器已经能够帮我们优化好了,就不注意编程习惯的话,那就大错特错了。看下面的例子:
public class Test {
public String test1(String [] array ) {
String result = "" ;
for (String s : array ) {
result += s;
}
return result;
}
public String test2(String [] array ) {
StringBuilder result = new StringBuilder();
for (String s : array ) {
result.append(s);
}
return result.toString();
}
}
D:\>javap -c Test
Compiled from "Test.java"
public class Test {
public Test();
Code:
0 : aload_0
1 : invokespecial #1
4 : return
public java.lang.String test1(java.lang.String[]);
Code:
0 : ldc #2
2 : astore_2
3 : aload_1
4 : astore_3
5 : aload_3
6 : arraylength
7 : istore 4
9 : iconst_0
10 : istore 5
12 : iload 5
14 : iload 4
16 : if_icmpge 51
19 : aload_3
20 : iload 5
22 : aaload
23 : astore 6
25 : new #3
28 : dup
29 : invokespecial #4
32 : aload_2
33 : invokevirtual #5
36 : aload 6
38 : invokevirtual #5
41 : invokevirtual #6
44 : astore_2
45 : iinc 5 , 1
48 : goto 12
51 : aload_2
52 : areturn
public java.lang.String test2(java.lang.String[]);
Code:
0 : new #3
3 : dup
4 : invokespecial #4
7 : astore_2
8 : aload_1
9 : astore_3
10 : aload_3
11 : arraylength
12 : istore 4
14 : iconst_0
15 : istore 5
17 : iload 5
19 : iload 4
21 : if_icmpge 43
24 : aload_3
25 : iload 5
27 : aaload
28 : astore 6
30 : aload_2
31 : aload 6
33 : invokevirtual #5
36 : pop
37 : iinc 5 , 1
40 : goto 17
43 : aload_2
44 : invokevirtual #6
47 : areturn
}
我们可以得知第一种方式在退出一个循环后即会返回toString() 然后第二次还得重新生成StringBuilder 对象。而第二种就没有这个行为。由此可见,平常在进行字符串的拼接时,使用StringBuilder 代替“+”进行拼接能够带来性能的提升。 关于StringBuilder 的用法,我相信只要看看方法名即可,这里就不用过多介绍了,关于字符串的插入、拼接、删除、替换、子串 它都有对应的方法。只有你想不到,没有它做不到。
无意识的递归
我们知道直接将一个对象引用和字符串相加,其实会调用对象的toString() 方法。或者更通俗一点,我们在一个需要字符串的位置输入了一个对象引用,那么就会调用这个对象的toString() 方法。所以如果我们写出如下代码:
public class Test {
public String toString () {
return this + "haha" ;
}
public static void main (String args[]) {
System.out .println(new Test().toString());
}
}
这样就会造成递归调用。解决方式就是调用Object.toString() 。
String上的操作
import java.util .Arrays
public class StringTest {
public static void main(String args[]) {
//-----------------常用构造方法
String s0 = "hello"
String s1 = new String("hello" )
String s2 = new String(new char[]{'h' , 'i' })
String s3 = new String(new char[]{'h' , 'e' , 'l' }, 0 , 2 )
//-----------------常用方法举例
char c0 = s0.charAt (1 )
int c0_ucode = s0.codePointAt (1 )
int h_ucode = s0.codePointBefore (1 )
System.out .println (s0.compareTo ("hello" ))
System.out .println (s0.compareTo ("hi" ))
System.out .println (s0.compareTo ("he" ))
System.out .println (s0.compareToIgnoreCase ("HeLlO" ))
System.out .println (s0.concat (" world" ))
System.out .println (s0 + " world" )
System.out .println (s0.contains ("el" ))
System.out .println (s0.contentEquals (new StringBuffer("hello" )))
System.out .println (s0.startsWith ("he" ))
System.out .println (s0.endsWith ("lo" ))
System.out .println (s0.equals ("hello" ))
System.out .println (s0.equalsIgnoreCase ("Hello" ))
System.out .println (Arrays.toString (s0.getBytes ()))
char[] dst = "0123456789" .toCharArray ()
s0.getChars (0 , 2 , dst, 4 )
System.out .println (dst)
//lastIndexOf的用法和indexOf相同,区别是反向搜索。
System.out .println (s0.indexOf (101 ))
System.out .println ("helelo" .indexOf (101 , 2 ))
System.out .println ("hehelo" .indexOf ("he" ))
System.out .println ("hehelo" .indexOf ("he" , 1 ))
//http://blog.csdn .net /baidu_31657889/article/details/52315902 推荐一篇博客关于intern()
//在jdk1.6 及以下,对String对象调用intern(),如果常量池中没有该String对象,则新建,返回常量池里的对象引用。但是原来的对象没有变化。
//在jdk1.7 及以上,对String对象调用intern(),如果常量池中没有该String对象,编译器会使该对象成为常量池里的对象。更加节约空间。
//如果常量池里已经有了该对象,那么就什么都不做,直接返回常量池里的对象引用。
String hange = new String("mia" ) + new String("och" )
hange.intern ()
System.out .println (hange == "miaoch" )
String hello = new String("hello" )
hello.intern ()
System.out .println (hello == "hello" )
System.out .println ("" .isEmpty ())
System.out .println ("hange" .matches ("han[g-h]{1}e" ))
System.out .println ("hange" .regionMatches (1 , "**ang*" , 2 , 3 ))
System.out .println ("hange" .regionMatches (true, 1 , "**ANg*" , 2 , 3 ))
//replace,replaceAll,replaceFirst。第一个只能根据准确的字符串来替换字符串。后两个根据正则表达式替换
System.out .println ("hangeeee" .replace ("e" , "o" ))
System.out .println ("hange,e,ee" .replaceAll ("[ae,]" , "" ))
System.out .println ("hange,e,ee" .replaceFirst ("[ae,]" , "" ))
System.out .println (Arrays.toString ("ha,nge,e,ee" .split ("," )))
System.out .println (Arrays.toString ("ha nge e ee" .split ("\\s" , 3 )))
System.out .println ("hange" .substring (1 ))
System.out .println ("hange" .substring (1 , 2 ))
System.out .println ("hange" .toUpperCase ())
System.out .println ("HAnge" .toLowerCase ())
System.out .println (" ha nge " .trim ())
//----------------------------静态方法
System.out .println (String.format ("%d,%s" , 1 , "he" ))
System.out .println (String.valueOf (123 ))
}
}
格式化输出
我们可以用System.out.printf()
来格式化输出一个字符串。这个方法其实直接调用了System.out.format()
,而该方法又调用了Formatter 类的成员方法。学过C基本都知道是如何使用的,这里再巩固一下。
String.format() 和printf() 都是借用了Formatter 的format() 成员方法。可以不必知道它是如何实现的,但必须了解一下它的使用方法。
类型写法 备注 %d 整数型 %c Unicode字符 %b boolean值 %s String %f 浮点数(十进制) %a 浮点数(十六进制) %e 浮点数(科学计数) %x 整数(十六进制) %o 整数(八进制) %h 散列码(十六进制) %tx 日期与时间类型(x代表不同的日期与时间转换符) %% 字符“%” %n 字符“\n”
时间类型的转换符 样例 格式 c 星期六 十月 27 14:21:20 CST 2007 包括全部日期和时间信息 F 2007-10-27 “年-月-日”格式 D 10/27/07 “月/日/年”格式 r 02:25:51 下午 “HH:MM:SS PM”格式(12时制) T 14:28:16 “HH:MM:SS”格式(24时制) R 14:28 “HH:MM”格式(24时制)
还有更细的输出小时和分钟的,我这里就不放了。有兴趣的可以自己去研究。 除此以外还可以在%d 的中间插入数字以表示这个字符串所占位数%5d 就表示要占用五个字符串位置。如果不够,默认会靠右对齐。不过也可以加上“-” 号使其靠左对齐。另外还可以使用小数点,针对字符串和浮点数,小数点有不一样的效果:
import java.util.Date;
public class Test {
public static void main (String args[]) {
System.out .printf("字母a的散列码是:%h %n" , 'a' );
System.out .printf("%5d\n" , 1234 );
System.out .printf("%-5d\n" , 1234 );
System.out .printf("%010d\n" , 1234 );
System.out .printf("%5d\n" , 123456 );
System.out .printf("%-10.4f\n" , 1234.12345 );
System.out .printf("%-10.4e\n" , 1234.12345 );
System.out .printf("%5.5s\n" , "123456" );
System.out .printf("%,10d\n" , 12345678 );
System.out .printf("%(10d\n" , -12345678 );
System.out .printf("%#10x\n" , 12345678 );
System.out .printf("%#10o\n" , 12345678 );
System.out .printf("%.3f %<.4f\n" , 1.23456 );
System.out .printf("%2$s %3$s\n" , 99 , "abc" , "efg" );
System.out .printf("%tc\n" , new Date());
System.out .printf("%tR\n" , new Date());
}
}
字母a的散列码是:61
1234
1234
0000001234
123456
1234.1235
1.2341e+03
12345
12 ,345 ,678
(12345678 )
0xbc614e
057060516
1.235 1.2346
abc efg
星期三 七月 19 14 :45 :24 CST 2017
14 :45
一个十六进制转换工具
我们可以利用format() 方法编写一个查看二进制文件的工具类。
package test;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class Hex {
public static String format (byte [] data) {
StringBuilder result = new StringBuilder();
int n = 0 ;
for (byte b : data) {
if (n % 16 == 0 ) {
result.append(String.format("%05x: " , n));
}
result.append(String.format("%02x " , b));
n++;
if (n % 16 == 0 ) {
result.append("\n" );
}
}
result.append("\n" );
return result.toString();
}
public static byte [] readFile (String filePath) throws IOException {
File file = new File(filePath);
FileInputStream fis = new FileInputStream(file);
StringBuilder sb = new StringBuilder();
byte [] buf = new byte [1024 ];
int n;
while ((n=fis.read(buf, 0 , 1024 )) != -1 ) {
sb.append(new String(buf, 0 , n));
}
fis.close();
return sb.toString().getBytes();
}
public static void main (String args[]) throws IOException {
System.out.println(format(readFile("src/test/Hex.java" )));
}
}
正则表达式
正则表达式非常的强大,关于符号的用法可以查看我转载的一篇文章 。下面是一个简单的例子,复杂的功能我这里就不介绍了。
public class Test {
public static void main (String args[]) {
String s = "hange21miao22dd5dsad1as1" ;
String regex = "([^\\d]+)([\\d]+)" ;
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(s);
while (m.find()) {
System.out .println(m.groupCount());
System.out .println(m.group (0 ));
System.out .println(m.group (1 ));
System.out .println(m.group (2 ));
System.out .println(m.start());
System.out .println(m.end());
System.out .println("------------" );
}
}
}
------------执行结果:部分
2
hange21
hange
21
0
7
------------
2
miao22
miao
22
7
13
------------
...
扫描输出
这个主要讲讲我们经常使用的Scanner 类,Scanner的构造器可以接受任何类型的输入对象 ,包括File 、InputStream 、String 或者Readable (接口)对象。下面是使用Scanner 类的一个例子。
import java.util.Scanner;
public class Test {
public static void main (String args[]) {
String s1 = new String("han ge miao ch" );
String s2 = new String("han,ge,miao,ch" );
String s3 = new String("han. g[em,i ao.ch" );
Scanner reader1 = new Scanner(s1);
while (reader1.hasNext()) {
System.out .print(reader1.next() + " " );
}
System.out .println();
Scanner reader2 = new Scanner(s2);
reader2.useDelimiter("," );
while (reader2.hasNext()) {
System.out .print(reader2.next() + " " );
}
System.out .println();
Scanner reader3 = new Scanner(s3);
reader3.useDelimiter("[,\\.\\[\\s]+" );
while (reader3.hasNext()) {
System.out .print(reader3.next() + " " );
}
}
}
--------------执行结果
han ge miao ch
han ge miao ch
han g em i ao ch
String s = "58.123.123.123@02/10/2005\n" +
"10.123.10.123@02/10/2005\n" +
"52.143.123.2@02/10/2005\n" +
"192.168.1.1@02/10/2005\n" ;
import java.util .Scanner
import java.util .regex .MatchResult
import java.util .regex .Matcher
import java.util .regex .Pattern
public class Test {
public static void main(String args[]) {
String s = "58.123.123.123@02/10/2005\n" +
"10.123.10.123@02/10/2005\n" +
"52.143.123.2@02/10/2005\n" +
"192.168.1.1@02/10/2005\n"
Scanner sc = new Scanner(s)
String regex = "(\\d+.\\d+.\\d.+\\d+)@((\\d{2})/(\\d{2})/(\\d{4}))"
System.out .println ("方式1:" )
while (sc.hasNext (regex)) {
sc.next (regex)
MatchResult match = sc.match ()
String ip = match.group (1 )
String date = match.group (2 )
String day = match.group (3 )
String month = match.group (4 )
String year = match.group (5 )
System.out .println ("ip:" + ip)
System.out .printf ("%s 年月日:%s年%s月%s日\n" , date, year, month, day)
}
System.out .println ("方式2:" )
Pattern p = Pattern.compile (regex)
Matcher m = p.matcher (s)
while (m.find ()) {
String ip = m.group (1 )
String date = m.group (2 )
String day = m.group (3 )
String month = m.group (4 )
String year = m.group (5 )
System.out .println ("ip:" + ip)
System.out .printf ("%s 年月日:%s年%s月%s日\n" , date, year, month, day)
}
}
}