2017年9月23日土曜日

2: どんな配列も別の配列型にキャストすべきでない(オートキャストもさせるべきでない)理由

<このシリーズの、前の記事 | このシリーズの目次 | このシリーズの、次の記事>

本文 START

配列はコンバートしよう、キャストでなく

話題: Javaプログラミング言語

どんなObject配列インスタンスもString配列にはキャストできない、たとえObject配列がString要素だけしか持っていなくても

-Hypothesizer

ああ、String要素だけを格納したObject []インスタンスをString []型にはキャストできないのか。そうか . . .

-Rebutter

勿論、できないとも。

-Hypothesizer

勿論?それはそんなに自明なことか?うーん、我々が話しているのは以下のようなプログラムのことだ。

@Java Source Code
 void aMethod (String [] p_stringsArray) {
  for (String l_string: p_stringsArray) {
   System.out.println (l_string);
  }
 }
 
 void anotherMethod () {
  Object [] l_objectsArray = new Object [2];
  l_objectsArray [0] = "A String";
  l_objectsArray [1] = "Another String";
  aMethod ( (String []) l_objectsArray);
 }

このプログラムは正常にコンパイルできるが、実行時に例外、"java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String"を投げる。

-Rebutter

正常にコンパイルできるのか?むしろそのことに驚いた、実行時例外にではなく。

-Hypothesizer

そうかい?なぜだ?

-Rebutter

それはオブジェクト指向言語の原則に違反していて、コンパイラはコンパイル時にそれを知っているはずだから。

-Hypothesizer

私はむしろ、「このキャストで何の害があるんだ?」と思ったんだが。

-Rebutter

何の害もないと思うかもしれないが、それは君のプログラムに限ったことだ。一般的に言って、そのような、オブジェクト指向言語の原則への違反は何らかの害を引き起こす可能性がある。

-Hypothesizer

このキャストがどう、オブジェクト指向言語の原則への違反なのか分からないんだが。

-Rebutter

うーん、オブジェクトのキャストがプリミティブのキャストと本質的に違うことは理解しているか?

-Hypothesizer

していると思っている。

-Rebutter

復習しておこう。

プリミティブのキャストとオブジェクトのキャストの違いを復習しよう

-Hypothesizer

プリミティブがキャストされるときは、値そのものがコンバートされる。例えば、以下のキャストを考えてみよう。

@Java Source Code
  int l_int = 1;
  short l_short = (short) l_int;

'l_int'に格納されている値は4バイト値であり、'l_short'に格納されている値は2バイト値だ。値はキャストによってコンバートされている。

他方、オブジェクトがキャストされるときは、オブジェクトそのものが全然コンバートされない。例えば、以下のキャストを考えよう。

@Java Source Code
  Object l_object = "A String";
  String l_string = (String) l_object;

このStringオブジェクト、"A String"はキャストによってコンバートされていない。オブジェクトにとってキャストはオブジェクトのコンバートではなく、プログラマーからコンパイラへの請け合いにすぎない。つまり、「コンパイラさん、あなたは'l_object'が一種のStringだと知らないかもしれませんが、私は知っています。確かにそれは一種のStringなんですよ。だから、お願いだからコンパイルを通してください。何か問題があれば、責任は私が受けます。」

-Rebutter

そう、理解しているようだ。コンパイラは、'l_object'が一種のStringである可能性があれば、コンパイラエラーを出さない。しかし、'l_object'が実行時に実際に一種のStringでなければ、ランタイムが'java.lang.ClassCastException'例外を投げる。

Object配列インスタンスが一種のString配列であることは決してない

-Hypothesizer

それでどうした?

-Rebutter

どうやら、君は、Object配列インスタンスが一種のString配列であることは決してないことを理解していないようだ。

-Hypothesizer

そうなのか?

-Rebutter

Strings配列の定義を考えてみろ。

-Hypothesizer

String []の定義は、要素としてどんなStringインスタンスも入れられる配列だ。

