20210608のJavaに関する記事は9件です。

Collectionsインターフェースを利用してListを並び替える方法

Collectionsインターフェースを利用してListを並び替える方法 今回はCollectionsインターフェースを利用してListを並び替える方法を紹介させて頂きます。 Comparatorインターフェースを利用して並び替える場合 基本データ型が入ったList内の要素を昇順で並び替える場合は Collections.sort(List); 降順で並び替える場合は Collections.sort(List,Collections.reverseOrder()); を使用することでList内の要素を並び替える事ができますが オブジェクト(インスタンス)が入ったListをインスタンス変数を参照して並び替えるにはComparatorインターフェースのcompareメソッドを実装したクラスを自分で実装し、利用する必要があります。 実際にCompareメソッドを実装し、利用してみたコードがこちらです。 class Fruit { private String name; private Integer price; public Fruit(String name,Integer price) { this.name = name; this.price = price; } public String getName() { return this.name; } public Integer getPrice() { return this.price; } } //インスタンス変数nameの昇順で並び替えを行う class nameComparator implements Comparator<Fruit> { public int compare(Fruit fruit1,Fruit fruit2) { return fruit1.getName().compareTo(fruit2.getName()); } } //インスタンス変数priceの昇順で並び替えを行う class priceComparator implements Comparator<Fruit> { public int compare(Fruit fruit1,Fruit fruit2) { return fruit1.getPrice().compareTo(fruit2.getPrice()); } } public class Main { public static void main(String[] args) { List<Fruit> fruitList = new ArrayList<>(); //fruitListにFruitオブジェクトを追加 fruitList.add(new Fruit("Orange",800)); fruitList.add(new Fruit("Melon",2000)); fruitList.add(new Fruit("Apple",300)); fruitList.add(new Fruit("Grape",500)); System.out.println("fruitListの中身"); print(fruitList); System.out.println(""); System.out.println("fruitListの中身(nameComparatorを利用してsort後)"); Collections.sort(fruitList,new nameComparator()); print(fruitList); System.out.println(""); System.out.println("fruitListの中身(priceComparatorを利用してsort後)"); Collections.sort(fruitList,new priceComparator()); print(fruitList); } //List<Fruit>の要素のインスタンス変数を出力するメソッド public static void print(List<Fruit> fruitList) { for(Fruit fruit : fruitList) { System.out.println(fruit.getName() + " " + fruit.getPrice()); } } } 実行結果 fruitListの中身 Orange 800 Melon 2000 Apple 300 Grape 500 fruitListの中身(nameComparatorを利用してsort後) Apple 300 Grape 500 Melon 2000 Orange 800 fruitListの中身(priceComparatorを利用してsort後) Apple 300 Grape 500 Orange 800 Melon 2000 Fruitオブジェクトのname変数やprice変数の昇順で並び替える事ができました。 降順で並び替える際は以下の様にcompareメソッド内で使用されているcompareToの引数を逆にして実装します。 //昇順の場合 return fruit1.getPrice().compareTo(fruit2.getPrice()); //降順の場合 return fruit2.getPrice().compareTo(fruit1.getPrice()); ラムダ式を利用してComparatorインターフェースを実装してみる Comparatorインターフェースの実装を上記のコードのように実装してしまうとコードが長くなりやすく、ラムダ式を利用して実装する事で短くまとまったコードになるためより良いと思います。 ラムダ式を利用してコードを書き直した場合。 class Fruit { private String name; private Integer price; public Fruit(String name,Integer price) { this.name = name; this.price = price; } public String getName() { return this.name; } public Integer getPrice() { return this.price; } } public class Main { public static void main(String[] args) { List<Fruit> fruitList = new ArrayList<>(); //fruitListにFruitオブジェクトを追加 fruitList.add(new Fruit("Orange",800)); fruitList.add(new Fruit("Melon",2000)); fruitList.add(new Fruit("Apple",300)); fruitList.add(new Fruit("Grape",500)); System.out.println("fruitListの中身"); print(fruitList); System.out.println(""); System.out.println("fruitListの中身(name変数でsort後)"); Collections.sort(fruitList,(fruit1,fruit2) -> fruit1.getName().compareTo(fruit2.getName())); print(fruitList); System.out.println(""); System.out.println("fruitListの中身(price変数でsort後)"); Collections.sort(fruitList,(fruit1,fruit2) -> fruit1.getPrice().compareTo(fruit2.getPrice())); print(fruitList); } //List<Fruit>の要素のインスタンス変数を出力するメソッド public static void print(List<Fruit> fruitList) { for(Fruit fruit : fruitList) { System.out.println(fruit.getName() + " " + fruit.getPrice()); } } } 実行結果はラムダ式を使用せずに記述した場合と同じなので割愛します。 ラムダ式を利用することでComparatorインターフェイスの実装が短くまとまりました。 まとめ オブジェクトを入れたListを並び替える場合はComparatorインターフェースの実装クラスを作成し、sortメソッドで利用する。 ラムダ式を利用することでComparatorの実装を短くまとめられる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Processing]ダイクストラ法の探索を可視化

