S-JIS[2007-06-20/2021-03-21] 変更履歴
Javaで出来るけど「普通はしないだろ」という微妙(奇妙)なネタ。
奇妙だけど許される/仕様通り | 奇妙だから許されない |
|
|
|
|
微妙だけど、どう? | |
|
Javaのソース内で変数名やクラス名に日本語が使えるのはよく知られていると思う。
普通は英数字と「_」を使うが。他に「$」も使える。
public static void main(String[] args) { int $ = 1; String a$ = "abc"; System.out.println($ + a$); }
なんだかとってもヤな感じ(爆)
CVSを使ってると、CVSキーワードとして扱われてしまう 。
public String $Date$() {
return new java.util.Date().toString();
}
↓コミットすると
public String $Date: 2007/06/20 13:50:06 $() {
return new java.util.Date().toString();
}
これはさすがにコンパイルエラーになる(苦笑)
また、内部クラスを作ると、コンパイルした時点で「外部クラス名$内部クラス名.class
」というファイルが作られる。
一方、そういうクラス名のクラスをソースに書くことも出来る。
class Main$Sub { } public class Main { class Sub { ←この内部クラスの実体が「Main$Sub」 } }
この場合、作られるクラスファイル名が「Main$Sub.class
」で重複してしまって一つしか作られないのに、コンパイルエラーにはならない。
どうやら先に定義した「Main$Sub
」が生き残るようだが、外部のクラスから「new Main$Sub()
」をしようとすると内部クラスの方を見に行くみたいで、コンパイルエラーとなる。
また、定義順を逆にするとコンパイルエラーとなる。
public class Main { class Sub { ←この内部クラスの実体が「Main$Sub」 } } class Main$Sub { ←「型Main$Subは既に定義されています」というコンパイルエラー }
列挙型(enum)の実装でもフィールド名やメソッド名に$が使われる。[2008-06-11]
要するに、コンパイラーが自動で生成するものには$を使うということなので、プログラマーは$を使わない方が良さそうだ。
(Java言語仕様第3版3.8章には、「$を使うべきでない」と書かれている。[2008-09-13])
\uhhhhは、Unicodeの1文字を表す書き方(Unicodeエスケープ)。[2008-04-08]
\uの直後には、Unicodeの文字コードを十六進数4桁で続ける。
ソース上に直接は書けないような文字を書くのに利用できる。
char c = 'A';
等価→
char c = '\u0041';
しかし、文字列中に改行コードや引用符を書きたい場合は\u以外のエスケープ文字(「\r」「\n」や「\'」「\"」)を使うべきであって、\uでは代用できない。
例えば
「char c = '\u0027'; //「'」のつもり
」はコンパイルエラーになる。
なぜかと言うと、\uは、コンパイルの一番最初の段階(字句解析)で該当コードの文字に変換されるから。(つまりC言語やC++のプリプロセスのような位置付けなんだろう)
つまり「'\u0027'
」は「'''
」に変換されているのであり、これは誰が見てもコンパイルエラーだろう。
改行コードも同様。
char c = '\u000a'; //「\n」のつもり
↓実はこう変換される
char c = '
';
では「'\\u0027'
」なら「'\''
」になって大丈夫、と言ってみたいところだが、「\\」は「\」
そのものを指すと解釈されるのでu以降はエスケープ文字の一部とは見なされず、全体で「\u0027」という文字列だと認識されてしまってダメ。
という訳で「'\u005c\u0027'
」なら「'\''
」を表すので大丈夫。
で、実は、\uは文字列定数や文字定数内に限らず、Javaのソース上ならどこでも(コメント内でも)使える。[/2008-05-02]
つまり、以下のようなソースは正当。
int \u0061\u0062\u0063 = 1; System.out.println(abc);
Javaとしては正当と言っても、こんなソースは見たくないが(爆)
元々は、Unicodeの全文字をASCII文字だけで表すための規定らしい。
(使用目的・存在理由としてはC言語のトリグラフシーケンスと同じかもしれないが、あれよりはマシか?
トリグラフシーケンスは記号を駆使しているという意味では好きなんだけどw、実際に使ったら非常に分かりにくいだろうから…分かりにくいのは同じか(爆))
ついでに、「\」の直後の「u」はいくつ書いてもOK。
つまり、こんなの↓も問題ない。
double \u0061 = 1; \uuuuuuuu0061 *= 2;
バカである(爆) Java的に問題ないと言っても、こんなソースを書く人間の頭には問題あると思うぞ(笑)
※uが多重で書けるのは、変換の回数をカウントできるようにする為らしい。[2008-05-02]
すなわち、uが2つあれば、2回変換すれば本来の文字になる。「\uu0061」→「\u0061」→「a」
プロパティーファイルの変換を何度も行うような場合を想定しているのかな?
なお、八進数表記(\の直後に数字3桁)は\uと異なり、\r\n等のエスケープ文字と同等の扱いらしい。
char c = '\n';
等価→
char c = '\012';
ソース内のパッケージ名部分にも\uは使える。[2008-09-13]
しかしコンパイルするとパッケージ部分はディレクトリーになる為、その環境で使えない文字になるかもしれない。
そういう場合は、「@」に変換されるんだそうだ。
まぁ、あまりお世話になることは無いだろうなぁ。パッケージ名やクラス名に半角英数字以外は使わない方がいいということに変わりは無い。
Javaで配列の変数を宣言する方法は二種類ある。[2008-05-11]
int a[] = new int[10]; // C言語風 int[] a = new int[10]; // Javaでは基本的にこちらが使われる
多次元配列を宣言するには、これらを組み合わせることが出来る。
int a[][] = new int[2][3]; int[] a[] = new int[2][3]; int[][] a = new int[2][3]; // Javaでは基本的にこれが使われる
しかし推奨されるのは一番最後の方法。
なぜかというと、一行で複数の変数を宣言する場合に勘違いを生む可能性があるから。
int a[][], b; // bはintの変数(配列ではない) int[] a[], b; // bはintの一次元配列(b[]) int[][] a, b; // bはintの二次元配列(aと同じくb[][])
// a,b,cとも等しく二次元配列 int a[][], b[][], c[][]; int[] a[], b[], c[]; int[][] a, b, c;
逆にそういった混乱を招かない為に、変数宣言は基本的に一行で一変数しか書かないことも推奨されている。
(Javadocの事を考えても、フィールドでは一行に複数変数を宣言しない方がいい)
メソッドの戻り値が配列の場合も、C言語と同様に[]
を後ろに書くことが出来る。[2019-12-08]
int[] method() { 〜 } int method()[] { 〜 }
さすがにこれを使っているのは見たことが無いが^^;
Java16で導入されたレコードでは、レコードコンポーネントに配列を指定する際に、C言語形式の宣言は出来ない。[2021-03-21]
record Example(int[] a) { // OK }
record Example(int a[]) { // コンパイルエラー }
Javaでは、ソース上に書かれたクラスのメソッドではなく、実行時の実体(インスタンス)のメソッドが呼ばれる。[2008-05-02]
class A { public void method() { System.out.println("A"); } }
class B extends A { @Override public void method() { System.out.println("B"); } }
A a = new B(); …BのインスタンスをAの変数に代入 a.method(); …Aのメソッド呼び出しに見えるが、実行時にはBのmethod()が呼ばれる
似た話で、シリアライズでも、フィールドに宣言されたクラスでなく、フィールドに入っている実行時の実体(インスタンス)のクラスがシリアライズ可能である必要がある。
(逆に可変長引数ではキャストによってコンパイル時に動作が変わるケースがある。[2008-06-27])
(フィールドもキャストによってアクセス対象の変数が変わる。[2008-08-09])
C++では、変数宣言時のクラスの関数(メソッド)に「virtual」が付いていたときだけサブクラス(実際のインスタンス)の関数が呼ばれる。
付いていないときは変数宣言したクラスの関数が呼ばれる(ソースの見た目どおり)。
class A { public: void method() { printf("A\n"); } }; |
class A { public: virtual void method() { printf("A\n"); } }; |
class A { public: void method() { printf("A\n"); } }; |
class B : public A { public: void method() { printf("B\n"); } }; |
class B : public A { public: virtual void method() { printf("B\n"); } }; |
|
class C : public B { public: void method() { printf("C\n"); } }; |
||
A *a = new B(); a->method(); //Aのmethod()のvirtualの有無により |
C *c = new C(); B *b = c; A *a = c; a->method(); //Aが呼ばれる b->method(); //Cが呼ばれる c->method(); //Cが呼ばれる |
|
クラスAの関数が呼ばれる。 | クラスBの関数が呼ばれる。 |
つまりC++では、ソース上に指定されているクラスの関数をそのまま呼び出そうとする。
そしてその関数にvirtualが付いているとき(仮想関数であるとき)だけ、そのインスタンスの仮想テーブル(vtable)を使ってサブクラスでオーバーライドされている関数があるかどうか探し、あればそれを呼び出す仕組みになっている。
Javaでは、全てのメソッドが「C++でいうvirtual」付きの関数(仮想関数)として扱われる。
(Javaのインストラクションコードでは、メソッド呼び出しは「invokevirtual」という名前の命令になっている)
なお、C++では、newしたインスタンスを解放する「delete
」を忘れずにコーディングすること!
上記の例ではデストラクターは定義していないが、C++でデストラクターを書くときはvirtual付きにするのが定石。そうすればdelete時に実体のサブクラスのデストラクターがちゃんと呼ばれるようになる。
(virtual付きのデストラクターが無い場合、「delete a;
」では実行時に落ちる)
C#では、クラス名を前に付けることにより、どのクラスのメソッドを呼ぶか決めることが出来るらしい。
しかしJavaにはそのような文法は無い。
(親インターフェースや外側クラスを指定するような時は書ける。[2019-12-08])
どのメソッドが呼ばれるかは実行時のインスタンスによる(変数のクラスはあまり関係ない)のだが、privateに関しては変数のクラスも影響することがある。[2012-05-20]
例えば、Aを継承したBに対し、AのprivateメンバーはBの変数としては扱えない(コンパイルエラーになる)。
class A { private int value; private int getValue() { return value; } public boolean equasB1(B b) { return this.value == b.value; } public boolean equalsB2(B b) { return this.getValue() == b.getValue(); } } class B extends A { }
ここでのvalueやgetValue()はAのprivateメンバーなので、Bとしてはアクセスできない。
A(親クラス)にキャストしてやると、自分自身のクラスなのでアクセスできる。
class A { private int value; private int getValue() { return value; } public boolean equasB1(B b) { A a = b; return this.value == a.value; // return this.value == ((A)b).value; } public boolean equalsB2(B b) { A a = b; return this.getValue() == a.getValue(); // return this.getValue() == ((A)b).getValue(); } } class B extends A { }
普通は親クラスへのキャスト(親クラスの変数への代入)なんてわざわざ行わないので、それをやっているというのが奇妙に見えるところ(笑)
変数名やメソッド名に同じ名前を使える箇所がある。同じ名前が使えない箇所もある。[2008-06-12]
新しい名前を付けることによって他の名前が見えなくなる(隠される)ことをシャドーイング(shadowed)という。[2008-08-30]
見えている方は可視(visible)という。
フィールド(メンバー変数)名とローカル変数(メソッドの引数(パラメーター)名を含む)には同じ名前を使える。
class クラス { int value; //フィールド void setValue(int value) { //パラメーター this.value = value; } void method() { int value = 123; //ローカル変数 System.out.println(value); //ローカル変数の値が使用される System.out.println(this.value); //フィールドの値が使用される } }
セッターメソッドで引数名とフィールド名を同一にするのは頻繁に使われるので可。
でもそれ以外のメソッドではどちらの変数を使っているのか分かりにくくなる(バグの元になる)ので、なるべく重複させない方がいいと思う。
Eclipseを使っていれば色が変わってくれるから分かるけどね…。
とげ括弧{ }によってブロックを作ると、その範囲だけで使えるローカル変数を定義できる。
ただし、同名のローカル変数(パラメーター(メソッドの引数)を含む)が既に定義されている場合は不可。
void ok() { { int n; } { int n; //可(別スコープ) } }
void ng() { int n; { int n; //不可(重複ローカル変数) } }
void ok() { { int n; } int n; //可 }
void ng() { switch(〜) { case 1: int n; break; case 2: int n; //不可 break; } }
親クラス内にあるメソッドと同名(かつ同一引数同一戻り型、すなわちシグニチャーが同一)のメソッドを定義することが出来る。ってそれはオーバーライド(override)と呼ばれる(笑)
メソッド名が同一で、引数の型が異なるメソッドを定義することも出来る。これはオーバーロード(overload)と呼ばれる。
メソッド名と引数が同一で戻り型だけが異なるメソッドは定義できない。→共変戻り値は例外
クラスを継承している場合、親クラスと同一のフィールド名のフィールド(メンバー変数)を定義することが出来る。
これによって親クラスのフィールドが見えなくなることを隠蔽(hide)という。[2008-08-31]
隠蔽されたフィールドは、superやキャストしたthisを使えば指定することが出来る。
class 親クラス { int value; } class 子クラス extends 親クラス { int value; void method() { this.value = 123; super.value = 456; } }
つまり、メソッドと違って、フィールドはオーバーライドされず、別物として存在し別々にアクセスできる。
(親クラスと、さらにその親クラスに同名のフィールドがある場合、キャストすれば祖先(?)のフィールドにアクセスできる。[2008-08-09])
内部クラスでも外側クラスと同じ名前のフィールドを定義することが出来る。
class 外側クラス { int value; class 内部クラス { int value; void method() { this.value = 123; 外側クラス.this.value = 789; } } }
(内部クラスの場合は外側クラスの名前を指定できるので、内部クラスのさらに内部のクラスから外側クラスにアクセスできる)
同一クラス内で、同じ名前のメソッドとフィールドを定義できる。
class クラス { int name; void name() { } void method() { name = 1; //フィールドアクセス name(); //メソッド呼び出し } }
同じ名前でも、「()」が付いていればメソッド呼び出し、そうでなければ変数名だと分かるので、区別がつく。
とは言え紛らわしいのは確かなので、こういう事はしない方がいいだろう。
(→列挙型のswitch文では自動的にこういうフィールド・メソッドが作られる)
C言語やC++だと、関数(メソッド)名に「()」が付いていない状態なら関数ポインターになるので、区別が付かない。したがって変数名と関数名で同一の名前を使うことは出来ない。
クラス名と同じ名前の変数(ローカル変数やフィールド)を作ると、単純名ではクラスにアクセスできなくなる。[2008-09-13]
こういう宣言をオブスキュア(obscured)な宣言と呼ぶ。
class Name { static int value; public static void main(String[] args) { System.out.println(Name.value); //問題なし(クラスNameのvalueというフィールド) } }
↓
class Name { static int value; public static void main(String[] args) { int Name = 0; System.out.println(Name.value); //コンパイルエラー(変数Nameはintなので、valueというフィールドは無い) } }
Javaの普通の命名規約に従っていれば クラス名は大文字から始まり変数名は小文字から始まるので、こういう事はまず起きない。
同様のことはパッケージ名でも起きるらしいが…やはり滅多に起きないと思う。
package sample; public class Obscure { public static void main(String[] args) { System.out.println(sample.Obscure.class); } } /* class sample { //もしこのクラスを定義すると、sample.Obscureはこのクラス内の内部クラスとして認識され } //そんなクラスは無いのでコンパイルエラーになる /**/
breakやcontinueでラベルを指定できる対象であるforやwhile・switchなどには、複数のラベルを定義することが出来る。[2008-06-11]
label1: label2: label3: switch (n) { case 1: break label1; case 2: break label2; case 3: break label3; }
…けど、何の為に同じブロックに複数のラベルを付けなきゃいけないんだろう?(苦笑)
ラベルはブロックの先頭で定義するもの。
これは、そのブロックでしか使えない。いわば、そのブロックだけのスコープを持っているようなもの。
for文の変数がそのブロックのみのスコープになるのと似ている。
label1:
while (true) {
break label1;
}
label1: ←同じラベルがまた使える
while (true) {
break label1;
}
|
for (int i = 0; i < 10; i++) {
〜
}
↓同じ変数iがまた使える
for (int i = 0; i < 10; i++) {
〜
}
|
Javaでループの変数にi,j,kを使うのはお約束になっている(コーディング規約でそう書かれる事も多い)し、
for文で同じ変数名を使い回すと別スコープになることが定められているので 再利用しても誤解は無い。
しかしラベルに関してはやめておいた方がいいだろう。
(for文なら変数定義がブロック内にあるように見える(ローカル変数のスコープがブロック内というルールに似ていて違和感が無い)のに対し、ラベル定義はそういう風に見えないし)
お互いに直接関係の無いクラスとインターフェースが同一シグニチャーのメソッドを定義しているとき、
class S {
public void method() { System.out.println("S::method"); }
}
interface I {
public void method();
}
Sを継承してIを実装したクラスでは、Iの呼び出しでSのメソッドが呼べる。
class A extends S implements I { }
I a = new A();
a.method(); //インターフェースIのメソッド呼び出しだが、ちゃんとSのメソッドが呼ばれる。
変数の宣言(クラス)がどうであれ、実体(インスタンス)のクラスが使われるということだ。[2008-05-02]
(リフレクションで“クラスAのメソッド”としてMethodを取得できることを考えれば、シグニチャーだけ一致していれば「どこで定義されていようが構わない」というのは妥当か)
なお、Javaでは、戻り値の型が異なる同一シグニチャーのメソッドを持つクラス・インターフェースを同時に実装することは出来ない。[2008-05-02]
interface I1 { public int func(); } interface I2 { public long func(); }
class C implements I1, I2 { public int func() { return 0; } //I2のfunc()を実装できない、というコンパイルエラー }
ただし、共変関係にある戻り値なら実装可能。(JDK1.5以降)
interface I1 { public Number func(); } interface I2 { public Integer func(); }
class C implements I1, I2 { public Integer func() { return Integer.valueOf(0); } }
Aというクラスがaaaというパッケージにあり、そこにprotectedなメソッドmがあったとする。[2007-11-08]
package aaa; public class A { protected void m() { System.out.println("A called"); } }
そして、Aを継承したA1・A2という2つのクラスをaaaとは別のパッケージに作ったとする。
すると、A2のメソッドからa1のメソッドmを呼ぶことは出来ない。
(mのスコープはprotectedなので、直接継承したクラスからしかアクセスできない)
package bbb;
import aaa.A;
class A1 extends A {
}
class A2 extends A {
public void test(A1 a1) {
a1.m(); //「不可視である」というコンパイルエラー
}
}
この場合、A1でmをオーバーライドするとA2から呼ぶことが出来るようになる。
(A1とA2は同一パッケージなので、別クラスであってもprivate以外のメンバーにはアクセスできる)
class A1 extends A { @Override protected void m() { super.m(); } }
A1のメソッドmだけ見ると、なんでそんな事をしているのか疑問になるようなコーディング(苦笑)
せめて可視性が変わっていれば(protected→publicとか)そういう目的だと思うだろうが、この話ではそれすら変わっていないから。
javaコマンドでクラスを指定すると、そのクラスの「public
static void main(String[] args)
」が実行されるのは常識。[2008-09-13]
JDK1.5では、「public static void main(String...
args)
」でも良くなった!
でも呼び出す側はjavaコマンドだし、使う側はargsを配列として扱うのだし、意味ねー!(笑)
また、クラス以外でもmainを書いて実行することが出来る。[2019-12-08]
クラス内のフィールド(メンバー変数)は、以下の順序で初期化される。
クラス.class
の使用でも、Class#forName()によるクラス取得でも)に、スーパークラスの(静的なフィールドの)初期化が行われる。class Super { //スーパークラス static { //スーパークラスの静的初期化子 System.out.println("Super::static-field"); } { //スーパークラスのインスタンス初期化子 System.out.println("Super::field"); } Super() { //スーパークラスのコンストラクター System.out.println("Super::construct"); } } class Sub extends Super { //当該クラス static { //当該クラスの静的初期化子 System.out.println("Sub::static-field"); } { //当該クラスのインスタンス初期化子 System.out.println("Sub::field"); } Sub() { //当該クラスのコンストラクター System.out.println("Sub::construct"); } }
↓「new Sub()
」を実行すると
Super::static-field Sub::static-field ←staticフィールドが真っ先に初期化され Super::field Super::construct ←次に親クラスのインスタンスが生成され Sub::field Sub::construct ←最後に自分のクラスが作られる
↓2回目以降の「new Sub()
」は、当然staticフィールドは初期化されない
Super::field Super::construct Sub::field Sub::construct
※staticなフィールドが初期化されるのはそのクラスに初めてアクセスされる時であって、クラスがロードされた時ではない。
あれ? 初めてアクセスされる時に初めてロードされるんだったっけ?
静的初期化子(Static Initializer)とは、「static { 〜
}」ブロックのこと。staticな変数の初期化をプログラムできる。
MapやListの初期化に使える。
static final Map map = new HashMap(); static { map.put("A", "abc"); map.put("B", "def"); }
インスタンス初期化子(Instance Initializer)とは、クラス内の「{ 〜
}」ブロックのこと。メソッド名のないメソッドのようなもの。
使われている所を見たことは無いが、コンストラクターより前に実行されるため、複数のコンストラクターがある場合に共通初期処理のように使えなくもないかもしれない。
無名内部クラスではコンストラクターが定義できないので、インスタンス初期化子が初期化に使える。[2010-01-09]
List<String> list = new ArrayList<String>() { { super.add("a"); super.add("b"); super.add("c"); } };
↓しかし以下のようなコーディングは突き詰めすぎでかっちょよすぎるので、しない方がいいだろう(笑)
//矢野勉さんのJavaの匿名クラスを使ってかっこよくオブジェクトを初期化するテクニック List<String> list = new ArrayList<String>() {{ add("a");add("b");add("c"); }};
「メンバー変数 = 初期値;
」という初期化と“初期化子”による初期化は、ソース内に書かれた順に、上から実行される。
この初期化では、ソースのその行より下にある変数は使用することが出来ない(まだ初期化されていない為)。参照しようとすると「フィールドは定義前には参照できません」というコンパイルエラーになる。
※実際には、インスタンス初期化子はコンストラクター内の一番先頭に展開される。なので、ソース上のコンストラクター内が最後に実行されることに変わりは無い。
複数のコンストラクターがあれば同じ内容が全コンストラクターに書かれるので、無駄が大きそう。
いずれにしても、初期化の順序に依存するようなプログラミングはすべきではない。
順序依存の例.java:
class 順序依存の例 { int n = 1; int n1 = n++; int n2 = n++; }
変数の初期化について。[2008-04-26]
Javaでは、ローカル以外の変数(フィールドとか配列の各要素)については、初期値を明示的に指定しなければデフォルト値が自動的に設定される。
ローカル変数については、初期化せずに使用しようとするとエラーになる(コンパイル時点で警告される)。
C言語やC++ではローカル変数を明示的に初期化しないと値が不定となる為、思わぬバグの原因となった。
したがって、変数は宣言時点で初期化することが推奨されていた。
しかしJavaの場合はローカル変数の初期化漏れはコンパイル時に分かるので、逆に初期化はしない方がいいと思う。(そういう主張をしている人は他に聞いたことはないが)
String str; ←あえて初期化しない
if (条件) {
str = "zzz";
}
int len = str.length();
このプログラムは、if文の条件が満たされない時にローカル変数strが初期化されない為、コンパイルエラーとなる。
ここでstrの定義時に下手な初期値を与えてしまうと、コンパイルは通るが、実行時にelseが実行されるまで「else時の設定漏れ」というバグが発覚しない。
String str = null; if (条件) { str = "zzz"; } int len = str.length(); …NullPointerException! elseが実行されるまでバグが分からない
Stringならnullの代わりに""で初期化しておけばNullPointerExceptionは起きないが、elseの時に欲しい値が得られないというバグの発覚が遅れるだけ、ということになりかねない。
なお、「String str = new String();
」で初期化するのは最悪。
String以外でも、使わないインスタンスを初期化の為だけに生成する人がいるが、非常によろしくない。
意図せぬロジックが実行されるとまずいという考えなら、初期化場所は(共通となる)変数定義時でなく、個別にすべきだと思う。
String str; ←ここでは初期化しない if (条件) { str = "zzz"; ←個別に初期化する } else { str = ""; ←個別に初期化する } int len = str.length();
初期化が変数の定義時に行われている場合、後からロジックを追加して変数への代入が無かったら、漏れなのか、意図して同じ値を使いたいのか分からない。
String str = "";
if (条件) {
str = "zzz";
} else if (新条件) {
// strの初期化漏れ?それともデフォルト値でよい?
} else {
str = "";
}
int len = str.length();
しかしこの方式、try〜finallyの定石では使えないんだよなー。
Closeable obj = null; try { if (条件) { obj = new 〜(); } } finally { if (obj != null) try{ obj.close(); }catch(IOException e){} }
初期値を与えておかないと、finally節の方でエラーになっちゃうから。