Java開発ツールは,ソースコードが利用できる場合にはいつでも,必要に応じて自動再コンパイルをサポートしなければならない。
Javaの特定の実装では,型のソース及びバイナリを版管理データベースに記憶しておいて,型のバイナリ互換な版をクライアントに提供することによって,リンクエラーを防止するためにデータベースの完全性保証機構を利用したClassLoader (20.14)を実装してもよい 。
広範囲に配布されるパッケージ及びクラスの開発者は,さらに別の問題に直面する。
広域分散システムの典型例とするインターネットにおいては,変更対象の型に直接又は間接的に依存する既存のバイナリを自動的に再コンパイルすることは,実用的でないか又は不可能である。
その代わりに,Javaでは,既存のバイナリとの互換性を維持する(損なわない)限り,パッケージに対して,又はクラス並びにインタフェース型に対して,開発者が変更を許される範囲を定義する。
これらのことは,ACM SIGPLAN Notices,第30巻,10号,10月1995年,として発行された Proceedings of OOPSLA 95 の426ページから438ページまでの論文に示されている。
その論文の枠組み内で,Javaバイナリは,論文の著者が識別したすべての意味のある変換のもとで,リリース間のバイナリ互換性を保証する。
この方式のもとでのJavaがサポートするいくつかの重要なバイナリ互換な変更の一覧を,次に示す。
privateなフィールド,メソッド又はコンストラクタの削除。
13.では,すべてのJava実装によって保証されるバイナリ互換性について最小限の標準を規定する。
Java は,クラス及びインタフェースのバイナリに,互換性のあるソースから生成したのか不明なものが混在していても,それらのソースが,ここで述べる互換な手段で変更されている場合には互換性を保証する。
Java開発システムにおいては,再コンパイルできない既存のバイナリに与える変更の影響を,開発者に向けて警告する機能を提供することを推奨する。
13.では,まず,すべてのJavaバイナリ形式が備えなければならない特性を規定する(13.1)。
次に,具体例を挙げながらバイナリの互換性を定義する (13.2)。
最後に,パッケージ(13.3),クラス (13.4)及びインタフェース(13.5) に加えられる可能性がある多くの変更を,どの変更はバイナリ互換性の維持が保証され,どの変更は保証されないかを規定しながら列挙する。
Javaバイナリファイルの多くは,Java仮想計算機規定(The Java Virtual Machine Specification)が規定するクラスファイルの形式に正確に準拠しているが,この規定は,特定のバイナリファイル形式を使用することを強制していない。
むしろ,この規定は,コンパイルした型に対するすべてのバイナリ形式が守らなければならない特性を規定している。
これらの特性の多くは,バイナリの互換性を維持するソースコード変換をサポートするために,特に選ばれたものとする。
要件は次のとおりとする。
static,final 及びコンパイル時の定数式で初期化されたフィールドへの参照は,表記された定数値への参照としてコンパイル時に解決される。
バイナリファイル内のコードには,そのような定数フィールドへの参照は存在しないほうがよい。(その定数フィールドを含むクラス又はインタフェース内は,それを初期化するコードをもつため除外する。)
さらに,それらの定数フィールドは,常に初期化済みのものとして出現しなければならない。それらのフィールドの型に対するデフォルト初期値は,絶対に参照されてはならない。詳細は,13.4.8を参照のこと。
void宣言されていて返却値を返さないことの表示,のいずれかを含まなければならない。メソッドのシグネチャには,次のすべてを含まなければならない。
コンストラクタのシグネチャには,次の両方を含まなければならない。
クラス又はインタフェースに対するJavaのバイナリ表現には,次のすべてを含まなければならない。
java.lang.Object以外のクラスの場合は,このクラスの直接的スーパクラスへの記号参照。
private宣言をしていない各フィールドの規定。これはそのフィールドの単純名及びフィールドの型への記号参照として与える。
private宣言していない各メソッドに対して,前述と同様にそのシグネチャ及び返却値の型。
あるJavaシステムが,パッケージ全体に含まれるクラス及びインタフェースのグループを表現するバイナリ形式を定義していれば,このバイナリ形式は,デフォルトアクセス(パッケージ)で宣言しているフィールド,メソッド又はコンストラクタに関する情報を公開する必要はない。
以降の節では,既存のバイナリとの互換性を損なうことなく,クラス及びインタフェース型宣言に加えてもよい変更を規定する。
Java仮想計算機及びその標準classファイル形式は,これらの変更をサポートしている。他のJavaバイナリ形式も,同様にこれらの変更をサポートしなければならない。
型の変更は,エラー無しにリンクされていた既存のバイナリが引き続きエラー無しにリンクできれば,既存のバイナリとバイナリ互換(binary compatible)( バイナリの互換性を損なう(break binary compatibility)ことはないと同義)とする。
13.1で規定するとおり,メソッド及びフィールドへの記号参照は,そのメソッド又はフィールドを宣言している正確なクラス又はインタフェースを指定する。
これは,バイナリは,他のクラス及びインタフェースのアクセス可能なメンバ及びコンストラクタに依存してコンパイルされることを意味する。
バイナリの互換性を維持するために,クラス又はインタフェースは,これらのアクセス可能なメンバ及びコンストラクタを,さらにその存在及び振舞いを,そのクラス又はインタフェースの利用者との契約(contract)として扱わなければならない。
Javaは,契約への追加及び偶然による名前の衝突が,バイナリの互換性を損なうことを防止する設計がなされている。特に,次のとおりとする。
バイナリの互換性は,ソースの互換性と同一ではない。
特に,13.4.5の例では,すべてを一緒にはコンパイルしないソースから,一連の互換性があるバイナリを生成可能なことを示している。
これは,典型的な例である。新しい宣言が加えられ,ソースコードを変更していない部分の名前の意味が変わる。その一方で,ソースコードを変更していない部分に対応する既存のバイナリは,完全修飾された以前の意味のままの名前を保持している。
整合性のあるソースコードの集合を作成するには,以前の意味に対応する修飾名又はフィールドアクセス式を与えることが必要である。
新規の型が,無関係な型に既に付けられている名前を再使用しない限り,新規のクラス又はインタフェース型を,既存のバイナリとの互換性を損なわずに,パッケージに追加してもよい。
新規の型が,無関係な型に既に付けられている名前を再使用した場合,両方の型のバイナリを同じクラスローダによってロードできないため,名前の衝突が生じる可能性がある。
それ自体がpublicでなく,それが,あるpublic型のスーパクラス又はスーパインタフェースでないクラス及びインタフェース型の中の変更は,それらを宣言しているパッケージ内の型にだけ影響を及ぼす。
それらの変更の影響を受ける型は,ここに示すもの以外の非互換性があっても,そのパッケージの影響を受けるバイナリを一緒に更新すれば,削除又は別途変更してもよい。
この節では,クラス並びにそのメンバ及びコンストラクタの宣言の変更が既存のバイナリへ及ぼす影響について規定する。
abstractクラス
abstract宣言をしていないクラスを,abstract宣言に変更した場合,既存のバイナリは,そのクラスの新しいインスタンスを生成しようとして,リンク時にInstantiationErrorを投げるか,又は(クラスClassのメソッドnewInstance(20.3.6)を用いた場合)実行時にInstantiationExceptionを投げる。
したがって,広範囲に配布するクラスに対しては,それらの変更はしないほうがよい。
abstract宣言をしていたクラスを,abstract宣言なしに変更することは,既存のバイナリとの互換性を損なわない。
finalクラス
final宣言をしていないクラスを,final宣言に変更した場合,このクラスの既存のサブクラスのバイナリをロードするとVerifyErrorを投げる。それは,finalクラスはサブクラスをもてないことによる。
広範囲に配布するクラスに対しては,それらの変更はしないほうがよい。
final宣言をしていたクラスを,final宣言なしに変更することは,既存のバイナリとの互換性を損なわない。
publicクラス
public宣言をしていないクラスを,public宣言に変更することは,既存のバイナリとの互換性を損なわない。
public宣言をしていたクラスを,public宣言なしに変更した場合,そのクラス型へのアクセスが必要なのにアクセスできなくなった既存のバイナリをリンクするとIllegalAccessErrorを投げる。
広範囲に配布するクラスに対しては,それらの変更はしないほうがよい。
あるクラスが,それ自体のスーパクラスになることができる場合,ロード時にClassCircularityErrorを投げる。
新規にコンパイルしたバイナリを既存のバイナリと共にロードするときに,それらの循環を引き起こす可能性のあるクラス階層への変更は,広範囲に配布するクラスに対してはしないほうがよい。
そのクラス型の,スーパクラス又はスーパインタフェースのそれぞれの全体集合に含まれていたメンバがなくならない限り,そのクラス型の直接のスーパクラス又は直接のスーパインタフェースの集合へ変更を加えても既存のバイナリとの互換性を損なわない。
クラス変数及びクラスメソッドの使用に関しては,あるクラスのスーパクラスの集合へ変更を加えても既存のバイナリとの互換性を損なわない。
それは,クラス変数及びクラスメソッドの使用が,コンパイル時にそれらを宣言するクラスの名前への記号参照として解決することによる。
したがって,それらの使用は,クラス階層の構造ではなく,その変数又はメソッドを宣言しているクラスが存在し続けることにだけ依存する。
直接のスーパクラス又は直接のスーパインタフェースの集合への変更によって,もはやスーパクラス又はスーパインタフェースではなくなるクラス又はインタフェースが生じれば,既存のバイナリを修正したクラスのバイナリと共にロードした場合,リンク時エラーとなる可能性がある。
広範囲に配布するクラスに対しては,それらの変更はしないほうがよい。
結果として生じるエラーは,以前にコンパイルした演算が,型システムを侵害しようとしたときに,Java仮想計算機の検証器によって検出される。
例えば,次のテストプログラムを仮定する。
class Hyper {
char h = 'h';
}
class Super extends Hyper {
char s = 's';
}
class Test extends Super {
public static void main(String[] args) {
Hyper h = new Super();
System.out.println(h.h);
}
}
h
class Super { char s = 's'; }
クラスSuperのこの版は,Hyperのサブクラスではない。
次にHyper及びTestの既存のバイナリを,Superの新版と共に実行すると,リンク時にVerifyErrorを投げる。
Superは,Hyperのサブクラスでないため,new Super()の結果は,型Hyperの変数に代入できず,検証器はそれを受け付けない。
検証プロセスがないとどうなるかを考えてみる価値はある。
この場合,先のプログラムは,実行し次の結果を出力する。
s
これは,個々のバイナリは,正しいJavaコンパイラによって生成されても,検証器を使わないと,不整合なバイナリ同士をリンクすることによって型システムが崩れることを示す。
さらに詳しい例を示す。
参照タイプ型からint型へのキャストの実装例である。
検証プロセスの実行に失敗すると,Javaのある実装ではこれが実行できてしまう。
メソッドディスパッチの表を用い,リンカが,それらの表にオフセットを昇順に整列して代入する実装を想定する。
それから,次のJavaプログラムをコンパイルしたとする。
class Hyper {
int zero(Object o) {
return 0;
}
}
class Super extends Hyper {
int peek(int i) {
return i;
}
}
class Test extends Super {
public static void main(String[] args) throws Throwable
{
Super as = new Super();
System.out.println(as);
System.out.println(Integer.toHexString(as.zero(as)));
}
}
想定した実装では,クラスSuperは2個のメソッドをもつ。
1番目は,クラスHyperから継承されたメソッドzero,2番目は,メソッドpeekとする。
Superの任意のサブクラスも,そのメソッド表の最初の2個のエントリに,前述と同じ2個のメソッドをもつ。
(実際には,メソッド表の中には,これらすべてのメソッドの前にクラスObject から継承されたすべてのメソッドが存在する。しかし,説明を簡単にするために,ここではそれは無視する。)
メソッド呼出しas.zero(as) に対して,コンパイラは,メソッド表の1番目のメソッドを呼び出す指定を行う。
型の安全性が維持されている限りこれは常に正しい。
コンパイルしたコードを実行すると,次の結果を出力する。
Super@ee300858 0
これは正しい出力結果である。
しかし,extends節以外は同じ,Superの新版をコンパイルすると,次のとおりとなる。
class Super {
int peek(int i) {
return i;
}
}
Superのメソッド表の1番目のメソッドは,zeroではなく,peekになる。
Superの新しいバイナリコードをHyper及びTestの古いバイナリコードと共に使用すると,メソッド呼出しas.zero(as)に対して,Hyper内のメソッドzeroではなく,Super内のメソッドpeekの呼出しを引き起こす。
これは,もちろん型の侵害である。 実引数は,Super型であるが,仮引数は,int型である。
内部データ表現及び型の侵害の結果に関して,若干の都合のよい仮定をすれば,この誤ったプログラムの実行結果は,次のとおりとなる。
Super@ee300848 ee300848
同様な方法によって,メモリ内の任意の位置を変更することができるメソッドpokeを作り上げることができる。
これで得られる教訓は,検証器抜き又はその使用を誤ったJavaを実装すると,型の安全性を維持できず,そのため結局正しいJava実装とはならない。
スーパクラス又はサブクラスのメンバとして,同じ名前(フィールドに対して),又は同じ名前,シグネチャ及び返却値の型(メソッドに対し)をもつクラスメンバを追加しても,既存のバイナリとの互換性は損なわない。 元のフィールド又はメソッドへの参照は,それらを宣言していたクラス名を含む記号参照としてコンパイル時に解決されている。 この規則は,他の別の規則よりも,コンパイルしたJavaコードを変更に対してより頑健にする。 リンクするクラスの集合が,コンパイル時エラーとなっても実行時にはエラーは発生しない。
class Hyper {
String h = "Hyper";
}
class Super extends Hyper { }
class Test extends Super {
public static void main(String[] args) {
String s = new Test().h;
System.out.println(s);
}
}
これをコンパイルし実行すると,次の出力を得る。
Hyper
class Super extends Hyper {
char h = 'h';
}
この結果得られるバイナリを,Hyper及びTestの既存のバイナリと共に使用すると,結果は依然としてHyperである。
Hyper
class Hyper {
String h = "Hyper";
}
class Super extends Hyper {
char h = 'h';
}
class Test extends Super {
public static void main(String[] args) {
String s = new Test().h;
System.out.println(s);
}
}
これは,コンパイル時エラーになる。
それは,ソースプログラムmain内のhは,現時点ではSuperで宣言したフィールドcharを参照すると翻訳されるが,型charの値は,型Stringには代入できないことによる。
既存のバイナリで使用している,private宣言していないクラスメンバ又はコンストラクタを削除すると,そのメンバが,スーパクラスメソッドを上書きするインスタンスメソッドであっても,リンクエラーが生じることがある。
これは,記号解決する際に,リンカが,コンパイル時に識別されたクラスの中だけを見ることによる。
class Hyper {
void hello() {
System.out.println("hello from Hyper");
}
}
class Super extends Hyper {
void hello() {
System.out.println("hello from Super"); }
}
class Test {
public static void main(String[] args) {
new Super().hello();
}
}
hello from Super
class Super extends Hyper { }
Super及びHyperを再コンパイルし,Testを再コンパイルしなかった場合,リンク時にNoSuchMethodErrorとなる。
それは,メソッドhelloをクラスSuper 内で宣言していないことによる。
バイナリの互換性を維持するためには,メソッドを削除してはならない。
その代わりとして,”転送メソッド”を使用することが望ましい。この例では,Super の宣言を次のとおりに差し替える。
class Super extends Hyper {
void hello() {
super.hello();
}
}
Super及びHyperを再コンパイルし,これらの新規バイナリをTestの元のバイナリと共に実行すると,次の出力を得る。
hello from Hyper
これは,先の例で期待した結果である。
キーワードsuperは,現在のクラスで宣言したすべてのメソッドをバイパスして,スーパクラス内で宣言したメソッドにアクセスするために使用する。
super.Identifier
この式は,コンパイル時に,特定のスーパクラスS 内で宣言したメソッドM として解決される。
メソッドMは,実行時にそのクラス内で宣言されていなければならない。そうでなければ,リンクエラーとなる。
メソッドM がインスタンスメソッドの場合,実行時に呼ばれるメソッドMRは,M と同じシグネチャをもつメソッドであって,それは,superが関与する式を含むクラスの直接のスーパクラスのメンバとなる。
class Hyper {
void hello() {
System.out.println("hello from Hyper");
}
}
class Super extends Hyper { }
class Test extends Super {
public static void main(String[] args) {
new Test().hello();
}
void hello() {
super.hello();
}
}
hello from Hyper
class Super extends Hyper {
void hello() {
System.out.println("hello from Super");
}
}
Super及びHyperを再コンパイルし,Testを再コンパイルしなかった場合,これらの新しいバイナリをTestの既存のバイナリと共に実行すると次の出力を得る。
hello from Super
メンバ又はコンストラクタのアクセス宣言を,アクセス量を減らす方向に変更することは,既存のバイナリとの互換性を損ない,バイナリの記号解決の際にリンクエラーを投げる可能性がある。
アクセス量の減少は,アクセス修飾子をデフォルトアクセスからprivateアクセスへ,protectedアクセスからデフォルト若しくはprivateアクセスへ,又はpublicアクセスからprotected,デフォルト若しくはprivateアクセスへ変更する場合にだけ許される。
したがって,アクセス量を減らすためにメンバ又はコンストラクタを変更することは,広範囲に配布するクラスに対してしないほうがよい。
Javaは,サブクラスが(既に)アクセス量を減らす方向にメソッドを定義しているときに,メンバ又はコンストラクタをよりアクセスし易くなる変更をしてもリンクエラーを発生しない定義がなされている。
したがって,例えば,パッケージpointsが,クラスPointを定義しているとする。
package points;
public class Point {
public int x, y;
protected void print() {
System.out.println("(" + x + "," + y + ")");
}
}
class Test extends points.Point {
protected void print() {
System.out.println("Test"); }
public static void main(String[] args) {
Test t = new Test();
t.print();
}
}
これらのクラスを,コンパイルしTestを実行すると,次の出力を得る。
Test
クラスPoint内のメソッドprintをpublicに変更し,クラスPointだけを再コンパイルして,Testの既存のバイナリと共に実行すると,コンパイル時には,publicメソッドをprotectedメソッドで無効にするのは不適当であるにもかかわらず,リンクエラーは発生しない。
(これは,printをpublicに変更しない限り,新しいクラスPointを使用してクラスTestを再コンパイルすることはできない,という事実による。)
既存サブクラスのバイナリを損なわずに,スーパクラスをprotectedメソッドからpublicに変更することを許していることは,Javaのバイナリを,より壊れにくくしている。
別の方法,つまり,その変更がリンクエラーを発生させたとすると,それは,明確な利益のない新たなバイナリの非互換性を生みだす。
クラスへのフィールドの追加は,再コンパイルしていない既存のバイナリとの互換性を損なわない。 そのクラス内の,以前はスーパクラスのフィールドを参照していたフィールドアクセスの型が適合しなくなることによって,再コンパイルが不可能になったとしても,互換性を損なわない。 その参照を伴なうコンパイル済みのクラスは,スーパクラス内で宣言したフィールドを参照し続ける。次のコードをコンパイルし実行する。
class Hyper { String h = "hyper"; }
class Super extends Hyper {
String s = "super";
}
class Test {
public static void main(String[] args) {
System.out.println(new Super().h);
}
}
hyper
class Super extends Hyper {
String s = "super"; int h = 0;
}
Hyper及びSuperを再コンパイルし,得られた新しいバイナリを,Test の古いバイナリと共に実行すると,次の出力を得る。
hyper
Hyperのフィールドhは,Super内でいかなる型として宣言されていても,mainの元のバイナリから出力する。
これは,実行時に発生する不整合の頻度を減らす効果を得る。
(理想を言えば,その不自然さを排除するために,再コンパイルが必要なすべてのソースファイルは,その一つでも変更したら再コンパイルするのがよい。
しかしそれらの膨大な再コンパイル処理は,しばしば,実用的ではなく,特にインタネットにおいては,不可能である。
しかも,先に述べたとおり,それらの再コンパイル処理は,場合によってはソースコードの更なる変更を必要とする。)
クラスからのフィールドの削除は,このフィールドを参照しているすべての既存のバイナリとの互換性を損なう。
既存のバイナリからのそれらの参照をリンクすると,NoSuchFieldError を投げる。
広範囲に配布するクラスからは,privateフィールドだけを安全に削除することができる。
finalフィールド及び定数
finalでないフィールドをfinalに変更した場合,そのフィールドに新しい値を代入しようとする既存のバイナリとの互換性が損なわれる。
class Super { static char s; }
class Test extends Super {
public static void main(String[] args) {
s = 'a';
System.out.println(s);
}
}
a
class Super { static char s; }
Superを再コンパイルして,Testを再コンパイルしなかった場合,新規バイナリをTestの既存のバイナリと共に実行すると,IncompatibleClassChangeErrorとなる。
static ,final 及びコンパイル時の定数式で初期化済みのフィールドを,プリミティブ定数(primitive constant)と呼ぶ。
インタフェース内のフィールドは,暗黙的にstatic ,final であって,例外もあるが,多くの場合は定数である。
フィールドがプリミティブ定数でない場合,キーワードfinalの削除又はフィールドに設定する初期値の変更は,既存のバイナリとの互換性を損なわない。
フィールドがプリミティブ定数である場合,キーワードfinalの削除又はフィールドの値の変更は,既存のバイナリを実行しなければ,互換性を損なわない。
しかし,既存のバイナリは,再コンパイルしない限り,その定数に対する新しい値を参照することはない。
次に例を示す。
class Flags {
final static boolean debug = true;
}
class Test {
public static void main(String[] args) {
if (Flags.debug)
System.out.println("debug is true");
}
}
debug is true
class Flags {
final static boolean debug = false;
}
Flagsを再コンパイルし,Testは再コンパイルしなかった場合,新しいバイナリをTestの既存のバイナリと共に実行すると,次の出力を得る。
debug is true
これは,debugの値が,コンパイル時のプリミティブ定数であって,Testのコンパイル時に,クラスFlagsへの参照を作成せずに使用可能なことによる。
この結果は,14.19で示す条件付きコンパイルをサポートするという決定の副作用となる14.19 。
次の修正例で示すとおり,Flagsをインタフェースに変更しても,この振舞いは変わらない。
interface Flags { boolean debug = true; }
class Test {
public static void main(String[] args) {
if (Flags.debug)
System.out.println("debug is true");
}
}
(プリミティブ定数をインライン展開することを求める理由の一つは,Java のswitch文の各caseに定数が必要なことであって,どの二つの定数も同じ値であってはならないことによる。
Java は,コンパイル時に switch 文の定数値の重複をチェックする。
class ファイル形式は,case値の記号リンクを行わない。)
広範囲に配布するコード内で,“不定の定数”という問題を回避する最善の方法として,絶対に変更されそうにない値だけをプリミティブ定数として宣言する。
インタフェース内のプリミティブ定数の多くは,Javaがサポートしていない列挙型を置き換える小さな整数値とする。
これらの小さな値は,任意に選ぶことができ,変更が必要とならないことが望ましい。真の数学的定数は別として,Javaコードでは,static 及び finalとして宣言したクラス変数はなるべく使わないことが望ましい。
finalの読出し専用の性質が求められる場合,private static変数及びその値を得るための適当なアクセス用メソッドを宣言するほうがよい。これを例で示す。
private static int N;
public static int getN() {
return N;
}
public static final int N = ...;
Nが,読出し専用の必要がない場合,次のコードには何の問題も無い。
public static int N = ...;
一般的な規則として,真に変わらない定数値だけをインタフェース内で宣言するのがよい。
インタフェースのプリミティブ型のフィールドを変更できる場合には,その値は,慣用語法的に次のとおりに表現してよいことを注意しておく。ただし,これはしないほうがよい。
interface Flags {
boolean debug = new Boolean(true).booleanValue();
}
これによって,その値が定数でないことを保証する。他のプリミティブ型についても,同様の慣用語法が存在する。
留意すべきもう一つの点は,定数値をもつstatic finalフィールドは(プリミティブ型又はString型かどうかによらず),決して自体の型のデフォルト初期値をもつように見えてはならないこととする(4.5.4)。
この意味は,それらのすべてのフィールドは,クラスの初期化時において最初に初期化されるように見えることとする( 8.3.2.1 , 9.3.1 , 12.4.2)。
staticフィールド
private宣言をしていないフィールドが,static宣言をしていない場合であって,それを static 宣言に変更した場合,又はその逆の場合,既存のバイナリが,そのフィールドをもう一方の種類のフィールドであることを期待して使用すれば,リンク時エラー,特にIncompatibleClassChangeErrorとなる。
広範囲に配布するコードでは,そのような変更はしないほうがよい。
transientフィールド
フィールドのtransient修飾子の追加又は削除は,既存のバイナリとの互換性を損なわない。
volatileフィールド
private宣言をしていないフィールドが,volatile宣言をしていない場合であって,それを volatile 宣言に変更した場合,又はその逆の場合,既存のバイナリが,そのフィールドを逆の揮発性であることを期待して使用すれば,リンク時エラー,特にIncompatibleClassChangeErrorとなる。
広範囲に配布するコードでは,そのような変更はしないほうがよい。
クラスへのメソッド又はコンストラクタ宣言の追加は,いかなる既存のバイナリとの互換性も損なわない。以前はスーパクラスのメソッドを参照していたメソッド呼出しの型が適合しなくなることによって,型が再コンパイル不可能になったとしても,互換性を損なわない。
それらの参照をもつコンパイル済みのクラスは,スーパクラス内で宣言したメソッドを参照し続ける。
クラスからのメソッド又はコンストラクタの削除は,このメソッド又はコンストラクタを参照している,すべての既存のバイナリとの互換性を損なう。
既存のバイナリからのその参照をリンクしたら,NoSuchMethodError を投げる。
広範囲に配布するクラスからは,privateなメソッド又はコンストラクタだけを安全に削除できる。
クラスのソースコードに,宣言されたコンストラクタが存在しない場合,Javaコンパイラは,自動的に仮引数をもたないコンストラクタを生成する。
それらのクラスのソースコードに,1個以上のコンストラクタ宣言を追加すると,このデフォルトコンストラクタが自動的に生成されることを防ぐことができる。
新しく作成したコンストラクタの一つが仮引数をもたないかぎり,つまり,デフォルトコンストラクタを置き換えないかぎり,効率的にコンストラクタを削除できる。
自動的に生成した,仮引数をもたないコンストラクタには,その宣言のクラスと同じアクセス修飾子を与える。
したがって,既存のバイナリとの互換性を維持するためには,すべての置換えは,同じか,より多くのアクセスをもたなければならない。
メソッド又はコンストラクタの形式仮引数の名前の変更は,既存のバイナリには影響しない。
メソッド名,メソッド若しくはコンストラクタの形式仮引数の型の変更,又はメソッド若しくはコンストラクタ宣言への仮引数の追加若しくは削除は,新しいシグネチャをもつメソッド又はコンストラクタを生成する。
古いシグネチャのメソッド又はコンストラクタの削除,及び新しいシグネチャのメソッド又はコンストラクタの追加,これら両方を組み合わせた影響をもつ( 13.4.12を参照のこと)。
メソッドの結果の型の変更,結果の型のvoidへの置換え,又はvoidのある結果の型への置換えは,古いメソッド又はコンストラクタの削除,及び新しい結果の型又は新しいvoidの結果をもつ新しいメソッド又はコンストラクタの追加,これら両方を組み合わせた影響をもつ( 13.4.12 を参照のこと)。
abstractメソッド
abstract宣言をしていたメソッドからの abstract宣言の削除は,既存のバイナリとの互換性を損なわない。
abstract宣言をしていないメソッドを,abstract宣言に変更することは,既にそのメソッドを呼び出していた既存のバイナリとの互換性を損ない,AbstractMethodErrorとなる。
class Super {
void out() {
System.out.println("Out"); } }
class Test extends Super {
public static void main(String[] args) {
Test t = new Test();
System.out.println("Way ");
t.out();
}
}
Way Out
abstract class Super {
abstract void out();
}
Super を再コンパイルし,Testは再コンパイルしなかった場合,新しいバイナリをTestの既存のバイナリと共に実行すると,クラスTest は,メソッドoutを実装しておらず,そのため抽象的となる(又は,となるはずである)ため,AbstractMethodErrorを生じる。
finalメソッドfinalでないインスタンスメソッドをfinalに変更することは,メソッドの上書きに依存している既存のバイナリとの互換性を損なうことになる。
class Super { void out() { System.out.println("out"); } }
class Test extends Super {
public static void main(String[] args) {
Test t = new Test();
t.out();
}
void out() { super.out(); }
}
out
class Super { final void out() { System.out.println("!"); } }
Superを再コンパイルし,Testは再コンパイルしない場合,新しいバイナリを Test の既存のバイナリとともに実行するとVerifyErrorとなる。
それは,クラスTest は,インスタンスメソッド out を不適切に無効にしようと試みたことによる。
final でないクラス(つまり static)メソッドをfinalに変更することは,既存のバイナリとの互換性を損なわない。それは,実際に呼び出すべきメソッドのクラスは,コンパイル時に解決されることによる。
メソッドから,final修飾子を削除することは,既存のバイナリとの互換性を損なわない。
nativeメソッド
メソッドのnative修飾子を追加又は削除することは,既存のバイナリとの互換性を損なわない。
再コンパイルしていない既存のnativeメソッドに関するJavaの型への変更の影響は,この規定の範囲外とし,Java実装の説明とともに提供されなければならない。 処理系は,その影響を制限する方法で,nativeメソッドを実装することが望ましいが,必須ではない。
staticメソッド
privateを宣言していないメソッドが,static宣言されていた(つまり,クラスメソッド)とき,それを,static宣言しない(つまり,インスタンスメソッド))ものに変更した場合,又はその逆の場合,既存のバイナリとの互換性は損なわれる可能性があり,これらのメソッドを既存のバイナリで使用すると,リンク時にエラー,つまり,IncompatibleClassChangeError となる。
広範囲に配布するクラスに対しては,そのような変更はしないほうがよい。
synchronizedメソッド
メソッドのsynchronized修飾子を追加又は削除することは,既存のバイナリとの互換性を損なわない。
throws節
メソッド又はコンストラクタのthrows節の変更は,既存のバイナリとの互換性を損なわない。
これらの節は,コンパイル時にだけ検査される。
メソッド又はコンストラクタの本体の変更は,既存のバイナリとの互換性を損なわない。
コンパイラは,例えば,次のいづれかでない限り,コンパイル時にメソッドをインライン展開できないことに注意すること。
private。
メソッドのキーワード final は,メソッドが安全にインライン展開できるという意味ではない。それは,単にそのメソッドが上書きできないことを意味するだけとする。
コンパイラが特別な知識をもたない限り,そのメソッドの新しい版をリンク時に提供することは以前として可能とする。
一般的に,Javaの処理系は,遅延束縛(実行時の)コード生成及び最適化を使用することが望ましい。
既存のメソッド名をオーバロードする新しいメソッドの追加は,既存のバイナリとの互換性を損なわない。
それぞれのメソッド呼出しのために使用するメソッドシグネチャは,これらの既存のバイナリのコンパイル時に決定される。
したがって,それらのシグネチャが,両方共に適用可能であって,最初に選択したメソッドシグネチャよりも明確であっても,新しく追加したメソッドは使用されない。
一方,新規のオーバロードされたメソッド又はコンストラクタを追加することは,次にそのクラス又はインタフェースをコンパイルするときに,コンパイル時エラーとなる可能性がある。
それは,最も明確なメソッド又はコンストラクタが存在しないことによる(15.11.2.2) 。
Javaプログラムの実行には,それらのエラーは発生しない。これは,オーバロード解決は,実行時には行われないことによる。
class Super {
static void out(float f) {
System.out.println("float"); } }
class Test {
public static void main(String[] args) {
Super.out(2);
}
}
float
class Super {
static void out(float f) {
System.out.println("float");
}
static void out(int i) {
System.out.println("int");
}
}
Superを再コンパイルし,Testを再コンパイルしない場合,その新しいバイナリをTestの既存のバイナリと共に実行すると,以前として,次の出力を得る。
float
しかし,その後,この新しい Super を使用して Test を再コンパイルした場合,次の出力を得る。
intこれは,以前のプログラムが,本来期待していた結果である。
インスタンスメソッドをサブクラスに追加し,それが,スーパクラス内のメソッドを上書きした場合,既存のバイナリ内のメソッド呼出しでは,サブクラス内のメソッドが使用され,これらのバイナリは影響しない。 クラスメソッドをクラスに追加した場合,このメソッドは,発見されない。 それは,クラスメソッドの呼出しは,そのメソッドを宣言したクラスの完全修飾名を使用して,コンパイル時に解決されることによる。
class Hyper {
void hello() {
System.out.print("Hello, ");
}
static void world() {
System.out.println("world!");
}
}
class Super extends Hyper { }
class Test {
public static void main(String[] args) {
Super s = new Super();
s.hello();
s.world();
}
}
Hello, world!
class Super extends Hyper {
void hello() {
System.out.print("Goodbye, cruel ");
}
static void world() {
System.out.println("earth!");
}
}
Super を再コンパイルし,Hyper又はTestは再コンパイルしない場合,その新しいバイナリをHyper及びTestの既存のバイナリと共に実行すると,次を出力する。
Goodbye, cruel world!
s.world();
この例では,この呼出しが,コンパイル時に,クラスメソッド world を含むクラスへの記号参照に解決されることを,次に記述したかのように,示す。
Hyper.world();
これが,この例で,SuperではなくHyperのworldメソッドが呼び出される理由である。
もちろん,新しいバイナリを作成するために,すべてのクラスを再コンパイルすれば,次の結果を出力することができる。
Goodbye, cruel earth!
クラスの静的初期化子 (8.5) の追加,削除,又は変更は,既存のバイナリに影響を与えない。
13.5では,インタフェース及びそのメンバの宣言の変更が,既存のバイナリに与える影響を規定する。
publicインタフェース
public宣言をしていないインタフェースを,public宣言に変更することは,既存のバイナリとの互換性を損なわない。
public宣言をしているインタフェースを,public宣言をしないものに変更した場合,そのインタフェース型を必要とするがもはやアクセスをもたない既存のバイナリをリンクしたとき,エラーIllegalAccessErrorを投げる。
したがって,広範囲に配布するインタフェースに対しては,それらの変更はしないほうがよい。
インタフェース階層の変更は,13.4.4で規定したクラス階層に対する変更と同じで,エラーを引き起こす。
特に,以前はそのクラスのスーパインタフェースであったものを,スーパインタフェースでなくす変更は,既存のバイナリとの互換性を損なう可能性があり,VerifyError となる。
インタフェースへのメンバの追加は,既存のバイナリとの互換性を損なわない。
インタフェースからのメンバの削除は,既存のバイナリ内でリンクエラーを引き起こす場合がある。
interface I {
void hello();
}
class Test implements I {
public static void main(String[] args) {
I anI = new Test();
anI.hello();
}
public void hello() {
System.out.println("hello");
}
}
hello
インタフェースIの新しい版をコンパイルしたと仮定する。
interface I { }
"I"を再コンパイルし,Testを再コンパイルしない場合,新しいバイナリをTestに対する既存のバイナリと共に実行すると,NoSuchMethodError となる。
インタフェースのフィールド宣言を変更するための考慮点は, 13.4.7 及び 13.4.8 で規定した,クラスのstatic final フィールドに対するものと同じとする。
abstractメソッド宣言
インタフェースのabstractメソッド宣言を変更するための考慮点は, 13.4.13 , 13.4.14, 13.4.20 ,及び 13.4.22 で規定した,クラスのabstractメソッドに対するものと同じとする。