導入 地図アプリとか乗換案内アプリとか、移動する時にコストを考えないといけない場合の探索はダイクストラ法が使われるんですって で、実行したら出来たわけですが、 その過程がわかりにくいので見えたい 座標がバラバラでコンソール表示がつらい という理由から可視化しました 実行結果 解説 状況 画像 スタート ゴール 見探索のノード緑 探索中のノード青 探索済のノード赤 移動コスト ノードまでの最小コスト 探索対象ノード 探索先 最小コスト経路 に変化させながら探索してます 入力 スタート地点、ゴール地点、探索終了後にもう1回探索するか(y or n) 参考 processing導入の参考サイト IntelliJ IDEAでProcessingを書く グラフ(マップ)とダイクストラ法の手順の参考 ダイクストラ (Dijkstra) 法 ソースコード 内訳 Processingを実行する都合上、処理は1つのクラスに全部押込みました 内訳を説明すると Nodeクラス Edgeクラス グローバル変数 メインメソッド(影薄い) Processing settting() setup() draw()ここでダイクストラ法を順々に実行 特に重要じゃないメソッド マップの状況を描くメソッド 合計:258行 ~300行いってないのでセーフ= コード Dijkstra_List Dijkstra_List.java import processing.core.*; import java.util.ArrayList; import java.util.Scanner; //processing導入の参考サイト //https://qiita.com/shion1118/items/49b803b3217e642cfbd1 //グラフの参考 //https://nw.tsuda.ac.jp/lec/dijkstra/ public class Dijkstra_List extends PApplet { private class Node { String name;//ノード名 int cost;//最小コスト Node minFrom;//最短距離の来た道 ArrayList<Edge> edge;//エッジのリスト Determine determine;//確定状況 PVector pos;//位置 Node(String name, PVector pos) { this.name = name; this.pos = new PVector(pos.x * width, pos.y * height); cost = Integer.MAX_VALUE; edge = new ArrayList<>(); determine = Determine.未確定; } @Override public String toString() { return getColor() + name + "\u001b[00m"; } public void draw() { fill(pColor()); noStroke(); ellipse(pos.x, pos.y, 20, 20); fill(0); text(name, pos.x, pos.y + 5); text((cost == Integer.MAX_VALUE ? "∞" : cost + ""), pos.x, pos.y - 15); } private String getColor() { StringBuilder str = new StringBuilder("\u001b[00;"); switch (determine) { case 未確定 -> str.append("32"); case 確定中 -> str.append("33"); case 確定済 -> str.append("31"); } return str.append("m").toString(); } private int pColor() { return switch (determine) { case 未確定 -> color(100, 255, 100); case 確定中 -> color(100, 100, 255); case 確定済 -> color(255, 100, 100); }; } } public class Edge { final Node TO; final int COST; Edge(int cost, Node to) { this.COST = cost; this.TO = to; } } Scanner sc; Node[] graph; int start = -1, goal = -1; Node currentNode; int searchEdgeIndex = -1; DijkstraMode flow = DijkstraMode.スタート設定; public static void main(String[] args) { PApplet.main("Dijkstra_List"); } @Override public void settings() { size(1500, 500);//windowサイズ } @Override public void setup() { frameRate(2); getSurface().setAlwaysOnTop(true);//windowを常に前面にする textAlign(CENTER); sc = new Scanner(System.in); init(); } void init() { setData(); start = goal = searchEdgeIndex = -1; currentNode = null; flow = DijkstraMode.スタート設定; printMap(); } @Override public void draw() { switch (flow) { case スタート設定 -> { while ((start = searchName(input("start name"))) == -1) { for (Node n : graph) System.out.print(n.name + " "); System.out.println(":の中から選んでください"); } flow = DijkstraMode.ゴール設定; } case ゴール設定 -> { while ((goal = searchName(input("goal name"))) == -1) { for (Node n : graph) System.out.print(n.name + " "); System.out.println(":の中から選んでください"); } System.out.println("探索開始"); graph[start].cost = 0; flow = DijkstraMode.最短距離ノード設定; } case 最短距離ノード設定 -> { int minCostNode = minCost(); if (minCostNode == -1) { flow = DijkstraMode.ルート確定; break; } currentNode = graph[minCostNode]; currentNode.determine = Determine.確定中; searchEdgeIndex = 0; flow = DijkstraMode.接続ノードコスト更新; } case 接続ノードコスト更新 -> { Edge edge = currentNode.edge.get(searchEdgeIndex++); if (edge.TO.determine != Determine.確定済 && currentNode.cost + edge.COST < edge.TO.cost) { edge.TO.determine = Determine.確定中; edge.TO.cost = currentNode.cost + edge.COST; edge.TO.minFrom = currentNode; //System.out.println(currentNode + " to "+edge.TO +" cost"+ edge.TO.cost); } if (searchEdgeIndex >= currentNode.edge.size()) { searchEdgeIndex = -1; flow = DijkstraMode.ノードを確定; } } case ノードを確定 -> { currentNode.determine = Determine.確定済; flow = DijkstraMode.最短距離ノード設定; } case ルート確定 -> { if (input("continue ? y or other word").equals("y")) { init(); return; } else exit(); } } printMap(); } int minCost() { int index = -1, min = Integer.MAX_VALUE; for (int i = 0; i < graph.length; i++) { if (graph[i].determine == Determine.確定済) continue; if (graph[i].cost >= min) continue; min = graph[i].cost; index = i; } return index; } void setData() { graph = new Node[8]; graph[0] = new Node("A", new PVector(0.1f, 0.5f)); graph[1] = new Node("B", new PVector(0.3f, 0.3f)); graph[2] = new Node("C", new PVector(0.4f, 0.6f)); graph[3] = new Node("D", new PVector(0.3f, 0.8f)); graph[4] = new Node("E", new PVector(0.6f, 0.1f)); graph[5] = new Node("F", new PVector(0.7f, 0.2f)); graph[6] = new Node("G", new PVector(0.7f, 0.7f)); graph[7] = new Node("H", new PVector(0.9f, 0.5f)); setBranch(0, 1, 1); setBranch(0, 2, 7); setBranch(0, 3, 2); setBranch(1, 4, 2); setBranch(1, 5, 4); setBranch(2, 5, 2); setBranch(2, 6, 3); setBranch(3, 6, 5); setBranch(4, 5, 1); setBranch(5, 7, 6); setBranch(6, 7, 2); } void setBranch(int from, int to, int cost) { graph[from].edge.add(new Edge(cost, graph[to])); graph[to].edge.add(new Edge(cost, graph[from])); } int searchName(String name) { for (int i = 0; i < graph.length; i++) if (graph[i].name.equals(name)) return i; return -1; } String input(String name) { System.out.print(name + "\t->"); return sc.next(); } void printMap() { background(200); for (Node node : graph) for (Edge edge : node.edge) edgeLine(node, edge, color(255));//エッジを描画 if (searchEdgeIndex != -1) { //接続してるノードのコストを更新する様子の線 edgeLine(currentNode, currentNode.edge.get(searchEdgeIndex), color(50, 50, 255)); } if (start != -1) {//スタート地点をマーク noFill(); stroke(50, 255, 50); strokeWeight(3); ellipse(graph[start].pos.x, graph[start].pos.y, 30, 30); text("Start", graph[start].pos.x, graph[start].pos.y + 30); } if (goal != -1) { noFill(); stroke(255, 50, 50); strokeWeight(3);//ゴール地点をマーク ellipse(graph[goal].pos.x, graph[goal].pos.y, 30, 30); text("Goal", graph[goal].pos.x, graph[goal].pos.y + 30); if (graph[goal].minFrom != null) {//ゴールへのルートがある場合は表示 stroke(color(100, 255, 100)); strokeWeight(4); for (Node n = graph[goal]; n.minFrom != null; n = n.minFrom) line(n.pos.x, n.pos.y, n.minFrom.pos.x, n.minFrom.pos.y); } } if (currentNode != null) {//最短距離を確定させるノードをマーク noFill(); stroke(50, 50, 255); strokeWeight(2); ellipse(currentNode.pos.x, currentNode.pos.y, 25, 25); } //各ノードを表示 for (Node node : graph) node.draw(); //進行モードを表示 getSurface().setTitle(flow.name()); } void edgeLine(Node node, Edge edge, int color) { PVector center = node.pos.copy().add(edge.TO.pos).mult(0.5f); stroke(color); line(node.pos.x, node.pos.y, edge.TO.pos.x, edge.TO.pos.y); fill(0); text(edge.COST, center.x, center.y - 5); } } DijkstraMode DijkstraMode.java public enum DijkstraMode { スタート設定, ゴール設定, 最短距離ノード設定, 接続ノードコスト更新, ノードを確定, ルート確定 } Determine Determine.java public enum Determine { 未確定, 確定中, 確定済 } まとめ 結果だけ得るよりも、途中経過を得る方がアルゴリズムがよく理解できるような気がします 苦労した点 ダイクストラ法をアニメーションするために処理を分割する 文字サイズや色 アルゴリズムに関係ない所で苦労します。本当にありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Java] LIFOで経路探索

