美文网首页
Java字符串处理

Java字符串处理

作者: 狮_子歌歌 | 来源:发表于2017-01-09 15:54 被阅读1581次

    转义字符

    在字符表中有两大类字符集,一类是Control Character,一类是Printable Character。

    对于可打印的字符,直接用其本身来表示例如,大小写字母、所有数字、所有的标点符号和一些其他符号。

    而Control Character该怎么表示?在Java中可以用编码来表示,例如\u0000表示空字符;也可以使用转义字符,例如制表符'\t'。

    转义字符

    转义字符是一种以“\”开头的字符。例如退格符用'\b'表示,换行符用'\n'表示。转义字符中的''表示它后面的字符已失去它原来的含义,转变成另外的特定含义。反斜杠与其后面的字符一起构成一个特定的字符。

    转义字符是表示字符的一种特殊形式。转义字符以反斜''开头,后面跟一个字符或一个八进制或十六进制数表示。转义字符具有特定的含义,不同于字符原有的意义,故称转义字符。

    注意转义字符是一个字符,并不是字符序列。

    通常使用转义字符表示ASCII码字符集中不可打印的控制字符和特定功能的字符。

    特定功能字符指的是在程序设计语言中,一个字符表示特别的含义,而失去了原本的意义。如用于表示字符常量的单撇号('),用于表示字符串常量的双撇号(")和反斜杠(\)等。

    这样就很好理解为什么用"."分割字符串时,必须使用"\."。调用String.split()方法时传入的第一个参数是正则表达式模版。而在正则表达式语法中"."表示特定的含义:匹配除换行符 \n 之外的任何单字符。要匹配 . ,请使用 . 。。而反斜杠在Java中也具有特定含义(具体什么含义不知道...大概是用来构成转义字的吧),所以要表示反斜杠字符就应该写成'\'。这样一来就构成了"\."。

    Java中转义字符表

    转义字符 含义
    '\60'、'\101'、'\141'分别表示字符'0'、'A'和'a' 八进制转义字符。它是由反斜杠''和随后的1~3个八进制数字构成的字符序列
    '\u0000'表示空字符 可以在字符直接量中使用 Unicode 转义序列,该转义序列由六个 ASCII 字符组成:\u 加上一个四位数值的十六进制数。
    '\r'、'\n'、'\b'、'\t'、'\f',分别表示回车,换行,退格,制表,换页 控制字符,也就是Unicode中第一类字符
    '\'、'''、'"',分别表示反斜杠,单引号,双引号 特定功能字符,在程序设计语言中具有特别含义的字符,从而通过转义字符表达原本含义

    可能对于八进制转义字符用途不是很理解。字符'0'、'A'和'a'的ASCII码的八进制值分别为60、101和141。字符集中的所有字符都可以用八进制转义字符表示。

    网上文章中还指出下列转义字符:

        点的转义:. ==> u002E 
        美元符号的转义:$ ==> u0024 
        乘方符号的转义:^ ==> u005E 
        左大括号的转义:{ ==> u007B 
        左方括号的转义:[ ==> u005B 
        左圆括号的转义:( ==> u0028 
        竖线的转义:| ==> u007C 
        右圆括号的转义:) ==> u0029 
        星号的转义:* ==> u002A 
        加号的转义:+ ==> u002B 
        问号的转义:? ==> u003F 
    

    虽然没有完全试验过,但是尝试了一个

    String str = "Java转义字符(补遗)";
    str = str.replace("\(补遗\)", "");
    System.out.println(str);
    

    执行代码会提示错误: 非法转义符,说明这些字符在Java中并没有特殊含义,不需要转义。至于为什么会造成这种错觉应该是正则表达式原因。如"\."这里面转义的是反斜杠,并不是'.'。

    参考

    java转义符

    C语言中的转义字符

    Java中的转义字符

    正则表达式

    概述

    正则表达式(regular expression)描述了一种字符串匹配的模式,可以用来检查一个串是否含有某种子串、将匹配的子串做替换或者从某个串中取出符合某个条件的子串等。

    构建正则表达式

    用多种元字符与运算符将小的表达式结合在一起来创建更大的表达式。

    正则表达式的组件可以是单个的字符、字符集合、字符范围、字符间的选择或者所有这些组件的任意组合。

    正则表达式是由普通字符(例如字符 a 到 z)以及特殊字符(称为"元字符")组成的文字模式。模式描述在搜索文本时要匹配的一个或多个字符串。正则表达式作为一个模板,将某个字符模式与所搜索的字符串进行匹配。

    普通字符

    普通字符主要指的是在正则表达式语法中没有被指定为特殊含义(这些字符被称为元字符)的所有字符(包括非打印字符,也就是Control Character)。包含了所有大小写字母、数字、所有标点符号和一些其他符号。

    Control Character

    正则表达式中的Control Character转义字符

    字符 描述
    \cx 匹配由x指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符。
    \f 匹配一个换页符。等价于 \x0c 和 \cL。
    \n 匹配一个换行符。等价于 \x0a 和 \cJ。
    \r 匹配一个回车符。等价于 \x0d 和 \cM。
    \s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。
    \S 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
    \t 匹配一个制表符。等价于 \x09 和 \cI。
    \v 匹配一个垂直制表符。等价于 \x0b 和 \cK。

    表格中类似\x0c都是ASCII表中字符十六进制编码。

    元字符

    元字符也就是正则表达式中具有特定功能的字符,主要有限定符,定位符,其他元字符。如果需要在匹配字符中包含这些元字符,那么必须转义,也就是转义字符'\元字符'

    限定符

    限定符用来指定正则表达式的一个给定组件必须要出现多少次才能满足匹配。

    字符 描述
    * 匹配前面的子表达式零次或多次。例如,zo* 能匹配 "z" 以及 "zoo"。* 等价于{0,}。
    + 匹配前面的子表达式一次或多次。例如,'zo+' 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等价于 {1,}。
    ? 匹配前面的子表达式零次或一次。例如,"do(es)?" 可以匹配 "do" 或 "does" 中的"do" 。? 等价于 {0,1}。
    {n} n 是一个非负整数。匹配确定的 n 次。例如,'o{2}' 不能匹配 "Bob" 中的 'o',但是能匹配 "food" 中的两个 o。
    {n,} n 是一个非负整数。至少匹配n 次。例如,'o{2,}' 不能匹配 "Bob" 中的 'o',但能匹配 "foooood" 中的所有 o。'o{1,}' 等价于 'o+'。'o{0,}' 则等价于 'o*'。
    {n,m} m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,"o{1,3}" 将匹配 "fooooood" 中的前三个 o。'o{0,1}' 等价于 'o?'。请注意在逗号和两个数之间不能有空格。

    *、+和?都是贪婪限定符,它们会尽可能多的匹配子表达式。在它们后面加上?就可以实现最小匹配。举个例子

    public class TestQualifier {
        public static void main(String[] args) {
            String s = "<H1>Chapter 1 – Introduction to Regular Expressions</H1>";
            System.out.println(s);
            String s1 = s.replaceAll("<.*>", "");//1
            System.out.println(s1);
            String s2 = s.replaceAll("<.*?>", "");//2
            System.out.println(s2);
        }
    }
    

    Console输出结果:

    非贪婪限定符.png

    可以明显看到区别。第一次替换由于贪婪限定符的存在,将整个字符串替换成""。第二次替换采用了非贪婪限定符,实现最小匹配,只是将<H1></H1>替换成""。

    定位符

    定位符能够将正则表达式固定到行首或行尾。它们还能够创建这样的正则表达式,这些正则表达式出现在一个单词内、在一个单词的开头或者一个单词的结尾。

    字符 描述
    ^ 匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与 \n 或 \r 之后的位置匹配。
    $ 匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与 \n 或 \r 之前的位置匹配。
    \b 匹配一个字边界,即字与空格间的位置。
    \B 非字边界匹配。

    注意,不能将限定符与定位点一起使用。由于在紧靠换行或者字边界的前面或后面不能有一个以上位置,因此不允许诸如 ^* 之类的表达式。

    其他元字符
    字符 描述
    ( ) 标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。
    . 匹配除换行符 \n 之外的任何单字符。
    [ 标记一个中括号表达式的开始。
    \ 将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。
    ^ 匹配输入字符串的开始位置,除非在方括号表达式中使用,此时它表示不接受该字符集合。
    { 标记限定符表达式的开始。
    竖直线 指明两项之间的一个选择。
    \d 匹配一个数字字符。等价于 [0-9]。
    \D 匹配一个非数字字符。等价于 [^0-9]。
    \w 匹配包括下划线的任何单词字符。等价于'[A-Za-z0-9_]'。
    \W 匹配任何非单词字符。等价于 '[^A-Za-z0-9_]'。
    \xn 匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如,'\x41' 匹配 "A"。'\x041' 则等价于 '\x04' & "1"。正则表达式中可以使用 ASCII 编码。
    \un 匹配 n,其中 n 是一个用四个十六进制数字表示的 Unicode 字符。例如, \u00A9 匹配版权符号 (?)。
    \xn 匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如,'\x41' 匹配 "A"。'\x041' 则等价于 '\x04' & "1"。正则表达式中可以使用 ASCII 编码。
    \nml 如果 n 为八进制数字 (0-3),且 m 和 l 均为八进制数字 (0-7),则匹配八进制转义值 nml。

    表格中有提到正则表达式可以使用ASCII编码,而Java采用的是UTF-16字符集(16位的Unicode字符集)。也就是说正则表达式模版中可以使用ASCII编码代替字符,但是是使用ASCII编码转换成相应的字符。

    public class TestRegexHex {
        public static void main(String[] args) {
            String str = "12A34B567N89M";
            str = str.replaceAll("[\\x41-\\x5A]", " ");
            System.out.println(str);
        }
    }
    

    Console输出:12 34 567 89。如果要匹配Unicode字符,则必须通过\un形式匹配(n就是十六进制数字,表示字符对应的Unicode编码)。

    这部分理解好像有点问题

    子表达式

    限定符表格中可以看到一个限定符( ),它就是用来创建子表达式。子表达式可以有自己的匹配模式。例如do(es)?可以匹配"do"或者"does";(T|t)h(E|e)可以匹配"ThE"or"The"or"thE"or"the"。

    注意限定符[ ]制定的匹配模式是匹配该集合中的一个字符。并不是全部字符。

    反向引用

    允许在同一正则表达式的后部引用前面的子表达式。

    所匹配的每个子表达式的字符序列都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 '\n' 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。

    因为子表达式可以嵌套另一个子表达式,所以它的位置是参与计数的左括号的位置。例如,正则表达式/([Jj]ava([Ss]cript)?)\sis\s(fun\w*)/中,嵌套的子表达式([Ss]cript)可以用\2来指代。

    可以使用非捕获元字符 '?:'、'?=' 或 '?!' 来重写捕获,忽略对相关匹配的保存。如(?:pattern)

    这里可能有个误区,引用的并不是匹配模式,而是引用上一个表达式匹配后获取的字符序列。保证引用的字符序列
    匹配。而且要缓存一定要使用()限定符。

    通配符&正则表达式

    通配符是系统命令使用,一般用来匹配文件名等系统命令操作。正则表达式是操作字符串。最明显的差异,在通配符中*通配符匹配零个或多个字符;在正则表达式中*匹配前面的子表达式零次或多次。

    参考

    上述元字符表格参考自
    正则表达式 - 语法
    ,是部分元字符,更多元字符可以参考该文章。

    正则表达式基础

    正则表达式用法总结

    Java源文件编码

    测试一个Java源文件,编码格式为GBK,修改默认encoding参数为GBK编译并运行。

    import java.io.*;
    public class TestEncoding {
        /**
        * Mac OS默认javac encoding 参数设置为UTF-8
        * 从内存中提取字符串采用的也是UTF-8编码
        * 最后控制台显示也是UTF-8
        **/
        public static void main(String[] args) {
            String str = "生生世世";
            System.out.println(str);
            String s = null;
            try {
                s = new String(str.getBytes("GBK"));
            }catch(UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            System.out.println(s);
        }
    }
    

    大致的流程就是使用GBK编码解码源文件,将"生生世世"以Unicode存储在内存中。然后从内存中以GBK编码对内存中"生生世世"Unicode进行编码,然后传递给控制台,最后控制台使用UTF-8进行解码成相应的字符导致乱码:生生世世 ��������

    参考

    java源文件编码问题

    java源文件和class文件编码详解

    Scanner

    知识点

    命令行执行字节码文件

    当源文件中第一行指定了包名,且编译时通过-d指定放置生成的类文件位置后,应该在shell切换到包所在目录下执行java 包名.类名命令。例如源文件

    package src.a.b;
    import java.io.*;
    import java.util.*;
    public class TestScanner {
        public static void main(String[] args) {
            //File file = new File("../../Regex/src/TestEncoding.java");
            //File file = new File("../Regex/src/TestEncoding.java");
            File file = null;
            String path = System.getProperty("user.dir");
            System.out.println(path);
            String[] pathArray = path.split("/");
            for(String p : pathArray) {
                System.out.println(p);
            }
            StringBuilder builder = new StringBuilder(32);
            builder.append("/");
            if(pathArray.length > 0) {
                for(String p : pathArray) {
                    if(p.equals("workspace")){
                        builder.append(p);
                        break;
                    }
                    if(!p.isEmpty()) {
                        builder.append(p + "/");
                    }
    
                }
                file = new File(builder.toString(), "Regex/src/TestEncoding.java");
            }else {
                System.out.println("no file path!");
            }
            System.out.println(file.getPath());
            InputStream input = null;
            Scanner in = null;
            try {
                input = new FileInputStream(file);
                in = new Scanner(input, "GBK");
                // while(in.hasNextLine()) {
                //     System.out.println(in.nextLine());
                // }
                do{
                    System.out.println(in.nextLine());
                }while(in.hasNextLine());
            }catch (FileNotFoundException e) {
                e.printStackTrace();
            }finally {
                in.close();
            }
        }
    }
    

    包相对路径是src/a/b,而包所在目录是Scanner,应当在该目录下执行java src.a.b.TestScanner命令。Console输出:

    Java命令行执行类文件.png

    可以从第一条打印看出,当前路径确实在Scanner下。

    参考

    Java-第四课命令行执行Java文件

    如何在代码中获取上级目录

    File类中有两个方法getParent()getParentFile()。这两个方法都可以获取上级目录,唯一不同的是返回值。前者返回String,后者返回File实例。

    那么现在的问题就是如何构建当前文件路径的File实例。可以通过File file = new File(".")来获取当前工作目录。这个目录和System.getProperty("user.dir")相同。

    但是如果这个时候直接通过file对象调用getParent()或者getParentFile()返回值都是null。大概是因为不能用相对路径来获取上级目录,这里就可以使用System.getProperty("user.dir")返回的路径来构建一个File实例,通过它来获取上级目录路径或者File对象。

    还有一种通过相对路径获取上级目录File对象的方法File file = new File("..")

    注意这里所有说的当前目录都是执行java命令行指令的目录,并不是指当前源代码的类文件路径。

    总结

    File file = new File(".");等价于System.getProperty("user.dir")指的是当前工作目录。

    File file = new File("..");指的是当前工作目录上级目录。想访问上上级目录可以使用../..

    当实例File对象传入的路径字符串以"/"开头,指定的是UNIX系统平台根目录。

    参考

    java 文件路径中的“.”,new File(".")

    Scanner用途

    在平时写demo时,主要用于获取控制台输入。当通过System.in创建一个Scanner扫描器时,控制台会一直等待输入,直到开发者敲击回车键结束,把所有输入对象传递给Scanner作为扫描对象。要获取输入的内容可以调用一系列nextXXX();方法。

    不仅如此,还可以为文件,流,字符串等创建扫描器。以此来方便的逐段扫描内容,并对扫描得到的内容做一定的处理。而且还可以指定字符集解码内容。

    Scanner是根据正则表达式来分隔,nextLine()根据系统平台的行分隔符,而next()默认分割符是空格(包括所有换行符,换页符,制表符,垂直制表符,回车符,空格),可以通过useDelimiter()来自定义分割符。

    Scanner并不能移动控制台的光标,只能够在扫面内容中移动。控制台只会一直等待开发者输入内容直到敲击回车键结束,然后把所有内容传递给Scanner。

    Scanner&BufferReader

    由于Scanner是JDK1.5提供的,所以在此之前都是通过BufferedReader来获取控制台输入。

    public class TestBufferedInput{
        public static void main(String[] args) throws Exception{
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("Enter your name: ");
            StringBuilder builder = new StringBuilder(32);
            builder.append("Your name is " + reader.readLine());
            System.out.println("Enter your age: ");
            builder.append(",you are " + reader.readLine() + " years old.");
            System.out.println("Enter your company: ");
            builder.append("You work for " + reader.readLine() + ".");
            System.out.println(builder.toString());
        }
    }
    

    相比较Scanner,BufferedReader没有那么简便,只能读取一行或者单个字符,也可以使用read(char, int int)读取多个字符。

    但是BufferedReader是字符输入流中读取文本,缓冲各个字符,从而提供字符、数组和行的高效读取!速度要比Scanner快!而且也可以设置缓冲区的大小,或者可使用默认的大小。大多数情况下,默认值就足够大了。

    Scanner取得输入数据的依据是空白字符:如空格、制表符、回车符、换行符、换页符,Scanner就会返回下一个输入。

    参考

    java.util.Scanner应用详解

    JAVA输入流和scanner的区别

    Scanner和BufferedReader的区别和用法

    字符串分割

    Java默认的分割符是空白字符:制表符,回车符,换行符,空格,换特符

    String.split()

    一般字符串分割处理第一时间想到的就是split()方法,可以直接传入正则表达式进行处理,返回一个数组。实现起来非常方便,但是效率很低。

        public static String[] splitByStringSplit(String s, String regex) throws Exception{
            String[] strArray = s.split(regex);
            if(strArray.length > 0) {
                return strArray;
            }else {
                throw new Exception("Can't split string with this regex!");
            }
        }
    

    split(String, int limit)方法中的limit参数,限制分割数目。

    split()分割完全是按照正则表达式,并不回去不区分字符串中的标识符、数和带引号的字符串,它们也不识别并跳过注释。

    StringTokenizer

    第二种可以使用效率较高的StringTokenizer来分割。是JDK专门用来提供分割字符串处理的工具类。可以根据自定义delim(分割符)进行处理,并将结果进行封装提供对应方法进行遍历取值。

    它有三种构造函数:

    1. StringTokenizer(String str),使用Java 默认的分割符,不返回分割符。
    2. StringTokenizer(String str, String delim),使用自定的分割符构造StringTokenizer对象,但是不返回分割符。
    3. StringTokenizer(String str, String delim, boolean returnDelims),使用zidingyi的分割符构造StringTokenizer对象,同时可以指定是否返回分割符。

    常用方法:

    1. hasMoreTokens();判断字符串中是否还有token。
    2. nextToken();获取下一个token。
    3. nextToken(String delim);返回当前分割符到下一个新的分割符之间的token。

    与split()类似按照指定或者默认的分割符去处理字符串。

        public static String[] splitByStringTokenizer(String s, String delim) throws Exception{
            StringTokenizer tokenizer = new StringTokenizer(s, delim);
            List<String> strList = new ArrayList<>();
            while(tokenizer.hasMoreTokens()) {
                strList.add(tokenizer.nextToken());
            }
            int count = strList.size();
            if(count > 0) {
                return strList.toArray(new String[count]);
            }else {
                throw new Exception("Can't split string with this delim!");
            }
        }
    

    substring()配合indexOf()高效分割处理

    subString()是采用了时间换取空间技术,因此它的执行效率相对会很快,但是会造成内存溢出问题,需要很好的处理内存问题。而indexOf()方法是一个执行速度非常快的方法。

        public static String[] splitByStringSubstring(String s, String delim) throws Exception{
            List<String> strList = new ArrayList<>();
            do{
                int index = s.indexOf(delim);
                if(index < 0) {
                    break;
                }
                strList.add(s.substring(0, index));
                if(index == s.length()) {
                    break;
                }
                s = s.substring(index + 1);
            }while(!s.isEmpty());
            int count = strList.size();
            if(count > 0) {
                return strList.toArray(new String[count]);
            }else {
                throw new Exception("Can't split string with this delim!");
            }
        }
    

    三种方案比较

    public class TestStringSplit{
        public static void main(String[] args) throws Exception{
            String str = "hello.java.delphi.asp.php";
            String regex = "\\.";
            String delim = ".";
            long startTime = 0;
            long endTime = 0;
            String[] strArray = null;
            startTime = System.nanoTime();
            strArray = StringSpilt.splitByStringSplit(str, regex);
            endTime = System.nanoTime();
            System.out.println("String split by split method: " + (endTime - startTime));
    
            startTime = System.nanoTime();
            strArray = StringSpilt.splitByStringTokenizer(str, delim);
            endTime = System.nanoTime();
            System.out.println("String split by StringTokenizer: " + (endTime - startTime));
    
            startTime = System.nanoTime();
            strArray = StringSpilt.splitByStringSubstring(str, delim);
            endTime = System.nanoTime();
            System.out.println("String split by substring method: " + (endTime - startTime));
        }
    }
    

    Console输出:

    String split by split method: 1671486
    
    String split by StringTokenizer: 527509
    
    String split by substring method: 87169
    

    可见效率差距。

    参考

    Java 字符串分割三种方法

    StringTokenizer的用法及示例

    相关文章

      网友评论

          本文标题:Java字符串处理

          本文链接:https://www.haomeiwen.com/subject/atrhbttx.html