Java9から導入されたMatcherのreplaceAllを使って文字列を置換する

f:id:excite-kazuki:20210605183312p:plain

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 PHPで書かれたAPIからSpringBoot / Javaでリビルドを進めていく上で、 独自のイメージタグからHTMLのimgタグに置換する処理をJavaで実装することになりました。 シンプルでスッキリとしたコードを書くことができたので共有します。

導入

下記図のように、ある文字列の中から正規表現に一致する文字列を使用して別の文字列に置換したいとします。 このとき、文字列.replaceAll(正規表現, 置換する文字列) では正規表現に一致した文字列の中身を考慮することができないため、意図した結果を得ることができません。 そのため、何らかの方法で正規表現に一致した文字列を取得した上で置換する必要があります。

f:id:excite-kazuki:20210601193055p:plain

今回扱うデータ

今回扱うデータは下記のとおりです。 独自のイメージタグがあり、イメージタグの中にパス、長さ、高さの3つの要素が含まれているとします。

@Data
public class ImageTag {
    private String src;
    private int width;
    private int height;

    public String toImg() {
        return String.format(
                "<img src=\"%s\" width=\"%d\" height=\"%d\"/>", 
                src, width, height
        );
    }
}

また、イメージタグの正規表現imageTagPatternとして扱い、 抽出したイメージタグからImageTagクラスのインスタンスを生成するメソッドをtoImageTagとして扱います。

// [IMAGE|src|width|height] に一致するような正規表現(省略)
Pattern imageTagPattern = Pattern.compile("...");

// [IMAGE|src|width|height]を受け取り、ImageTagのインスタンスを生成するメソッド
public ImageTag toImageTag(String imageTagStr) {
  /* src, width, heightをsplit()などを使用して抽出 */
  return new ImageTag()
            .setSrc(src)
            .setWidth(width)
            .setHeight(height);
}

結論

Java9から導入されたMatcherのreplaceAll(Function<MatchResult, String>)使用することで、正規表現に一致した文字列を置換することができます。 下記コードでは、e正規表現と一致したもの(MatchResult)を表し、返り値(String)が置換するものを表しています。 今回のケースでは、正規表現と一致した文字列のイメージタグからImageTagインスタンスを生成し、インスタンスからimgタグを作成しています。 (インスタンスを生成せずに、直接変換することもできます。)

何をしたいのかがひと目でわかり、とても見通しの良いコードに仕上げることができました👏

public String replace(String text) {
    return imageTagPattern
            .matcher(text)
            .replaceAll(e -> toImageTag(e.group()).toImg());
}

Streamを使用した他の実装方法

replaceAll(Function<MatchResult, String>)を使用せずに実装する方法についても紹介します。

イテレータを作成して繰り返し置換する方法

Java9から導入されたMatcherのresults()を使用することで、正規表現に一致した文字列をStreamで取得することができます。 イテレータを作成して引数で受け取ったtextに対して繰り返し置換を行うようにしてみました。 replaceAll(Function<MatchResult, String>)を使用したコードと比較すると、コードが長くなってしまいますが、 比較的わかりやすいコードになるのかなと思います。

public String replace1(String text) {
    final Iterator<String> imageTagIterator = imageTagPattern.matcher(text)
            .results()
            .map(e -> e.group())
            .iterator();

    while (imageTagIterator.hasNext()) {
        final String imageTagStr = imageTagIterator.next();
        final ImageTag imageTag = toImageTag(imageTagStr);
        text = text.replace(imageTagStr, imageTag.toImg());
    }
    return content;
}

AtomicReferenceを使用して繰り返し置換する方法

AtomicReferenceを使用することで、引数で受け取ったtextforEachの中で繰り返し置換することができます。 Javaでは、ラムダ式の中で外部の変数(この場合はtext)を書き換えることができないため、一工夫しないといけません。 どうしてもラムダ式の中で外部の変数を書き換えたいとき以外は、この方法を避けたほうがよいのではないかと思います。

public String replace2(String text) {
    final AtomicReference<String> reference = new AtomicReference<>();
    reference.set(text);

    imageTagPattern
            .matcher(reference)
            .results()
            .forEach(e -> {
                final String s1 = reference.get();
                final ImageTag imageTag = toImageTag(e.group());
                final String s2 = s1.replace(e.group(), i.toImg());
                reference.set(s2);
            });

    return reference.get();
}

まとめ

Java9から導入されたreplaceAll(Function<MatchResult, String>)を使用することで、シンプルな置換処理を記述することができるようになりました。 調べてみると、正規表現に一致した文字列を別の文字列に置換する記事が多くヒットしてしまい、なかなかたどり着けなかったです。 また、Java8では導入されていないため、現在プロダクトで使用しているバージョンと同じドキュメントを読むのが大事だなと感じました。

誰かのお役に立てれば幸いです!