LIFOで経路探索 迷路をとくアルゴリズムはLIFOを使えばいけるそうな 迷路 まずはこのような感じで迷路があったとして 等間隔に地点を設けます しかしBCDやFなどはただの通路なので省いても問題ありません ので削除してこう このようなマップで探索をします LIFO 今回は深さ優先で探索します つまり行き止まりまでとりあえず進む人間の進み方と同じです。 ① スタート地点を1とした時に 地点1を追加 ② 一番上にある地点と繋がっている地点を追加 この場合7と2なので追加 これを行き止まりになるまで繰り返します ので 2と繋がっている6を追加 ④ 6は行き止まりでゴールでもないので削除 ⑤ 2ももう行ける地点がないので削除 ⑥ 7と繋がっている14と8を追加 という流れです 出来上がったものがこちらになります Main DFS.java import java.util.Scanner; import java.util.Stack; //https://qiita.com/drken/items/4a7869c5e304883f539b#3-%E6%B7%B1%E3%81%95%E5%84%AA%E5%85%88%E6%8E%A2%E7%B4%A2-dfs-%E3%81%A8%E5%B9%85%E5%84%AA%E5%85%88%E6%8E%A2%E7%B4%A2-bfs public class LIFO { static class NodeM extends Node { int x, y; NodeM(String name, int x, int y) { super(name); this.x = x; this.y = y; } } static Scanner sc; static boolean debug = false; public static void main(String[] args) { sc = new Scanner(System.in); NodeM[] graph; graph = setData(); printMap(graph, 5, 5); int start;//存在する名前を入力するまでずっと受付くん while((start = searchNode(graph,input("始点"))) == -1); int goal; while((goal = searchNode(graph,input("終点"))) == -1); debug = !input("過程の表示 0 or 1").equals("0"); Stack<NodeM> dfs = DFS(graph, start, goal); if (dfs.size() != 0) { System.out.println("ゴールへの経路:" + dfs); } else { System.out.println("見つかりませんでした"); } printMap(graph, 5, 5); } static int searchNode(Node[] graph,String name){ for (int i = 0; i < graph.length; i++) if(graph[i].name.equals(name))return i; return -1; } //深さ優先探索 static Stack<NodeM> DFS(NodeM[] graph, int start, int goal) { Stack<NodeM> dfs = new Stack<>(); dfs.push(graph[start]);//初期位置をスタックに追加 for (int i = 0; dfs.size() > 0 && i < 100; i++) { if (debug) System.out.println(dfs); NodeM currentNode = dfs.peek(); currentNode.visit = Visit.探索中; for (Node toNode : currentNode.branch) { System.out.print(i + "\t:" + currentNode + "->" + toNode + " "); if (toNode.visit != Visit.未探索) { System.out.println(toNode.visit); continue; } System.out.println(); toNode.from = currentNode; dfs.push((NodeM) toNode); if (toNode.name.equals(graph[goal].name)) { int notSearch; do { notSearch = onlyVisited(dfs, graph[goal]); if (notSearch == -1) break; dfs.remove(notSearch);//ゴールへの経路じゃないものを削除 } while (true); return dfs; //ゴールにたどり着いたら処理終了 } } //本来の処理であればif()とdfs.pop()はいらない if (checkAllVisited(currentNode)) dfs.pop(); } return dfs; } static int onlyVisited(Stack<NodeM> dfs, NodeM goal) { for (int i = 0; i < dfs.size(); i++) { if (dfs.get(i).visit != Visit.探索中 && dfs.get(i) != goal) return i; } return -1; } static boolean checkAllVisited(Node node) { for (Node n : node.branch) { if (n == node.from) continue; if (n.visit != Visit.探索済) return false; } node.visit = Visit.探索済; if (debug) System.out.println(node.visit + ":" + node); return true; } static NodeM[] setData() { NodeM[] graph = new NodeM[17]; graph[0] = new NodeM("01", 0, 0); graph[1] = new NodeM("02", 4, 0); graph[2] = new NodeM("03", 1, 1); graph[3] = new NodeM("04", 2, 1); graph[4] = new NodeM("05", 3, 1); graph[5] = new NodeM("06", 4, 1); graph[6] = new NodeM("07", 0, 2); graph[7] = new NodeM("08", 2, 2); graph[8] = new NodeM("09", 3, 2); graph[9] = new NodeM("10", 4, 2); graph[10] = new NodeM("11", 1, 3); graph[11] = new NodeM("12", 2, 3); graph[12] = new NodeM("13", 3, 3); graph[13] = new NodeM("14", 0, 4); graph[14] = new NodeM("15", 2, 4); graph[15] = new NodeM("16", 3, 4); graph[16] = new NodeM("17", 4, 4); setBranch(graph, 0, 1); setBranch(graph, 1, 5); setBranch(graph, 0, 6); setBranch(graph, 6, 7); setBranch(graph, 6, 13); setBranch(graph, 7, 3); setBranch(graph, 7, 11); setBranch(graph, 3, 2); setBranch(graph, 3, 4); setBranch(graph, 11, 10); setBranch(graph, 11, 12); setBranch(graph, 12, 8); setBranch(graph, 8, 9); setBranch(graph, 9, 16); setBranch(graph, 16, 15); setBranch(graph, 13, 14); return graph; } static void setBranch(NodeM[] graph, int n1, int n2) { graph[n1].branch.add(graph[n2]); graph[n2].branch.add(graph[n1]); } static void printMap(NodeM[] graph, int mapX, int mapY) { mapX = mapX * 2 - 1; mapY = mapY * 2 - 1; String[][] map = new String[mapX][mapY]; for (NodeM node : graph) { map[node.x * 2][node.y * 2] = node.toString(); for (Node branch : node.branch) { NodeM b = (NodeM) branch; int dist = b.x - node.x; if (dist > 0) {//接続関係でXの差が正の時だけ関係性を表示 for (int i = 0, offset = 1; i < dist; i++, offset += 2) { map[node.x * 2 + offset][node.y * 2] = "=="; } } else if ((dist = b.y - node.y) > 0) {//接続関係でYの差が正の時だけ関係性を表示 for (int i = 0, offset = 1; i < dist; i++, offset += 2) { map[node.x * 2][node.y * 2 + offset] = "||"; } } } } for (int j = 0; j < mapY; j++) { for (int i = 0; i < mapX; i++) { if (map[i][j] == null) System.out.print(" "); else System.out.print(map[i][j]); } System.out.println(); } } static int input(String prompt) { System.out.print(prompt + "\t->"); return sc.nextInt(); } } Node Node.java import java.util.ArrayList; public class Node{ String name; Node from; ArrayList<Node> branch; Visit visit; Node(String name){ this.name = name; branch = new ArrayList<>(); visit = Visit.未探索; } @Override public String toString() { return getColor() + name + "\u001b[00m"; } String getColor(){ StringBuilder str = new StringBuilder("\u001b[00;"); switch (visit){ case 未探索:str.append("32");break; case 探索中:str.append("33");break; case 探索済:str.append("31");break; } str.append("m"); return str.toString(); } } Visit Visit.java public enum Visit{ 未探索, 探索中, 探索済 } 実行結果 実行するとコンソールにマップが表示されます 始点の名前、終点の名前、途中経過を表示するか(0 or 1)を入力します 14と15は探索しに行って、行き止まりだったので探索済みである赤色に、 03や04らは探索する前にゴールが見つかったので、見探索の緑色に、 06や17らは探索途中なので、黄色になっています まとめ 今回は深さ優先探索のLIFO方式で行い、無事探索することができました 他にはFIFO方式で探索する方法もありますが、やってないので書きません 今回苦労した点は以下になります 文字の色付け マップの接続関係表示 探索アルゴリズムに全く関係ありません。本当にありがとうございました。 次回作 [Processing]ダイクストラ法の探索を可視化
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Eclipse+Forgeでマイクラのmod開発~目次

マイクラのmod開発シリーズもだいぶ増えてきたので目次の記事を作りました。「そうだ、modを作ろう」と一念発起してからこの記事執筆時点で1カ月強。JavaやIDEに触れること自体数年ぶりでしたがリハビリも経てだいぶ色々遊べるようになってきました。。 今後もここの記事を充実させていこうと思っていますが、この記事を参考にして一人でも多くの人が面白いmodを作り、さらにマイクラのエコシステムが盛り上がっていくことを祈ってます。 環境構築編。Eclipse前提ですがEclipseからマイクラの起動、デバッグができるようにするところまでの手順を書いています。 modを作って動かすところまで。中身は空っぽのmodですが一応自作modを作って動いているのを確認するところまでを最低限の作業で実施する方法を書いています。 イベントハンドリングの方法を書いています。イベントハンドリングに重要なForgeにおける二つのイベントバスや、イベントバスにおけるポリモーフィズムの解説、サンプルのありかなどを解説。 マイクラのゲーム中で使えるコマンドの実装方法を書いています。マイクラのコマンドの構造は結構ややこしい(?)のでサンプルも一杯乗せてどのように構造を定義していくのかを解説。最後にちょっと愚痴も書いてますw
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Eclipse+Forgeでマイクラのmod開発~ 自作コマンドを実装してみる

