放課後プログラミング

調べたことや考えたことなどを忘れないために書きます。

SonarQubeの上手な使い方

SonarQubeとは、コードの品質を可視化し、継続的に監視することで、抱えている技術的負債を制御可能にするためのプラットフォームのこと。*1
技術的負債を抑えて保守性を高めることは、チームの生産性を高いレベルに維持することに大きく寄与するので、継続的に内製開発をする場合は是非とも検討したいツールに見える。

そんなSonarQubeについて半年くらい前に会社で紹介したところ、部署全体で導入することになった。

で、半年くらい経ったが、現状SonarQubeは(Jenkinsと併せて使うことで)ある程度は技術的負債の発生を抑止することに役立っている。
しかし、導入してJenkinsビルド時に一緒に解析するように設定しただけでは、技術的負債を制御可能にはならない。
それを可能にするためにはどのようにSonarQubeを使えばいいか、僕が実践している方法を書く。
したがって、この投稿は導入してみたけどイマイチだなって思った人向けだ。もしかしたら使い方を変えれば認識も変わるかもしれない。

機械的に判断できる箇所しか検知できないことを意識する

SonarQubeを導入すれば保守性については万事OKとなるわけではない。それどころか、現状僕がいるチームではSonarQubeの保守性の維持に対する貢献度は5%に満たないだろう*2
保守性とはそもそも保守する人間に依存した性能だ。保守者が猿であれば誰も保守性の高いコードは書けない。したがって保守者の間でどこまでが共通認識となっているかを把握し、その共通認識となっている知識を前提として読んだ際に最大限理解しやすく書くことで、保守性を最大化できる*3
しかしSonarQubeにはそのような共通認識を細かく記述する機能はないし*4、それができたとしても、その共通認識から人間であれば殆どが理解しうる記述かどうかを判別する知性もない。あるのは一般的に言われる「AはBのように書くべき」という極めて単純なルールの違反を機械的に検知する機能だけだ。当たり前のことだが、それを意識することで、「これは機械に任せよう」「これは人間で対応しよう」という境界が見えやすくなり、このツールを有効に使うことができるようになる。

SonarQubeを有効利用する手順

半年ほどSonarQubeを使ってそれなりに役立っている利用方法を紹介する。

手順1 : QualityGateを設定し、BuildBreakerプラグインを導入する

  1. QualityGateを設定し、BuildBreakerプラグインを導入することで、Jenikinsでビルドする際にコードの品質が閾値を跨ぐとビルドをFAILUREにさせられる。
  2. この際にBlocker,Critical,MajorのIssueが0以上であればFAILするようにしておく。閾値を0より大きくすると今回は仕方ないから増やそうみたいになって良くないので、徹底する。
  3. とにかくIssueの扱い方でSonarQubeを有意義に使えるかどうかが決まると言っても過言じゃない。
  4. テストカバレッジや重複コードの閾値などはお好みで*5

手順2 : プロダクト専用のQualityProfileを作成する

  1. デフォルトのQualityProfileをコピーしたもので良い。プロダクト専用にしておくのが重要。

手順3 : Jenkinsでビルドする

  1. JenkinsのビルドとともにSonarQubeの静的解析が走り、例えばMajor Issueが100個出て失敗したりする。こっからの対応方法が重要!
  2. この中には「うちのプロダクトではこれは別にOKだと思うけどなあ」というものが結構ある。それを一つ一つ精査し、その検知が不要であればQualityProfileの編集画面でそのルールをMinorやInfoに格下げする。
  3. あるいは、例えばロガーの命名はデフォルトでLOG(?:GER)しか認められていないが、単一クラスでも複数種類のロガーを使い分けていてFOO_LOGGERBAR_LOGGERとしたい場合などは、ルールの定義を書き換える。
  4. もちろんルールが妥当であれば、コードの方を直す。

