rubytomato's “Getting Started”

Webアプリケーション開発の入門的な記事を投稿していきます。

PostgreSQLのプロシージャで大量のテストデータを作成する

はじめに

PostgreSQLPL/pgSQLという言語で作成するプロシージャで大量のテストデータを作成する方法を簡単に説明します。 テストや検証で大量データが必要なときに、この記事のソースコードを改修して利用することを想定しています。

環境

この記事の内容はWindows 10で作成、動作確認しています。

参考

大量データを格納するテーブル

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という無名ブロックを実行するコマンドがあります。(標準SQLDOはありません。) 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でシンプルなプロシージャを作成する

はじめに

この記事はPostgreSQLPL/pgSQLという言語で作成するプロシージャについて、難しい処理は行わないシンプルなプロシージャをベースに、それを少しずつ拡張しながらプロシージャの開発について説明していきます。

環境

この記事の内容はWindows 10で作成、動作確認しています。

参考

シンプルなプロシージャ

最初に下記のシンプルなプロシージャで、プロシージャの構造について説明したいと思います。 このプロシージャは、実行するとコンソール画面に『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_foundtoo_many_rowsとで、別々のエラー後処理を行っていますが、同じエラー後処理で対応したい場合はORでつなげます。

BEGIN

  -- 何らかのエラーが起きる処理

EXCEPTION
  WHEN no_data_found OR too_many_rows THEN
    -- エラー後処理
END;

OTHERS

OTHERSという特殊なエラー条件があります。このエラー条件はQUERY_CANCELEDASSERT_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_warningsplpgsql.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のコーディングに最適なコードエディターなので、本記事でもこれを使って説明します。

ダウンロード

マイクロソフトダウンロードサイトにアクセスしてインストーラーをダウンロードします。

f:id:rubytomato:20200503230215p:plain
Fig.1

Windows版のインストーラーはUser InstallerSystem Installerzipの3種類あります。

  • User Installerはインストールに管理者権限が不要
  • System Installerはインストールに管理者権限が必要
  • zipはZipアーカイブを自身で展開して手動インストールするタイプ

この記事ではUser Installerの64bit版を選択しました。

f:id:rubytomato:20200503230302p:plain
Fig.2

インストール

2020年5月時点のWindows版のダウンロードファイル名はVSCodeUserSetup-x64-1.44.2.exeです。ダウンロードしたインストーラーを実行してインストールします。 基本的にはインストーラーのデフォルト設定のままインストールすれば問題ありません。追加タスクの選択画面(Fig.6)でいくつか設定ができますが、ここはお好みで設定してください。

f:id:rubytomato:20200503230413p:plain
Fig.3

f:id:rubytomato:20200503230428p:plain
Fig.4

f:id:rubytomato:20200503230444p:plain
Fig.5

f:id:rubytomato:20200503230459p:plain
Fig.6

f:id:rubytomato:20200503230514p:plain
Fig.7

f:id:rubytomato:20200503230531p:plain
Fig.8

インストールが完了するとWelcome画面(Fig.9)が立ち上がります。

f:id:rubytomato:20200503230614p:plain
Fig.9

バージョンの確認

メニューバーのHelpAbout でバージョンを確認できます。バージョンを確認するとFig.10の通りVersion 1.44.2 (user setup)がインストールされたことがわかります。

f:id:rubytomato:20200503230631p:plain
Fig.10

拡張機能のインストール

JavaScriptの学習に入る前にいくつか定番の拡張機能をインストールしておきます。 左側のサイドメニューの一番下のアイコンをクリックします。Fig11は拡張機能を管理するメニューで、マーケットプレイスから拡張機能を検索したりインストールすることや、インストールされている拡張機能やアンインストールすることができます。

f:id:rubytomato:20200503230735p:plain
Fig.11

拡張機能マーケットプレイスとは

拡張機能VSCodeの大きな特徴の1つです。拡張機能をインストールすることでVSCodeに新しい機能を追加することができます。どのような拡張機能があるかはマーケットプレイスというサービスで確認できます。

マーケットプレイスにアクセスすると、定番のものや最近人気が出たもの、新しく追加されたものなどを探せます。