今回はmodでコマンドを作成してみます。前回の記事からだいぶ間が空いてしまったのは、今更ながら(Yet Another)JavaでVoIPするためのライブラリの記事に書いたVoIPで遊んでいたから。。またmod開発に戻ってきました。 ちなみに今回は書くことが多かったり、いっぱいサンプルコードが必要だったので長編です。 環境 OS: Windows10 Jdk: openjdk version "15.0.2" 2021-01-19 ※16.xでもいいかもだけどgradleやらなにやら色々変わってしまって心配だったので今回は15で。 IDE: Eclipse IDE for Java Developers (Version 4.19.0) Buildship(Eclipseのgradleプラグイン): 3.0 Minecraft: Java版 1.16.5 Forge: 1.16.5 まずは動かしてみる やはりまずは簡単なコードを動かしてみるのがわかりやすいでしょう、ということで動かしてみます。 CommandTest.java package jp.munecraft.mod.commandtestmod; // importは省略 @Mod("commandtestmod") public class CommandTest { private static final Logger LOGGER = LogManager.getLogger(); public CommandTest() { LOGGER.info("Hello command test mod"); MinecraftForge.EVENT_BUS.register(this); } @SubscribeEvent public void onRegisterCommand(RegisterCommandsEvent event) { LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge") .executes(context -> { LOGGER.info("hoge command executed"); return Command.SINGLE_SUCCESS; }); event.getDispatcher().register(builder); } } では、さっそく動かしてみる。 お、ちゃんとhを打った時点でhogeコマンドが候補に出てきて認識しているようですね。hogeまで打ってエンターをするとログにhoge command executedが出てきたので動いている模様。簡単ですね! とはいえ一つずつ解説。。 まずはコマンド登録。登録はForgeのイベントバスでRegisterCommandEventを拾って行います。上のコードの中ではonRegisterCommandメソッドがイベントハンドラですね(イベントハンドリングについて、詳しくはこちらの過去記事参照)。この中でコマンドを組み立てます。この、コマンドの組み立て方が(私には。。)かなりトリッキーなのですが、とりあえず最初のCommand.literal("hoge")がコマンド名だと思ってくださいませ。 続いてのexecutesがコマンド実行時の処理本体です。ここにはCommandContent<CommandSource>を引数に取るラムダを渡します。今回はhoge command executedがログ出力されるだけですが、ここに好きな処理を書いてください。 最後に組み立てたコマンドの実態であるLiteralArgumentBuilder<CommandSource> builderをevent.getDispatcher().register(builder);で登録してあげればOKです。 コマンドの分岐 組込みのコマンドtimeなどを使うとき、timeの後に時間の指定方法がいくつか選択肢として出てくるのを見たことがあると思います。こんな感じ。 timeコマンドは時間の指定方法として朝昼晩を指定したり時刻を指定したり時間を進める、といったことができますが、それらを第二引数以降で指定させることができます。このようなコマンドの分岐や引数の指定は前述のコマンドの組み立ての際にあれやこれやすることでできます。 さっそくサンプルコードを見てみましょう。 CommandTest.java @SubscribeEvent public void onRegisterCommand(RegisterCommandsEvent event) { LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge") .then(Commands.literal("fuga") .executes(context -> { LOGGER.info("hoge fuga command executed"); return Command.SINGLE_SUCCESS; })) .then(Commands.literal("piyo") .executes(contect -> { LOGGER.info("hoge piyo command executed"); return Command.SINGLE_SUCCESS; })); event.getDispatcher().register(builder); } onRegisterCommandメソッドだけ書き換えてみました。ちょっと複雑になってきました。正直わかりにくいです。多分何とかパターンとかいうデザインパターンで作られてるんでしょうが普通にif-elseとかswitchで書かせてくれた方が可読性も高いしいいと思うのだけど。。。と愚痴ってもしょうがないので解説。(最後にちょっとまとめて愚痴りますw) then()メソッドやexecute()メソッドはLiteralArgumentBuilder<CommandSource>のインスタンスを返してくれます。これに対してthen()やらexecutes()を数珠つなぎにしてコマンドの分岐の構造を定義していきます。上の例ではインデントを気を付けているのでまだ理解しやすいと思いますが、同列に並んでるthenがコマンドの分岐になります。なので、hogeコマンドの後の分岐としてfugaとpiyoが出てくると。試しに動かしてみます。 うまく動きました。hogeを打った後スペースを一つ入れると分岐のfugaとpiyoが出てきますね。このどちらかを入力して実行するとサンプルコードにある通りのログが出力されます。fugaやpiyoの内側、一段ネストされた中にthen()を並べればさらに複雑な分岐条件も作ることができます。ただ、ネストしまくるとどんどん分岐の構造がわかりにくく。。パーレンの対応関係とインデントに気を付けて設計してくださいませ。。一応一つだけ例を載せておきますね。fugaの後を分岐させてみました。 CommandTest.java @SubscribeEvent public void onRegisterCommand(RegisterCommandsEvent event) { LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge") .then(Commands.literal("fuga") .then(Commands.literal("hogehoge") .executes(context -> { LOGGER.info("hoge fuga hogehoge executed"); return Command.SINGLE_SUCCESS; })) .then(Commands.literal("piyopiyo") .executes(context -> { LOGGER.info("hoge fuga piyopiyo executed"); return Command.SINGLE_SUCCESS; }))) .then(Commands.literal("piyo") .executes(contect -> { LOGGER.info("hoge piyo executed"); return Command.SINGLE_SUCCESS; })); event.getDispatcher().register(builder); } こんな感じです。実行してみると。 うまく行ったようですね。ちなみにこの程度のコードを書くだけでもパーレンの対応を何度か確認しながら書きました。。(おっと、また愚痴りそうに) ちなみにこのfugaとpiyo、コマンドの引数やん!って思われると思います。いや、そうなんですけどね。ただ、Forge(マイクラ自体?)のコマンドではこのthen()からCommands.literal()で指定されたものと、この後で開設する引数(Argument)は明確に扱いが違うのであえて「分岐」と書いています。 コマンドの引数 続いてコマンドの引数です。コマンドに対して整数値や浮動小数点、文字列、カスタム型の引数を渡すことができます。まずはサンプルコード。 CommandTest.java @SubscribeEvent public void onRegisterCommand(RegisterCommandsEvent event) { LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge") .then(Commands.argument("argumentName", IntegerArgumentType.integer()) .executes(context -> { return executeHoge(context); })); event.getDispatcher().register(builder); } public int executeHoge(CommandContext<CommandSource> context) { int argumentInteger = IntegerArgumentType.getInteger(context, "argumentName"); LOGGER.info("hoge command executed: " + argumentInteger); return Command.SINGLE_SUCCESS; } コードをだいぶすっきりさせました。まず分岐の説明のために複雑に構造化されていた部分はバッサリ消して、hogeの後に整数の引数を一つとるようなコードに変えています。また、ラムダの中にごちゃごちゃ処理を書くとわけがわからなくなるのでexecuteHoge()メソッドに切り出しました。実行してみます。 hogeの後に数字を入力したところ何やら<argumentName>なる表示が上に出るようになりました。そのままコマンドを実行してみます。 [01:38:13] [Server thread/INFO] [minecraft/MinecraftServer]: Saving chunks for level 'ServerLevel[New World]'/minecraft:overworld [01:38:14] [Render thread/INFO] [minecraft/AdvancementList]: Loaded 0 advancements [01:38:15] [Server thread/INFO] [minecraft/MinecraftServer]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_nether [01:38:15] [Server thread/INFO] [minecraft/MinecraftServer]: Saving chunks for level 'ServerLevel[New World]'/minecraft:the_end [01:38:15] [Server thread/DEBUG] [ne.mi.fm.FMLWorldPersistenceHook/WP]: Gathering id map for writing to world save New World [01:38:15] [Server thread/DEBUG] [ne.mi.fm.FMLWorldPersistenceHook/WP]: ID Map collection complete New World [01:38:22] [Server thread/INFO] [jp.mu.mo.co.CommandTest/]: hoge command executed: 1234 最後の行にログが出ていて、引数の整数がちゃんと受け取れているのがわかりますね。パチパチパチ。では解説。 引数を受け取る場合、then()の中でCommands.argument()メソッドを呼びます。このCommands.argument()メソッドの第一引数(サンプルコードではargumentName)は引数の名前を指定します。あとで引数の値を取り出すときのキーになります。 第二引数にはArgumentType型のインスタンスを受け取りますが、このサンプルではIntegerArgumentTypeを渡しています。IntegerArgumentType.integer()はオーバーロードされていて、最小値や範囲を指定したIntegerArgumentTypeを作って渡すことができます。 続いて引数の受け取り側、executeHoge()の中を見てみます。ポイントはint argumentInteger = IntegerArgumentType.getInteger(context, "argumentName");ですね。みりゃわかるけど。第一引数はCommandContext、第二引数は先ほど指定した引数の名前です。これでコマンド実行時に指定した引数を受け取れる、というわけですな。 引数の後に前述の分岐を置くこともできます。分岐にはなってませんが、整数の引数の後に"fuga"のliteralが来るようなコードを書いてみます。 CommandTest.java @SubscribeEvent public void onRegisterCommand(RegisterCommandsEvent event) { LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge") .then(Commands.argument("integerArgument", IntegerArgumentType.integer()) .then(Commands.literal("fuga") .then(Commands.argument("stringArgument", StringArgumentType.string()) .executes(context -> { return executeHoge(context); })))); event.getDispatcher().register(builder); } public int executeHoge(CommandContext<CommandSource> context) { int integerArgument = IntegerArgumentType.getInteger(context, "integerArgument"); String stringArgument = StringArgumentType.getString(context, "stringArgument"); LOGGER.info("hoge command executed"); LOGGER.info("integerArgument : "+ integerArgument); LOGGER.info("stringArgument : "+ stringArgument); return Command.SINGLE_SUCCESS; } 実行してみると。。。 途中の分岐である"fuga"が表示されてますね。このまま入力を続けて最後の文字列引数に"abcd"を入れてコマンドを実行してみると。。。 [13:48:32] [Server thread/DEBUG] [ne.mi.fm.FMLWorldPersistenceHook/WP]: Gathering id map for writing to world save New World [13:48:32] [Server thread/DEBUG] [ne.mi.fm.FMLWorldPersistenceHook/WP]: ID Map collection complete New World [13:51:10] [Server thread/INFO] [jp.mu.mo.co.CommandTest/]: hoge command executed [13:51:10] [Server thread/INFO] [jp.mu.mo.co.CommandTest/]: integerArgument : 4321 [13:51:10] [Server thread/INFO] [jp.mu.mo.co.CommandTest/]: stringArgument : abcd ちゃんと引数も受け取れてますね。 引数の型 Forge(マイクラ)ではいくつかの組み込みの型が用意されていて、大体のことはそれらを使えば事足りると思います。筆者も全部は全然使いこなせていないのですが、どこにそれらのクラスがあるのかだけご紹介 minecraftの組み込み引数 net.minecraft.command.argumentsパッケージ内に存在しています。ちょっと中身を眺めてみると。。。 XxxxxxArgument.classと命名されているクラスが引数クラスのようですね。名前だけではぱっと見何に使うのか想像がつかないものもたくさんあります。。まぁ、いずれ。。 brigadierライブラリ 本家のMojangがMITライセンスで公開しているコマンドライブラリです。gitリポジトリはこちら。ただ、Mdkを使っていれば同梱されているので個別に落としてくる必要はないです。中身を少し眺めてみると。。。 上の方のサンプルでも使っていたStringArgumentTypeやIntegerArgumentTypeがあることが確認できます。他にもBoolとかFloatとか。まだ使ったことないものもありますが、まぁ直感的に使えそうな感じですね。 権限 マイクラではコマンドを実行する権限を制御することができます。といっても、現時点で分かっているのはチートOn/Offで使える/使えないを切り替える、という使い方。どうやらもうちょっと色々できそうな感じですが、より詳細な使い方はわかってきたら加筆する、ということで。。 CommandTest.java @SubscribeEvent public void onRegisterCommand(RegisterCommandsEvent event) { LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge") .requires(context -> { return true; }) .executes(context -> { return executeHoge(context); }); event.getDispatcher().register(builder); } public int executeHoge(CommandContext<CommandSource> context) { LOGGER.info("hoge command executed"); return Command.SINGLE_SUCCESS; } またコードをシンプルに戻しました。ポイントはCommand.literal("hoge")の後のrequires()ですね。このrequires()はPredicateを引数に取ります。つまり、ラムダでboolを返せばよいと。で、このサンプルコードのようにfalseを返すとそのコマンドは使えなくなる(マスクされる)というわけです。動かしてみましょう。 ほら、hogeコマンドが出てこなくなった。もちろん、サンプルコードみたいに常にfalseを返すような実装をすると一生使えないコマンドになるので、使わせたくないときだけfalseを返せばいいわけです。 ちなみにこのコマンドのマスク、コマンドの分岐でも使えます。例えばこんなコード。 CommandTest.java @SubscribeEvent public void onRegisterCommand(RegisterCommandsEvent event) { LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge") .then(Commands.literal("fuga") .requires(context -> { return false; }) .executes(context -> { return executeHoge(context); })) .then(Commands.literal("piyo") .executes(context -> { return executeHoge(context); })); event.getDispatcher().register(builder); } public int executeHoge(CommandContext<CommandSource> context) { LOGGER.info("hoge command executed"); return Command.SINGLE_SUCCESS; } "hoge"の後の"fuga"だけマスクしてみました。すると、 "fuga"が選択肢から消えましたね。こんな使い方したいケースはあるかわからないですが、特定の条件下だけで分岐を許す、みたいなこともできそうですね。 でででで、お次は実用的(?)な使い方。権限によるマスクをやってみます。早速コード。。 CommandTest.java @SubscribeEvent public void onRegisterCommand(RegisterCommandsEvent event) { LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge") .requires(context -> { return context.hasPermission(4); }) .executes(context -> { return executeHoge(context); }); event.getDispatcher().register(builder); } public int executeHoge(CommandContext<CommandSource> context) { LOGGER.info("hoge command executed"); return Command.SINGLE_SUCCESS; } 今回のポイントはrequires()の中のreturn context.hasPermission(4);ですね。あとで解説しますが、これはチートか許可されているかどうかを判定するコードになっています。チートOn/Offで試してみると。。。 チートOff チートOn チートがOnの時だけしか使えなくなってますね。で、解説。context.hasPermission(4);は現在の権限(Permission)が引数(ここでは4)以上の場合だけtrueを返します。つまり、チートがOnの時は権限が4以上になっているということですね。 「4以上」ということは権限が3や2や1になることもあるのかな。。と思ったんですが、試せた範囲だと以下のようになることがわかりました。 チートOff : 0 チートOn : 4 つまり0か4の2択。。。でも、CommandSourceクラスのソースを見ると CommandSource.java public boolean hasPermission(int p_197034_1_) { return this.permissionLevel >= p_197034_1_; } となっているので、数字が大きくなるほど権限が多い、ということを示唆してますよね。。。現時点では0か4以外の権限になるケースがわかっていないので、この謎はまたわかった時に追記します。。 おしまい コマンド編は一応こんなところです。完璧に使いこなそうと思うとMdkやbrigadierのソースを追わないとダメなケースも出てくると思いますが、この記事の基本が理解できていればきっとサクサクと作れるようになるでしょう!(ほんとかな) 最後に参考情報です。自作コマンドを作るうえではnet.minecraft.command.implの中に組み込みのコマンドのコードがいっぱい入っているのでこれらを参考にするとよいでしょう。後述の愚痴に書くように、なかなか読むのもおっくうになるような実装もありますが、まぁ頑張って読み解けば参考になる情報はいっぱい埋まってると思います。 愚痴 こっからはmodの開発に直接関係ないです。。が、今回調べててこの作りはひどくね?と思ったところがいくつかあったので愚痴っていこうと思います。。。Mdkもminecraftも私より優秀な人が書いているので的外れな指摘かもですが。。。 コマンドの引数、分岐をthen()、requires()でつないでいきますよね。これが一直線に並ぶならいいと思うんですが、分岐のための木構造をとるのでわけわからなくなるんですよね。例えばこのコード。 CommandTest.java @SubscribeEvent public void onRegisterCommand(RegisterCommandsEvent event) { LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge") .then(Commands.literal("piyo")) .executes(context -> { LOGGER.info("hoge piyo"); return Command.SINGLE_SUCCESS; }) .then((Commands.literal("fuga")) .executes(context -> { LOGGER.info("hoge fuga"); return Command.SINGLE_SUCCESS; })); event.getDispatcher().register(builder); } 正しそうに見えません?実は正しくないっす。実行すると ありゃりゃ。ランタイムエラーになったっちゃったよ。。。トホホ まず正解から。これが正解。 CommandTest.java @SubscribeEvent public void onRegisterCommand(RegisterCommandsEvent event) { LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge") .then(Commands.literal("piyo") .executes(context -> { LOGGER.info("hoge piyo"); return Command.SINGLE_SUCCESS; })) .then((Commands.literal("fuga")) .executes(context -> { LOGGER.info("hoge fuga"); return Command.SINGLE_SUCCESS; })); event.getDispatcher().register(builder); } どこが間違ってるかわかります?ここを修正したんです。 CommandTest.java @SubscribeEvent public void onRegisterCommand(RegisterCommandsEvent event) { LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge") .then(Commands.literal("piyo") // <- ここ! .executes(context -> { LOGGER.info("hoge piyo"); return Command.SINGLE_SUCCESS; })) // <- とここ! .then((Commands.literal("fuga")) .executes(context -> { LOGGER.info("hoge fuga"); return Command.SINGLE_SUCCESS; })); event.getDispatcher().register(builder); } 最初の誤ってるコードと比較してみてください。パーレンの対応が間違ってたんですね。つまり、then()でコマンドを分岐させる場合、分岐させるもう一つのthen()の戻り値に対してthen()してやる必要があるんですが、誤っていたコードでは最初のexecutes()の戻り値に対してthen()しちゃってるんですよね。 パーレンの対応一個間違っただけで動かないなんて。。。といっても「プログラムなんてそんなもんだろ!」という声も聞こえてきそうですが、この実装がイケてない理由は コマンドを実行するまで分岐構造のミスに気付かない 先ほどの誤ったコード、コンパイルはおろかコマンドの登録までできちゃいます。で、誤りに気付くのはコマンド実行時。。しかも出力されるエラー、ログはクソ非常にわかりづらく、何が間違ってるのかわからんという。。 可読性低杉 ここに載せたサンプルコードはかなり意識してインデントしたのでまだ構造が読めると思います。ただ、IDEのインデントに任せてももっとごちゃごちゃしちゃって読めまへん。IntlliJだともっときれいにインデントしてくれんのかいな。。ちなみにさらに絶望したのは組み込みコマンドtimeの実装。まぁ見てよ。 TimeCommand.class public static void register(CommandDispatcher<CommandSource> p_198823_0_) { p_198823_0_.register(Commands.literal("time").requires((p_198828_0_) -> { return p_198828_0_.hasPermission(2); }).then(Commands.literal("set").then(Commands.literal("day").executes((p_198832_0_) -> { return setTime(p_198832_0_.getSource(), 1000); })).then(Commands.literal("noon").executes((p_198825_0_) -> { return setTime(p_198825_0_.getSource(), 6000); })).then(Commands.literal("night").executes((p_198822_0_) -> { return setTime(p_198822_0_.getSource(), 13000); })).then(Commands.literal("midnight").executes((p_200563_0_) -> { return setTime(p_200563_0_.getSource(), 18000); })).then(Commands.argument("time", TimeArgument.time()).executes((p_200564_0_) -> { return setTime(p_200564_0_.getSource(), IntegerArgumentType.getInteger(p_200564_0_, "time")); }))).then(Commands.literal("add").then(Commands.argument("time", TimeArgument.time()).executes((p_198830_0_) -> { return addTime(p_198830_0_.getSource(), IntegerArgumentType.getInteger(p_198830_0_, "time")); }))).then(Commands.literal("query").then(Commands.literal("daytime").executes((p_198827_0_) -> { return queryTime(p_198827_0_.getSource(), getDayTime(p_198827_0_.getSource().getLevel())); })).then(Commands.literal("gametime").executes((p_198821_0_) -> { return queryTime(p_198821_0_.getSource(), (int)(p_198821_0_.getSource().getLevel().getGameTime() % 2147483647L)); })).then(Commands.literal("day").executes((p_198831_0_) -> { return queryTime(p_198831_0_.getSource(), (int)(p_198831_0_.getSource().getLevel().getDayTime() / 24000L % 2147483647L)); })))); } これで分岐の木構造が頭の中にコンパイルされた人は病気ですすごいです。なんかのデザインパターンを使ってるのかもしれないですが、ここまでくると乱用じゃね?可読性やらデバッグの容易性を考えたら愚直にif/elseを並べて構造がそのままわかるような実装方法にすりゃいいと思うんですがね。 引数指定誤りのミスに気づけない 次はこんなコード。文字列の引数をとってそれをログに出力するという本編にも書いたようなコードです。 CommandTest.java @SubscribeEvent public void onRegisterCommand(RegisterCommandsEvent event) { LiteralArgumentBuilder<CommandSource> builder = Commands.literal("hoge") .then(Commands.argument("stringArgument", StringArgumentType.string()) .executes(context -> { return executeHoge(context); })); event.getDispatcher().register(builder); } public int executeHoge(CommandContext<CommandSource> context) { String stringArgument = StringArgumentType.getString(context, "stringArgment"); LOGGER.info("hoge command executed: " + stringArgument); return Command.SINGLE_SUCCESS; } コマンドを実行してみると。 でました、ランタイムエラー。例によって出力されるエラー、ログはクソ非常にわかりづらくって何がお起こってるのかわからねぇ。このコード、何が間違っていたかというと、executeHoge()内で引数を取得する時の引数のキーstringArgumentの綴りが誤ってるところ。 キーをString STRING_ARGUMENT = "stringArgument"こんな感じで定数にして、引数定義と参照のところで使えばいいじゃん、という声が聞こえてきそうですが。。。これの何がイケてないかというと そもそも2か所で同じキーを使わせるところ 上に書いた定数を使う方法は、誤りを回避する方法であって、そもそも2か所で同じキーを使わせないような作りにしてくれればいいのに、と思うわけですよ。例えば、String stringArgument = context.requireArgument("stringArgument")みたいに引数定義と同時にその値がとれてしまえばいいわけじゃないかなぁと思うわけですわ。 原因が分からなさ杉 今回の誤りの裏で何が起こっていたのか。デバッガを使って調べたところ実はIllegalArgumentExceptionがthrowされてます。で、minecraftの実装が見事にその例外をもみ消してクソみたいな分かりにくいエラーだけ出してくれてます。余計なことをせずにログにStackTraceを出してくれりゃいいのに。。もしくは、getString()が存在しないキーを指定された場合はnullを返すような作りになってた方が未だなんぼかわかりやすいと思うんだけどねぇ。。 で、最後に思ったこと 全ての原因は、マイクラが起動時のイベントでコマンドの構造をすべて組み立てて事前登録させる、という設計になっている点なのかなぁと思いました。なのでthen()の数珠つなぎで木構造を作らせたり、引数のキーを引数定義のところと実行時のラムダの2か所で参照させざるを得なかったとか。。 コマンドの構造を事前登録させたかった理由まではわかりませんが、だったとしてももうちょっとマシにできたんじゃないのかなぁとか思ったり。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[雑談]プログラマー、エンジニアの2社目としての振り返り