このように運用していると、だんだんとQualityProfileがそのプロダクトに最適化されていき、余計なIssueでビルドが失敗せず、尚且つ監視したいルールは違反した時に検知できる、という状態になる。
このようにプロダクトに合ったルールに書き換えていくことで、SonarQubeは本来の力を発揮できるようになる。

忘れてはならないのは、SonarQubeは保守性のほんの僅かな部分でしか効果を発揮できないということ。日頃からチーム内で密にコミュニケーションを取り、共通認識のレベルを高めておくことのほうが断然、保守性を高めることには効果的である。
それではみんな仲良くソフトウェア開発を楽しみましょう。

*1:SonarQube

*2:それ以外はコードレビューやペアプロによるスキル(プログラミングの技術に加え、特定ドメインにおける知識等も含む、これらのチーム内での共通認識のレベルをここではスキルと呼んでいる)の向上によって保守性を維持できる。

*3:そのため、自分が読みやすいからといってそれが保守性が高い状態とは言い切れない。意味不明なコードや文章を書いてしまう人はこれを認識し損ねているか、共通認識を見誤っているかのどちらかの可能性が高い。そのコードや文章を読む人が意味不明と思った時点で、それは良くない。僕が書いたコードや文章も、対象としている人が読んで意味不明と思ったら、それはうまく書けてないということ。

*4:あったとしてもそれを入力することは非常に大変であることが『人工知能は人間を超えるか (角川EPUB選書)』に書かれている

*5:参考:http://qiita.com/naiad/items/163e0a47b62df89cdfcf

spring-bootでwebsocket serverとclient

Spring Bootを使ってWebsocketサーバとクライアントを作ってみたgithub.com
github.com

動かすとこんな感じ
f:id:hiroaki-kono:20150523190646p:plain

サーバの動作
  • クライアントが接続してきたら不定期(100ms~2000msの間隔)に接続中の全クライアントに対して何ms経過したかを送信
  • 接続しているクライアントからメッセージを受信したら接続中の全クライアントに対してメッセージのechoを送信
クライアントの動作
  • 起動時にwebsocketサーバに接続
  • websocketサーバから受信したメッセージは右のテキストエリアに表示
  • 自分が送信を試みたメッセージは左のテキストエリアに表示

簡単な対戦ゲームくらいならすぐに作れそう

プロセス内で別プロセスを呼んでやり取りする

親プロセスと子プロセス間でコミュニケーションを取りたい。その方法の簡単なメモ。

1. 別プロセスの実行

String cmd = "java HelloWorld"
ProcessBuilder pb = new ProcessBuilder(cmd.split(" "));
pb.start();

pb.start();を実行することでプロセスを実行できる。

2. 子プロセスとのやり取り(親プロセス側に書く)

Process p = pb.start();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(p.getOutputStream()));
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));

try {
    bw.write(1 + "\n");
    bw.flush();

    return br.readLine();
} catch (IOException e) {
    return ""
}

1.でpb.start();したときの戻り値からOutputStreamとInputStreamを取得できる。それを便利なようにそれぞれBufferedWriterとBufferedReaderに変換しておく。
writerのwriteメソッドでメッセージを渡し、flushすることで、子プロセスの標準入力へと送りつけることができる。

3. 親プロセスとのやり取り(子プロセス側に書く)

Scanner scanner = new Scanner(System.in);

int num = scanner.nextInt();
System.out.println(num++);
System.out.flush();

標準入力を受けるScannerを作り、そこからnext()やnextInt()などを使って読み込めば良い。また、親プロセスにメッセージを送るには標準出力に出せば良い。
これで、子プロセスに数字を送るとインクリメントした値を親プロセスに返してきてくれるという無駄なやり取りができるようになった。

実際に意味のあるメッセージのやり取りを行う場合は、メッセージの形(プロトコル)を定義し、やり取りを行うのが良いと思う。例えば複数回メッセージのやり取りを行うのであれば、メッセージの終わりを示すルールを定義する(具体的には、最後の行を継続するしないのフラグとして、"CONTINUE"なら継続、"END"なら終了とする、といったルール)。当然その他のメッセージの内容にも何行目は何を示すかなどを定義し、お互いがそれに従って実装する必要がある。

