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中查找令牌值。
##代码##