LoginSignup
2
3

More than 5 years have passed since last update.

JavaFXとか使って手探り開発した時のメモ

Last updated at Posted at 2018-12-23

前書き

あくまでメモ書きですので、試行錯誤したり汚いことしたりします。
それでも「マンドクセ」とか思ったらよりやりやすそうなやり方を探したり。

そんなわけで基本的にはここに書いてあることは正しいとか正しくないとかって話じゃないし、順番も順序だててはいない。私の試行順である。
めちゃくちゃなのでここから何かを得ようとはしないほうがいい。もしそれでも何か探すというなら、目次から探したほうがたぶんよい。(ページ内検索してもいいか)

作業の片手間の息抜きなので、あまりこっち見ないけど、アドバイスとか指摘されると号泣しながら喜ぶ。

記事内容が迷走するので、一通り終わったら項目ごとに別記事にするかも(未定)

作ろうとしてるもの

数独解いてくれるモノ。学校の自由課題にて。
FXで描画された画面上にマスと各種ボタンを配置。
ユーザの手入力とボタン操作で動作させる。

新参者なのでエラーウィンドウ表示させたりみたいなことはしない。
soutなりserrに吐かせる。

環境その他

Java(8) ...はある程度学習済み(使用経験は浅い)
JavaFX(8) , FXML を使ってみる。
IDEとして、講師オヌヌメのintelliJIDEA(ver.3.2)最近アプデが入って若干UIとか変わった模様。リファクタ機能が強すぎて、こないだeclipse使ったらいろんなところかゆかった。

壁とか対処とか調べごととか

不慣れなJavaFXでまずつまずいたりする。

いくつかの簡単な対応について

  • FXMLで指定したfx:idはControllerでは@FXMLを直前で指定しないと怒られる。delimiter単位でつけないとやっぱり怒られる。
  • 配列xに、配列yをx=yみたいにして渡すと(仮称)参照値渡し(後述)となる。xとyは同じオブジェクトを指す名前となる。
    • 新しい別のオブジェクトとして扱いたい場合はcloneメソッドを使用して、x=y.clone()とする。
    • この問題はString型の同一の文字列を格納した別のオブジェクトを条件演算子==で結ぶ(すなわち、str==new String(str))と、オブジェクトの参照値を比較するために、falseが返されることを連想させる。ただしString型自体が若干特殊な仕様を含んでいるようなので、取り扱いに注意すべきか。
  • 数独では行と列のほかに、3x3のsubRegionでも重複を禁じている。この3x3のグループを順番に追う式を書いた。