Japanese Language Pack for Visual Studio Code

1つ目はメニューやメッセージを日本語化する拡張機能です。

検索フィールドに"japanese"と入力してEnterを押すと、検索結果が表示されます。この中から"Japanese Language Pack for Visual Studio Code"という拡張機能をインストールします。 インストールはFig.12の"Install"という緑色のラベルをクリックするだけです。

f:id:rubytomato:20200503230756p:plain
Fig.12

インストールすると再起動を促されるので再起動します。Fig.13が再起動後のWelcome画面でメニューが日本語化されています。(この画面下の"起動時にウェルカムページを表示"のチェックを外してください)

f:id:rubytomato:20200503230815p:plain
Fig.13

Visual Studio IntelliCode

次にインストールするのはAIを活用したIntelliSenseの機能が利用できる拡張機能です。コード補完で表示される候補がAIによって最適化されます。

f:id:rubytomato:20200503230836p:plain
Fig.14

Live Server

インストールするとローカルPCで開発用途の簡易HTTPサーバーを起動することができ、コーディング中のhtmlやJavaScriptなどの動作確認が簡単にできるようになります。 またホットリロードというファイルの変更を検知してブラウザ上のページを自動的にリロードする機能もあります。

f:id:rubytomato:20200503230946p:plain
Fig.15

とりあえず現時点ではこの3つだけインストールします。 これでVSCode拡張機能のインストールは完了です。

JavaScript学習用のプロジェクトを作成

学習環境が整ったので、以降はJavaScriptの学習に関する内容になります。 まずはVSCodeソースコードを管理できるようにプロジェクトを作成します。といってもソースコードを保存するフォルダーを作成して、そこにhtmlファイルとjsファイルを作成するだけです。

プロジェクトフォルダーの作成

この記事ではexercise-jsというフォルダーを作成しました。(作成場所やフォルダー名は任意です) 以後はこの場所にプロジェクトフォルダーがあるという前提で説明を行いますので、任意の場所にフォルダーを作成したい場合は適宜読み替えてください。

Fig.16はVSCodeexercise-jsを開いた直後の画面です。

f:id:rubytomato:20200503231006p:plain
Fig.16

htmlファイルの作成

JavaScriptの動作確認はブラウザ(この記事ではChromeを使用します)で行いますので、そのためのhtmlファイルを作成します。 Fig.17の"新しいファイル"を作成するアイコンをクリックし、ファイル名にindex.htmlと入力してhtmlファイルを作成します。

f:id:rubytomato:20200503231836p:plain
Fig.17

空のhtmlファイルがエディタ画面に表示されるのでFig.18のようにhtmlと入力し、候補からhtml:5を選択してEnterを押すとテンプレートのコードが展開されます。

f:id:rubytomato:20200503231905g:plain
Fig.18

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については後述します)。

f:id:rubytomato:20200503232011g:plain
Fig.19

この状態でLive Serverを立ち上げてブラウザでindex.htmlを表示してみます。 エディタ画面上で右クリックしメニューからOpen with Live Serverをクリックします。

f:id:rubytomato:20200503232034p:plain
Fig.20

ブラウザが起動し、Fig.21の画面が表示されたと思います。

f:id:rubytomato:20200503232049p:plain
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>

f:id:rubytomato:20200503232330p:plain
Fig.22

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フォルダーを作成します。

f:id:rubytomato:20200503232355p:plain
Fig.23

そのjsフォルダにmain.jsという名前のjsファイルを作成しFig.24のように1行だけコードを記述します。

f:id:rubytomato:20200503232417p:plain
Fig.24

次にindex.htmlを開き、下のgifのようにbodyタグの直前にscriptタグを追加します。

f:id:rubytomato:20200503232438g:plain
Fig.25

ファイルを保存するとページがリロードされます。 JavaScriptconsole.log(...)の出力を確認するには、ブラウザでF12(またはCtrl + Shift + i)を押して開発者ツールを開きconsoleタブを選択します。 consoleタブに"it works!"という文字列が表示されていれば成功です。

f:id:rubytomato:20200503232459p:plain
Fig.26