-Rebutter

そうではない。Stringインスタンスしか入れられないという点が重要だ。さもなければ、以下のようなことはできないだろう。

@Java Source Code
 void aMethod (String [] p_stringsArray) {
  for (String l_string: p_stringsArray) {
   System.out.println (l_string);
  }
 }
-Hypothesizer

ああ、全ての要素がStringであることが保証されていなければ、それはできない。すると、String []のもっと正確な定義は、要素としてどんなStringインスタンスも、しかしStringインスタンスだけが入れられる配列だ。

-Rebutter

何かが「一種のString []」だと言えるには、その何かは、この定義を満たしていなければならない。それが定義の意味だ。何らかのObject []がこの定義を満たすか?

-Hypothesizer

どんなObject []インスタンスにも、Stringでない要素を入れることはできるので、確かに、どんなObject []インスタンスも、この定義を満たさない。たとえ、あるObject []インスタンスがある時点でString要素のみを持っていたとしても、だからといって、そのObject []インスタンスにString要素だけを入れられるということにはならない。上の最初のプログラムについて言えば、あのObject []インスタンスがキャスト時にString要素のみを持っていたとしても、その後にそれにStringでない要素を、例えば別スレッドから、入れることはできる。Object []インスタンスは、変更不能ではないので、それがある時点でString要素のみを持っているからといって、一種のString []であるとは言えない。

だから、どんなObject []インスタンスも一種のString []ではなく、そのようなキャストは決して許されるべきではない。なぜなら、そうでないと、String []にキャストしたものが一種のString []であると信頼できなくなるから。どんなきてれつな世界になるだろう?String []変数の値が一種のString []ではないかもしれないなんて . . .。それでは、タイプ定義は何のためにあるのか?

実のところ、String配列インスタンスが一種のObject配列であることも決してない

-Hypothesizer

他方で、こんなことができる。

@Java Source Code
 void aMethod (Object [] p_objectsArray) {
  for (Object l_object: p_objectsArray) {
   System.out.println (l_object.toString ());
  }
 }
 
 void anotherMethod () {
  String [] l_stringsArray = new String [2];
  l_stringsArray [0] = "A String";
  l_stringsArray [1] = "Another String";
  aMethod (l_stringsArray);
 }
-Rebutter

はあ?それができるって?

-Hypothesizer

実は、できる。

-Rebutter

そのString []はObject []にキャストする必要すらないのか?

-Hypothesizer

自動的にObject []にキャストされるようだ。そして、プログラムは実行時例外も投げない。

-Rebutter

それは問題だぞ。分かるだろうが、どんなString []も一種のObject []ではない。

-Hypothesizer

それは、前のものよりも明らかだ。Object []の定義は、要素としてどんなObjectインスタンスも、しかしObjectインスタンスだけが入れられる配列だ。どんなString []もこの定義を満たさないので(それにはStringインスタンスだけが入れられる)、それは一種のObject []ではない。

-Rebutter

Stringが一種のObjectであることの類推から、String []が一種のObject []であるかのように感じられるかもしれないが、そうではない。

-Hypothesizer

どんなタイプ'XXX'の配列も一種のタイプ'YYY'の配列ではない、'XXX'が'YYY'でない場合は。'XXX'が'YYY'の祖先であろうが子孫であろうが関係ない。だkら、配列タイプ間のキャストは、本質的に不当だ。

-Rebutter

コンパイラは、そのような不当なキャストをコンパイル時に検出できるはずだ。実のところ、上の最初のプログラムは正常のコンパイルされるべきでない。

-Hypothesizer

コンパイラは、以下のようなことができるからそれをコンパイルさせるわけ。

@Java Source Code
 void anotherMethod () {
  Object [] l_objectsArray = new String [2];
  l_objectsArray [0] = "A String";
  l_objectsArray [1] = "Another String";
  aMethod ( (String []) l_objectsArray);
 }
