Hatena::ブログ(Diary)

達人プログラマーを目指して このページをアンテナに追加 RSSフィード

2014-05-19

JJUG CCC 2014 Springに参加してきました

本日、日本Javaユーザーグループ(JJUG)主催のCCC 2014 SpringというJava勉強会に行ってきました。会場は、ベルサール西新宿で、都営大江戸線都庁前のA5出口を出て、新宿中央公園の5分くらい歩いたところにありました。今はスマートフォンで地図を確認しながら行けるので、初めての場所でも方向音痴の私でも電車の駅さえ間違わなければ大丈夫ですね。

CCCというのはCross Community Conferenceの略で、さまざまなコミュニティーの交流の場となる会議という趣旨でしょうか?このCCCというイベントは2012から開催されているようなのですが( CCC | 日本Javaユーザーグループ)、今回初めて参加させていただきました。残念ながら個人的な都合から、基調講演と午後の前半のセッションのみで後半と懇親会には参加できませんでしたが、参加したセッションについてまとめます。その他のセッションについては、以下に情報があります。

K-1 詳説 Java SE 8 – CCC Edition(櫻庭 祐一氏)

最初のセッションは、最先端Java技術を研究し、最近Java8紹介記事も多く執筆されている櫻庭さん(@skrb)より、Java SE8の新機能のポイントについて紹介していただきました。Java SE8というと、まず、Lambdaプロジェクトの成果であるラムダ式ストリームAPIを使った新しい関数型のJavaプログラミングが注目されますが、それ以外にも、

など、その他の新機能についても解説されました。途中、

「for文やforEachを使ったら負け。ラムダ式禁止と戦おう」

というコメントが印象的でした。現状は後の岩崎氏の講演で説明があったように、アプリケーションサーバーなどの対応が進んでいないという制約から、Java8が実際の案件で普及するまでにはにはもうしばらく時間がかかるだろうとは思いますが、今のうちに新しい機能について先行して学習しておき、実際使えるチャンスがあったら、便利な新機能を積極的に導入できるよう準備しておきたいですね。

なお、この基調講演の元ネタは、「現在、ITpro において連載している詳説 Java SE 8 を CCC で再現します。」とのことですので、

詳解 Java SE 8 第1回 Java SE 8概説

からはじまる、素晴らしい詳細な連載記事も是非ご覧ください。

K-2 Java 8 ラムダ式と Stream の概要について(Stuart Marks氏)

基調講演の2番目は、オラクルのStuart Marks氏より、ラムダ式ストリームAPIについて、基本的なところから解説していただきました。いきなり、新しい関数型の記述方法で説明されると、慣れていない場合混乱する場合もあると思いますが、以前の書き方と対応させながら丁寧に解説していただき、理解しやすかったです。

今回の講義で特に印象的だったのは、関数型や不変性という話を前提にせず、あくまでも従来からある無名内部クラスの代替として型安全にコードブロックを渡すための便利な記法、内部イテレーターの記述手段としてラムダ式を説明していたことです。例として

List<Person> list = ... ;

// Java7 
for (Iterator<Person> iter = list.iterator(); iter.hasNext();) {
    Person p = iter.next();
    if("Jones".equals(p.getName())) {
        iter.remove();
    }
}

// Java8
list.removeIf(p -> "Jones".equals(p.getName()));

のように、Collectionインターフェースに追加されている、removeIfを使って、従来のイテレーターの処理を置き換えるコードが出てきました。さらに、従来の外部イテレータとfor文を使って処理した場合と比較して、同期化が必要なコレクションの場合に、きちんと同期化をしてくれるというようなところも便利ですね。*2

先の櫻庭さんの発言にもありますし、今まで私自身ラムダ式を使う以上、なるべく関数型の発想で考えるべきであり、可変な(mutable)な操作は避けるべきと考えていました。しかしながら、もともと、Javaは可変な変数を当たり前に使う手続き型(imperative)なコードを前提としており、不変性などは後から追加されたというところがありますし、このようにラムダ式は可変な操作にも使えるのだというのはかえって新鮮な発見でした。特に、古いコードをラムダ式を使って簡潔にリファクタリングするという場合などには、あえて不変性にこだわらないというのも一つの考え方かもしれません。

Stuart氏の話で、もう一つ印象的だったのは大量の基本型の数値を使った計算に、ストリームAPI適用するという以下の例*3です。