Live Serverについて

Live Serverを一度起動すると、ステータスバーにLive Serverのステータスが表示されるようになります。 Fig.27はポート5500でLive Serverが起動中であることを示していて、ここをクリックすると待機中になります。

f:id:rubytomato:20200503232515p:plain
Fig.27

待機中はFig.28のような表示になり、以降はここをクリックするだけで起動と待機を切り替えることができます。

f:id:rubytomato:20200503232532p:plain
Fig.28

設定

Live Serverの設定はユーザー設定から変えることができます。ユーザー設定はCtrl + ,を押すと表示されます(Fig.29)。 Live Serverの設定項目を探すには検索フィールドに"live server"と入力して絞り込みを行います。

f:id:rubytomato:20200503232751p:plain
Fig.29

主な設定項目

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はポートを変更したときの状態です。

f:id:rubytomato:20200503232907p:plain
Fig.30

ファンクションを作成する

index.htmlを開きFig.31のようにdivタグを追加します。

f:id:rubytomato:20200503232924g:plain
Fig.31

次に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"が表示されたと思います。

f:id:rubytomato:20200503233034p:plain
Fig.32

テンプレートリテラル

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"が定義されていないことが原因だとわかります。

f:id:rubytomato:20200503233102p:plain
Fig.33

これを厳密に言うと"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を読み込んでいる行を上の行と入れ替えている例です。

f:id:rubytomato:20200503233122g:plain
Fig.34

行を入れ替えたらファイルを保存し再度ブラウザで動作確認を行います。これでページに"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のようなテーブルが表示されていると思います。

f:id:rubytomato:20200503233929p:plain
Fig.35

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のようにスタイルが適用されたと思います。

f:id:rubytomato:20200503233951p:plain
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クラスまで作成しました。

f:id:rubytomato:20200404113228p:plain
Fig1.

レッスン2

レッスン2では、レッスン1で作成した「おかし」を表現するSnackクラスに加え、「おかしを売る店」を表現するShopクラスと「おかしを買う客」を表現するPersonクラスを使って、お店とお客がおかしを売買する機能をコーディングしていきます。

おかしを売買できる条件として以下の3つを仕様とします。

  • お店(Shop)は扱っているおかししか売れないこと
  • お客(Person)はおかしを買うのに代金を支払うこと
  • お客(Person)は買ったおかしをおかし袋に仕舞えること(最大5個まで仕舞える)

また、そのほかの仕様として

  • お店(Shop)はおかしをいくつでも販売できる。(おかしの在庫数は管理しない)
  • おかしの売買は『おかしの名前』で行う。(『おかしの名前』が一意で管理できる(重複しない)こと)

とします。

レッスン2用のパッケージを作成する

まずレッスン2用のパッケージcom.example.lesson02を作成し、lesson02パッケージにDemoクラスを作成します。Fig2はここまで作成した状態です。

f:id:rubytomato:20200404113719p:plain
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クラスのhashCodeequalsメソッドを正しくオーバーライドする必要があります。 たとえばSnackクラスのhashCodeequalsメソッドをオーバーライドしていない状態で以下のコードを実行すると、同じおかしの名前を持つ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つのオブジェクトを ”同じおかし”である(おかしの名前が同じなので)と判別するには、hashCodeequalsメソッドを以下のようにオーバーライドします。

  • メニューバーのソース(S)hashCode() および equals() の生成(H)...を選択
  • キーボードのAlt + Shift + Sを押し、メニューのhashCode() および equals() の生成(H)...を選択

"hashCode() および equals() の生成"画面(Fig3)のnameフィールドだけにチェックを入れ、"生成"ボタンをクリックします。

f:id:rubytomato:20200404113257p:plain
Fig3.

すると、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)でnamepriceフィールドにチェックを入れてコードを生成しなおします。

Shopクラスを作成する

まずcom.example.lesson02.modelパッケージを作成し、modelパッケージにShopクラスを作成します。 Fig4はmodelパッケージおよびShopクラスを作成した直後のプロジェクトの状態です。

f:id:rubytomato:20200404113314p:plain
Fig4.