for (int i = 0; i < 9; i++) for (int j = 0; j < 9; j++) {
  int idev3= i/3 , jdev3= j/3 ;

  int x= idev3 * 3 + jdev3 ,
      y= i % 3 * 3 + j % 3 ;

  subRegion[x][y] = cell[i][j]
//  逆関数が同様の式になる(名前忘れた)ので、[i][j]と[x][y]は逆にしても目的を果たす。
//  subRegion[i][j] = cell[x][y];
}
  • 二次元配列x = new Object[i][j]のlength`について。
    • x.lengthi
    • x[0].lengthj。ただしもしi==0ならば、ArrayIndexOutOfBoundsExceptionを吐く。

JavaFXで9x9の入力を表示したい

数独を解くっていうのだからこれは必須。

中学生の頃に学校で体験的に学習していた仮称10進BASICで、お遊びで数独解かせようとしたときは、スペース区切りで9つの数字(空白は0)9行を標準入力(?)にぶち込んでやってたけど、いちいち手入力がめんどくて死んだ。あと配列なので.contains(object)とかないです。forで「アルカナー」ってやってました。

ここでは入力だけして、解くのは別のクラスでやる予定なので、FX周りのコーディングでは配列でもいいんだけど。

しかしそもそもFXの入力に配列が使えるのかとかその辺からアヤシくなるのであった。(FXMLを使うと特に。データのやり取りのややっこしいことと言ったら...)

FXML内部の配列的データを配列的なままとりだす

create array of Label using FXML in JavaFX - StackOverflaw

FXML内でfx:id="myfxid"を与えたArrayListを用意して、その中に要素となるデータをぶち込みController側では@FXML ArrayList myfxid;みたいにして呼ぶと、中身も一緒に読み込めるとか。

でもこれ、81個のTextFieldを作りたかったら、FXML上で81個手打ち(コピペと微修正)とかsceneBuilder上のduplicateとかで用意してやらねばならない。めんどい。

TableViewにTextFieldを埋め込み

(YouTube) JavaFX TableView | Adding TextField in tableView Cell

Oracle公式 JavaFX UIコンポーネントの操作
の事例にTextViewの埋め込みをして見せるというもの。
TableColumnの名前毎にsetterやgetterを行にするクラスに入れて差し上げる必要があるらしく簡略化した書き方が思いつかないのでパス

既存のFXMLのレイアウト(primaryStage?)に、Contorollerとかから追記

これのやり方が地味に探しにくかった。
というか別件探ってたら出てきた。

最初僕は「今まで通りstartの中で記述すりゃええべ」とか思ってたわけで、Parent root = FXMLLoader.load(getClass().getResource("Example.fxml"));
した後、root.getChildren()からいじくろうとしたわけですが。(こういういじり方ができるかどうか把握してなかったけど)

IDEに「ダメです」って言われちゃって。出てくるのは「root.getChildrenUnmodifiable()」とかいうなんか名前からして変更不可って言ってないか?というようなの。
OracleせんせーのAPIリファレンスから見ると、案の定というか読み取り専用データを返すというもの。

ただし救いはあった。

ひとつ上の「TableViewにTextFieldを埋め込み」の動画の後半にて、@Override initialize(/*略*/)メソッドを見かけたので。

public class Controller implements Initializable {}と継承してやると、initializeメソッドを継承させて、その中でFXの表示物の初期化等ができる(?)。

若干古いけど、
タツノオトシゴの日記 JavaFXのControllerの書式について
にも記述がある。

上記の動画では、公式のほうの事例でApplicationClass.startに書かれていたfxの記述処理を、initializeメソッドの中で書いていた。

FXMLのほうでfx:idを設定したモノをControllerで操作してやれば、中身をいじって書き換えることもできるか?

実行順について

こうなってくると気になるのが実行順である。
「initializer」と呼ばれるものはおかしな実行順をたどることがあるので、スコープとともに確認しておきたい。
そこで用意したのが↓

TestApp.java
/* import 祭 */
public class TestApp extends Application {
  public static void main(String[] args) { launch(args); }

  @Override
  public void start(Stage primaryStage) throws IOException {
    System.out.println("set fxml 2 Parent root");
    Parent root = FXMLLoader.load(getClass().getResource("Test.fxml"));

    System.out.println("set root 2 scene");
    Scene scene =new Scene(root);

    System.out.println("set scene 2 primaryStage");
    primaryStage.setScene(scene);

    System.out.println("primaryStage.show");
    primaryStage.show();
  }
}
Test.fxml
<?xml version="1.0" encoding="UTF-8"?>
<!--?importの嵐-->
<AnchorPane prefHeight="100.0" prefWidth="100.0" xmlns="http://javafx.com/javafx/8.0.172-ea" 
xmlns:fx="http://javafx.com/fxml/1" fx:controller="Controller">
   <children> <!--Childrenの嵐--> </children>
</AnchorPane>
Controller.java
/* import いっぱい */
public class Controller implements Initializable {
  @Override
  public void initialize(URL location, ResourceBundle resources) {
    System.out.println("initializerCalled");
  }
}

IOEを丸投げしていいのかどうかしらないけどおおよそこんな感じ。
実行結果の標準出力が↓

set fxml 2 Parent root
initializerCalled
set root 2 scene
set scene 2 primaryStage
primaryStage.show

Process finished with exit code 0

要は
Test.mainから始まり、launchからstartして(この辺の理解はあいまい)
FXMLLoader.load(fxmlふぁいる)ってところでfxmlを読み込んで、「(load)
fxml読み込まれたときにfx:controllerで指定されたControler.javaを読み込んで、「(fxml)
initializeメソッドが実行されて 」(/fxml) 」(/load)
と、こういうことだな!(どういうことだ)

上のほう(既存のFXML~~の項の冒頭)で書いた、FXMLLoader.loadで取り出したParent rootに、直接書き込むことはできないが、loadされるときのfxmlファイルで指定したControllerから初期化することはできると、こういうことみたいですね。
やりたいことは実現したので、ぼくまんぞく。

@FXMLの指定は、読み込まれるときに自分(Controller)を読みだそうとしているfxmlに対して「こういうIDのものあったら情報くれ」ってしてるんでしょうかね。

phpのfor文の中にechoでphpのコード書いて配列ちっくなの(\$_val.1,\$_val2..."\$_val".\$_iみたいに)作ったりしたけど、こっちのほうが直観的に記述できて僕は好き。

最終的に(9x9入力フィールド)

FXML側で容れ物としてID付きGridPaneを用意してあげて、
Controller.initializeで中に納まるTextField(配列)の定義をしてあげることに。

アイディアとしてはteratailに質問として挙がっていた
【JavaFX】格子状に画像を配置する方法について【GUI】
の、質問者様のコードが大変参考になった。
(余談だけど、ついていた回答は、オブジェクトについての考え方がアヤシイかもしれない。
imageView.clone()みたいなことができれば話は別だが、そんなメソッドはないし...
まあかなり古い記事だし、FXMLの記述と内部仕様は違うかもしれないし。)

以下コード

Controllerクラス内

// FXMLであらかじめ用意。配置されるXY座標とか、Grid Line VisibleはFXML側で指定しておく。
  @FXML GridPane inputGrid;

// 後で太めの罫線を作ってあげたりしたいので、変更しやすいように。
  Double cellSize=30.0;

  @Override
  public void initialize(URL location, ResourceBundle resources) {

  // テーブルの行列の数
    int hnum = 9;    int vnum = 9;

  // 入力を要求する部分
    TextField[][] cellValues = new TextField[vnum][hnum];
        //後でそれぞれnewで与えるが、メモリ(ポインタ(あっ))を与えておかないとぬるぽ吐く。

    for (int i = 0; i < hnum; i++) {
      for (int j = 0; j < vnum; j++) {

        cellValues[i][j] = new TextField("");
        // コンストラクタ呼んでインスタンスオブジェクトを渡してあげないとやっぱりぬるぽ吐く

    // TextView側でPrefW/Hを指定してあげると、GridPaneがそれに合わせてリサイズしてくれる。
    // GridPane側のW/HをUSE_COMPUTED_SIZEにしてあげようね。
        cellValues[i][j].setPrefWidth(cellSize);
        cellValues[i][j].setPrefHeight(cellSize);
// 追記: WidthとHeightを同時に設定できるsetPrefSizeメソッドを見つけた。
//      下は上二行と同じ機能として働く
//      cellValues[i][j].setPrefSize(cellSize,cellSize);


    // GridPane上での座標値をTextViewのオプションにぶち込んで
        GridPane.setConstraints(cellValues[i][j], i, j);
    // 子に指定する
        inputGrid.getChildren().add(cellValues[j][i]);
// 追記: 上の二行が内部で実行されているaddメソッドを見つけた。
//       バージョンの違いによるものだろうか。下が記述例
//      inputGrid.add(cellValues[clmIndx][rowIndx],clmIndx,rowIndx);


      }//for j/
    }//for i/
  }// initialize/

検索操作をする限り、FXMLを使わずに見栄えをよくするには、各種プロパティ(位置決め)等をいちいち「setProperty」的なメソッドで設定してやる必要がある様子。
こういう見栄えがかかわる設定を記述すると、startを記述するApplication.javaのstartメソッドがやたら長ーくなりそうだなと。

つまり、
見栄えがかかわるところをFXML(というかsceneBuilder)に任せて、
網羅的にIDが発生したり、コンピュータお得意の繰り返しの記述をするときはControllerをInitializableにしてinitializeメソッドに記述する。

この件に限らず、何か予め決まったフォーマットで用意された、リソースを管理するデータベースなりファイルなりを使って、JavaFXでGUIアプリを作るときは、こういう手法をとるものなのかなぁなんて思ってみたり。

罫線の表示

罫線はGridPaneのプロパティに「GridLineVisible」というのがあるが、どこかのstackOverflowの回答者が指摘していたところによると、「api documentでデバッグ用って書いてあるんだし使うんじゃねー」とのこと。

代わりにcssによる方法が紹介されていたけど、僕的には3x3のグループを見やすくしたいので、ちょっと...
だったら3x3でGridPane作って、GridPane入れ子(3x3)作れって話かもしれないけど、それもめんどい。

最終的に、CELL_SIZEなる定数を定義して使ってはいるものの、かなり汚い書き方になった。

Controller
  private void showRuler(Group group) {
    for (int i = 0; i <= 3; i++) {
      Line hline=new Line(CELL_SIZE*3*i,0,CELL_SIZE*3*i,CELL_SIZE*9);
      Line vline=new Line(0,CELL_SIZE*3*i,CELL_SIZE*9,CELL_SIZE*3*i);
      hline.setStrokeWidth(1.5);
      vline.setStrokeWidth(1.5);

      group.getChildren().addAll(hline,vline);
    }
  }

group引数は、罫線を引くGridPaneを含むGroupを指定させる。
呼び出し側のgroupの座標位置をFXMLで指定しておけば、こちら側でlayoutXやlayoutYを指定する必要がなくなる。

Lineで描画させること自体がもうなんか汚い感じするし、拡大縮小に対応する気皆無。
SimpleTableViewみたいなの作ってくりゃれ。(組もうと思えば組めそうだが。)

入力制約を付ける

入力値が整数値の、特に一桁でないと困る。
1~9と、空欄扱いとして0だけを許して入力させたい。

調べると、いろんなやり方がある模様。用途によって使い分けるとよいそうだ。
[input] JavaFXで数値のTextFieldを作成するための推奨される方法は何ですか?
ゆっちのblog - TextField を 0 から 9 までの数値しか入力できないようにしてみた

FormatterとかPatternとかで、正規表現を扱う際はこちらも参考になった。
Qiita - 正規表現の基本@sea_ship

私が選んだのは、if文で直感的に(?)いじくれるchangedのListenerを追加する方法。
beapも鳴らさせてみたり。

以下はControllerクラス内でTextFieldをGridPaneに配置してるfor(i)for(j)の中

Controller.setInputGrid()
  TextField textField=new TextField("");

// add NumbersOnlyLimitation for the TextField
  textField.textProperty().addListener((observable, oldValue, newValue) -> {
    String str;
//    System.out.println(newValue);  //for debug
    if(newValue.matches("[0-9]?")){            // 0~9 を1文字か、0文字のとき
// 正常値として受け入れる
      str=newValue;
    }else if(newValue.matches("[0-9]{2}")) {   // 0~9 が2文字のとき
// 新しい入力を正常値として受け入れ、古い値は捨てる
      str=String.valueOf(newValue.charAt(1));
    }else{                                     // それ以外
      Toolkit.getDefaultToolkit().beep();      // ぺろーん(エラー音)
// 以前の値が正常値なら変更なし。それ以外は""
      str=oldValue.matches("[0-9]") ? oldValue : "" ; 
    }
    textField.setText(str); // いちいちこれ書くのめんどかったので str 記述した
  });

  textField.setPrefSize(CELL_SIZE, CELL_SIZE);
  textField.setAlignment(Pos.CENTER);
  textField.setFont(Font.font("", FontWeight.BOLD,-1.0)); // Boldsymbolにしたかったけど、大して太くならない...

  cellValues[clmIndx][rowIndx]=textField;
// objectにpropertyも含まれる(?)のでこれでイケる。
// というか配列のままListener作ろうとしたらすっげ怒られた

  inputGrid.add(cellValues[clmIndx][rowIndx], clmIndx, rowIndx);

最初は!newValue.matches("[^0-9]+")のときに""に置換とかから始まった記憶(あいまい)

また、matchesの中を"[0-9]"としてやると、0文字、すなわち""を受け付けてくれない。
最初に思いついたのが「バックスペースに正規表現があったハズだからそいつが入力されたらにしよう!」とかいう、
多分誰もやろうとしないやり方で、書き方の一部がコード中に残っているがnewValue.charAt(1)=="\b"みたいにしたのだが、コイツがぬるぽを量産してくれやがるので、標準出力が真っ赤に。

「\bじゃなけりゃ何が入ってんだよオオン?」みたいなノリで標準出力にnewValueを出力させて実験をしてみた。(soutがコメントで残っている)
バックスペースされると、バックスペースされた後の""がnewValueになるので、charAt(1)がぬるぽを吐く様子。

上の状態でsoutさせた時の入力と出力のexample
空白行も再現

入力 : a1b23\b8d
出力:
a

1
1b
1
12
2
23
3

8
8d
8

\bはバックスペースで空白行は再現通り。
ちなみに任意文字列をコピペすると、半角数字1~2文字以外は無視(beap)され、最後に入力される数字だけ入る。
文字列""(何もなし)をコピペしてもbeapが鳴るのはちょっとなぁ。

入力制約 - これでよいのか

このやり方だと、ChangeListenerのオブジェクトさんが81個生成されちゃって、なんか無駄が多い気がするのよねぇ...
なんとか単一のメソッドにして、共通利用できないかしらん。

で、ウンウンうなって出来上がったのが↓

Controller.setInputGrid()

// StringPropertyのBeanのhashとTextFieldのMap
    HashMap<Integer,TextField> hashMap=new HashMap<Integer, TextField>(81,1f);

    ChangeListener<String> cl=new ChangeListener<String>() {
      @Override
      public void changed(ObservableValue<? extends String> observable, String oV, String nV) {
        String str;
        if(nV.matches("[0-9]?")){ /*中略*/ str=FILLED_VALUE }

//  今世紀稀に見る汚いキャスト
        StringProperty sp=(StringProperty)observable;
//    System.out.println(sp.getBean());
        hashMap.get(sp.getBean().hashCode()).setText(str);
      }
    };

    for (int clmIndx = 0; clmIndx < columnSize; clmIndx++) {
      for (int rowIndx = 0; rowIndx < RowSize; rowIndx++) {
        TextField textField=new TextField("");
        if(hashMap.containsKey(textField.textProperty().getBean().hashCode())) {
// 申し訳程度のエラー出力。hashの衝突がイヤならBeanをそのまま格納すればいいじゃない()
          System.err.println("textField.textProperty().getBean() Duplicated");
        }
        hashMap.put(textField.textProperty().getBean().hashCode(), textField);

//     System.out.println(textField.textProperty().getBean());

//  add NumbersOnlyLimitation for the TextField
        textField.textProperty().addListener(cl);

もとのコードとどちらがメモリ使用量がいいかとかちょっとわかんない。
というか、可読性という意味では間違いなくクソになった。

入力を受け付ける容れ物を用意する

入力されたデータを扱いやすい形に加工するクラスを定義していく。
ここをそれなりに充実させることで、データを扱う側のクラスでの記述を単純化することを図りたい。

特に僕がやりたいことは、
テーブル上のセルを、各[行・列・3x3のサブリージョン]毎に読みこむ「読み出し器」のクラスを用意する
ことである。

各グループの中だけで、その数が既にあるかとかを管理するのであれば、九列、九行、九升の27グループ(以下「リージョン」と呼ぶ)を、テーブルから読み込むモノを用意してあげればよいはず。

また、実際に解くときには、例えば「そのリージョンの中で同一のnか所にしか入らない数字がn個ある場合は、それ以外の場所にそれらの数字は入らない」とかの、いわゆる「定石」も取り入れたい。(もっともこれは解く側のメソッドでの実装にすると思うが。)

さらに細分化して、各セルには、「各々に入りうる数字」「逆に、入らない数字」のリストも用意してやりたい。

さりげなく定義したが、ここから先、

数独を解くうえで数字一文字が収められるべき各マスを「セル(Cell)」
セル内の数字の重複禁止制約を受ける範囲のかたまりを「リージョン(Region)」
リージョンはさらに、「行リージョン(rowRegion)」「列リージョン(columnRegion)」「3x3のサブリージョン(subReion)」にわけられる。(処理内容は同じ。)
すべてのセルを集めたモノを「テーブル(Table)」

と表記していくことにする。
ただし、テーブルに関しては、断りなく「ボード(Board)」と表記することもあるかもしれない。
これは、開発を始めたころにこのように呼んでいたためである。

共有データを操作させるにあたって

Javaでは、C言語でいうところのポインタを、「参照変数」とか「参照値」とか呼ぶっぽい。

各リージョンに参照値を納めさせて、テーブル一個分のセルのデータ群だけで処理ができるようにしたい。
つまり、リージョンからの操作で、テーブルに存在するセルのデータも変更されるようにしたい。

以下はつたないイメージ図

Cell Region image

値渡し?参照渡し?評価戦略?なにそれ。

ここで、「Javaってポインタみたいなデータの処理できるんかえ?」と疑問に思ったので、いろいろ見たが、
もう参照渡しとは言わせない@mdstoy
こちらの記事が大変参考になった。
最後の「もう参照渡しとは言わせない 2018 冬 - 資料」のスライドまで読んだ。
「「事実上参照渡し」説!?」の項にある用例とか、スライドで5回は繰り返された「値で渡します」で、たぶん理解できた(と思う(すごく不安))

Javaの「参照渡し」問題まとめ@acevifも読みながら自分なりにまとめてみた。

  • Javaのメソッドでは値渡ししかしない
  • Javaのオブジェクトを扱ったりする参照変数(Object obj;obj)は、あるオブジェクトを参照するための、参照値である。
    • そのくせ参照先そのものであるかのようにふるまう。(参照先of(obj).method()じゃなくて、obj.method()ができる。)
      • ちょっとキモい感じはするが、こうしない場合、上みたいないちいち参照先を取り出す書式を書き込む必要が出てくる。C言語の「*pointer == object」の「*」みたいな。
  • メソッドの引数にプリミティブじゃない、参照変数で保持させているモノを与えた場合、『参照値渡し』とでも呼ぶべき方法で、「参照できるようになっている参照値」を渡している。...失礼。参照値「で」渡している1
  • 「(仮称)参照値渡し」を理解するためにひとつずつ。method(arg){} main(){method(val)}として、
    • 「値渡し」では、仮引数に、実引数として用意された値の複製を渡して、処理を行う。
      • argvalと同じ値で用意されるが、モノは別。
      • argが示す値を変更しても、valが示す値は変更されない。
    • 「参照渡し」では、仮引数に、実引数として用意された値そのものを渡して、処理を行う。
      • argvalとまったく同じモノとして扱われる。
      • argが示す値を変更すると、valが示す値も変更される。
    • 「(仮称)参照値渡し」では、仮引数に、実引数として用意された参照値の複製を渡して、処理を行う。
      • argvalと同じ参照値で用意されるが、モノは別。
      • argが示す参照値を変更しても、valが示す参照値は変更されない。
      • だけど、オブジェクトを参照するための参照値を渡されてるから、
        argが示す「参照値が指す「オブジェクト」」」と、「valが示す「参照値が指す「オブジェクト」」」は一致し、
        オブジェクトそのものに手を加えることができる。
        • これにより、argが示す参照値(すなわちvalが示す参照値が指すオブジェクトを指している参照値)を更新しない限り、argvalが示す参照値が指すオブジェクトとして扱うことができる。
        • 上述の「そのくせ参照先そのものであるかのようにふるまう」性質のために、記述の見た目が参照渡しをしたかのように見えている。
        • C言語ライクに書くなら**argを操作することで**valを操作できる。

であれば、試しにCライクに書いてみればよい。

カラーシンタックスはJava
private void method(ArrayList<String> **arg){
    *arg = new ArrayList<String>();
    **arg.add("PHP");
}

ArrayList<String> **list = new ArrayList<String>(); //ここちょっと気持ち悪い
**list.add("Java");
method(list);
System.out.println(**list);    // [Java]

二つの言語に喧嘩売る感じの記述なのですごい気持ち悪い書き方を含んでしまったが、直観的にはこれであっているハズ。

別の細かな例えとか情報

私が教えてもらっている先生がJavaの授業の序盤のほうでおっしゃっていた、「Javaではポインタという言葉は基本的に使いませんが、プリミティブ型以外の、つまり参照型の変数はぜーんぶポインタだからね」という言葉の意味を、今更理解したような気がする。

仮引数int *argに実引数int *valを値渡しすると、
C言語表記ぶちこんで書くと、ポインタ変数argは、ポインタの指す先*argを変更することで*valを変更することができるが、
関数内でargのポインタの指す先を変更した場合は、
arg!=valになってしまうので、*argを変更しても*valは変更されない
みたいな。

※上述記事のコメント欄にきちんと解説してる記事があったのでペタリ
C++ 値渡し、ポインタ渡し、参照渡しを使い分けよう@agate-pris
Javaでは、この記事でいうところの、値渡しとポインタ渡しができる、といったところだろう。

いずれにせよ、記事主の結論「「参照値」という「値」を「値渡し」」という言葉の意味は理解できたような気がする。

話に出ている「評価戦略(evaluation strategy)」のwikipedia記事とか、きちんと読まないとダメですねぇこれは。(それも英語で)

ちなみに、これらの記事を読む前の僕は、プリミティブ型のintの配列int[][] cellの、たとえばcell[i][j]のポインタを、rowRegion[i].get(j)に収めようとして死んでました。というか、それじゃあ全然情報が足りないことにも気づいていませんでした。
ここで記事として問題点を書こうとしたときに、初めて「俺はポインタみたいなのを扱いたいんだなぁ」と思い当たり、「Javaのポインタってなんだっけ」に始まり、上述の記事に出会って...という感じ。

...プリミティブ型のモノの配列の一要素はあくまでプリミティブ型なので、値渡ししかしないJavaではどうあがいてもムリなことをしようとしていたのでした。
まあint型をメンバに持つ単純なクラス作ればできるんでしょうけども。

それより前の話

値渡し云々を知るより前の僕の拙いあしどり。

  1. Boardクラスの中に「staticなTableクラス」と「ArrayListを継承したRegionクラス」を作り、Tableクラスの中に「staticでint[][]型のcellフィールド」を用意して、Boardクラス内に「Regionのオブジェクトにcell[i][j]を入れたもの」をreturnするメソッドを作ろうとする。
  2. cellの操作をしやすくするために座標(x,y)みたいにijを入れるとcellの操作を行いやすくするため、int i,jフィールドとcellを操作するメソッドを、Tableクラスの中に記述し始める。
  3. RegionのオブジェクトにTableのオブジェクトを収めようとする段になって「!!?」となって、慌ててPosXYクラスを作成し、Tableクラスに入れる。
  4. 少なくない呼び出し回数のメソッドが、呼び出されるたびにnew ArrayList<>()されるのキモいなぁと気づく。
  5. 何とかしてcell[][]の各要素をポインタみたいにできないかなぁと思う。
  6. 現実逃避気味に「#入力を受け付ける容れ物を用意する」から下をぼちぼち記述し始める。
  7. いや、クラス作れよ。てか int[][] cellってなんだよCellクラス作れよ←今ココ

...........^q^)
と!!!、いうわけで

Cellクラスとか設計していきまーす。

各クラスの設計説明の前書き

冒頭でも書いたし、内容読めばわかる人にはわかるでしょうし、プロフにも書いたはずなのでわかるとは思いますが、

わたくし、バリバリの初心者にござる。
何やるにしても拙いものになると思いますので、どうかご容赦を。(批判とか改善策とかのコメントは歓迎いたします。)
同輩の初学者様がたも、こんなの参考にしちゃダメです。参考にするにしても、教育者の意見をもらってからにしましょう。

Cell クラスの設計

前提として、Cellはテーブルのように並んでいるなかのひとつとして見る。
よって、ファクトリメソッドによってインスタンスの生成に、二次元配列でのみ出力する制限をした。
出力されるcellオブジェクトや、マスとしての名称をcell
cellの複数のかたまり(Cell[][])をcells集合と名付けた。

また、マスに数字を入れる、つまりvalueに数字を代入し確定させる動作を「埋める」と表現することにした。英語ではFillとなる。
実際に数独やってても「3をここに埋めて~」みたいな言い方するしね。

できるだけざっくりさせたサマリーが以下。

  • フィールド
    • Pos pos : cells集合上で、自身の位置を示すPosクラスオブジェクト。Posクラスについて簡単に説明すると、行番号と列番号をフィールドとして持ち、それらに対する処理の制限や、3x3のsubRegionを用いる際に使う行番号や列番号を使った処理を担う。
    • int value : cellに埋まっている数字。空欄は0
    • boolean[10] possible : このcellに、要素番号にあたる数字が埋まる可能性があるかどうかを格納。つまり、cellに5が入る可能性があるならpossible[5] ==true
  • コンストラクタ
    • 全部privateメイク。ファクトリから呼び出してもらう。引数はPosとvalueの初期値。possibleの初期化も行う。
  • static メソッド
    • ファクトリ(2種類) : Cellの二次元配列を返す。今回は、int二次元配列をcells集合に変換するものと、cells集合を複製するものとを作った。配列はcloneしても参照先が変わってくれないって知って「まじかー」ってなった。
      • makeCellArray : 引数がint二次元配列。引数をvaluesに代入していきできたcells集合を返す。
      • cloneCells : 引数がcells集合。同じ状態で別のオブジェクトとして操作できるcells集合の複製を返す。
  • メソッド
    • deny(int) : cellに埋まる可能性がない数字の情報をpossibleに記録する
    • isBecome(int arg) : argがcellに埋まる可能性があるかどうかを返す。実質possible[arg]のgetter
    • isFilled() : cellが埋められているかどうかを返す。空欄が0なので、value ==0を返す。
    • tryFill(int arg) : argをcellに埋める試みを行う。possibleではない場合は埋めない。cellにargを入れることが出来た、あるいは既にargが埋まっている場合にはtrueが返る。
    • その他getterやsetter。必要に応じてprivateアクセスにしたり。

まとめた書き方すると、

  • マスに入った値を保存するint valueとそれを埋めるメソッドtryFillと「もうこのセル埋まってますかねぇ」を問い合わせるisFilled
  • マスに入る可能性がない数字のデータを保存するboolean[10] possibleと「この数字は入らねえから」をpossibleに保存するメソッドdenyと「この数字入りますかねぇ」をpossibleに問い合わせるメソッドisBecome
  • その他諸々インスタンス生成するメソッドとかコンストラクタとか

という感じでしょうか。
これを書いてる今思ってることとして、行番号列番号はcell自体に持たせなくてもいいんじゃないかとか思ったりするけどどうなんでしょ。
配列の一要素としてcells[i][j]で呼ばれることを前提とするなら、いらないかなぁ。
僕的には、HashMapの、keyが二種類あるみたいなやつがあればいいんですけど。keyを片方だけ指定するとIteratableな「行一覧」みたいのが出てくるとかそういうの。

追記(19/02/01)
配列じゃなくて、Tableクラスにオブジェクトを捕まえさせることにしました。
やっぱりメモリの関係がよろしくないのではとか考えた末のことです。
詳しくはTableクラスに。

Copy constructor does not copy field boolean[]

IDEAを使っていると、警告とかsuggestionとかがスクロールバーに表示される。
赤いやつは大体コンパイルエラーになる類のもので、修正しないとコンパイルしてくれない。
黄色いのは「このメソッド中でしか使ってねーしprivateでよくね?」とか「このフィールド値与えてるけど利用しませんねぇ!」とか「このif文の条件式常にtrueなんだけどウケる。いらないんじゃねーの」とかそういうのが多いが、今回はこれらも一応気にすることにしている。

その中になんか見過ごせない(見出し)のがあった。

実際の文は「boolean[]」ではなく「"possible"」だったが。
IDEAはmoreを押すことで情報が増えるので、moreの中身を見ると、

Reports copy constructors that dont copy all fields in the class.
Fields with the modifier transient are considered unnecessary to copy

ん?え?
transientを付けてないpossibleについて何か言ってきてるのはわかるが、
一行目は「コピーされてないフィールドがあるゾ」で、二行目は「transientつけるとコピーしなくていいものとしてみなされるゾ」だと?
ワケワカメ。

宣言とか警告出てた部分をお見せする

Cell
//諸々省略

private boolean[] possible = new boolean[10];

private Cell(@NotNull Pos pos, int value) {

//  initialize pos
  this.pos = pos;

//  initialize Value
  setValue(value);

//  initialize possible[]
  if (value <0 | 9< value) {
    System.err.println("Cell constructor : out of range(==[0,9]) value");
    System.err.println("                     where pos points > "+pos.getStringIJ());
  } else if (isFilled()) {
    possible[value] = true;
  } else {
    for (int i = 0; i < possible.length; i++) {
      possible[i] = true;
    }
  }
}

private Cell(@NotNull Cell cell){     // Copy constructor does not copy field "possible"
  this(cell.getPos(),cell.getValue());
}

警告文が矛盾しててハテナ状態だったので、実験してみた。

Cellクラス内部

  public static void main(String[] args) { //クラス内にmainベタ書き

    int[][] x={{0}};                      // テスト用に要素が一個だけのvaluesを作る
    Cell[][] cell1= makeCellArray(x);     // ちなみに9x9の制約はBoardクラスで行う
    cell[0][0].deny(2);                   // この操作でpossible[2]がfalseになるハズ

    Cell[][] cell2=cloneCells(cell1);      // cloneCellsで呼び出されるコンストラクタはCell(Cell cell)。...ややこし

    System.out.println("cell1");
    for (boolean t:cell1[0][0].possible){  // foreachでpossible配列の中身全部はきだしてもらいまーす
      System.out.print(" "+( t?1:0 ));     // さりげなく三項演算子。true falseをC言語と同じ出力にするゾ
    }

    System.out.println(); //改行

    for (boolean t:cell2[0][0].possible){  // possibleはprivateアクセスだが、同一クラス内なのでイケる
      System.out.print(" "+( t?1:0 ));
    }
    System.out.println();
    System.out.println("cell2");
  }
出力さん
cell1
 1 1 0 1 1 1 1 1 1 1
 1 1 1 1 1 1 1 1 1 1
cell2

あーーーーーーー!!!!!!あ~=====~=~==~=~=!!!!

ってあれ。初期化してないbooleanはfalseのハズでは。
あちげえ。これコンストラクタ側はvalueを0としてpossibleを初期化する仕事はしてるけど、「2は入らない」情報が渡ってない。(ここの記事書いてる途中で気づいた。)

cellのcloneメソッドは封印したけど、フィールドのcloneは封じてないのだぜ!ってなわけで、possible.cloneを渡すことにした。
...ってあれ、コンストラクタ呼ばなくていいのでは...

...書き直した。


さて、しかしここで話は終わらない。
渡しが気になったのはtransientだ。ナニコレ

とりあえずpossibleをtransientにして、修正前のコードを実行すると、なんと、cell2.possible[2]がfalseになってた(!!!!)
え、なにこれ。

調べてm(記事書きとか調べるのとかコーディングとか若干飽きてきたのでゲームします。失踪しません。明日また続きやるもん)

追記(19/01/13)
後に丸投げにすると忘れるよね()
todo書いとこ

// todo : transient とか

追記(19/02/01)
一応の完成(とけるところ)まで、こちらでのアウトプットなしにこじつけたので、以下はそこまでの軌跡のようなものになります。

Region クラス

余計な機能をいろいろとつけてしまったので、完成(仮)状態で使用しているメインとなるメソッドだけ解説する。
以前は、「配列でよくね」とかいうやばいこと言ってたが、結局ArrayListになった。

Region.java
public boolean valueFill(int value) {
  boolean changed = false;
  for (int i = 0; i < CellLength; i++) {
      changed |= get(i).deny(value);
    }
  }
  return changed;
}

region.get(index).tryFill(value) ==trueとなったとき
(言い換え:region の中で index 番目の cellvalue を埋めることに成功した場合)
同じregionの別の cells(:= indexOf(cells[i]) !=index)には、 value を埋めることはできないので、
region を保持する予定のクラスでこのメソッドを呼び出してもらうことで、
自動的に cell 以外の cellsvaluecell.deny(value)されるようにしましょうね、
という意図で作ったメソッド
return このメソッド呼び出しの結果、更新が発生したことの真偽値( cell.deny の返り値に true があるか)

Table クラス

java.util.HashMapを継承して作った。
posをkeyに、cellをvalueにしてデータを格納する。
値が埋まっていないcellの一覧を返す、getEmptyCellsメソッドを持つ。

Board クラス

フィールドに Tableと、Region配列(行・列・サブそれぞれ9個ずつ計27個)を持ち、これらを統合して操作するクラス。
複製を作るときはコイツを new すればよいという寸法。
コンストラクタに board オブジェクトを渡すことで複製を作成する。
deepコピーをしたいときの不安要素を除外するために書いた。

また、Cell::tryFillと、Region::valueFillを行うメソッドを用意し、後述するNPSクラスから、「値を埋めるメソッド」として使用することにしている。

NumberPlaceSolver クラス

実際に数独を解くためのアルゴリズム群を実行するクラスとして書いた。
フィールドとして、

  • 再帰処理を伴う「仮置き」の処理をする際、再帰しすぎている場合のセーフティを提供するためのint値(再帰処理の「深さ」)
  • 深さの制限をかける定数(82)
  • Board クラスインスタンス

を持つ。

インナークラスとしてメソッドを格納する「Algorithm」クラスを書いた。
WANT todo : 「メソッド一覧」みたいなところに書き込まれたメソッドを全部実行する、みたいな処理

今のところ、「仮置き」のみを実装。
仮置きするときは、仮置きするcellの場所と値と、「現状(this)」とを引数に、新しくNumberPlaceSolverのインスタンスを生成することになっている。


順番を間違えてた話

最初は、「使う側から作っていこう!」と意識していたつもりだったが、

画面表示を作って、NPSクラスを作って、......じゃあCellクラス作ろうか!(←!?)

となってしまったので、不要なメソッドとか無駄の多い設計になってしまった。
あと、当初の目的だった
「複数スレッドで同時に仮置きをさせて、それぞれでdenyをさせる。さらに、staticなtableに対してdenyをさせることで、同時に複数の値をdenyできるようにする」という実装を途中であきらめ(?)てしまったので、思っていたのとかなり方向がズレてしまった。
一応、解けるようには完成したが、半分以上埋まっていないとすっげー時間がかかるものになってしまったので、改善していきたい。


  1. 正直な話、「~~ 「で」 渡している」とかいう日本語がヨクワカランかった。英語を翻訳するときに、「で」で翻訳するしかない「by」を使っているためこういう翻訳が正しいとしか言いようがないのだろう。原文が「call by reference あるいは pass by reference」なのだ。(Javaの「参照渡し」問題まとめ@acevif)。かなり話がそれるが、byの語源をかるーく調べて、ヒットしたところによると、「周辺」から転じて「近くにある」、転じて「わきの」「副次的な」としている。 

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3