久々の更新になります。 今、派遣エンジニアとして働いてます。(登録制の派遣です。) 今回は6月末で現場を終了することになりましたので(9か月間 契約満了)、 『現場で吸収できたこと』の"振り返り"をしたいなと思います。 まずは、現場の方々お世話になりました。またありがとうございました!! ★★契約終了理由★★ ①コロナでお客さんの売上下がって受注が厳しくなりお客さんとの契約もどうするか状態であること ②契約が継続するかのタイミングだった ③担当していた案件が利益が低いことが 理由みたいです。。。 担当したシステムは、 1. 中小企業診断士向けのWEBシステム 2. 映画館チケットのWEB販売システム(複数社)/受託パッケージシステム の保守開発を担当しました。 担当工程は、 詳細設計、実装、単体テスト、結合テスト、総合テスト、リリース作業、保守(バク、エラー調査、改修) 使用言語ですが、 Java Javascript HTML JSP SQL を経験できました。 次にフレームワークは、 1. JSF 2. struts 3. jQuery ツールに関しては、 1. Eclipse 2. コマンドプロンプト 3. Teraterm 4. Apache tomcat 5. Oracle 6. A5:SQL Mk-2 7. Zoom 8. teams 9. Backlog 10. SVN 11. powershell 12. git 13. AWS(Elastic Load Balancing等) 14. MySQL 使用しました。 業務内容に関しては、 1. システム調査(エラー、バグ) 2. 追加実装 3. プログラム修正 4. ページ作成 5. 動作確認 6. リファクタリング 7. ドキュメント作成 8. テスト業務 9. リリース作業のサポート 全体的な感想として、 入ったばかりの時は、テストが多かった印象です。 受託開発の会社だったので(系列会社はSierの大手会社)、 別の部隊のシステムやアプリのテスト作業などのサポートに入った思い出があります。(バグやデグレがひどくかなり予定より遅れてる案件に入りました。汗) 比率的に テスト : 実装and詳細設計 = 7 : 3 の割合でした。 2か月くらい過ぎてくらいから、 比率が テスト : 実装and詳細設計 = 3 : 7  の割合になりました。 紹介している現場の前の現場も、 フロントエンド側だったこともあり(たまにバックエンドもやってました)、 今回もフロントエンド側の追加実装とか改修はほぼほぼこなしました。 バックエンドも少し追加実装したり、改修作業やりました。 あとは、Excelで実装したあとの資料作り(詳細設計andテストエビデンス)作りは初めてやったので、(自社開発でやったときはjira softwareのwikiに軽く記載程度でしたので)いい経験になったかなと思ってます。 古い技術や環境の現場ではありましたが、 周りの方は優しい方多かったです。 エンジニアは細かいところに気付くが故に、 1. マウンティングしてしまう方 2. 表現が言葉足らずになり切れてしまう人 3. 知識やスキルがわからない人を置いてけぼりにする人 多いので、この現場では比較的少なかったですね。。。笑 逆に細かいところに気付くことで、 1. バグが起きにくい 2. コミュニケーションが行き届く?笑 3. トラブルが起きたとき対応できる 等々みたいなメリットもあると思いますが、やりにくいので助かりました。 話はそれましたが、 今回は要件定義、基本設計以外は関わることができたかなと思います。 次は、Go言語勉強しているのでGo言語の現場で働きたいのが希望です。 とはいえ無理なら他の言語や今まで使ってきた言語でもよいかなと....。 ※プライベートPC(DELLのWindows10 4年強くらい使用)が調子悪くなりました。 なので、PCをMacに変更予定です。買ったら、本格的にWEBアプリケーション(チャットツールっぽいもの)開発予定です。。 今回、2社目(厳密に言うと5社目。最初の3つはSES事業で"経歴詐称"や"客先は面談内容と全く違う事させる"、"パワハラ、モラハラ、セクハラは当たり前"等々があるヤバい会社でした。すぐ辞めました。汗)でしたが、よかったです。 IT関連の会社を選びときは、慎重に!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Stream#reduceが分かる記事

