第十三章:字符串

字符串

  • 字符串操作是计算机程序设计中最常见的行为。这章让我们来了解一下这个既熟悉又陌生的编程伙伴。

不可变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());//HELLO
        System.out.println(s);//hello
    }
}
  • 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字节码:
//如果看不懂,可以去看看<深入理解java虚拟机>第六章,我就没看懂
D:\>javap -c Test
Compiled from "Test.java"
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String hange
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: ldc           #5                  // String abc
      12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      15: aload_1
      16: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: ldc           #7                  // String def
      21: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      24: bipush        47
      26: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
      29: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      32: astore_2
      33: getstatic     #10                 // Field java/lang/System.out:Ljava/io/PrintStream;
      36: aload_2
      37: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      40: return
}
  • 通过字节码可以看出,编译器自动为我们产生了StringBuilder对象(Java SE5 引入,较StringBuffer速度更快。StringBuffer线程安全开销较大。),并且使用了它的append()方法,以避免产生多余的无用对象。最后又调用了StringBuildertoString()方法返回了一个字符串。如果我们认为编译器已经能够帮我们优化好了,就不注意编程习惯的话,那就大错特错了。看下面的例子:
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                  // Method java/lang/Object."<init>":()V
       4: return

  public java.lang.String test1(java.lang.String[]);
    Code:
       0: ldc           #2                  // String
       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                  // class java/lang/StringBuilder
      28: dup
      29: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      32: aload_2
      33: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      36: aload         6
      38: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      41: invokevirtual #6                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      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                  // class java/lang/StringBuilder
       3: dup
       4: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
       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                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      36: pop
      37: iinc          5, 1
      40: goto          17
      43: aload_2
      44: invokevirtual #6                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      47: areturn
}
  • 我们可以得知第一种方式在退出一个循环后即会返回toString()然后第二次还得重新生成StringBuilder对象。而第二种就没有这个行为。由此可见,平常在进行字符串的拼接时,使用StringBuilder代替“+”进行拼接能够带来性能的提升。
  • 关于StringBuilder的用法,我相信只要看看方法名即可,这里就不用过多介绍了,关于字符串的插入、拼接、删除、替换、子串它都有对应的方法。只有你想不到,没有它做不到。

无意识的递归

  • 我们知道直接将一个对象引用和字符串相加,其实会调用对象的toString()方法。或者更通俗一点,我们在一个需要字符串的位置输入了一个对象引用,那么就会调用这个对象的toString()方法。所以如果我们写出如下代码:
public class Test {
    public String toString() {
        return this + "haha";
        //应该使用下面这种写法
        //return super.toString() + "haha";
    }
    public static void main(String args[]) {
        //Exception in thread "main" java.lang.StackOverflowError
        //System.out.println(new Test());
        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";//hello 不算构造方法,不过最常用。
        String s1 = new String("hello");//hello
        String s2 = new String(new char[]{'h', 'i'});//hi
        String s3 = new String(new char[]{'h', 'e', 'l'}, 0, 2);//he
        //-----------------常用方法举例
        char c0 = s0.charAt(1);//e
        int c0_ucode = s0.codePointAt(1);//101
        int h_ucode = s0.codePointBefore(1);//h的unicode ,如果输入0会抛异常
        System.out.println(s0.compareTo("hello"));//正数前者大于后者,0为相等。
        System.out.println(s0.compareTo("hi"));//e的unicode - i的unicode
        System.out.println(s0.compareTo("he"));//如果前缀都相同,返回s0.length - s3.length
        System.out.println(s0.compareToIgnoreCase("HeLlO"));//不考虑大小写
        System.out.println(s0.concat(" world"));//字符串拼接
        System.out.println(s0 + " world");//字符串拼接
        System.out.println(s0.contains("el"));//boolean 是否包含
        System.out.println(s0.contentEquals(new StringBuffer("hello")));//boolean 比较实现CharSequence接口的类型
        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);//将s0的0到2(不包含2)下标的字符数组加入到从4的下标开始的dst中。
        System.out.println(dst);
        //lastIndexOf的用法和indexOf相同,区别是反向搜索。
        System.out.println(s0.indexOf(101));//输入的是unicode码
        System.out.println("helelo".indexOf(101, 2));//从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");//不能直接用miaoch 或者 "mia" + "och",那样会直接在常量池里创建对象。
        hange.intern();//jdk1.7会使其成为常量池里的对象。
        System.out.println(hange == "miaoch");//jdk1.7 true jdk1.6 false
        String hello = new String("hello");
        hello.intern();//已有,返回常量"hello",对hello无修改
        System.out.println(hello == "hello");//false
        System.out.println("".isEmpty());//true
        System.out.println("hange".matches("han[g-h]{1}e"));//正则表达式后面再说
        System.out.println("hange".regionMatches(1, "**ang*", 2, 3));//用于比较子串是否equals
        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,]", ""));//replaceFirst只匹配一次。
        System.out.println(Arrays.toString("ha,nge,e,ee".split(",")));//split是很常用的一个方法,注意传入的是正则表达式而不是普通的字符串。
        System.out.println(Arrays.toString("ha nge e ee".split("\\s", 3)));//最多分解成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));//很多重载方法,主要就是将其他类型转换成String,常用来将数字转换为字符串。

    }
}

格式化输出

  • 我们可以用System.out.printf()来格式化输出一个字符串。这个方法其实直接调用了System.out.format(),而该方法又调用了Formatter类的成员方法。学过C基本都知道是如何使用的,这里再巩固一下。

Formatter

  • String.format()printf()都是借用了Formatterformat()成员方法。可以不必知道它是如何实现的,但必须了解一下它的使用方法。
类型写法备注
%d整数型
%cUnicode字符
%bboolean值
%sString
%f浮点数(十进制)
%a浮点数(十六进制)
%e浮点数(科学计数)
%x整数(十六进制)
%o整数(八进制)
%h散列码(十六进制)
%tx日期与时间类型(x代表不同的日期与时间转换符)
%%字符“%”
%n字符“\n”
时间类型的转换符样例格式
c星期六 十月 27 14:21:20 CST 2007包括全部日期和时间信息
F2007-10-27“年-月-日”格式
D10/27/07“月/日/年”格式
r02:25:51 下午“HH:MM:SS PM”格式(12时制)
T14:28:16“HH:MM:SS”格式(24时制)
R14: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);//用0去补齐
        System.out.printf("%5d\n", 123456);//如果超出限制个数则正常显示
        System.out.printf("%-10.4f\n", 1234.12345);//小数点对f保留几位小数
        System.out.printf("%-10.4e\n", 1234.12345);//小数点对e保留几位有效数字
        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);//添加0x前缀
        System.out.printf("%#10o\n", 12345678);//添加0前缀
        System.out.printf("%.3f %<.4f\n", 1.23456);//%<格式化前一个转换符所描述的参数
        System.out.printf("%2$s %3$s\n", 99, "abc", "efg");//2$设置索引
        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));//和gourp()一样
            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的构造器可以接受任何类型的输入对象,包括FileInputStreamString或者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 
  • 现在让我们来做一个比较困难的问题:
//给出如下字符串:如何解析获得ip地址和日期:
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按行获得字符串,然后取@符号之前之后的字符串
//但是如果我要求按照年月日的顺序去排序,你可能又得重新设置后一段的顺序。
//下面我来介绍另外一种使用Scanner的方式。
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);
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值