-Rebutter

平たく言って、 "Object [] l_objectsArray = new String [2];"は許されるべきでない。このString []インスタンスは一種のObject []ではないのだから。コンパイラがそれを許すから、"(String []) l_objectsArray"が可能になる。コンパイラが正当なタイプキャストチェックを始めから行なっていれば、配列タイプ間のすべてのキャストは、コンパイル時にエラーとして検出できていたはずだ。

そのような不当なキャストを許すことの害

-Hypothesizer

そのような不当なキャストを許すことの害を理解するために、以下のようなプログラムを考えてみよう。

@Java Source Code
 public static void aMethod (Object [] p_objectsArray) {
  if (p_objectsArray != null && p_objectsArray.length > 0) {
   p_objectsArray [0] = new Integer (1);
   for (Object l_object: p_objectsArray) {
    System.out.println (l_object.toString ());
   }
  }
 }
-Rebutter

これは、極めて正常で落ちる可能性のないプログラムに思える。このメソッドの作者は、引数をObject []として宣言し、したがって、そのとおりに扱った。もし、このメソッドが落ちるのであれば、それは不条理だろう。

-Hypothesizer

しかし、これは、以下のように簡単に落とすことができる。

@Java Source Code
 public static void anotherMethod () {
  String [] l_stringsArray = new String [2];
  l_stringsArray [0] = "A String";
  l_stringsArray [1] = "Another String";
  aMethod (l_stringsArray);
 }
-Rebutter

引数をObject []だと宣言しているのに、それが一種のObject []だと信頼できないというのはあんまりだ。. . . しかも、我々はキャストをしてすらいない。

-Hypothesizer

キャストは自己責任で行なうので、キャストが間違っていれば問題が起こることはある。しかし、キャストを全然してもいないのに、このようなタイプ例外が起こってしまう . . .

それでは、どうすべきなのか?

-Hypothesizer

結局、配列を別の配列タイプの変数に入れる(配列をメソッドの引数に渡す場合も含む)ときは、キャストは忘れて、遠慮なくコンバートすべきだ。

-Rebutter

キャストは、自動キャストを含め、コンパイラが許すかもしれず、それで何事もなくすむ可能性さえあるが、決して正当ではない。

-Hypothesizer

配列のコンバートは、'java.util.Arrays.copyOf'を使って、以下のように行なえる。

@Java Source Code
import java.util.Arrays;

  String [] l_stringsArray = new String [2];
  l_stringsArray [0] = "A String";
  l_stringsArray [1] = "Another String";
  Object [] l_objectsArray = Arrays. <Object, String>copyOf (l_stringsArray, l_stringsArray.length, Object [].class);
-Rebutter

キャストに比べて長々しく見えるかもしれないが、これは必要なことだ。

-Hypothesizer

この関数を呼ぶのが面倒に感じられるのであれば、私が思うには、配列をコピーするシンタックスシュガーがプログラム言語に実装されるべきだ。不当で奇妙なキャストを許すというのが解決策であるべきでは絶対ない。

それに、様々なオブジェクト配列タイプ(要素がプリミティブでない配列タイプ)の基底クラスが望まれるのであれば、すべてのオブジェクト配列タイプの基底抽象クラスがあるべきだと思う。

-Rebutter

察するに、その基底クラスでは、要素をObjectタイプとして取り出すことができるが、何も入れられない。というのも、それに何を入れられるかはそのタイプからは判断できないから。

-Hypothesizer

そう、しかし、この基底タイプをメソッドの引数として使用でき、引数が参照しているインスタンスを非抽象配列タイプ(例えば、String [])にキャストすることもできる。もし、それが本当にString []インスタンスであればだが。そうすれば、それに要素を入れられる。このキャストは不当でも奇妙でもなく、オブジェクト指向言語のすべての原則に沿っている。

本文 END

<このシリーズの、前の記事 | このシリーズの目次 | このシリーズの、次の記事>