Movatterモバイル変換


[0]ホーム

URL:


S-JIS[2007-02-13/2023-09-24]変更履歴

文字列(String)

Javaでは、(不変の)文字列はStringクラスで扱う。


char

charは、Unicodeで1文字を表す(つもりで用意された)プリミティブ型[2007-05-03]

シングルクォーテーションで囲むと文字定数になる。[2010-06-20]
文字の中にエスケープ文字を指定可能。

Unicodeは元々16ビットで1文字を表すつもりだったらしく、char型も16bit。
しかしUnicodeは21ビットで1文字を表すよう改められたらしく、char1つでは厳密には1文字を表せなくなった。
(char1桁ではサロゲートを表せない。Java言語仕様第3版3.10.4章参照[2008-09-13]

そこでJDK1.5から、コードポイント(codePoint)が導入された。コードポイントとは、Unicodeで1文字分のコードを示す言い方。Javaではコードポイントはint型で表す。
Stringクラス等には、コードポイントを扱うメソッドが色々追加されている。→コードポイントを扱う例
コードポイント1つで1文字を表すが char1桁では足りないため、“長い文字コード(補助文字/サロゲート)”の場合はchar2桁で1文字を表すようになったらしい。(いわばUTF-16で表現)
(この2桁のcharをサロゲートペアと呼ぶ。先に来るのが上位サロゲート、次のcharが下位サロゲート[2008-04-23]

たぶん今Javaの規約を作るならchar型は24bitにしたいところだろうけど、それじゃメモリ境界的にパフォーマンスがよくないから、32bitになるんだろうなぁ…。

参考: ITアーキテクトの文字操作の変革

JDKがどのバージョンのUnicodeに準拠しているかは、CharacterクラスのJavadocに書かれている。[2008-09-13]
Java言語仕様第3版3.1章によれば、JDK1.4はUnicode3.0、JDK1.5はUnicode4.0。


String

Stringは、内容が変更されない文字列を保持する。
JavaVM(実行時)では、中身はUnicode。いわばchar型の固定長の配列。

ダブルクォーテーションで囲むと文字列定数になる。[2010-06-20]
文字列の中にタブや改行を直接入れることは出来ないので、エスケープ文字を使う。

複数行にわたる文字列はダブルクォーテーション3つで囲む。→テキストブロック(Java15以降)[2020-09-16]

文字列テキストブロック
"aaa\nbbb\n""""
aaa
bbb
"""

Stringは不変なので、初期化の際に(キャラセットの指定もせずに)newでインスタンス化するのは無意味。

馬鹿な例:

String str = new String();str = "abc";//↑直後に代入しているので初期化した値(new String())は使われず、全くの無駄!! せめてnullとか""にすべき

"abc"が代入された時点で 以前strに保持されていたStringインスタンスは使用されなくなるので、全く意味が無い。
それどころか、newで作成したときにメモリ確保が行われ、GCで解放処理を行う必要が生じてしまうので、無駄・邪魔・余計

馬鹿な例 その2:[2007-02-14]

String str = new String("abc");//内容は"abc"と同じなのに新しいオブジェクトを作っているのでムダ
String str = "abc".toString();//"abc"と全く同じオブジェクトを返す…

"abc"new String("abc")は別インスタンスだが、"abc"new String("abc").intern()は同一インスタンス。intern()は、文字列プールの中で一意となるStringを返す為。[2007-06-20]
参考:JavaFAQ: 文字列


Stringでよく使うメソッド
内容備考更新日
文字列の長さint len = str.length();サロゲートが含まれている場合、文字数でなくcharの個数となる。2008-04-23
int len = str.codePointCount(0, str.length());サロゲートを考慮した文字数(コードポイントの個数)を数える方法。2008-04-23
部分文字列String sub = str.substring(start, end);
String sub = str.substring(start);
  
トリムString trim = str.trim();前後の空白を除去したStringを返す。
(空白は、' '以下の文字コード(要するにASCIIの制御コード))
2018-10-01
String strip = str.strip();前後の空白を除去したStringを返す。(Java11以降)
(空白は、全角スペース等を含む)
2018-10-01
String strip = str.stripLeading();先頭の空白を除去したStringを返す。(Java11以降)2018-10-01
String strip = str.stripTrailing();末尾の空白を除去したStringを返す。(Java11以降)2018-10-01
等値比較if ("abc".equals(str)) 〜strがnullでもfalse(不一致)が返る。
str.equals("abc")だと、strがnullのときに例外発生。
 
if ("abc".equalsIgnoreCase(str)) 〜大文字小文字の区別をせずに比較する。2009-02-01
StringBuilder sb = 〜;
if ("abc".contentEquals(sb)) 〜
StringBuilder・StringBuffer(CharSequence)と比較する。2010-03-06
大小比較if (str.compareTo("abc") == 0) 〜  
if (str.compareToIgnoreCase("abc") == 0) 〜大文字小文字の区別をせずに比較する。 
if (CharSequence.compare(str, "abc") == 0) 〜CharSequenceに比較用のstaticメソッドがある。(Java11以降)2018-10-01
包含チェックif (str.contains("abc")) 〜文字列が含まれているかどうかをチェックする。
→含まれる文字列を正規表現で表したい場合はMatcher#find()を使う。
2014-09-28
先頭比較if (str.startsWith("a")) 〜  
末尾比較if (str.endsWith("c")) 〜  
空文字列チェックif (str.isEmpty()) 〜やっている事はstr.length()==0。(JDK1.6以降) 
if (str.isBlank()) 〜空白文字のみかどうか。(Java11以降)
(空白は、Character#isWhitespace()で判定)
2018-10-01
文字探索int n = str.indexOf('c');先頭から探索し、見つけた位置を返す。見つからなかった場合は-1を返す。 
int n = str.indexOf('c', 1, str.length() - 1);指定された範囲から探索する。(Java21以降)2023-09-24
int n = str.lastIndexOf('c');末尾から探索し、見つけた位置を返す。見つからなかった場合は-1を返す。 
分割String[] ss = "a,b,c".split(",");
→{ "a", "b", "c" }
指定された文字(正規表現)で分割する。(JDK1.4以降)
JDK1.7以前とJDK1.8以降の違い[2019-06-02]

第2引数を付けると、その個数まで分割される。
-1だと無制限、0だと末尾がカンマのみで構成されていた場合にその項目は無視される。
第2引数を省略した場合は、0を指定したのと同じ扱い。

2009-02-02
String[] ss = str.split(",", 個数);2009-05-10
String[] ss = "a,b;c".splitWithDelimiters("[,;]", -1);
→{ "a", ",", "b", ";", "c" }
文字列を分割する。分割文字も結果に含まれる。(Java21以降)2023-09-24
Stream<String> ss = str.lines();改行コードで分割する。(Java11以降)2018-10-01
結合String joined = String.join(", ", "a", "b", "c");
→"a, b, c"
第1引数を区切り文字として、複数の文字列を結合した文字列を作成する。(JDK1.8以降)
(ScalaのmkStringの劣化版みたいな感じ)
StringJoiner
2014-03-19
List<String> list = Arrays.asList("a", "b", "c");
String joined = String.join(", ", list);
String s = "abc".repeat(3);
→"abcabcabc"
指定回数繰り返した文字列を返す。(Java11以降)
0を指定した場合は空文字列が返る。
2018-10-01
エンコード変換byte[] bytes = str.getBytes("MS932");
String str = new String(bytes,"MS932");
JDK1.6から、キャラセットの文字列の代わりにCharsetがそのまま使えるようになった。 
整形System.out.println(String.format("%d:%s", n, str));

System.out.printf("%d:%s", n, str);
C言語のprintfと同様($は使えるが*は使えない)のフォーマット出力を行う。→Javaの書式
JDK1.5から使用可能。これは可変長引数を使っているので、JDK1.5以上でコンパイルする必要がある。
書式変換の実体はFormatterクラス
2008-05-20
String s = "%d:%s".formatted(n, str);Java15以降。(主にテキストブロックで使用する)2020-09-16
"abc\ndef".indent(2)

"  abc\n  def\n"
各行にインデントを付与する。(Java12以降)
引数が負の値の場合はインデントを除去する。
2019-03-31
変換Integer n = "123".transform(Integer::valueOf)

Integer n = Integer.valueOf("123")と同等
Stringを引数にとるメソッドを受け取り、それを実行する。(Java12以降)2019-03-31

String#split()

String#split()で空文字列を指定した場合の結果が、JDK1.7以前とJDK1.8以降で異なる。[2019-06-02]

String[] r = "abc".split("");System.out.println(r.length);for (String s : r) {System.out.printf("[%s]\n", s);}
JDK1.7以前JDK1.8以降
4
[]
[a]
[b]
[c]
3
[a]
[b]
[c]

StringBuffer/StringBuilder

StringBufferStringBuilderは、可変の文字列を扱う。というか内容変更可能な文字列。[/2007-02-21]

StringBuilderはJDK1.5で追加されたクラスで、やれる事はStringBufferと同様。
JDK1.5では、今までのStringBufferはStringBuilderのMTセーフ版という位置づけになったらしい。というか、StringBufferは元々各メソッドがsynchronizedされていたので、されていないStringBuilderが作られた、というのが正しい。
すなわち、スレッドを使わない(複数スレッドから並行してアクセスしない)のであれば、StringBuilderの方が高速でお勧めらしい。

文字列の結合にはいくつかの方法があるが、Stringの足し算(「+演算子」を使った文字列の結合)は、実質的にStringBuffer#append()(JDK1.5以降ではStringBuilder)が使われる。

実態のイメージ: (→どう変換されるかの実験

str +="abc";↓StringBuilder sb = new StringBuilder();sb.append(str);sb.append("abc");str = sb.toString();

したがって、文字列の結合を繰り返すような処理でStringのまま結合すると毎回StringBuilderが作られて無駄なので、(効率を重視するならば)最初から明示的にStringBuilderを使う方が良い。→ループでの文字列結合の比較
また、(ループせずに)単独で使う場合でも、場合によってはString#concat()を使う方が早い。

StringBuilderは(StringBufferも)、コンストラクターで容量を指定しない場合、初期容量は16文字分。[2007-02-22]
appendすることによって容量を超えると拡張されて その分効率が落ちるので、必要な容量が多いことが分かっているのであれば、コンストラクターでサイズをちゃんと指定した方が良い。

StringBuffer/StringBuilderよく使うメソッド
内容備考更新日
文字列の追加sb.append("abc");
sb.append('c');
sb.append(100);
Stringの結合「+」で使えるは全て使える。 
文字列を繰り返し追加sb.repeat("a", 3);Java21以降2023-09-24
String取得String str = sb.toString();  
文字列の長さint len = sb.length();  
文字列クリアsb.setLength(0);  
比較sb.compareTo(sb2) == 0Java11以降2018-10-01

PrintWriter/PrintStream

StringBuilder(やStringBuffer)には、残念ながら改行コード付きで文字列を入れるメソッドは無い。[2008-07-26]
そういう事をしたい場合は、PrintWriterStringWriter)やPrintStreamを利用してみるといいかも。

StringWriter sw = new StringWriter();PrintWriter pw = new PrintWriter(sw);pw.println("abc");pw.println("def");pw.printf("%d%n", 123);pw.close();String str = sw.toString();System.out.print(str);
ByteArrayOutputStream bos = new ByteArrayOutputStream();PrintStream ps = new PrintStream(bos);ps.println("abc");ps.println("def");ps.printf("%d%n", 123);ps.close();String str = bos.toString();System.out.print(str);

見た目はどちらもprintln()だしprintf()も使えるので、どっちも同じようなものだけど…
10万回実行してみた時間では、StringWriter版が約1900ミリ秒、PrintStream版が約5500ミリ秒だったので、StringWriterを使う方が断然良さそうだ。
(上記のソースを10万回実行、つまりWriterやOutputStreamのインスタンス生成まで含めてループさせている)

StringWriterの中身はStringBufferなので、getBuffer()でStringBufferを取得できる。
でも、StringBufferかぁ。“MTセーフなWriter”としては当然なんだろうけど。
でもこういう時にはStringBuilder版が欲しいなぁ。

てゆーか、素直にSringBuilderにappendln()があればいいんだよな…。

ちなみにこれらの場合、BufferedWriterやBufferedOutputStreamでラップすると、余計に遅くなる。[2009-02-14]
new PrintWriter(new BufferedWriter(sw))で3600ミリ秒、new PrintStream(new BufferedOutputStream(bos))は6300ミリ秒。
インスタンス生成は除いてprintln()とprintf()だけ10万回ループさせると、バッファリングしてもしなくてもあまり変わらず、Writerが1400ミリ秒、Streamが1900ミリ秒くらい。
バッファリングはファイル相手とかでないとあまり意味が無いのかも。インスタンス生成まで含めてループする場合は、バッファリングのインスタンス生成が増える分だけ遅くなってしまう訳か。


StringJoiner

JDK1.8で、区切り文字を指定して1つの文字列に結合するクラスが用意された。[2014-03-19]

import java.util.StringJoiner;
 出力内容備考
String.join()String joined = String.join(", ", "abc", "def", "ghi");abc, def, ghiString.join()の内部ではStringJoinerが使われている。
List<String> list = Arrays.asList("abc", "def", "ghi");
String joined = String.join(", ", list);
StringJoinerList<String> list = Arrays.asList("a", "b", "c");
StringJoiner joiner = newStringJoiner(", ", "<", ">");
for (String s : list) {
    joiner.add(s);
}
System.out.println(joiner.toString());
<a, b, c>内部ではStringBuilderを使っているので、速度はStringBuilderと遜色ないらしい。
(→acro-engineerさんのあなたのJavaコードをスッキリさせる、地味に便利な新API 10選(後編)のStringJoiner)
ただし、内部のStringBuilderの初期サイズを指定できないので、大データになる場合は注意が必要そう。
List<String> list = Arrays.asList("a", "b", "c");
StringJoiner joiner = newStringJoiner(", ", "<", ">");
list.forEach(s -> joiner.add(s));
System.out.println(joiner.toString());

コードポイントの例

コードポイントを扱う例。[2017-01-28]

内容備考
コードポイント数の取得int len = str.codePointCount(0, str.length()); 
コードポイント一覧の取得for (int i = 0; i < str.length();) {
  int c = str.codePointAt(i);

  i += Character.charCount(c);
}
参考:stackoverflow
IntStream stream = str.codePoints();JDK1.8
StringBuilderへの追加sb.appendCodePoint(c); 
コードポイントの判定boolean b = Character.isValidCodePoint(c); 
コードポイントからcharへの変換char[] chars = Character.toChars(c); 
char high = Character.highSurrogate(c);JDK1.7
char low  = Character.lowSurrogate(c);JDK1.7
サロゲートの判定boolean b = Character.isSurrogate(ch);JDK1.7
boolean b = Character.isSurrogatePair(high, low); 
charからコードポイントへの変換int c = Character.toCodePoint(high, low); 

文字のStream

JDK1.8で、文字列(StringStringBuilder等のCharSequence)を「文字(charコードポイント)が並んでいるもの(Stream)」として扱えるようになった。[2014-04-29]

CharSequence#chars()でcharのStream、CharSequence#codePoints()でコードポイントのStreamを取得できる。
ただし、具体的にはどちらもIntStreamクラス、すなわちintのStreamになる。
特にchars()はcharのStreamなのでCharStreamといった専用のクラスになって欲しいところだし、あるいはStream<Character>の方がしっくり来るのだが。
しかしcharの為だけにCharStreamを作るのは労力が大きすぎるだろうし、Stream<Character>だとボクシングが発生するのでいまいちな感じ。
そもそもcharのStreamだとサロゲートを扱うのが困難だろうから、基本的にコードポイントの方が使われるはず。
そういった判断からIntStreamを使うようにしたのだと思う。

文字のStreamの例
 chars()codePoints()Scala相当
allMatch
全てが数字かどうかを判定する例
String s = "123";
boolean isNumber = s.chars()
  .allMatch(c-> Character.isDigit((char) c));
String s = "123";
boolean isNumber = s.codePoints()
  .allMatch(Character::isDigit);
val s = "123"
val isNumber = s.forall(_.isDigit)
anyMatch
半角英小文字があるかを判定する例
String s = "12a3";
boolean contains = s.chars()
  .anyMatch(c-> 'a' <= c && c <= 'z');
String s = "12a3";
boolean contains = s.codePoints()
  .anyMatch(c-> 'a' <= c && c <= 'z');
val s = "12a3"
val contains = s.exists(c => 'a' <= c && c <= 'z')
filter
数字のみ抽出する例
(出力は"123456789"
String s = "123:456-789";
String t = s.chars()
  .filter(c-> Character.isDigit((char) c))
  .collect(
    StringBuilder::new,
    (sb, c)-> sb.append((char) c),
    StringBuilder::append)
  .toString();
String s = "123:456-789";
String t = s.codePoints()
  .filter(Character::isDigit)
  .collect(
    StringBuilder::new,
    StringBuilder::appendCodePoint,
    StringBuilder::append)
  .toString();
val s = "123:456-789"
val t = s.filter(_.isDigit)
map
大文字小文字を入れ替える例
(出力は"abcDEF123"
String s = "ABCdef123";
String t = s.chars()
  .map(c->
    Character.isUpperCase((char) c) ? Character.toLowerCase((char) c) :
    Character.isLowerCase((char) c) ? Character.toUpperCase((char) c) : c)
  .collect(
    StringBuilder::new,
    (sb, c)-> sb.append((char) c),
    StringBuilder::append)
  .toString();
String s = "ABCdef123";
String t = s.codePoints()
  .map(c->
    Character.isUpperCase(c) ? Character.toLowerCase(c) :
    Character.isLowerCase(c) ? Character.toUpperCase(c) : c)
  .collect(
    StringBuilder::new,
    StringBuilder::appendCodePoint,
    StringBuilder::append)
  .toString();
val s = "ABCdef123"
val t = s.map(c =>
  if (c.isUpper) c.toLower else
  if (c.isLower) c.toUpper else c)
mapToObj
大文字を「_小文字」に変える例
(出力は"example_model"
String s = "exampleModel";
Stream<String> ss = s.chars()
  .mapToObj(c->
    Character.isUpperCase((char) c) ?
      "_" + Character.toLowerCase((char) c) :
      Character.toString((char) c));
String t = ss.collect(Collectors.joining());
String s = "exampleModel";
Stream<String> ss = s.codePoints()
  .mapToObj(c->
    Character.isUpperCase(c) ?
      "_" + String.valueOf(Character.toChars(Character.toLowerCase(c))) :
      String.valueOf(Character.toChars(c)));
String t = ss.collect(Collectors.joining());
val s = "exampleModel"
val ss = s.map(c =>
  if (c.isUpper) "_" + c.toLower
  else c.toString)
val t = ss.mkString
flatMap
大文字を「_小文字」に変える例
(出力は"example_model"
String s = "exampleModel";
String t = s.chars()
  .flatMap(c->
    Character.isUpperCase((char) c) ?
      IntStream.of('_', Character.toLowerCase((char) c)) :
      IntStream.of(c))
  .collect(
    StringBuilder::new,
    (sb, c)-> sb.append((char) c),
    StringBuilder::append)
  .toString();
String s = "exampleModel";
String t = s.codePoints()
  .flatMap(c->
    Character.isUpperCase(c) ?
      IntStream.of('_', Character.toLowerCase(c)) :
      IntStream.of(c))
  .collect(
    StringBuilder::new,
    StringBuilder::appendCodePoint,
    StringBuilder::append)
  .toString();
val s = "exampleModel"
val t = s.flatMap(c =>
  if (c.isUpper) Seq('_', c.toLower)
  else Seq(c))

最後にcollectメソッドを使わないとStringに変換できないところが痛いorz
IntStreamには(Streamのような)「引数が1つ(Collectorインターフェースだけを受け取る)のcollectメソッド」が無いので、別途用意しておくことも出来ないし。


文字列の結合

文字列の連結には いくつかのコーディング方法がある。[2007-07-01]

方式概要備考お勧め
+演算子Stringの文字列を結合する演算子コーディングは楽。
実質的には、JDK1.4まではStringBuffer、JDK1.5以降は効率の良いStringBuilderが使用される。
単独行の場合は+演算子
concatStringの文字列を結合するメソッドconcat()にnullを渡すとNullPointerExceptionになる。
また、String以外の型には対応していないので
渡すのであればString.valueOf(値)とする。
 
appendStringBuilderの文字列を結合するメソッドappend()にnullを渡すと"null"という文字列になる。
また、String以外の型にも対応している。
ループの場合はStringBuilder
char配列charの配列で結合concat()append()も、内部ではこれと同様のことをしている。 

文字列の結合は、Javaの仕様上はStringクラスに対して「+演算子」を使って行うことが出来る。
しかしStringは固定文字列しか扱えないので、文字列の結合とは、実質的には「結合された新しい文字列のStringインスタンスを作っている」ことになる。

もうちょっと言えば、文字列は結局charの配列なので、新しいchar配列を作り出してそこにコピーしていることになる。
StringもStringBuilderも、内部ではcharの配列を持っている。(Stringはchar配列の内容を変更しない、StringBuilderは変更する、というのが設計上の違い)
「+演算子」もconcat()もappend()も、内部ではchar配列上で2つの文字群をつなげて新しいStringを作っている事に変わりは無い。


単独での結合にどれくらいの時間がかかるか実験してみた。(→マシンスペック
同じ結合を100万回繰り返して、ミリ秒単位で時間を出力。つまり一回当たりの実行時間がナノ秒単位で出ることになる。
(ここで言う「単独」とは、次の処理で使われない、完結した文字列を作り出すことを指している)

long start = System.currentTimeMillis();String str = "";for (int i = 0; i < 1000000; i++) {str = add("abc", "def");//文字列を結合するルーチンを呼ぶ}long end = System.currentTimeMillis();System.out.println(end - start);System.out.println(str);
2つの文字列の結合を単独で行う場合
方式概要扱い易さ実行速度
+演算子Stringの文字列を結合する演算子
String str = s1 + s2;
310ns
concatStringの文字列を結合するメソッド
String str = s1.concat(s2);
150ns
appendStringBuilderの文字列を結合するメソッド
String str = new StringBuilder(s1).append(s2).toString();
280ns
char配列charの配列で結合
char[] buf = new char[s1.length() + s2.length()];int pos = 0;s1.getChars(0, s1.length(), buf, pos);
pos += s1.length();
s2.getChars(0, s2.length(), buf, pos);
String str = new String(buf);
×190ns

2つのStringを結合する場合、String#concat()とStringBuilder#append()では、concat()の方が高速。
なぜなら、どちらもnew char[]でchar配列を準備して最後にnew String()するのだが、StringBuilderを使う方法では、(最初に)StringBuilder自身のインスタンス生成をしないといけないから。
(また、concat()はnullチェック等を行わないから)

自分自身で行ったchar配列での結合がconcat()より遅い理由は、最後の「new String()」にある。
new String()では、その中で新しいchar配列を作り、渡されたchar配列から内容をコピーしている。(Stringは不変であることが仕様なので、渡された配列の参照をそのまま保持する訳にはいかないのだろう…プログラマーが書き換えたら影響を受けてしまうので)
対してconcat()では、作ったchar配列(の参照)をそのまま新しいStringで保持しているのでコピーする手間が無い分早い。(concat()が使っているStringのコンストラクターはpublicではないので、一般プログラマーは使えない)

「+演算子」を使うと実質的にはStringBuilderが使われるにも関わらず独自に使ったStringBuilderとの差が出ている訳は、具体的には「+演算子」は「new StringBuilder(String.valueOf(s1)).append(s2).toString()」に変換されているから。
このString.valueOf()の分だけ遅い。→どう変換されるかの実験


3つ以上の文字列の結合を単独で行う場合
方式概要扱い易さ実行速度
3つ4つ
+演算子Stringの文字列を結合する演算子
String str = s1 + s2 + s3;
375ns405ns
concatStringの文字列を結合するメソッド
String str = s1.concat(s2).concat(s3);
310ns460ns
appendStringBuilderの文字列を結合するメソッド
String str = new StringBuilder(s1).append(s2).append(s3).toString();
340ns380ns
char配列charの配列で結合
char[] buf = new char[s1.length() + s2.length() + s3.length()];int pos = 0;s1.getChars(0, s1.length(), buf, pos);pos += s1.length();s2.getChars(0, s2.length(), buf, pos);pos += s2.length();s3.getChars(0, s3.length(), buf, pos);String str = new String(buf);
×260ns300ns

3つ以上のStringを結合する場合は、String#concat()よりもStringBuilder#append()の方が高速になってくる。
なぜなら、concat()では(結合一回につきnew char[]とnew String()を一回ずつ行うので)結合の回数分のインスタンス生成が発生するが、StringBuilderでは(バッファサイズが足りなくならない限り)new char[]もnew String()も一回しか行わないから。

自分でchar配列を使う方法が早いのは、nullチェック等が入っていないから。(append()では、nullを"null"という文字列にしたり バッファが超えないか確認したり、色々やっている。char配列で扱う前にString.valueOf(s1)を追加すると、append()と同等かそれ以上の時間がかかるようになる)


ループして文字列を結合していって最後に1つの文字列を作り上げる場合は、実行効率面での注意が特に必要となる

文字列の結合をループ内で行う場合
方式概要扱い易さ実行速度
+演算子Stringの文字列を結合する演算子
String str = "";for (int i = 0; i < 10000; i++) {str += i;}
×1万回で980ms
concatStringの文字列を結合するメソッド
String str = "";for (int i = 0; i < 10000; i++) {str = str.concat(String.valudOf(i));}
×1万回で480ms
appendStringBuilderの文字列を結合するメソッド
StringBuilder sb = new StringBuilder();for (int i = 0; i < 1000000; i++) {sb.append(i);}String str = sb.toString();
100万回で250ms
StringBuilder sb = new StringBuilder(6*1000000);for (int i = 0; i < 1000000; i++) {sb.append(i);}String str = sb.toString();
100万回で150ms
(バッファの初期値に
最大サイズを指定)
char配列charの配列で結合
char[] buf = new char[6 * 1000000];int pos = 0;for (int i = 0; i < 1000000; i++) {String s = String.valueOf(i);s.getChars(0, s.length(), buf, pos);pos += s.length();}String str = new String(buf, 0, pos);
×100万回で230ms

StringBuilderを1つ作ってループ内でappend()していくのと 毎回Stringを作っている(「+演算子」とconcat())のでは、実行速度の桁が違う(上記の例で200〜500倍も違う)。

初期値を指定していないStringBuilder(途中でバッファの拡張が何回か行われる)より初期値を指定した(バッファサイズが変わらない)独自のchar配列の方が早いのは当然だが。
試しにStringBuilderに初期値を指定してみると、圧倒的に早くなった。これに独自char配列方式が負けたのは、String.valueOf(int)のせい。valueOf(int)が無ければ110msくらいなので、初期値指定StringBuilderよりも早くなる。
append(int)ではString.valueOf(int)(=Integer#toString(int))より効率のいいIntegerクラス内のメソッドを使ってchar配列に変換しているようだ。(これもpublicメソッドではない…)


JDK1.5で「+演算子」が実際にどう変換されるかjadを使って見てみた。

元のソース実体考察
String str = "abc" + "def";
String str = "abc" + 123;
String str = "abcdef";
String str = "abc123";
固定値同士の結合は、ちゃんと結合して固定文字列にしてくれるようだ。
String s = "def";String str = "abc" + s;
int i = 123;String str = "abc" + i;
String s = "def";String str = (new StringBuilder("abc")).append(s).toString();
int i = 123;String str = (new StringBuilder("abc")).append(i).toString();
この辺りは、素直にStringBuilderが使われている。
String s = "def";String str = 123 + s;
String s = "def";String str = (new StringBuilder(String.valueOf(123))).append(s).toString();
コンストラクターにはintをとるものが無い(厳密にはintは初期バッファサイズ指定になってしまう)ので、String.valueOf()を使って文字列に変換している。
なぜnew StringBuilder().append(123)にしないかというと、デフォルトの初期サイズが違ってくるから?
文字列を引数とするコンストラクターだと文字数+16、デフォルトだと単なる16。
String s = "abc";String str = s + "def";
String s = "abc";String str = (new StringBuilder(String.valueOf(s))).append("def").toString();
Stringならコンストラクターに直接変数を渡せば良さそうな気がするが、String.valueOf()を使っている。なぜなら、変数値がnullだと"null"という文字列にしないといけないから。
汎用的に使われる方法としては、nullの対応をしないわけにいかないのだろう。
しかし、直前に変数を初期化していればnullでないことは分かりそうなものではある…が、それは最適化の範疇なんだろうなぁ。勝手に最適化するとロード時のバイトコード変更が出来なくなる訳だし。[/2008-01-27]
String str = "abc";str += "def";str += "ghi";
String str = "abc";str = (new StringBuilder(String.valueOf(str))).append("def").toString();str = (new StringBuilder(String.valueOf(str))).append("ghi").toString();
複数行にまたがった結合だが、個別にStringBuilderが作られているなんて無駄な…。
String s = "abc";StringBuilder sb = new StringBuilder();sb.append(123 + s);
String s = "abc";StringBuilder sb = new StringBuilder();sb.append((new StringBuilder(String.valueOf(123))).append(s).toString());
メソッドの引数内で結合している場合、そこが素直にStringBuilderを使った形に変換される。
そのメソッドがStringBuilder#append()であってもおかまいなし(嘆)これまた無駄だなぁ…。

つまり、文字列の結合を1行でしか使わない場合は、わざわざ自分でStringBuilderに書き換える意味は無い。

複数行にまたがって結合する場合は、自分でStringBuilder#append()を使ってやらないと実行効率が悪い。
特にappend()の引数の中で「+演算子」を使って結合するのは、とっても無駄な感じ。
なんだか残念賞な感じだ。これがappendしたい対象のStringBuilderインスタンスを使った形に分割されれば良かったのに。
sb.append(123 + s)sb.append(123).append(s)
ただ、これもロード時のバイトコード変更を考えるとやっちゃダメなんだろうなー…。[/2008-01-27]

あとは、JITコンパイル(実行時のコンパイル)の最適化がどれくらい効いてくれるかだなー。(HotSpotって、最適化してくれるのか??)


文字列の結合に関する結論

結合を単独で使う場合、いくつの文字列を結合するかによって早さが異なるので、一概にどの方法がいいとは言えない。
しかし差が一番顕著な“2つの文字列の結合”のケースでもconcat()とappend()で2倍程度、3つ・4つの結合になれば差は縮まっていく(逆転する)ようなので、よほど速度に厳しいのでない限りは、多機能なStringBuilder(+演算子)で充分だろう。
文字列をいくつ結合するかによってコーディングを変えるなんて、ばからしい。(分かりにくいし、将来バージョンのJavaでは速い方法が変わってくるかもしれない)

自分でStringBufferやStringBuilderを使うより「+演算子」を使った方がいい理由は、JDKのバージョンによってコンパイラーが勝手に効率よい方を使ってくれること。[2007-07-04]
JDK1.5で明らかになったように、JDK1.4で「+演算子」を使っていたプログラムは(StringBufferが使われていたわけだが)、ソースを全く修正することなくJDK1.5でコンパイルし直すだけで自動的にStringBuilderが使われて実行効率がよくなる。
今後StringBuilderに替わる効率よいクラスが出来たとしたら、同じことが期待できるだろう。と言っても、そんな事そうそうあるとも思えないけどねー(爆)

でも、結合をループで使う場合や複数行で使う場合はStringBuilderを使うべきというか、使わなきゃ許さんって感じ
(また、append()の引数で文字列結合はせず、別々にappend()すること)


文字列の等値比較

文字列が等しいかどうか比較するには、==演算子でなくequals()メソッドを使う。[2008-04-16]
==はインスタンス自体が等しいかどうかの比較を意味し、equals()は保持している内容が等しいかどうかを調べる。→オブジェクトの比較イメージ

String s1 = "abc", s2 = new String("abc");」に対し、

s1 == "abc"等しい s1.equals("abc")等しい s1.intern() == "abc"等しい s1.compareTo("abc") == 0等しい
s2 == "abc"不一致 s2.equals("abc")等しい s2.intern() == "abc"等しい s2.compareTo("abc") == 0等しい
s1 == s2不一致 s1.equals(s2)等しい s1.intern() == s2.intern()等しい s1.compareTo(s2) == 0等しい

intern()文字列プールの中から一意となるStringを返すので、この戻り値同士ならば==で比較すると一致する。
しかしintern()はかなりコストが高い処理なので、実用的ではない。

for (int i = 0; i < 1000000; i++) {String s = new String("abc");if (sと"abc"が一致しているか判断) {}}
一致判断ロジック100万回ループの
おおよその時間
備考
s.equals("abc")約  90ミリ秒 
s.compareTo("abc") == 0約  90ミリ秒 
s.intern() == "abc"約 340ミリ秒 
s.intern() == "abc".intern()約 640ミリ秒 
s == "abc"約  80ミリ秒比較結果が“一致”にならないので実験対象としてはあまり意味は無いのだけれど
new String()分の時間の推測には使えるか。

等値比較の書き順

文字列変数が定数と等しいかどうかを比較する場合は「str.equals("abc")」と書くのが普通。[2008-05-02]
しかしこれだと、strがnullの場合にNullPointerExceptionが発生してしまう。
なのでnullになる可能性があるのであれば、「str != null && str.equals("abc")」とするのが正しい。

しかしこれだと判断したいのは「strが"abc"と等しいかどうか」なのに、ソース上に条件が増えてしまってちょっとくどい。
この場合、オブジェクトを逆にして「"abc".equals(str)」と書くことが出来る。
そうすれば、strがnullでもfalseになるので、意図した動作になる。

しかし基本的にはプログラムは仕様と同じになるように書くべきなので、strが必ずnull以外だという分かっている(そういう仕様である)場合は、「str.equals("abc")」と書く方がいいと思う。
基本的にnullチェックはstrに値を入れた時にすべき(もしくはメソッドの引数であれば、Javadocにnull不可の旨を書く(=nullを渡してはいけないという仕様))だと思うので、equals()を使う前にnullをはじいておけば、「str.equals("abc")」で問題ない。

コーディングそういうソースを見たときの判断
str.equals("abc")strは必ずnull以外である。(もしnullならバグ。NullPointerExceptionが起きてよし)
"abc".equals(str)strはnullが有り得る。

自分では、必ず上記の表の判断に沿うように使っている。
すなわち「"abc".equals(str)」を使うのは、nullが有り得るという時(nullチェックを省略したい手抜きプログラムを含む)のみ。


半角カタカナ→全角カタカナ→全角ひらがな変換

半角片仮名から全角片仮名への変換は、Normalizerクラス(JDK1.6以降)を使って行うことが出来る。[2012-06-15]

import java.text.Normalizer;import java.text.Normalizer.Form;
String zen = Normalizer.normalize("アイウエオ", Form.NFKC);

※半角片仮名はアイウエオ順に並んでいるが、全角片仮名は間に小文字が入っていたりして綺麗に並んでいない為、文字コードの差分を使って単純に変換することは出来ない。


全角カタカナ→全角ひらがな

全角片仮名から全角平仮名への変換(あるいはその逆)は、文字コードの差分を使って行うことが出来る。
(全角カタカナと全角ひらがなは並び順は同一なので単純に変換できる)

String from = "アイウエオ";StringBuilder sb = new StringBuilder(from.length());for (int i = 0; i < from.length(); i++) {char c = (char) (from.charAt(i) + 'あ' - 'ア');sb.append(c);}String hira = sb.toString();

全角英数→半角英数変換

全角英数から半角英数への変換もNormalizerクラス(JDK1.6以降)を使って行うことが出来る。[2014-04-29]

import java.text.Normalizer;import java.text.Normalizer.Form;
String half = Normalizer.normalize("ABC123", Form.NFKC);

※これは半角カタカナを全角カタカナに変換する方法と全く同じなので、半角カタカナが混ざっているとそれも変換されてしまう点は注意が必要。


Java目次へ戻る /新機能へ戻る /技術メモへ戻る
メールの送信先:ひしだま

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


[8]ページ先頭

©2009-2025 Movatter.jp