java:オブジェクト指向を利用する本当の理由

オブジェクト指向を利用する理由として、プログラミングの入門書には
「オブジェクト指向を用いると、現実世界をプログラミングでそのまま表現できる」
という意のことが書いてあることが多いです。
その例として

  • 犬に「ワン」と鳴かせ、猫に「ニャーン」と鳴かせる
  • 乗用車に普通に走らせ、レーシングカーに速く走らせる

といった例が用いられることが多いです。

しかし、実務での使われ方を見ると
「現実世界をプログラミングでそのまま表現する」
というのは、目的ではなく手段と言うべきものです。
実務では
「ソースコードから重複を取り除き保守性を高めるため」
という目的で使用されます。

現実世界の関係性を機械の都合に置き換えると、ソースコードに重複が発生しやすくなります。
そこで、「継承」や「ポリモーフィズム」(同じメソッド名の指定でオブジェクト毎に異なる処理をさせること)といった特徴を持つオブジェクト指向を適用することで、現実世界の関係性をそのまま記述することができるようになり、重複の発生を防ぐことができるようになります。
(同じく、オブジェクト指向の特徴として挙げられる「カプセル化」も、オブジェクト指向のルールを守らせるという意味で強力な特徴です)

目的が保守性の向上であるため、現実世界では意識されにくい「思考ルーチン」「出力機能」といったものも、クラス化されることがあります。
また、保守性の向上に繋がらないのであれば、現実世界にあるものをクラスとして記述しないこともあります。

重複を取り除くことによる保守性向上、というのはイメージしにくい所だと思いますので、以下ではJavaのサンプルコードを用いて説明していきます。


今回のサンプルコードでは、「乗り物に乗って、タイミングを見計らって、乗り物を走らせる」という処理を実装していきます。
「乗り物に乗る」という処理と「乗り物を走らせる」という処理は別々のタイミングで実行する必要があるので、別々に実装する必要があります。
また、乗り物は複数種類があり、それぞれの種類毎で異なる処理を行う必要があるとします。

オブジェクト指向を使用しない場合は、下記に示すようなサンプルコードになります。

【オブジェクト指向を使用しない場合のサンプルコード】

・OOPTestMain.java

【サンプルコードの実行結果】

「乗り物に乗る」という処理と「乗り物を走らせる」という処理は、別々のメソッドとして実装しています。
乗り物の種類毎で処理を分ける必要があり、オブジェクト指向を使用しないサンプルコードではフラグ変数により分けています。

注目するべきは、各々のメソッドの中でフラグ変数を参照していることです。
各々のメソッドの中でフラグ変数を参照しているため、フラグ変数を参照する箇所はメソッドの数だけ存在することになります。
フラグ変数の参照方法が変更される場合、例えば「自転車」を示すフラグが2から3に変更になる場合、修正する箇所もメソッドの数だけ存在することになります。
(具体的な修正箇所はサンプルコード中にコメントで示した通りで、今回の場合は2か所です)
修正箇所が増えることで保守性が低下します。具体的には、修正漏れのようなミスが発生しやすくなります。


サンプルコードにオブジェクト指向を適用すると下記のようなコードになります。

【オブジェクト指向を使用する場合のサンプルコード】

・OOPTestMain.java

・OOPTestVehicle.java

・OOPTestCar.java

・OOPTestBicycle.java

注目するべきは、ポリモーフィズムを使用することで、フラグ変数参照に相当する記述を削減できていることです。
フラグ変数の参照に相当する処理としては、クラス名の参照がありますが、クラス名に変更が発生したとしても修正箇所は1カ所に留まるため、保守性が向上します。


今回の記事は、Javaプログラマーとして活躍し始めた後に意識するべき内容であり、入門レベルよりも少し高度なものになります。
しかし、意識するのとしないのとでは、プログラムをリリースした後の保守のしやすさが変わってきます。
指示されたものを作って終わりではない、お客様からの信頼を得られるシステムを主体的に作るためには、こうした設計思想も学ぶ必要があります。

設計思想を学ぶ上では、前提知識として、プログラミング言語の基礎的の文法の理解が必要になります。
先日弊社から発売した書籍「絶対にJavaプログラマーになりたい人へ」で基礎的な文法の理解を固めることで、今回のような設計思想の話も理解しやすくなるのではないかと思います。
(興味がある方は是非とも購入のご検討を!)

