不可变String
String对象是不可变的 String类中每一个看起来会修改String值的方法 实际上都是创建了一个全新的String对象 以包含修改后的字符串内容 而最初的String对象则丝毫未动
当把q传给upcase()方法时 实际传递的是引用的一个拷贝 其实 每当把String对象作为方法的参数时 都会复制一份引用 而该引用所指的对象其实一直待在单一的物理位置上 从未动过
重载 + 与StringBuilder
不可变性会带来一定的效率问题 为String对象重载的 + 操作符就是一个例子 重载的意思是 一个操作符在应用于特定的类时 被赋予了特殊的意义(用于String的 + 与 += 是Java中仅有的两个重载过的操作符 而Java并不允许程序员重载任何操作符)
操作符 + 可以用来连接String
想看看以上代码到底是如何工作的吗 可以用JDK自带的工具javap来反编译以上代码 命令如下
这里的-c标志表示将生成JVM字节码
编译器自动引入了java.lang.StringBuilder类 虽然我们在源代码中并没有使用StringBuilder类 但是编译器却自作主张地使用了它 因为它更高效
在这个例子中 编译器创建了一个StringBuilder对象 用以构造最终的String 并为每个字符串调用一次StringBuilder的append()方法 总计四次 最后调用toString()生成结果 并存为s(使用的命令为astore_2)
现在 也许你会觉得可以随意使用String对象 反正编译器会为你自动地优化性能 可是在这之前 让我们更深入地看看编译器能为我们优化到什么程度 下面的程序采用两种方式生成一个String 方法一使用了多个String对象 方法二在代码中使用了StringBuilder
现在运行javap -c WitherStringBuilder 可以看到两个方法对应的(简化过的)字节码 首先是implicit()方法
要注意的重点是 StringBuilder是在循环之内构造的 这意味着每经过循环一次 就会创建一个新的StringBuilder对象
下面是explicit()方法对应的字节码
可以看到 不仅循环部分的代码更简短 更简单 而且它只生成了一个StringBuilder对象 显式地创建StringBuilder还允许你预先为其指定大小 如果你已经知道最终的字符串大概有多长 那预先指定StringBuilder的大小可以避免多次重新分配缓冲
因此 当你为一个类编写toString()方法时 如果字符串操作比较简单 那就可以信赖编译器 它会为你合理地构造最终的字符串结果 但是 如果你要在toString()方法中使用循环 那么最好自己创建一个StringBuilder对象 用它来构造最终的结果 请参考以下示例
最终的结果是用append()语句一点点拼接起来的 如果你想走捷径 例如append(a+ “:” +c) 那编译器就会掉入陷阱 从而为你另外创建一个StringBuilder对象处理括号内的字符串操作
如果拿不准该用哪种方式 随时可以用javap来分析你的程序
StringBuilder是Java SE5引入的 在这之前Java用的是StringBuffer 后者是线程安全的 因此开销也会大些 所以在Java SE5/6中 字符串操作应该还会更快一点
无意识的递归
Java中的每个类从根本上都是继承自Object 标准容器类自然也不例外 因此容器类都有toString()方法 并且覆写了该方法 使得它生成的String结果能够表达容器自身 以及容器所包含的对象 例如ArrayList.toString() 它会遍历ArrayList中包含的所有对象 调用每个元素上的toString()方法
如果你希望toString()方法打印出对象的内存地址 也许你会考虑使用this关键字
当你创建了InfiniteRecursion对象 并将其打印出来的时候 你会得到一串非常长的异常 如果你将该InfiniteRecursion对象存入一个ArrayList中 然后打印该ArrayList 你也会得到同样的异常 其实 当如下代码运行时
这里发生了自动类型转换 由InfiniteRecursion类型转换成String类型 因为编译器看到一个String对象后面跟着一个 + 而再后面的对象不是String 于是编译器试着将this转换成一个String 它怎么转换呢 正是通过调用this上的toString()方法 于是就发生了递归调用
如果你真的想要打印出对象的内存地址 应该调用Object.toString()方法 这才是负责此任务的方法 所以 你不该使用this 而是应该调用super.toString()方法
String上的操作
以下是String对象具备的一些基本方法 重载的方法归纳在同一行中
从这个表中可以看出 当需要改变字符串的内容时 String类的方法都会返回一个新的String对象 同时 如果内容没有发生改变 String的方法只是返回指向原对象的引用而已 这可以节约存储空间以及避免额外的开销
格式化输出
printf()
这一行代码在运行的时候 首先将x的值插入到%d的位置 然后将y的值插入到%f的位置 这些占位符称作格式修饰符 它们不但说明了插入数据的位置 同时还说明了将插入什么类型的变量 以及如何对其格式化 在这个例子中 %d表示x是一个整数 %f表示y是以一个浮点数(float或者double)
System.out.format()
Java SE5引入的format方法可用于PrintStream或PrintWriter对象 其中也包括System.out对象
Formatter类
可以将Formatter看作一个翻译器 它将你的格式化字符串与数据翻译成需要的结果 当你创建一个Formatter对象的时候 需要向其构造器传递一些信息 告诉它最终的结果将向哪里输出
所有的tommy都将输出到System.out 而所有的terry则都输出到System.out的一个别名中 Formatter的构造器经过重载可以接受多种输出目的地 不过最常用的还是PrintStream() OutputStream和File
格式化说明符
在插入数据时 如果想要控制空格与对齐 你需要更精细复杂的格式修饰符 以下是其抽象的语法
最常见的应用是控制一个域的最小尺寸 这可以通过指定width来实现 Formatter对象通过在必要时添加空格 来确保一个域至少达到某个长度 在默认的情况下 数据是右对齐 不过可以通过使用 - 标志来改变对齐方向
与width相对的是precision 它用来指明最大尺寸 width可以应用于各种类型的数据转换 并且其行为方式都一样 precision则不然 不是所有类型的数据都能使用precision 而且 应用于不同类型的数据转换时 precision的意义也不同 在将precision应用于String时 它表示打印String时输出字符的最大数量 而在将precision应用于浮点数时 它表示小数部分要显示出来的位数(默认是6位小数) 如果小数位数过多则舍入 太少则在尾部补零 由于整数没有小数部分 所以precision无法应用于整数 如果你对整数应用precision 则会触发异常
下面的程序应用格式修饰符来打印一个购物收据
Formatter转换
下面的表格包含了最常用的类型转换
下面的程序演示了这些转换是如何工作的
被注释的代码表示 针对相应类型的变量 这些转换是无效的 如果执行这些转换 则会触发异常
注意 程序中的每个变量都用到了b转换 虽然它对各种类型都是合法的 但其行为却不一定与你想象的一致 对于boolean基本类型或Boolean对象 其转换结果是对应的true或false 但是 对其他类型的参数 只要该参数不为null 那转换的结果就永远都是true 即使是数字0 转换结果依然为true 而这在其他语言中(包括C) 往往转换为false 所以 将b应用于非布尔类型的对象时请格外小心
String.format()
String.format()是一个static方法 它接受与Formatter.format()方法一样的参数 但返回一个String对象 当你只需使用format()方法一次的时候 String.format()用起来很方便
其实在String.format()内部 它也是创建一个Formatter对象 然后将你传入的参数转给该Formatter
正则表达式
基础
一般来说 正则表达式就是以某种方式来描述字符串 因此你可以说 如果一个字符串含有这些东西 那么它就是我正在找的东西 例如 要找一个数字 它可能有一个负号在最前面 那你就写一个负号加上一个问号 就像这样
-?
要描述一个整数 你可以说它有一位或多位阿拉伯数字 在正则表达式中 用\d表示一位数字 如果在其他语言中使用过正则表达式 那你立刻就能发现Java对反斜线\的不同处理 在其他语言中 \表示 我想要在正则表达式中插入一个普通的(字面上的)反斜线 请不要给它任何特殊的意义 而在Java中 \的意思是 我要插入一个正则表达式的反斜线 所以其后的字符具有特殊的意义 例如 如果你想表示一位数字 那么正则表达式应该是\d 如果你想插入一个普通的反斜线 则应该这样\\ 不过换行和制表符之类的东西只需使用单反斜线 \n\t
要表示 一个或多个之前的表达式 应该使用+ 所以 如果要表示 可能有一个负号 后面跟着一位或多位数字 可以这样
-?\d+
应用正则表达式的最简单的途径 就是利用String类内建的功能 例如 你可以检查一个String是否匹配如上所述的正则表达式
String类还自带了一个非常有用的正则表达式工具——split()方法 其功能是 将字符串从正则表达式匹配的地方切开
String.split()还有一个重载的版本 它允许你限制字符串分割的次数
String类自带的最后一个正则表达式工具是 替换 你可以只替换正则表达式第一个匹配的字串 或是替换所有匹配的地方
String之外的正则表达式还有更强大的替换工具 例如 可以通过方法调用执行替换 而且 如果正则表达式不是只使用一次的话 非String对象的正则表达式明显具备更佳的性能
创建正则表达式
以下是一些创建字符类的典型方式 以及一些预定义的类
下面的每一个正则表达式都能成功匹配字符序列 Rudolph
量词
量词描述了一个模式吸收输入文本的方式
- 贪婪型:量词总是贪婪的 除非有其他的选项被设置 贪婪表达式会为所有可能的模式发现尽可能多的匹配 导致此问题的一个典型理由就是假定我们的模式仅能匹配第一个可能的字符组 如果它是贪婪的 那么它就会继续往下匹配
- 勉强型:用问号来指定 这个量词匹配满足模式所需的最少字符数 因此也称作懒惰的 最少匹配的 非贪婪的 或不贪婪的
- 占有型:目前 这种类型的量词只有在Java语言中才可用(在其他语言中不可用) 并且也更高级 因此我们大概不会立刻用到它 当正则表达式被应用于字符串时 它会产生相当多的状态 以便在匹配失效时可以回溯 而 占有的 量词并不保存这些中间状态 因此它们可以防止回溯 它们常常用于防止正则表达式失控 因此可以使正则表达式执行起来更有效
表达式X通常必须要用圆括号括起来 以便它能够按照我们期望的效果去执行
接口CharSequence从CharBuffer String StringBuffer StringBuilder类之中抽象出了字符序列的一般化定义
因此 这些类都实现了该接口 多数正则表达式操作都接受CharSequence类型的参数
Pattern和Matcher
一般来说 比起功能有限的String类 我们更愿意构造功能强大的正则表达式对象 只需导入java.util.regex包 然后用static Pattern.compile()方法来编译你的正则表达式即可 它会根据你的String类型的正则表达式生成一个Pattern对象 接下来 把你想要检索的字符串传入Pattern对象的matcher()方法 matcher()方法会生成一个Matcher对象 它有很多功能可用 例如 它的replaceAll()方法能够将所有匹配的部分都替换成你传入的参数
下面的类可以用来测试正则表达式 看看它们能否匹配一个输入字符串
find()
Matcher.fin()方法可用来在CharSequence中查找多个匹配
组(Groups)
组是用括号划分的正则表达式 可以根据组的编号来引用某个组 组号为0的表示整个表达式 组号1表示被第一对括号括起的组 依此类推 因此 在下面这个表达式
A(B©)D
中有三个组 组0是ABCD 组1是BC 组2是C
Matcher对象提供了一系列方法 用以获取与组相关的信息 public int groupCount()返回该匹配器的模式中的分组数目 第0组不包括在内 public String group()返回前一次匹配操作(例如find())的第0组(整个匹配) public String group(int i)返回在前一次匹配操作期间指定的组号 如果匹配成功 但是指定的组没有匹配输入字符串的任何部分 则将会返回null public int start(int group)返回在前一次匹配操作中寻找到的组的起始索引 public int end(int group)返回在前一次匹配操作中寻找到的组的最后一个字符索引加一的值
下面是正则表达式组的例子
start()与end()
在匹配操作成功之后 start()返回先前匹配的起始位置的索引 而end()返回所匹配的最后字符的索引加一的值 匹配操作失败之后(或先于一个正在进行的匹配操作去尝试)调用start()或end()将会产生IllegalStateException 下面的示例还同时展示了matches()和lookingAt()的用法
注意 find()可以在输入的任意位置定位正则表达式 而lookingAt()和matches()只有在正则表达式与输入的最开始处就开始匹配时才会成功 matches()只有在整个输入都匹配正则表达式时才会成功 而lookingAt() 只要输入的第一部分匹配就会成功
Pattern()标记
Pattern类的compile()方法还有另一个版本 它接受一个标记参数 以调整匹配的行为
其中的flag来自以下Pattern类中的常量
在这些标记中 Pattern.CASE_INSENSITIVE Pattern.MULTILINE以及Pattern.COMMENTS(对声明或文档有用)特别有用 请注意 你可以直接在正则表达式中使用其中的大多数标记 只需要将上表中括号括起的字符插入到正则表达式中 你希望它起作用的位置即可
你还可以通过 或 (|) 操作符组合多个标记的功能
split()
split()方法将输入字符串断开成字符串对象数组 断开边界由下列正则表达式确定
这是一个快速而方便的方法 可以按照通用边界断开输入文本
第二种形式的split()方法可以限制将输入分割成字符串的数量
替换操作
正则表达式特别便于替换文本 它提供了许多方法 replaceFirst(String replacement)以参数字符串replacement替换掉第一个匹配成功的部分 replaceAll(String replacement)以参数字符串replacement替换所有匹配成功的部分 appendReplacement(StringBuffer sbuf, String replacement)执行渐进式的替换 而不是像replaceFirst()和replaceAll()那样只替换第一个匹配或全部匹配 这是一个非常重要的方法 它允许你调用其他方法来生成或处理replacement(replaceFirst()和replaceAll()则只能使用一个固定的字符串) 使你能够以编程的方式将目标分割成组 从而具备更强大的替换功能 appendTail(StringBuffer sbuf) 在执行了一次或多次appendReplacement()之后 调用此方法可以将输入字符串余下的部分复制到sbuf中
下面的程序演示了如何使用这些替换方法 开头部分注释掉的文本 就是正则表达式要处理的输入字符串
reset()
通过reset()方法 可以将现有的Matcher对象应用于一个新的字符序列
使用不带参数的reset()方法 可以将Matcher对象重新设置到当前字符序列的起始位置
正则表达式与Java I/O
下面的例子将向你演示 如何应用正则表达式在一个文件中进行搜索匹配操作 它有两个参数:文件名以及要匹配的正则表达式 输出的是有匹配的部分以及匹配部分在行中的位置
扫描输入
到目前为止 从文件或标准输入读取数据还是一件相当痛苦的事情 一般的解决之道就是读入一行文本 对其进行分词 然后使用Integer Double等类的各种解析方法来解析数据
readLine()方法将一行输入转为String对象 如果每一行数据正好对应一个输入值 那这个方法也还可行 但是 如果两个输入值在同一行中 事情就不好办了 我们必须分解这个行 才能分别翻译所需的输入值 在这个例子中 分解的操作发生在创建numArray时 不过要注意 split()方法是J2SE1.4中的方法 所以在这之前 你还必须做些别的准备
终于 Java SE5新增了Scanner类 它可以大大减轻扫描输入的工作负担
在前面的两个例子中 一个有趣的区别是 BetterRead.java没有针对IOException添加try区块 因为 Scanner有一个假设 在输入结束时会抛出IOException 所以Scanner会把IOException吞掉 不过 通过 ioException()方法 你可以找到最近发生的异常 因此 你可以在必要时检查它
Scanner定界符
在默认的情况下 Scanner根据空白字符对输入进行分词 但是你可以用正则表达式指定自己所需的定界符
这个例子使用逗号(包括逗号前后任意的空白字符)作为定界符 同样的技术也可以用来读取逗号分隔的文件 我们可以用useDelimiter()来设置定界符 同时 还有一个delimiter()方法 用来返回当前正在作为定界符使用的Pattern对象
用正则表达式扫描
除了能够扫描基本类型之外 你还可以使用自定义的正则表达式进行扫描 这在扫描复杂数据的时候非常有用 下面的例子将扫描一个防火墙日志文件中记录的威胁数据
当next()方法配合指定的正则表达式使用时 将找到下一个匹配该模式的输入部分 调用match()方法就可以获得匹配的结果 如上所示 它的工作方式与之前看到正则表达式匹配相似 在配合正则表达式使用扫描时 有一点需要注意 它仅仅针对下一个输入分词进行匹配 如果你的正则表达式中含有定界符 那永远都不可能匹配成功
StringTokenizer
在Java引入正则表达式(J2SE1.4)和Scanner类(Java SE5)之前 分割字符串的唯一方法是使用StringTokenizer来分词 不过 现在有了正则表达式和Scanner 我们可以使用更加简单 更加简洁的方式来完成同样的工作了 下面的例子中 我们将StringTokenizer与另两种技术做了一个比较
使用正则表达式或Scanner对象 我们能够以更加复杂的模式来分割一个字符串 而这对于StringTokenizer来说就很困难了 基本上 我们可以放心的说 StringTokenizer已经可以废弃不用了