JavaFXのAnchorPaneとかVBoxとか使うと変な隙間ができる

正しいお作法はよく知らないが、結論から言うとBorderPaneを一番上に使うことで解決した。

掲題で言っている隙間とは、例えば以下のようなfxmlを書いた時(Scene Builderで作った時)

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.VBox?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0"
            prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
    <children>
        <VBox prefHeight="400.0" prefWidth="600.0">
            <children>
                <AnchorPane prefHeight="80.0" prefWidth="600.0" style="-fx-background-color: #888;"/>
                <AnchorPane prefHeight="240.0" prefWidth="600.0"/>
                <AnchorPane prefHeight="80.0" prefWidth="600.0" style="-fx-background-color: #888;"/>
            </children>
        </VBox>
    </children>
</AnchorPane>

Scene Builder上では以下のように見えている
f:id:hiroaki-kono:20150506053134p:plain

にも関わらず、実行してみると以下のようなwindowが現れる。
f:id:hiroaki-kono:20150506052952p:plain

これがググってみたが何故か原因は出てこずわからなかった。デフォルトのpaddingが設定されているのかと思ってcssで0にしてみても変わらずだったのでよくわからない。

ググり方が悪いのか僕の環境特有なのかわからないが、以前にも同じことで悩んだので解決策だけメモしておく。

java.lang.String#trim()は全角スペースを消してくれない

java.lang.String#trim()javadocには(拙訳)

1. 空文字列もしくは最初と最後が\u0020より大きい文字ならそのオブジェクトをそのまま返す
2. それ以外の場合、\u0020以下の文字しか存在しない文字列なら新しい空文字列オブジェクトを返す
3. それ以外の場合、最初の\u0020以下ではない文字のインデックスをk、最後の\u0020以下ではない文字のインデックスをmとしたとき、this.substring(k, m+1)の結果を返す

とあります。
\u0020以下とはすなわち制御文字と半角スペースのことで、全角スペースを消すような仕様はありません。

なので自分用にtrim()を実装し直しました。

元々のjava.lang.String#trim()

public String trim() {
    int len = value.length;
    int st = 0;
    char[] val = value;    /* avoid getfield opcode */

    while ((st < len) && (val[st] <= ' ')) {
        st++;
    }
    while ((st < len) && (val[len - 1] <= ' ')) {
        len--;
    }
    return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}

staticメソッドとして書き換えたバージョン

class StringUtils {
    /**
     * also trims 2byte space chars.
     *
     * @see java.lang.String#trim()
     * this won't trim 2 byte space chars.
     */
    public static String trim(String value) {
        if (value == null) {
            return null;
        }
        int len = value.length();
        int st = 0;
        char[] val = value.toCharArray();

        while ((st < len) && (val[st] <= ' ' || val[st] == ' ')) {
            st++;
        }
        while ((st < len) && (val[len - 1] <= ' ' || val[len - 1] == ' ')) {
            len--;
        }
        return ((st > 0) || (len < value.length())) ? value.substring(st, len) : value;
    }
}
public class StringUtilsTest {

    @Test // = OK
    public void testTrim_前後の空白を取り除く() throws Exception {
        //exercise
        String actual = CStringUtils.trim(" hoge ");
        //assertion
        assertThat(actual, is("hoge"));
    }

    @Test // = OK
    public void testTrim_前後の全角空白を取り除く() throws Exception {
        //exercise
        String actual = CStringUtils.trim(" hoge ");
        //assertion
        assertThat(actual, is("hoge"));
    }
}

適宜whileの条件に自分が消したい文字を追加していけば自分の好きな仕様のtrim()が実装できますね。

ワイルドカードの使い方 (ジェネリクスの使い方 3)