Java:リテラル用の領域の確保について

Javaの変数の型は、プリミティブ型と参照型に大別されます。
そして、変数の値が同じであるかどうかを確認する場合、プリミティブ型は == で同じ値であることを確認できる(同じ値の場合はTrueになる)のに対し、参照型の場合は原則として == では確認できず、equalsメソッドを使う必要があります。

しかし、参照型の場合であっても、== で確認できる場合があります。
それは、参照型の変数にリテラル(ソースコード内に直接記述された定数値)を直接代入した場合です。

以下で、順番を追って説明していきます。

【メモリーへの値の保持の方法】

変数の値はメモリーに保持されます。
メモリーは1バイト(8ビット)ずつ細かく区分けされており、それぞれの区分けについて「アドレス」と呼ばれる値により一意に場所を特定します。
(アドレスのバイト数は、64ビットOSの場合は8バイトです)

プリミティブ型も参照型も、「メモリーに保持される」ということは変わりませんが、メモリーに保持する値は変わります。プリミティブ型変数では値そのものをメモリーに格納するのに対し、参照型変数ではメモリー上にその変数用の保存領域を確保した上で、その保存領域の場所を指し示すアドレスを変数の領域に格納します。

例として、以下のソースコードについて考えます。

このソースコードの場合のメモリーのイメージは以下です。
プリミティブ型変数の「i1」と、参照型変数の「il1」では、値の保持の方法が異なります。

【リテラル用の領域の確保】

リテラルの場合は、リテラル用の領域が別途確保されます。
そして、同じ値のリテラル値が複数の箇所で記述されていたとしても、全ての箇所において同じ領域が使いまわされます。

参照型変数でインスタンス化した(newした)場合は、仮に同じ値だとしても、インスタンス化する度に異なる領域が確保されるため、その挙動とは異なるものとなります。

例として、以下のソースコードについて考えます。

このソースコードの場合のメモリーのイメージは以下です。
リテラル値を代入した「s1」「s2」と、インスタンス化で新たに領域を確保した「s3」「s4」では、値の保持の方法が異なります。

【比較を行った場合の挙動の違い】

上記より、リテラル値を代入した場合と、インスタンス化を行った場合では、値の保持の方法が異なります。

この違いが表れるケースの一つとして、値が同一であるかどうかを確認するケースがあります。
リテラル値を代入した参照型変数同士を比較する場合は、インスタンス変数同士の比較と同じように、== で値が同一であることを確認できます。
(もちろん、equalsメソッドでも比較可能です)
しかし、インスタンス化を行った参照型変数の比較では、== で値が同一であることを確認することはできず、equalsメソッドを使用する必要があります。

また、equalsメソッドであれば、リテラル値を代入した参照型変数とインスタンス化を行った参照型変数の比較も可能です。

これをソースコードで示すと以下の通りです。
(実行結果となる標準出力はコメントで示しています)


リテラル用のメモリー領域の確保について深堀りする機会があったので、記事を書きました。

実務では、普段からString型のような参照型の変数をequalsメソッドで比較する癖をつけておけば大丈夫です。
しかし、リテラル代入と == の組み合わせで比較している場合、後の修正でリテラル代入の代わりにインスタンス化を使う箇所が現れると上手く比較できなくなるので、注意が必要です。

ソースコードの重複の排除を関数で学ぶ

「ソースコードから重複を排除して保守性を高める」という考え方は、実務で良いコードを書く上で重要な考え方です。
この考え方が身に付かない内はオブジェクト指向の理解も不十分になるのですが、いきなりオブジェクト指向から入るとこの考え方の重要性がわかりにくくなることがあります。

そこで、ソースコードから重複を排除することの意義を、関数の使い方から学ぶことが有効になることがあります。

今回は、関数を使うことでソースコードから重複を排除し、保守性が高まる例を挙げていきたいと思います。
(サンプルコードはJavaで記述します)


今回のサンプルコードでは、複数の商品の販売価格を計算します。
まずは関数を使わないサンプルコードから書いていこうと思います。

【サンプルコード(関数未使用・修正前)】

・FunctionTestMain.java

【実行結果】


先ほどのソースコードに対して、「店舗独自の割引額を考慮する」という修正を入れていきます。

