Java:替换流,数组,文件等中的字符串

时间:2020-01-09 10:35:36  来源:igfitidea点击:

有时,我们需要替换流,数组,文件或者大字符串中的字符串或者标记。

我们可以使用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}XYZ
ITokenResolver 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}将替换为值value1JJ 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中查找令牌值。

##代码##