以前の投稿「ジェネリクスの使い方 1」「ジェネリクスの使い方 2」で大まかにジェネリクスについてまとめましたが、1,2の投稿だけではワイルドカードを正しく効果的に使うのは難しいと感じました。なぜならワイルドカードの利点について言及できていなかったからです。
今回の投稿ではワイルドカードについて更に掘り下げて、これを使うと何が嬉しくて、いつ使えば良いのかに焦点を当てます。

ワイルドカードでできることのまとめ

下記にワイルドカードでできる操作をまとめました、それぞれの代入操作で左右オペランドの型がどのような制限のある型なのかを意識して見ると理解しやすいと思います。

//準備
class A {}
class B extends A {}
class C extends B {}

List<A> listA = new ArrayList<>();
List<B> listB = new ArrayList<>();
List<C> listC = new ArrayList<>();
List<Object> listO = new ArrayList<>();
A a;
B b;
C c;
Object o;


//非境界ワイルドカード型まとめ
List<?> unbounded;

unbounded = listA; // OK // ワイルドカードに境界がないため、
unbounded = listB; // OK // どのような型に対応付けされたList型(及びListのサブクラス型)でも
unbounded = listC; // OK // 代入可能です。
unbounded = listO; // OK //

listA = unbounded; // NG // 特定の型に対応付けされた総称型に、
listB = unbounded; // NG // 未知の型を持てるにワイルドカード型の総称型を代入することはできません。
listC = unbounded; // NG // また、List<A>型が代入されたList<?>型を再度List<A>型に代入することもできません。
listO = unbounded; // NG // List<?>型に入った時点で<A>への対応付け情報が消えるためです。
                         // 全ての型が継承しているObject型でも代入できないのは、総称型の非変性のためです。

a = unbounded.get(0); // NG // 未知の型の値をA型変数に入れることはできません。
b = unbounded.get(0); // NG // こちらも同様。
c = unbounded.get(0); // NG // こちらも同様。
o = unbounded.get(0); // OK // 全てのオブジェクトはObject型を継承しているため、
                            // Object型の変数には未知の型でも代入可能です。

unbounded.add(a);    // NG // Listにおける非境界ワイルドカード型の場合、
unbounded.add(b);    // NG // セットされる総称型変数に対応付けされた型が完全に不明なため、
unbounded.add(c);    // NG // どのような型でもセットすることはできません。
unbounded.add(o);    // NG // 抽象的に言えば、特定の型に対応付けされた型の変数を、
                           // 非境界ワイルドカード型の変数に代入することはできません。
unbounded.add(null); // OK // ただし、nullに限って境界の有無に関わらず常にセット可能です。


//上限境界ワイルドカード型
List<? extends B> upperBounded;

upperBounded = listA; // NG // 上限境界とは、当該クラスとそのサブクラスのいずれかに対応付けされた
upperBounded = listB; // OK // 総称型のみ代入可能という制限のことをいいます。
upperBounded = listC; // OK // そのため、Bクラスとそのサブクラス以外ではエラーとなります。
upperBounded = listO; // NG //

listA = upperBounded; // NG // 制限が加わったため、listBやそのスーパークラス型の変数には代入可能としても良く見えますが、
listB = upperBounded; // NG // やはり総称型の非変性のため、どのような型に対応付けされた総称型変数にも
listC = upperBounded; // NG // 代入はできません。
listO = upperBounded; // NG //

a = upperBounded.get(0); // OK // 上限境界があるため、ワイルドカードに入りうる型は非境界の場合よりも
b = upperBounded.get(0); // OK // 範囲が狭まっています。この場合では、B型とそのサブクラス型しか入らないため、
c = upperBounded.get(0); // NG // B型とそののスーパークラスには全て代入可能です。
o = upperBounded.get(0); // OK // これらはB b; A a = b;とできて、B b; C c = b;とできないことと全く同じ意味です。

