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上に十分に揃っているのですが、情報を付け加えたくなったらまた記事を起こしたいと思います。

ゲームに熱中する心理的仕掛けとゲーミフィケーション

ゲームには没入感を高めるための心理的仕掛けがあります。
その心理的仕掛けを他分野に応用する「ゲーミフィケーション」と呼ばれる手法が、現在注目を浴びています。
今回の記事では、心理的仕掛けとゲーミフィケーションについて、概要を簡単に書きたいと思います。

【没入感を高める心理的仕掛け】

ゲームに仕込まれている心理的仕掛けには色々なものがありますが、主軸となっているのは「世界観」と「正の強化」の2つです。
出来の良いタイトルであれば、この2つの心理的仕掛けは必ずと言って良いほど仕込まれています。

・世界観

ゲームの内部で行われている処理は、あくまでも論理的な演算(ゲームシステム)です。
この論理的な演算を売りにしても、一般的な人々にとってはとっつきにくく、興味を持ってもらません。
そこで、「近未来のSF」「剣と魔法のファンタジー」といった魅力的な世界観でカバーすることで、一般的な人々に興味を持ってもらえるようにしています。

・正の強化

「正の強化」とは行動主義心理学の用語であり、ある行動をした結果として快の感情を受けることで、その行動を増加させるというものです。
ゲームからは、「前の敵より少しだけ強い敵」「ちょうど良いレベルの対戦相手」といった、少し頑張れば乗り越えられる、ちょうど良い高さのハードルが適切なタイミングで提示されます。
このハードルを乗り越えることで達成感という快の感情を得ることができ、次のハードルを探して再びゲームを手に取るようになります。
最終的には、達成感を得るために、ゲームをプレイする時間を意識的に捻出するようになります。

【ゲーミフィケーション】

ゲーミフィケーションとは、ゲームの心理的仕掛けを日常生活に応用することであり、教育成果を高めたり顧客満足度を高めたりする上で有効とされている手法です。
もちろん、ゲームとしてプロダクトを作成することでこのメリットをわかりやすく享受することができます。
しかし、そこまでしなくとも、ゲームの心理的仕掛けを応用することで、例えば以下のようなメリットを享受することに期待できます。

・世界観

無味乾燥に思える行動について、人々が興味を持つような世界観でカバーすることで、積極的にその行動を行ってもらうことができます。
例えば、一般的な高校の理系科目の教育では数学的な概念が前面に押し出されていることが多く、一般的な人々が興味を持つような内容になっているかは疑わしいです。
しかし、ロボットを動かす、人々の経済活動を調べる、新素材を開発する、といった具体的な活動については、一般的な人々にも興味を持ってもらいやすいと考えられます。
そして、これらの活動に高校数学が応用されているということを知れば、理系科目に興味を持ってもらいやすくなります。

・正の強化

ゲームではちょうど良い高さのハードルを受動的に与え続けられますが、日常生活ではこのハードルを自分で探さなければならなりません。
高すぎるハードルを設定してしまうと達成感を得ることができず、やがて挫折してしまいます。
最終的なハードルを設定した後に、少しの努力で達成可能なハードルを細かく設定することが肝要です。
例えば、試験に合格したい場合、いきなり「模試で○○点以上を取る」という高いハードルを設定してしまうと、それを達成できなかった時に挫折してしまいますし、達成できない確率も高くなってしまいます。
しかし、「参考書を1日10ページ以上読む」という低めのハードルであれば、挫折せずに長く続けることができますし、無理をしない範囲でそのハードルを超えること(例えば参考書を10ページを超えてキリが良い所まで読む)を達成することも多いですし、努力を継続できることで最終的には試験に合格するだけの力を蓄積できる可能性が高くなります。


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

情報分野の技術はプロダクトを実装する上で重要ですが、より上位のレイヤー、例えば要求定義・要件定義や組織運営といったレイヤーでは、人と人の繋がりを考えることが重要になります。
知識面で言うと、戦略や心理学といった分野の知識が重要になりますし、IT分野の人材の質を高めるために設けられている情報処理技術者試験でもその分野の知識が問われることがあります。

