はじめに
データを2方向のインデックスで管理するやり方として、二次元配列を使うことが多いかと思います。
String[][]names={{"山内","森","泉","吉川","渋谷"},{"松本","河野","水野","川口","牛尾"},{"嘉藤","西村","竹多","吉田","井藤"},{"川野","加藤","山田","佐々木","藤田"},{"柴田","尾崎","大森","丘","矢口"}};System.out.println(names[1][4]);// 牛尾
Image may be NSFW.
Clik here to view.
2方向のインデックスで管理するなら一次元配列であっても2方向のインデックスを用意すればデータ管理できるのではないかと思い、一次元配列で二次元配列的な考えを真似てみることにしました1。
準備
まずは二次元配列でいうところの横幅と縦幅を示す変数を2つ定義します。
以降、$width$を配列幅、$height$を配列高さと表現します。
intwidth=5;intheight=5;
そして先程の二次元配列を一次元化したものを、予め用意しておきます2。
String[]names={"山内","森","泉","吉川","渋谷","松本","河野","水野","川口","牛尾","嘉藤","西村","竹多","吉田","井藤","川野","加藤","山田","佐々木","藤田","柴田","尾崎","大森","丘","矢口"};Iterator<String>nameTokens=Arrays.stream(names).iterator();Person[]people=IntStream.range(0,width*height).mapToObj(i->newPerson(nameTokens.next())).toArray(Person[]::new);
Image may be NSFW.
Clik here to view.
配列を図にしたものを掲げます。この配列は、配列幅・配列高さ共に5の配列です。
各行の横方向のインデックスを$x$、各列の縦方向のインデックスを$y$とすると3、例えば「牛尾」を示すインデックスは、$x=4,\ y=1$となります。
また、各行の末尾の要素における横方向のインデックスは$$x=width-1$$となり、各列の末尾の要素における縦方向のインデックスは$$y=height-1$$となります。
実践
要素の走査
要素の走査は、次のコードを用いて行います。
for(inti=0,x=0,y=0;i<array.length;i++,x++){...if(x==width-1){x=-1;y++;}}
対象とする配列の要素を全て(インデックス0からarray.length
まで)走査します。今回はpeople配列を定義しているので、array.length
をpeople.length
に置き換えます。for文で宣言されるカウンタ変数は次のとおりです。
変数 | 意味 |
---|---|
i | 配列のインデックス |
x | 配列の横方向のインデックスを示す値 |
y | 配列の縦方向のインデックスを示す値 |
if (x == width - 1) {...}
は、走査コードの...
部分の処理を終えた後、その行の末尾の要素ならばy
をインクリメントさせ、次の行に移ることを意味します。x = -1
は調節用です。
要素の抽出と置換
これを利用して、例えば走査コードの...
部分に
if(y==1&&x==4)System.out.println(people[i]);
と定義すれば、「牛尾」を出力することができます。
for(inti=0,x=0,y=0;i<array.length;i++,x++){if(y==1&&x==4)System.out.println(people[i]);// Person [name=牛尾]if(x==width-1){x=-1;y++;}}
また、
if(y==1&&x==4)array[i]=newPerson("ほげ");
と定義すれば、「牛尾」を「ほげ」に置き換えることができます。
for(inti=0,x=0,y=0;i<array.length;i++,x++){if(y==1&&x==4)array[i]=newPerson("ほげ");if(x==width-1){x=-1;y++;}}
毎回手動で定義するのは面倒くさいことになると思い、モジュール化しました4。
Matrixクラス
classMatrix<T,R>{privateT[]array;publicfinalintwidth;publicfinalintheight;publicinti;publicintx;publicinty;privateConsumer<Matrix<T,R>>action;privateFunction<Matrix<T,R>,R>function;privateMatrix(T[]array,intwidth,intheight){this.array=array;this.width=width;this.height=height;}static<T,R>Matrix<T,R>of(T[]array,intwidth,intheight){if(ObjectUtils.isEmpty(array))thrownewIllegalArgumentException("第1引数にとる配列が空又はnullです。");if(array.length!=width*height)thrownewIllegalArgumentException("第1引数にとる配列の長さと「第2引数×第3引数」の値が一致しません。");returnnewMatrix<>(array,width,height);}Matrix<T,R>setAction(Consumer<Matrix<T,R>>action){if(Objects.nonNull(function))function=null;this.action=action;returnthis;}Matrix<T,R>setAction(Function<Matrix<T,R>,R>function){if(Objects.nonNull(action))action=null;this.function=function;returnthis;}@SuppressWarnings("unchecked")Rget(intxIndex,intyIndex){if(isIllegalIndex(xIndex,yIndex))returnnull;returnsetAction(m->{if(m.y==yIndex&&m.x==xIndex)return(R)array[m.i];returnnull;}).run();}Matrix<T,R>put(Tobj,intxIndex,intyIndex){if(isIllegalIndex(xIndex,yIndex))returnthis;setAction(m->{if(m.y==yIndex&&m.x==xIndex)array[m.i]=obj;}).run();returnthis;}T[]array(){returnarray;}Matrix<T,R>insertShiftLeft(Tobj,intxIndex,intyIndex){if(isIllegalIndex(xIndex,yIndex))returnthis;setAction(m->{if(m.i<width*yIndex+xIndex)array[m.i]=array[m.i+1];if(m.y==yIndex&&m.x==xIndex)array[m.i]=obj;}).run();returnthis;}Matrix<T,R>insertShiftRight(Tobj,intxIndex,intyIndex){if(isIllegalIndex(xIndex,yIndex))returnthis;T[]cloneArray=array.clone();setAction(m->{if(width*height-m.i-1>width*yIndex+xIndex)array[width*height-m.i-1]=cloneArray[width*height-m.i-2];if(m.y==yIndex&&m.x==xIndex)array[m.i]=obj;}).run();returnthis;}Rrun(){if(Objects.isNull(action)&&Objects.isNull(function))thrownewIllegalStateException("順次処理を定義してください。");Rresult=null;Supplier<R>l_action=null;if(Objects.nonNull(action))l_action=()->{action.accept(this);returnnull;};elseif(Objects.nonNull(function))l_action=()->function.apply(this);for(inti=0,x=0,y=0;i<array.length;i++,x++){this.i=i;this.x=x;this.y=y;result=l_action.get();if(Objects.nonNull(result))break;if(x==width-1){x=-1;y++;}}returnresult;}privatebooleanisIllegalIndex(intxIndex,intyIndex){if(xIndex<0||width-1<xIndex)returntrue;if(yIndex<0||height-1<yIndex)returntrue;returnfalse;}}
戻り値なしと戻り値ありの両方を想定しました。<T, R>of(...)
のT
の部分に配列の型を指定します。R
の部分については、戻り値ありの場合は戻り値の型を指定します。戻り値なしの場合はjava.lang.Void
を指定し、戻り値をなしとすることを明示します5。
Matrix.<Person,Void>of(people,width,height).setAction(m->{if(m.y==1&&m.x==4)System.out.println(people[m.i]);// Person [name=牛尾]}).run();
setActionメソッドで走査コードの...
部分を定義してからrunメソッドを呼び出します。関数型インターフェースを用いており、i
、x
、y
はそれぞれm.i
、m.x
、m.y
と置き換えて使います。m
はラムダ式の引数名6なので、m
でなくても構いません。
戻り値ありの場合は、少し複雑ですが次のようにreturn
付きの関数型インターフェースを利用します。
何らかの条件と一致するときに、null以外のオブジェクトを返すように定義して使います。また、条件と一致しないときはnullを返すようにします7。
StringpersonName=Matrix.<Person,String>of(people,width,height).setAction(m->{if(m.y==1&&m.x==4)returnpeople[m.i].toString();returnnull;}).run();System.out.println(personName);// Person [name=牛尾]
これらの操作は、getメソッド・putメソッドとして実装しています。
Matrix<Person,Person>m=Matrix.of(people,width,height);System.out.println(m.get(0,3));// Person [name=川野]System.out.println(m.get(3,2));// Person [name=吉田]System.out.println(m.get(4,1));// Person [name=牛尾]m.put(newPerson("ほげ"),0,3);System.out.println(m.get(0,3));// Person [name=ほげ]
要素の挿入とシフト
一次元配列を利用しているので、次のように定義すれば要素を挿入することができます。
配列の長さは固定なので「山田」以前の要素は左に一つずつずれ、先頭の要素(山内)は消えます。
intxIndex=2;intyIndex=3;Matrix.<Person,Void>of(people,width,height).setAction(m->{if(m.i<width*yIndex+xIndex)array[m.i]=array[m.i+1];if(m.y==yIndex&&m.x==xIndex)array[m.i]=newPerson("ほげ");}).run();
反対に、次のように定義すれば「山田」以降の要素は右に一つずつずれ、末尾の要素(矢口)は消えます。一つの配列だけで操作すると参照コピーの問題からか$x=0$指定時の要素のシフトが期待どおりとならないため、一旦配列を複製し、その配列から元の配列に対し操作を施すようにしています。
intxIndex=2;intyIndex=3;Person[]cloneArray=people.clone();Matrix.<Person,Void>of(people,width,height).setAction(m->{if(width*height-m.i-1>width*yIndex+xIndex)array[width*height-m.i-1]=cloneArray[width*height-m.i-2];if(m.y==yIndex&&m.x==xIndex)array[m.i]=newPerson("ほげ");}).run();
これらの操作は、insertShiftLeftメソッド・insertShiftRightメソッドとして実装しています。
Matrix.<Person,Void>of(people,width,height).insertShiftLeft(newPerson("ほげ"),2,4);// 「大森」と「丘」の間に「ほげ」を挿入し、左にシフトする。
Matrix.<Person,Void>of(people,width,height).insertShiftRight(newPerson("ほげ"),2,4);// 「尾崎」と「大森」の間に「ほげ」を挿入し、右にシフトする。
まとめ
一次元配列でも、二次元配列的なデータ管理を行うことができました。後で配列幅や配列高さを変えたくなっても$width$や$height$の値を変えるだけで対応することができます。
intwidth=7;intheight=2;String[]names={"山内","森","泉","吉川","渋谷","松本","河野","嘉藤","西村","竹多","吉田","井藤","川野","加藤"};Iterator<String>nameTokens=Arrays.stream(names).iterator();Person[]people=...// 省略Matrix<Person,Person>m=Matrix.of(people,width,height);System.out.println(m.get(3,1));// Person [name=吉田]
他にも、やろうと思えば次のように各行の最も左側にあるnullでない要素を一つずつ抽出するメソッドをMatrixクラスに定義することもできます。走査コードの変数群を使えば、右側、上側、下側と、四方の要素を出力することができます。
@SuppressWarnings("unchecked")publicT[]getNonNullRightElements(){T[]elements=(T[])Array.newInstance(array.getClass().getComponentType(),height);setAction(m->{if(Objects.nonNull(array[m.i]))elements[m.y]=array[m.i];}).run();returnArrayUtils.removeAllOccurrences(elements,null);}
これを応用すれば、次の図のような当たり判定があるマスをピンポイントに抽出することもできます。
Image may be NSFW.
Clik here to view.
あくまでも好奇心からなる実験的なものです。決してアンチ二次元配列などではありません。 ↩
Personクラスでは、名前を表すフィールドを定義し、名前を出力するためのtoStringメソッドをオーバーライドさせています。 ↩
$x,\ y$は、共に正の方向に1ずつ増加する(インクリメントする)ものとします。 ↩
戻り値なしと戻り値ありを想定し関数型インターフェースを導入したので、大分コードが汚くなってしまいました。可読性を向上させる方法などございましたら、教えていただけるとありがたいです。 ↩
あくまでも明示するだけなので、実際にはnullなVoidオブジェクトが返されます。 ↩
ここではMatrixの頭文字として使っています。 ↩
条件と一致しないときにnull以外を返してしまうと、必ずそのオブジェクトが返されてしまいます。 ↩