upperBounded.add(a);    // NG // <? extends B>という上限境界があるとき、?はBとそのサブクラスに対応付けされる
upperBounded.add(b);    // NG // ことが許容されますが、それが実際にはC型に対応付けされているのか、あるいは
upperBounded.add(c);    // NG // 別のサブクラスやもっと深いサブクラスが存在して、それらに対応付けされているのかはわかりません。
upperBounded.add(o);    // NG // そのため、B型やそのサブクラス型の変数であってもセットすることはできません。
upperBounded.add(null); // OK // すなわち、特定の型を下限が未知の型に代入することはできません。


//下限境界ワイルドカード型
List<? super B> lowerBounded;

lowerBounded = listA; // OK // 下限境界とは、当該クラスとそのスーパークラスのいずれかに対応付けされた
lowerBounded = listB; // OK // 総称型のみ代入可能という制限のことをいいます。
lowerBounded = listC; // NG // そのため、BのサブクラスとなるCに対応付けされた総称型の変数は
lowerBounded = listO; // OK // 代入することができません。
                            // ObjectクラスからBクラスまでの直系に存在しないクラス型の変数も代入できません。

listA = lowerBounded; // NG // 今まで説明してきたとおり、総称型の非変性により、これらは常に代入できません。
listB = lowerBounded; // NG //
listC = lowerBounded; // NG //
listO = lowerBounded; // NG //

a = lowerBounded.get(0); // NG // <? super B>という下限境界により、Bとそのスーパークラス型に
b = lowerBounded.get(0); // NG // 対応付けされた総称型のみ許容されるため、それが実際にはB型なのか
c = lowerBounded.get(0); // NG // A型なのかあるいはもっと上位のクラス型なのかわからないため、
o = lowerBounded.get(0); // OK // 全てのオブジェクトが継承しているObject型にのみ代入が可能です。

lowerBounded.add(a);    // NG // 下限境界により、Bとそのスーパークラス型に対応付けされた総称型であるとわかるため、
lowerBounded.add(b);    // OK // Bとそのサブクラス型の変数は全てセットすることができます。
lowerBounded.add(c);    // OK // これらも、B b; A a = b;とできて、
lowerBounded.add(o);    // NG // B b; C c = b;とできないことと全く同じ意味です。
lowerBounded.add(null); // OK //


//ワイルドカードが使える場所と使えない場所
class A<?>{}                 // NG // クラス宣言の型
class A extends B<?>{}       // NG // クラス宣言のスーパータイプの型
class A extends B<List<?>>{} // OK // クラス宣言のスーパータイプの型の型
? method(){}                 // NG // メソッドの返り値型
List<?> method(){}           // OK // メソッドの返り値型の型
void method(? x){}           // NG // 変数宣言の型
void method(List<?> list){}  // OK // 変数宣言の型の型
? x;                         // NG // 変数宣言の型
List<?> list;                // OK // 変数宣言の型の型
new ArrayList<?>();          // NG // インスタンス生成の型
new ArrayList<List<?>>();    // OK // インスタンス生成の型の型
//基本的にそこに書ける最上位の型としてワイルドカードは使えない
PECS - (Producer - extends, Consumer super)

上記のまとめはPECSを示しています。
すなわち、Producer(生産者)はextendsを使い、Consumer(消費者)はsuperを使いましょうという意味です。
生産者はgetされるオブジェクトで、消費者は何かをsetするオブジェクトです。
これは、extends Xを使ってXを上限境界に設定すれば、getした結果をX以上の型に入れられ、(生産し、提供できる)
super Xを使ってXを下限境界に設定すれば、X以下の型なら自由にset(消費行動)できるためです。

いつ使えばいいのか(1)

ではどのような時に使うと「ワイルドカード機能があってよかった」と思えるのでしょうか。
producerリストの内容をconsumerリストにディープコピーする2つの同じ動作をするメソッドを見比べてみると、ワイルドカード機能は不要に見えます。

<T> void copy(List<? extends T> producer, List<T> consumer){
    consumer.clear();
    for (T t : producer){
        consumer.add(t);
    }
}

<T, T2 extends T> void copy(List<T2> producer, List<T> consumer){
    consumer.clear();
    for (T t : producer){
        consumer.add(t);
    }
}