System.out.println(
    LongStream.range(0, 1_000_000L)
        .parallel()
        .map(i -> ((i & 1) == 0 ? 1 : -1 * (2 * i + 1))
        .mapToDouble(i -> 4.0 / i)
        .sum());

従来の常識としては、大量の数値をList<Long>などのコレクションを使って計算すると、基本型からオブジェクへの変換(boxing)が行われて性能で問題が出るので、代わりにlong[]のように基本型の配列を使わなくてはならないということがありました。しかし、これはコレクションなど便利なAPIが使えずC言語のようなコードを書かないといけないということを意味し、パラレル化などの最適化には相当の努力が必要でした。ストリームAPIでは、LongStreamやmapToDoubleといった基本型専用のAPIをきちんと使いさえすれば、遅延評価がきちんと行われ、実際に大量の値のboxingが発生せず、オーバーヘッドを最小限に抑えることが可能なようです。確かに、Javaでは基本型の扱いはユーザーにとって完全に透過というわけにはいかず注意が必要なのですが、こうした数値計算にも関数型を活用できるというのは面白いですね。

R1-1 Java 8 for Java EE 7/6(岩崎 浩文氏)

金融、製造、公共など様々なドメインJavaEEを使った多くのシステム開発を担当してこられ、雑誌等への執筆もされている楽天の岩崎氏(@HirofumiIwasaki)より、アプリケーションサーバーのJava8対応の状況についてお話いただきました。

JavaSE 8が正式にリリースされたということで、早速現在のプロジェクトで活用しようという気になってしまいますが、実際のところは、JavaEE 7ではJavaSE 8はサポート対象外であり、さらに、そもそもJavaEE 7に対応したアプリケーションサーバーGlassfishWildFlyといったオープンソースサーバーに限られ、その他の商用のサーバーではいまだにJavaEE6しかサポートされていないという事実があります。ただし、講義中で紹介されていたようにGlassfishのdaily build版ではJava8が使えるということですし、WildFlyなど他のサーバーについても比較的早く対応されるのではないかという期待がありますね。

また、パラレルコレクションなどは通常のEJBのようなオンライントランザクション系のシステムとは相性が悪いと思います。しかし、EJBJavaEEのすべてではないですし、JavaSE 8が利用できるようになれば、日付APIやラムダを使ったロジックの簡潔化など、いろいろなメリットがあるものと期待されます。

なお、日本語の情報が少ないという問題はありますが、JavaEEについては以下のNetBeansチュートリアルにあるサンプルに取り組むのがよいとのことです。

https://netbeans.org/kb/trails/java-ee.html

H-2 Javaトラブルに備えよう(上妻 宜人氏)

上妻氏は見習いプログラミング日記Java EEを中心とした情報の発信をされていますが、単にプログラミングを中心とした開発手法のみならず、運用時に必要なモニタリングツールの知識など有用な情報を書かれています。どうしても、新しいAPIや言語の機能、開発ツールなどに興味がいきがちですが、実際には運用時のトラブルシューティングをするためのログ解析手法などは非常に大切です。また、必要なツールを正しく使いこなせば、トラブルシューティングが容易であるというのもJavaの重要な特徴の一つなのではないかと思います。つまり、Javaは単にプログラミング言語というだけではく、デプロイメント・運用時の環境まで含めたプラットフォームなのであるということですね。

講義の中では、トラブルシューティングのために適切な情報の取得が大切であるとして、まず、以下の情報出力の方法について説明されました。

さらに、OutOfMemoryErrorが発生するさまざまなケースを突出型、じわじわ型などパターンごとに解析する手法について解説されました。基本的な手法は昔から大きく変わっていないとはいえ、具体的なコマンドの使い方はバージョンによって異なっているため、上妻氏の最近の経験に基づいた情報は非常に有用なものであると思います。いざ、トラブルが発生すると焦ってしまい、冷静な判断ができないということもあると思いますので、Javaエンジニアとしては日頃からこうした解析の手法に慣れておくことは大切だと思いました。*4

H-3 初めての Java EE 開発から学んだこと(菊田 洋一氏)

株式会社 構造計画研究所の菊田氏(@kikutaro_)より、ご自身が初めてJava EEの開発に取り組まれたプロジェクトの経験をもとにお話されました。菊田氏は以下のブログでもJava EEの情報を発信しておられます。Challenge Java EE !

恥ずかしながら、講演の中で私のブログもちょっと紹介していただいたのですが、3年近く前(もうそんなに経つんですね)にちょっとJavaEE 6について調べてこのブログでも紹介したことがあった(Java EE6標準の範囲でフルスタックのWebアプリケーションが簡単に作成できることを確かめてみました。 - 達人プログラマーを目指して)ので、昔のことを思い出してちょっと懐かしくなりました。

残念ながら、私自身はその後JavaEEから離れてしまい、新しいEEのフレームワークを実際の案件で使うことはできなかったのですが、菊田氏の発表はJavaEEを実際に適用した成果に基づくものとして、大変興味深く聴かせていただきました。

発表にもあるように、もともと.NET系の開発からJavaEEに移行されて、まだほんの一年半という短い期間にも関わらず、データ層からプレゼン層まで「フルスタック」のアーキテクチャを構築されたということは大変感銘を受けました。これはもちろん菊田氏の努力の賜物だと思いますが、JavaEEが標準のフレームワークを組み合わせるだけで実用的に使える、.NET並みに簡単に使えるようになったということの証明となる事例なのではないかと思います。

といったことは、私も以前から感じていたことで、多くの点で大変共感しました。菊田さんご本人の以下のエントリーもどうぞ

#JJUG_CCC Spring 2014で「初めての Java EE 開発から学んだこと」というタイトルで発表させて頂きました! - Challenge Java EE !

*1:時間の関係で残念ながらほとんどスキップされましたが、櫻庭さんはJavaFXを推進されており、日頃プレゼン資料自体もJavaFXで作成しているとのことです。http://www.slideshare.net/skrb/20140321-javafx8も参考になります。

*2:Coolectionsのようなユーティリティクラスと違い、インターフェースに定義されたデフォルトメソッドはオーバーライド可能なので、サブクラスごとに独自の実装が持てる。

*3円周率を計算するテイラー級数の計算

*4:DevOpsという考え方があるように、最近はプログラマーも開発だけでなく、サーバーの運用やトラブルシュートをすることが多いと思われます。

2011-12-17

Javaのクラスとオブジェクトについて再度解説を試みる

オブジェクト指向プログラミングの考え方については、今までこのブログでも何度か取り上げてきました。

[オブジェクト指向] - 達人プログラマーを目指して

オブジェクト指向プログラミングプログラミング技法のすべてではないとはいえ、Javaのようなオブジェクト指向言語で本格的なプログラムを作るには理解を避けて通ることができませんし、また、関数型言語など他のパラダイムの言語を利用するにしても、オブジェクト指向の考え方をまったく理解しないまま使いこなすということは困難でしょう。オブジェクト指向の考え方はデータ構造やアルゴリズムといったことと同様に、プロフェッショナルプログラマーが理解しておくべき基本的な素養といってもよいと思います。実際、海外では募集要項でオブジェクト指向の理解を前提とすると書かれていることが普通ですし、プログラマーの面接試験で、アルゴリズムと並んでオブジェクト指向プログラミングの基本についての正しい理解を問うケースが多いようです。

しかしながら、特殊なケースを除いて、我が国ではいまだになかなか普及していないようですね。数週間の新人研修やOJTのみで短期間に理解できるほど簡単ではないというのは事実かもしれません。それゆえ、SIの開発案件ではJavaVisualBasicといったオブジェクト指向言語を用いながらも、実際にはオブジェクト指向を利用しないで開発できるような規約を作るといったこともかなり一般的に行われているようです。

オブジェクト指向プログラミングの普及を妨げる原因の一つとして、わかりやすい入門向けの解説を作ることが難しく、また、実際にそのような解説が書かれている入門書も非常に少ないということがあると思います。実際に、Javaの入門書の場合、ずっとmainメソッドのみでifやforなどの構文を説明した後、いきなりVehicle(乗り物)クラスやCarクラスなどの説明に飛躍するといったケースも多く、初めてオブジェクト指向プログラミングを学ぶ人にとっては非常に理解しにくいところがあると思います。*1

さらに、一言でオブジェクト指向、(あるいはもっと範囲を狭めてオブジェクト指向プログラミング)と言っても、プログラミング言語によっていろいろな考え方やアプローチがあり、さまざまな人々がいろいろな解説を試みているので、学習する側の立場としてはどの考え方が正しいのか混乱に拍車がかかるというところがあるかもしれません。

私自身は「オブジェクト指向とはどう考えるべきか」という哲学的な研究をしているわけではなく、単に仕事をするうえで便利な考え方の枠組みの一つとして考えています。要するに、

  • 共通のロジックを流用しやすくする(再利用)
  • 大規模なプログラムを分割して扱い易くする(モジュール化)
  • 変化や拡張に強いプログラムにする(拡張性)
  • 設計のアイデアを共有する(パターン)
  • ビジネスルールをプログラムとしてわかりやすい形で表現する(モデル化)

といった目的を達成するための手段一つとして、欠かせないものとして考えています。

ここでは、Java言語のオブジェクト指向プログラミングの基礎となるクラスとオブジェクトについて、再度、自分なりの解説を試みてみることにしたいと思います。

staticおじさんの世界におけるクラスの役割

一昔前のJavaの解説では、「Javaは純粋なオブジェクト指向言語である」という説明のされ方をされることが多くありました。ご存じのように、Javaではどんなに簡単なプログラムでもクラスを作成することが必須です。実際、HelloWorldは以下のようにクラス内に定義する必要があります。

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

最初から、クラスというオブジェクト指向の概念を使う必要があるから、純粋なオブジェクト指向言語なのであるということです。これは、「純粋なオブジェクト指向」という言葉の定義にもよりますが、一般にはちょっと不適切で勘違いしやすい用法だと思います。確かに、このプログラムに登場する文字列配列オブジェクトですが、ユーザー定義のオブジェクトは利用していません。ただし、HelloWorldというユーザー定義のクラスは使っているので、クラス指向といった方がよいかもしれません。

さて、このHelloWorldプログラムと同様の形式を拡張して、すべてのクラスのメンバー(フィールド、メソッド)にstaticキーワードをつけることで、完全にstaticな世界で*2プログラムを作成することができます。staticフィールドやstaticメソッドは、Smalltalkなどの純粋なオブジェクト指向言語の用語をまねてクラス変数、クラスメソッドなどと呼ばれることもありますが、本来staticというのは静的、すなわち、動的(dynamic)と反対の意味を持つ用語です。Java言語において、staticキーワードの持つ意味はあまり理解されていないようですが、staticフィールドについては、一旦クラスローダーによってクラス定義がJVMに読み込まれたら、クラスごとに固定的に変数領域が割り当てられてずっと存在しているということを示しています。また、staticメソッドはそのようなフィールドを操作するためのメソッドとして、やはりクラスごとに定義が読み込まれます。*3

この点を理解するためには、以下のごく簡単な例を考えてみてください。

public class ClassA {
    public static int field;

    public static void method() {
        System.out.pirntln("ClassAのメソッド");
    };
}

public class ClassB {
    public static int field;

    public static void method() {
        System.out.pirntln("ClassBのメソッド");
    };
}

public void MainClass {
    public static void main(String[] args) {
        ClassA.field = 100;
        ClassB.field = 200;

        System.out.println(ClassA.field); // 100
        System.out.println(ClassB.field); // 200

        ClassA.method(); // ClassAのメソッド
        ClassB.method(); // ClassBのメソッド
    }
}

ClassA、ClassBのそれぞれに独立した変数の定義、メソッドの定義が存在し、クラスが読み込まれるとともに静的にメモリーに読み込まれます。このことは、フィールド名やメソッド名が以上の例のように同じであっても影響を受けません。特に、この例で、int型のstaticフィールドに対応するメモリー領域は、クラスがロードされると共にクラスごとに用意されることに注意してください。

このように、staticな世界におけるクラスの役割とは、単にたくさんあるフィールドやメソッドを種類ごとに分類して、適切な部品の単位に分割するというモジュール化の役割があるに過ぎません。つまり、クラスは単にstaticフィールドやstaticメソッドの入れ物として機能しています。また、Javaの場合、基本的には一つのクラスを一つのソースファイルに記述しますから、クラスというのは物理的なソースコードの分割単位としての役割もあります。英語のclassifyに分類分けするという意味があるように、クラスには分類されたものという意味もありますから、大規模なプログラムで必要な変数メソッドの定義を適切な単位に分割して扱うというのは、オブジェクト指向以前にクラスの持つ重要な役割であることがわかります。

抽象データ型の扱えることの便利さを理解することがstaticな世界を卒業する最初の一歩*4

このように、staticおじさんの世界でも適切にクラスを定義して、フィールドやメソッドをしかるべきクラスに定義することで、大規模なプログラムを適切な単位に分割して管理することができるようになります。しかしながら、この場合の問題は、プログラムが扱える変数パラメーターの型が基本型、文字列型、またその配列といった非常に限定された型しか扱えないということです。FORTRANCOBOLなどの昔の言語しかしらないstaticおじさん*5の問題点はプログラム言語が扱えるのはこのような限定された型のデータのみであると決めつけてしまっているところにあると思います。

ところが、実際の複雑なプログラムでは、

  • 注文をデータベースに登録する
  • 入力フォームを開く
  • カタログ一覧を検索する

といったように、プログラムで処理したい単位はintやfloatなどといった基本的な型のデータなのではなく、「注文」「入力フォーム」「カタログ項目」といった人間にとってもっと自然な単位でデータを扱いたいということがあります。JVMの仕組みを考えてみればわかるように、本当はコンピューターが扱いやすいデータというのは有限のビットの集まりとして表現できる数値や文字といったデータなのですが、そういう詳細のことは無視して、データをより抽象化して扱えるようになれば、プログラムを実際のユースケース(仕様)に近い形で記述でき、よりわかりやすく記述できるようになります。実際にJavaの場合は、intやfloatといった基本型の値だけでなく、文字列、日付、注文書といったさまざまなオブジェクトを利用することで目的に応じてあらゆるデータを変数から参照したり、メソッドパラメータ戻り値としてやり取りできるようになります。

もちろん、オブジェクトを真に使いこなせるようになるには、カプセル化ポリモーフィズムといったさまざまな事柄を理解する必要があるのですが、いきなりそのような概念を理解しようとせず、まずはこのようなさまざまなデータを扱えることの便利さを理解することが、staticな世界を卒業する最初の一歩として重要なのではないでしょうか。

一般のJavaオブジェクトを理解する前に、まず配列オブジェクトの性質を理解するとよい

ところで、一般のオブジェクトはクラスを使って自分で定義し、newを使ってメモリーに割り当てて使うわけですが、最初に勉強する際に、オブジェクトの性質と同時にクラスの定義方法を学習するのは、一気にいろいろな概念を覚えなくてはならずなかなか難しいかもしれません。そこでお勧めなのは、最初に配列の性質をきちんと理解するという学習手順です。配列はクラスで定義する必要がないため特殊ですが、Java配列

  • new演算子を使ってオブジェクトJVMのヒープと呼ばれるメモリー領域に動的に生成される。
  • 配列変数は参照型の変数である。
  • 多数の値をまとめて扱う抽象データ型の一種である。

という性質を考えると、一般のオブジェクトと共通の性質を備えていることがわかります。

一般的な入門書の説明では、配列は複数の値を読み書きする箱が並んだ絵が説明に使われることが多くあります。一個の変数が値を格納する箱なので、これが連続して横に並んだものが配列というわけです。これは、昔のBASICやC、COBOLなど多くの言語の配列の説明には最適ですし、この説明自身は間違っていないのですが、これだけではJava配列が持つ以上の重要な性質を見逃してしまうことになります。


int[] x = new int[3];
int[] y = x;

x[0] = 1;
x[1] = 2;
x[2] = 3;

System.out.println(y[0]); // 1
System.out.println(y[1]); // 2
System.out.println(y[2]); // 3

以上のコードは理解している人にとっては当然のことですが、配列オブジェクトは一個しか生成されておらず、xとyという二つの別々の変数によって同一オブジェクトが参照される状態となっているため、変数xの要素を変更すると、連動してyの参照する値も変わって見えるということです。これは、独立した値の箱として扱われる普通のint型変数の振る舞いと大きく違っています。


int x = 1;
int y = x;

x = 2;

System.out.println(y); // 1のまま

もちろん、参照型の変数オブジェクトを参照するためのものなので、nullという特別な値が代入されることで何もオブジェクトを指していない状態にもなれます。この状態の変数にアクセスすると、有名なNullPointerExceptionが発生することになります。

なお、C言語そっくりにするため、Java配列を宣言する際には中括弧の初期化子を使って宣言することも可能です。

int[] x = {1, 2, 3};

あるいは、Javaでは推奨されませんが、以下のように書くこともできて、これだと本当にC言語のように見えます。

int x[] = {1, 2, 3};

ただし、似ているようにみえるのは見かけだけで、C言語配列とはメモリーの割り当て方が全く異なることに気を付けてください。このように、初期化子を使った場合でも、実際にはnewを使って動的に生成した場合と同様に配列オブジェクトは常に実行時に動的に生成されてメモリーに確保されます。*6

独自のメソッドを定義できないなど制約もありますが、配列は実際にオブジェクトの一種です。まずは、この配列の意味や配列変数の使い方に習熟することで、動的にオブジェクトを生成して使うというstaticでないプログラミングの世界への第一歩を踏み出すことができます。

ユーザー定義のオブジェクトを定義して生成する手段としてのクラス

以上で説明した、動的な配列オブジェクトの生成と配列変数によるオブジェクトの参照ということが理解できたら、一般のオブジェクトを理解することも難しくありません。配列との違いは、以下の点です。

配列の場合は、同一の型の値の集合しか表現できませんが、クラスを使えば、任意の値の組み合わせからなる構造のオブジェクトを生成できるようになります。

public class Person {
    public int age;
    public String name;
}

Person personX = new Person();
Person personY = personX; // 二つの変数が同じオブジェクトを参照するようになる

personX.age = 20;
personX.name = "test";

System.out.println(personY.age); // 20
System.out.println(personY.name); // test

フィールドを自由に定義できることと、要素にアクセスする際の演算子がピリオドである点を除けば、配列オブジェクトとまったく同様の性質を持っていることがわかります。要するに、Java言語においてオブジェクトとは、ヒープに割り当てられたメモリ領域で、オブジェクト生成元のクラスを型として持つ変数によって参照されるものということになります。まずは、データの塊がヒープに生成されて、変数を使ってそれを参照して使うということをイメージできるようになることが何よりも大切です。つまり、Javaオブジェクト指向プログラミングを理解する前提としては、

という性質をしっかりと理解することが重要だと思います。クラスがロードされたタイミングで最初から用意されるstaticなメモリ領域とは異なり、newされるたびに新しいオブジェクトが動的にヒープに割り当てられていきます。

ところで、どうしてJavaオブジェクトには、このような性質があるのでしょうか。これも、配列の性質を考えてみれば納得ができると思います。配列は一般にはたくさんの要素を含むので、あらかじめ静的に変数の領域を確保したり、メソッドが呼び出されるつどメモリーを毎回スタックに割り当てるのはあまり効率的とは言えません。実行時に必要なサイズで動的にメモリーを割り当てて、必要がなくなるまで(ガーベッジコレクションされる)ヒープに確保して使うというのは、一般的なプログラムでは効率的と考えられます。さらに、変数への代入時やパラメーターの受け渡し時にすべての値のコピーを行うよりも、単に既にメモリー上にあるオブジェクト変数が参照するようにした方が、プログラムの実行性能からも有利です。他の言語では、さまざまな方法でオブジェクトを割り当てることができるものがありますが、Javaの場合は常に動的にヒープに割り当てて、変数から参照するというという決まりになっています。常に、最適というわけではないかもしれませんが、JVMガーベッジコレクションの仕組みと合わせて、この割り切った仕様により、Javaでは効率的にオブジェクトを扱えるようになっていると考えることができます。

オブジェクト指向プログラミングは、オブジェクトを定義するクラスとモジュールとしてのクラスを同一視することから始まる

前節で説明したクラスは、他の言語では構造体、レコード、あるいはユーザ定義型と呼ばれるものに過ぎず、複数の値から構成されるデータ構造を定義していました。

しかし、ここで本当に理解すべき重要なことは、Javaではオブジェクトの構造を定義して、そこから生成されたオブジェクトを参照する変数の型となるためのクラスと、「staticおじさんの世界におけるクラスの役割」の節で説明した、大きなプログラムを分割するモジュールとしての役割を持つクラスを同一視しているということにあります。

まず、Javaにおいて、この事実が当たり前と思えるようになれば、相当オブジェクト指向で考えられるようになっていると言えます。これは、Javaで適切な単位でクラスを設計し、オブジェクト指向プログラミングできるようになるための大前提であり、オブ脳の基本回路が脳に形成されたといっていいでしょう。

この同一視により、カプセル化という考え方も自然に理解できると思います。つまり、モジュールとして何を一緒に含めるのが自然であるかということを考えれば、オブジェクトに含まれるデータに関連する処理をメソッドとして一緒にクラスに定義し、フィールド自体はprivateにして外部から勝手に操作されないようにする考え方も納得がいくのではないでしょうか。String型のオブジェクトに含まれる文字列を加工するためのメソッドはStringクラスに定義されているので、文字列オブジェクトに対してそのまま呼び出すことができます。一般的にはこのように関連するデータと処理を同じクラス内に定義することが基本となります。

どうすれば、オブ脳が鍛えられるのでしょうか。こればかりは、JDKオープンソースのライブラリーをお手本として、また自分なりに似たような設計を試しながらプログラミングの経験を積むことに尽きると思います。

まとめ

ここでは、一般の入門書にあるような説明とは違ったアプローチで、Javaのクラスとオブジェクトについて理解するための説明を試みてみました。

実際には、最後の項目はかなり内容を端折っているので、クラスを有効なモジュールとして設計するためには、カプセル化の他に継承ポリモーフィズムなどさまざまなことを理解する必要があります。

いまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味について - 達人プログラマーを目指して

さらに、実際の問題を上手に分割してクラスに割り当てるということができるためには、やはり、それなりの経験を積んで勘を養う必要があります。*7そして、クラスをオブジェクトと結びつけて自然な単位として扱うのがなぜよいのかといったことを納得するには時間がかかるでしょう。しかし、プログラミング言語としてのオブジェクトやクラスの意味や存在意義はここに書いたように非常に単純なものに過ぎないということもできます。

もちろん、オブジェクト指向プログラミング言語で実現する方法はさまざまですし、Rubyなど、より純粋な言語を使って学ぶべきであるという意見もあるかもしれません。しかし、大部分のJavaプログラマーにとっては、モジュール配列、構造体といった言語の仕組みの自然な拡張として、クラスやオブジェクトを理解するというパスがわかりやすいのではないかと思いますが、いかがでしょうか。乗り物や動物を使った説明や、逆に専門的すぎて難しい解説を読んでよく理解できなかったという人は、まずは、ここに書いたようにプログラミング上の当たり前の考え方との比較から徐々に理解するのがよいのではないかと思います。そして、この基本が理解できたら、お手本となるソースはいくらでも転がっているのですから、オブジェクト指向の良いクラスライブラリーを読んだり、拡張したりすることに是非挑戦してみてください。

*1:ただし、オブジェクト指向プログラミングを動物や乗り物で解説するのは和書だけでなく、世界的にも一般的な傾向のようです。

*2:ただし、厳密には文字列配列オブジェクトは除く

*3:勘違いしやすいところですが、メソッド定義はstaticでもそうでなくても静的にクラスごとに読み込まれます。ただし、staticでないメソッドは暗黙のthisオブジェクトを受け取ることで、動的なインスタンス変数にアクセスできる点が違います。

*4:抽象データ型や参照はオブジェクト指向言語に限らず重要だと思います。スクリプト言語Perlでも、バージョン5から参照が使えるようになって随分と高度なプログラミングが可能になっていますし、このあたりは本格的なプログラミングには欠かせないツールなのだと思います。

*5:本家のstaticおじさんはC#VisualBasic使いなので、適切でなかったかもしれません。ここではオブジェクト嫌いなおじさんという意味の一般用語として使っています。

*6:重要な違いとして、C言語配列サイズはコンパイル時に決定されますが、Java配列は実行時に決められます。

*7:経験を補うものとして、デザインパターンなどがあるわけですが。

2011-11-25

Javaの型パラメーターに対してstaticメソッドを呼び出した場合の挙動

以前にJava配列関連で調べたことがあったのですが、Javaの総称型は型消去によって直感的でない挙動をする場合があります。

Java言語のClassクラスが持つちょっと不思議な性質について - 達人プログラマーを目指して

Java5の型システムを理解するにはリフレクションAPIを使ってみるのが最短の近道になる - 達人プログラマーを目指して

特に、総称型の型パラメーターTについては以下はコンパイルできないという制約があります。

  • new T()
  • new T[配列サイズ]
  • catch (T ...
  • extends T
  • T.class
  • instanceof T

また、staticメソッドやstatic初期化ブロック内でクラスの型パラメータを使えないという制約もあります。

AngelikaLanger.com - Java Generics FAQs - Type Parameters - Angelika Langer Training/Consulting

このような制約は型Tが実際にはコンパイル時に消去されるということを考えれば納得のできるものです。これらの制約は初心者だけでなく、C++、C#、Haskellといった言語を使いこなす上級者であっても勘違いしやすいところですね。

それで、最近ちょっと話題になって気づいたのですが、実は型パラメーターTに対してstaticメンバのアクセスはどうなるのかという点が実は盲点で、自分もちょっと勘違いしていました。当然new Tが認められていないのだから、T.staticメンバという形式の呼び出しはコンパイルエラーなのだと考えていたのですが、実際に以下のコードを実行してみるとわかるように、Tは単に消去型として処理されるのですね。だから、型の上限が設定されていた場合はその上限の型のstaticメンバを呼び出すことができてしまうようです。

class Parent {
    public static String field = "Parent";
    
    public static void method() {
        System.out.println("Parent");
    }
}

class Child extends Parent {
    public static String field = "Child";

    public static void method() {
        System.out.println("Child");
    }
}

public class Tester<T extends Parent> {

    public void test() {
        T.method(); 
        System.out.println(T.field);
    }

    public static void main(String[] args) {
        Tester<Child> child = new Tester<>();
        child.test();
        // Parent
        // Parent
    }
}

実際にはTの消去型は上限のParentなのでTesterクラスは型消去されると以下と等価になります。


public class Tester {

    public void test() {
        Parent.method(); 
        System.out.println(Parent.field);
    }

    public static void main(String[] args) {
        Tester child = new Tester();
        child.test();
        // Parent
        // Parent
    }
}

そのように考えれば、このように実際にバインドされたChildでなく「Parent」が表示されてしまうのも納得がいきますが、かなり直感に反する気もします。Tester<Child>のようにChild型がTにバインドされているのだから、Tのstaticメンバーの呼び出しはChild側にバインドされるのが自然に思えます。

どうして、Javaではこのように型パラメーターに対するstaticメンバーの呼び出しを認めているのでしょうか。newの場合と同じ理屈ならコンパイルエラーにもできたはずなのですけれどね。

(追記)

ちなみに、staticでない場合を試してみました。

class Parent {
    public String field = "Parent";
    
    public void method() {
        System.out.println("Parent");
    }
}

class Child extends Parent {
    public String field = "Child";

    public void method() {
        System.out.println("Child");
    }
}

public class Tester<T extends Parent> {

    public void test(T instance) {
        instance.method();
        System.out.println(instance.field);
    }

    public static void main(String[] args) {
        Tester<Child> child = new Tester<>();
        child.test(new Child());
        // Child
        // Parent
    }
}

new T()はコンパイルできないため、外部でChildをインスタンス化して渡すようにしています。普通のインスタンスメソッドの場合は、

いまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味について - 達人プログラマーを目指して

で説明したとおり、ポリモーフィズムがあるので、意図したとおりChildの方のメソッドが呼び出されていることが確認できます。しかし、publicフィールドの場合は、staticメソッドと同様にポリモーフィズムがないためParentの側が表示されました。

ブクマのコメントにもありましたが、教訓としては、インスタンスメソッド以外サブクラスで「オーバーライド」するのは避けた方が良いということですね。

2011-07-11

JavaのFileクラスは不変(immutable)クラスという点に関する注意点

長年Javaを書いてきた人間としてはちょっと情けないことに、先日、会社で自分の書いたコードが原因でちょっとしたバグを出してしまいました。きちんとテストファーストで単体試験は書いていたのですがテストが不十分でしたね。

バグの原因は、Fileクラスの仕様をちょっと勘違いして使っていたことが原因でした。FileクラスにはrenameTo()というメソッドがあって、このメソッドの呼び出しにより、操作が成功すればもともとFileクラスのオブジェクトに対応していたファイルの名前がファイルシステム上で変更されます。ここで、うっかり、Fileクラスが可変なクラスだと勘違いしてしまっていたのですが、実は、Java Docにも明記されている通り、Fileクラスは不変(immutable)なクラスであり、一度生成したら状態が決して変更されることがない設計となっています。これは、以下のテストケースを見ると確認できます。

    @Test
    public void legacyAPI() throws IOException {
        File srcFile = new File("temp.txt");
        File targetFile = new File("temp2.txt");

        if (!srcFile.createNewFile()) {
            throw new IOException();
        }
        
        if (!srcFile.renameTo(targetFile)) {
            throw new IOException();
        };
        
        assertThat(srcFile.getName(), is("temp.txt")); //srcFileの状態はrename前のまま。
        assertThat(targetFile.getName(), is("temp2.txt"));
        
        if (!targetFile.delete()) {
            throw new IOException();
        }
    }

ただ、言い訳ではありませんが、FileクラスのrenameTo()というメソッド

boolean renameTo(File dest)

というシグネチャで定義されており、いかにも状態が変更されそうな雰囲気なため、Fileが不変ということを忘れていると、私のようにうっかりミスしてしまいそうです。実際、EclipseなどのIDEの入力補完に頼っていると間違いそうですね。おまけに、戻り値で成功か失敗かがbooleanで返ってくるのですが、どうしてIOExceptionを送出するようになっていないのでしょうか。以下のようになっていたら、間違いにくいと思うのですれどね。

File renameTo(File dest) throws IOException

このように例外を使わず、戻り値で成功失敗を判断させるというのはFileクラスの他のメソッドにも見られますが、必ず戻り値をチェックするようにする必要があります。このようになっている原因はC言語の雰囲気の影響を受けていることと、FileクラスがJDK1.0の時代からある相当古いクラスであるということもあるかもしれませんが注意が必要だと思います。

なお、Java7からはファイルシステムを扱う新しいAPIが導入されています。

Error Page 404

Java SE 7のjava.nio.file.Filesがとても便利な件 - きしだのはてな

Fileクラスの代わりにPathクラスを使って特定のディレクトリやファイルが表現され、Filesクラスを使って実際のファイルの操作ができます。

    @Test
    public void java7API() throws IOException {
        Path srcPath = FileSystems.getDefault().getPath("temp.txt");
        Path targetPath = FileSystems.getDefault().getPath("temp2.txt");

        assertThat(srcPath.toString(), is("temp.txt"));
        
        Path createdPath = Files.createFile(srcPath);
        
        assertThat(srcPath, is(createdPath));
        Files.move(srcPath, targetPath);

        Files.delete(targetPath);
    }

Fileクラスは依然としてDeprecatedなわけではありませんが、Java7からはなるべくこちらのAPIを使うようにすべきでしょう。また、Java6までの環境では必要に応じてcommons-ioなどのライブラリーを使うとよいと思います。

なお、この問題は話題のGroovyを使うときにも同様に出くわしますので注意が必要です。

2011-06-23

いまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味について

正しく意味を理解している方にとっては、まったく常識レベルの話であり、何をいまさらと思われる方々も多いかと思いますが、大規模案件のレガシーコードなど、私が仕事で見かけるJavaのコードを読むと、「このコードを書いたSEやPGの方々は、はたして継承の意味を正しく理解していないのではないか」と思われる設計のコードに出会うことが少なからずあります。現在では改良されましたが(Javaプログラミング能力認定試験の問題がかなり改善されていました - 達人プログラマーを目指して)、以前のJavaプログラム認定試験の問題は、そうした不適切な設計がされている典型的な例となっていたのですが、実際、SI業界ではあのような品質のコードのシステムが今でも現役で多数稼動しているというだけでなく、現在でも新たに生み出されているというのは残念ながら紛れもない事実のようなのです。

確かに新人研修で「哺乳類を継承して犬クラスと猫クラスができる」といったようなオブジェクト指向の説明を聞いただけで、簡単に理解できるものではありませんが、以降手続き型のレガシーコードしか相手にしていないPGやコードを目にすることもなくExcel方眼紙ばかり描いて(書いて)いるSEの方々は、継承の意味などをきちんと理解する前に忘れてしまっているという方も多いかもしれません。私の経験上、この業界ではこうした基本的な知識が理解されていないという現場が、むしろ、典型的なケースなのではないかという疑いすらあると思えてくるのです。(あの認定試験問題の品質をSI業界の代表的なプログラム品質と考えることの是非 - 達人プログラマーを目指して

オブジェクト指向プログラミングの方法を理解して使いこなせるようになるには、通常はそれなりの努力と期間を要するものですし、きちんと指導してもらえる先輩に巡り合うということも大切ですが、ここでは、「継承」の意味に絞って、最低限知っておいてほしいポイントについて、今更ですがまとめてみたいと思います。あらかじめお断りしておくと、これを読んですぐに理解できるという保証もできないのですが、あと一歩のところで正しい理解に到達できない人にとってヒントになるところもあるかもしれません。そして、もちろん、継承オブジェクト指向プログラミングのほんの一部分の要素でしかありませんが、その意味を理解することで、次のステップに進みやすくなるとも考えられるのです。(ここではJava言語を例として説明していますが、C#やVisual Basicなど、業務で利用する主流のオブジェクト指向プログラミング言語でも基本的なポイントはほぼ同じです。)

クラスの継承の文字通りの意味

Java言語ではクラスを継承するときにはextendsというキーワードを使ってクラスを宣言します。これは、文字通り「拡張する」という意味ですが、親クラスの定義を拡張して新しいクラスを定義するという働きがあります。以下のコード例をみてください。*1

class Parent {
    public String fieldA = "field A";
	
    public String methodA() {
        return "method A";
    }
}

class Child extends Parent {
    public String fieldB = "field B";

    public String methodB() {
	return "method B";
    }
}

この場合、以下のようにChildクラスのインスタンスを生成して、フィールドやメソッドにアクセスすると以下のようになります。

public class Inheritance {

    public static void main(String[] args) {
        Child child = new Child();
        System.out.println(child.fieldA); // field A
        System.out.println(child.fieldB); // field B
        System.out.println(child.methodA()); // method A
        System.out.println(child.methodB()); // method B
    }
}

もともとの「継承する」「拡張する」という意味の通り、ChildクラスはParentクラスのフィールドやメソッド継承元の親から引き継いで自分自身で定義しているような動作となっています。つまり、Childを以下のように定義した場合と同じ動作ということですね。

class Child {
    public String fieldA = "field A";
    public String fieldB = "field B";

    public String methodA() {
	return "method A";
    }

    public String methodB() {
	return "method B";
    }
}

継承を活用することで、このように共通のデータ(フィールド)や処理(メソッド)を親クラスにまとめて定義することができるのです。

子(サブ)クラスで同一の形のメソッドをオーバーライドする

「なんだ、かんたんじゃないか」これで、一人前に継承を理解できたと考える人もいるかもしれません。ですが、残念ながら話はここで終わらないのです。むしろ、実は、ここまでの話はJava言語の継承の働きの中でも本当に20%というか、継承の本当に威力のあるポイントを見逃していることになるのです。話のクライマックスはまだこれからなのです。

話が面白くなるのは、子クラスが親クラスと同じ形(シグネチャ)のメソッドを定義している場合です。以下の定義を見てください。

class Parent {
    public String fieldA = "field A";
	
    public String methodA() {
        return "method A";
    }
}

class Child extends Parent {
    public String fieldB = "field B";

    @Override
    public String methodA() { // 親クラスのメソッドをオーバーライドする。
        return "method A in Child";
    }

    public String methodB() {
	return "method B";
    }
}

ここでは、Childクラスにおいて親クラスで定義されているmethodA()と同じ形のメソッドを再度定義しなおしています。この形で、親クラスのメソッドを子クラスで再定義することをオーバーライドと呼んでいます。なお、Java5以降のバージョンでは、正しくオーバーライドしていることをコンパイラにチェックさせるために@Overrideというアノテーションを明示的につけることが推奨されています。(うっかりスペルミスをしたり、シグネチャが異なっていたりするとコンパイルエラーとなります。)

次に、これを実行してみましょう。

public class Inheritance {

    public static void main(String[] args) {
        Child child = new Child();
        System.out.println(child.fieldA); // field A
        System.out.println(child.fieldB); // field B
        System.out.println(child.methodA()); // method A in Child
        System.out.println(child.methodB()); // method B
    }
}

期待通り、child.methodA()の呼び出しはParentクラス中のメソッドでなく、Chlildクラス中のメソッド呼び出しに置き換えられています。これも、オーバーライドの機構を理解してしまえば、難しいところはないと思います。通常は親クラスのメソッド継承されてくるのに、子クラスでオーバーライドすると、継承元の親クラスの側のメソッドでなく子クラスのメソッドが呼び出されるということです。共通部分を親クラスに定義しておき、必要に応じて、差分があれば子クラスでオーバーライドできるということで、便利ですね。

継承には型の継承というもう一つの重要な側面がある

実は、多くのJavaプログラマーの理解がこの段階で止まってしまっているのではないかと思われるのですが、Java言語の継承には型の継承というもう一つの重要な側面があります。つまり、今までの説明では「継承とは親クラスのフィールドやメソッドを子クラスで再利用するための便利な方法」という意味しかなかったのですが、それに加えて、子クラスのオブジェクトは親クラスの型と代入互換性があるという性質があるのです。Java変数は型をつけて宣言する必要があったことを思い出してください。

int a = 3;
String b = "hello";
b = a; // コンパイルエラー

つまり、以上の例のように基本的には同じ型の変数にしか代入できないように、コンパイラがチェックしてくれます。一方、クラスに親子関係があると、親クラスの型の変数に子クラスの型のインスタンスを代入できるという重要な規則があります。

Child child = new Child();
Parent parent = child; // コンパイルOK

この規則は、冷静になって考えると自然なものであると納得ができます。今まで説明した継承のメカニズムによって、子クラスは親クラスのすべてのデータや振る舞いを保持しているのですから、場合によっては親クラスの型であると抽象化して考えても問題ないということですね。そして、以下の結果を見てください。

public class Inheritance {

    public static void main(String[] args) {
        Parent parent = new Child();
	System.out.println(parent.fieldA); // field A
	System.out.println(parent.methodA()); // method A in Child
    }
}

ここで、非常に大切なポイントはparent.methodA()の呼び出し結果がParentクラスのmethodA()でなく、ChildクラスのmethodA()にバインドされているという事実です。このポイントはレイトバインディングや仮想メソッド呼び出し*2などと説明されることがありますが、とにかく、宣言されている変数の型ではなく、実際に変数に代入されているインスタンスの型によって実行時の振る舞いが決まるということです。

つまり、継承によるメソッドのオーバーライドが、Java言語でポリモーフィズムを実現する手段となっているという事実を理解してください。これが、実はJava言語における継承の威力の中でも最も重要な働きをするポイントとなっているのです。

ポリモーフィズムについては、以前、ドラゴンボールで学ぶオブジェクト指向 改 - 達人プログラマーを目指してで、

共通のメソッド呼び出しで、対象とするオブジェクトの種類に応じてまったく異なるさまざまな処理を実行可能な性質をポリモーフィズム(多態性)と呼びます。

のように説明しています。なお、この説明だけだと、まだ何が嬉しいのかピンと来ないかもしれませんが、親クラスの型の変数に代入できるという性質は変数だけでなく、メソッドパラメーターにも同様に成り立ちます。だから、

public void someMethod(Parent parent) {
   // parentを使って何らかの処理
}

のようなメソッドを一度定義しておくと、Parentクラスの任意の子クラスをパラメーターとして渡して処理をさせることができるというわけです。つまり、このポリモーフィズムの性質を使うことで、拡張性や再利用性の高いライブラリーを作成できるのです。実際、たとえば証券のドメインであれば、ある銘柄の注文を表すOrderクラスを入力として受け取るメソッドを定義しておくと、株式や債券など様々な種類のOrderの子クラスを処理するように拡張させるといった設計が可能になるのです。

型の継承によるポリモーフィズム的な側面にのみ着目したのが抽象メソッドインターフェース

このようにJava継承の構文が持つ働きには

という二つの側面があるのです。特に、後者の概念はなかなか難しく、(私の説明のまずさもありますが)話を聞いただけですぐに理解できないかもしれません。しかし、コードを実際に写経*3するなどして、実際に試しながら、じっくりと理解することにしましょう。Javaプログラミングでは、この壁を越えらえるかどうかが非常に重要なポイントで、ここをいったんクリアできれば、デザインパターンフレームワークなど、オブジェクト指向プログラミングの広大な世界を冒険する準備ができたことになります。

さて、この型の継承ポリモーフィズムということに着目すると、もともとの親クラスのメソッドの実装は不要になる場合があります。その場合は、以下のように親クラスをabstractクラスにして、オーバーライドするメソッドをabstractメソッドとして宣言することができます。

abstract class Parent {
    public String fieldA = "field A";
	
    public abstract String methodA(); // 抽象メソッド
}

この場合も、前回と同様にParent型の変数のChildのインスタンスを代入して実行すると、正しくChildのメソッドが実行されるのです。

public class Inheritance {

    public static void main(String[] args) {
        Parent parent = new Child();
	System.out.println(parent.fieldA); // field A
	System.out.println(parent.methodA()); // method A in Child
    }
}

Parentクラスの抽象メソッドは型としてmethodA()が呼び出せるということをコンパイラに知らせている働きがある一方で、実際にメソッドの実装はポリモーフィズムにより、Childクラスのメソッドにバインドされています。

さらに、Javaの場合、継承による型の継承ポリモーフィズムという点を究極に推し進めたものとしてインターフェースが定義できます。上記のParentをインターフェースに置き換えると以下のようになります。

interface Parent {	
    String methodA();
}

class Child implements Parent {
    public String fieldB = "field B";
    
    @Override
    public String methodA() {
        return "method A in Child";
    }

    public String methodB() {
        return "method B";
    }
}

public class Inheritance {

    public static void main(String[] args) {
        Parent parent = new Child(); // Childのインスタンスをインターフェース型に代入
        System.out.println(parent.methodA()); // method A in Child
    }
}

(補足)Java言語ではフィールドはオーバーライドできない*4

本エントリに対して重要なコメントをいただきましたので、子クラスで親クラスと同一名のフィールドを定義した場合にどうなるかについて補足させていただきます。通常は、親クラスと同一名のフィールドを子クラスで定義すべきではなく、このセクションで書いた内容の理解は後回しでもよいと思いますが、うっかりバグの原因となることがあるので、注意が必要であるということは知っておく方がよいかもしれません。

実際、子クラスを以下のように定義して確認してみます。

class Parent {
    String fieldA = "field A";

    public String methodA() {
        return "method A";
    }
}

class Child extends Parent {
    String fieldA = "field A in Child"; //親クラスと同一名称のフィールドを定義
    String fieldB = "field B";

    @Override
    public String methodA() {
        return "method A in Child";
    }

    public String methodB() {
        return "method B";
    }
}

public class Inheritance {
    public static void main(String[] args) {
        Child child = new Child();
        Parent parent = child;

        // フィールドは宣言されている変数の型で決まる
        System.out.println(parent.fieldA); // field A
        System.out.println(child.fieldA); // field A in Child

        // Child型の変数で隠ぺいされているParentのフィールドを参照するにはキャストすることも可
        System.out.println(((Parent)child).fieldA); // field A

        // メソッドはオーバーライドされてポリモーフィックに呼び出される
        System.out.println(parent.methodA()); // method A in Child
        System.out.println(child.methodA()); // method A in Child
    }
}

結果は以上のようになりました。メソッドの場合はすでに説明したように子クラスの実装で親クラスの実装がオーバーライドされており、変数の型によらずに、実際に生成されているオブジェクトの型に応じて子クラスのメソッドが呼び出されるのでした。しかし、同一名のフィールドを定義した場合は変数の型によって、親クラスか子クラスのどちらかのフィールドが参照されています。これはどういうことかというと、フィールドは同一名称であっても決してオーバーライドできないということを意味しています。つまり、たまたま名前が同じ変数が親と子に別々に含まれているだけであって(異なる名前のフィールドの継承とおなじく)子クラスのインスタンスにはどちらの変数も含まれているということになります。ただし、同一名のフィールドをこのように宣言してしまうと変数の型によってどちらかのフィールドが参照できなくなってしまうのです。これは、一般にグローバル変数とローカル変数で同じ名前のものがあると、ローカル変数しか見えなくなってしまうといったことと似ています。それぞれの変数の領域は存在しているのですが、単に名前が隠ぺいされて参照できなくなっているということです。

(補足)staticメソッドもオーバーライドできず、ポリモーフィズムが存在しない

つぎに、staticメソッド継承について調べてみます。以下の例は、以前の例に対して、staticキーワードを各メソッドに追加しています。

class Parent {
    String fieldA = "field A";

    public static String methodA() {
        return "method A";
    }
}

class Child extends Parent {
    String fieldA = "field A in Child";
    String fieldB = "field B";

    public static String methodA() { // 親クラスと同一形のstaticメソッド。普通はやらない。
        return "method A in Child";
    }

    public static String methodB() {
        return "method B";
    }
}

public class Inheritance {
    public static void main(String[] args) {
        Child child = new Child();
        Parent parent = child;

        // staticメソッドはクラスごとに存在しており、変数の型で呼び出し対象が静的に決まる。
        // つまり、ポリモーフィズムが存在しない。
        System.out.println(parent.methodA()); // method A
        System.out.println(child.methodA()); // method A in Child

        System.out.println(((Parent)child).methodA()); // method A

        // 紛らわしいので、普通はインスタンスでなくて、クラスに対して呼び出す。
        System.out.println(Parent.methodA()); // method A
        System.out.println(Child.methodA()); // method A in Child
    }
}

staticでない普通のメソッドと違い、staticメソッドはクラスごとに別々の実装が独立して存在しており、子クラスで同一名のメソッドを定義してもオーバーライドできません。見かけ上インスタンス経由で呼び出した場合、インスタンスの型でなく宣言されている変数の型によって呼び出し先が決まります。前の節で説明したフィールドの場合と似たように解決されています。

このようにstaticメソッドにはポリモーフィズムが存在せず、コンパイル時に呼び出し先が一つに固定されます。staticメソッドの呼び出しにより、コンパイル時に実装が一つに固定されてしまうということです。(一方、普通のメソッドは、ポリモーフィズムにより、オーバーライドしている子クラスの数だけ無数に実装が存在している可能性があります。)staticメソッドが拡張性が低く、また、クラスを分離した単体試験が難しくなるということと関連しています。

(補足)コンストラクタ、初期化ブロック、static初期化ブロックについて

コメント欄にて川久保さんにご指摘いただきましたので、コンストラクタや初期化ブロックに関して簡単に補足させていただきます。初期化ブロックはともかく、コンストラクタに関する理解は重要だと思います。

コンストラクタは継承されない(しかし、子クラスのコンストラクタから呼び出される)
class Parent {
    public Parent() {
        System.out.println("Parent no args constructor");
    }
    
    public Parent(String arg) {
        System.out.println("Parent one arg constructor arg = " + arg);
    }
}

class Child extends Parent {
    public Child() {
//        super(); コメントをはずしても同じ
        System.out.println("Child no args constructor");
    }
}

public class Inheritance {
    public static void main(String[] args) {
        Parent parent = new Parent(); //Parent no args constructor
        Parent parent2 = new Parent("test"); //Parent one arg constractor arg = test
        
        Child child = new Child(); // Parent no args constructor, Child no args constructor

  // コメントをはずすとコンパイルが通らない。 
  // コンストラクタはメソッドのようには継承されない
  //      Child child2 = new Child("test");
    }
}

ご指摘のとおり、Java言語の場合にはコンストラクタは通常のメソッドのように自動的に子クラスに継承されません。したがって、以上の例では文字列パラメーターをとるコンストラクタは親クラスには存在しますが、子クラスには存在しません。*5ただし、子クラスのコンストラクタ中から親クラスのコンストラクタが呼び出されます。(呼び出しが省略された場合は親クラスのパラメーターのないコンストラクタの呼び出しが自動的に行われる仕様。)

インスタンス初期化ブロックはnew実行時に自動的に実行され、オーバーライドできない

存在自体知らない人も多いかもしれませんが、コンストラクタの代わりに初期化処理を行うための、インスタンス初期化ブロックという仕掛けがJDK1.1のころからあります。以下の例を実行してみると、各クラスのコンストラクタの実行前に呼び出されることがわかります。親クラスの初期化ブロックは自動的に呼び出されるため、子クラスでオーバーライドできません。

class Parent {

    // インスタンス初期化ブロック
    {
        System.out.println("Parent instance initilizer block");
    }

    public Parent() {
        System.out.println("Parent no args constructor");
    }
}

class Child extends Parent {
    // インスタンス初期化ブロック
    {
        System.out.println("Child instance initilizer block");
    }

    public Child() {
        System.out.println("Child no args constructor");
    }
}

public class Inheritance {
    public static void main(String[] args) {
        // Parent instance initilizer block
        // Parent no args constructor
        Parent parent = new Parent();

        System.out.println();

        // Parent instance initilizer block
        // Parent no args constructor
        // Child instance initilizer block
        // Child no args constructor
        Child child = new Child();
    }
}

この動作は、各フィールドの初期化子に似ています。

static初期化ブロックはクラスロード時に自動的に一度だけ実行され、オーバーライドできない

同様に、static初期化ブロックの例も示します。こちらはインスタンスの生成時ではなく、クラスのロード時に一回だけ実行される点が違います。いずれにしても、サブクラスでオーバーライドするということはできません。

class Parent {
    // static初期化ブロック
    static {
        System.out.println("Parent static initilizer block");
    }

    public Parent() {
        System.out.println("Parent no args constructor");
    }
}

class Child extends Parent {
    // static初期化ブロック
    static {
        System.out.println("Child static initilizer block");
    }

    public Child() {
        System.out.println("Child no args constructor");
    }
}

public class Inheritance {
    public static void main(String[] args) {
        // Parent static initilizer block
        // Parent no args constructor
        Parent parent = new Parent();

        // Parent no args constructor
        Parent parent2 = new Parent();

        System.out.println();

        // Child static initilizer block
        // Parent no args constructor
        // Child no args constructor
        Child child = new Child();

        // Parent no args constructor
        // Child no args constructor
        Child child2 = new Child();
    }
}

まとめ

通常、会社の研修や入門書ではオブジェクト指向の考え方と一緒に継承の説明を一気にされることが多いと思います。そのため、なかなか本質を理解しにくかったり、重要なポイントを見逃してしまうということもあると思います。ここでは、カプセル化や関連などオブジェクト指向に関する他の説明はいったん無視して、Java言語における継承の意味に絞って説明を試みてみました。

という二つの側面があるという点を理解することが大切です。特に、後者はなかなか分かりにくいところがあると思いますので、すぐに理解できなくても悲観することはありません。私も最初そうでしたが、ふとある時突然「そういうことだったのか」という瞬間が来るものです。

SI業界では、プログラミングの研修に十分な時間がとられないことも多く、こうしたごく基本的なことを理解しないまま、設計を行ったり、コードを書いたりするということも多いかもしれません。しかし、より高い生産性のプログラマーを目指すために、こうした基本的な知識を正しく理解しておくことは、プロフェッショナルプログラマーとして最低限の義務なのではないでしょうか。

なお、今回は最短の説明でJava言語の継承の働きを説明するため、オブジェクト指向の説明はあえて省略してしまいましたが、オブジェクト指向については、以下もご参照ください。

ドラゴンボールで学ぶオブジェクト指向 改 - 達人プログラマーを目指して

(追記)

本エントリでは、オブジェクト指向の概念モデルやクラスライブラリーとして意味のないParentやChildといったクラスを説明に使いました。もちろん、入門書などでは普通は図形や実世界の物などを使って継承関係を説明することが多いと思います。今回の説明のアプローチに対して、非難もあるかと思いますが、オブジェクト指向の意味やモデリングということを同時に理解しようとすると、考えることが多すぎて、逆に理解を妨げるということもあると思うのです。また、クラス設計というのは実際にモデル化すべき対象となる問題やプログラム(すなわちコンテキスト)を考えないと、あまり意味がないということもあります。

したがって、まず、初心者の方はオブジェクト指向の考え方はいったん後回しにして、プログラミング言語の機能としての継承の使い方を形から理解するというところから入るというアプローチもあると思います。その後で、JDKやOSSのクラスライブラリーやフレームワークの使い方を理解しながら、デザインパターンリファクタリングなどを学習し、その過程で自然にオブジェクト指向的なモデリングの考え方ができるようになれば、DDDなどの本を勉強して実際の問題を適切にクラスで設計できるようになるという流れもあると思います。

多くのオブジェクト指向の説明ではそういった段階を飛び越していきなり概念モデルに入ろうとするため、なかなか理解しにくいところがあるのではと思うのです。(自分のドラゴンボールの説明はそういうアプローチですが。)特に、インターフェースポリモーフィズムの威力を理解するには、まずデザインパターンの中で、StrategyパターンかCommandパターンあたりから勉強することをお勧めします。以下の本はデザインパターンについて、初心者にもわかりやすく書かれていると思います。

(追記)

ブクマで

Parent parent = new Child(); の変数名はchildを使いたいなあ。

というコメントをいただいています。確かに、ポリモーフィズムによって呼び出し時のメソッドはオーバーライドされていればChildクラスとして振る舞うので気持ちがわからなくないのですが、あくまでも呼び出し元はParentという型として認識しているという点もポイントかと思います。ポリモーフィズムを説明する時に私がよく使うたとえとして「筆記用具で文字を書く」という文があります。筆記用具は抽象親クラスであり、文字を書く人はあくまでも筆記用具を使っていると考えているのですが、実際に使っているのは筆記用具の子クラスのオブジェクトである鉛筆だったりボールペンだったりするということがあります。文字の太さや色は実際に使っているオブジェクトによって決まるのですが、そのオブジェクトを使う人は特に両者の違いを意識しなくても文字を書けます。変数名にparentを使っているのはそういう気持ちからすれば、一般的に自然だと思います。たとえ話も危険な場合がありますが、ポリモーフィズムは本来は日常我々が行っている抽象化プログラミングで実現する手段としてはすごく自然な考え方だと思います。

*1:ここではアクセス修飾子の問題はあえて考えなくて済むようにすべてpublicとしています。もちろん、通常、フィールドはprivateなどにしてカプセル化すべきです。

*2コンパイル時に呼び出し先のメソッドが解決できないため、実行時に判断する必要があるということ。

*3:単に話を聞いたり本を読んだりするだけでなく、エディタやIDEを使って実際に例題を書き写し、動作を確認しながら理解すること。

*4Java言語はC++など多くのオブジェクト指向言語と同様に、フィールドとメソッドを明確に区別しています。つまり、統一アクセスの原理が言語上はサポートされていません。よって、フィールドをpublicにせず、getterとsetterを定義するということが通常は行われます。C#やVBではプロパティというしくみがあり、また、Scalaではそもそもフィールドとメソッドの区別はよりあいまいです。

*5:この辺りはオブジェクト指向言語によって違いもあるところがあるので、注意が必要です。たとえば、Delphiなどではコンストラクタは継承されます。