関数を使用しない場合、割引額(discountRate)を掛けるという修正を複数個所に入れることになります。
これが、ソースコードに重複が発生している状態です。

ソースコードに重複が発生していると、一部だけ修正を漏らすことによるバグに繋がりやすくなります。
このバグを潰すために、テストする範囲も広がってしまいます。

【サンプルコード(関数未使用・修正後)】

・FunctionTestMain.java

【実行結果】


次に、ソースコードを一旦修正前の状態に戻して、関数を入れていきます。

販売額を計算する関数(salesPriceCalc)を入れることで、ソースコードから重複を取り除くことができています。

【サンプルコード(関数使用・修正前)】

・FunctionTestMain.java

【実行結果】


関数を使用したソースコードに対して、先ほどと同じように割引額を考慮する修正を入れます。

重複が関数により排除されているので、割引額を入れる修正は1カ所で済んでいます。
修正箇所が減っているため、修正漏れを心配する必要がなくなり、ソースコードの保守が容易になっています。
言い換えると、時間をかけずにバグが出にくい修正を行うことができるようになります。
修正が繰り返される実務のソースコードでは、これは重要なことです。

【サンプルコード(関数使用・修正後)】

・FunctionTestMain.java

【実行結果】


今回解説したことは、実際の新人研修でも教えることが多いです。
このような簡単な例を用いることで、ソースコードの重複の排除について、わかりやすく説明することができます。


ところで、突然の発表なのですが、このブログでの私の記事はこれが最後となります。
このブログの記事を読んだことがある、と意外な所から声をかけていただくこともあり、大変嬉しく思っています。
少しでも皆様のお役に立てていたのであれば幸いです。

今まで記事を読んでいただき、ありがとうございました!

java:参照型変数やメモリの理解に苦しむ若手Java技術者に向けて

Javaの参照型変数の中身は、何年かJavaの実装経験を積んだ人でもイメージすることが難しいです。
しかし、ここがイメージできていないと、思わぬ落とし穴にはまることもあります。

C言語を経験していればイメージしやすくなりますが、そのためだけにC言語を学ぶのもハードルが高く感じると思います。
そこで、今回の記事で、参照型変数の中身のイメージについて、重要な点のみをピックアップして説明していきたいと思います。

0.そもそもメモリとは

参照型変数について理解する前に、まずはメモリについて意識する必要があります。

メモリとは、プログラムの実行中に取り扱っているデータを一時的に保存する領域です。
数学の問題を解く時に、答えを出す前に途中式を紙に書くと思いますが、その紙がメモリのようなものだと考えれば良いです。
なお、一般的には、プログラムの実行結果は最終的にファイルやDBに恒久的に保存するか、画面に表示するかします。
数学の問題の例で言うと、恒久的な保存領域や画面表示が答えに相当すると考えれば良いです。

そして、メモリは1バイト(8ビット)ずつ細かく区分けされており、それぞれの区分けについて「アドレス」と呼ばれる値により一意に場所を特定します。
アドレスのバイト数は、64ビットのOSの場合は8バイトになります。

文章で書かれてもイメージが難しいと思いますが、後ほど図を使って説明します。
とりあえず、「メモリと呼ばれる一時的な保存領域がある」という意識を持っていただければ、と思います。

1.プリミティブ型と参照型

Javaの変数は、プリミティブ型と参照型の2種類に大きく分けることができます。
どちらの型なのかによって、メモリに格納する内容が変化します。

プリミティブ型に分類される型については、以下の8つの型が存在します。

細かいですが、boolean型についてはデータの保持に使うのは1ビットのみで、メモリに値を保存する時にはバイト単位になっているはずです。
Javaの実装上は1バイトになることが多いようなのですが、boolean型が何バイトになるのかは正式な取り決めはないので、万が一これを気にする必要がある場合は調査した方が良いです。

そして、プリミティブ型以外の型の変数は、全て参照型変数に分類されます。
newでオブジェクトを生成する変数は全て参照型変数です。
また、プリミティブ型の配列についても、参照型変数に該当します。
プリミティブ型のラッパークラス(Integer型、Character型、等)やString型についても参照型変数に該当しますが、特定の場面においてはプリミティブ型に見えるような動きをします(詳しくは後述します)。

