次は内部ルーチンについて説明します。ルーチン = メソッドと思ってください。VBの Private Function や、強いて言えばCOBOLのPERFORMの飛び先のセクションや段落に近いです。
先ほどの演習ではたかだか一つのテキストボックスの内容をそのまま一つ出力するだけでした。しかしより多くの出力があった場合、毎回毎回 replaceAll で変換処理を記述するのも少々煩わしいものがあります。そこで出力HTMLの変換部分を内部の共通ルーチン化しておきましょう。
今まで作ったサーブレットでは public void doGet メソッドを使ってきました。このメソッドは外部、APサーバーであるTOMCATから呼ばれるため public にしてきました。内部ルーチンはあくまでそのクラス内だけで使うメソッドで、外部には公開せず外部からは使わせません。そのために private を指定します。
・メソッド定義の書き方
書き方は
アクセス修飾子 戻り値の型 メソッド名(引数) となります。
アクセス修飾子は基本的に public または private と思ってください。意味は前述のとおりです。
戻り値の型は、そのメソッドの処理結果として返るデータの型を指定します。doGet や doPost のように戻り値を持たない場合、 void と指定します。
メソッド名は文字通りメソッドの識別子で、呼び出すときの名前です。
引数はそのメソッドを使う場合に引き渡すべきデータの種類を記述します。引数を持たないメソッドの場合は () と空の括弧を書いてください。引数を持つ場合は 引数の型 引数名 が一組となり、複数の引数があるときはこの組を , で区切って必要な分、記述します。
さて今回内部ルーチン化する処理ですが、あくまで内部用なのでアクセス修飾子は private になります。
戻り値は変換結果の文字列なので String になり、先に引数を説明すると変換もとの文字列だけなので () の中は (String 引数名) となります。
最後にメソッド名ですが、これはHTMLEncodeにしましょう。
マイクロソフトのWebサーバーであるIISはAPサーバーの機能も持っており、サーバー側で動くVB Scriptで処理を記述するASP(Active Server Page)という機能があります。この中でServerオブジェクトにHTMLEncodeというメソッドがあり、これが今回やろうとしているようなHTMLの変換をより完全にしてくれます。このメソッドの名前をちょっと拝借しましょう。
以上をまとめると内部ルーチンのメソッド定義部分の例は
private String HTMLEncode(String p_src_str)
となると思います(引数名はあくまで例です)。
あとはこのメソッドに対する { } の中で、実際の処理を記述するわけです。
さてこのメソッドを終わる(抜ける)ときですが、 return (); を記述してください。今回は戻り値は String 型なので、return (結果がString型の式); となります。式は変数、引数、String を返すメソッドや計算式、いずれでも可能です(return の () は無くてもいいんですが、目印になるということで私の趣味で付けてます)。
・書く場所と使い方
書く場所はクラス内ならどこでもいいんですが、public のメソッド定義の後にしましょう。今回は doGet もしくは doPost メソッドがあるので、それの後ろにしましょう。
この内部ルーチンの使い方ですが、メソッドを使うときの書き方は クラス名.メソッド名 です。
今回クラス名は自分自身になるのでそれを表す特別なクラス名である this を使います。従って this.HTMLEncode(引数) となります。
このサンプル を見る前に自分で作ってみると思いますが、次のようなテストをしておきましょう。
自分が作った内部ルーチンを呼び出すところを、 this.HTMLEncode(null) としてみてください。
それとこれだけだとあまり面白くないので、ついでにJSP編の最後に作った「色指定チェック用JSP」もサーブレット化し、かつ内部ルーチンを使ってみましょう。
さてHTMLEncodeという内部ルーチンを作ったので、サーブレットからの出力が複数項目ある場合でも簡単に変換できるようになりました。
しかしこの機能は今後あなたが作る全てのサーブレットや、さらにJSPでも簡単に使えたほうが便利です。もちろんソース上で該当メソッド部分をコピーして、新たに作るソースに貼り付けてもいいんですが、あまりスマートじゃありません。
そこでこのHTMLEncodeを言わば外部共通ルーチン化しておきましょう。
そのための仕組みがパッケージ化です。Javaが持っている、標準で組み込まれる以外の機能や、外部から持ってきた機能を使うときにインポートを行いました。同じようにHTMLEncodeも、これを必要とするプログラムでインポートすることにより使えるようにしよう、ということです。
インポートする場合の基本的な記述方法は パッケージ名.クラス名 でした。このうちパッケージ名の付け方について説明します。
Java推奨の方法は「属する組織のドメイン名を逆にしたもの」です。例えばこれが出ているサイトのドメインは nihon-eng.co.jp なので、パッケージ名は jp.co.nihon-eng が推奨パッケージ名になります。さらにその下にサブパッケージになる名前を付けて細分化することも可能です。
こうする理由は重複を無くすため、です。様々なところでそれぞれの独自のクラスを作りパッケージ化しようとします。そうすると同じクラス名になるものも当然出てくるでしょう。そうしたときにクラス名だけを見ると同じでも、パッケージ名までみると必ず一意になるようにするためです。組織のドメイン名を元にしておけば、組織内でクラス名を一意になるようにしておけば世界中どこにいっても一意になります(ドメイン名が全世界で一意になるように管理されているので)。
しかし今回はJava勉強用環境なので、あなたの名前をパッケージ名としておけばいいでしょう。ここからの説明とサンプルでは、私の名前を元にした yok というパッケージを使っていきます。
さてパッケージ化したソースファイルとクラスファイル(実行ファイル)ですが、それを置いておくディレクトリ構造はパッケージ名と対応付いたものにしておく必要があります。
具体的に jp.co.nihon-eng パッケージで例を示すと、Javaソースがあるディレクトリをカレントディレクトリとすると、そこから\jp\co\nihon-engという3階層のディレクトリを作ります。そこの下にパッケージ化するJavaソースを置いてコンパイルします。
そしてそのパッケージを使うJavaソースをコンパイルする際は、コンパイル時に参照されるためパッケージのクラスファイルをそこに置いておく必要があります。
同様に実行環境についてもディレクトリを作ります。クラスファイルについては %CATALINA_HOME%\webapps\TestAp\WEB-INF\classes にコンパイル後のクラスファイルを置いてました。そこから\jp\co\nihon-engという3階層のディレクトリを作り、そこにパッケージ化したクラスファイルを置きます。ですから%CATALINA_HOME%\webapps\TestAp\WEB-INF\classes\jp\co\nihon-eng がパッケージ化したクラスファイルの置き場所になります。
サーブレットのソースファイルの置き場も実行ファイルと同じ %CATALINA_HOME%\webapps\TestAp\WEB-INF\classes にしている場合、私の名前を元にしたyokをパッケージ名の例にすると %CATALINA_HOME%\webapps\TestAp\WEB-INF\classes\yok がパッケージ化したソースとクラスファイルの置き場所です。
ここでJSPのパッケージ化について説明しておきます。
まずJSPはAPサーバーによってサーブレットソースに変換された上でコンパイルされます。TOMCATの場合サーブレットに変換されたソースは %CATALINA_HOME%work\Catalina\localhost\TestAp に作成されます。これをコンパイルしたクラスファイルも同じディレクトリに作られます。
JSPではソース中に、次に説明するパッケージ化の宣言を書くことは出来ません。サーブレットへの変換時に自動的に挿入されます。TOMCATの場合は
package org.apache.jsp;
です。
ただしJSPソースファイルの置き場所によってこれを変えることが出来ます。
今までの説明でJSPソースファイルは %CATALINA_HOME%\webapps\TestAp に置いていました。ここにyokというディレクトリを作り、そこにJSPソースファイルを置くようにします。そうするとこのディレクトリ構造が自動生成されるパッケージに反映され、
package org.apache.jsp.yok;
というパッケージ宣言が作成されます。
この場合はサーブレットに変換されたソースとコンパイル結果のクラスファイルは、 %CATALINA_HOME%work\Catalina\localhost\TestAp\yok に作成されます。これを実行する場合はブラウザのアドレスバーには http://localhost:8080/TestAp/yok/JSP名.jsp と入力します。パッケージ化したサーブレットとの対応が取りやすくなります。
それと「名前無しパッケージ」についても説明しておきましょう。これはパッケージ名が無い、パッケージ化していないクラスのことです。
パッケージ化していないため、前に書いたディレクトリ構造の話も当てはまりません。ソースファイルについては今まで作ってきたサーブレットのソースと同じ階層のディレクトリに置いておき、実行環境についてもクラスファイルの置き場所もサーブレットのクラスファイルと同じ、 %CATALINA_HOME%\webapps\TestAp\WEB-INF\classes に置いてください(同じ階層に置かないといけません)。
この下の「ソースの構成」以下の説明の中で「パッケージ化したクラス」と「名前無しパッケージ」で違う部分は、「パッケージ化の宣言」有無と、「名前無しパッケージ」のクラスは使うときにインポートの必要がないことです。その他の説明は同じです。
・クラス名
Javaの基本は1クラス分のソース = 1ファイルです。HTMLEncodeはメソッド名なので、パッケージ化するにあたりクラス名を付ける必要があります。ここは本来のオブジェクト指向からは若干外れますが、機能中心にして Tools というクラス名にしておきましょう。つまりHTMLEncodeメソッドを記述するソースファイルは Tools.java にする、ということです。
今後自分で共通機能としてパッケージ化したいものは、当面このファイル中にメソッドとして記述していけばいいでしょう。
・パッケージ化の宣言
パッケージ化するクラスのソースでは一番先頭に、このクラスをどのパッケージに含めるかを指定します。書き方は
package パッケージ名;
となります。
あとは通常のサーブレットソースの書き方とだいたい同じです。
・コンストラクタ
一番異なるのは コンストラクタ という特別なメソッド記述が必要な点です。これはクラス名と同じ名前を持つメソッドになります。
ただしメソッドと違い、戻り値は指定しません。また public を指定しておかないと、このクラスを定義したパッケージ外から使うことが出来なくなります。
この後ろの「・使い方 」で説明するように、あるクラスを使う場合はまずそのクラスを実体化(インスタンス化)させる必要があります。コンストラクタとはその実体化のときにだけ、暗黙のうちに呼び出されるメソッドです(実体化以外のところで明示的に呼ぶことは出来ません)。
今回は無いのですが、そのクラスがインスタンス変数を持ち、かつクラスの実体化のときにそのインスタンス変数を初期化したり、特定の動作をさせたいときに、その内容を記述します。これも今回は必要ありませんが、実体化の処理時に引数が必要なら、それも記述してください。
今の段階ではToolsクラスには特別なコンストラクタは必要ありません。従ってToolsというメソッドを定義し、具体的な処理は何もなく { と } だけを書いておいてください。
なおこの引数を持たず、処理が何もないコンストラクタのことを、デフォルトコンストラクタ と呼びます。あるクラス定義があって、それがコンストラクタを一つも宣言していないときには自動的にデフォルトコンストラクタが提供されます。
それから先ほどの演習課題では内部ルーチンとしていたので private 宣言をしていました。しかし今度は共通機能として作るので、Tools以外のクラスからも使われます。そのため外部から使えるように public 宣言にする必要があります。
以上をまとめるとこうなります。
package yok;
public class Tools
{
// Toolsクラスのコンストラクタ
public Tools()
{
}
public String HTMLEncode(String p_src_str)
{
if (p_src_str == null)
{
return (null);
}
else
{
return (p_src_str.replaceAll("&", "&").
replaceAll("/", "/").
replaceAll("<", "<").
replaceAll(">", ">").
replaceAll("\"", """).
replaceAll("!", "!").
replaceAll("\\?", "?").
replaceAll("=", "=").
replaceAll("%", "%"));
}
}
}
※ なおこれは文字列操作の練習の意味合いが強く、実行効率は全く考慮していません。それを内部ルーチンやパッケージ化の練習の材料として、そのまま使ってきました。
もっと処理効率を考慮したものは後ほど例示しますので、現バージョンは間違っても実システムで使ったりしないでください。
・使い方
まずはこれを使うほうのサーブレットもしくはJSPソースで yok.Tools をインポートします。
次にクラス型の変数を定義します。型は今回作成したクラスであるToolsになります。
そしてそのクラスを実体化(インスタンス化)させて、そのインスタンスへの参照をTools型の変数に代入します。ここの部分を書いておくと
Tools w_Tools = null; // Toolsクラスのインスタンス用
w_Tools = new Tools(); // Toolsクラスを実体化(インスタンス化)する
というように実体化するクラス名の前に new という演算子を付けて、クラス型の参照データ型変数に代入します。Toolsクラスのコンストラクタの定義を見るとわかるように、引数を必要としていないのでこうしてください。
これで晴れてHTMLEncodeメソッドが使える、というわけです。使うときには w_Tools.HTMLEncode と記述します。
念のためパッケージを使うほうのサンプル を置いておきます。
さてコンストラクタの説明も済んだので、前に説明した文字列( String )クラスのコンストラクタと、文字列バッファ( StringBuffer )について簡単に触れておきます。
ここまでは String クラスを使うときは例えば、 String型変数 = p_request.getParameter("パラメータ名"); のようにしてました。これは request クラスの getParameter メソッドは戻り値として文字列を返す、つまり String のインスタンスを返すので、そこへの参照を String 型変数に格納していました。このように他が生成したインスタンスを代入するのではなく、自分で String のインスタンスを生成するためのコンストラクタについて主なものを説明します。
String の主なコンストラクタ
コンストラクタ
引数(パラメータ)
説明
String
−
空文字列の String を生成する
char[] 文字配列
指定した文字配列の内容を文字列にした String を生成する
char[] 文字配列, int 開始位置, int 文字数
指定した文字配列の開始位置から文字数分の内容を文字列にした String を生成する
StringBuffer 文字列バッファ
指定した StringBuffer の内容を文字列にした String を生成する
StringBuffer 型に対する toString() メソッドと同じになる
String 文字列
指定した String の内容をコピーした String を生成する
String 型に対する clone() メソッドと同じになる
文字列は変更不可でした。文字列の一部を変更すると、変更後の新しい文字列が生成されました。文字列バッファ( StringBuffer )はこれと違い変更可能です。最初に確保した文字列バッファに対してその一部を変更することができ、新しい文字列バッファが生成されることはありません。そのため一部や全体を頻繁に変更をするような場合、新しいインスタンスが生成されないので String よりも効率的です。なおここでいう変更とは既存の文字の並びの後ろに別のものを追加したり、削除することも含みます。
文字列バッファは容量とデータ長を持っています。インスタンス化する際に必要量のあたりが付くのであれば、容量を指定したほうがいいと思います。中に入る文字列の長さが文字列バッファの変更操作によって容量を超えるときは自動的に拡張されます。
データ長は実際に文字列バッファに入っている文字列の長さです。従って 容量 ≧ データ長 の関係が成り立ちます。
頻繁に変更を行う処理があるときは String よりも StringBuffer の方が処理性能(速度、メモリ効率)は良いのですが、 StringBuffer には String の equals メソッドに対応するものがありません。そのため比較をするときな自分でメソッドを作成するか、比較の度に String に変換する必要があります。
まずメソッドに先立ち、 StringBuffer クラスのコンストラクタについて説明します。
StringBuffer のコンストラクタ
コンストラクタ
引数(パラメータ)
説明
StringBuffer
−
容量16文字分の StringBuffer を生成する
int 容量
指定した容量の文字数分の StringBuffer を生成する
String 文字列
指定した文字列の文字数分の StringBuffer を生成し、内容はその文字列と同じにする
メソッド中で指定する位置については String と同じ考え方です。つまり最初の文字は先頭から0番目なので、文字位置は0 になります。
StringBufferクラスの主なメソッド
メソッド
戻り値
引数(パラメータ)
説明
capacity int
−
文字列バッファの現在の容量を返す
length int
−
文字列バッファ中の文字列の長さ(文字数)を返す
setLength void
int 容量
文字列バッファの容量を指定した長さに変更する
現在文字列バッファにある文字数よりも小さい値を指定したときは、はみ出る分は切り捨てられる
大きい値を指定したときはnull文字で埋められる
charAt char
int 位置
指定した位置にある文字を返す
substring String
int 開始位置
指定した開始位置から最後まで切り出した部分文字列を、新しい文字列として返す
substring String
int 開始位置, int 終了位置
指定した開始位置から、指定した終了位置の直前の文字までを切り出した部分文字列を、新しい文字列として返す(終了位置の文字は、切り出した部分文字列には含まれません)
indexOf int
String 検索文字列
文字列中で指定した検索文字列が、指定した開始位置以降で最初に現れる位置を返す
検索文字列が無いときは-1が返る
String 検索文字列, int 開始位置
文字列中で指定した検索文字列が、指定した開始位置以降で最初に現れる位置を返す
検索文字列が無いときは-1が返る
toString String
−
文字列バッファ中の文字列を、文字列(String )として返す
getChars void
int 開始位置, int 終了位置, char[] 受取り文字配列, int 受取り側格納位置
文字列バッファ中の文字列から、指定した開始位置から、指定した終了位置の直前の文字までを切り出した部分文字列を、指定した文字列配列の指定した格納位置から格納する
append StringBuffer
char 連結文字
文字列バッファ中の文字列の最後に、指定した文字を追加した文字列バッファを返す
char[] 連結文字配列
文字列バッファ中の文字列の最後に、指定した文字配列の内容を追加した文字列バッファを返す
String 連結文字列
文字列バッファ中の文字列の最後に、指定した文字列を追加した文字列バッファを返す
StringBuffer 連結文字列バッファ
文字列バッファ中の文字列の最後に、指定した文字列バッファの内容を追加した文字列バッファを返す
insert StringBuffer
int 挿入位置,char 挿入文字
文字列バッファ中の指定した位置に、指定した文字を挿入した文字列バッファを返す
挿入位置以降の文字列は挿入した分だけ後ろにずれ、文字数は挿入した文字数だけ増える(この場合は1文字分)
int 挿入位置,char[] 挿入文字配列
文字列バッファ中の指定した位置に、指定した文字配列の内容を挿入した文字列バッファを返す
挿入位置以降の文字列は挿入した分だけ後ろにずれ、文字数は挿入した文字数だけ増える
int 挿入位置,String 挿入文字列
文字列バッファ中の指定した位置に、指定した文字列を挿入した文字列バッファを返す
挿入位置以降の文字列は挿入した分だけ後ろにずれ、文字数は挿入した文字数だけ増える
delete StringBuffer
int 開始位置, int 終了位置
指定した開始位置から、指定した終了位置の直前の文字までを、削除した文字列バッファを返す
削除した以降の文字列は削除した分前にずれ、文字数は削除した分だけ減る
replace StringBuffer
int 開始位置, int 終了位置, String 置換文字列
指定した開始位置から、指定した終了位置の直前の文字までを、置換文字列で置換えた文字列バッファを返す
動作としては delete メソッド後 insert メソッドを行ったのと同じになる
setCharAt void
int 置換文字位置, char 置換文字
指定した位置の文字を、指定した置換文字で置換する
やっとここでデータ構造の配列の話になります。
大抵の本では配列の説明は、Javaで扱うデータ型や変数の定義のところで一緒にしていると思います。ではなぜこのページではここまで引っ張ったかというと、Javaの配列は仕組みを考えると意外と深いんです。そんな仕組みなんてことを気にしなければもっと前で説明しても良かったんですが、メソッド呼び出しの引数の受渡しに関する説明と関連付けたかったので、ここまで持ってきました。
・基本的な配列の使い方
これはそれほど特別なことは無いのですが、強いて言うならば「変数が配列であることの宣言」と「実際にある要素数分の配列を生成する」ことが、原則として別になる、ということでしょうか。
配列である宣言の書き方は
データ型[] 変数名;
または
データ型 変数名[];
のどちらかです。空の [ と ] を付けることによって配列である宣言になります。これは1次元でしたが2次元の配列であれば
データ型[][] 変数名;
または
データ型 変数名[][];
となります。
[] はデータ型の後ろに付けてもいいし、変数名の後ろに付けてもどちらでも構いません。自分がプログラムを組むときにどちらかに統一してください。両方を混ぜた書き方は絶対に止めましょう。例えば
データ型[] 変数名[];
と書くとこれは2次元配列宣言の意味になってしまいます。
次に配列の生成ですが
配列宣言した変数名 = new データ型[ 要素数];
となります。2次元であれば
配列宣言した変数名 = new データ型[ 要素数][ 要素数];
です。例えば 配列宣言した変数名 = new データ型[ 3][ 4]; とすると3×4の2次元配列が出来ます。
また宣言と生成を一つにして、
データ型[] 変数名 = new データ型[ 要素数];
や
データ型 変数名[] = new データ型[ 要素数];
という書き方もできます。
また予め初期値を入れた配列を生成したい場合、
データ型[] 変数名 = { データ型の式, データ型の式, ・・・データ型の式};
としてください。データ型の式の部分は、定数、変数、計算式、メソッドです。定義した変数名のデータ型と同じデータ型になるものであればOKです。
・指標の範囲
Javaで宣言する配列の大きさは、その配列の要素数です。またJavaでは配列の先頭の要素を指す指標値は0 になります。
従って指定可能な指標の範囲は、0 から 要素数 - 1 まで、になります。
例えばVBでは配列指定は
Dim 変数名(最大の指標値) as データ型
となります。最小の指標値は Option Base宣言に依り、デフォルトは0です。Option Base 1と設定すると最小の指標値は1になります。
従ってVBの場合、実際の要素数はOption Baseの設定で変ることになります。
Javaでは配列の大きさは要素数で指定ですし、先頭の指標は0と決まっています。
・配列の仕組み
配列宣言時に指定するデータ型は、基本データ型だけではなく参照データ型も可能です。
配列の説明の前では内部ルーチンの外部ルーチン、パッケージ化について説明しました。その中でクラスを使うときは new を使って実体化(インスタンス化)させる、という説明もしました。さて、これは配列の実体を生成するときも同じでしたね。
実は配列というのはクラス型の参照データ型になります。これは配列そのものの話で、基本データ型の配列を定義した場合、個々の要素は基本データ型です。
例えば int[] w_tbl = new int[10]; と定義したとき、w_tbl は参照データ型になりますが、w_tbl[4] は int 型の基本データ型になります。
また配列そのものはクラス型ですが、これには length というフィールドがあり、今の例でいうと w_tbl.length とすると int 型でその配列の大きさ、つまり要素数が返ります。これを利用すれば配列の全要素に対して何か処理をしたい場合、例えば
int w_ix = 0;
int[] w_tbl = new int[ 10];
for ( w_ix = 0; w_ix < w_tbl.length; w_ix++)
{
// 配列の全要素に対する処理(w_tbl(w_ix)に対する処理)
}
といった書き方ができます。
・配列の次元
先ほどの配列定義の例では2次元配列の記述も示しました。しかし実はJavaには1次元配列しかありません。2次元の場合は配列の配列、配列そのものがクラス型の参照データ型ということで、配列への参照(ポインタ)の配列が出来ているのです。
この1次元配列しかなく高次元配列は配列の配列の・・・で実現する、という点についてはCOBOLと同じです。COBOLでは配列はOCCURSという繰返し項目宣言をします。例えば
05 変数名 PIC S9(9) COMP OCCURS 10.
といった具合に。これは4バイトの符号付2進整数が10個の配列、という意味になります。これは基本項目ですが2次元の配列なら集団項目を使い、
05 変数名1 OCCURS 10.
10 変数名2 PIC S9(9) COMP OCCURS 10.
とすると変数名1は10個の配列であり、その内訳として4バイトの符号付2進整数が10個の配列を含んでいる、という意味です。変数名2を使えば2次元配列で4バイトの符号付2進整数が10×10個、という意味になります。
COBOLの集団項目はCの構造体やその他の言語のRecord型と同じようなものと考えてください。
ちなみにJavaでは構造体やRecord型というものはありません。これらは全てクラスとして定義することになります。
この仕組みを利用するとJavaの場合、ある次元の要素毎に異なる要素数を持つ配列、を定義することができます。言葉だと説明しにくいですが、配列の概念を図にしたのでそれを見てもらえば分かると思います。大抵の言語では例えば2次元なら長方形の表イメージになりますが、Javaではでこぼこの表が作れる、ということです。
1次元配列のとき、予め初期値を入れた配列を生成したい場合に
データ型[] 変数名 = { データ型の式, データ型の式, ・・・データ型の式};
と説明しました。多次元の場合これを利用して変数を指定し例えば、
int[] w_tb1 = { 1, 2, 3};
int[] w_tb2 = { 4, 5};
int[][] w_tb3 = { w_tb1, w_tb2};
ということも出来ます。
・配列のコピー
「配列の仕組み」で説明したように基本データ型の配列の場合、配列の個々の要素は基本データ型ですが、その配列そのものはクラス型の参照データ型になります。そのため配列をコピーしようとして = を使うと文字列型と同じようなことが起きます(ポインタ情報のみコピーされ実体はコピーされない)。
配列そのものはクラス型ということで、配列の実体をコピーしたい場合は Object クラスの clone() メソッドを使用します。Object クラスとはJavaのクラス階層のルートになり、全てのクラスの基底クラスにあたります。ということは配列もクラス型なのでこのObject を継承しているため、基底クラスのメソッドである clone() メソッドが使用可能、ということです。
この clone メソッドの戻り値は Object なので代入のときに配列型に変換する必要があります。例えば int[] w_tb1 = { 1, 2, 3}; を int[] w_tb2; にコピーしたい場合、
int[] w_tb1 = { 1, 2, 3};
int[] w_tb2 = null;
w_tb2 = (int[]) w_tb1.clone();
としてください。
いままでの多次元配列と配列のコピーについてサンプルを提示します。以下のソースを切り貼りして実行し、ソースと実行結果を見比べてみてください(ファイル名は array_test1.jsp とでもしておいてください)。
<%@ page
contentType="text/html; charset=Shift_JIS"
%>
<!doctype html public "-//W3C//DTD HTML 4.0 Transitional//EN"
"http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<head>
<title> Java配列のテスト1 </title>
</head>
<body bgcolor="#aaddaa">
<%
int w_i = 0;
int w_j = 0;
int[] w_tb1 = {1, 2, 3};
int[] w_tb2 = {4, 5};
int[][] w_tb3 = {w_tb1, w_tb2};
int[] w_tb4 = null;
int[] w_tb5 = null;
out.print("<br />");
out.print("w_tb3 size=" + w_tb3.length);
for (w_i = 0; w_i < w_tb3.length; w_i++)
{
for (w_j = 0; w_j < w_tb3[w_i].length; w_j++)
{
out.print("<br />");
out.print(" w_tb3[" + w_i + "][" + w_j + "] = " + w_tb3[w_i][w_j]);
}
}
out.print("<br /><br />");
out.print("Before w_tb1 size=" + w_tb1.length);
for (w_i = 0; w_i < w_tb1.length; w_i++)
{
out.print("<br />");
out.print(" w_tb1[" + w_i + "] =" + w_tb1[w_i]);
}
w_tb4 = w_tb1; // w_tb4 に w_Tb1 自体を代入
w_tb4[0] = 7;
w_tb4[1] = 8;
w_tb4[2] = 9;
out.print("<br /><br />");
out.print("After w_tb1 size=" + w_tb1.length);
for (w_i = 0; w_i < w_tb1.length; w_i++)
{
out.print("<br />");
out.print(" w_tb1[" + w_i + "] =" + w_tb1[w_i]);
}
out.print("<br /><br />");
out.print(" w_tb4 size=" + w_tb4.length);
for (w_i = 0; w_i < w_tb4.length; w_i++)
{
out.print("<br />");
out.print(" w_tb4[" + w_i + "] =" + w_tb4[w_i]);
}
w_tb5 = (int[])w_tb1.clone(); // w_tb5 に w_Tb のコピーを代入
w_tb5[0] = 10;
w_tb5[1] = 11;
w_tb5[2] = 12;
out.print("<br /><br />");
out.print("After w_tb1 size=" + w_tb1.length);
for (w_i = 0; w_i < w_tb1.length; w_i++)
{
out.print("<br />");
out.print(" w_tb1[" + w_i + "] =" + w_tb1[w_i]);
}
out.print("<br /><br />");
out.print(" w_tb5 size=" + w_tb5.length);
for (w_i = 0; w_i < w_tb5.length; w_i++)
{
out.print("<br />");
out.print(" w_tb5[" + w_i + "] =" + w_tb5[w_i]);
}
%>
</body>
</html>
メソッド呼び出しにおける引数の受渡し方法ですが、Javaでは全て「値渡し」(値呼び、ということもある)になります。これは呼び出し側が渡す引数のコピーを作り、呼ばれたほうはコピーを参照して操作する、というものです。
他の渡し方には「参照渡し」、「名前渡し」(名前呼び)があります。多くの言語、アセンブラ、COBOL、C、VBのデフォルトでは「参照渡し」が使われています。これが一番実行効率が良いためです。ただ効率がいい反面、思わぬ副作用やミスの温床ともなり勝ちです。これに比べると「値渡し」ははるかに安全です。
ちなみに「名前渡し」については実装した言語を見たこと(体験したこと)が無いので、説明できるほどよく知りません(Algol系のAlgolNやAlgol68という言語で実装されていたと思いますが実物を見たことがありあません。Adaはどうだったかな)。
さてJavaの引数渡しですが、渡す引数のコピーを作り呼ばれたほうはコピーを操作ということは、呼ばれた側でどうその引数を操作しようが、自分の内部の変数と同じように扱っても呼び出し側の領域に影響しない、ということです。従って意図せずして呼び出し側のデータを壊すことが無いので安全、ということです。
ということは「Javaのデータ型と定義 」で説明した私流の変数名の付け方、自分の内部の変数は変数名の頭にw_を付け、引数として受け取る領域は変数名の頭にp_を付ける、というのはあまり意味が無いように思えます。
しかし実はここに落とし穴があり、コピーが作られるので呼ばれた側でどう操作しても安全なのは引数が基本データ型の場合だけ なのです。クラス型の参照データ型の場合、コピーがつくられるのはそのもの自体、クラスの実体(インスタンス)へのポインタ情報だけなのです。
参照データ型とはいえ String 型については変更不可であり、呼ばれた側で操作して文字列を変更すると新しい文字列が生成され、それのポインタ情報が引数で受けた場所に入ります。そのためこの場合にはやはり安全なのですが、これ以外の参照データ型の場合は直接呼出し側のデータを操作することになります。
まずは次のサーブレットをファイル名 parm_test.java で切り貼りしてコンパイルし、ソースと実行結果を見比べてみてください。
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.io.IOException;
public class parm_test extends HttpServlet
{
private PrintWriter wc_out;
public void doGet (HttpServletRequest p_request,
HttpServletResponse p_response)
throws IOException, ServletException
{
int w_i = 0; // 配列インデックス用
int[] w_tb1 = {1, 2, 3, 4, 5};
int w_dat = 0;
String w_str_dat = null;
w_dat = 0;
w_str_dat = "initial text";
p_request.setCharacterEncoding("Shift_JIS");
p_response.setContentType("text/html; charset=Shift_JIS");
wc_out = p_response.getWriter();
wc_out.print(
"<!doctype html public \"-//W3C//DTD HTML 4.0 Transitional//EN\" "
+ "\"http://www.w3.org/TR/REC-html40/loose.dtd\">");
wc_out.print("<html>");
wc_out.print("<head>");
wc_out.print("<title> Javaの引数渡しのテスト </title>");
wc_out.print("</head>");
wc_out.print("<body bgcolor=\"#aaddaa\">");
wc_out.print("<br /><br />");
wc_out.print("Before Call : w_tb1 size=" + w_tb1.length);
for (w_i = 0; w_i < w_tb1.length; w_i++)
{
wc_out.print("<br />");
wc_out.print(" w_tb1[" + w_i + "] =" + w_tb1[w_i]);
}
wc_out.print("<br />");
wc_out.print(" w_dat =" + w_dat);
wc_out.print("<br />");
wc_out.print(" w_str_dat =" + w_str_dat);
wc_out.print("<br /><hr /><br />");
// 内部メソッド呼び出し
this.innr_method(w_tb1, w_dat, w_str_dat);
wc_out.print("After Call : w_tb1 size=" + w_tb1.length);
for (w_i = 0; w_i < w_tb1.length; w_i++)
{
wc_out.print("<br />");
wc_out.print(" w_tb1[" + w_i + "] =" + w_tb1[w_i], w_dat);
}
wc_out.print("<br />");
wc_out.print(" W_dat1 =" + W_dat1);
wc_out.print("<br />");
wc_out.print(" w_str_dat =" + w_str_dat);
wc_out.print("<br />");
wc_out.println("</form>");
wc_out.println("</body>");
wc_out.println("</html>");
}
// 内部メソッドの定義
private void innr_method(int[] p_tb, int p_dat, String p_str_dat)
{
int w_i = 0; // 配列インデックス用
for (w_i = 0; w_i < p_tb.length; w_i++)
{
p_tb[w_i] = p_tb[w_i] + 10;
}
p_dat = p_dat + 10;
p_str_dat = p_str_dat + " add text";
// wc_outはインスタンス変数なので、parm_testクラス内であれば
// アクセス可能
wc_out.print("innr_method : p_tb size=" + p_tb.length);
for (w_i = 0; w_i < p_tb.length; W_i++)
{
wc_out.print("<br />");
wc_out.print(" p_tb[" + w_i + "] =" + p_tb[w_i]);
}
wc_out.print("<br />");
wc_out.print(" p_dat =" + p_dat);
wc_out.print("<br />");
wc_out.print(" p_str_dat =" + p_str_dat);
wc_out.print(""<br />"<br />"<hr />"<br />");
}
}
図解してみるとこんな感じになると思います。
こういった状況もあり、やはり私流の変数名の付け方はJavaにおいても十分に意味があると思います。
変数名のつけ方2
ここの説明やサンプルについて、メソッドやコンストラクタが受け取る引数(パラメータ)の名前に関してはp_を頭に付ける、としてきました。実を言うとこれをやめてa_にし、説明・サンプルとも変更しようかと思ったことがありました。
昔からプログラミングをしている人にとっては、引数をパラメータと呼ぶことに違和感はないと思います。サブルーチンに渡すパラメータ(引数)、関数が受けるパラメータ(引数)といったように。
しかし引数はどちらかというとパラメータ(Parameter)ではなくアーギュメント(Argument)です。それでa_という接頭辞に変えようかとも思ったのです。
しかし今説明したように、Javaの引数渡しの仕組みがあります。クラス型の参照データ型の場合は呼ばれたほうが引数として受けるのはポインタ情報だけでした。ということで極私的な名前付けの引数はパラメータだからp_は、その引数が参照データ型の場合は同時にポインタのp_も表せて二重の意味をもたせられるので、このままで行きます。
さて配列の説明も終わりましたので、約束どおり「文字列の変換」いらい説明に使用した HTMLEncode の、もう少し処理効率を考慮したものを例示します。
いくつか説明していないクラスやメソッドが出てくるのですが、まずはソースを見てください。その後で内容について説明します(ソースの注釈を見れば十分かもしれませんが)。
public class Tools
{
// 変換対象文字の文字コード(unicode)範囲上限と下限
private final int c_MIN_CVT_CH = 33;
private final int c_MAX_CVT_CH = 63;
// 変換対象判定と変換後文字列テーブル
// 文字コードに対応するエントリがnullなら変換対象外、null以外ならその
// 文字列に変換する
private final String[] c_CV_TBL =
{"!", """, null, null, "%", "&", null, null, null, // 33-41
null, null, null, null, null, "/", null, null, null, null, // 42-51
null, null, null, null, null, null, null, null, "<", "=", // 52-61
">", "?"}; // 62-63
// Toolsクラスのコンストラクタ
public Tools()
{
}
public String HTMLEncode(String p_src_str)
{
StringBuffer w_ret_str = null;
Character w_ch = null;
char[] w_tst_ch = null;
int w_ix = 0;
int w_ch_cd = 0;
if (p_src_str == null)
{
return (null);
}
else
{
// 変換後文字列長として元データの倍のサイズを仮確保
// (StringBufferは自動拡張されるが、とりあえず倍とした)
w_ret_str = new StringBuffer(p_src_str.length() * 2);
// 元データの文字列を文字配列に変換
w_tst_ch = p_src_str.toCharArray();
// 元データを1文字ずつ変換対象かチェックしていく
for (w_ix = 0; w_ix < w_tst_ch.length; w_ix++)
{
// 取り出した1文字をCharacterクラス型に変換し、
// 文字コードを取得する
w_ch = new Character(w_tst_ch[w_ix]);
w_ch_cd = w_ch.hashCode();
// 文字コードが変換対象範囲外ならそのまま変換後にセット
if ( (w_ch_cd < c_MIN_CVT_CH)
|| (w_ch_cd > c_MAX_CVT_CH))
{
w_ret_str = w_ret_str.append(w_tst_ch[w_ix]);
}
else
{
// 文字コードから配列要素位置に変換
w_ch_cd = w_ch_cd - c_MIN_CVT_CH;
if (c_CV_TBL[w_ch_cd] == null)
{
// 変換対象外ならそのまま変換後にセット
w_ret_str = w_ret_str.append(w_tst_ch[w_ix]);
}
else
{
// 変換対象なら変換後文字列をセット
w_ret_str = w_ret_str.append(c_CV_TBL[w_ch_cd]);
}
}
}
// 変換後のStringBufferをStringに変換して返す
return (w_ret_str.toString());
}
}
・考え方
このやり方の考え方(アルゴリズム)を先に説明します。
文字列を1文字ずつ分解した文字配列に変換する。
変換対象文字に対応した配列を作る。配列の要素には変換後の文字列を入れておく。文字に対応した要素が空のときは変換対象外とする。
文字配列を使い、1文字ずつ判定する。配列の要素位置を決めるときは文字を文字コードに変換し、その文字コードから要素位置を決める。
といった仕組みです。
文字に対応した配列を作るといっても、全ての文字分作るのはあまりにも無駄すぎます。変換対象文字は予め決まっているので、それを文字コード順にしたときに範囲が決まります。そこでその範囲の下限から上限までの配列だけを作成すれば十分です。配列を見に行く前に、その文字のコードが範囲内かどうかを判定すればよい、ということです。
なお文字コードですがJava内部では全てunicodeという体系で管理されている、という説明をしました。unicodeにおいて先頭の128文字分のコードは、ASCIIコードと同じです。厳密にはASCIIコードは16進で2桁の範囲なので、この先頭に16進の00(X'00')を付加したものがASCIIコードに対応するunicodeコードになります。
今回変換を行う文字は全て、1文字の英記号なのでASCII文字になります。そこで変換対象文字のASCIIコードを調べておけば、自ずとunicodeコードも分かる、ということです。
さて今までは文字列を扱うのに String クラスを使ってきました。 String クラスの場合は文字列中の文字の変更はできず、変更後の新しい文字列が生成されます。しかし今回の課題のように文字列の変更を頻繁に行うときは、これははなはだ非効率的です。そのため変更可能な文字列を扱うため、 StringBuffer クラスを使うようにしました。
考え方として文字列を文字に分解し、1文字ずつ変換対象判定を行っていきます。通常は1文字を表す際には char 型という基本データ型を使ってきました。しかし今回は文字に対応する文字コードを取得する必要があります。そのために Character クラスを使いました。
Character クラスのインスタンスは内部に一つの char 型データ、1文字を持っています。このクラスの hashCode メソッドにより、その文字のunicodeコードが取得できます。結果は int 型です。
これで変換対象文字のunicodeコードの上限下限をチェックし、範囲内であれば変換後文字列テーブル(配列)を検索します。配列の要素位置は0から始まるので、取得したunicodeコードから下限のunicodeコードを引くことにより、配列要素位置になるように変換しています。
変換後文字列テーブルの内容が null なら変換対象外なので元の文字をそのまま、 null 以外なら変換後文字列としてテーブルの要素を、変換後文字列にセットしています。このときは StringBuffer クラスの append メソッドを使って結合していきます。
実際のところ変換する文字がこの程度の数ならば、こんなこった仕組みを使わずに1文字ずつ if 〜 else if で判定して変換してもよかったんです。パフォーマンスにも差がそれほどない、どころかむしろその方が若干速いかもしれません。
ここは一つ、こういったテーブル(配列)の使い方もある、ということで参考にしておいてください。
・実行結果
最初に例示したHTMLEncodeと今回直したHTMLEncodeの実行結果の差ですが、
・Windows2000 + SP3
・Java2 SDK SE 1.4.1
・TOMCAT4.1.18
・Intel Celeron1.2GHz(120MHz × 10)
・メモリ 256MB
という環境で、100文字のうち60文字が変換対象というデータで100,000回HTMLEncodeを実行しました。
結果は
・最初のHTMLCode :25.377秒
・今度のHTMLCode : 3.334秒
でした。
最初のものでは変換対象1文字ごとに毎回正規表現パターンチェックを行っていたので、それと元データを1文字ごとに一回だけチェックする方法との違いだと思います。
ここで HTMLEncode を利用して、違った形の実装方法を説明しておきます。
オブジェクトやクラスの説明として一番最初のほうの「Javaのデータ型と定義 」では、データとそのデータに対して適用可能な操作(メソッド)をまとめて定義したもの と説明しました。もう少しだけ説明しておくと、クラスはクラス変数やインスタンス変数を持つことができます(これらの違いや細かいことはもっと後で出てきます)。そしてこれらを操作するためのメソッド、クラス変数やインスタンス変数をクラスの属性と考えると、それらに外部から値を設定したり取り出すためのアクセサメソッドと分類されるもの(アクセサメソッドはメソッド名の最初の3文字が get や put で始めるのが慣例です)、クラスに処理を依頼する通常のメソッド、後はコンストラクタなどの特別なメソッドがあります。これらクラス変数やインスタンス変数とメソッドが一体となったものがクラスやオブジェクトになります。
さてもう一度上の改良版HTMLEncodeのソースを見てみましょう。
するとインスタンスレベルの変数は定義されてますが final を指定しているので定数としての定義です。HTMLEncodeメソッド自体もインスタンスレベルの定数は参照していますが、インスタンス変数の操作はしていません。つまりHTMLEncodeは受けた引数のみを操作して結果を返す、つまり機能だけを提供しています。
こうした機能だけを提供する、いわゆる共通関数や共通サブルーチンのようなメソッドの実装方法としてstatic メソッドがあります。
・staticメソッドの違い
static メソッドと通常のメソッドの違いはまず、そのメソッドのクラスをインスタンス化する必要がない、ということです。メソッドを使うときはそれを持っているクラスをインスタンス化して参照データ型変数に代入しておき、 そのクラスの参照データ型変数.メソッド名 とする必要があります。例えばHTMLEncodeメソッドであればToolsクラスをインスタンス化してTools型の変数w_Toolsに代入しておき、 w_Tools.HTMLEncode としていました。
それに対してstatic メソッドの場合は、もちろん通常のメソッドのようにそれを持っているクラスをインスタンス化してから呼び出してもいいのですが、インスタンス化せずに直接 クラス名.メソッド名 で呼び出せます。同じくHTMLEncodeを例にすると、ここのを説明を前提にするとyokパッケージがインポートされている必要がありますが、インスタンス化せずに直接 Tools.HTMLEncode と呼び出せます。
HTMLEncodeメソッドの定義部分は
public String HTMLEncode(String p_src_str)
となっていました。これをstatic メソッドにするときはstatic という修飾子を付けて
public static String HTMLEncode(String p_src_str)
とします。
・staticメソッドで操作(参照)可能な変数のレベル
通常のメソッドが操作できる変数は、
そのメソッド内に定義した局所(ローカル)変数
そのメソッドが受け取る引数(パラメータ)
そのメソッドを含むクラスのインスタンス変数
そのメソッドを含むクラスのクラス変数
でした。なおここでいう変数とは final 宣言をした定数も含みます。
一方static メソッドにした場合はインスタンス変数は操作(参照)できません。インスタンス変数というのはクラスをインスタンス化したときに、インスタンス毎に領域が確保されます。static メソッドをクラスをインスタンス化せずに直接 クラス名.メソッド名 で呼び出すということは、インスタンス化していないためこの領域も確保されないためです。
しかし通常のメソッドと同じように、クラスをインスタンス化してからメソッドを呼び出すときはインスタンス変数が使えてもいいように思うかもしれません。しかしそれではメソッドの呼出し方法によって動作が変わることになってしまいます。
あるクラスに複数のstatic メソッドがあり、それらで同じ領域の変数を操作したいときはクラス変数として定義する必要があります。
そのためには変数定義時にも static 修飾子を付けます。HTMLEncodeの最初の部分を抜き出すと
// 変換対象文字の文字コード(unicode)範囲上限と下限
private final int c_MIN_CVT_CH = 33;
private final int c_MAX_CVT_CH = 63;
となっていたものを
// 変換対象文字の文字コード(unicode)範囲上限と下限
private static final int c_MIN_CVT_CH = 33;
private static final int c_MAX_CVT_CH = 63;
とします。そうするとこの変数(この場合 final 宣言しているので定数扱いですが)はクラス変数になり、static メソッドから操作(参照)できるようになります。
※ 実際にはHTMLEncodeでインスタンス変数にしていたものは全て、メソッド内の局所変数でも良かったのです。しばらく後の方で出てくる説明の都合上、あえてインスタンス変数としていました。
いちおうstatic メソッドにしたHTMLEncodeを使うほうのサンプル を置いておきます。
ちなみにHTMLEncodeをstatic メソッドにしたものと通常の、インスタンス化してから呼び出すメソッドのものを100,000回呼び出すテストをしたところ処理時間は、
・static メソッド :4.337秒
・通常のメソッド:4.186秒
となりました。
たぶん、インスタンス化すると参照データ型にそのクラスへのポインタが入り、クラスのメソッドを呼び出すときはポインタを使ってアクセスするのに比べ、static メソッドのときはJava実行環境を通してクラスにアクセスすることの差だと思います。そのためstatic メソッドの方が若干オーバーヘッドがかかっている気がします。もっとも10万回で0.15秒の差ですから気にすることはないでしょう。
逆にインスタンス化するということは、そのクラスがインスタンスレベルの変数(定数)を持っていればインスタンス化するたびにその領域が確保されます。インスタンスレベルの変数(定数)を持っていなくても、インスタンス毎の管理情報が確保されるはずです。static メソッドを呼び出すときはインスタンス化をしなくてもよいので、これらの領域は確保されません。そのためメモリ使用上のオーバーヘッドは、インスタンス化する場合に比べて少ないと思います。
多くの言語ではJavaのメソッドに対応する関数や手続きの識別は、その名前だけで決まります。しかしJavaはそうではありません。メソッドの識別はその名前と引数の組み合わせで行われます。引数の組み合わせは、引数の型、数、並び順、によります。
これがどういうことかというと、一つのクラスに異なる引数を持つ同じ名前のメソッドが定義できる、ということです。
例えば String クラスの substring メソッドでは
substring( 開始位置) :開始位置から最後までを部分文字列として切り出す
substring( 開始位置, 終了位置) :開始位置から終了位置の前までを部分文字列として切り出す
という、同じ名前で異なる引数を持つメソッドがあります。
またこれは通常のメソッドだけでなく、コンストラクタメソッドについても同じことが言えます。コンストラクタが引数を持つとき、異なる引数を持つコンストラクタが定義できるということです。
ここでもHTMLEncodeを例にとり、より汎用性を上げていきながら説明しましょう。元にするのはstatic メソッドにしたHTMLEncodeです。
HTMLEncodeの引数は String クラスでした。そして内部ではその引数を char 型の配列に変換しています。またある意味で String に似た StringBuffer クラスも使用しています。
さてHTMLEncodeを使う側も変換対象を char 型の配列や StringBuffer クラスとして保持しているかもしれません。そうしたときにHTMLEncodeを使うためだけに、メソッド呼出しの前で個別に String に変換するのは無駄というものです。HTMLEncodeが引数として char 型の配列や StringBuffer クラスも扱えれば済む話です。
HTMLEncodeメソッドの定義はいまは
public static String HTMLEncode(String p_src_str)
だけですが、他に
public static String HTMLEncode(StringBuffer p_src_str)
と
public static String HTMLEncode(char[] p_src_str)
というメソッド定義が必要になります。
さてもっとも手っ取り早いのはそれぞれのメソッドで、 char 型の配列や StringBuffer クラスで受けた引数をString クラスに変換して、引数が String のHTMLEncodeメソッドを呼び出すというものです。それぞれのメソッドはこうなります。
public static String HTMLEncode(StringBuffer p_src_str)
{
return (HTMLEncode(p_src_str.toString()));
}
public static String HTMLEncode(char[] p_src_str)
{
return (Tools.HTMLEncode(new String(p_src_str)));
}
なお今まで内部ルーチンを呼び出すときに指定していた this というのは、インスタンス化した自分自身のオブジェクトを表します。ここではstatic にしているためインスタンス化はしないことがあり得ます。そのため this は使えないので何も書かずにメソッド名だけを書くか、クラス名.メソッド名 の書き方にしてください(この場合のクラス名は自分自身のクラス名です)。
これだと String への変換を、呼び出し側でなく内部で自動的に行うので呼ぶほうは気にする必要がない、という面では改善です。ただ結局毎回毎回 String への変換をするだけなのであまり面白くありません。HTMLEncode(String p_src_str)の処理部分をコピーして、 char[] や StringBuffer を扱えるように変更してみましょう。
簡単にヒントを挙げてみます。 HTMLEncode(char[] p_src_str) ですが、
// 元データの文字列を文字配列に変換
w_tst_ch = p_src_str.toCharArray();
の部分は引数で文字配列を受けるので、そのまま使えるためこの変換はいりません。あわせて文字配列の変数、 w_tst_ch の定義も不要になります。これに伴いこの変数を使っている部分はすべて、直接引数を使うことになります。
次にHTMLEncode(StringBuffer p_src_str)ですが、 StringBuffer から指定した位置の一文字を取り出すときは charAt メソッドを使い、
文字列バッファ.charAt(位置) としてください。位置は int 型で、先頭の文字位置が先頭から 0 番目になります。戻り値は char 型です。そして先ほどと同様 w_tst_ch を使っている部分はすべて、直接引数を使うことになります。
あるいは StringBuffer クラスの getChars メソッドを使って文字配列にコピーしてもかまいません。APIドキュメントで調べて試してみましょう。具体的には
p_src_str.getChars(0, p_src_str.length(), コピー先文字配列, 0) となるはずです。このメソッドの戻り値は void です。
またHTMLEncode(String p_src_str)も StringBuffer と同様に、 String から指定した位置の一文字を取り出すときは charAt メソッドを使い、文字列.charAt(位置) と出来ます。そうすると先ほどと同様 w_tst_ch を使っている部分はすべて、直接引数を使うことになります。
さてここまで修正したソースで、各HTMLEncodeメソッドの処理を見てください。そうすると修正方法によって多少の差はありますが、1文字を取り出す
// 文字コードを取得する
w_ch = new Character(w_tst_ch[w_ix]);
の後ろ、
w_ch_cd = w_ch.hashCode();
からはみな同じはずです。ここから後ろの部分は
「その1文字が変換対象か調べ、対象なら変換後の文字列、対象外なら元の文字を変換後領域にセット」
しているのです。この「1文字の変換」処理を内部ルーチン化し、共通処理として使うようにしましょう。メソッド名は ChrEncode とします。引数は chr 型で変換対象文字と、 StringBuffer クラスで変換後領域の2つです。
ここまでの変更を加えたHTMLEncodeのToolsクラスのサンプル を置いておきます。最初のものからの変更点は
・static メソッド化
・「1文字分の変換」を内部ルーチン化
・クラスレベルの定数定義を内部ルーチン内に移動
です。
本当ならもっとオブジェクト指向らしくして「1文字分の変換」の ChrEncode は内部メソッドではなく、
Character クラスを拡張して encode というメソッドを持たせたいところです。
class CharacterX extends Character
{
// CharacterXクラスのコンストラクタ
public CharacterX(char p_ch)
{
// superクラス(基底クラス)のコンストラクタを呼び出す
// この場合は Character クラスのコンストラクタ
super(p_ch);
}
public String encode()
{
// superクラス(基底クラス)が保持している文字をEncodeする
// 変換対象文字の文字コード(unicode)範囲上限と下限
final int c_MIN_CVT_CH = 33;
final int c_MAX_CVT_CH = 63;
// 変換対象判定と変換後文字列テーブル
// 文字コードに対応するエントリがnullなら変換対象外、null以外ならその
// 文字列に変換する
final String[] c_CV_TBL =
{"!", """, null, null, "%", "&", null, // 33-39
null, null, null, null, null, null, null, "/", null, null, // 40-49
null, null, null, null, null, null, null, null, null, null, // 50-59
"<", "=", ">", "?"}; // 60-63
int w_ch_cd = 0;
// superクラス(基底クラス)が保持している文字の文字コードを取得する
w_ch_cd = super.hashCode();
// 文字コードが変換対象範囲外ならそのまま文字列にして返す
if ( (w_ch_cd < c_MIN_CVT_CH)
|| (w_ch_cd > c_MAX_CVT_CH))
{
return (super.toString());
}
else
{
// 文字コードから配列要素位置に変換
w_ch_cd = w_ch_cd - c_MIN_CVT_CH;
if (c_CV_TBL[w_ch_cd] == null)
{
// 変換対象外ならそのまま文字列にして返す
return (super.toString());
}
else
{
// 変換対象なら変換後文字列を返す
return (c_CV_TBL[w_ch_cd]);
}
}
return;
}
}
というようなのを作りたかったんですが、 Character クラスは public final class として宣言されています。
変数宣言で final 指定をするとその変数は以降での更新が出来なくなり、定数として扱えました。クラス宣言で final 指定をすると、これを拡張したクラスを作ることは出来なくなります。そのためこのやりたかった、 Character クラスを拡張したクラスを作ることは出来ません。ですので先ほどの内部ルーチン化したものまでとします。
・さてどうするか
この内部ルーチン化したHTMLEncodeの、「1文字の変換」という共通機能をルーチン化して使いまわすというのは、プログラム設計としてはごく真っ当だと思います。
最初に提示した内部ルーチン化していないHTMLEncodeを10万回呼び出すテストの結果は、処理時間は4秒ちょっとでした。しかしこの内部ルーチン化したほうだと10万回で約19秒かかりました。やはり1文字毎にメソッド呼出しを行うので、それのオーバーヘッドのせいですね。10万回で約19秒なので1回あたりの処理時間はごくわずかなんですが、HTMLEncodeが共通ルーチンの性格を持っている以上、この約5倍という差は気になります。
そこでいろいろ説明してきましたが、結局元に戻したものを最終版としましょう。
オブジェクト指向本来としてはシステム設計や保守、維持管理、他のシステムへの流用のしやすさにおいての効率性を求める意味合いが強く、こういう細かい処理性能上のいわば妥協はしないほうがいいんですが。しかし現実のシステムにおいてはどうしても処理性能は重要になります。それにJava自体が Character クラス、 Integer クラスとは別に char 型や int 型を基本データ型として持ち、処理性能上の妥協をしているわけですから良しとしましょう。。
この「・メソッドの識別 」の最初で、引数が StringBuffer や char の配列のHTMLEncodeからは、引数を String に変換して文字列引数のHTMLEncodeメソッドを呼び出す、という方法を説明しました。結局これに戻ることにします。
ただし文字列引数のHTMLEncodeメソッド内では引数を文字配列に変換しています。そこで引数が char の配列のHTMLEncodeを基準にし、引数が StringBuffer や String のときはこれを文字配列に変換してHTMLEncodeを呼び出すことにします。結局これ を最終版のサンプルとします。