オラクルの公式ドキュメント曰く、ワイルドカードを用いた方が簡潔なコードとなるため、別の場所で参照されない型は型パラメータを使わずにワイルドカードを用いた方が良いとしています。
上記の例で言えば、後者メソッドの型パラメータT2はわざわざパラメータ化しているにも関わらずどこからも使われておらず、たんにTのサブクラス型の何かという情報を持たせたいだけなので、前者メソッドの方法で十分なのです。
また、万が一下記のようなメソッドを実装したい場合はワイルドカードを使わざるを得ないかと思います。

public List<?> getList(boolean b){
    return b
           ? new ArrayList<String>(){{ add("a"); }}
           : new ArrayList<Integer>(){{ add(1); }};
}

// 普通はもっと上位ロジックの部分で分岐させた方が他のロジックも書きやすいのではないかと思います。
// なぜならList<?>を返り値として受けてもそのリストに対してnull以外setすることができず、
// オブジェクト型としてしかgetできないからです。(要するに上記メソッドは不便なだけ)
if(b){
    List<String> list = new ArrayList<String>(){{ add("a"); }};
    ... //listに行う処理
} else {
    List<Integer> list = new ArrayList<Integer>(){{ add(1); }};
    ... //listに行う処理
}

いつ使えばいいのか(2)

上記で述べた利点のためだけにワイルドカード機能が追加されたわけではありません。
ここまでは全てジェネリクスがサポートされたコードを前提として解説してきましたが、ワイルドカードが本質的に力を発揮するのはジェネリクスがサポートされる以前のコードをジェネリクスを使ったコードに書き換える時です。この作業は慎重に行わないと以前と比べて制限が厳しくなってしまう場合があります。

たとえばここで、CollectionインタフェースのcontainsAll()について見てみます。

// ジェネリクスサポート前
interface Collection {
    public boolean containsAll(Collection c);
}

//ジェネリクスサポート後
interface Collection<E> {
    public boolean containsAll(Collection<?> c);
}

ここで、なぜジェネリクスサポート後のcontainsAll()メソッド宣言は

public boolean containsAll(Collection<E> c);

ではないのでしょうか?
理由はジェネリクスサポート前の制限との整合性を保つためです。
containsAll()AbstractCollectionで下記のように実装されて、ArrayListに継承されています。

public abstract class AbstractCollection<E> implements Collection<E> {
    public boolean containsAll(Collection<?> c) {
        for (Object e : c)
            if (!contains(e))
                return false;
        return true;
    }
}
List list1 = new ArrayList(){{ add(1); add(2); }};
List list2 = new ArrayList(){{ add("a"); add("b"); }};
System.out.println(list1.containsAll(list2)); // -> false
List<Integer> list1 = new ArrayList<Integer>(){{ add(1); add(2); }};
List<String> list2 = new ArrayList<String>(){{ add("a"); add("b"); }};
System.out.println(list1.containsAll(list2)); // -> false

このように、ジェネリクスを用いない実装とジェネリクスを用いた実装で整合性が保たれています。
次に、以下のように<E>を使ったbadContainsAll()を実装し、それを使ってみるとどうでしょうか。

class BadList<E> extends ArrayList<E> {
    public boolean badContainsAll(Collection<E> c){
        for (Object e : c)
            if (!contains(e))
                return false;
        return true;
    }
}
BadList list1 = new BadList(){{ add(1); add(2); }};
BadList list2 = new BadList(){{ add("a"); add("b"); }};
System.out.println(list1.badContainsAll(list2)); // -> false
BadList<Integer> list1 = new BadList<Integer>(){{ add(1); add(2); }};
BadList<String> list2 = new BadList<String>(){{ add("a"); add("b"); }};
System.out.println(list1.badContainsAll(list2)); // -> コンパイルエラー

// Error: java: クラス BadList<E>のメソッド badContainsAllは指定された型に適用できません。
//   期待値: java.util.Collection<java.lang.Integer>
//   検出値: BadList<java.lang.String>
//   理由: 実引数BadList<java.lang.String>はメソッド呼出変換によってjava.util.Collection<java.lang.Integer>に変換できません