Stream#reduceが分かる記事  こんにちは。  これは、JavaのStream APIのreduceをよく分かりたいという趣旨の記事です。  reduceはStream APIの中でも最も難解な関数のひとつだと思います。mapやfilterは使っているうちに慣れたという人でも、reduceをバリバリ使う人はあまりいないのではないでしょうか。  そして、そろそろreduceとかも知っておこうかな……と思ってドキュメントを読んでみたものの、見慣れない単語ににぎょっとして結局あまり使っていないという人は多いのではないでしょうか。  reduceにはいくつかのオーバーロードがありますので、それぞれの日本語の説明を引用してみます。 結合的累積関数を使用して、このストリームの要素に対してリダクションを実行し、リダクションされた値を記述するOptionalを返します(ある場合)。 指定された単位元の値と結合的な累積関数を使ってこのストリームの要素に対してリダクションを実行し、リデュースされた値を返します。 指定された単位元、累積関数、および結合的関数を使用して、このストリームの要素に対してリダクションを実行します。  これを読んでなるほどとなる人、関数のシグネチャが想像つく人はLGTMを押してブラウザバックです。あなたにこの記事は必要ありません。  なんとなく分かる気がするけどよく読むと単語一つ一つがなんかよく分からん、という人はこの記事の読者です。今のうちにストックを押しておくといいでしょう。  キーワードは「単位元」「結合的な関数」「累積関数」です。 Javaプログラマのためのモノイド  さっそく新しい単語が出てきて申し訳ないのですが、「結合的な関数」と「単位元」を持つ構造を「モノイド」と呼びます。 結合的な関数  ある関数が結合的であるとは、その二項関数が次のような性質を満たすことを言います。 (a \times b) \times c = a \times (b \times c)  $ \times $という演算が右結合でも左結合でも計算結果が変わらないよ、ということです。  Javaで書くと、次のような感じです。  次のようなクラスAとそのメソッドfがあったとして、 class A { public A f(A a); }  関数fが結合的であるとき、次を満たすことが期待できます。 @Test void testAssociativity(A a, A b, A c) { assertEquals(a.f(b).f(c), a.f(b.f(c))); }  staticメソッドを使うともっと分かりやすいかもしれません。  次のようなクラスAと静的なメソッドfがあったとして、 class A { public static A f(A a, A b); }  結合的な関数fは次を満たします。 @Test void testAssociativity(A a, A b, A c) { assertEquals(f(f(a, b), c), f(a, f(b, c)); }  結合則は足し算や掛け算では当たり前の性質ですが、Javaコードで見るとずいぶん特殊な性質に見えますね。 単位元  ある二項関数 $\times$ における単位元とは、次のような等式を満たす元 $ e $ のことを言います。 a \times e = e \times a = a  Java風に書くと、次のような感じです。  次のような関数fと単位元eがあって、 class A { public static A e = new A(); public A f(A a); }  eがfの単位元であるとき、次を満たすことが期待できます。 @Test void testLeftIdentity(A a) { A actual = A.e.f(a); assertEquals(a, actual); } @Test void testRightIdentity(A a) { A actual = a.f(A.e); assertEquals(a, actual); }  結合則のとき同様に、staticなfでも書いてみましょうか。  次のような静的な関数fと単位元eがあって、 class A { public static A e = new A(); public static A f(A a, A b); }  eがfの単位元であるとき、次が成り立ちます。 @Test void testLeftIdentity(A a) { A actual = A.f(a, A.e); assertEquals(a, actual); } @Test void testRightIdentity(A a) { A actual = A.f(A.e, a); assertEquals(a, actual); }  足し算における $0$, 掛け算における $1$ が単位元ですね。 Javaプログラマのための畳み込み  ようやく本題ですね。  Javaでは「リダクション」「リデュース」などと呼ばれるものは、一般には畳み込み処理と呼ばれます。  畳み込み処理とは、簡単に言えば複数の値を一つの値にする処理のことで、例えば合計を取るIntStream#sumや長さを取るList#sizeは代表的な畳み込み処理だといえます。  つまり、Stream#reduceはまさに一般化された"畳み込み"そのものというわけですね。  畳み込み処理は下のような形で表現されます。 a_1 \times (a_2 \times (a_3 \times (\dots \times (a_{n - 1} \times a_n))))  これは$ a_1 $ から $ a_n $ までの値を $ \times $ という累積関数で畳み込んでいる、という風に読みます。  ちなみに上を右畳み込みといい、括弧を付ける順序を変えた下を左畳み込みといいます。 ((((a_1 \times a_2) \times a_3) \times \dots) \times a_{n - 1}) \times a_n  ここで、累積関数 $ \times $ が結合的だった場合、右畳み込みと左畳み込みの結果が変わらないことが分かります。この性質は、並列Streamを処理するときに特に役に立ちます。  さらに単位元を要素数0のときの値とすることで、結果がOptionalにならないreduceを作ることができます。  このように、モノイドと畳み込み処理は非常に相性が良いのです。 reduce 3種類のまとめ  以上を踏まえて3種類あるreduceのオーバーロードの型に説明を付けていきましょう。 Optional<T> reduce​(BinaryOperator<T> accumulator) 結合的な累積関数accumulatorを用いて、Streamを畳み込む Streamの長さが0の場合はOptional.emptyが返る T reduce​(T identity, BinaryOperator<T> accumulator) 結合的な累積関数accumulatorを用いて、Streamを畳み込む Streamの長さが0の場合はidentityが返る <U> U reduce​(U identity, BiFunction<U,​? super T,​U> accumulator, BinaryOperator<U> combiner) TからモノイドUへの変換機能と累積関数を兼ねた少し特殊なaccumulatorを使って畳み込む 並列Streamの場合は畳み込み結果が複数生まれるので、それをcombinerを使ってもう一度畳み込む  前とは違って、この説明が読めるようになっていればreduceを分かったことになります。  また、うっかりBinaryOperatorを結合的ではない実装にしてしまい、並列Streamの時だけ挙動が変という厄介なreduceを書いてしまう心配もありません。 付録:プログラミングに表れる色々なモノイド  さて、モノイドを知るとreduceを使いこなせそうなことが分かって頂けたと思います。  そこで最後におまけとしてプログラミングによく現れる色々なモノイドを見ていきましょう。 数値の足し算  まずはint, +, 0の組です。これは直観的に分かりますね。 (m + n) + l = m + (n + l) // associativity n + 0 = 0 + n = n // identity  reduceに入れてあげると、合計を計算するsumを実装できます。  上の例で出した通り、int, *, 1でも同じようにモノイドになります。 リストの結合  次に、ArrayList, ArrayList#addAll, new ArrayList()の組もモノイドを構成します。 list1.addAll(list2.addAll(list3)) = list1.addAll(list2).addAll(list3) // associativity (new ArrayList()).addAll(list) = list.addAll(new ArrayList()) = list // identity  通常、空のリストにはCollection.emptyList()を使いますが、ここではaddAllを使う関係でnew ArrayList()としています。  また必ずしもArrayListである必要はなく、Listライクな構造であれば何でもモノイドを作れそうです。例えばString, +, ""でもリストのモノイドの例になりそうです。  reduceに入れてあげると、全てのリストを結合させた一つのリストを作ることができます。 min  次はint, Math.min, Integer.MAX_VALUEを見ていきましょう。 Math.min(Math.min(m, n), l) = Math.min(m, Math.min(n, l)) // associativity Math.min(n, Integer.MAX_VALUE) = Math.min(Integer.MAX_VALUE, n) = n // identity  引数のうちより小さい方の値を返すMath.min関数は、その型の最大値とモノイドを形成します。  結合則は比較順序に拘わらず3つの数のうちの最小値を取れることを示していますね。  reduceに入れてあげると、Streamの中の最小値を取得することができます。  同様にして、int, Math.max, Integer.MIN_VALUEもモノイドを形成します。 論理AND (&&)  まだまだあります。boolean, &&, trueもまたモノイドを形成します。 (m && n) + && = m && (n && l) // associativity n && true = true && n = n // identity  言われてみればそりゃそうですよね。  reduceに入れてあげると、Streamの中の全ての値がtrueであるかどうかを検証することができます。  同様にして boolean, ||, falseもモノイドを形成します。 ビットAND (&)  ビットAND自体あまり使わないかもしれませんが、int, &, ~0というビットANDもモノイドを形成します。 (m & n) + & = m & (n & l) // associativity n & (~0) = (~0) & n = n // identity  reduceに入れてあげると、Streamの中の全ての値のビットANDを取ることができます。  同様にして int, |, 0もモノイドを形成します。 エルビス演算子 (?:) または null合体演算子 (??)  エルビス演算子はJavaにはありませんが、GroovyやKotlinにはある便利なやつです。  C# ではnull合体演算子と呼ばれますね。  これは左辺がnullだった場合に右辺を返すという演算子なのですが、面白いことにT, ?:, nullという組がモノイドを形成します。 (a ?: b) ?: c = a ?: (b ?: c) // associativity null ?: a = a ?: null = a // identity  このエルビス演算子は、Javaで書くとちょうど次のような関数です。 public static <T> T elvis(T a, T b) { return a != null ? a : b }  少し変わり種ですが、面白いですよね。  reduceに入れてあげると、Streamの中の最初の非nullの値を取ることができます。 関数合成 (Function#andThen)  もう一つ変わり種で、Function, Function#andThen, Function.identity()はモノイドを形成します。 f.andThen(g).andThen(h) = a.andThen(g.andThen(h)) // associativity f.andThen(Function.identity()) = Function.identity().andThen(f) // identity  Function.identity()は何もしない関数です。  reduceに入れてあげると、全ての関数を実行する一つの関数を取得できます。 アプリケーション設定  最後は自作の型でモノイドを作ってreduceしてみましょう。  ある程度実用的な例として、何かしらのアプリケーションの設定をモノイドにしてみます。  このアプリケーションは設定を1. 設定ファイル 2. 環境変数 3. コマンドライン引数 で指定でき、さらに設定は1 < 2 < 3の順序で優先するような、よくある仕様とします。  アプリケーション設定を次のようなクラスで表現します。 class ApplicationConfig { private String field1; // 他にもたくさんのフィールド... public Applicationconfig(String field1, ...) { this.field1 = field1; ... } public String getField1() { return this.field1; } ... }  まず、2つのアプリケーション設定を合成するような関数と、その合成の単位元を定義します。 final class ApplicationConfigUtils { // 単位元。フィールド全てにnullを指定する public static ApplicationConfig identity = new ApplicationConfig(null, ...); // 結合的な二項演算 public static ApplicationConfig compose(ApplicationConfig a, ApplicationConfig b) { return new ApplicationConfig( a.getField1() != null ? a.getField1() : b.getField1(), ... ); } }  前準備はこれだけです。  各パラメータを読み込む関数を作り、Stream.ofに並べてreduceすれば期待する設定を得ることができます。 ApplicationConfig config = Stream.of( loadConfigFromCommandLine(), loadConfigFromEnvironmentVariables(), loadConfigFromFiles() ).reduce(ApplicationConfig.identity, ApplicationConfigUtils::compose); 終わりに  モノイドを勉強することで、Stream#reduceが分かった。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HelloWorldについて

