代入変換とメソッド起動変換

ここでよく取り扱ってるんで、まとめがてらちゃんと解説。

Java言語仕様第3版のCHAPTER 5: Conversions and Promotionsを簡単にまとめたものだと思ってください。

式の変換

式の変換が適用されるケースは5通りあります。

  • 代入変換
    • 式の結果を変数に格納
    • 例: Integer i = 1
  • メソッド起動変換
    • 式の結果を実引数としたメソッド/コンストラクタを実行
    • 例: s.equals("Hello")
  • キャスト変換
    • キャストによる変換
    • 例: (String) obj.clone()
  • 文字列変換
    • 文字列連結演算のオペランドをString型へと変換
    • 例: "Hello" + 256
  • 数値の格上げ

今回は、その中でも代入変換とメソッド起動変換を。

代入変換

「変数に値を代入するには、変数と同じ型かそのサブタイプの値でなければなりません」なんて説明はどこの入門書にも書いてあると思いますが、ここではもうちょっと詳しくやります。

Javaの代入で、T型の変数にE型の式の結果を代入するには、E型の式に代入変換を適用してT型の値に変換しなければなりません。この代入変換には下の5種類の変換を組み合わせて適用することができます*1

  • プリミティブ型の拡大変換
  • 参照型の拡大変換
  • ボクシング変換
  • アンボクシング変換
  • 整数定数の暗黙の縮小変換

代入変換の特徴は、「整数定数の暗黙の縮小変換」という変換が適用される点です。これはint型の定数がbyte, short, charの範囲内に収まっている場合、わざわざキャストしなくても自動的に縮小変換してくれる機能です。

なお、何度か触れてきたメソッドの戻り値の型推論に関しては代入変換に含まれていません。これは「代入変換を適用するコンテキストで戻り値の型に推論されていない型変数が含まれていた場合、型推論の前提条件を追加する」という、式の計算方法を変えているだけです。

以下は代入変換の例です。

// プリミティブ型の拡大変換 (int -> long)
long v1 = 1;
// 参照型の拡大変換 (String -> CharSequence)
CharSequence v2 = "2";
// ボクシング変換 (Integer -> int)
int v3 = new Integer(3);
// アンボクシング変換 (long -> Long)
Long v4 = 4L;
// 整数定数の暗黙の縮小変換 (char -> byte)
byte v5 = '5';

代入変換は代入式だけでなく、下記のそれぞれで利用されます。仕様書を横断して拾い集めているため、多少漏れてるかも。

  • 代入式 (15.26 Assignment Operators)
    • 代入する値の型が、代入先の変数の型と一致するように変換
    • 例: a = b
  • フィールドやローカル変数の初期化式 (8.3.2 Initialization of Fields)
    • 代入式と全く同じ扱い
    • 例: Integer a = 1; // ボクシング変換
  • 配列初期化子 (10.6 Array Initializers)
    • 個々の要素が持つ型が、配列の構成要素型と一致するように変換
    • 例: new CharSequence[] { "aaa" } // 参照型の拡大変換
  • switch文のcaseラベルの値 (14.11 The switch Statement)
    • caseの右に書いた値の型が、switchの右に書いた式の型と一致するように変換 *2
    • 例: switch ((byte) 1) { case 1: } // 整数定数の暗黙の縮小変換
  • 拡張for文のパラメータ (14.14.2 The enhanced for statement)
    • Iterable< T >またはT[]として、Tがパラメータの型と一致するように変換
    • 例: for (Enum e : TimeUnit.values()) ... // 参照型の拡大変換
  • return文 (14.17 The return Statement)
    • 返却する値の型が、メソッドの戻り値型と一致するように変換
    • 例: int foo() { return new Integer(1); } // アンボクシング変換
  • throw文 (14.18 The throw Statement)
    • スローするオブジェクトの型がThrowable型と一致するように変換
    • throw new NullPointerException(); // 参照型の拡大変換
  • 注釈型要素の規定値 (9.6 Annotation Types)
    • 規定値の型が、要素の型と一致するように変換
    • 例: @interface Value { byte value() default 1; } // プリミティブ型の拡大変換
  • 注釈要素の値 (9.7 Annotations)
    • 要素の値の型が、要素の型と一致するように変換
    • 例: @Value(2) // プリミティブ型の拡大変換