型安全という意味では正しい挙動に見えますが、ジェネリクスを用いない実装とジェネリクスを用いた実装で動作に違いが出ています。

まとめ

完全に新規のプロダクトをジェネリクスサポート後のjavaで実装する場合はワイルドカードが大活躍する場面はあまりありませんが、ジェネリクスサポート前のプロダクトをジェネリクスで書き換える際には必要に応じてワイルドカードを使うことになります。
たとえばjava.util.Collections.max()メソッドは、このように宣言されています。

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) {
    ...
}

型パラメータがかなり複雑に利用されています。
ジェネリクスサポート前の同メソッドはこのように宣言されています。

public static Object max(Collection coll) {
    ...
}

本来であれば

public static <T extends Comparable<? super T>> T max(Collection<T> coll) {
    ....
}

とするのが自然に思えますが、ジェネリクスサポート前のmax()の実装に挙動を合わせるために、
1. ジェネリクスサポート前の引数型がCollectionのため、中身に複数クラスのデータが入る可能性があり、<? extends T>としてそのようなデータも受け入れられるようにしています。
2. ジェネリクスサポート前の返り値型がObject型のため、上限境界を引き上げるためにextends Objectを加えています。

このように古い実装を新しい実装に書き換える際に、非変な型パラメータを用いたジェネリクスだけでは対応できない部分があるため、ワイルドカード機能が実装されています。

知らないと混乱するJavaの変性

変性とはすなわち、共変性・反変性・非変性(不変性)のことで、

共変 (covariant): 広い型(例:double)から狭い型(例:float)へ変換すること。
反変 (contravariant) : 狭い型(例:float)から広い型(例:double)へ変換すること。
不変 (invariant): 型を変換できないこと。

wikipedia:共変性と反変性_(計算機科学)

狭いとは機能が狭いという意味で、JavaのクラスではObject型が一番狭く、そのサブクラスはObject型より機能が拡張されているため広い型ということになります。
Javaの変性について調べたので下記に簡単にまとめました。

//プリミティブ型
float flt;
double dbl = 1.0D;
flt = dbl; // NG // コンパイルエラー「Error: java: 不適合な型: 精度が失われる可能性があるdoubleからfloatへの変換」
dbl = flt; // OK //
// Javaのプリミティブ型は反変です。狭い型から広い型へ代入すると、なかった情報は勝手に補完されます。


//プリミティブ配列型
float[] fltArr;
double[] dblArr = new double[1];
fltArr = dblArr; // NG // コンパイルエラー「Error: java: 不適合な型: double[]をfloat[]に変換できません:」
dblArr = fltArr; // NG // コンパイルエラー「Error: java: 不適合な型: float[]をdouble[]に変換できません:」
//プリミティブ配列型は非変です。


//複合データ型
class A {}
class B extends A {}
A a;
B b = new B();
a = b; // OK //
b = a; // NG // コンパイルエラー「Error: java: 不適合な型: AをBに変換できません:」
// プリミティブ型とは逆に、複合データ型は共変です。広い型から狭い型に代入すると、溢れた情報は失われます。


//複合データ配列型
A[] aArr;
B[] bArr = new B[1];
aArr = bArr; // OK //
bArr = aArr; // NG // コンパイルエラー「Error: java: 不適合な型: org.example.A[]をorg.example.B[]に変換できません:」
// 複合データ型と同様に共変です。


//総称型
List<A> listA;
List<B> listB = new ArrayList<>();
listA = listB; // NG //  コンパイルエラー「Error: java: 不適合な型: java.util.List<B>をjava.util.List<A>に変換できません:」
listB = listA; // NG //  コンパイルエラー「Error: java: 不適合な型: java.util.List<A>をjava.util.List<B>に変換できません:」
// 総称型は非変です。共変性や反変性を適用したい場合はワイルドカードを使います。