以下がShopクラスを作成した直後のソースコードです。

package com.example.lesson02.model;

public class Shop {

}
フィールドを定義する

Shopクラスに、お店が扱うおかしの種類を持つSet型のsnackVarieties、おかしの売上金を持つint型のsalesの2つのフィールドを定義します。同じ名前のおかしを持たないようにsnackVarietiesSet型にしています。

アクセサメソッドは必要がないので実装しません。

/**
 * お店が扱うおかしの種類
 */
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クラスを作成した直後のプロジェクトの状態です。

f:id:rubytomato:20200404113340p:plain
Fig5.

以下が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を継承した例外クラスをスローするようにします。

f:id:rubytomato:20200404113424p:plain
Fig6.

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クラスの実装に移ります。

f:id:rubytomato:20200404113447p:plain
Fig7.

/**
 * おかしを売る
 * @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クラスまで作成しました。

f:id:rubytomato:20200330101516p:plain
Fig1.

つぎにレッスン1のはじめで

まで説明しました。

レッスン1(つづき)

レッスン1のつづきでは、Eclipseを使ったJavaプログラムの書き方をもう少し踏み込んで説明したいと思います。 このレッスンでは、おかしの情報を扱うSnackというクラスを題材に、クラス、フィールド、コンストラクタ―の定義方法を説明します。

Snackクラスを作成する

まずcom.example.lesson01.modelパッケージを作成し、modelパッケージにSnackクラスを作成します。 パッケージおよびクラスの作成方法が分からない場合は前編の記事をご覧ください。 Fig2はmodelパッケージおよびSnackクラスを作成した直後のプロジェクトの状態です。

f:id:rubytomato:20200404105736p:plain
Fig2.

以下がSnackクラスを作成した直後のソースコードです。

package com.example.lesson01.model;

public class Snack {

}
フィールドを定義する

Snackクラスに、おかしの名前を持つname、値段を持つprice、原材料を持つingredientsの3つのフィールドを定義します。 namepriceは必須のフィールドで、ingredientsは情報があればセットするというオプショナルなフィールドというルールにします。

package com.example.lesson01.model;

public class Snack {

    /**
    * 名前 (必須)
    */
    private String name;

    /**
    * 値段 (必須)
    */
    private int price;

    /**
    * 原材料 (任意)
    */
    private String ingredients;

}
アクセサメソッドを実装する

定義したフィールドのアクセサメソッドを定義します。アクセサメソッドとはフィールドのゲッターや、セッターメソッドのことです。 たとえばnameフィールドであれば、nameフィールドの値を取得するgetNameメソッドがゲッターメソッド、値をセットするsetNameメソッドがセッターメソッドです。 メソッドの先頭にgetsetが付くことからゲッター(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)...を選択

f:id:rubytomato:20200404105816p:plain
Fig3.

"getter および setter の生成"画面(Fig4)でフィールドをすべて選択し、"生成"ボタンをクリックします。

f:id:rubytomato:20200330101611p:plain
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クラスのインポート文がコーディングされていないために起きているエラーです。

f:id:rubytomato:20200404105859p:plain
Fig5.

インポート文を追加するために、赤い下線の部分にカーソルを置きCtrl + 1を押し、メニューからSnack をインポートします (com.example.lesson01.model)というメニューを選択します。これでインポート文が追加されエラーは解消します。

f:id:rubytomato:20200404105920p:plain
Fig6.

Snack umaibou = new Snack();というコードでは、変数の宣言とコンストラクターの呼び出しを行っています。 このコードによりumaibouというSnack型の変数に、Snackクラスのオブジェクトが代入されます。

Snack umaibou = new Snack();
^^^^^ ^^^^^^^   ^^^ ^^^^^^^
 |     |         |   |
 |     |         |   +--- コンストラクター名
 |     |         |
 |     |         +------- newキーワードでコンストラクタ―を呼び出す
 |     |
 |     +----------------- 変数名
 |
 +----------------------- 変数の型
オブジェクトのフィールドに値をセットする