今回執筆した「ゲーミフィケーション」は、有力なプロダクトを作成したり、教育効果を高めたりする上で、有用な知識であり、知っておいて損はないと思います。

これからは、戦略や心理学といった分野の知識についても、IT業界との関わりが深いものであれば書いていきたいと思います!

「見積もり概論」社内勉強会用のパワポの公開

社内勉強会向けに、見積もりについて説明するパワポを作成したため、公開します!
見積もり概論

見積もりの必要性・手法・経験則について、簡単に説明しています。

詳細設計以降の作業の見積もりを行う場合は、見積もり手法と言うよりは、技術力の方が重要になります。
しかし、それ以前の工程の時点で見積もりを行う場合、具体的なものが見える前に見積もらなければならないという難しさがあり、見積もりがブレた場合の影響も大きいので、このパワポで解説するような知見が重要な意味を持ってきます。

上流工程を担うようになった場合に役に立つ知見なので、それまで頭の片隅に置いていただければと思います。

例外ケースは処理の始めに除外する

プログラムで何かしらの処理を記述する場合、本当に実装したい処理(本処理)に入る前に、例外ケースを除外するテクニックがあります。
このように記述することで、本処理では例外ケースを考えずに済むため処理内容を考えやすくなりますし、例外ケースの場合に時間がかかる本処理を実行しなくて済むようになるので性能面でもメリットがあります。

例として、競技プログラミングの問題を取り上げて説明してみます。

今回取り上げる問題は以下です。
 100円玉がA枚、
 10円玉がB枚あります。
 X円を作る方法が何通りあるか出力しなさい。

ループ処理を実装して総当たりを行えばこの問題を解くことはできますが、その場合は性能面が犠牲になるため、望ましい解答ではありません。
今回は、ループ処理を使わずにこの問題を解いてみます。
(言語はJavaです。なお、本来は、標準入力を入力としますが、今回はソース中の定数を入力としています。)


【解答例】

・HundredYenTenYen.java

【実行例】

・100円玉だけで100以降の位を払え、10の位を払った時に10円玉10枚組が減らない場合

 備考:以下の3パターンがある。
  100円玉5枚+10円玉5枚
  100円玉4枚+10円玉15枚
  100円玉3枚+10円玉25枚

・100円玉だけで100以降の位を払え、10の位を払った時に10円玉10枚組が減る場合

 備考:以下の2パターンがある。
  100円玉4枚+10円玉6枚
  100円玉3枚+10円玉16枚

・100円玉だけでは100以降の位を払えない場合

 備考:以下の1パターンがある。
  100円玉6枚+10円玉24枚

・持っている10円玉の枚数で、作りたい円の10の位を実現できない場合

・作りたい円が10の倍数ではない場合

・作りたい円が100円玉・10円玉の合計金額を超えている場合


この解答例では、「10円玉10枚の組み合わせは、100円玉1枚を代替できる」という点に着目し、④~⑦の箇所でパターン数を割り出しています。

そして、ポイントは、①~③で例外ケースを除外している所にあります。
①~③の例外処理を事前に噛ませることで、④~⑦に到達した時点で
・入力された金額は100円玉と10円玉で払い切れる
・入力された金額は必ず10の倍数である
・10の位を払う時に10円玉が足りなくなることはない
ということが保証されるため、④~⑦の処理を考えやすくなっています。

また、今回の例では当てはまりませんが、もし④~⑦が時間のかかる処理である場合は、例外ケースの場合に処理時間の短縮をすることもできます。

このように、例外ケースを事前に除外するテクニックが役に立つことがあるので、覚えておいて損はないでしょう。


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

プログラムのアルゴリズムを考える上ではいくつかコツやテクニックのようなものがあるので、これからもそういった情報を発信できればと思います。