HelloWorldの実行について ①プロジェクトの作成 ②パッケージの作成 ③クラスの作成 ※public static void main(String[] args) にチェックを入れる ④コードの入力 package test1; public class Test1 { public static void main(String[] args) { // TODO 自動生成されたメソッド・スタブ System.out.println("Hello World"); } } ⑤実行
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SQLインジェクション

SQLインジェクションとは アプリケーションのセキュリティ上の不備を意図的に利用し、アプリケーションが想定しないSQL文を実行させることにより、データベースシステムを不正に操作する攻撃方法のこと、また、その攻撃を可能とする脆弱性のことです。 SQLに別のSQL文を「注入(inject)」されることから、「ダイレクトSQLコマンドインジェクション」もしくは「SQL注入」とも呼ばれます。 アプリケーションが入力値を適切にエスケープしないままSQL中に展開することによりで発生します。 次のようなSQLが発行されることを前提に説明します。 SELECT * FROM users WHERE name = '(入力値)'; ここで入力値に "1' OR '1' = '1" という文字列を与えた場合を考えると、SQL文は次のように展開されます。 SELECT * FROM users WHERE name = '1' OR '1' = '1'; 上記のSQL文では条件が常に真となるため、nameカラムの値にかかわらず、レコードが全件取得できてしまいます。このような攻撃は、入力値が1つだけのものに限りません。 また、これを応用してSELECTの条件式にデータベース内の情報を確認するようなサブクエリーを含ませ、その抽出の成否によって本来参照することのできないデータベース内の情報(テーブル名など)を知ることができる、ブラインドSQLインジェクションと呼ばれる手法も存在する。 また、複数のSQL文を注入することによるデータの破壊や改竄、ストアドプロシージャを実行させ、情報の漏洩や改竄、OSコマンドの実行などを引き起こすこが可能のる場合があります。 対策 型を指定する 静的プレースホルダの利用する 発行するsql文の中で文字列展開をしない 文字エンコーディングを指定する 入力値に対してのチェックを行う Javaでの静的プレースホルダ実装例 import java.sql.PreparedStatement; public class Main { public static void main(String[] args) { /** * 省略 */ stmt = conn.prepareStatement("SELECT name, secret FROM users WHERE user = 'true' AND name = ? AND password = ? "); stmt.setString(1, name); stmt.setString(2, password); rs = stmt.executeQuery(); } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む