【Java】Java入門(Javaの様々な機能編)
Javaでオブジェクト指向を理解した後に知っておきたいトピックを解説していきます。
Javaの基本編とオブジェクト指向編については以下の記事を参照ください。
目次
- Enum
- スレッド
- ガーベジコレクション
- コレクションフレームワーク
- JUnit
- 文字列を掘り下げる
- データベースアクセス
- 関数オブジェクト
- Javaのライセンスについて
Enum
Enum(イーナム)は日本語では列挙型といます。
Enumは一言でいうと定数の集合です。
つまり、複数の定数を一つにまとめたものです。
Enumは、定数をより使いやすくする仕組みです。
package app; public class Main { public static void main(String[] args) { // メソッドを呼び出し janken(1); janken(2); janken(3); janken(4); } // 引数じゃんけんをするメソッド public static void janken(int n) { switch (n) { case 1: System.out.println("グー"); break; case 2: System.out.println("チョキ"); break; case 3: System.out.println("パー"); break; } } }
このソースコードは正直言って良いソースコードではありません。
jankenメソッドの中のswitchで指定しているラベル(1, 2, 3)は何を表しているのか分かりません。
また、呼び出し側のソースも、引数の値だけを見てもどんな結果になるのかが想像できません。
あらかじめ「グーは1」「チョキは2」「パーは3」を表していることを知っておく必要があります。
このような読みにくいソースコードを読みやすくする解決策として、定数を使ってソースコードを読みやすくする方法が考えられます。
Main.java
package app; public class Main { // 定数を追加する public static final int GU = 1; public static final int CHOKI = 2; public static final int PA = 3; public static void main(String[] args) { // 引数に定数を使用する janken(GU); janken(CHOKI); janken(PA); janken(4); // 意図しない値も入る } // 引数じゃんけんをするメソッド public static void janken(int n) { switch (n) { case GU: // ラベルに定数を使用する System.out.println("グー"); break; case CHOKI: System.out.println("チョキ"); break; case PA: System.out.println("パー"); break; } } }
このように修正すると、ソースコードは先ほどよりも読みやすくなりました。
メソッドのラベルが定数名になることで、メソッドの中身が読みやすくなりました。
また、メソッドの呼び出しでも引数に定数を入れることで、意味が分かりやすくなりました。
しかし、まだ問題は残ります。
メソッドの呼び出しの個所で、引数に4を入れて呼び出している個所が存在します。
引数をint型で受け取っているため、問題なく値を受け渡すことができますが、本来は意図しない値です。
3種類の決められた値のみを受け取れる仕組みがあるとこのようなミスを減らすことができます。
Enumを使うことでこのような仕組みを実現することができます。
package app; public class Main { public enum Janken { GU, CHOKI, PA }; public static void main(String[] args) { // 引数に定数を使用する janken(Janken.GU); janken(Janken.CHOKI); janken(Janken.PA); // janken(4); // エラーになる } // 引数じゃんけんをするメソッド public static void janken(Janken j) { switch (j) { case GU: // ラベルに定数を使用する System.out.println("グー"); break; case CHOKI: System.out.println("チョキ"); break; case PA: System.out.println("パー"); break; } } }
今度はEnumを使用して修正した結果です。
引数でJanken型を指定しているため、Jankenの中で定義された要素しか指定することができなくなります。
そのため、意図しない値が入ってくることがなくなり、プログラムのミスを減らすことができます。
Enumのまとめ
スレッド
スレッドとは糸のことです。
プログラミングでは処理の流れをスレッドという言葉で表します。
今まで見てきたプログラムは、一つの処理が終わった後、次に処理に進むようになっていました。
複数の処理を同時並行で実行するものはありませんでした。
しかし、実際にプログラムを作成する際には、複数の処理を同時並行で実行したい場面もあります。
(データのダウンロードをしながら画面を表示する、など)
そのような場合、マルチスレッドという仕組みを使用します。
マルチスレッドを実現する方法は2つあります。
- Threadクラスの継承
- Runnableインターフェースの実装
Threadクラスの継承
まずはThreadクラス継承して実装する方法を見ていきます。
SampleThread.java
package app; // Threadクラスを継承 public class SampleThread extends Thread { private int number; public SampleThread(int number) { this.number = number; } // runメソッドをオーバーライド @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("number:" + number); } } }
Main.java
package app; // 動作確認 public class Main { public static void main(String[] args) { SampleThread s1 = new SampleThread(1); SampleThread s2 = new SampleThread(2); // startメソッドを呼ぶことで、runメソッドの処理がマルチスレッドで実行される s1.start(); s2.start(); } }
結果
number:1 number:1 number:1 number:1 number:1 number:1 number:1 number:2 number:2 number:2 ... 以下、number:1とnumber:2がそれぞれ100個出力されるまで続く
実行結果は実行する度に変わります。
このプログラムは、素直に上から順に処理されると考えて結果を予想すると、「number:1」が100個出力された後、「number:2」が100個出力されるはずです。
しかし、実際には「number:1」と「number:2」がランダムで出力される結果となり、それぞれの処理が独立して平行してるように見えます。
これがマルチスレッドの処理です。
Runnableインターフェースの実装
続いてはRunnableインターフェースを実装する方法を見ていきます。
SampleRunnable.java
package app; public class SampleRunnable implements Runnable { private int number; public SampleRunnable(int number) { this.number = number; } // runメソッドをオーバーライド @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println("number:" + number); } } }
Main.java
package app; // 動作確認 public class Main { public static void main(String[] args) { SampleRunnable s1 = new SampleRunnable(1); SampleRunnable s2 = new SampleRunnable(2); // インターフェースにRunnable型のインスタンスを入れる Thread t1 = new Thread(s1); Thread t2 = new Thread(s2); // オーバーライドしたrunメソッドの処理がマルチスレッドで処理される t1.start(); t2.start(); } }
結果
number:2 number:2 number:2 number:2 number:2 number:2 number:2 number:1 number:1 number:2 ... 以下、number:1とnumber:2がそれぞれ100個出力されるまで続く
こちらも結果は先ほどの処理と同じで、実行されるたびに結果が変わります。
Javaでは多重継承ができない問題があるため、特定のクラスを継承していた場合にはThreadクラスを継承したマルチスレッドは使用できません。
既に別のクラスを継承している場合でも、Runnableインターフェースを使用すればマルチスレッドを実現することができます。
sleepメソッド
処理をしている中で、一定の時間処理を停止したいという場合もあります。
その場合にThreadクラスのsleepメソッドを使用することで実現することができます。
Main.java
package app; public class Main { public static void main(String[] args) { for(int i = 0; i<10; i++) { System.out.println(i); try { // sleepメソッドにより、1000ミリ秒、処理がストップする Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
結果
0 1 2 3 4 5 6 7 8 9
このプログラムを実行すると、1秒おきに0~9までの数値が出力されます。
synchronized
マルチスレッドのプログラムは、つくり方によっては意図しない動作をする可能性があります。
以下のプログラムを確認してみてください。
NumCheck.java
package app; public class NumCheck { private int num; // フィールドの数値が偶数か奇数かを判定して結果を出力するメソッド public void check(int num) { this.num = num; System.out.println("値は" + this.num + "です。"); if (this.num % 2 == 0) { System.out.println("偶数です"); } else { System.out.println("奇数です"); } } }
NumCheckTest.java
package app; // マルチスレッド用のクラス // NumCheckクラスのcheckメソッドをマルチスレッドで実行できるようにする public class NumCheckTest extends Thread { private NumCheck numCheck; private int num; public NumCheckTest(NumCheck numCheck, int num) { this.numCheck = numCheck; this.num = num; } @Override public void run() { numCheck.check(num); } }
Main.java
package app; // 動作確認 public class Main { public static void main(String[] args) { NumCheck check = new NumCheck(); // 0~9までの数値でNumCheckクラスのcheckメソッドを実行する for (int i = 0; i < 10; i++) { new NumCheckTest(check, i).start(); } } }
結果
値は1です。 偶数です 値は2です。 偶数です 値は1です。 値は3です。 奇数です 奇数です 値は4です。 奇数です 値は5です。 奇数です 値は6です。 偶数です 値は9です。 奇数です 値は8です。 奇数です 値は7です。 奇数です
このプログラムも当然実行するたびに結果が異なります。
実行結果はおかしな結果となりました。
数値と偶数奇数の結果が合っていませんし、出力順もバラバラです。
平行に処理されるので、冷静に考えれば当たり前に思う方もいるかもしれませんが、これはプログラムの整合性が取れておらず、本来は意図しない結果です。
この不整合をなくすようにするには、マルチスレッドで処理されるメソッド(今回の場合checkメソッド)にsynchronizedとういキーワードを付けます。
NumCheck.java
package app; public class NumCheck { private int num; // synchronizedを追加する public synchronized void check(int num) { this.num = num; System.out.println("値は" + this.num + "です。"); if (this.num % 2 == 0) { System.out.println("偶数です"); } else { System.out.println("奇数です"); } } }
結果
値は0です。 偶数です 値は2です。 偶数です 値は1です。 奇数です 値は3です。 奇数です 値は4です。 偶数です 値は8です。 偶数です 値は6です。 偶数です 値は7です。 奇数です 値は5です。 奇数です 値は9です。 奇数です
こうすることで、メソッドが処理される順序はバラバラですが、それぞれの結果の整合性は取れるようになっています。
synchronizedを入れることで、メソッドに排他制御がかかり、そのメソッドが終了するまでは他のスレッドが処理できないようになります。
スレッドのまとめ
- 複数の処理を平行で行うにはマルチスレッドを使用する
- マルチスレッドを使用するには、Threadクラスを継承する方法とRunnableインターフェースを実装する方法がある
- メソッドに排他制御を掛けるにはsynchronizedを付ける
ガーベジコレクション
ガーベジとはごみを表す言葉です。
ガーベジコレクションは直訳するとごみを集めることです。
プログラミングの世界でガーベジコレクションとは、メモリ上にある不要な領域(変数を使用するために確保したが、その変数が使用されなくなったところなど)を再び使える領域として解放してあげることです。
JavaではJVMが使用されていないメモリ領域を解放をする仕組みを持っており、その仕組みをガーベジコレクションと呼びます。
プログラムで変数を宣言したりインスタンスを作成したりすると、値を格納できる分のメモリ上の領域が確保されます。
新しい変数やインスタンスが増えるたびに新しいメモリ領域が確保され、使用できるメモリの領域は減っていくわけですが、当然処理が進む中で使用されなくなる変数も出てきます。
そのような参照されなくなった変数の領域は、再び解放してあげなければメモリを効率よく使用することができません。
JVMが自動的に参照されなくなったインスタンスなどを探し、自動的に削除してメモリを解放してあげる仕組みがガーベジコレクションです。
package app; public class Main { public static void main(String[] args) { // 文字列"ABC"のインスタンスが作成される String str = new String("ABC"); // strの参照先が"123"に変わることで、"ABC"はどこからも参照されなくなる str = new String("123"); System.out.println(str); } }
この例では、strに"123"という文字列のインスタンスを代入した時点で、"ABC"という文字列のインスタンスはどこからも参照されなくなります。 このような場合に、JVMが適当なタイミングで参照されていないインスタンスを削除してくれます。
ちなみに、自分で意図的にガーベジコレクションを実行したい場合はSystem.gc()メソッドを呼び出すことで実行可能です。
ガーベジコレクションのまとめ
- Javaでは使用されていないメモリ領域は自動的に解放される仕組みになっている
- その仕組みをガーベジコレクションと呼ぶ
- ガーベジコレクションは適当なタイミングで自動的に処理されるが、自分で呼び出すことも可能
コレクションフレームワーク
コレクションは、まとまったデータを扱う場合に使用できるJavaのライブラリです。
Javaではまとまったデータを扱う仕組みとして配列を学びましたが、配列には様々な制限があります。
例えば、一度要素数が決まった後に要素数を変更することができなかったり、数値でしかアクセスできないなどです。
これら配列ではできない問題点を解決したライブラリがコレクションフレームワークです。
コレクションには様々な機能がありますが、大きく分けて
- List
- Map
- Set
の3つがあります。
List
リストは複数のデータを格納するための仕組みで、配列に似ています。
配列と違うのは、配列は最初に要素数を確保した後に要素数を変更できなかったのに対し、リストでは動的に要素数を増やすことができます。
Main.java
package app; // コレクションに関するクラスは「java.util」のパッケージに入っている import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) { // Listはインターフェース。 // Listを実装したクラスでよく使用されるものにArrayListがある // <>の部分をジェネリクスと呼ぶ List<String> list = new ArrayList<>(); // 要素を追加するにはaddメソッドを使用します。 list.add("A"); list.add("B"); list.add("C"); // 要素数を調べるには、sizeメソッドを使用します。 for(int i = 0;i < list.size(); i++) { // 要素を取り出すにはgetメソッドを使用します // 引数で格納された番号を渡します System.out.println(list.get(i)); } } }
結果
A B C
宣言方法、要素の追加方法、要素数の取得方法など、細かい部分では配列と違いがありますが、処理の流れはそこまで難しくないでしょう。
配列では宣言時や初期化処理の段階で要素数が決まってしまいますが、Listでは要素数を指定している個所はありません。
addメソッドを繰り返す段階で自動的に要素数が増える仕組みとなっているため、配列よりも柔軟なプログラムが書けます。
もう一つ配列とListでは大きな違いがあります。
それは、配列は基本型の値を格納できたのに対し、Listの場合格納できる値は、参照型の値だけです。
整数を入れたい場合もint型は使用できません。
しかし、Listの中に基本型の値を格納したい場面は当然あります。
そのような場合、基本型に対応した参照型(ラッパークラス)を利用することで解決できます。
int型の場合、対応している参照型はInteger型です。
Main.java
package app; import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) { // intの値を入れたい場合はIntegerを指定 List<Integer> list = new ArrayList<Integer>(); list.add(1); list.add(2); list.add(3); for(int i = 0;i < list.size(); i++) { System.out.println(list.get(i)); } } }
結果
1 2 3
ジェネリクス
Listの宣言の個所に「<>」で囲われた部分があります。
これはジェネリクスと呼ばれるものです。
この中に、格納したい要素の型を指定します。
基本型(intなど)は指定することができず、参照型のみ指定可能です。
ジェネリクスの使用は必須ではありませんが、使用することで開発が安全になります。
以下の例はジェネリクスを使用しないプログラムの例です。
警告は出ますが実行することは可能です。
Main.java
package app; import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) { // ジェネリクスを書かない場合 List list = new ArrayList(); list.add(1); // オブジェクト型を格納できる list.add("2"); // つまり何でも格納できる for(int i = 0;i < list.size(); i++) { // Object型で格納されているためキャストが必要 Integer num = (Integer)list.get(i); // Integer以外の値が入っていたら例外が発生する System.out.println(num); } } }
結果
1 Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer at app.Main.main(Main.java:15)
ジェネリクスを使用しない場合は、Object型として値が格納されるため、取り出して使用する際にはキャストして型を元に戻してあげる必要があります。 しかし、キャストする際、型が異なっていた場合は例外が発生します。 この例ではInteger型の値とString型の値が入っています。 String型はInteger型にキャストすることはできないため、例外が発生します。
ジェネリクスを使用した場合、要素を追加するとき、ジェネリクスで指定した型しか追加できません。
コンパイル時点で型のチェックをができるため、実行時エラーの発生を防ぐことができます。
そのため、安全なプログラムの作成が可能となります。
ちなみに、ジェネリクスを使用する際宣言時に右側にも型を書いても同じ動きをします。
バージョンアップに伴って右側は省略しても動作するようになりました。
List<String> list = new ArrayList<String>(); // これでもOK // List<String> list = new ArrayList<>();
拡張for文
配列を扱ったときに、for文を簡潔に書く方法として拡張for文という繰り返し構文がありました。
これはListに対しても有効です。
Main.java
package app; import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) { List<String> list = new ArrayList<String>(); list.add("A"); list.add("B"); list.add("C"); // 拡張for文 for(String s : list) { System.out.println(s); } } }
結果
A B C
イテレータ
データの集合に対して繰り返し処理する仕組みとして、イテレータという仕組みがあります。
Main.java
package app; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class Main { public static void main(String[] args) { List<String> list = new ArrayList<String>(); list.add("A"); list.add("B"); list.add("C"); // list.iterator() によりIteratorのインスタンスを取得できる // hasNextメソッドで、次の要素があるかどうかをbooleanで返す for (Iterator<String> it = list.iterator(); it.hasNext();) { // nextメソッドで次の要素を取得する System.out.println(it.next()); } } }
イテレータはコレクションの要素に対して現在地のカーソルのようなものを持っているイメージです。
hasNextメソッドではカーソルの次の位置に対して要素があるかどうかをチェックし、nextメソッドではカーソルを移動してその要素を戻り値として返します。
オブジェクト指向におけるクラス設計の手法に「デザインパターン」と呼ばれるものがあります。
デザインパターンは、オブジェクト指向言語での開発でよく使用されるパターンをまとめたカタログのようなものです。
その中の一つに「イテレータパターン」と呼ばれるものがあります。
これは、特定の集合体に対して順番に処理していく仕組みを一般化したものです。
先ほどのIteratorインターフェースを使ってListの要素を順番よく処理した例は、このイテレータパターンがJavaのライブラリで実装されている例になります。
処理の内容としてはやっていることは拡張for文と同じですが、イテレータ書き方の方が難しく感じるのではないかと思います。
実は、拡張for文で処理を書くと、それはイテレータを使用した処理に置き換えられています。
拡張for文の方が簡単に書けるため、Listに対してイテレータを使用する機会はほとんどないと思いますが、中でこのような仕組みが使用されていることを知識として知っておくとよいでしょう。
ラッパークラスとオートボクシング
ジェネリクスのところで基本型に対応する参照型があるという説明をしました。
このようなクラスのことをラッパークラスといいます。
それぞれの基本型に対応するラッパークラスは以下になります。
byte :Byte short : Short int : Integer long : Long float : Float double : Double char : Character
基本型とラッパークラスは、型としては別の型なので、int型の値をInteger型に代入したり、逆にInteger型の値をint型に代入する場合、本来は変換が必要です。
基本型と参照型の変換には名前が付いており、基本型からラッパークラスへの変換を「ボクシング」ラッパークラスから基本型への変換を「アンボクシング」といいます。
古いJavaのバージョンではメソッドを使用して型変換をしていましたが、現在は自動的に変換してくれます。
これを「オートボクシング」といいます。
※内部的にはメソッドによって型変換するように、コンパイル時にソースコードが変換されています。
// int ⇒ Integer への変換 // ボクシング int num = 10; Integer num2 = Integer.valueOf(num); // Integer ⇒ int への変換 // アンボクシング Integer num3 = new Integer(10); int num4 = num3.intValue(); // オートボクシング int num5 = 10; Integer num6 = num5;
Map
Listの他によく使用されるコレクションの一つがMapです。
Mapはキーと値をセットでデータを保持する仕組みです。
Main.java
package app; import java.util.HashMap; import java.util.Map; public class Main { public static void main(String[] args) { // ジェネリクスで<キー, 値>として型を指定する Map <String, Integer> score = new HashMap<>(); // Mapではputメソッドを使用してキーと値を同時に格納する score.put("alice", 80); score.put("bob", 75); score.put("chris", 90); String s = "alice"; // キーが存在する場合はtrue if (score.containsKey(s)) { // キーを指定して値を取得する System.out.println("アリスの点数:" + score.get(s)); } } }
結果
アリスの点数:80
ここの例で使用した以外にも様々なメソッドが用意されています。
詳しくはJavaのAPIリファレンスを参照しながら調べてみてください。
Set
TODO
LinkedList
TODO
コレクションフレームワークのまとめ
- コレクションフレームワークはまとまったデータを扱うためのライブラリ
- List・Map・Setなどがある
- Listを使うと動的に要素数を増やすことができる
- Mapを使うとキーと値のセットの要素を扱うことができる
- ジェネリクスを使うことで安全な開発を行うことができる
- コレクションでは参照型の値しか扱うことができない
- 基本型に対応した参照型のクラスをラッパークラスという
- ラッパークラスと基本型の値は自動で変換(オートボクシング)が行われる
JUnit
テストとは
プログラム開発では、全てのプログラムのコンパイルが通った時点で終了するわけではありません。
コンパイルが正常に通っていても、実際に動かしてみた場合には実行時エラーで終了する可能性があります。
また、たとえエラーが発生しなかったとしても、意図していた通りに動くとは限りません。
例えば、計算を行うプログラムの場合、エラーが起きなかったとしても、計算結果が間違っていたら、正しいプログラムとは言えません。
このように、プログラムは作り終わった段階で完成といえる状態にはなっていません。
プログラムを作った後には、プログラムが意図したとおりに動くのかどうかを確認する作業が必要になります。
その作業がテスト(検証)作業です。
プログラムはテストが終わった後、初めて完成したといえます。
テストの種類
実際のシステム開発の中ではテストもいくつかの種類があります。
例えば、
などがあります。
単体テスト
ユニットテストとも言います。
メソッド単位のテスト、クラス単位でのテスト、画面単位のテストなどが単体テストにあたります。
簡単に言うと一つの機能単位でのテストです。
単体テストは一般的にはプログラミングの工程に含まれる場合が多いです。
結合テスト
一つ一つの機能を連動させて動かした場合に正常に動作するかどうかを確認するテストのことです。
例えば、
- ユーザーを新規登録
- 登録したユーザーでログイン
- サービスを利用
- ログアウト
のような一連の流れがうまくいくかどうかを確かめる作業です。
システムテスト
総合テストとも呼ばれます。
システムテストでは、作成したプログラムを実際のサーバー環境や実際のデータを使ってテストします。
よりユーザーが使用する環境に近い状態でテストし、動作に問題ないかを検証します。
パフォーマンスに問題がないかどうかや、障害が発生した場合のリカバリーなど、システムの機能以外の部分に関してのテストも行います。
運用テスト
運用テストでは、実際にプログラムを使用するユーザーに動かしてもらい、問題がないかを検証します。
JUnitとは
JUnitは、Javaプログラムの単体テストのためのフレームワークです。
テスティングフレームワークとも呼ばれます。
JUnitでは、テスト用のクラスを新たに作成し、テストしたいメソッドを検証する処理を書きます。
処理を書き終えたらJUnitのクラスを実行します。
実行結果がそのままテスト結果となります。
なぜテストをするためにわざわざプログラムを作成するのでしょうか。
JUnitなどのテスティングフレームワークを使用せずにテストを行う場合、作成したプログラムを実行し、実行結果が意図したものになったかどうかを確認します。
このようなテストの場合、いくつかの問題点があります。
テストの目的はプログラムの質を上げることであり、バグを発見することです。
そのため、テストを実施していると、NGが出てバグを発見することもあります
バグが出てきた場合、そのままではいけないので、プログラムを修正する必要があります。
プログラムを修正したら再度テストを実施するのですが、NGが出た個所だけを再テストして完了ではありません。
プログラムのある機能を修正すると、その修正によって他の機能にも影響が出ている可能性もあります。
それを検証するためには、今まで実施したテストも再度実施する必要が出てきます。
つまり、手動でテストを行っていた場合、プログラムの修正が発生するたびに何度の同じテストを手動で実施する必要があるため、とても手間がかかります。
JUnitは、プログラムでテストを書くことにより、テストプログラムの実行だけで再テストができます。
そのため、うまく活用すれば効率よくテストすることができます。
当然、JUnitだけで全てのテストを賄うことはできません。
画面のレイアウトなどは実際に起動して目視しなければ確かめることはできませんし、メソッドによっては性質上手動で確認する必要があるものもでてくるでしょう。
テスティングフレームワークにこだわり過ぎる必要ありませんが、使用できる場面では積極的に使用するとよいでしょう。
テスト仕様書とテストケース
TODO
Junitの使い方
ここではVS Codeでの通常のJavaプロジェクトにおいてJunitを使用する方法を説明します。
JUnitはJava標準のライブラリには含まれていないため、使用するにはライブラリを追加する必要があります。
JUnitを使用するには2つのライブラリ(jarファイル)が必要です。
下記のサイトから
junit.jar
と
hamcrest-core.jar
をダウンロードします。
バージョンは変更される可能性がありますが、私が試したときにはそれぞれ以下のバージョンのファイルでした。
junit-4.13-rc-2.jar
hamcrest-core-1.3.jar
https://github.com/junit-team/junit4/wiki/Download-and-Install
Javaのプロジェクトのフォルダの中に「lib」フォルダを作成します。
(srcやbinと同じ階層)
.classpathのファイルを開き、classpath要素の中に以下の内容を追加します。
pathの値はそれぞれのファイル名に合わせてください。
<classpathentry kind="lib" path="lib/junit-4.13-rc-2.jar"/> <classpathentry kind="lib" path="lib/hamcrest-core-1.3.jar"/>
内容が反映させて問題がなければテストクラスでテストの実行ができるようになります。
Mavenで
TODO
テストクラスの作成
まずはテスト対象となるクラスと、テストを実施するクラスを作成して結果を確認してみます。
テスト対象となるクラス
Utility.java
package app; public class Utility { // FizzBuzzメソッド // 3の倍数ならFizz、5の倍数ならBuzz、3の倍数かつ5の倍数ならFizzBuzz、それ以外は数値をそのまま返す public static String FizzBuzz(int i) { if (i % 15 == 0) { return "FizzBuzz"; } else if (i % 3 == 0) { return "Fizz"; } else if (i % 5 == 0) { return "Buzz"; } else { return String.valueOf(i); } } // 偶数かどうかを判断するメソッド public static boolean isEven(int n) { if(n % 2 == 0) { return true; } else { return false; } } }
テストクラス
UtilityTest.java
package test; import static org.junit.Assert.*; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; public class UtilityTest { // 実行開始時に最初に呼ばれる @BeforeClass public static void 始まり() { System.out.println("テスト開始"); } // テストメソッド実行前に呼ばれる @Before public void テストメソッド前() { System.out.println("テスト前"); } @Test public void FizzBuzz3の倍数() { String result = Utility.FizzBuzz(3); // 引数の2つの値が一致したら成功 assertEquals("Fizz", result); } @Test public void FizzBuzz5の倍数() { String result = Utility.FizzBuzz(5); // 失敗した場合は第一引数のメッセージを表示する assertEquals("エラー","Buzz", result); } @Test public void FizzBuzz15の倍数() { String result = Utility.FizzBuzz(15); assertEquals("FizzBuzz", result); } @Test public void FizzBuzz以外() { String result = Utility.FizzBuzz(17); assertEquals("17", result); } @Test public void Even2() { boolean result = Utility.isEven(2); // 引数の値がtrueの場合は成功 assertTrue(result); } @Test public void Even4() { boolean result = Utility.isEven(4); // 失敗の場合は第一引数のメッセージを出力 assertTrue("4がダメ", result); } @Test public void Even1() { boolean result = Utility.isEven(1); // 引数の値がfalseの場合はエラー assertFalse(result); } @Test public void Even3() { boolean result = Utility.isEven(3); // 失敗の場合は第一引数のメッセージを出力 assertFalse("3がダメ", result); } // テストメソッド実行後に呼ばれる @After public void テストメソッド後() { System.out.println("テスト後"); } // テスト終了後に最後に呼ばれる @AfterClass public static void 終わり() { System.out.println("テスト終了"); } }
結果
テスト開始 テスト前 テスト後 テスト前 テスト後 テスト前 テスト後 テスト前 テスト後 テスト前 テスト後 テスト前 テスト後 テスト前 テスト後 テスト前 テスト後 テスト終了
コンソールへの出力は、@BeforeClass、@Before、@After、@AfterClassのアノテーションがついたメソッドがそれぞれどのタイミングで実施されるのかを確認するためのもので、それ以外に特に意味はありません。
テストクラスの場合、重要なのはテスト結果です。
VS Codeを使用している場合は、テストを実行したら「Java Test Report」のタブが表示されます。
(表示されない場合は、画面下の×やチェックマークがついた「View test report」を押すと表示されます。)
ここにメソッドごとのテスト結果が表示されます。
テストクラスのクラス名
テストクラスも基本的にJavaのクラスなので、作成方法は通常のクラスと変わりません。
クラス名も任意に付けることができますが、テストクラスであることを示すためにクラス名の前か後ろに「Test」を付けるのが一般的です。
また、特定のクラスに対するテストクラスに場合、「テスト対象クラスのクラス名 + Test」とすると分かりやすくなります。
JUnitで使用できるアノテーション
テストクラスではメソッドにアノテーションを付けることで、テスト用のメソッドであることをお知らせします。
JUnitで使用できるアノテーションにはいくつかの種類がありますが、テスト用のメソッドには@Testのアノテーションを付けます。
通常は@Testが付くメソッドは、テストケースの分だけ作成します。
処理を実行する前に一度だけ実行したい処理があるのであれば、@BeforeClassアノテーションを使用します。
処理の最後に一度だけ実行したい処理があるのであれば、@AfterClassアノテーションを使用します。
@Testが書かれた処理の前後に毎回実施したい処理がある場合、@Beforeや@Afterを使用します。
JUnitで使用できるメソッド
JUnitでは、assertXxxxという形式のメソッドが多数用意されていて、それらを使うことでテストを実施します。
assertEqualsメソッドでは、引数に期待値と実行結果を渡し、一致していればテスト成功で、一致しなければ失敗となります。
様々なメソッドがあるのでテスト内容によって使い分けられるようにしておきましょう。
assertThat
TODO
日本語のメソッド名
上記のテストクラスのメソッド名で日本を使用しています。
実はメソッド名に日本語を使用することも可能です。
ただし、テスト用ではない通常のメソッドでは推奨されません。
テストクラスはプログラムのテストを行うためのプログラムなので、システムのリリース時には必要とされないクラスです。
そのため分かりやすさのためにメソッド名で日本語が使用されることがあります。
staticインポート
クラスフィールドやクラスメソッドの使用を楽にするためのインポート方法です。
importの宣言部で「import static org.junit.Assert.*;」という見慣れない記述があります。
ソースコードでは「import static」とありますが、名称としてはstaticインポートと呼びます。
staticインポートを使用すると、クラスフィールドやクラスメソッドにアクセスする際に、クラス名を省略することができいます。
staticインポートの使用例
Main.java
package app; import static java.lang.System.*; public class Main { public static void main(String[] args) { // Systemクラスのクラスフィールドであるoutをクラス名なしで使用できる out.println("Hello"); } }
Hello
テスト駆動開発
TODO
JUnitのまとめ
文字列を掘り下げる
ここまで何気なく使用してきた文字列(Stringクラス)について掘り下げます。
Stringのインスタンスのつくり方
当然のことながら、Stringはクラスです。
なので、String型の変数に値を代入した場合には、Stringクラスのインスタンスが作成されます。
Javaでは通常インスタンスを作成する際には「new」というキーワードが必要でした。
しかし、Stringではnewを使用しなくても文字列を作成することができます。
これは、Stringは頻繁に使用されるクラスなので、newをしなくても作成できるように特別扱いされているからです。
通常のクラスと同様に、newを使用してインスタンスを作成することも可能です。
Main.java
package app; public class Main { public static void main(String[] args) { // こっちがよく使用される文字列の作成 String str = "Hello"; // newして文字列を作成することもできる String str2 = new String("Hello"); } }
2つの作成方法の振る舞いの違い
文字列のインスタンスを作成するには、上で紹介した2つの方法があります。
どちらの方法で作成してもインスタンスの内容に違いはありませんが、インスタンスの作られ方に違いがあります。
次の例を確認してください。
Main.java
package app; public class Main { public static void main(String[] args) { String str1 = "Hello"; String str2 = "Hello"; if(str1 == str2) { System.out.println("同じ"); } else { System.out.println("異なる"); } String str3 = "World"; String str4 = new String("World"); if(str3 == str4) { System.out.println("同じ"); } else { System.out.println("異なる"); } } }
結果
同じ 異なる
結果を見ると、同じ処理をしているにも関わらず動作が異なることが分かります。
参照型の変数を==で比較した場合、左辺のインスタンスと右辺のインスタンスのアドレスが等しいかどうかを判断します。
値が等しいかどうかは通常関係ありません。
(値を比較したい場合はequalsメソッドを使用します。)
この例でstr1とstr2を==で比較して同じになっているということは、str1とstr2は同じインスタンスを指しているということです。
つまり、Stringのインスタンスをnewを使用せずに作成した場合、同じメソッド内で既に同じ文字列が作成されている場合、そのインスタンスを利用します。
そのため、==で比較しても同じという結果になります。
newを使用してインスタンスを作成した場合は無条件に新しいインスタンスが作成されるため、アドレスを比較しても異なる結果になります。
文字列の比較
上で説明したように、参照型を==で比較をした場合はインスタンスを格納しているアドレスが比較されます。
値を比較したい場合はequalsメソッドを使用します。
Main.java
package app; public class Main { public static void main(String[] args) { String str1 = "Hello"; // 1 if(str1.equals("Hello")) { System.out.println("同じ"); } else { System.out.println("異なる"); } // 2 if("Hello".equals(str1)) { System.out.println("同じ"); } else { System.out.println("異なる"); } } }
文字列の変数を文字列リテラルと比較する場合、1と2の2つの方法があります。
差はないように見えますが、変数の値によっては動作に差が出ます。
str1の値がnullだった場合、1の方法ではNullPointerExceptionが発生します。
一方で2の方法であれば、変数の値がnullだとしても、例外は発生しません。
つまり、1の方法に比べると2の方法が安全です。
開発プロジェクトによって書き方が決まる場合もあるので、書き方を選べない場合もあるかもしれませんが、知識として知っておくと良いでしょう。
StringBuilder
Stringのインスタンスは、イミュータブル(変更不可)です。
つまり、一度作成した後は変更することができないということです。
例を見ながら詳しく説明していきます。
Main.java
package app; public class Main { public static void main(String[] args) { String s = "abc"; s += "def"; System.out.println(s); } }
結果
abcdef
このプログラムでは、まず最初に文字列"abc"のインスタンスが作成されます。
その後、文字列結合で"def"という文字を結合していますが、これは"abc"という文字列に"def"という文字列がくっついたわけではありません。
"absdef"という新しい文字列のインスタンスが作成され、そのインスタンスの参照値がsに代入されます。
つまり、このプログラムでは"abc"と"abcdef"という2つの文字列インスタンスが作られるということです。
Stringのインスタンスは、イミュータブルとはつまりこういうことです。
一度作られたインスタンスは値を変えることができないので、文字列結合や文字列の加工をした場合、元の文字列が変更されるわけではなく、新しいインスタンスが作成されるということです。
先ほどの例では作成されるインスタンスは2つなので、Stringでも大した問題はありません。
次に以下の例を確認してください。
Main.java
package app; public class Main { public static void main(String[] args) { String s = ""; for(int i = 0; i < 100; i++) { s += String.valueOf(i); } System.out.println(s); } }
結果
0123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
この処理ではStringを使用数とループの回数分だけ文字列インスタンスが作成されてしまします。
インスタンスが作成されるときにはメモリの領域を確保する必要があるため、このようなプログラムは実行時に大きなコストがかかります。
このように文字列を何度も加工する処理が必要な場合、StringBuilderを使用することが推奨されています。
Main.java
package app; public class Main { public static void main(String[] args) { // StringBiolderのインスタンスの作成 StringBuilder sb = new StringBuilder(""); for(int i = 0; i < 100; i++) { // 文字列をくっつける場合はappendメソッドを使用する sb.append(String.valueOf(i)); } // Stringとして出力 System.out.println(sb.toString()); } }
結果
0123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
StringBuilderはStringクラスとは別のクラスなので、インスタンスを作成する際にはnewを使用する必要があります。
StringBuilderではappendメソッドを使用することで文字列を結合することができます。
StringBuilderではStringと違い、文字列を結合したときには元の文字列の最後尾にそのまま追加した文字がくっつく形となり、インスタンスの数は増えません。
文字列の結合を多く行うプログラムの場合、StringBuilderを使用することを検討しましょう。
Stringのまとめ
- StringはJavaで特別扱いされているのでnewなしでインスタンスを作成できる
- newを使った場合と使わない場合でインスタンスの作られ方の挙動が異なる
- 文字列の結合を多く使用するプログラムではStringBuilderを使用するのが良い
データベースアクセス
プログラミングを使って本格的なシステムを作成する場合、データベースの存在は欠かせません。
ここではJavaでデータベースへアクセスする方法について解説します。
ここでは、DBとSQLに関してはある程度学習済みの前提で話を進めます。
JDBCの準備
まず、JavaからDBへの接続を行うには、JDBCドライバと呼ばれるものが必要です。
JDBCドライバは簡単に言うとJavaでDBを操作するためのライブラリです。
実体はJarファイルなのですが、各DB製品のベンダーが用意している場合がほとんどなので、使用しているDB製品の開発元から入手しておきましょう。
ここでは、DBはPostgreSQLを使用するものとして話を進めます。
PostgreSQLをインストーラからインストールした場合、続けてJDBCのインストールもできますので、合わせてインストールしておきましょう。
インストールが完了したら、インストールしたフォルダの中にjdbcのファイルがダウンロードされているはずです。
DBのインストール時に導入していなくても
https://jdbc.postgresql.org/download.html
のサイトからダウンロードできます。
環境設定
ここではVS Code での環境設定について説明します。
まずはプロジェクトの中に「lib」フォルダを作成します。
(既にある場合はそのまま利用します。)
その中にJDBCであるjarファイルを配置します。
私の環境では「postgresql-42.2.5.jar」を配置しています。
配置出来たら、
.classpathファイルのclasspath要素の中に以下の内容を追記します。
<classpathentry kind="lib" path="lib/postgresql-42.2.5.jar"/>
Main.java
package app; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class Main { public static void main(String[] args) { Connection con = null; PreparedStatement stmt = null; try { // 1. JDBCドライバの読み込み Class.forName("org.postgresql.Driver"); // 2. DBへの接続の確立 // ホスト名、ポート、DB名、ユーザー名、パスワードを指定する con = DriverManager.getConnection("jdbc:postgresql://localhost:5432/testdb", "user", "password"); // 3. SQL文の作成 // DBで実行できる形のSQL。セミコロンは不要。 String sql = "SELECT * FROM users"; // 4. ステートメントを作成 // SQLの実行準備をします stmt = con.prepareStatement(sql); // 5. SQL実行 ResultSet rs = stmt.executeQuery(); System.out.println("user_id | user_name | e_mail"); // 結果の取得 while (rs.next()) { int id = rs.getInt("user_id"); String name = rs.getString("user_name"); String mail = rs.getString("e_mail"); System.out.println(id + " | " + name + " | " + mail); } } catch (Exception e) { e.printStackTrace(); } finally { // 6. ステートメントのclose if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } // 6. コネクションのクローズ if (con != null) { try { con.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }
結果
user_id | user_name | e_mail 1 | Alice | alice@xxx.com 2 | Bob | bob@xxx.com 3 | Cris | cris@xxx.com
結果はDBの環境によって異なります。
今回の例では、ローカルにpostgreSQLをインストールし「testdb」というDBを作成しています。
「user」というユーザー名と「password」というパスワードでログインできるようにしています。
「users」という3つのカラムを持つテーブルを作成し、3レコードが格納されています。
SQL実行までの手順
DBへの接続の手順としては以下のようになります。
- JDBCの読み込み
プログラム実行時に1度は実行する必要があります。
1度実行すれば、同じプログラム内で再度接続するときにはこの処理は不要です。 - DBへの接続
DBへ接続する場合はDriverManagerクラスのgetConnectionメソッドを使用します。
接続が成功した場合はConnectionクラスのインスタンスが取得できます。
イメージとしては、コマンドラインのDB接続ツール(postgreSQLの場合はpsql)でDBへ接続したようなイメージです。 - SQL文の作成 文字列でSQL文を作成します。
- ConnectionクラスのprepareStatementメソッドを使用してSQL文の実行準備をします。
これはコマンドラインツールでSQL文を入力した状態のイメージです。 - SQLの実行
SELECT文の場合はexecuteQueryメソッドを呼び出すことでSQL文が実行されます。
これはSQL文が入力されたコマンドラインツールでEnterキーを押してSQLを実行したイメージです。 - リソースの解放
DBの使用を終えた後は、ステートメントやコネクションのcolseメソッドを呼び出して、リソースを解放してあげる必要があります。
コネクションでのcloseメソッドは、コマンドラインツールを終了させるようなイメージです。
クローズ処理の必要性
DBに限らず、ファイル操作やネットワークに関する処理など、Javaのプログラムの外部のリソースを使用する処理を行う場合、処理の最後にcloseメソッドを使ってリソースを解放してあげる必要があります。
なぜそのような処理が必要になるのでしょうか。
上記のソースコードでは、finally句を書かずに、closeメソッドを実行しなかったとしても、同じ結果が得られます。
だとするとclose処理は不要なのでは、と思うかもしれません。
しかし、closeの処理を行わずに何度も同じプログラムを実行していると思わぬ不具合が生じます。
上記の例ではgetConnectionメソッドを使用した際に接続が確立されますが、closeメソッドを実行しなかった場合は、その接続が切断されずに残ったままになります。
その状態で同じプログラムを何度も動作させると、実行するたびに接続だけがどんどん増えていきます。
これは、プログラムを実行するたびにコマンドラインツールを新しく起動しているようなイメージです。
DBは設定で同時に接続できる数が決まっています。
つまり、close処理を行わずに接続数が増えていくと、じき接続数の上限に達して、新しい接続が確保できなくなります。
そうなると新しくプログラムを実行した場合にエラーが発生し、DBが利用することができなくなってしまいます。
このような自体になってしまわないように、忘れずにclose処理を行うことが大事です。
処理がうまくいったとしても、うまくいかず例外が発生したとしても、closeの処理は実施する必要があるので、通常はfinally句の中に記述します。
ResultSetの使い方
SELECT文を実行した結果はResultSetのオブジェクトをして格納されます。
ResultSetは構造としてはコレクションフレームワークのところで登場したイテレータに似ています。
オブジェクトの中にカーソルがあるイメージです。
インスタンスを取得した最初の状態ではどのレコードも指していませんが、nextメソッドを使用することで最初のレコードを指すようになります。
try-with-resources
クローズ処理を行うことの必要性は先にも述べましたが、毎回finallyの処理の中にcloseの処理を書くのはソースコードが冗長になりますし、実装し忘れてしまう可能性もあります。
そこでJavaではバージョンアップに伴ってcloseメソッドを書かなくても自動的にclose処理してくれるような仕組みが追加されました。
それがtry-with-resourcesです。
Main.java
package app; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class Main { public static void main(final String[] args) { try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException e1) { e1.printStackTrace(); return; } // try-with-resources try (Connection con = DriverManager.getConnection("jdbc:postgresql://localhost:5432/testdb", "user", "password"); PreparedStatement stmt = con.prepareStatement("SELECT * FROM users");) { // SQL実行 ResultSet rs = stmt.executeQuery(); System.out.println("user_id | user_name | e_mail"); // 結果の取得 while (rs.next()) { final int id = rs.getInt("user_id"); final String name = rs.getString("user_name"); final String mail = rs.getString("e_mail"); System.out.println(id + " | " + name + " | " + mail); } } catch (final SQLException e) { e.printStackTrace(); } // 自動的にcloseが呼び出されるためfinallyが不要になる } }
結果
user_id | user_name | e_mail 1 | Alice | alice@xxx.com 2 | Bob | bob@xxx.com 3 | Cris | cris@xxx.com
try-with-resourcesを使用する場合は、try句の後に括弧の中でリソースを取得する処理を書きます。
そうすることで自動的にclose処理が実行されます。
close処理が自動的に実行されるクラスはAutoCloseableまたはCloseableインターフェースを実装しているクラスに限ります。
更新処理
SQLでは、SELECT文でデータを取得してくるだけではなく、INSERT・UPDATE・DELETE等を使用していデータを更新する場合もあります。
更新の場合、SELECT文と使用するメソッドや戻り値の型が異なります。
Main.java
package app; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; public class Main { public static void main(final String[] args) { try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException e1) { e1.printStackTrace(); return; } // DELETE文 final String SQL = "DELETE FROM users WHERE user_id = 3"; try (Connection con = DriverManager.getConnection("jdbc:postgresql://localhost:5432/testdb", "user", "password"); PreparedStatement stmt = con.prepareStatement(SQL);) { // SQL実行 // 更新の場合はexecuteUpdateメソッド // 戻り値は更新件数 int result = stmt.executeUpdate(); System.out.println("削除件数" + result + "件"); } catch (final SQLException e) { e.printStackTrace(); } } }
結果
削除件数1件
SQLインジェクションとプレースホルダ
続いては入力値を元にレコードを検索するような処理を見てみましょう。
package app; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class Main { public static void main(final String[] args) { try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException e1) { e1.printStackTrace(); return; } String param = "Bob"; // 入力値を想定 // 入力された値で名前で検索する String sql = "SELECT * FROM users WHERE user_name = '" + param + "'"; // try-with-resources try (Connection con = DriverManager.getConnection("jdbc:postgresql://localhost:5432/testdb", "user", "password"); PreparedStatement stmt = con.prepareStatement(sql);) { // SQL実行 ResultSet rs = stmt.executeQuery(); while(rs.next()) { // 検索にヒットした人の情報を出力する System.out.println("id:" + rs.getInt("user_id") + " name:" + rs.getString("user_name") + " mail:" + rs.getString("e_mail")); } } catch (final SQLException e) { e.printStackTrace(); } } }
結果
id:2 name:Bob mail:bob@xxx.com
正常に結果を得ることができました。
続いて入力値を変更してみます。
package app; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class Main { public static void main(final String[] args) { try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException e1) { e1.printStackTrace(); return; } String param = "Bob' or '1' = '1"; // 入力値を想定 // 入力された値で名前で検索する String sql = "SELECT * FROM users WHERE user_name = '" + param + "'"; // try-with-resources try (Connection con = DriverManager.getConnection("jdbc:postgresql://localhost:5432/testdb", "user", "password"); PreparedStatement stmt = con.prepareStatement(sql);) { // SQL実行 ResultSet rs = stmt.executeQuery(); while(rs.next()) { // 検索にヒットした人の情報を出力する System.out.println("id:" + rs.getInt("user_id") + " name:" + rs.getString("user_name") + " mail:" + rs.getString("e_mail")); } } catch (final SQLException e) { e.printStackTrace(); } } }
結果
id:1 name:Alice mail:alice@xxx.com id:2 name:Bob mail:bob@xxx.com
おかしな結果となりました。
今回の例では、実行されるSQL文は
SELECT * FROM users WHERE user_name = 'Bob' or '1' = '1'
となります。
「'1' = '1'」は当然ながら必ずtrueになる処理です。
この条件がorでくっついてしまったため、結果的に全てのレコードが条件に合致する形となり、全件のデータが出力されます。
これはSQLインジェクションと呼ばれる攻撃手法です。
'(シングルクォーテーション)や;(セミコロン)など、SQL文の中で意味のある記号を混ぜ込むことで、本来意図しないSQLを実行し、不正にデータを取得したり、データを更新したりする攻撃です。
SQLインジェクションができるような状態だと、パスワードを知らなくても不正にログインすることができたり、データを削除したりすることが可能となってしまいます。
SQL文を実行するプログラムを作成するときにはSQLインジェクションの対策をしてあげる必要があります。
SQLインジェクションの対策には、プレースホルダと呼ばれるものを利用します。
Main.java
package app; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class Main { public static void main(final String[] args) { try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException e1) { e1.printStackTrace(); return; } String param = "Bob' or '1' = '1"; // 入力値を想定 // ?の部分をプレースホルダと呼ぶ // ここに後からパラメータをセットする String sql = "SELECT * FROM users WHERE user_name = ?"; // try-with-resources try (Connection con = DriverManager.getConnection("jdbc:postgresql://localhost:5432/testdb", "user", "password"); PreparedStatement stmt = con.prepareStatement(sql);) { // プレースホルダに値をセットする // 1は1番目という意味。複数のパラメータがある場合は順番にセットしていく stmt.setString(1, param); // SQL実行 ResultSet rs = stmt.executeQuery(); while(rs.next()) { // 検索にヒットした人の情報を出力する System.out.println("id:" + rs.getInt("user_id") + " name:" + rs.getString("user_name") + " mail:" + rs.getString("e_mail")); } } catch (final SQLException e) { e.printStackTrace(); } } }
実行結果は何も出力されません。
プレースホルダを利用すると、SQL文が解釈された後、?の部分に後からパラメータがセットされます。
そうすると、'や;も文字として解釈され、SQL文の'や;としての意味を持たなくなります。
そのためプレースホルダを使うことがSQLインジェクションの対策となります。
入力値をSQL文の一部として使用する場合には、必ずプレースホルダをするようにしましょう。
トランザクション
複数のSQL文を実行する場合、1つのトランザクションとして扱いたい場合もあります。
以下はJavaでトランザクション処理を行う方法です。
package app; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; public class Main { public static void main(final String[] args) { try { Class.forName("org.postgresql.Driver"); } catch (final ClassNotFoundException e1) { e1.printStackTrace(); return; } final String insert = "insert into users values(10, 'alice', 'zzz@gmail.com')"; // idというカラムはないため、実行に失敗する final String delete = "delete from users where id = 1"; // try-with-resources try (Connection con = DriverManager.getConnection("jdbc:postgresql://localhost:54321/testdb", "user", "password");) { // トランザクションの開始 con.setAutoCommit(false); // Insert文の実行 PreparedStatement stmt = con.prepareStatement(insert); stmt.executeUpdate(); // Delete文の実行 stmt = con.prepareStatement(delete); stmt.executeUpdate(); // ステートメントの終了 stmt.close(); // コミット con.commit(); } catch (final SQLException e) { e.printStackTrace(); } } }
トランザクションを開始するには、ConnectionクラスのsetAutoCommitメソッドを使用し、引数にfalseを指定します。
デフォルトではtrueになっており、その場合、SQL文を実行するたびに自動でコミットされます。
複数のSQL文が全て終わった段階でコミットしたい場合には、setAutoCommitメソッドでfalseを渡し、自動でコミットされないようにします。
コミットをするにはConnectionクラスのcommitメソッドを使用します。
ロールバックするにはrollbackメソッドを使用します。
DB接続処理のクラス設計
今までのサンプルでは、全て1つのメソッドの中にDBへの接続・SQL実行・close処理などをまとめて書いていました。
この程度のプログラムなら問題ありませんが、テーブルの数が増え、プログラムの規模が大きくなってくると、一つのメソッドの中に様々な処理を書くのは可読性やメンテナンス性が落ちてしまいます。
実は、Java(というよりもオブジェクト指向の言語)でDBを操作する処理を作成する場合によく使用されるクラス設計のパターンがあります。
それはDAOパターンと呼ばれる設計パターンです。
DAOとは、Data Access Object の略で、要はテーブルにアクセスするための専用のクラスを作成し、それ以外の処理をするクラスとは切り分ける設計方法です。
DAOをどんなクラスにするのかについては明確な答えはありませんが、よくあるパターンとしては、テーブル1つにつき、そのテーブルに対応したDAOクラスを作成します。
メソッドとしては、DML(SELECT・INSERT・UPDATE・DELETE)にそれぞれ対応したメソッドを作成し、必要に応じてその他のメソッドを実装します。
また、DAOとは別で、テーブルから取得したデータを保持するための専用のクラスを作ることも多いです。
このデータの入れ物として扱うクラスをEntityと呼んだりします。
EntityもDAOと同様、1つのテーブルに対して1つ作成することが多いです。
テーブルのカラムに対応したフィールドを持ち、そのフィールドに対するアクセッサメソッドを保持します。
Entityは、SELECT文を実行するメソッドの戻り値の型として使用されたり、更新系の処理を行うメソッドの引数の型として使用されます。
例
TODO
コネクションプーリング
Java等のシステムを動かす言語からDBへ接続する処理はそれなりに負荷のかかる処理です。
そのため、DB操作を行うたびに毎回接続と切断を繰り返すのは、サーバーに対して大きな負荷がかかります。
そこで、APサーバー(アプリケーションサーバー)では、起動したときにあらかじめDBへの接続をいくつか確保(プール)しておき、DBの処理を行うときはその中から接続を1つ借りて接続するという技術があります。
この技術のことをコネクションプーリングといいます。
コネクションプーリングが使える環境であれば是非とも使用するようにしましょう。
フレームワークを使用する場合はフレームワークの機能で使用されていることが多いです。
エラーについて
TODO
DB接続のまとめ
- JavaからDB接続の操作を行うには、JDBCが必要
- DB接続の処理を行う場合、最後にclose処理を行う必要がある
- try-with-resourcesを使うことで自動でclose処理が行われる
- SELECT文の実行はexecuteQueryメソッドを使用し、ResultSetで戻り値を受け取る
- 更新の場合はexecuteUpdateメソッドを使用し、intで更新件数を受け取る
- SQLインジェクション対策として入力値をパラメータとして使用する場合は、必ずプレースホルダーを利用する
- トランザクションを有効にするには、setAutoCommitメソッドを使用する
関数オブジェクト
ここでは関数オブジェクトについて学んでいきます。
そもそも関数とは何でしょう。
ほとんどの人は数学の授業でも関数という言葉を聞いたことがあるはずです。
ここでいう関数は数学での関数と同じ意味です。
改めて説明すると関数とは「何かの入力を受け取り、処理した結果を出力するもの」です。
数学では「y = 3x + 5」のような式を1次関数と呼びますが、これは、入力値としてxが決まると、結果としてyの値が一つだけ決まります。
このようなものを関数と呼びます。
実はこのような仕組みはJavaの中でも既に存在しています。
それは「メソッド」です。
メソッドは、引数として何らかの値を受け取り、受け取った値を使って処理をした結果を戻り値として返します。
(引数や戻り値がない場合もありますが)
つまり、メソッドは関数の一種になります。
ただし、厳密にはメソッド = 関数 ではありません。
メソッドと関数の違いは、名前が必須であるかどうかです。
今までメソッドを定義する場合、まずメソッド名を定義する必要がありました。
しかし、関数というのは、上記の定義を満たしていれば、名前は必須ではありません。
※実際、他のプログラミング言語、例えばJavaScriptでは、無名関数と呼ばれる、名前を定義せずに処理だけ定義する書き方ができます。
プログラミング言語で、プログラム実行中に生み出したり変数に代入できたりするものを第1級オブジェクトといいます。
Java 8 から、関数が第1級オブジェクトに加わりました。
つまり、関数を変数に代入したり、メソッドの引数で受け取ることができるようになりました。
ここまでのまとめ
- 関数とは「入力を受け取って、処理した結果を出力するもの」
- メソッドは関数の一部
- 関数は名前が必須ではない
- Java 8 からは、関数を変数に代入できるようになった
変数への関数の代入方法は以下になります。
型 変数名 = クラス名::静的メソッド名 型 変数名 = インスタンス名::インスタンスメソッド名
ここで示した変数の型は、インターフェースの型となります。
どんなインターフェースでもいいわけではなく、SAMインターフェースという特殊なインターフェースになります。
このとき変数に格納されるのは、メソッド本体ではなく、メソッドに対する参照です。
変数にインスタンスを代入したときには、インスタンスそのものではなく、そのインスタンスのアドレスが入るのと同じです。
SAMインターフェース
先の説明で出てきたSAMインターフェースとは、single-abstract-method interface の略です。
意味は、抽象メソッドを1つしか含まないインターフェースのことです。
インターフェース名の縛りはありません。
関数オブジェクトを変数や引数に格納する場合は、その型がSAMインターフェースである必要があります。
また、そのSAMインターフェースが持つ抽象メソッドの引数や戻り値が、格納したい関数オブジェクトと一致している必要があります。
Java 8から、java.util.functionパッケージのAPIとして、SAMインターフェースがいくつか用意されています。
IntBinaryOperator、IntToLongFunction などがあります。
詳しくはJavaのリファレンスを参照してください。
このパッケージの中に自分が扱いたい引数と戻り値のメソッドを持ったインターフェースがあれば、それを使用することができます。
用意されているものを使用しなくても、SAMインターフェースの条件を満たしていれば自作もできます。
ラムダ式
ラムダ式とは、Java 8から使用できるようになった関数をその場で作成するための構文のことです。
「->」という記号を使います。
ラムダ式の構文
(型 引数名1, 型 引数名2, ...) -> { 処理 return 戻り値; }
ラムダ式は関数をプログラムの実行中に必要になったタイミングで生み出して即時利用することができる仕組みです。
ラムダ式を使用する場合、事前にメソッドを定義しておく方法と比べて以下の違いがあります。
- 事前にメモリの領域を確保しなくて済む。
- JVMによって評価された時点で、関数が生成される。
メソッドの場合は、プログラムが実行されるタイミングで、必要なメソッドはメモリにロードされています。
一方でラムダ式の場合は、実行中の必要なタイミングでメモリにロードされます。
ラムダ式の中の注意点としては、その関数の外部にある変数を利用することもできるが、その変数の値を書き換えることはできないという点です。
以下はラムダ式の例です。
Main.java
package app; import java.util.function.IntToDoubleFunction; public class Main { public static void main(final String[] args) { // 文字列を受け取る // (String s) -> {return "Hello";} // 何も受け取らずに戻り値を返す // () -> {return true;} // int型を引数に受け取り、double型を返す関数をfuncに代入 // IntToDoubleFunctionはjava.util.functionに含まれるSAMインターフェース IntToDoubleFunction func = (int n) -> { return n / 2.0; }; System.out.println(func.applyAsDouble(10)); // ラムダ式の引数宣言では型を省略できる // SAMインターフェースから特定される func = (n) -> { return n / 3.0; }; System.out.println(func.applyAsDouble(10)); // 引数が一つの場合は括弧も省略可 func = n -> { return n / 4.0; }; System.out.println(func.applyAsDouble(10)); // 処理がreturnのみなら中かっことreturnも省略可能 func = n -> n / 5.0; System.out.println(func.applyAsDouble(10)); } }
結果
5.0 3.3333333333333335 2.5 2.0
高級関数
関数を引数に受け取る関数のことを高級関数といいます。
以下は高級関数を活用した例です。
CalcFunction.java
package app; // SAMインターフェース public interface CalcFunction { int call(int n, int m); }
CalcExtension.java
package app; public interface CalcExtension { // 高級関数 int calc(CalcFunction f, int n, int m); }
package app; public class Main { // クラスメソッド // 加算 public static int add(int n, int m) { return n + m; } // インスタンスメソッド // 減算 public int sub(int n, int m) { return n - m; } public static void main(final String[] args) { // クラスメソッドを変数に格納 CalcFunction func = Main::add; System.out.println(func.call(10, 20)); // インスタンスメソッドを変数に格納 Main ma = new Main(); CalcFunction func2 = ma::sub; System.out.println(func2.call(100, 20)); // ラムダ式で関数を作成する // CalcFunction func3 = (int n, int m) -> {return n * m;}; CalcFunction func3 = (n, m) -> {return n * m;}; // System.out.println(func3.call(10, 10)); // 高級関数 CalcExtension calcEx = (CalcFunction f, int n, int m) -> { return f.call(n, m);}; System.out.println(calcEx.calc(Main::add, 30, 20)); System.out.println(calcEx.calc(ma::sub, 30, 20)); System.out.println(calcEx.calc(func3, 30, 20)); } }
結果
30 80 100 50 10 600
ここまでのまとめ
- 関数とは「入力を受け取って、処理した結果を出力するもの」
- メソッドは関数の一部
- 関数は名前が必須ではない
- Java 8 からは、関数を変数に代入できるようになった
- 関数が代入できる変数の型は、SAMインターフェースの型(抽象メソッドが一つだけのメソッド)
- 代入できる関数は、SAMインターフェースのメソッドの引数と戻り値の型が同じメソッド
- SAMインターフェースは標準でもいくつか用意されているが、自作も可能
- ラムダ式とは、関数をその場で作成する構文とその仕組み。「->」。
- ラムダ式を使うことで、必要になったタイミングで関数がメモリにロードされるようになる
- 関数を引数に受け取る関数のことを、高級関数という
Stream API
Java 8 からStream APIと呼ばれる機能が追加されました。
これは具体的にはjava.util.stream.Streamのインスタンスのことです。
java.util.Collectionを実装している全てのクラスが、streamメソッドを持つようになり、このメソッドを使用することでStreamのインスタンスを得ることができます。
Streamは、コレクションの各要素に対して一括処理や集計などの処理を行う様座なメソッドを持っています。
Main.java
package app; import java.util.List; import java.util.ArrayList; public class Main { public static void main(String[] args) { List<Integer> list = new ArrayList<>(); list.add(78); list.add(95); list.add(80); list.add(72); list.add(80); // streamのインスタンスを取得 // 値が80以上の要素だけを取得 // stream型をintStream型に変換 // 要素の合計を取得 System.out.print("合計(stream):"); System.out.println(list.stream().filter(s -> s.intValue() >= 80).mapToInt(s -> s.intValue()).sum()); // 上記をstreamを使わないで実施したとすると int sum = 0; for (int n : list) { if (n >= 80) { sum += n; } } System.out.print("合計(ループ):"); System.out.println(sum); // 2の倍数の要素を取得 // それぞれの要素に2を掛ける // それぞれの要素にsysoutの処理を流す System.out.println("各要素を2倍にする"); list.stream().filter(n -> n % 2 == 0).map(n -> n * 2).forEach(System.out::println); // 上と同じ // list.stream().filter(n -> n % 2 == 0).map(n -> n * 2).forEach(n -> System.out.println(n)); // フィルターなしで全要素に処理を適用 System.out.println("全要素を出力"); list.stream().forEach(System.out::println); // 並列処理バージョン // マルチコアCPUを搭載しているマシンの場合は高速化が期待できる System.out.println("高速化"); list.parallelStream().filter(n -> n % 2 == 0).map(n -> n * 2).forEach(System.out::println); } }
合計(stream):255 合計(ループ):255 各要素を2倍にする 156 160 144 160 全要素を出力 78 95 80 72 80 高速化 160 160 144 156
java.util.streamパッケージには、Streamインターフェース以外にもIntStream, LongStream, DoubleStream などがあります。
StreamのmapToXXXメソッドで、それぞれのstreamに変換することができます。
それらのstreamで、集約のメソッド(sumなど)を持っています。
関数オブジェクトのまとめ
- 関数とは入力値を受け取って結果を返すもの
- メソッドも関数の一部だが、関数は名前が不要
- Java 8 から第一級オブジェクトとして関数が加わった
- 関数オブジェクトはSAMインターフェースに格納できる
- SAMインターフェースとは抽象メソッドを1つだけ持つインターフェース
- ラムダ式を使うことでメソッドを必要なタイミングで生み出すことができる
- 関数を引数に受け取る関数のことを高級関数という。SAMインターフェースのオブジェクトやラムダ式を渡すことができる
- Java 8 からStream APIが追加された
- Streamを利用するとコレクションの各要素に処理を施したり集計したりできる
JDKのライセンスについてのお話
ここではJavaのライセンスについての解説します。
Java言語はリリースされてから長い間、無償で使用することができました。
しかし、2018年からJavaの有償化についての話が話題になりました。
ここで一度ライセンスに関して整理しておきます。
まず、Javaによる開発を行うためにはJDKが必要でした。
実はJDKにはいくつかの種類があり、Javaで開発・運用を行う際、どのJDKを使用するかがポイントになります。
JDKには大きく分けて2つの種類があります。
「OpenJDK」と「OracleJDK」の2つです。
まずはこの2つの違いを説明します。
バージョン8以前の話
まず「Java 8」以前のOpenJDKとOracleJDKの違いについて説明します。
Javaの開発は基本的にOracle社が行っています。
OracleはOpenJDKとOracleJDKの両方を開発して提供しています。
OpenJDKの場合、ソースコードのみが提供されていました。
コンパイルされたファイル(バイナリファイル)は提供されていません。
ですのでOracleがリリースしたOpenJDKを利用する場合は、自分でソースコードを取得し、ビルドしてバイナリを作成する必要がありました。
一方、OracleJDKは、バイナリで提供されています。
OracleJDKの場合はOpenJDKとは逆でソースコードは公開されていません。
OracleJDKは、OpenJDKの機能にOracleが持っている機能やツール群を追加し、それが「OracleJDK」として提供されていました。
ライセンス形態はBCLライセンスです。
(無償で利用できて、複製・配布も可能)
そしてOracleは年に4回、Javaへのセキュリティパッチを適用しています。
この適用にはJDKのバグフィックスや、追加機能なども入ります。
適用の順序としては、まずOracleJDKに対してパッチを適用してリリースされます。
その後にOpenJDKに対しての適用が行われており、OracleJDKとOpenJDKで同期はとれていない状態でした。
それでもパッチの適用は行われていたため、無償でアップデートして最新の状態で利用することができました。
しかし、2019年の1月にJavaのJDK8のサポートが終了しました。
ここからは無償でのアップデートはできなくなりました。
バージョン9以降の話
Java 9 からはOpenJDKとOracleJDKが、両方ともバイナリとして提供されます。
まずOpenJDKを修正し、その後OpenJDKとOracleJDKのバイナリがリリースされます。
リリースされた後は、年に2回(3月と9月)にフューチャー・リリース(新機能の追加)が行われます。
フューチャーリリースでは、バージョンの番号が上がります。
つまり、Java 9 以降は、半年ごとにバージョンが上がっていく仕組みになります。
各フューチャー・アップデートに対して2回ずつアップデート・リリース(脆弱性対策)が行われます。
OpenJDKのライセンスはGPLです。
この説明だけ見ると無償でOpenJDKだけを利用しても問題なさそうですが、問題はアップデートの終了のタイミングです。
Javaの新バージョンがリリースされると、旧バージョンのOpenJDKはアップデートがされなくなります。
つまり、実質半年だけしかアップデートが行えません。
つまり、バグやセキュリティの問題があったとしても、更新ができなくなります。
同じバージョンを利用しながらアップデートを続けるには、OracleJDKの有償サポートを受ける必要があります。
また、商用でOracleJDKを利用する場合には、ライセンスが必須になるそうです。
もしOpenJDKを使用する場合、Javaのバージョンが上がるたびに(つまり半年ごとに)システムのJavaのバージョンも連動して上げていくような運用にすれば最新状態のJavaでシステムの運用は可能となります。
JDKは、Oracle以外のベンダーも提供しています。
Red HatやAWSなども。
JDKのまとめ
- JDKにはOpenJDKとOracleJDKがある。
- OpenJDKは無償。OracleJDKは有償。
- Java 9 以降、OpenJDKはリリース後半年間しかアップデートされない。
- OracleJDKは、長期で同じバージョンを使用する場合のアップデート、有償版のアップデート、メールやWebでの問い合わせが可能。
- OracleJDKのライセンスはマシンプラス使う人数分。
Java 9以降まとめ
- リリースは、フューチャー・リリース(年2回:3月と9月)とアップデート・リリースがある。
- アップデート・リリースはフューチャー・リリースに対して2回行われる。 つまり、フューチャー・リリースとアップデート・リリースを合わせて、年に6回リリースが行われる。
- これまでOracleが有償で提供していた機能の一部が、JDK11からは無償で利用できる。
- ライセンスはGPLに統一。
- 機能的にはOracleJDKとOpenJDKに差異はなくなった。
TODO
- 文字コード
- ファイル入出力