LoginSignup
7
5

More than 3 years have passed since last update.

Effective Java を Kotlin で読む(4):第5章 ジェネリックス

Last updated at Posted at 2018-09-18

effectivekotlin.png

章目次

Effective Java を Kotlin で読む(1):第2章 オブジェクトの生成と消滅
Effective Java を Kotlin で読む(2):第3章 すべてのオブジェクトに共通のメソッド
Effective Java を Kotlin で読む(3):第4章 クラスとインタフェース
Effective Java を Kotlin で読む(4):第5章 ジェネリックス 👈この記事
Effective Java を Kotlin で読む(5):第6章 enum とアノテーション
Effective Java を Kotlin で読む(6):第7章 メソッド
Effective Java を Kotlin で読む(7):第8章 プログラミング一般
Effective Java を Kotlin で読む(8):第9章 例外
Effective Java を Kotlin で読む(9):第10章 並行性
Effective Java を Kotlin で読む(10):第11章 シリアライズ

第5章 ジェネリックス

項目23 新たなコードで原型を使用しない

概要

型パラメータを宣言に持つクラスやインタフェースは、ジェネリック型(総称型)として知られている。ジェネリック型には型パラメータを持たせる事ができるが(例: List<String> )、これを持たない形を原型と呼ぶ。(例: List<E> に対応する原型は List

ジェネリックスは Java 5 で導入された機能であり、原型はそもそも互換性の為にサポートされている。原型を使用すると、ジェネリックスの安全性と表現力のすべてを失うことになるため、原型を使用すべきではない。

Kotlin で読む

Kotlin でも Java 同様ジェネリックスが言語でサポートされている。
が、原型はサポートされていない!
Generics: in, out, where - Kotlin Programming Language

以下のように必ず型パラメータを指定して利用する。
また、Java と異なり型推論にダイヤモンド演算子は必要なし。

class Box<T>(t: T) {
    var value = t
}

// こう使う
// val box: Box = Box(123) 原型は存在しない
val box1: Box<Int> = Box<Int>(123)
val box2: Box<String> = Box("apple") // 型推論: Box<>("apple") ではない
val box3 = Box("pen") // 型推論: とてもスマート
println(box1.value)
println(box2.value)
println(box3.value)

Try Kotlin で確認

項目24 無検査警告を取り除く

概要

ジェネリックスでプログラムする際、コンパイラは以下の警告を発する場合がある。

  • 無検査キャスト警告
  • 無検査メソッド呼び出し警告
  • 無検査ジェネリック配列生成警告
  • 無検査変換警告

警告が無ければ、コードが型安全である事が保証される為、取り除くことが可能なすべての無検査警告を取り除くべきである。

ただし、どうしても警告を取り除く事ができない場合は、最小限のスコープに @SuppressWarnings("unchecked") アノテーションを使用し、その際は適切なコメントを追加する事。

Kotlin で読む

Kotlin では原型がサポートされていない為、無検査メソッド呼び出し・無検査変換警告は通常発生しない。また、項目25でも述べるが Kotlin の配列は不変なので無検査ジェネリック配列生成警告も発生しない。

しかし Kotlin のジェネリックスも Java と同じく、実行時には型情報が消去されている。故にキャストに関してはプログラマが意識して警告を取り除く必要がある。

どうしても警告を取り除けない場合は @Suppress("UNCHECKED_CAST") を用いて警告を抑制する事ができる。

class Foo<T> {
    @Suppress("UNCHECKED_CAST")
    fun bar() = "foobar" as T // ここで無検査キャスト ただし実行時には型情報が消えているので例外は発生しない
}

fun main(args: Array<String>) {
    val foo = Foo<Int>()
    val v: Int = foo.bar() // 使用時に ClassCastException が発生
}

Try Kotlin で確認

項目25 配列よりリストを選ぶ

概要

ジェネリックスにより、基本となる型は同じでも型引数が異なる型が生まれる。
これらの型同士の関係性を表す概念を変位(variance)と呼ぶ。
Java のジェネリックスで考えると次のようになる。

  • 不変(invariant)
    • 型同士関係がないこと
  • 共変(convariant)
    • クラスAがクラスBのサブタイプである場合、 Hoge<A>Hoge<B> のサブタイプであること
  • 反変(contravariance)
    • クラスAがクラスBのサブタイプである場合、 Hoge<B>Hoge<A> のサブタイプであること
    • 共変の逆

これを踏まえて、配列とジェネリック型には以下の重要な違いがある。

  • 配列は共変(convariant)であり、ジェネリックスは不変(invariant)である事

つまり、
String[]Object[] のサブタイプだが、
List<String>List<Object> のサブタイプでもスーパータイプでも無い。

// コンパイル可能
Object[] objectArray = new String[1];
objectArray[0] = new Object(); // ただし、ここで ArrayStoreException がスローされる

// コンパイル不可能
List<Object> objectList = new ArrayList<String>();

この理由から

  • 配列は具象化されている(reified)が、ジェネリックスはイレイジャ(erasure)で実装されている。

つまり、実行時において、配列は型情報を保持しているが、ジェネリックスでは型情報は削除されている(コンパイル時までしか型情報を持たない)。

結論としては、配列とジェネリックスは異なる型規則を持っており、うまく調和しない。それらを混在させコンパイル時エラーや警告が出るくらいなら、配列を使わずリストを常に使った方が良い。

Kotlin で読む

Kotlin の配列は List やその他と同じく不変(invariant)である。よって Java と違い以下のコードはコンパイル時点でエラーが発生する。異なる型規則を気にする必要は無い。

val anyArray: Array<Any> = Array<Int>() // コンパイル不可: Type mismatch

※ 余談: Array<Int> は Java の Integer[] に相当する(型引数にプリミティブ型は使えない)。各種プリミティブ型配列に対応したクラスも用意されている(int[] に対する IntArray 等)ので、ボクシングのオーバーヘッドが気になる時はこちらを使う。
Basic Types: Numbers, Strings, Arrays - Kotlin Programming Language#Arrays

項目26 ジェネリック型を使用する

概要

新たに型を設計する時は、クライアントのコードでキャストが不要か確認すべきである。ジェネリック型は、キャストが必要な型より安全で使いやすい為、可能であればこちらを選ぶべき。

書籍では、Objectに基づくスタック実装をジェネリック化することを例として説明している。

Kotlin で読む

例えば、以下のような Any 型に基づくスタック実装があるとする。

// エラー処理等いろいろ省略したスタック実装
class AnyStack {
    private val elements = Array<Any?>(1024) { null }
    var size = 0

    fun push(e: Any?) {
        elements[size++] = e
    }
    fun pop() = elements[--size]
}

// こう使う
val stack1 = AnyStack()
stack1.push(123)
val v1 = stack1.pop() as Int // キャストが必要

ジェネリックスを用いてこのように書ける。

class GenericStack<T> { // T には nullable 型も入る
    private val elements = Array<Any?>(1024) { null }
    var size = 0

    fun push(e: T) {
        elements[size++] = e
    }
    @Suppress("UNCHECKED_CAST")
    fun pop() = elements[--size] as T // 無検査キャスト 実行時には型情報は消える
}

// こう使う
val stack2 = GenericStack<Int>()
stack2.push(123)
val v2 = stack2.pop()

Try Kotlin で確認

項目27 ジェネリックメソッドを使用する

概要

ジェネリックはメソッドにも利用可能である。ジェネリッククラスと同様に、入力パラメータと戻り値をキャストすることをクライアントに要求するメソッドは、ジェネリック化すべきである。

また、Java 6 まではジェネリックメソッドにおいてのみ型推論が行われていた為、書籍ではジェネリック static ファクトリーメソッドを使う事でインスタンス生成においても型推論を利用できるようにする手法を紹介していた。が、これは Java 7 においてダイヤモンド演算子が導入された事で現在は不要なテクニックとなっている。

// java 6 まで
Map<String, List<String>> map1 = new HashMap<String, List<String>>();
// java 7 から
Map<String, List<String>> map2 = new HashMap<>(); // <> : ダイヤモンド演算子
// ジェネリック static ファクトリーメソッド はこんな感じ
public static <K,V> HashMap<K,V> newHashMap() {
  return new HashMap<K,V>();
}
// こう使う
Map<String, List<String>> map3 = newHashMap();

Kotlin で読む

Kotlin でもジェネリックメソッドが利用可能。

// 通常
fun <T> getMiddle1(array: Array<T>): T? {
    return array.getOrNull(array.size / 2)
}

// 拡張関数でも使える
fun <T> Array<T>.getMiddle2(): T? {
    return getOrNull(size / 2)
}

// こう使う
val array = arrayOf(1, 2, 3, 4, 5)
val m1 = getMiddle1(array)
val m2 = array.getMiddle2()

また、特筆すべきは具象型パラメータ付き関数の存在である。基本的にジェネリックスで型情報は実行時に削除されるのだが、インラインメソッドの場合のみ型パラメータにreified修飾子をつける事で、メソッド内で型情報を利用するコードが書けるようになる。

inline fun <reified T> printClass() = println(T::class.java) // Tから型情報を得られる

// こう使う
printClass<String>() // -> class java.lang.String

Try Kotlin で確認

項目28 API の柔軟性向上のために境界ワイルドカードを使用する

概要

Java のジェネリックスでは、ワイルドカードが提供されており、以下のような分類がある。

  • 非境界ワイルドカード
    • 例: List<?>
  • 上限境界ワイルドカード
    • 例: List<? extends Number>
    • 共変を表現できる
  • 下限境界ワイルドカード
    • 例: List<? super Number>
    • 反変を表現できる
// 非境界ワイルドカード
// List<?> : すべてのパラメータ化されたListのスーパータイプ
List<?> l11 = new ArrayList<Object>();
List<?> l12 = new ArrayList<Number>();
List<?> l13 = new ArrayList<Integer>();
// 上限境界ワイルドカード
// List<? extends Number> : Numberを継承するクラスでパラメータ化されたListのスーパータイプ
List<? extends Number> l21 = new ArrayList<Object>(); // コンパイルエラー
List<? extends Number> l22 = new ArrayList<Number>();
List<? extends Number> l23 = new ArrayList<Integer>();
// 下限境界ワイルドカード
// List<? super Number> : Numberが継承するクラスでパラメータ化されたListのスーパータイプ
List<? super Number> l31 = new ArrayList<Object>();
List<? super Number> l32 = new ArrayList<Number>();
List<? super Number> l33 = new ArrayList<Integer>(); // コンパイルエラー

境界ワイルドカードを使うと、不変であるパラメータ化された型に柔軟性を持たせる事ができる。

ただし、境界ワイルドカードを使う場合は、そのパラメータがプロデューサー(生産者)かコンシューマー(消費者)のどちらの役割かを考えなくてはならない(PECS原則)。プロデューサーかつコンシューマーの場合は、正確な型一致が必要となる。

class Stack<E> {
    @SuppressWarnings("unchecked")
    E[] array = (E[]) new Object[1024];
    int size = 0;

    // src は生産者
    public void pushAll(List<? extends E> src) {
        for (E e : src) array[size++] = e;
    }

    // dst は消費者
    public void popAll(List<? super E> dst) {
        while (size > 0) dst.add(array[--size]);
    }
}

// こう使える
List<Integer> src = Arrays.asList(1, 2, 3);
List<Object> dst = new ArrayList<>();
Stack<Number> stack = new Stack<>();
stack.pushAll(src);
stack.popAll(dst); // {3, 2, 1}

ワイルドカードを使う事で、内部的にはより複雑なコードを書く必要があるが、APIはより柔軟になる。広く使用されるライブラリー等では、ワイルドカード型を適切に使用すべきである。

Kotlin で読む

Kotlin は変位指定の為の修飾子(分散アノテーション)を持っている。
型パラメータを修飾する outin の修飾子は、それぞれ以下の特徴を持つ

  • out 修飾子
    • 共変を表現する(<out E> が Java の <? extends E> に対応)
    • メソッドの戻り値(out ポジション)としてのみ型パラメータが利用可能になる制限がつく
      • つまり出力(生産)の操作のみ可能
  • in 修飾子
    • 反変を表現する(<in E> が Java の <? super E> に対応)
    • メソッドの引数(in ポジション)としてのみ型パラメータを利用可能になる制限がつく
      • つまり入力(消費)の操作のみ可能

先程の Java の Stack の例を Kotlin で書いてみる

class Stack<E> {
    @Suppress("UNCHECKED_CAST")
    var array = arrayOfNulls<Any>(1024) as Array<E>
    var size = 0

    fun pushAll(src: List<E>) { // out 修飾子は不要
        for (e in src) array[size++] = e
    }

    fun popAll(dst: MutableList<in E>) {
        while (size > 0) dst.add(array[--size])
    }
}

// こう使える
val src: List<Int> = listOf(1, 2, 3)
val dst: MutableList<Any> = mutableListOf()
val stack: Stack<Number> = Stack()
stack.pushAll(src)
stack.popAll(dst) // {3, 2, 1}

Try Kotlin で確認

pushAll メソッドのパラメータ src: List<E>out 修飾子は必要ない。
何故か? List のインタフェースは以下のようになっている。

public interface List<out E> : Collection<E> { ... }

このように、Kotlin ではクラスが型パラメータに対してどのような変位を持っているかを指定する事ができる。(宣言箇所分散)

Java が使用箇所で変位を指定するのに対し、Kotlin では宣言箇所でも変位指定ができる
ため、利用箇所で毎回変位指定する必要がなく、よりコードがすっきり書ける。

項目29 型安全な異種コンテナーを検討する

概要

コンテナでは通常、特定の型でパラメータ化されたものを利用する。
(例:Map<Integer, String>
しかし、時にはさらなる柔軟性が求められる事がある。

例えば、データベースの行は任意の数の多くの列を持っており、型安全な方法でそれらすべての列にアクセスできると良い。

書籍では、MapのキーとしてClassオブジェクトを利用することで、型安全な異種コンテナーの実装方法を紹介している。具体的には以下のようになる。

public class Favorites {
  private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>(); // Java 7 以降ならダイヤモンド演算子で良い

  public <T> void putFavorite(Class<T> type, T instance) {
    if (type == null) throw new NullPointerException("Type is null");
    favorites.put(type, instance);
  }

  public <T> T getFavorite(Class<T> type) {
    return type.cast(favorites.get(type));
  }
}

// こう使える
Favorites fav = new Favorites();
fav.putFavorite(String.class, "hoge");
fav.putFavorite(Integer.class, 123);
fav.getFavorite(String.class); // "hoge"
fav.getFavorite(Integer.class); // 123

Kotlin で読む

特にないので Kotlin で書き直してみます

class Favorites {
    private val favorites = HashMap<Class<*>, Any>()

    fun <T> putFavorite(type: Class<T>, instance: T) {
        favorites[type] = instance as Any
    }

    fun <T> getFavorite(type: Class<T>): T? {
        return type.cast(favorites[type])
    }
}

// こう使える
val fav = Favorites()

fav.putFavorite(String::class.java, "hoge")
fav.putFavorite(Integer::class.java, Integer(123))
fav.getFavorite(String::class.java) // "hoge"
fav.getFavorite(Integer::class.java) // 123

せっかくなので拡張関数と具象型パラメータ付き関数を利用してみる。

inline fun <reified T> Favorites.putFavorite2(e: T) = this.putFavorite(T::class.java, e)
inline fun <reified T> Favorites.getFavorite2() = this.getFavorite(T::class.java)

// こう使える
fav.putFavorite2("fuga")
fav.putFavorite2(321)
fav.getFavorite2<String>() // "fuga"
fav.getFavorite2<Int>() // 321

Try Kotlin で確認

味わい深いですね。

おわり

参考資料等

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5