プリミティブ型変数と参照型変数では、メモリに格納する値が異なります。
プリミティブ型変数では値そのものをメモリに格納するのに対し、参照型変数ではメモリ上にその変数用の保存領域を確保した上で、その保存領域の場所を指し示すアドレスを変数の領域に格納します。
図に表すと、以下のようになります。

この違いは、プログラムの挙動の違いとなって現れます。それを以降で説明していきます。

2.変数をコピーした際の挙動の違い

プリミティブ型変数と参照型変数の挙動の違いを実感するのは、変数をコピーした時でしょう。
実務でも躓きやすいポイントなので、サンプルコード付きで詳しく説明していきます。

以下は、プリミティブ型の変数をコピーした後に、コピー元の変数を変更する例です。
コピー元の変数の変更は、コピー先の変数に影響しません。
これはイメージ通りだと思います。

しかし、同じような書き方で参照型変数をコピーした場合、コピー元の変数の変更がコピー先の変数にも影響してしまいます。
以下は、配列をコピーした例と、ArrayList型をコピーした例です。

なぜこのようなことが起こるのかと言うと、この書き方ではアドレス値をコピーしてしまっており、領域を新たに確保しているわけではないからです。
このようなコピーは、シャローコピー(浅いコピー)と呼ばれます。

文章だけではイメージするのが難しいので、図も交えながら、シャローコピーの挙動を説明します。

コピーした段階では、アドレス値をコピーしてしまっており、指し示す領域はコピー元と同一になってしまっています。

そのため、コピー後にコピー元の変数を変更すると、その影響がコピー先の変数にも表れてしまいます。

これを回避するためには、新たに領域を確保し、その領域に格納する値をコピーした後、新たな領域を指し示すアドレス値を変数に格納するような形でコピーする必要があります。
このようなコピーは、ディープコピー(深いコピー)と呼ばれます。

ディープコピーのイメージは以下の通りになります。

コードで言うと、以下のようなコードになります。

上記のコードは原始的なディープコピーの方法です。
ディープコピーを行うためのCloneableインターフェースがJavaでは用意されており、これを使用した方が効率的にディープコピーを行うことができることもあります。
詳しくは、java:オブジェクトの中身をコピーする方法(cloneメソッド実装)を参照して下さい。

なお、プリミティブ型変数と参照型変数の挙動の違いを実感する代表的な場面としては、他にはメソッドの引数に変数を引き渡す場面が挙げられます。
情報処理技術者試験では値渡しと参照渡しの違いについても出題範囲になっていますが、これは値そのものを渡しているか、アドレス値を渡しているかの違いです。
参照型変数を引数で渡す時にアドレス値を渡している感覚(参照渡しをしている感覚)を持っていないと、これもバグの原因になり得ますので、注意が必要です。

3.メモリの開放

参照型変数の値を保持するための領域は、newする度に確保され直します。
(配列の場合は、配列を宣言し直す度に確保され直されます)

例えば、以下のコードでは、2回目のnewにより領域が再確保されています。

これを図解すると以下のようになります。

ここで問題になるのが、newし直す前に確保していた領域です。
この領域はプログラムで確保していたものの、使われなくなった領域です。
newで確保した領域が他のプロセス(プログラム)から使われることはあってはならないので、確保した領域は他のプロセスから使えなくなるような制御がかかります。
再び他のプロセスから使えるようにするためにはメモリを開放する必要があるのですが、この開放を忘れたまま領域の再確保を繰り返すと、他のプロセスが使えるメモリの領域が徐々に減っていきます。
これが「メモリリーク」と呼ばれる現象であり、放置すると空きメモリの不足により、プロセスの挙動や、場合によってはシステム全体の挙動が不安定になります。

C言語では、メモリの開放はコーディングにより明示的に行う必要がありました。
しかし、Javaでは「ガベージコレクション」と呼ばれる仕組みにより、開放するべき領域がある程度増えたら自動的に開放が行われるようになりました。

意図しないタイミングでのガベージコレクションは予期せぬ性能劣化を招く可能性があるので、性能要件がシビアなシステムではガベージコレクションについても気を配る必要があります。
ガベージコレクションについて詳しく見ていくにはこの記事ではとても足りないのですが、調べる上でメモリのイメージがついていれば理解は早まると思います。