オブジェクトを生成したらセッターメソッドを使ってオブジェクトのフィールドに値をセットします。 このときにEclipseのコード補完を使うとタイピング量が減り且つ正確にコードがかけます。 umaibou.とまでタイプするとFig7のようにコード補完の候補が表示されます。候補が表示されない場合はCtrl + Spaceを押してください。

f:id:rubytomato:20200404110041p:plain
Fig7.
セッターメソッドの候補だけを表示するには、さらにumaibou.setとまでタイプします。そうするとsetから始まるメソッドやフィールドだけが候補になるので選択が楽になります。この中から選択するとそのコードで補完されます。
f:id:rubytomato:20200404110059p:plain
Fig8.

以下が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ソースコードが開きます。

f:id:rubytomato:20200404110403g:plain
Fig9.

toStringメソッドをオーバーライドするコードは、これまでと同様にEclipseの機能を使って自動的に生成する方が効率がよく正確です。 生成は下記のメニューバーかキーボードのショートカットの操作から行えます。カーソルは生成するコードを出力したい場所へおいてください。

  • メニューバーのソース(S)toString() 生成(R)...を選択
  • キーボードのShift + Alt + Sを押しメニューのtoString() 生成(R)...を選択

"toString() 生成"画面(Fig10)が表示されるので出力するフィールドを選択します。ここでは3つすべて選択し"生成"ボタンをクリックします。

f:id:rubytomato:20200330102110p:plain
Fig10.

すると、以下のコードが追加されたと思います。@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を押して確定させます。

f:id:rubytomato:20200404110453g:plain
Fig11.

コンストラクタを実装する

さらにめんたい味のおかしを追加してみます。

// めんたい味のおかしを生成
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=コーン,植物油脂,糖類,パプリカ,香辛料,パン粉,たん白加水分解物,たら調味パウダー,食塩,調味料,香料,パプリカ色素,甘味料]

例えば、プログラムの仕様(ルール)で、namepriceフィールドは必ずセットしなければならないと決められていた場合、このコードはバグにあたります。このバグは以下のようにpriceフィールドに値をセットするコードを追加すること対応できますが、これよりもっと適切な対応方法があります。

// めんたい味のおかしを生成
Snack umaibouMentai = new Snack();
umaibouMentai.setName("うまいぼう めんたい味");
umaibouMentai.setPrice(10);    // この行を追加してバグ修正する
umaibouMentai.setIngredients("コーン,植物油脂,糖類,パプリカ,香辛料,パン粉,たん白加水分解物,たら調味パウダー,食塩,調味料,香料,パプリカ色素,甘味料");
引数と取るコンストラクターを実装する

オブジェクト生成後に個別にセッターで値をセットするのではなく引数を取るコンストラクターを実装して初期化するようにします。

エディタでSnack.javaを開きコンストラクターを実装しますが、これもEclipseの機能を使って自動的に生成します。生成は下記のメニューバーかキーボードのショートカットの操作から行えます。カーソルは生成するコードを出力したい場所へ置いてください。

  • メニューバーのソース(S)フィールドを使用してコンストラクタ―を生成(A)...を選択
  • キーボードのShift + Alt + Sを押しメニューのフィールドを使用してコンストラクタ―を生成(A)...を選

"フィールドを使用してコンストラクタ―を生成"画面(Fig12)で、初期化するフィールドにnamepriceを選択し"生成"ボタンをクリックします。

f:id:rubytomato:20200330102159p:plain
Fig12.

下記が生成されたコンストラクタ―のコードです。コンストラクターはメソッドのように見えますが戻り値が無いことと、コンストラクタ―名がクラス名と同じでなければならないという特徴があります。

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クラスに赤い×アイコンが付いたと思います。

f:id:rubytomato:20200404110600p:plain
Fig13.

デフォルトコンストラクター

3カ所に赤い×アイコンが付いていると思いますが、これはデフォルトコンストラクターが見つからないためのエラーです。 画面下の"問題"ビューを見ると(Fig14)、"コンストラクタ― Snack() は未定義です" というエラーが3つ表示されていると思います。

f:id:rubytomato:20200404110617p:plain
Fig14.

クラスに明示的にコンストラクタ―を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.namethis.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