PostgreSQLのプロシージャで大量のテストデータを作成する
はじめに
PostgreSQLのPL/pgSQLという言語で作成するプロシージャで大量のテストデータを作成する方法を簡単に説明します。 テストや検証で大量データが必要なときに、この記事のソースコードを改修して利用することを想定しています。
環境
この記事の内容はWindows 10で作成、動作確認しています。
- OS: Windows 10
- PostgreSQL 12.2
参考
大量データを格納するテーブル
random_tbl
というテーブルを作成します。id以外のカラムはランダムな値で更新します。そのためカラム名にも意味は持たせていません。
CREATE TABLE random_tbl ( id BIGSERIAL, fld_int1 INTEGER NULL, fld_var2 VARCHAR(1000) NULL, fld_boo3 BOOLEAN NULL, fld_dat4 DATE NULL, fld_big5 BIGINT NULL PRIMARY KEY (id) );
テストデータを作成するプロシージャ
SET plpgsql.extra_warnings TO 'all'; SET plpgsql.extra_errors TO 'all'; DROP FUNCTION IF EXISTS gen_random_integer(); /** * 0から2147483647までの範囲でランダムな数値を返す */ CREATE OR REPLACE FUNCTION gen_random_integer() RETURNS INTEGER LANGUAGE plpgsql AS $$ BEGIN RETURN (RANDOM() * 2147483647)::INTEGER; END $$; DROP FUNCTION IF EXISTS gen_random_bigint(); /** * 0から9223372036854775807までの範囲でランダムな数値を返す */ CREATE OR REPLACE FUNCTION gen_random_bigint() RETURNS BIGINT LANGUAGE plpgsql AS $$ BEGIN RETURN (RANDOM() * 9223372036854775807)::BIGINT; END $$; DROP FUNCTION IF EXISTS gen_random_varchar(); /** * 200文字から1000文字のランダムな文字列を返す */ CREATE OR REPLACE FUNCTION gen_random_varchar() RETURNS VARCHAR LANGUAGE plpgsql AS $$ DECLARE v_rnd INTEGER := 0; v_tmp VARCHAR := ''; v_str VARCHAR := ''; BEGIN --1から5の乱数 v_rnd := (RANDOM() * 4)::INTEGER + 1; --1ループあたり200文字のランダムな文字列を生成する --ループ回数はランダム、1ループで200文字、5ループで1000文字 FOR i IN 1..v_rnd LOOP --code point 12353(ぁ) から 12435(ん)までのランダムな文字を100文字連結する SELECT STRING_AGG(CHR(12353 + (RANDOM() * 82)::INTEGER), '') INTO v_tmp FROM GENERATE_SERIES(1, 100); v_str := v_str || v_tmp; --code point 12449(ァ) から 12533(ヵ)までのランダムな文字を100文字連結する SELECT STRING_AGG(CHR(12449 + (RANDOM() * 84)::INTEGER), '') INTO v_tmp FROM GENERATE_SERIES(1, 100); v_str := v_str || v_tmp; END LOOP; RETURN v_str; END $$; DROP FUNCTION IF EXISTS gen_random_date(DATE, DATE); /** * パラメータv_date_from から v_date_toの範囲でランダムな日付を返す */ CREATE OR REPLACE FUNCTION gen_random_date(v_date_from DATE, v_date_to DATE) RETURNS DATE LANGUAGE plpgsql AS $$ DECLARE v_rnd INTEGER := 0; v_numOfDays INTEGER := v_date_to - v_date_from; BEGIN v_rnd := (RANDOM() * v_numOfDays)::INTEGER; RETURN v_date_from + v_rnd; END $$; DROP FUNCTION IF EXISTS gen_random_boolean(); /** * ランダムにtrue/falseを返す */ CREATE OR REPLACE FUNCTION gen_random_boolean() RETURNS BOOLEAN LANGUAGE plpgsql AS $$ DECLARE v_rnd INTEGER := 0; BEGIN v_rnd := (RANDOM() * 1)::INTEGER; IF v_rnd = 0 THEN RETURN FALSE; ELSE RETURN TRUE; END IF; END $$; DROP PROCEDURE IF EXISTS generate_random_data(INTEGER, INTEGER, DATE, DATE, BOOLEAN); /** * ランダムなデータを生成するメイン処理 */ CREATE OR REPLACE PROCEDURE generate_random_data( v_generate_num INTEGER, -- 生成件数 v_commit_num INTEGER DEFAULT 1000, -- コミットする件数 v_date_from DATE DEFAULT '1901-01-01', -- 生成する日付の範囲(開始) v_date_to DATE DEFAULT '2099-12-31', -- 生成する日付の範囲(終了) v_truncate BOOLEAN DEFAULT FALSE -- truncateするか ) LANGUAGE plpgsql AS $$ DECLARE v_seed DOUBLE PRECISION := TO_CHAR(CURRENT_TIMESTAMP, 'US')::INTEGER * 0.000001; v_row random_tbl%ROWTYPE; BEGIN RAISE NOTICE 'calling ''generate_random_data'' at %.', now(); RAISE NOTICE 'args generate_num:(%) commit_num:(%) date_from:(%) date_to:(%) truncate:(%)', v_generate_num, v_commit_num, v_date_from, v_date_to, v_truncate; IF v_date_from > v_date_to THEN RAISE EXCEPTION 'invalid date range from:(%) to:(%)', v_date_from, v_date_to USING HINT = 'check v_date_from or v_date_to parameter'; END IF; IF v_truncate = TRUE THEN RAISE NOTICE 'truncate table random_tbl'; TRUNCATE TABLE random_tbl; END IF; --RANDOM()のseedを設定 PERFORM SETSEED(v_seed); FOR i IN 1..v_generate_num LOOP v_row.fld_int1 := gen_random_integer(); v_row.fld_var2 := gen_random_varchar(); v_row.fld_boo3 := gen_random_boolean(); v_row.fld_dat4 := gen_random_date(v_date_from, v_date_to); v_row.fld_big5 := gen_random_bigint(); INSERT INTO random_tbl (fld_int1, fld_var2, fld_boo3, fld_dat4, fld_big5) VALUES (v_row.fld_int1, v_row.fld_var2, v_row.fld_boo3, v_row.fld_dat4, v_row.fld_big5); IF i % v_commit_num = 0 THEN RAISE NOTICE 'commit (%)', i; COMMIT; END IF; END LOOP; END $$;
プログラムの説明
ランダムな値を返すファンクション
PL/pgSQLではプロシージャ内にサブファンクションを定義することができません(それらしいことは可能です)。 なのでプロシージャとは別にファンクションを作成し、それらをプロシージャ内から呼ぶようにしています。
v_row.fld_int1 := gen_random_integer(); v_row.fld_var2 := gen_random_varchar(); v_row.fld_boo3 := gen_random_boolean(); v_row.fld_dat4 := gen_random_date(v_date_from, v_date_to); v_row.fld_big5 := gen_random_bigint();
RANDOM関数のシード
RANDOM()
関数のシードを設定するにはSETSEED(dp)
関数を実行しますが、PL/pgSQL内から実行するにはPERFORM
を使用します。
PERFORM SETSEED(v_seed);
プロシージャの作成
上記のソースコードをgenerate_random_data.sqlという名前のファイル保存した場合、コンパイルは下記のようになります。
PSQLでログインし\i
メタコマンドでソースファイルを指定します。
初回実行時はファンクションもプロシージャも存在しないので下記のようなメッセージが表示されますが、NOTICEレベルなので問題ありません。
> \i generate_random_data.sql SET SET psql:generate_random_data.sql:4: NOTICE: function gen_random_integer() does not exist, skipping DROP FUNCTION CREATE FUNCTION psql:generate_random_data.sql:17: NOTICE: function gen_random_bigint() does not exist, skipping DROP FUNCTION CREATE FUNCTION psql:generate_random_data.sql:30: NOTICE: function gen_random_varchar() does not exist, skipping DROP FUNCTION CREATE FUNCTION psql:generate_random_data.sql:61: NOTICE: function gen_random_date(date,date) does not exist, skipping DROP FUNCTION CREATE FUNCTION psql:generate_random_data.sql:78: NOTICE: function gen_random_boolean() does not exist, skipping DROP FUNCTION CREATE FUNCTION psql:generate_random_data.sql:98: NOTICE: procedure generate_random_data(pg_catalog.int4,pg_catalog.int4,date,date,pg_catalog.bool) does not exist, skipping DROP PROCEDURE CREATE PROCEDURE
2回目以降は
> \i generate_random_data.sql SET SET DROP FUNCTION CREATE FUNCTION DROP FUNCTION CREATE FUNCTION DROP FUNCTION CREATE FUNCTION DROP FUNCTION CREATE FUNCTION DROP FUNCTION CREATE FUNCTION DROP PROCEDURE CREATE PROCEDURE
のようになります。
プロシージャの実行
CALL
コマンドで実行します。
> call generate_random_data(1000000, 1000, '2020-01-01', '2020-12-31', TRUE);
引数
全部で5つの引数を取ります。
引数の位置 | 説明 | デフォルト値 |
---|---|---|
1 | 生成するレコード件数を指定 | なし |
2 | コミットする単位 | 1000 |
3 | ランダムに生成する日付の範囲(from) | '1901-01-01' |
4 | ランダムに生成する日付の範囲(to) | '2099-12-31' |
5 | 実行時にrandom_tblをtruncateするか | FALSE |
第1引数の生成するレコード件数以外はデフォルト値があるので、下記のようにも実行できます。
> call generate_random_data(100000);
プロシージャの実行時間を計測する
\timing
メタコマンドで実行時間を計測できます。
> \timing タイミングは on です。 > call generate_random_data(100000); // 省略 CALL 時間: 47202.196 ミリ秒(00:47.202)
Tips
ランダムな時刻を持つ日付を生成する
現在の日付にランダムな時刻を付加します。
DO $$ DECLARE v_interval INTERVAL := '0'; v_rnd_ts TIMESTAMP; BEGIN v_interval := (((RANDOM() * 86398)::INTEGER + 1)::VARCHAR)::INTERVAL; v_rnd_ts := CURRENT_DATE::TIMESTAMP + v_interval; RAISE NOTICE 'random timestamp=%', v_rnd_ts; END $$;
配列のインデックスを0から始める
PostgreSQLはデフォルトでは配列のインデックスは1から始まります。 たとえば、下記のように配列を宣言した場合
DO $$ DECLARE v_bool BOOLEAN[] := '{f, t}'; BEGIN RAISE NOTICE '1=%, 2=%', v_bool[1], v_bool[2]; END $$;
結果はこのようになります。
NOTICE: 1=f, 2=t
インデックスを0から始めるには下記のように宣言します。
DO $$ DECLARE v_bool BOOLEAN[] := '[0:1]={f, t}'; BEGIN RAISE NOTICE '0=%, 1=%', v_bool[0], v_bool[1]; END $$;
NOTICE: 0=f, 1=t
となります。
無名ブロック
PostgreSQLにはDO
という無名ブロックを実行するコマンドがあります。(標準SQLにDO
はありません。)
DO
コマンドで試したいコードを無名ブロックとして実行することが可能です。
以下のコードを実行するには、コピーしてPSQLのプロンプトへペーストするだけです。プロシージャやファンクションの作成は行われないので簡単にコードを試すことができます。
DO $$ DECLARE v_rnd INTEGER := 0; BEGIN PERFORM setseed(0.053); v_rnd := (RANDOM() * 10)::INTEGER; RAISE NOTICE '0=%', v_rnd; END $$;
『入門向け』PostgreSQLでシンプルなプロシージャを作成する
はじめに
この記事はPostgreSQLのPL/pgSQL
という言語で作成するプロシージャについて、難しい処理は行わないシンプルなプロシージャをベースに、それを少しずつ拡張しながらプロシージャの開発について説明していきます。
環境
この記事の内容はWindows 10で作成、動作確認しています。
- OS: Windows 10
- PostgreSQL 12.2
参考
シンプルなプロシージャ
最初に下記のシンプルなプロシージャで、プロシージャの構造について説明したいと思います。 このプロシージャは、実行するとコンソール画面に『calling 'sample_proc()' at 2020-07-16 13:04:27.652065+09』のようなメッセージを表示するだけです。
/* * 1) プロシージャの宣言 */ CREATE PROCEDURE sample_proc() -- 2) 記述言語を指定 LANGUAGE plpgsql -- 3) プロシージャ本体を囲む引用符を$$に指定 AS $$ /* 4) プロシージャ本体は BEGIN - ENDブロックで囲む */ BEGIN /* * 5) RAISE NOTICEはメッセージを標準出力に出力する */ RAISE NOTICE 'calling ''sample_proc()'' at %.', now(); END $$;
プロシージャの作成
プロシージャを実行するには、その前にCREATE PROCEDURE
コマンドでプロシージャを作成する必要があります。
上記のコードをsample_proc.sqlという名前のソースファイルに保存します。
次にpsqlでログインし、\i
メタコマンドでこのソースファイルを読み込んでプロシージャを作成します。
> \i sample_proc.sql CREATE PROCEDURE
コンソールにCREATE PROCEDURE
と表示されればプロシージャの作成は成功です。
メタコマンドとは
メタコマンドとはSQLコマンドとは違う、PostgreSQLに対して指示を行うコマンドです。
良く使うメタコマンドでは、テーブル、ビュー、シーケンスの一覧を表示する\d
、データベースの一覧を表示する\l
などがあります。
\i
メタコマンドは、指定したファイルを読み込んで、そのファイルに記述されているSQLコマンドを実行します。
プロシージャの構造
1) プロシージャの宣言
プロシージャを作成するにはCREATE PROCEDURE
コマンドを使用します。その後に続くsample_proc
がプロシージャ名です。
プロシージャが引数を取る場合は( )
に引数のリストを指定しますが、引数がなければ空の( )
のままです。
CREATE PROCEDURE sample_proc() ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^ | | | +--- プロシージャ名 | +-------------------- プロシージャを作成するコマンド
このソースファイルを実行してプロシージャを作成すると1度目は成功しますが、2度目は以下のエラーで失敗します。
psql:sample_proc.sql:15: ERROR: function "sample_proc" already exists with same argument types
このエラーメッセージの通り、すでにsample_proc
というプロシージャが作成されているため、2回目の作成がエラーになります。
この場合は、ソースコードの先頭にプロシージャを削除するDROP PROCEDURE
コマンドを追加すると、sample_proc
というプロシージャが作成されていれば削除し、次にプロシージャの作成が行われるのでこのエラーを回避できます。
DROP PROCEDURE IF EXISTS sample_proc(); /* * 1) プロシージャの宣言 */ CREATE PROCEDURE sample_proc() //...省略...
なお、この書き方の他にCREATE OR REPLACE
を使う方法もあります。この記事ではこちらの方法でコードを書きます。
/* * 1) プロシージャの宣言 */ CREATE OR REPLACE PROCEDURE sample_proc() //...省略...
2) 記述言語
プロシージャの記述言語を指定します。PL/pgSQLの場合はplpgsql
とします。
3) プロシージャ本体の引用符
プロシージャ本体のコードは文字列リテラルとして記述します。通常文字列リテラルは単一引用符('
)で囲みますが、単一引用符だとソースコードの記述に不便なことがある(シングルクォートやバックスラッシュのエスケープ)ので慣習的に$$
を指定することが多いようです。
4) プロシージャ本体
BEGINブロック
プロシージャのコードは下記のようにBEGIN - ENDブロックで定義します。なおこのBEGIN
というキーワードはブロック構造を定義するためのもので、トランザクションの開始を表すBEGIN
とは別のものです。
なのでBEGIN
ブロックの開始はトランザクションの開始を意味しません。
BEGIN プロシージャのコード END
サブブロック
BEGINブロックはネストすることができます。下記はプロシージャ本体のBEGINブロックに、2つのサブブロックが含まれているコードを表しています。 なお、サブブロックの終了を示すENDにはセミコロンが必要ですが、プロシージャ本体を定義するENDのセミコロンは省略できます。
BEGIN BEGIN サブブロック END; BEGIN サブブロック END; END
5) メッセージの出力
簡単にメッセージ出力の方法に触れておきます。PL/pgSQLではメッセージ出力にRAISE <level>
構文を使用します。
基本的なRAISEの構文は下記のようになります。level
は省略可能で省略した場合はEXCEPTION
になります。'format'
にはメッセージを文字列リテラルで記述します。メッセージに式の結果や変数の値を埋め込みたい場合は%
というプレースフォルダを使用します。
'format'
の後にカンマ区切りでプレースフォルダに埋め込みたい値を記述します。
RAISE [level] 'format' [, expression [, ...] ]
この例ではNOTICE
というレベルでメッセージを出力し、%の位置にnow()
の結果を埋め込んでいます。
RAISE NOTICE 'calling ''sample_proc()'' at %.', now();
指定できるレベルには下記のものがあります。
- DEBUG
- LOG
- INFO
- NOTICE
- WARNING
- EXCEPTION (デフォルト)
レベルを省略した場合のデフォルトはEXCEPTION
ですが、このレベルを指定するとエラーを発生させトランザクションを失敗させます。
EXCEPTION
以外のレベルを指定した場合、優先度の異なるメッセージがクライアント(標準出力)やサーバーログに出力されます。
6) コメント
ブロックコメントは /* */
で囲みます。
1行コメントは、--
で始めます。
/* * ブロックコメント */ -- 一行コメント
プロシージャの実行方法
call
コマンドでプロシージャを実行します。
> call sample_proc(); NOTICE: calling 'sample_proc()' at 2020-07-16 13:04:27.652065+09. CALL
プロシージャの削除
DROP PROCEDURE
コマンドでプロシージャを削除します。
> drop procedure sample_proc; DROP PROCEDURE
シンプルなプロシージャに少し手を加えてみる
次に上記の例で扱ったプロシージャに、引数で指定された値をテーブルに追加するという処理を追加してみます。 ここで扱うテーブルは下記のmemoという名前のテーブルです。
CREATE TABLE memo ( id BIGSERIAL, title VARCHAR(1000) NOT NULL, description TEXT NULL, done BOOLEAN NULL, create_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, update_at TIMESTAMP WITHOUT TIME ZONE NULL, PRIMARY KEY (id) );
引数を取る
memoテーブルに追加する値を引数で取るようにします。引数はプロシージャ名の後に続く()
の中に引数名とそのデータ型をペアで列挙します。
この例では引数名にv_
という接頭辞を付けて定義しています
CREATE OR REPLACE PROCEDURE sample_proc( v_title VARCHAR, v_description TEXT, v_done BOOLEAN)
プロシージャを実行するときは
> call sample_proc('new memo title', 'new memo description', false);
のようにして引数を与えます。
引数にデフォルト値を指定する
引数を省略した場合、デフォルト値を適用するにはDEFAULT
を使用します。
たとえばプロシージャの実行時に3番目の引数(v_done
)が省略された場合、デフォルト値をfalse
とするには下記のように記述します。
CREATE OR REPLACE PROCEDURE sample_proc( v_title VARCHAR, v_description TEXT, v_done BOOLEAN DEFAULT false)
3番目の引数は省略可能なので、以下のようにもプロシージャを実行することができます。この場合引数v_done
の値はfalse
になります。
> call sample_proc('new memo title', 'new memo description');
テーブルにデータを追加する
データを追加するにはINSERT文を使いますが、プロシージャではSQL文を直接扱えるので下記のようにINSERT文を記述できます。
CREATE OR REPLACE PROCEDURE sample_proc( v_title VARCHAR, v_description TEXT, v_done BOOLEAN DEFAULT false) LANGUAGE plpgsql AS $$ BEGIN RAISE NOTICE 'calling ''sample_proc()'' at %.', now(); INSERT INTO memo (title, description, done) VALUES (v_title, v_description, v_done); END $$;
この時点でソースファイルからプロシージャを作成しなおして実行してみます。
> \i sample_proc.sql CREATE PROCEDURE > call sample_proc('memo title', 'memo description', false); NOTICE: calling 'sample_proc()' at 2020-07-16 17:03:01.048974+09. CALL
psqlなどでデータが追加されたか確認してみます。
> select * from memo; id | title | description | done | create_at | update_at ----+----------------+-------------------------------------+------+----------------------------+----------- 1 | memo title | memo description | f | 2020-07-16 17:03:01.048974 | (1 行)
変数を宣言してみる
BEGINブロックにはDECLARE
句という変数を宣言できる宣言部があり、ここで宣言した変数はプロシージャ本体で使用することができます。
この例ではv_last_id
というINTEGER型の変数を宣言し、memoテーブルにデータを追加した際に発番されたIDを格納するために使用しています。
DECLARE -- 変数の宣言 v_last_id INTEGER := 0; BEGIN RAISE NOTICE 'calling ''sample_proc()'' at %.', now(); INSERT INTO memo (title, description, done) VALUES (v_title, v_description, v_done) RETURNING id INTO v_last_id; RAISE NOTICE 'new memo id(%)', v_last_id; END;
このコードでプロシージャを再作成し実行すると
> call sample_proc('memo title', 'memo description', false); NOTICE: calling 'sample_proc()' at 2020-07-16 17:21:51.449391+09. NOTICE: new memo id(2) CALL
のように出力されます。
型のコピー
v_last_id
の変数宣言の部分は%TYPE
を使用して型のコピーをするとテーブル定義の変更に強いコードになります。
下記はmemoテーブルのidカラムと同じ型でv_last_id変数を宣言しています。
v_last_id memo.id%TYPE := 0;
%TYPE
は引数の型にも使用できるので下記のように書き直すことができます。
CREATE OR REPLACE PROCEDURE sample_proc( v_title memo.title%TYPE, v_description memo.description%TYPE, v_done memo.done%TYPE DEFAULT false)
この状態でプロシージャを再作成すると
> \i sample_proc.sql psql:sample_proc.sql:42: NOTICE: type reference memo.title%TYPE converted to character varying psql:sample_proc.sql:42: NOTICE: type reference memo.description%TYPE converted to text psql:sample_proc.sql:42: NOTICE: type reference memo.done%TYPE converted to boolean CREATE PROCEDURE
のメッセージが表示されます。
行型変数を使ってみる
%ROWTYPE
を使って行型変数を宣言することができます。
下記はmemoテーブルの行と同じ構造を持つ行型変数を宣言しています。
v_memo memo%ROWTYPE;
行型変数を使うとSELECTの結果を行型変数へ代入することができます。
SELECT * INTO v_memo FROM memo WHERE id = v_last_id;
この時点でのソースコード
ここまでの内容を反映したソースコードです。
CREATE OR REPLACE PROCEDURE sample_proc( v_title memo.title%TYPE, v_description memo.description%TYPE, v_done memo.done%TYPE DEFAULT false) LANGUAGE plpgsql AS $$ DECLARE v_last_id memo.id%TYPE := 0; v_memo memo%ROWTYPE; BEGIN RAISE NOTICE 'calling ''sample_proc()'' at %.', now(); INSERT INTO memo (title, description, done) VALUES (v_title, v_description, v_done) RETURNING id INTO v_last_id; RAISE NOTICE 'new memo id(%)', v_last_id; SELECT * INTO v_memo FROM memo WHERE id = v_last_id; RAISE NOTICE 'new memo title=%, description=%', v_memo.title, v_memo.description; END $$;
エラーを捕捉する処理を加える
プロシージャの処理中にエラーが起きた場合、そのエラーを捕捉して何らかの処理を行いたい場合はBEGINブロックのEXCEPTION
句に、エラーの捕捉条件と行いたい処理を記述します。
エラーの捕捉条件はWHEN {エラー条件} THEN
のように記述します。{エラー条件}
の部分には予め決められたエラーコード若しくは条件名を指定します。
どのようなエラーコード、条件名が定義されているかは付録A PostgreSQLエラーコードに一覧が記載されています。
下記は0除算時に発生するエラーdivision_by_zero
を捕捉し、メッセージを表示する例です。
BEGIN -- 0除算を行う処理 EXCEPTION WHEN division_by_zero THEN RAISE NOTICE 'caught division_by_zero'; END;
複数のエラーを捕捉する
エラーの捕捉条件は下記のように複数記述することができます。
BEGIN -- 何らかのエラーが起きる処理 EXCEPTION WHEN no_data_found THEN -- エラー後処理A WHEN too_many_rows THEN -- エラー後処理B END;
上記の例ではno_data_found
とtoo_many_rows
とで、別々のエラー後処理を行っていますが、同じエラー後処理で対応したい場合はOR
でつなげます。
BEGIN -- 何らかのエラーが起きる処理 EXCEPTION WHEN no_data_found OR too_many_rows THEN -- エラー後処理 END;
OTHERS
OTHERS
という特殊なエラー条件があります。このエラー条件はQUERY_CANCELED
とASSERT_FAILURE
を除く全てのエラーに合致します。
なので何らかのエラーを捕捉したいといった場合は下記のように書けます。
BEGIN -- 何らかのエラーが起きる処理 EXCEPTION WHEN OTHERS THEN -- エラー後処理 END;
エラーを捕捉しなかった場合
プロシージャの処理中にエラーが起きた場合、
- エラーを捕捉する
EXCEPTION
句を書いていない WHEN {エラー条件} THEN
で指定したエラー条件が合致しなかった
などのときはプロシージャの処理は中断し、呼び出し元のトランザクションも中断します。
この時点でのソースコード
エラーの捕捉を反映したソースコードです。
CREATE OR REPLACE PROCEDURE sample_proc( v_title memo.title%TYPE, v_description memo.description%TYPE, v_done memo.done%TYPE DEFAULT false) LANGUAGE plpgsql AS $$ DECLARE v_last_id memo.id%TYPE := 0; BEGIN RAISE NOTICE 'calling ''sample_proc()'' at %.', now(); BEGIN INSERT INTO memo (title, description, done) VALUES (v_title, v_description, v_done) RETURNING id INTO v_last_id; RAISE NOTICE 'new memo id(%)', v_last_id; EXCEPTION WHEN not_null_violation THEN RAISE EXCEPTION 'title column must not null'; END; DECLARE v_memo memo%ROWTYPE; BEGIN SELECT * INTO v_memo FROM memo WHERE id = v_last_id; RAISE NOTICE 'new memo title=%, description=%', v_memo.title, v_memo.description; EXCEPTION WHEN no_data_found THEN RAISE WARNING 'not found memo id(%)', v_last_id; END; END $$;
以上でシンプルなプロシージャの作成方法の説明は終わりです。
補足
追加チェック
PL/pgSQLにはソースコードの追加チェックを行う機能があります。
開発時やテスト時にはplpgsql.extra_warnings
とplpgsql.extra_errors
変数の値をall
にすることが推奨されています。
この変数を有効にすることで、警告やエラーとなる箇所があればプロシージャのコンパイル時や実行時にメッセージが出力されるようになります。
SET plpgsql.extra_warnings TO 'all'; SET plpgsql.extra_errors TO 'all';
これらの変数には
none
(デフォルト)all
shadowed_variables
strict_multi_assignment
too_many_rows
の値を指定することができます。all
は下3つをすべて指定することと同じ意味になります。
たとえば、プロシージャの下記の行を
SELECT * INTO v_memo FROM memo WHERE id = v_last_id;
このように修正して、実行してみます。(memoテーブルには複数行データが格納されています)
SELECT * INTO v_memo FROM memo;
実行結果の通りエラーは起きませんが、追加したデータとは違うデータが表示されています。
> \i sample_proc.sql CREATE PROCEDURE > call sample_proc('CCCCCC', 'DDDDDDDDDD'); NOTICE: calling 'sample_proc()' at 2020-07-17 15:50:11.70216+09. NOTICE: new memo id(6) NOTICE: new memo title=AAAAAA, description=BBBBBBBBBB CALL
ソースファイルの先頭にこの2行を追加して、再度実行してみます。
SET plpgsql.extra_warnings TO 'all'; SET plpgsql.extra_errors TO 'all';
このようにエラーが起きるようになりました。
> \i sample_proc.sql CREATE PROCEDURE > call sample_proc('EEEEEE', 'FFFFFFFFFF'); NOTICE: calling 'sample_proc()' at 2020-07-17 15:58:38.629452+09. NOTICE: new memo id(7) ERROR: query returned more than one row HINT: Make sure the query returns a single row, or use LIMIT 1. CONTEXT: PL/pgSQL function sample_proc(character varying,text,boolean) line 22 at SQL statement
作成したプロシージャを確認する
プロシージャの実行時にエラーが起きると、どの行でエラーが起きたかがエラーメッセージに含まれることがあります。
ERROR: query returned more than one row HINT: Make sure the query returns a single row, or use LIMIT 1. CONTEXT: PL/pgSQL function sample_proc(character varying,text,boolean) line 22 at SQL statement
しかし、この行番号(line 22 at SQL statement
)はコンパイル後の行番号なので手元のソースファイルを見ても位置を特定しにくいです。
この場合は\sf
メタコマンドを使用して作成したプロシージャを確認することができます。
プロシージャが引数を取る場合は、プロシージャ名だけでなく引数の型のリストも必要です。
react_db=> \sf sample_proc(varchar,text,boolean) CREATE OR REPLACE PROCEDURE public.sample_proc(v_title character varying, v_description text, v_done boolean DEFAULT false) LANGUAGE plpgsql AS $procedure$ DECLARE v_last_id memo.id%TYPE := 0; -- 4) 関数本体は BEGIN - ENDブロックで囲む BEGIN /* * 5) RAISE NOTICEはメッセージを標準出力に出力する */ RAISE NOTICE 'calling ''sample_proc()'' at %.', now(); BEGIN INSERT INTO memo (title, description, done) VALUES (v_title, v_description, v_done) RETURNING id INTO v_last_id; RAISE NOTICE 'new memo id(%)', v_last_id; EXCEPTION WHEN not_null_violation THEN RAISE EXCEPTION 'title column must not null'; END; DECLARE v_memo memo%ROWTYPE; BEGIN SELECT * INTO v_memo FROM memo /*WHERE id = v_last_id*/; RAISE NOTICE 'new memo title=%, description=%', v_memo.title, v_memo.description; EXCEPTION WHEN no_data_found THEN RAISE WARNING 'not found memo id(%)', v_last_id; END; END $procedure$
メタコマンドに+
を付けると行番号が付加されます。
これで22行目がどこかを確認することができます。
> \sf+ sample_proc(varchar,text,boolean) CREATE OR REPLACE PROCEDURE public.sample_proc(v_title character varying, v_description text, v_done boolean DEFAULT false) LANGUAGE plpgsql 1 AS $procedure$ 2 DECLARE 3 v_last_id memo.id%TYPE := 0; 4 -- 4) 関数本体は BEGIN - ENDブロックで囲む 5 BEGIN 6 /* 7 * 5) RAISE NOTICEはメッセージを標準出力に出力する 8 */ 9 RAISE NOTICE 'calling ''sample_proc()'' at %.', now(); 10 11 BEGIN 12 INSERT INTO memo (title, description, done) VALUES (v_title, v_description, v_done) RETURNING id INTO v_last_id; 13 RAISE NOTICE 'new memo id(%)', v_last_id; 14 EXCEPTION 15 WHEN not_null_violation THEN 16 RAISE EXCEPTION 'title column must not null'; 17 END; 18 19 DECLARE 20 v_memo memo%ROWTYPE; 21 BEGIN 22 SELECT * INTO v_memo FROM memo /*WHERE id = v_last_id*/; 23 RAISE NOTICE 'new memo title=%, description=%', v_memo.title, v_memo.description; 24 EXCEPTION 25 WHEN no_data_found THEN 26 RAISE WARNING 'not found memo id(%)', v_last_id; 27 END; 28 29 END 30 $procedure$
『入門向け』VSCodeでJavaScriptの学習環境を構築する
はじめに
この記事は、JavaScriptの基礎部分は既に入門書籍や他の入門サイトで学習中で、これから手を動かしてコーディングしながら知識を深めたいという人向けに作成しました。 コードエディターにはVSCodeを利用しますので、VSCodeの使い方を知りたいという人にもおすすめです。 なお、スクリーンショットを多用していますが画面やメニューの内容は2020年5月時点のバージョンのものです。今後バージョンアップによって変わることがありますので予めご了承ください。
環境
この記事の内容はWindows 10で作成、動作確認しています。MacOSユーザーの方は適宜内容を読み替えてください(特にショートカットキーやフォルダーパス)。
VSCode (Visual Studio Code)のインストール
JavaScriptの学習環境にVSCodeを利用するため、まだインストールされていない方は下記の内容を参考にインストールしてください。すでにインストール済みの方は”拡張機能のインストール”まで読み飛ばしてください。
VSCodeとは
VSCode (Visual Studio Code)とは、マイクロソフトが開発している無料で使える軽量コードエディターです。動作が軽いうえに高機能で細かいカスタマイズが可能な点などが評価されて多くの利用者がいます。 JavaScriptのみならずhtml/cssのコーディングに最適なコードエディターなので、本記事でもこれを使って説明します。
ダウンロード
マイクロソフトのダウンロードサイトにアクセスしてインストーラーをダウンロードします。
Windows版のインストーラーはUser Installer
、System Installer
、zip
の3種類あります。
User Installer
はインストールに管理者権限が不要System Installer
はインストールに管理者権限が必要zip
はZipアーカイブを自身で展開して手動インストールするタイプ
この記事ではUser Installerの64bit版を選択しました。
インストール
2020年5月時点のWindows版のダウンロードファイル名はVSCodeUserSetup-x64-1.44.2.exe
です。ダウンロードしたインストーラーを実行してインストールします。
基本的にはインストーラーのデフォルト設定のままインストールすれば問題ありません。追加タスクの選択画面(Fig.6)でいくつか設定ができますが、ここはお好みで設定してください。
インストールが完了するとWelcome画面(Fig.9)が立ち上がります。
バージョンの確認
メニューバーのHelp
→ About
でバージョンを確認できます。バージョンを確認するとFig.10の通りVersion 1.44.2 (user setup)
がインストールされたことがわかります。
拡張機能のインストール
JavaScriptの学習に入る前にいくつか定番の拡張機能をインストールしておきます。 左側のサイドメニューの一番下のアイコンをクリックします。Fig11は拡張機能を管理するメニューで、マーケットプレイスから拡張機能を検索したりインストールすることや、インストールされている拡張機能やアンインストールすることができます。
拡張機能はVSCodeの大きな特徴の1つです。拡張機能をインストールすることでVSCodeに新しい機能を追加することができます。どのような拡張機能があるかはマーケットプレイスというサービスで確認できます。
マーケットプレイスにアクセスすると、定番のものや最近人気が出たもの、新しく追加されたものなどを探せます。
Japanese Language Pack for Visual Studio Code
1つ目はメニューやメッセージを日本語化する拡張機能です。
検索フィールドに"japanese"と入力してEnterを押すと、検索結果が表示されます。この中から"Japanese Language Pack for Visual Studio Code"という拡張機能をインストールします。 インストールはFig.12の"Install"という緑色のラベルをクリックするだけです。
インストールすると再起動を促されるので再起動します。Fig.13が再起動後のWelcome画面でメニューが日本語化されています。(この画面下の"起動時にウェルカムページを表示"のチェックを外してください)
次にインストールするのはAIを活用したIntelliSenseの機能が利用できる拡張機能です。コード補完で表示される候補がAIによって最適化されます。
インストールするとローカルPCで開発用途の簡易HTTPサーバーを起動することができ、コーディング中のhtmlやJavaScriptなどの動作確認が簡単にできるようになります。 またホットリロードというファイルの変更を検知してブラウザ上のページを自動的にリロードする機能もあります。
とりあえず現時点ではこの3つだけインストールします。 これでVSCodeと拡張機能のインストールは完了です。
JavaScript学習用のプロジェクトを作成
学習環境が整ったので、以降はJavaScriptの学習に関する内容になります。 まずはVSCodeでソースコードを管理できるようにプロジェクトを作成します。といってもソースコードを保存するフォルダーを作成して、そこにhtmlファイルとjsファイルを作成するだけです。
プロジェクトフォルダーの作成
この記事ではexercise-js
というフォルダーを作成しました。(作成場所やフォルダー名は任意です)
以後はこの場所にプロジェクトフォルダーがあるという前提で説明を行いますので、任意の場所にフォルダーを作成したい場合は適宜読み替えてください。
Fig.16はVSCodeでexercise-js
を開いた直後の画面です。
htmlファイルの作成
JavaScriptの動作確認はブラウザ(この記事ではChromeを使用します)で行いますので、そのためのhtmlファイルを作成します。 Fig.17の"新しいファイル"を作成するアイコンをクリックし、ファイル名にindex.htmlと入力してhtmlファイルを作成します。
空のhtmlファイルがエディタ画面に表示されるのでFig.18のようにhtml
と入力し、候補からhtml:5
を選択してEnterを押すとテンプレートのコードが展開されます。
titleタグを下記のように修正します。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>exercise-js</title> </head> <body> </body> </html>
次にFig.19のようにbodyタグの中でdiv#app
と入力してEnterを押すとコードが展開されます。さらにdivタグの中でh1
と入力してEnterを押すとh1タグが展開されます。
このhtmlタグの展開はemmetという機能です(emmetについては後述します)。
この状態でLive Serverを立ち上げてブラウザでindex.htmlを表示してみます。
エディタ画面上で右クリックしメニューからOpen with Live Server
をクリックします。
ブラウザが起動し、Fig.21の画面が表示されたと思います。
続いて下記のようにh1タグの下にdivタグを追加してファイルを保存すると、自動的にページがリロードされて表示が変わります。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>exercise-js</title> </head> <body> <div id="app"> <h1>exercise-js</h1> <div>hello javascript</div> <!-- 追加する --> </div> </body> </html>
emmetについて
上記でも触れたemmetですが、これはVSCodeにデフォルトで組み込まれているhtml/cssを簡単にコーディングできる機能です。
たとえば div#app
と入力してEnterを押すと
<div id="app"></div>
と展開されます。入力したdiv
の部分がhtmlタグを表し、つぎの#
の部分がidを意味し、残りのapp
がid名となります。
同様に div.contents
と入力してEnterを押すと
<div class="contents"></div>
と展開されます。.
(ピリオド)の部分がclassを意味し、残りのcontents
がclass名となります。
ちなみにclass名は複数付けることができますが、.
でclass名を繋げることで複数指定できます。
例えば div.card.info
と入力してEnterを押すと
<div class="card info"></div>
と展開されます。
ネストしたタグを表現する
ul,liのようにネストしたタグもemmetを使って簡単にコーディングできます。
ul>li
と入力してEnterを押すと
<ul> <li></li> </ul>
と展開します。
liタグを複数個展開したい場合は個数を指定することもできます。個数は*3
のように表記し
ul>li*3
と入力してEnterを押すと
<ul> <li></li> <li></li> <li></li> </ul>
と、ulタグの中にliタグが3つ展開されます。
emmetの機能は他にもありますので、"VSCode emmet"等のキーワードで検索して調べてみてください。
jsファイルの作成
*.jsファイルはjs
というフォルダーに作成するようにします。
Fig.23の"新しいフォルダー"を作成するアイコンをクリックし、フォルダー名にjsと入力してjsフォルダーを作成します。
そのjsフォルダにmain.jsという名前のjsファイルを作成しFig.24のように1行だけコードを記述します。
次にindex.htmlを開き、下のgifのようにbodyタグの直前にscriptタグを追加します。
ファイルを保存するとページがリロードされます。
JavaScriptのconsole.log(...)
の出力を確認するには、ブラウザでF12(またはCtrl + Shift + i)を押して開発者ツールを開きconsoleタブを選択します。
consoleタブに"it works!"という文字列が表示されていれば成功です。
Live Serverについて
Live Serverを一度起動すると、ステータスバーにLive Serverのステータスが表示されるようになります。 Fig.27はポート5500でLive Serverが起動中であることを示していて、ここをクリックすると待機中になります。
待機中はFig.28のような表示になり、以降はここをクリックするだけで起動と待機を切り替えることができます。
設定
Live Serverの設定はユーザー設定から変えることができます。ユーザー設定はCtrl + ,を押すと表示されます(Fig.29)。 Live Serverの設定項目を探すには検索フィールドに"live server"と入力して絞り込みを行います。
主な設定項目
Custom Browser
Live Serverで使用するブラウザーを指定します。デフォルトはお気に入りのブラウザです。
Specify custom browser settings for Live Server. By Default it will open your default favorite browser.
File
Live Serverで開くファイルを指定します。デフォルトはindex.htmlです。
When set, serve this file (server root relative) for every 404 (usefull for single-page applications)
Full Reload
CSS変更時に完全なページのリロードを行います。デフォルトでは完全な再読み込みを行わずに変更のあるCSSを挿入します。
By Default Live Server inject CSS changes without full reloading of browser. you can change this behviour by making this setting as 'true'
Host
Live Serverが使用するホストを指定します。デフォルトは"127.0.0.1"です。 "localhost"に代えたい場合は"localhost"と指定します。
To swith between localhost or '127.0.0.1' or anything else. Default is '127.0.0.1'
Port
Live Serverが使用するポートを指定します。デフォルトは5500です。 0を指定するとポートはランダムになります。
Set Custom Port Number of Live Server. Set 0 if you want random port.
Root
ルートディレクトリを指定します。デフォルトはワークスペースです。 ワークスペース内のdistをルートに代えたい場合は、"/dist"と指定します。
Set Custom root of Live Server. To change root the server to sub folder of workspace, use '/' and relative path from workspace.
変更した項目
設定を変えると、項目の左側に青い線が表示されます。Fig.30はポートを変更したときの状態です。
ファンクションを作成する
index.htmlを開きFig.31のようにdivタグを追加します。
次にmain.jsを開き下記のコードを追記して保存します。
function createElement(message = "ワールド") { const template = `<p> hello ${message} </p>` return template } const message = createElement("world") const contents = document.getElementsByClassName("contents")[0] contents.innerHTML = message
Live Serverを起動していれば、ページがリロードされて"hello world"が表示されたと思います。
テンプレートリテラル
createElementファンクションでtemplateという名前の変数の定義していますが、この変数の初期化にテンプレートリテラルという構文を使っています。
テンプレートリテラルはECMAScript2015(ES6)で追加された構文で、文字列リテラルにプレースフォルダ(${ }
)を含めることができ、そこに変数や式を埋め込むことができます。
デフォルト引数
createElementファンクションのmessage引数にmessage = "ワールド"
のようにデフォルト値が与えられていますが、これもECMAScript2015(ES6)で追加されたデフォルト引数(またはデフォルトパラメータ)という構文です。
下記のように呼び出し時にパラメータを渡さなかった場合、デフォルト値が利用されます。
const message = createElement()
jsファイルを分離する
main.jsにコーディングしたfunctionを別のファイルに分けてみます。 jsフォルダにsub.jsというファイルを作成し、main.jsから下記のコードを移動させます。
下記がsub.jsのコードです。
function createElement(message = "ワールド") { const template = `<p> ${message} </p>` return template }
下記がmain.jsのコードです。
console.log("it works!") const message = createElement("world") const contents = document.getElementsByClassName("contents")[0] contents.innerHTML = message
次にindex.htmlを開きmain.jsを読み込んでいるscriptタグの下にsub.jsを読み込むscriptタグを追加します。
<script src="./js/main.js"></script> <script src="./js/sub.js"></script> <!-- 追加 -->
ファイル保存後にブラウザで動作確認すると"hello world"というメッセージが表示されなくなっていると思います。 consoleタブを見るとFig.33のようなエラーメッセージが表示されていて、このメッセージから"createElement"が定義されていないことが原因だとわかります。
これを厳密に言うと"createElement"は定義されますが、下記のようにsub.jsで"createElement"が定義される前に、main.js内で"createElement"ファンクションが呼び出されているため、ということになります。
<script src="./js/main.js"></script> <!-- createElementファンクションの呼び出し --> <script src="./js/sub.js"></script> <!-- createElementファンクションの定義 -->
この問題を解決するにはjsファイルの読み込み順を下記のように入れ替え、先に"createElement"ファンクションの定義を行うようにします。
<script src="./js/sub.js"></script> <!-- createElementファンクションの定義 --> <script src="./js/main.js"></script> <!-- createElementファンクションの呼び出し -->
ちなみに、行の入れ替えはVSCodeのショートカットキーを使うと簡単にできます。 入れ替えたい行にカーソルを置いた状態で、現在の行を上の行と入れ替えるには上矢印キー、もしくは下の行と入れ替えるには下矢印キーを押します。
Fig.34はsub.jsを読み込んでいる行を上の行と入れ替えている例です。
行を入れ替えたらファイルを保存し再度ブラウザで動作確認を行います。これでページに"hello world"というメッセージが表示されていると思います。 このようにJavaScriptは上から下へ順番に解釈されていくので定義順が重要です。
別の解決方法 (document.addEventListener)
上記でjsファイルの読み込み順を変えて解決しましたが、別にdocument.addEventListener
を使って解決する方法があります。この方法ではjsファイルの読み込み順を変える(意識する)必要はありません。
<script src="./js/main.js"></script> <!-- createElementファンクションの呼び出し --> <script src="./js/sub.js"></script> <!-- createElementファンクションの定義 -->
main.jsを下記のように書き換え、コード全体をdocument.addEventListener("DOMContentLoaded", function(event){ ... })
で囲みます。
document.addEventListener("DOMContentLoaded", function(event) { const message = createElement() const contents = document.getElementsByClassName("contents")[0] contents.innerHTML = message })
このコードは大別すると3つの要素に分かれています。
document.addEventListener("DOMContentLoaded", function(event) { ... }) ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^ | | | | | +--- (3) | | | +---------------------- (2) | +------------------------------------------------- (1)
1) document.addEventListener
addEventListener
は対象オブジェクトに、何らかのイベントが発生したときに、実行する何らかのファンクションを追加します。
この例での対象オブジェクトはdocument
です。つまりdocument
オブジェクトに(2)任意のイベントが発生した時に、実行する(3)任意のファンクションを追加します。
2) 任意のイベント
イベントの種類を表しています。
このDOMContentLoaded
イベントはdocument
オブジェクトのイベントで、HTMLが完全に読み込まれ解釈された時点で発生します。またJavaScriptの読み込みも完了している(例外があります)ので、定義しているファンクションも使えます。ただし、まだスタイルシート、画像、サブフレームの読み込みは終わっていない可能性があります。
3) 任意のファンクション
対象のオブジェクトに指定したイベントが発生したときに実行されるファンクションを指定します。
別の解決方法 (import/export)
さらにもう1つ別の解決方法があります。それはEcmaScript2015(ES6)で追加された、import/export
構文を使用する方法です。
sub.jsでexport
を使ってファンクションを公開します。
export function createElement(message = "ワールド") { const template = `<p> hello ${message} </p>` return template }
main.jsではimport
を使ってsub.jsから公開されているファンクションを読み込みます。
このように前述のdocument.addEventListener("DOMContentLoaded", function(event) { ... })
は使わなくても動作しますが、
import { createElement } from "./sub.js" const message = createElement() const contents = document.getElementsByClassName("contents")[0] contents.innerHTML = message
コードの堅牢制(バグの発生のしにくさ)を考えると、イベントを使った方がいいと思いますので下記のようにします。
import { createElement } from "./sub.js" document.addEventListener("DOMContentLoaded", function(event) { const message = createElement() const contents = document.getElementsByClassName("contents")[0] contents.innerHTML = message })
最後にhtmlファイルを修正します。htmlファイルで読み込む必要があるのはmain.jsだけなので、sub.jsを読み込むscriptタグは削除します。
なおimport/export
を使う場合はscriptタグにtype=module
という記述が必要なので追記します。
<script src="./js/main.js" type="module"></script> <!-- createElementファンクションの呼び出し --> <!-- <script src="./js/sub.js"></script> --> <!-- 読み込み不要 -->
classを使う
次にJavaScriptのclassを使ったコードを学習していきます。class構文もECMAScript2015(ES6)で追加された構文です。
jsフォルダーにitem.jsというファイルを作成し、下記のコードを記述します。main.jsで利用するためexport
を付けています。
export class Item { // コンストラクタ constructor(id, name, price) { this.id = id this.name = name this.price = price } // メソッド toString() { return `id:${this.id} name:${this.name} price:${this.price}` } }
main.jsを下記のように修正します。先頭の方でitem.jsのItem
クラスをインポートします。
Item
クラスのオブジェクトを生成するにはnew Item( ... )
としてnew
演算子を使用します。
import { createElement, setItemRow } from "./sub.js" import { Item } from "./item.js" // ← 追加 document.addEventListener("DOMContentLoaded", function(event) { console.log("DOM fully loaded and parsed", event) const message = createElement() const contents = document.getElementsByClassName("contents")[0] contents.innerHTML = message // ↓ 追加 const apple = new Item(1, "apple", 100) const orange = new Item(2, "orange", 80) const grape = new Item(3, "grape", 120) const items = [apple, orange, grape] // ↑ 追加 })
itemsという配列に格納したItemクラスのオブジェクトの文字列情報を、ItemクラスのtoString
メソッドで出力してみます。
これまで配列の操作には
for
を使う方法
for (let i=0; items.length>i; i++) { console.log(items[i].toString()) }
forEach
を使う方法
items.forEach(item => { console.log(item.toString()) })
がありましたが、ECMAScript2015(ES6)からfor of
という構文も追加されています。
for (const item of items) { console.log(item.toString()) }
せっかくなのでItemクラスのオブジェクトをHTMLのtableで表示するように修正を加えてみます。
index.htmlを修正してtableタグを追加します。tbody内にデータを出力するのはJavaScriptで行うので空の状態です。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>exercise-js</title> </head> <body> <div id="app"> <h1>exercise-js</h1> <div>hello javascript</div> <div class="contents"></div> <!-- ↓ 追加 --> <table class="item"> <thead> <tr> <th>id</th> <th>name</th> <th>price</th> </tr> </thead> <tbody></tbody> </table> <!-- ↑ 追加 --> </div> <script src="./js/main.js" type="module"></script> </body> </html>
sub.jsを修正してtbody内にtr、tdタグを出力するファンクションを追加します。trタグを出力するcreateItemRecordファンクションはmain.jsで使用するのでexport
を付けているのに対して、tdタグを出力するcreateItemDataファンクションはこのsub.js内でしか使用しないためexport
を付けません。このようにexport
を付けるかどうかは、そのファンクションの使用範囲(公開範囲)で決めます。
export function createItemRecord(item) { const id = createItemData(item.id) const name = createItemData(item.name) const price = createItemData(item.price) const tr = document.createElement("tr") tr.appendChild(id) tr.appendChild(name) tr.appendChild(price) return tr } function createItemData(value) { const td = document.createElement("td") const nd = document.createTextNode(value) td.appendChild(nd) return td }
main.jsの先頭のimport
文をこのように修正してcreateItemRecordファンクションをインポートします。
import { createElement, createItemRecord } from "./sub.js"
下記はitems配列のデータを使ってtbodyタグ内にtr>tdタグを追加するコードです。
const apple = new Item(1, "apple", 100) const orange = new Item(2, "orange", 80) const grape = new Item(3, "grape", 120) const items = [apple, orange, grape] const itemTable = document.querySelector("table.item > tbody") for (const item of items) { const itemRow = createItemRecord(item) itemTable.appendChild(itemRow) }
上記のコードを実行すると、ブラウザにFig35のようなテーブルが表示されていると思います。
CSSでスタイル
プロジェクトの直下にcssというフォルダーを作成し、そこにmain.cssというファイルを作成します。 とりあえず何も書いていないCSSファイルのままで、index.htmlを修正しheadタグ内にlinkタグを追加します。
<link rel="stylesheet" href="./css/main.css">
次にmain.cssファイルにスタイルを記述していきます。まず最初に下記のスタイルを記述してファイルを保存します。
* { padding: 0; margin: 0; box-sizing: border-box; } #app { margin: 20px; } .contents { margin: 20px 0; font-size: 20px; }
Live Serverを動かして動作確認をしていれば、main.cssファイルの保存を行ったタイミングでブラウザ上のページにスタイルが当たったことがわかると思います。 次にtableのスタイルを追加します。
table { border-collapse: collapse; font-size: 18px; } th { background-color: black; color: white; } th, td { padding: 0.5em 10px 0.5em; border-top: 1px solid #666; } tr:last-child td { border-bottom: 1px solid #666; } td:last-child { text-align: right; }
ファイルを保存するとページがリロードされてFig.36のようにスタイルが適用されたと思います。
さいごに
VSCodeを使ったJavaScriptの学習環境の構築とVSCodeの簡単な使い方の説明は以上になります。 これまでに作成したソースコード全体は下記の通りです。
index.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="./css/main.css"> <title>exercise-js</title> </head> <body> <div id="app"> <h1>exercise-js</h1> <div>hello javascript</div> <div class="contents"></div> <table class="item"> <thead> <tr> <th>id</th> <th>name</th> <th>price</th> </tr> </thead> <tbody></tbody> </table> </div> <script src="./js/main.js" type="module"></script> </body> </html>
main.css
* { padding: 0; margin: 0; box-sizing: border-box; } #app { margin: 20px; } .contents { margin: 20px 0; font-size: 20px; } table { border-collapse: collapse; font-size: 18px; } th { background-color: black; color: white; } th, td { padding: 0.5em 10px 0.5em; border-top: 1px solid #666; } tr:last-child td { border-bottom: 1px solid #666; } td:last-child { text-align: right; }
main.js
import { createElement, createItemRecord } from "./sub.js" import { Item } from "./item.js" console.log("it works!") document.addEventListener("DOMContentLoaded", function(event) { const message = createElement("world") const contents = document.getElementsByClassName("contents")[0] contents.innerHTML = message const apple = new Item(1, "apple", 100) const orange = new Item(2, "orange", 80) const grape = new Item(3, "grape", 120) const items = [apple, orange, grape,] const itemTable = document.querySelector("table.item > tbody") for (const item of items) { const itemRow = createItemRecord(item) itemTable.appendChild(itemRow) } })
sub.js
export function createElement(message = "ワールド") { const template = `<p> hello ${message} </p>` return template } export function createItemRecord(item) { const id = createItemData(item.id) const name = createItemData(item.name) const price = createItemData(item.price) const tr = document.createElement("tr") tr.appendChild(id) tr.appendChild(name) tr.appendChild(price) return tr } function createItemData(value) { const td = document.createElement("td") const nd = document.createTextNode(value) td.appendChild(nd) return td }
item.js
export class Item { // コンストラクタ constructor(id, name, price) { this.id = id this.name = name this.price = price } // メソッド toString() { return `id:${this.id} name:${this.name} price:${this.price}` } }
『入門向け』Java開発環境の構築とかんたんなコードの書き方と実行方法 (後編)
はじめに
この記事は、 rubytomato.hateblo.jp rubytomato.hateblo.jp に続く記事の後編です。 前編、中編ではJava開発環境の構築とEclipseの簡単な使い方を中心に説明しました。この記事では引き続きJavaプログラムの書き方と実行方法を中心に説明します。
Javaプログラムの書き方
中編のおさらい
中編のレッスン1では、Fig1のようにstudy01project
プロジェクトにDemo
クラスとSnack
クラスまで作成しました。
レッスン2
レッスン2では、レッスン1で作成した「おかし」を表現するSnack
クラスに加え、「おかしを売る店」を表現するShop
クラスと「おかしを買う客」を表現するPerson
クラスを使って、お店とお客がおかしを売買する機能をコーディングしていきます。
おかしを売買できる条件として以下の3つを仕様とします。
- お店(
Shop
)は扱っているおかししか売れないこと - お客(
Person
)はおかしを買うのに代金を支払うこと - お客(
Person
)は買ったおかしをおかし袋に仕舞えること(最大5個まで仕舞える)
また、そのほかの仕様として
- お店(
Shop
)はおかしをいくつでも販売できる。(おかしの在庫数は管理しない) - おかしの売買は『おかしの名前』で行う。(『おかしの名前』が一意で管理できる(重複しない)こと)
とします。
レッスン2用のパッケージを作成する
まずレッスン2用のパッケージcom.example.lesson02
を作成し、lesson02
パッケージにDemo
クラスを作成します。Fig2はここまで作成した状態です。
Demoクラス
Demo.javaのソースコードは以下の通りです。レッスン1同様にこのクラスのmain
メソッドにレッスン2で実行するコードを実装していきます。
package com.example.lesson02; public class Demo { public static void main(String[] args) { System.out.println("Lession02 Demo start"); // ここに動作確認するコードを追加していく System.out.println("Lession02 Demo end"); } }
Snackクラスを修正する
おかしの売買を『おかしの名前』で行うためには『おかしの名前』が重複なく一意であることが求められます。そのためにSnack
クラスのname
フィールドでオブジェクトを一意に識別できるように実装する必要があります。(※ちなみにオブジェクトを一意に識別するフィールドには社員IDや商品IDといったIDフィールドを使うことが一般的です。)
Snack
クラスの2つのオブジェクトが同一(おなじおかしを指すか)であるか判別するには、Snack
クラスのhashCode
とequals
メソッドを正しくオーバーライドする必要があります。
たとえばSnack
クラスのhashCode
とequals
メソッドをオーバーライドしていない状態で以下のコードを実行すると、同じおかしの名前を持つ2つのオブジェクト(salami_1とsalami_2)なのに”違うおかし”と判断されます。
Snack salami_1 = new Snack("うまいぼう サラミ味", 10); Snack salami_2 = new Snack("うまいぼう サラミ味", 10); if (salami_1.equals(salami_2)) { System.out.println("同じおかし"); } else { System.out.println("違うおかし"); }
この2つのオブジェクトを ”同じおかし”である(おかしの名前が同じなので)と判別するには、hashCode
とequals
メソッドを以下のようにオーバーライドします。
- メニューバーの
ソース(S)
→hashCode() および equals() の生成(H)...
を選択 - キーボードのAlt + Shift + Sを押し、メニューの
hashCode() および equals() の生成(H)...
を選択
"hashCode() および equals() の生成"画面(Fig3)のname
フィールドだけにチェックを入れ、"生成"ボタンをクリックします。
すると、Eclipseによって以下のコードが生成されます。これでname
フィールドが同じであれば”同じおかし”であると判別されるようになります。
@Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Snack other = (Snack) obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; }
先ほどのコードを実行すると2つのオブジェクトは”同じおかし”と判断されます。
ちなみに”おかしの名前”と”価格”の2つのフィールドが同一であれば同じおかしと判別したい場合は、"hashCode() および equals() の生成"画面(Fig3)でname
とprice
フィールドにチェックを入れてコードを生成しなおします。
Shopクラスを作成する
まずcom.example.lesson02.model
パッケージを作成し、model
パッケージにShop
クラスを作成します。
Fig4はmodel
パッケージおよびShop
クラスを作成した直後のプロジェクトの状態です。
以下がShop
クラスを作成した直後のソースコードです。
package com.example.lesson02.model; public class Shop { }
フィールドを定義する
Shop
クラスに、お店が扱うおかしの種類を持つSet
型のsnackVarieties
、おかしの売上金を持つint
型のsales
の2つのフィールドを定義します。同じ名前のおかしを持たないようにsnackVarieties
はSet
型にしています。
アクセサメソッドは必要がないので実装しません。
/** * お店が扱うおかしの種類 */ private Set<Snack> snackVarieties; /** * 売上金 */ private int sales;
ちなみに、下記のようにこのフィールドをList
型にすると同じ名前のおかしを複数追加できてしまいます。
private List<Snack> snackVarieties;
toStringメソッドをオーバーライドする
Eclipseの機能を使ってtoStringメソッドをオーバーライドします。
@Override public String toString() { return "Shop [snackVarieties=" + snackVarieties + ", sales=" + sales + "]"; }
コンストラクターを実装する
デフォルトコンストラクタ―と、引数におかしの種類と売上金を取るコンストラクタ―の2つを実装します。
// デフォルトコンストラクタ― public Shop() { this(new HashSet<>(), 0); } public Shop(Set<Snack> snackVarieties, int sales) { this.snackVarieties = snackVarieties; this.sales = sales; }
おかしの種類を追加するメソッドを実装する
お店が扱うおかしの種類を追加するメソッドをaddSnackVariety
という名前で実装します。上述した通りsnackVarieties
フィールドはSet
型なので、オブジェクトを追加する際の重複チェックは不要です。同じ名前のおかしを登録しても無視されます。
/** * お店が扱うおかしの種類を追加する * @param snack */ public void addSnackVariety(Snack snack) { this.snackVarieties.add(snack); }
さらに、お店が扱うおかしを複数まとめて追加するメソッドをaddSnackVarieties
という名前で実装します。このメソッドの(Snack ... snacks)
の部分を可変長引数といい、複数のパラメータをまとめて渡すことができます。
その可変長引数snacks
を取るSet.of
メソッドは可変長引数をSet型のコレクションにして返します。
/** * お店が扱うおかしの種類を複数まとめて追加する * @param snacks */ public void addSnackVarieties(Snack... snacks) { this.snackVarieties.addAll(Set.of(snacks)); }
お店がおかしを扱っているか確認するメソッドを実装する
引数で指定した名前のおかしをお店が扱っているか確認する処理をisAvailableSnack
メソッドに実装します。と言っても実際の検索処理はgetSnack
メソッドで行い、その結果を真偽値で返すだけの処理になります。
/** * 指定した名前のおかしを扱っているか確認する * @param snackName おかしの名前 * @return true:扱っている */ public boolean isAvailableSnack(String snackName) { return getSnack(snackName).isPresent(); }
Set
型のコレクションからおかしの名前で検索する処理はgetSnack
メソッドに実装します。このメソッドはShop
クラス内からのみ利用するのでメソッドの可視性はprivate
にして他のクラスから利用されないようにします。
また、指定した名前のおかしを必ず扱っているとは限らないのでこのメソッドの戻り値はOptional
でラップします。見つかったおかしのオブジェクトはOptional.of
でラップして返し、もし扱っていない(見つからなかった)場合はnullではなくOptional.empty()
を返すようにします。
/** * 指定した名前のおかしのオブジェクトを返す * @param snackName おかしの名前 * @return おかしのオブジェクト */ private Optional<Snack> getSnack(String snackName) { Iterator<Snack> ite = snackVarieties.iterator(); while (ite.hasNext()) { Snack snack = ite.next(); if (snack.getName().equals(snackName)) { return Optional.of(snack); } } return Optional.empty(); }
お店が扱っているおかしの種類を表示するメソッドを実装する
扱うおかしを出力する処理はprettyPrintSnackVariety
メソッドに実装します。この記事ではデバッグ用に使用していますが、オブジェクトの状態を出力するメソッドがあると便利なときがあります。
/** * お店が扱っているおかしを表示する */ public void prettyPrintSnackVariety() { if (snackVarieties.isEmpty()) { System.out.println("取り扱うおかしはありません"); return; } snackVarieties.forEach(snack -> { System.out.println(snack.getName() + "は" + snack.getPrice() + "円"); }); System.out.println("計" + snackVarieties.size() + "種類のおかしを取り扱い中"); }
Personクラスを作成する
model
パッケージにPerson
クラスを作成します。
Fig5はmodel
パッケージおよびPerson
クラスを作成した直後のプロジェクトの状態です。
以下がPerson
クラスを作成した直後のソースコードです。
package com.example.lesson02.model; public class Person { }
フィールドを定義する
Person
クラスに、人の名前を持つString
型のname
、所持金を持つint
型のmoney
、持っているおかしを持つList
型のsnackBag
の3つのフィールドと、おかしの最大所持数を持つ定数を定義します。おかし袋のsnackBag
フィールドはList
型なのでおなじ名前のおかしを複数持つことができます。
このクラスもアクセサメソッドは必要がないので実装しません。
/** * 名前 */ private String name; /** * 所持金 */ private int money; /** * おかし袋 */ private List<Snack> snackBag; /** * おかし袋に入れられるおかしの最大数 */ private static final int MAX_SNACK_NUM = 5;
toStringメソッドをオーバーライドする
Eclipseの機能を使ってtoStringメソッドをオーバーライドします。
@Override public String toString() { return "Person [name=" + name + ", money=" + money + ", snackBag=" + snackBag + "]"; }
コンストラクターを実装する
名前のname
、所持金のmoney
は必須のフィールドなので、コンストラクターの引数で初期化するようにします。
おかし袋のsnackBag
の要素数はMAX_SNACK_NUM
定数で初期化します。(※あくまでも定数の値で要素数を確保するだけで、要素数の最大値を設定しているわけではありません。)
public Person(String name, int money) { assert name != null; this.name = name; this.money = money; this.snackBag = new ArrayList<>(MAX_SNACK_NUM); }
持っているおかしを出力するメソッドを実装する
持っているおかしを出力する処理をprettyPrintHasSnack
メソッドに実装します。このメソッドもこの記事ではデバッグ用に使用します。
/** * 持っているおかしを表示する */ public void prettyPrintHasSnack() { StringBuilder sb = new StringBuilder(name); sb.append("は 所持金:"); sb.append(money); sb.append("円"); if (!snackBag.isEmpty()) { sb.append("とおかし "); String comma = ""; for (Snack snack : snackBag) { sb.append(String.format("%s(name:%s, price=%d)", comma, snack.getName(), snack.getPrice())); comma = ", "; } } sb.append(" を持っている"); System.out.println(sb.toString()); }
おかしを売買する機能を実装する
Personクラスの実装
Shop
クラスとおかしの売買を行うdeal
メソッド- おかしを買う
buySnack
メソッド
Shopクラスの実装
Person
クラスへおかしを売るsellSnack
メソッド
売買が失敗した場合
売買が出来なかった場合に例外をスローするようにします。想定する売買できない状況というのは
- お店(
Shop
)が取り扱っていないおかしを購入しようとした場合 - お客(
Person
)が購入するおかしの代金を支払えない場合 - お客(
Person
)のおかし袋に空きがない場合
があります。このような場合はDealException
というRuntimeException
を継承した例外クラスをスローするようにします。
package com.example.lesson02; public class DealException extends RuntimeException { public DealException(String message) { super(message); } }
Shopクラスにおかしを売るsellSnackメソッドを実装する
指定した名前のおかしがお店で扱われているかgetSnack
メソッドで確認し、扱われている場合はPerson
クラスのbuySnack
メソッドでおかしを買う処理を実行します。その処理が正常終了したら代金を売上金に加算します。
なおFig7のようにcustomer.buySnack(snack);
の部分にエラーが出るとおもいますが、とりあえずこのままにして次のPerson
クラスの実装に移ります。
/** * おかしを売る * @param snackName 売りおかしの名前 * @param customer おかしを買う人 * @throws DealException おかしの売買が失敗した場合 */ public void sellSnack(String snackName, Person customer) { Optional<Snack> optionalSnack = getSnack(snackName); optionalSnack.ifPresentOrElse(snack -> { // おかしが見つかった場合 // 購入 int customerPay = customer.buySnack(snack); // 売り上げの加算 sales += customerPay; }, () -> { // おかしが見つからなかった場合 throw new DealException(snackName + "は取り扱っていません"); }); }
getSnack
メソッドはOptional
型でラップしたSnack
クラスのオブジェクトを返します。Optional
型でラップするということは、指定した『おかしの名前』でおかしが見つからない場合があるということを表しているので、見つかった場合と見つからなかった場合の両方の処理を忘れずに実装します。
Optional<Snack> optionalSnack = getSnack(snackName);
その次のoptionalSnack.ifPresentOrElse( ... )
というコードの部分は、以下のコードの書き方と同じ意味になります。isPresent
メソッドがtrueを返す場合はおかしが見つかった場合ということなので、取引処理を実行します。それ以外は見つからなかった場合ということなので例外をスローします。
// 指定した『おかしの名前』でおかしが見つかった場合 if (optionalSnack.isPresent()) { Snack snack = optionalSnack.get(); // 購入 int customerPay = customer.buySnack(snack); // 売り上げの加算 sales += customerPay; // おかしが見つからなかった場合 } else { throw new DealException(snackName + "は取り扱っていません"); }
Personクラスにおかしの売買をするdealメソッドを実装する
Person
クラスとShop
クラス間のおかしを売買する処理はdeal
メソッドに実装します。メソッドの先頭で、引数がnullであれば不正(バグ)ということを示すためにassert
文でチェックを行います。
なお、実際の売買はShop
クラスのsellSnack
メソッドで行います。
/** * お店と取引する * @param shop 取引するお店 * @param snackName 買うおかしの名前 */ public void deal(Shop shop, String snackName) { assert shop != null; assert snackName != null; try { shop.sellSnack(snackName, this); } catch (DealException e) { System.out.println(snackName + "の購入に失敗しました"); System.err.println(e.getMessage()); } }
buySnack
メソッドではおかしを買える所持金を持っているかとおかし袋に空きがあるかのチェックを行い、条件を満たしていない場合はDealException
をスローして処理を中断するようにしています。
/** * おかしを買う * @param snack 買うおかしのオブジェクト * @return 支払った金額 * @throws DealException おかしが買えなかった場合にスロー */ int buySnack(Snack snack) throws DealException { if (money < snack.getPrice()) { throw new DealException("所持金が足りません"); } if (MAX_SNACK_NUM <= snackBag.size()) { throw new DealException("おかし袋に空きがありません"); } // 所持金からおかしの価格を引く money -= snack.getPrice(); // おかし袋におかしを追加する snackBag.add(snack); return snack.getPrice(); }
Demoクラス
上記の実装が終わったら、Demo
クラスのmain
メソッドに動作確認用のコードを書いて実行してみます。
まず、お店が扱うおかしのオブジェクトを準備します。
// サラミ味のおかしを生成 Snack umaibouSalami = new Snack("うまいぼう サラミ味", 10, "コーン,植物油脂,糖類,ポークパウダー,香辛料,パン粉,ビーフパウダー,酵母エキスパウダー,パセリ,調味料,香料,甘味料,カラメル色素,酸化防止剤"); // チーズ味のおかしを生成 Snack umaibouCheese = new Snack("うまいぼう チーズ味", 10, "コーン,植物油脂,チーズパウダー,乳糖,クリーミングパウダー,乳製品,パン粉,砂糖,食塩,香辛料,調味料,香料,パプリカ色素,甘味料,ph調整剤,乳化剤,ターメリック色素"); // めんたい味のおかしを生成 Snack umaibouMentai = new Snack("うまいぼう めんたい味", 10, "コーン,植物油脂,糖類,パプリカ,香辛料,パン粉,たん白加水分解物,たら調味パウダー,食塩,調味料,香料,パプリカ色素,甘味料");
次にお店のオブジェクトを生成し、扱うおかしを追加します。
// お店の扱うおかしの準備 Shop dagashiya = new Shop(); dagashiya.addSnackVariety(umaibouSalami); dagashiya.addSnackVariety(umaibouCheese); dagashiya.addSnackVariety(umaibouMentai); // 取り扱い中のおかしを出力 dagashiya.prettyPrintSnackVariety();
3つのおかしの登録はaddSnackVarieties
メソッドで1行で書くこともできます。
dagashiya.addSnackVarieties(umaibouSalami, umaibouCheese, umaibouMentai);
おかしを買う人のオブジェクトを生成します。おかしを1つも買っていない場合は以下のようなメッセージを出力します。
Person taro = new Person("たろう", 100); // 所持金と購入したおかしを出力 taro.prettyPrintHasSnack();
たろうは 所持金:100円 を持っている
deal
メソッドでお店からおかしを5つ買い、もう一度所持金と購入したおかしの情報を出力してみます。
taro.deal(dagashiya, "うまいぼう サラミ味"); taro.deal(dagashiya, "うまいぼう サラミ味"); taro.deal(dagashiya, "うまいぼう チーズ味"); taro.deal(dagashiya, "うまいぼう チーズ味"); taro.deal(dagashiya, "うまいぼう めんたい味"); // 所持金と購入したおかしを出力 taro.prettyPrintHasSnack();
たろうは 所持金:50円とおかし (name:うまいぼう サラミ味, price=10), (name:うまいぼう サラミ味, price=10), (name:うまいぼう チーズ味, price=10), (name:うまいぼう チーズ味, price=10), (name:うまいぼう めんたい味, price=10) を持っている
もう1人、おかしを買う人のオブジェクトを生成し、所持金より多いおかしを購入してみます。
Person jiro = new Person("じろう", 30); // 所持金と購入したおかしを出力 jiro.prettyPrintHasSnack(); jiro.deal(dagashiya, "うまいぼう サラミ味"); jiro.deal(dagashiya, "うまいぼう チーズ味"); jiro.deal(dagashiya, "うまいぼう めんたい味"); jiro.deal(dagashiya, "うまいぼう めんたい味"); // 所持金と購入したおかしを出力 jiro.prettyPrintHasSnack();
じろうは 所持金:30円 を持っている うまいぼう めんたい味の購入に失敗しました 所持金が足りません じろうは 所持金:0円とおかし (name:うまいぼう サラミ味, price=10), (name:うまいぼう チーズ味, price=10), (name:うまいぼう めんたい味, price=10) を持っている
レッスン2のさいごに
これでレッスン2は終了です。これまでに書いたJavaのソースコードは以下のようになります。
Snack.java
package com.example.lesson01.model; public class Snack { private static final String UNDEFINED_INGREDIENTS = "未定"; public Snack(String name, int price) { this(name, price, UNDEFINED_INGREDIENTS); // 3つの引数を取るコンストラクターを呼ぶ } public Snack(String name, int price, String ingredients) { assert name != null; assert price > 0; this.name = name; this.price = price; this.ingredients = ingredients == null ? UNDEFINED_INGREDIENTS : ingredients; } /** * 名前 (必須) */ private String name; /** * 値段 (必須) */ private int price; /** * 原材料 (任意) */ private String ingredients; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } public String getIngredients() { return ingredients; } public void setIngredients(String ingredients) { this.ingredients = ingredients; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Snack other = (Snack) obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } @Override public String toString() { return "Snack [name=" + name + ", price=" + price + ", ingredients=" + ingredients + "]"; } }
Shop.java
package com.example.lesson02.model; import java.util.HashSet; import java.util.Iterator; import java.util.Optional; import java.util.Set; import com.example.lesson01.model.Snack; import com.example.lesson02.DealException; public class Shop { // デフォルトコンストラクタ― public Shop() { this(new HashSet<>(), 0); } public Shop(Set<Snack> snackVarieties, int sales) { this.snackVarieties = snackVarieties; this.sales = sales; } /** * お店が扱うおかしの種類 */ private Set<Snack> snackVarieties; /** * 売上金 */ private int sales; /** * お店が扱うおかしの種類を追加する * @param snack */ public void addSnackVariety(Snack snack) { this.snackVarieties.add(snack); } /** * お店が扱うおかしの種類を複数まとめて追加する * @param snacks */ public void addSnackVarieties(Snack... snacks) { this.snackVarieties.addAll(Set.of(snacks)); } /** * 指定した名前のおかしを扱っているか確認する * @param snackName おかしの名前 * @return true:扱っている */ public boolean isAvailableSnack(String snackName) { return getSnack(snackName).isPresent(); } /** * 指定した名前のおかしのオブジェクトを返す * @param snackName おかしの名前 * @return おかしのオブジェクト */ private Optional<Snack> getSnack(String snackName) { Iterator<Snack> ite = snackVarieties.iterator(); while (ite.hasNext()) { Snack snack = ite.next(); if (snack.getName().equals(snackName)) { return Optional.of(snack); } } return Optional.empty(); } /** * お店が扱っているおかしを表示する */ public void prettyPrintSnackVariety() { if (snackVarieties.isEmpty()) { System.out.println("取り扱うおかしはありません"); return; } snackVarieties.forEach(snack -> { System.out.println(snack.getName() + "は" + snack.getPrice() + "円"); }); System.out.println("計" + snackVarieties.size() + "種類のおかしを取り扱い中"); } /** * おかしを売る * @param snackName 売りおかしの名前 * @param customer おかしを買う人 * @throws DealException おかしの売買が失敗した場合 */ public void sellSnack(String snackName, Person customer) { Optional<Snack> optionalSnack = getSnack(snackName); optionalSnack.ifPresentOrElse(snack -> { // おかしが見つかった場合 // 購入 int customerPay = customer.buySnack(snack); // 売り上げの加算 sales += customerPay; }, () -> { // おかしが見つからなかった場合 throw new DealException(snackName + "は取り扱っていません"); }); } @Override public String toString() { return "Shop [snackVarieties=" + snackVarieties + ", sales=" + sales + "]"; } }
Person.java
package com.example.lesson02.model; import java.util.ArrayList; import java.util.List; import com.example.lesson01.model.Snack; import com.example.lesson02.DealException; public class Person { public Person(String name, int money) { assert name != null; this.name = name; this.money = money; this.snackBag = new ArrayList<>(MAX_SNACK_NUM); } /** * 名前 */ private String name; /** * 所持金 */ private int money; /** * おかし袋 */ private List<Snack> snackBag; /** * おかし袋に入れられるおかしの最大数 */ private static final int MAX_SNACK_NUM = 5; /** * 持っているおかしを表示する */ public void prettyPrintHasSnack() { StringBuilder sb = new StringBuilder(name); sb.append("は 所持金:"); sb.append(money); sb.append("円"); if (!snackBag.isEmpty()) { sb.append("とおかし "); String comma = ""; for (Snack snack : snackBag) { sb.append(String.format("%s(name:%s, price=%d)", comma, snack.getName(), snack.getPrice())); comma = ", "; } } sb.append(" を持っている"); System.out.println(sb.toString()); } /** * お店と取引する * @param shop 取引するお店 * @param snackName 買うおかしの名前 */ public void deal(Shop shop, String snackName) { assert shop != null; assert snackName != null; try { shop.sellSnack(snackName, this); } catch (DealException e) { System.out.println(snackName + "の購入に失敗しました"); System.err.println(e.getMessage()); } } /** * おかしを買う * @param snack 買うおかしのオブジェクト * @return 支払った金額 * @throws DealException おかしが買えなかった場合にスロー */ int buySnack(Snack snack) throws DealException { if (money < snack.getPrice()) { throw new DealException("所持金が足りません"); } if (MAX_SNACK_NUM <= snackBag.size()) { throw new DealException("おかし袋に空きがありません"); } money -= snack.getPrice(); snackBag.add(snack); return snack.getPrice(); } @Override public String toString() { return "Person [name=" + name + ", money=" + money + ", snackBag=" + snackBag + "]"; } }
以上で、前編・中編・後編と続けたこの記事もひとまず終了です。
『入門向け』Java開発環境の構築とかんたんなコードの書き方と実行方法 (中編)
はじめに
この記事は、 rubytomato.hateblo.jp に続く記事の中編です。 前編ではJava開発環境の構築とEclipseの簡単な使い方を中心に説明しました。この記事ではJavaプログラムの書き方と実行方法を中心に説明します。
Javaプログラムの書き方
前編のおさらい
前編でどこまでおこなったか簡単におさらいをしておきます。
まず、Eclipse(Pleiades All in One Eclipse
)のセットアップとFig1のようにstudy01project
プロジェクトとDemo
クラスまで作成しました。
つぎにレッスン1のはじめで
まで説明しました。
レッスン1(つづき)
レッスン1のつづきでは、Eclipseを使ったJavaプログラムの書き方をもう少し踏み込んで説明したいと思います。
このレッスンでは、おかしの情報を扱うSnack
というクラスを題材に、クラス、フィールド、コンストラクタ―の定義方法を説明します。
Snackクラスを作成する
まずcom.example.lesson01.model
パッケージを作成し、model
パッケージにSnack
クラスを作成します。
パッケージおよびクラスの作成方法が分からない場合は前編の記事をご覧ください。
Fig2はmodel
パッケージおよびSnack
クラスを作成した直後のプロジェクトの状態です。
以下がSnack
クラスを作成した直後のソースコードです。
package com.example.lesson01.model; public class Snack { }
フィールドを定義する
Snack
クラスに、おかしの名前を持つname
、値段を持つprice
、原材料を持つingredients
の3つのフィールドを定義します。
name
とprice
は必須のフィールドで、ingredients
は情報があればセットするというオプショナルなフィールドというルールにします。
package com.example.lesson01.model; public class Snack { /** * 名前 (必須) */ private String name; /** * 値段 (必須) */ private int price; /** * 原材料 (任意) */ private String ingredients; }
アクセサメソッドを実装する
定義したフィールドのアクセサメソッドを定義します。アクセサメソッドとはフィールドのゲッターや、セッターメソッドのことです。
たとえばname
フィールドであれば、name
フィールドの値を取得するgetName
メソッドがゲッターメソッド、値をセットするsetName
メソッドがセッターメソッドです。
メソッドの先頭にget
やset
が付くことからゲッター(getter)・セッター(setter)メソッドと呼ばれています。
以下のコードはname
フィールドのアクセサメソッドの例です。
// 名前フィールドのゲッター public String getName() { return name; } // 名前フィールドのセッター public void setName(String name) { this.name = name; }
アクセサメソッドは、手動でコーディングするのは非効率なのでEclipseの機能を使って自動的に生成するようにします。生成は下記のメニューバーかキーボードのショートカットから行えます。
- メニューバーの
ソース(S)
→getter および setter の生成(R)...
を選択 - キーボードのShift + Alt + Sを押しメニューの
getter および setter の生成(R)...
を選択
"getter および setter の生成"画面(Fig4)でフィールドをすべて選択し、"生成"ボタンをクリックします。
以下がゲッター・セッターが生成された状態のソースコードです。
package com.example.lesson01.model; public class Snack { /** * 名前 (必須) */ private String name; /** * 値段 (必須) */ private int price; /** * 原材料 (任意) */ private String ingredients; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } public String getIngredients() { return ingredients; } public void setIngredients(String ingredients) { this.ingredients = ingredients; } }
Snackクラスのオブジェクトを生成する
Demo
クラスを開き、mainメソッドにSnack umaibou = new Snack();
というコードを追加します。
public static void main(String[] args) { System.out.println("Lession01 Demo start"); // サラミ味のおかしを生成 Snack umaibou = new Snack(); System.out.println("Lession01 Demo end"); }
するとFig5のようにSnack
の部分に赤い下線が表示されると思います。これはSnack
クラスのインポート文がコーディングされていないために起きているエラーです。
インポート文を追加するために、赤い下線の部分にカーソルを置きCtrl + 1を押し、メニューからSnack をインポートします (com.example.lesson01.model)
というメニューを選択します。これでインポート文が追加されエラーは解消します。
Snack umaibou = new Snack();
というコードでは、変数の宣言とコンストラクターの呼び出しを行っています。
このコードによりumaibou
というSnack
型の変数に、Snack
クラスのオブジェクトが代入されます。
Snack umaibou = new Snack();
^^^^^ ^^^^^^^ ^^^ ^^^^^^^
| | | |
| | | +--- コンストラクター名
| | |
| | +------- newキーワードでコンストラクタ―を呼び出す
| |
| +----------------- 変数名
|
+----------------------- 変数の型
オブジェクトのフィールドに値をセットする
オブジェクトを生成したらセッターメソッドを使ってオブジェクトのフィールドに値をセットします。
このときにEclipseのコード補完を使うとタイピング量が減り且つ正確にコードがかけます。
umaibou.
とまでタイプするとFig7のようにコード補完の候補が表示されます。候補が表示されない場合はCtrl + Spaceを押してください。
セッターメソッドの候補だけを表示するには、さらにumaibou.set
とまでタイプします。そうするとset
から始まるメソッドやフィールドだけが候補になるので選択が楽になります。この中から選択するとそのコードで補完されます。
以下が3つのフィールド(name
, price
, ingredients
)をセットするソースコードです。
// サラミ味のおかしを生成 Snack umaibou = new Snack(); umaibou.setName("うまいぼう サラミ味"); umaibou.setPrice(10); umaibou.setIngredients("コーン,植物油脂,糖類,ポークパウダー,香辛料,パン粉,ビーフパウダー,酵母エキスパウダー,パセリ,調味料,香料,甘味料,カラメル色素,酸化防止剤");
オブジェクトの情報を出力する
ここまででオブジェクトを生成しフィールドの値をセットするコードがかけましたので、そのオブジェクトの情報をコンソールへ出力してみたいと思います。
コンソールへ出力するにはSystem.out.println
を使います。以下のコードを追加しファイルを保存します。
System.out.println("name=" + umaibou.getName() + " price=" + umaibou.getPrice() + " ingredients=" + umaibou.getIngredients());
ここまでコーディングしたmainメソッドの全体を示します。
public static void main(String[] args) { System.out.println("Lession01 Demo start"); // サラミ味のおかしを生成 Snack umaibou = new Snack(); umaibou.setName("うまいぼう サラミ味"); umaibou.setPrice(10); umaibou.setIngredients("コーン,植物油脂,糖類,ポークパウダー,香辛料,パン粉,ビーフパウダー,酵母エキスパウダー,パセリ,調味料,香料,甘味料,カラメル色素,酸化防止剤"); System.out.println("name=" + umaibou.getName() + " price=" + umaibou.getPrice() + " ingredients=" + umaibou.getIngredients()); System.out.println("Lession01 Demo end"); }
Demo
クラスのmainメソッドを実行し、コンソールに以下の内容が出力されると成功です。これでオブジェクトの情報を表す文字列をコンソールに出力することができました。
Lession01 Demo start name=うまいぼう サラミ味 price=10 ingredients=コーン,植物油脂,糖類,ポークパウダー,香辛料,パン粉,ビーフパウダー,酵母エキスパウダー,パセリ,調味料,香料,甘味料,カラメル色素,酸化防止剤 Lession01 Demo end
次にもう1つ新しいおかしを追加します。これまでと同じようにオブジェクトを生成し、セッターメソッドでフィールドに値をセットします。
// チーズ味のおかしを生成 Snack umaibouCheese = new Snack(); umaibouCheese.setName("うまいぼう チーズ味"); umaibouCheese.setPrice(10); umaibouCheese.setIngredients("コーン,植物油脂,チーズパウダー,乳糖,クリーミングパウダー,乳製品,パン粉,砂糖,食塩,香辛料,調味料,香料,パプリカ色素,甘味料,ph調整剤,乳化剤,ターメリック色素");
おなじように、このオブジェクトの情報を出力するコードも加えます。
System.out.println("name=" + umaibouCheese.getName() + " price=" + umaibouCheese.getPrice() + " ingredients=" + umaibouCheese.getIngredients());
ここまでコーディングしたmainメソッドの全体を示します。
public static void main(String[] args) { System.out.println("Lession01 Demo start"); // サラミ味のおかしを生成 Snack umaibou = new Snack(); umaibou.setName("うまいぼう サラミ味"); umaibou.setPrice(10); umaibou.setIngredients("コーン,植物油脂,糖類,ポークパウダー,香辛料,パン粉,ビーフパウダー,酵母エキスパウダー,パセリ,調味料,香料,甘味料,カラメル色素,酸化防止剤"); // チーズ味のおかしを生成 Snack umaibouCheese = new Snack(); umaibouCheese.setName("うまいぼう チーズ味"); umaibouCheese.setPrice(10); umaibouCheese.setIngredients("コーン,植物油脂,チーズパウダー,乳糖,クリーミングパウダー,乳製品,パン粉,砂糖,食塩,香辛料,調味料,香料,パプリカ色素,甘味料,ph調整剤,乳化剤,ターメリック色素"); System.out.println("name=" + umaibou.getName() + " price=" + umaibou.getPrice() + " ingredients=" + umaibou.getIngredients()); System.out.println("name=" + umaibouCheese.getName() + " price=" + umaibouCheese.getPrice() + " ingredients=" + umaibouCheese.getIngredients()); System.out.println("Lession01 Demo end"); }
Demo
クラスのmainメソッドを実行して、以下のようにコンソールに2つのおかしの情報が出力されれば成功です。
Lession01 Demo start name=うまいぼう サラミ味 price=10 ingredients=コーン,植物油脂,糖類,ポークパウダー,香辛料,パン粉,ビーフパウダー,酵母エキスパウダー,パセリ,調味料,香料,甘味料,カラメル色素,酸化防止剤 name=うまいぼう チーズ味 price=10 ingredients=コーン,植物油脂,チーズパウダー,乳糖,クリーミングパウダー,乳製品,パン粉,砂糖,食塩,香辛料,調味料,香料,パプリカ色素,甘味料,ph調整剤,乳化剤,ターメリック色素 Lession01 Demo end
オブジェクトの情報を出力するのにtoStringメソッドを利用する
toStringメソッドはオブジェクトの情報を文字列にして返すメソッドです。このメソッドはjava.lang.Object
クラスというすべてのクラスのスーパークラスで定義されているので、どのクラスでも使用できます。
このメソッドを使ってオブジェクトの情報を出力するように修正します。以下の2行を
System.out.println("name=" + umaibou.getName() + " price=" + umaibou.getPrice() + " ingredients=" + umaibou.getIngredients()); System.out.println("name=" + umaibouCheese.getName() + " price=" + umaibouCheese.getPrice() + " ingredients=" + umaibouCheese.getIngredients());
以下のように書き換えます。
System.out.println(umaibou.toString()); System.out.println(umaibouCheese.toString());
修正してファイルを保存したらDemo
クラスのmainメソッドを実行してみます。真ん中の2行がumaibouとumaibouCheeseをtoStringメソッドで出力した結果です。修正前に比べると人間にはよくわからない文字列が出力されるようになってしまったと思います。
Lession01 Demo start com.example.lesson01.model.Snack@1d251891 com.example.lesson01.model.Snack@48140564 Lession01 Demo end
toStringメソッドをオーバーライドする
実はjava.lang.Object
クラスのtoStringメソッドのデフォルトの実装では、人間にとって意味のある(人間が読める)情報を出力しないため、Snack
クラスで改めてtoStringメソッドをオーバーライドする必要があります。
オーバーライドとは、すでに実装されているメソッドより、新しいメソッドを優先させることです。
Snack
クラスのtoStringメソッドをオーバーライドするためにSnack.javaをエディタで開きます。
ちなみに、Demo
クラスのソースコード上のSnackにカーソルを置いた状態でF3を押すか、Ctrlを押しながらマウスカーソルをSnackにあわせて、メニューの宣言を開く
を選択すると、Snack.javaのソースコードが開きます。
toStringメソッドをオーバーライドするコードは、これまでと同様にEclipseの機能を使って自動的に生成する方が効率がよく正確です。 生成は下記のメニューバーかキーボードのショートカットの操作から行えます。カーソルは生成するコードを出力したい場所へおいてください。
- メニューバーの
ソース(S)
→toString() 生成(R)...
を選択 - キーボードのShift + Alt + Sを押しメニューの
toString() 生成(R)...
を選択
"toString() 生成"画面(Fig10)が表示されるので出力するフィールドを選択します。ここでは3つすべて選択し"生成"ボタンをクリックします。
すると、以下のコードが追加されたと思います。@Override
という記述はアノテーションといって、このtoStringメソッドがSnack
クラスでオーバーライドされていることを表しています。
@Override public String toString() { return "Snack [name=" + name + ", price=" + price + ", ingredients=" + ingredients + "]"; }
ソースコードを保存したら、もう一度Demo
クラスのmainメソッドを実行してみます。コンソールに以下のように出力されれば成功です。
Lession01 Demo start Snack [name=うまいぼう サラミ味, price=10, ingredients=コーン,植物油脂,糖類,ポークパウダー,香辛料,パン粉,ビーフパウダー,酵母エキスパウダー,パセリ,調味料,香料,甘味料,カラメル色素,酸化防止剤] Snack [name=うまいぼう チーズ味, price=10, ingredients=コーン,植物油脂,チーズパウダー,乳糖,クリーミングパウダー,乳製品,パン粉,砂糖,食塩,香辛料,調味料,香料,パプリカ色素,甘味料,ph調整剤,乳化剤,ターメリック色素] Lession01 Demo end
変数名を変える
これまでのコードを見てみると、サラミ味のおかしの変数の名前はumaibou
、チーズ味のおかしの変数の名前はumaibouCheese
となっています。
サラミ味の変数名が少し抽象的なので、umaibouSalami
に変えたいと思います。
変数名を変えるのも手動で1行ずつ変更していくのではなく、Eclipseの機能を使った方が簡単で正確です。変更する変数名にカーソルをあわせておいた状態で、下記のメニューバーかキーボードのショートカットの操作から行えます。
- メニューバーの
リファクタリング(T)
→名前の変更(N)...
を選択 - キーボードのShift + Alt + Rを押す
するとエディタ上でカーソルがリファクタリングモードになりますので、変数名を変えてEnterを押して確定させます。
コンストラクタを実装する
さらにめんたい味のおかしを追加してみます。
// めんたい味のおかしを生成 Snack umaibouMentai = new Snack(); umaibouMentai.setName("うまいぼう めんたい味"); umaibouMentai.setIngredients("コーン,植物油脂,糖類,パプリカ,香辛料,パン粉,たん白加水分解物,たら調味パウダー,食塩,調味料,香料,パプリカ色素,甘味料");
めんたい味のおかしのオブジェクト情報を出力するコードも書きます。
System.out.println(umaibouMentai.toString());
Demo
クラスのmainメソッドを実行するとコンソールにめんたい味のおかしの情報が出力されたと思いますが、price
フィールドは0
となっています。これはオブジェクト生成後にセッターでpriceフィールドを初期化しなかったためです。フィールドの値は明示的に初期化しないとデフォルト値で初期化されます。price
フィールドの値が0で出力されたのはint型の初期値が0であるためです。
Snack [name=うまいぼう めんたい味, price=0, ingredients=コーン,植物油脂,糖類,パプリカ,香辛料,パン粉,たん白加水分解物,たら調味パウダー,食塩,調味料,香料,パプリカ色素,甘味料]
例えば、プログラムの仕様(ルール)で、name
、price
フィールドは必ずセットしなければならないと決められていた場合、このコードはバグにあたります。このバグは以下のようにprice
フィールドに値をセットするコードを追加すること対応できますが、これよりもっと適切な対応方法があります。
// めんたい味のおかしを生成 Snack umaibouMentai = new Snack(); umaibouMentai.setName("うまいぼう めんたい味"); umaibouMentai.setPrice(10); // この行を追加してバグ修正する umaibouMentai.setIngredients("コーン,植物油脂,糖類,パプリカ,香辛料,パン粉,たん白加水分解物,たら調味パウダー,食塩,調味料,香料,パプリカ色素,甘味料");
引数と取るコンストラクターを実装する
オブジェクト生成後に個別にセッターで値をセットするのではなく引数を取るコンストラクターを実装して初期化するようにします。
エディタでSnack.javaを開きコンストラクターを実装しますが、これもEclipseの機能を使って自動的に生成します。生成は下記のメニューバーかキーボードのショートカットの操作から行えます。カーソルは生成するコードを出力したい場所へ置いてください。
- メニューバーの
ソース(S)
→フィールドを使用してコンストラクタ―を生成(A)...
を選択 - キーボードのShift + Alt + Sを押しメニューの
フィールドを使用してコンストラクタ―を生成(A)...
を選
"フィールドを使用してコンストラクタ―を生成"画面(Fig12)で、初期化するフィールドにname
とprice
を選択し"生成"ボタンをクリックします。
下記が生成されたコンストラクタ―のコードです。コンストラクターはメソッドのように見えますが戻り値が無いことと、コンストラクタ―名がクラス名と同じでなければならないという特徴があります。
public Snack(String name, int price) { super(); this.name = name; this.price = price; }
このうちのsuper();
というコードは、親クラス(Super class)のコンストラクターを呼び出していますが、このコードは無くても良いので削除します。
また、引数のnameがnullだったりpriceが0以下だったり不正な値の場合はエラーになるようにassert
文を追加します。
public Snack(String name, int price) { assert name != null; assert price > 0; this.name = name; this.price = price; }
この修正を保存するとDemo
クラスに赤い×アイコンが付いたと思います。
デフォルトコンストラクター
3カ所に赤い×アイコンが付いていると思いますが、これはデフォルトコンストラクターが見つからないためのエラーです。 画面下の"問題"ビューを見ると(Fig14)、"コンストラクタ― Snack() は未定義です" というエラーが3つ表示されていると思います。
クラスに明示的にコンストラクタ―を1つも実装しない場合、デフォルトコンストラクタ―という暗黙のコンストラクターが追加されます。(ソースコード上はみえません) デフォルトコンストラクタ―は引数を取らず、また何の処理もしないコンストラクターです。
public Snack() { super(); }
これまでオブジェクトの生成は以下のようにコーディングしていましたが、このときに使用されるコンストラクターはデフォルトコンストラクターです。
Snack umaibouSalami = new Snack();
上記で2つの引数を取るコンストラクタ―を明示的に実装したことによりデフォルトコンストラクタ―が追加されなくなったため、デフォルトコンストラクターが見つからず(未定義)エラーになった、ということです。
Snack umaibouSalami = new Snack(); // この行がエラー umaibouSalami.setName("うまいぼう サラミ味"); umaibouSalami.setPrice(10); umaibouSalami.setIngredients("コーン,植物油脂,糖類,ポークパウダー,香辛料,パン粉,ビーフパウダー,酵母エキスパウダー,パセリ,調味料,香料,甘味料,カラメル色素,酸化防止剤");
このエラーを解消するために実装したコンストラクターを使うように修正します。同時にセッターメソッドで値をセットしているコードも不要になるので削除します。
Snack umaibouSalami = new Snack("うまいぼう サラミ味", 10); //umaibouSalami.setName("うまいぼう サラミ味"); // 不要になった行なので削除 //umaibouSalami.setPrice(10); // 不要になった行なので削除 umaibouSalami.setIngredients("コーン,植物油脂,糖類,ポークパウダー,香辛料,パン粉,ビーフパウダー,酵母エキスパウダー,パセリ,調味料,香料,甘味料,カラメル色素,酸化防止剤");
原材料のingredients
フィールドはオプション的な扱いなので、セットすべき値がある場合は明示的にセッターで値をセットします。
もし、原材料が不明な場合はingredients
フィールドに"未定”とセットしたい、という仕様があった場合はセッターで以下のようにコーディングするのではなく
umaibouSalami.setIngredients("未定");
以下のようにコンストラクターでセットする方が適切です。
public Snack(String name, int price) { assert name != null; assert price > 0; this.name = name; this.price = price; this.ingredients = "未定"; // オブジェクト生成直後は未定という値を持つ }
複数のコンストラクターを実装する
1つのクラスにコンストラクターを複数実装することができるので、Snack
クラスに3つの引数を取るコンストラクタ―を追加してみます。これをコンストラクタ―のオーバーロードといいます。
以下がこれまで実装したコンストラクタ―のコードです。
public Snack(String name, int price) { assert name != null; assert price > 0; this.name = name; this.price = price; this.ingredients = "未定"; } public Snack(String name, int price, String ingredients) { assert name != null; assert price > 0; this.name = name; this.price = price; this.ingredients = ingredients; }
これで、原材料にセットする値があるおかしの場合もコンストラクタ―でオブジェクトを初期化できます。
Snack umaibouSalami = new Snack("うまいぼう サラミ味", 10, "コーン,植物油脂,糖類,ポークパウダー,香辛料,パン粉,ビーフパウダー,酵母エキスパウダー,パセリ,調味料,香料,甘味料,カラメル色素,酸化防止剤");
コンストラクタ―から他のコンストラクタ―を呼ぶ
このコンストラクタ―の実装では、this.name
とthis.price
をセットするコードがまったく同じことを行っています。なのでこの重複するコードを以下のように最適化します。
まず、2つの引数を取るコンストラクタ―をthis(name, price, "未定");
というように修正しました。このコードのthis( ... )
という部分が3つの引数を取るコンストラクタ―を呼び出しています。これで重複するコードが取り除けました。
public Snack(String name, int price) { this(name, price, "未定"); // 3つの引数を取るコンストラクターを呼ぶ } public Snack(String name, int price, String ingredients) { assert name != null; assert price > 0; this.name = name; this.price = price; this.ingredients = ingredients; }
このコンストラクターでは"未定"という文字列が2カ所に表れているので、これを定数にします。
また、this.ingredients
は引数でnullが渡された場合は"未定"という文字列をセットするように3項演算子を使うように修正します。
private static final String UNDEFINED_INGREDIENTS = "未定"; public Snack(String name, int price) { this(name, price, UNDEFINED_INGREDIENTS); } public Snack(String name, int price, String ingredients) { assert name != null; assert price > 0; this.name = name; this.price = price; this.ingredients = ingredients == null ? UNDEFINED_INGREDIENTS : ingredients; }
レッスン1のさいごに
レッスン1は以上です。レッスン1ではクラス、フィールド、コンストラクタ―をEclipseの機能を使って書く方法を説明してきました。 Eclipseの機能を使うことでコーディングがより楽になります。
これまでに書いたJavaのソースコードは以下のようになります。
Snack.java
package com.example.lesson01.model; public class Snack { private static final String UNDEFINED_INGREDIENTS = "未定"; public Snack(String name, int price) { this(name, price, UNDEFINED_INGREDIENTS); } public Snack(String name, int price, String ingredients) { assert name != null; assert price > 0; this.name = name; this.price = price; this.ingredients = ingredients == null ? UNDEFINED_INGREDIENTS : ingredients; } /** * 名前 (必須) */ private String name; /** * 値段 (必須) */ private int price; /** * 原材料 (任意) */ private String ingredients; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } public String getIngredients() { return ingredients; } public void setIngredients(String ingredients) { this.ingredients = ingredients; } @Override public String toString() { return "Snack [name=" + name + ", price=" + price + ", ingredients=" + ingredients + "]"; } }
Demo.java
package com.example.lesson01; import com.example.lesson01.model.Snack; public class Demo { public static void main(String[] args) { System.out.println("Lession01 Demo start"); // サラミ味のおかしを生成 Snack umaibouSalami = new Snack("うまいぼう サラミ味", 10, "コーン,植物油脂,糖類,ポークパウダー,香辛料,パン粉,ビーフパウダー,酵母エキスパウダー,パセリ,調味料,香料,甘味料,カラメル色素,酸化防止剤"); // チーズ味のおかしを生成 Snack umaibouCheese = new Snack("うまいぼう チーズ味", 10, "コーン,植物油脂,チーズパウダー,乳糖,クリーミングパウダー,乳製品,パン粉,砂糖,食塩,香辛料,調味料,香料,パプリカ色素,甘味料,ph調整剤,乳化剤,ターメリック色素"); // めんたい味のおかしを生成 Snack umaibouMentai = new Snack("うまいぼう めんたい味", 10, "コーン,植物油脂,糖類,パプリカ,香辛料,パン粉,たん白加水分解物,たら調味パウダー,食塩,調味料,香料,パプリカ色素,甘味料"); System.out.println(umaibouSalami.toString()); System.out.println(umaibouCheese.toString()); System.out.println(umaibouMentai.toString()); System.out.println("Lession01 Demo end"); } }
続く後編ではJavaコードの書き方をもう少し踏み込んで説明します。(現在記事作成中です)
rubytomato.hateblo.jp