4.イミュータブルな参照型変数について

参照型変数の中には、イミュータブルな変数も存在します。
イミュータブル(immutable)とは「不変」という意味であり、ミュータブル(mutable)の対義語です。
オブジェクト指向言語においては、「イミュータブル」は、「オブジェクトの生成後に、そのオブジェクトの状態(メモリ領域に保持されている値)が変化しない」という意味を指します。

イミュータブルな参照型変数の場合、newして領域を確保した後にオブジェクトの状態を変更したい場合は、再度newして領域を確保し直す必要があります。

Javaにおいては、プリミティブ型のラッパークラスやString型が、イミュータブルな参照型として用意されています。
これらの変数については、newを書かなくとも、領域が都度確保され直す、という挙動となります。
その結果、値の代入や参照においては、あたかもプリミティブ型の変数かのような挙動となります。
(ただし、本当にプリミティブ型の変数というわけではないため、プリミティブ型変数と異なりnull値を持つことができ、メソッドも持っています)

Javaで用意されているイミュータブルな参照型変数のコピーする例は以下の通りとなります。
これはシャローコピーと同じ書き方ですが

実際の挙動としては以下のように明示的にnewしているのと同じ挙動となるため

結果としてはディープコピーの挙動となります。
(つまり、シャローコピーすることはできず、意図せずともプリミティブ型のような動きとなります)


いかがでしたでしょうか。

Javaの入門レベルを脱し、Javaのコーディングで意図しないバグの発生を防いだり、性能改善を図ったりしたいのであれば、メモリの話に踏み込まざるを得ません。
しかし、JavaではC言語と異なりメモリについて意識しなくてもコーディングできるようになっているので、生産性の高さと引き換えに、メモリについて想像するのが難しくなっています。
また、基本情報処理技術者試験で来年度(2023年度)からC言語を含む言語選択問題が出題対象から外れる(疑似言語に統一となる)ため、C言語に触れる機会が減ることが予想されます。

このような背景があるため、後継のJava技術者にメモリについて教える機会は今後ますます増えることが予想されます。
その際、C言語未学習者でも要点をかいつまんで理解してもらい、かつ躓きやすいポイントを押さえた記事が必要であると感じたため、今回の記事を執筆しました。

これからも、教育上必要と感じることがあれば、記事にしていきたいと思います!

余談ですが、Javaの挙動を確認するため、今回の記事の執筆時にpaiza.ioのサービスを使ってみました。
これは、オンライン上でコードを書き実行できるというものであり、Javaにも対応しています。
Chromeのデベロッパーツールのコンソール上でのJavaScript実行のようなことがJavaでもできるので、ちょっとした挙動確認でとても便利でした。

SpringFrameworkのAOPとは

AOPとは、「アスペクト指向プログラミング(Aspect Oriented Programming)」の略称です。
「アスペクト」を日本語に直訳すると「相」ですが、プログラミングの世界では「オブジェクト指向ではうまく分離できない横断的な機能」のことを指します。代表例としてはログ出力機能(各クラスの処理の開始・終了等のタイミングで割り込むような形で使われる)が挙げられます。
AOPをサポートするフレームワークでは、横断的な機能を別のプログラムとして切り出し、そのプログラムが特定のタイミングで呼び出されるように別途定義できるような機能が組み込まれています。

SpringFrameworkにも、AOPが機能の一つとして組み込まれています。
AOPを利用することで、横断的な機能を切り出して可読性を高めたり重複を排除したりすることができます。


以下は、SpringFrameworkでのAOPの書き方の一例です。
test関数の開始時と終了時に標準出力を割り込ませるサンプルコードです。

なお、便宜上、サンプルコードではRestControllerを併用していますが、AOPはRestControllerを併用する必要はありません。

【サンプルコード】

・SmpSpringBootApplication.java

・MyFirstAspect.java

【実行結果(コンソール)】

・SmpSpringBootApplication起動後、http://localhost:8080/testにアクセス


いかがでしたでしょうか。

SpringFrameworkのAOPについて簡単なサンプルコードが欲しかったので、簡単な解説を付けた上で記事に起こしました。
本日ではSpringFrameworkの情報は書籍やWeb上に十分に揃っているのですが、情報を付け加えたくなったらまた記事を起こしたいと思います。