ひとつ前のエントリでは、Eclipse JDTでthrow文に代入変換が正しく適用されていないことを指摘してます。
代入変換が適用されるコンテキストでは、メソッドの戻り値型に含まれる型変数が型推論によって特定されていない場合に「戻り値型が対象の型に代入変換によって変換可能である」という制約が追加されます。

メソッド起動変換

メソッド起動変換はメソッドの実引数を、呼び出し先メソッドの仮引数型と一致させる際に適用する変換で、代入変換とほんの少しだけ異なります。

ちょっと緩めに書くと、「仮引数型(F1, .., Fn)のメソッドを実引数型(A1, ..., An)で呼び出すには、それぞれのi=1..nに対してAiがメソッド起動変換によってFiに変換しなければならない」というルールがあります。
代入変換のときと同じように書けば、T型の仮引数を持つメソッドをE型の式を実引数として実行するには、E型の式にメソッド起動変換を適用してT型の値に変換しなければなりません、という感じになります。このメソッド起動変換には下の4種類の変換を組み合わせて適用することができます*3

  • プリミティブ型の拡大変換
  • 参照型の拡大変換
  • ボクシング変換
  • アンボクシング変換

メソッド起動変換は代入変換と異なり「整数定数の暗黙の縮小変換」を適用しません。下記のような感じ。

public static void main(String[] args) {
    // プリミティブ型の拡大変換 (int -> long)
    primitiveWideningLong(1);
    // 参照型の拡大変換 (String -> CharSequence)
    referenceWideningCharSequence("2");
    // ボクシング変換 (int -> Integer)
    boxingInteger(3);
    // アンボクシング変換 (Long -> long)
    unboxingLong(new Long(4L));
    // 整数定数の暗黙のナローイング変換 (エラー)
    // implicitConstant('5');
}

static void primitiveWideningLong(long v) {}
static void referenceWideningCharSequence(CharSequence c) {}
static void boxingInteger(Integer i) {}
static void unboxingLong(long l) {}
static void implicitConstant(byte b) {}

「整数定数の暗黙の縮小変換」が採用されなかった理由は、オーバーロードの解決がひどいことになるからだと思います。
たとえば、

System.out.println(65);
System.out.println((char) 65);

を実行すると、次のように表示されます。

65
A

これは、java.io.PrintStream型にprintln(int)とprintln(char)というオーバーロードメソッドが定義されており、実引数65, (char) 65はそれぞれのメソッドを呼び出しているためです。で、65という定数に暗黙の縮小変換が適用されてしまうとしたらSystem.out.println(65)はprintln(int)とprintln(char)のいずれにも適用可能になります。
intはcharのサブタイプで、オーバーロードメソッドはよりサブタイプな引数を持つものが優先されますので、System.out.println(65)はprintln(int)ではなく、println(char)のほうが呼び出されることになり、結果としてSystem.out.println((char) 65)の意味になってしまいます。これはイマイチな感じです。

メソッド起動変換は、下記のそれぞれで利用されます。こちらも多少漏れてるかも。


メソッド起動変換が適用できる箇所は代入変換コンテキストではないため、メソッドの戻り値型に含まれる型変数の型推論方法が異なります

<T> List<T> list() { ... }

というメソッドlist()をメソッド起動変換コンテキストで実行した場合、「Tが代入変換によってObject型に変換可能である」という制約が追加されたうえで型推論が再開されます。上記の例では、list()の戻り値型はList<Object>と推論されます
これをもし、List<Hoge>型の変数に代入するような代入変換コンテキストでlist()が呼び出された場合、「List<T>が代入変換によってList<Hoge>に変換可能である」という制約を追加します。その結果として、list()の戻り値はList<Hoge>となります


だいたい頭の中が整理された。

*1:恒等変換、未チェック変換は割愛

*2:switch((byte) 1) { case 128: } とかやるとエラーになる

*3:こちらも恒等変換、未チェック変換は割愛