ICU4Jで文字数をカウントする

Javaでは文字数をlength()で数えることができます。
1996年にJavaが出てきた当初は「半角も全角も1文字に数えれて便利だなー」などと思っていたわけです。 けれども、同じ1996年に策定されたUnicode 2.0で2文字分のコードを使って1文字を表すサロゲートペアが導入されて、2001年のUnicode 3.1あたりで実際にサロゲートペアで表される文字が登録されると事情が変わります。
まあ、2002年のJava 1.4ではまだサロゲートペアは扱えなかったので影響はなかったのだけど、2004年のJava 5でUnicode 4.0に対応し、JSR-204でサロゲートペアが扱えるようになったときに騒ぎになりましたね。

当時のさくらばさんの解説
J2SE 5.0 Tiger 虎の穴 Unicode 4.0 の補助文字のサポート

ただ、JSR-204ではあくまでも文字コードとしてUnicodeが扱えるようになったというもので、1文字ずつカウントするといったアプリケーションレベルの処理は導入されていません。Java 21で絵文字に対応したというのも、Unicodeに文字区分として絵文字が導入されたので対応したというものです。

ということで、アプリケーション処理として文字を扱いたいときにはICUを使います。JavaからはICU4Jですね。
ICU4J | ICU Documentation

Mavenリポジトリはこれ。

<dependency>
  <groupId>com.ibm.icu</groupId>
  <artifactId>icu4j</artifactId>
  <version>73.2</version>
</dependency>

で、文字をカウントするとこう。

サロゲートペアな絵文字の場合は、length()だと2文字とみなされます。

jshell> "こんにちは🙂".length()
$1 ==> 7

そこでICUBreakIteratorを使います。
BreakIterator (ICU4J 73)

jshell> import com.ibm.icu.text.BreakIterator

jshell> var bi = BreakIterator.getCharacterInstance()
bi ==> !!quoted_literals_only;$CR=[\p{Grapheme_Cluster_B ... ator$Regional_Indicator;.;

扱いたい文字列をsetText

jshell> bi.setText("こんにちは🙂")

で、こんな感じでカウント

int count = 0;
while (bi.next() != BreakIterator.DONE) {
    count++;
}

というのも面倒なので、Streamで。

int count = Stream.generate(() -> bi.next())
        .takeWhile(s -> s != BreakIterator.DONE)
        .count()

JShellで試すと、ちゃんと絵文字が1文字でカウントされました。

jshell> Stream.generate(() -> bi.next()).takeWhile(s -> s != BreakIterator.DONE).count()
$4 ==> 6

追記: 正規表現 \b{g}splitするのが手軽と教えてもらった

jshell> "こんにちは🙂".split("\\b{g}").length
$5 ==> 6