Java:替换流,数组,文件等中的字符串
有时,我们需要替换流,数组,文件或者大字符串中的字符串或者标记。
我们可以使用String.replace()方法,但是对于大量数据和大量替换,这将导致性能下降。为什么?
String.replace()方法创建一个新的String实例,该实例是原始String的副本,其中包含替换项。如果字符串的大小为1 MB,则最终将得到两个字符串,每个字符串的大小均为1 MB。如果必须执行5次替换,则必须在上一次replace()返回的字符串上每次调用replace()5次,如下所示:
String data = "1234567890"; // imagine a large string loaded from a file
data.replace("12", "ab")
.replace("34", "cd")
.replace("56", "ef")
.replace("78", "gh")
.replace("90", "ij")
结果将是原始字符串的5个副本,并且总内存消耗是原始数据的5倍。可以想像,此方法执行效果很差,并且伸缩性不佳。使用String.replace()方法的O符号为:
O(N * M)
...其中N =字符串的大小,M =要执行的替换次数。
TokenReplacingReader
在这里,我将不使用String.replace()方法,而是提供一种不同的,更具可扩展性的解决方案,称为TokenReplacingReader。首先,我将解释其理论上的工作原理,然后在本文结尾处为我们提供工作代码。
TokenReplacingReader从标准的java.io.Reader读取字符数据。
然后,应用程序通过TokenReplacingReader读取数据。应用程序从" TokenReplacingReader"读取的数据将是从" TokenReplacingReader"使用的" Reader"读取的数据,所有令牌均被新值替换。如果需要将数据写入磁盘或者某些输出流,则应用程序本身必须这样做。
当" TokenReplacingReader"以" $ {tokenName}"形式在该数据中找到一个令牌时,它将调用" ITokenResolver"以获取要插入到字符流中的值而不是令牌。
ITokenResolver是一个我们可以自己实现的接口。因此,我们自己的令牌解析器可以从适合应用程序的任何位置(如Map,数据库,JNDI目录等)查找令牌值。令牌名(不包含$ {})被传递给ITokenResolver.resolveToken (String tokenName)方法。
TokenReplacingReader本身是java.io.Reader的子类,因此任何可以使用Reader的类都可以使用TokenReplacingReader。
TokenReplacingReader用法示例
这是一个如何使用TokenReplacingReader的例子:
public static void main(String[] args) throws IOException {
Map<String, String> tokens = new HashMap<String, String>();
tokens.put("token1", "value1");
tokens.put("token2", "JJ ROCKS!!!");
MapTokenResolver resolver = new MapTokenResolver(tokens);
Reader source =
new StringReader("1234567890${token1}abcdefg${token2}XYZITokenResolver resolver = ... ; // get ITokenResolver instance.
Reader reader = new TokenReplacingReader(
new InputStreamReader(inputStream), resolver);
Reader reader = new TokenReplacingReader(
new FileReader(new File("c:\file.txt"), resolver);
Reader reader = new TokenReplacingReader(
new CharArrayReader(charArray), resolver);
Reader reader = new TokenReplacingReader(
new StringReader("biiig string...."), resolver);
0");
Reader reader = new TokenReplacingReader(source, resolver);
int data = reader.read();
while(data != -1){
System.out.print((char) data);
data = reader.read();
}
}
输入字符串中的两个标记$ {token1}和$ {token2}将替换为值value1和JJ ROCKS !!!。这些值由MapTokenResolver(一个ITokenResolver实现,通过在Map中查找值来解析)返回。
以下是一些其他示例,这些示例显示了如何使用TokenReplacingReader替换字符流,数组,文件和大字符串中的令牌。
O(N + M)
TokenReplacingReader性能
TokenReplacingReader使用的内存不如String.replace()方法那么多。数据在读取时被修改,因此所有数据仅被复制一次(但不再复制)。由于数据是逐字符复制的,因此内存消耗不会比正在读取的缓冲区/数据流大很多。
令牌替换的速度取决于我们对ITokenResolver接口的实现。
TokenReplacingReader的O符号是:
public class TokenReplacingReader extends Reader {
protected PushbackReader pushbackReader = null;
protected ITokenResolver tokenResolver = null;
protected StringBuilder tokenNameBuffer = new StringBuilder();
protected String tokenValue = null;
protected int tokenValueIndex = 0;
public TokenReplacingReader(Reader source, ITokenResolver resolver) {
this.pushbackReader = new PushbackReader(source, 2);
this.tokenResolver = resolver;
}
public int read(CharBuffer target) throws IOException {
throw new RuntimeException("Operation 不支持");
}
public int read() throws IOException {
if(this.tokenValue != null){
if(this.tokenValueIndex < this.tokenValue.length()){
return this.tokenValue.charAt(this.tokenValueIndex++);
}
if(this.tokenValueIndex == this.tokenValue.length()){
this.tokenValue = null;
this.tokenValueIndex = 0;
}
}
int data = this.pushbackReader.read();
if(data != '$') return data;
data = this.pushbackReader.read();
if(data != '{'){
this.pushbackReader.unread(data);
return '$';
}
this.tokenNameBuffer.delete(0, this.tokenNameBuffer.length());
data = this.pushbackReader.read();
while(data != '}'){
this.tokenNameBuffer.append((char) data);
data = this.pushbackReader.read();
}
this.tokenValue = this.tokenResolver
.resolveToken(this.tokenNameBuffer.toString());
if(this.tokenValue == null){
this.tokenValue = "${"+ this.tokenNameBuffer.toString() + "}";
}
if(this.tokenValue.length() == 0){
return read();
}
return this.tokenValue.charAt(this.tokenValueIndex++);
}
public int read(char cbuf[]) throws IOException {
return read(cbuf, 0, cbuf.length);
}
public int read(char cbuf[], int off, int len) throws IOException {
int charsRead = 0;
for(int i=0; i<len; i++){
int nextChar = read();
if(nextChar == -1) {
if(charsRead == 0){
charsRead = -1;
}
break;
}
charsRead = i + 1;
cbuf[off + i] = (char) nextChar;
}
return charsRead;
}
public void close() throws IOException {
this.pushbackReader.close();
}
public long skip(long n) throws IOException {
throw new RuntimeException("Operation 不支持");
}
public boolean ready() throws IOException {
return this.pushbackReader.ready();
}
public boolean markSupported() {
return false;
}
public void mark(int readAheadLimit) throws IOException {
throw new RuntimeException("Operation 不支持");
}
public void reset() throws IOException {
throw new RuntimeException("Operation 不支持");
}
}
...其中N是替换令牌的数据大小,M是替换次数。
这比String.replace()方法的O(N \ * M)更快。
更多用途
我们可以创建TokenReplacingReader的变体,该变体可以用单个字符值替换XML实体(例如&)。或者创建一种类似于脚本的小型语言作为令牌,该语言可以在令牌中获取参数,调用可重用函数等。只有想像力为使用此类令牌替换机制设置的限制。
另外,由于TokenReplacingReader是一个java.io.Reader,并且它是从Reader本身获取字符的,因此我们可以将其与其他java.io.Reader或者InputStreams进行链接事物(例如解压缩,解密,从UTF-8,UTF-16转换等)
TokenReplacingReader代码
这是TokenReplacingReader的代码,它是ITokenResolver接口的代码。我们还可以在GitHub上访问TokenReplacingReader代码。
注意:并非所有方法都已实现。仅向我们展示TokenReplacingReader的工作原理。我们可以自己实现其余的(如果需要)。
public interface ITokenResolver {
public String resolveToken(String tokenName);
}
public class MapTokenResolver implements ITokenResolver {
protected Map<String, String> tokenMap = new HashMap<String, String>();
public MapTokenResolver(Map<String, String> tokenMap) {
this.tokenMap = tokenMap;
}
public String resolveToken(String tokenName) {
return this.tokenMap.get(tokenName);
}
}
这是一个ITokenResolver实现示例,它在Map中查找令牌值。
##代码##
