bookmark_borderJava21からの「データ指向」は「オブジェクト指向」と何が違うのか

特にJava20,21以降で話題になっている「データ指向プログラミング」とは何か、オブジェクト指向と何が違うのかの個人的まとめです。

※本来は型や継承・implementsの縛りも関係しますが、この記事では「データは不変である」という要素に重きを置いています。

オブジェクト指向を置き換えるものではない

データ指向プログラミングはオブジェクト指向と共存する概念で、JavaのアップデートによってJavaがオブジェクト指向言語から脱却するという意味ではありません。

オブジェクト指向をより保守性高く使う上でベースとなる考え方・テクニックのことです。

Java20で「データ指向」という言葉が出てきた背景

保守性、(言語レベルで)セキュリティの高いJavaの書き方が今まで多数ベストプラクティスとして広まってきました。「プロパティは不変にすべき」「不変ゆえのgetter/setterの簡略化」などです。後続言語のKotlinやSwiftは言語レベルでそれをサポートするようになりました。

2023年になり、Javaもそれらの考え方を言語レベルで本格的にサポートするようになってきました。それらのアップデート内容は総じて言うと上述の考え方を一言で言う「データ指向」という考え方のもと実施されているようです。

データ指向プログラミングの原則

以下、データ指向のことを DOP と呼びます。(Data-Oriented Programming)

1. 「データ」は不変

DOPにおいて「データ」は不変です。例えばインスタンスにおいて一度値がセットされたプロパティには変更を許しません。意図しないデータの書き換えでバグやデータの不整合の可能性を減らすためです。

これを実現するには、Javaの record が役立ちます。

サンプル:データ指向を意識しない場合

Javaをはじめ、オブジェクト指向のプログラミング言語ではクラスにgetterとsetterを書くことが一般的です。

プロパティを読み取り専用にしたいときはプロパティをfinalにしてsetterを書かないという選択もありますが、もちろんfinalを書かなければその効果は消えます。setterを書かなかったとしても、プロパティに再代入するメソッドは他にいくらでも作成可能です。

public class Person {
    private String name;
    private int age;
    // (略)コンストラクタ
    // (略)getterとsetter
    // setter以外にもプロパティを書き換えるメソッドは書ける
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        System.out.println(person.getName());
    }
}

サンプル:データ指向の場合

recordはデータ指向を支えるキーワードです。recordでクラスを作成することでプロパティがfinalであることが保証され、setterを書く余地すら排除されます。

つまりrecordを使う限りそのプロパティへの再代入は文法上不可能になり、データ指向の考えに沿ったコードになります。

public record Person(String name, int age) { }

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        System.out.println(person.name());
    }
}

2. ロジックはデータから独立させる

データとそれを操作するロジックを分離することもDOPのベースにある考え方のひとつです。データは単なる「データ」であって、振る舞いを持たせません。データの整合性を保持しながら、システムの柔軟性と拡張性を高めることを目指しています。

サンプル:データ指向を意識しない場合

一般的なオブジェクト指向プログラミングでは、プロパティとそれに関連するメソッドがクラスに一緒に定義されます。データとロジックの結びつきが強固になるのは必然です。

メソッドからプロパティを書き換えることが可能になると、メソッドを呼び出す順番によって実行結果が変わる可能性が出てきます。その複雑さが現れるのはテストを書くときでしょう。

public class Person {
    private String name;
    private int age;
    // (略)コンストラクタ, getter, setter

    public String generateGreet() {
        return "こんにちは、" + this.name + "といいます。";
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        System.out.println(person.generateGreet());
        // こんにちは、Aliceといいます。
    }
}

痒い例:テストが複雑になる

クラスにデータとメソッドを一緒に書いた場合、メソッドがプロパティを書き換える(再代入など)可能性を考慮してテストを書かなければなりません。テストケースは増え、メソッドを実行する順番も動作に影響するでしょう。結果としてテストは複雑になります。

このクラスについてよく知らない人がこのクラスの generateGreetMessage() のテストコードを書いたとします。このとき、その人はテストで generateGreetMessage() を実行したことで nameage が書き換わる可能性を意識すべき余地は残ります。

public class PersonTest {
    
    @Test
    public void testIsSenior() {
        Person person = new Person("Alice", 70);
        assertEquals("こんにちは、Aliceといいます。", person.generateGreet());

                // ↑この実行で person.name や person.age
        // が書き換わる余地は文法上残されている。
        // personのメソッドの動作がgenerateGreetを実行したことで
        // 変わるかもしれないことを意識する必要が出てくる
    }
}

サンプル:データ指向の場合

DOPではビジネスロジックは独立した関数やサービスとして実装します。データは整合性を保ちやすくなり、処理の独立性も高くなります。

public record Person(String name, int age) { }

public class PersonService {
    // ビジネスロジックはPersonから独立した関数やサービスとして実装
    public static String generateGreet(Person person) {
        return "こんにちは、" + person.name() + "といいます。";
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Alice", 30);
        System.out.println(PersonService.generateGreet(person));
        // こんにちは、Aliceといいます。
    }
}

恩恵の例:テストしやすくなる

ビジネスロジックはサービスクラスや他の独立した処理に分離するのがDOPのベースにある考え方です。結果、テストの対象はPersonというよりその処理やサービスクラスになります。

Personはどれほど参照されようと中身の name や age は変化しません。単なるパラメータとして渡すだけで良くなり、期待される出力を確認することだけに意識を集中してテストケースを作成できます。

public class PersonServiceTest {
    
    @Test
    public void testIsSenior() {
        Person person = new Person("Alice", 70);
        assertEquals("こんにちは、Aliceといいます。", PersonService.generateGreet(person));

        // ↑この実行でpersonの中身が書き換わることを恐れなくて良い。
        // personに再代入が無い限り、
        // PersonService.generateGreet(person) はいつも同じ結果を返す
    }
}

要は

  • オブジェクト指向プログラミングの代わりではない。共存して使用する新しいプログラミングのスタイル。
  • データは不変にし、振る舞いはサービスクラスや他の独立した処理に分離。
    • 何度実行しても同じ結果を期待できる仕組みにすることで整合性、保守性の向上を目指す。

Javaがデータ指向を推す背景

処理がインスタンスの外に独立するということはビジネスロジックのクラスがプロジェクト内に増え、結果コードの管理負担が増大することは容易に想像できます。

しかしJavaOneの登壇者が述べる通り、昨今「大規模開発」と呼ばれるプロジェクトは小さなサービスの集合体の形を取っているケースが少なくありません。

Data-Oriented Programming in Java – YouTube

Javaのオブジェクト指向を支える「型」の概念も、大規模開発のためというより「データ指向」という考えを支えるツールに今や代わりつつあるという考え方のようです。

カテゴリー: Java

bookmark_border[Windows] javaコマンドが使えないときはこうする

コマンドプロンプトでjavaコマンドが使えないときのメモです。この記事の情報はすべて自己責任でご覧ください。

手順

1. Javaがインストールできているか確認する

スタートメニューで検索窓に「Java」と入れ、「Javaについて」や「Javaの構成」という選択肢が出てきたらインストールはできています。この記事の 3 に進んでください。

新しいJavaのバージョンをこの機会にインストールしたい場合は 2 に進むこともできます。

2. Javaをダウンロード, インストールする

Javaは複数の会社や組織が指針となる規格にそって開発、公開しているアプリケーションです。それで一口にJavaをダウンロードすると言ってもユーザーにはいくつか選択肢があります。

Javaを開発している代表的な会社はOracleです。もし他の会社や組織が公開するJavaが良いという場合はそれを使うこともできます。

OracleのURLはこちらです。どのJavaを使うにしてもライセンスは確認しましょう。

https://www.java.com/ja/download/ie_manual.jsp

Javaとはなにか

広義にはアプリケーションの名前です。世界中のコンピュータで極めて広く使われており、様々なアプリケーションを動かす基盤になる存在です。狭義にはプログラミング言語のことを指すこともあります。

3. javaコマンドが使えるか確認する

この時点でjavaコマンドが使えるようになっている可能性があります。

  1. コマンドプロンプトを起動する (スタートメニューで cmd と入力すると候補に出てくる)
  2. コマンドプロンプトで java と打ってEnter

このとき、次のどちらの結果になるか確認します。

  • たくさんの行がぶわっと出力される → 成功
  • 'java' は、内部コマンドまたは外部コマンド、操作可能なプログラムまたはバッチ ファイルとして認識されていません。 → コマンドプロンプトを終了し4へ

4. パスを通す

java というコマンドを実行したとき、どのプログラムを呼び出したら良いのかPCに教えてあげる操作をします。一度行えば基本的にずっと有効です。

スタートメニューで「環境変数」と入力し、そのままEnterします。

「環境変数」をクリックします。

上の白い枠の中にある「Path」を選択し、「編集」ボタンを押します。

※使っているPCに複数ユーザーアカウントがあり、他のユーザーもjavaコマンドを使いたいなら下の白い枠の中にある 「Path」を選択し、「編集」ボタンを押します。

「新規」を押します。

Javaのインストーラを実行すると大抵 C:\Program Files (x86) にフォルダが作られ、java.exeというアプリケーションがその中のどこかにインストールされます。

「新規」ボタンを押してこの画面の最終行に文字を入力できるようになったら 先程のインストールの工程でできたjava.exeが格納されているディレクトリのパスを指定します。

OracleのJavaをインストールした場合は C:\Program Files (x86)\Common Files\Oracle\Java\javapath に入っているかと思います。念の為そのフォルダをエクスプローラーで開き、java.exe というファイルがあるか確認しておきましょう。

入力できたら「OK」で終了します。ここまでで開いた画面はすべて閉じてOKです。

5. javaコマンドが使えるかもう一度確認する

コマンドプロンプトを開いているなら一度閉じてから再度起動します。

java と打って、文字がたくさんでてきたらうまくいっています。もし表示されない場合はパスの指定が間違っているかもしれません。

カテゴリー: Java

bookmark_borderMinecraft Forgeのインストーラが開けないときはこうする

Forgeのインストーラである.jarファイルがダブルクリックで開けないときの対処です。

※バージョン1.20でも同様の手順です。(2023/12/20時点)

コマンドプロンプトで開く

.jarファイルはJavaという言語で書かれたプログラムを1つにまとめたものです。コマンドプロンプトからjavaコマンドで実行できます。

この記事ではJavaがPCにインストールされていること前提で進めます。

1. コマンドプロンプトを起動する

Windowsキー + Rcmd と打って OK を押します。

2. javaコマンドでforgeの.jarファイルを指定し、実行する

java -jar まで打ったら、コマンドプロンプトにforgeのインストーラをドラッグ&ドロップします。(javaの後ろと-jarの後ろには半角スペースを1つずつ入れます)

つまり、 java -jar Forgeのインストーラのパス のように入力できていればOKです。

Enterで実行します。

ここでエラーになる場合

Javaがインストールできていないか、それをコマンドプロンプトから使う設定ができていません。こちらの記事を参考にできます。

3. インストール

インストーラが立ち上がるので、画面を操作してインストールします。インストールが終わるまでコマンドプロンプトは閉じないようにします。

インストール中いろんな文字がコマンドプロンプト上に出るかと思いますが気にせず終わるのを待ちます。

終わってforgeのインストーラのOKボタンを押して再びコマンドが打てる状態に戻ったらコマンドプロンプトも閉じてOKです。


[広告] Minecraft Java版でForgeも動かせるマルチプレイのサーバーを用意するには、Xserverがおすすめです。

2023/12/21現在、1.20.2 [Forge]にも対応しており、Modを使ったマルチプレイを構築したい方に適したプランがあります。

処理性能No.1!快適なゲーム環境なら「Xserver for Game」

下記のバージョンのForgeから選ぶことができ、最新バージョンのForgeに対応していないModを使いたい方にも向いています。

Xserver for Gameで提供しているMinecraft Forgeイメージのバージョン

  • 1.20.2
  • 1.20.1
  • 1.20
  • 1.19.4
  • 1.19.3
  • 1.19.2
  • 1.19.1
  • 1.19
  • 1.18.2
  • 1.18.1
  • 1.18
  • 1.17.1
  • 1.16.5
  • 1.16.4
  • 1.16.3
  • 1.16.2
  • 1.16.1
  • 1.12.2

処理性能No.1!快適なゲーム環境なら「Xserver for Game」




カテゴリー: Java

bookmark_border[解説] Scannerのclose()は必要?

Javaの入門書などで学習中、 java.util.Scanner を使うと次のように表示されることがあります。

リソース・リーク: 'scanner' が閉じられることはありません

これは何

Javaのコンパイラが出す警告で、エラーではありません(無視しても動くといえば動く)。

Scannerは使い終わった後 closeメソッドの呼び出しを期待されています。でも呼び出していないので警告を発しているという状況です。以下のようにcloseメソッドを呼び出すコードを書くと警告は消えます。

Scanner scanner = new Scanner(System.in);
// いろんな処理...
scanner.close();

scanner.close(); は必要?

様々な意見があるかと思いますが、以下の理由でJavaの練習で書くnew Scanner(System.in); に関してはclose()の必要はありません

理由1: close()するとSystem.in ごと閉じてしまう

一度closeしてから再度new Scanner(System.in); してSystem.inを使おうとするとエラーになります。

エラーになるコード

Scanner scanner = new Scanner(System.in);
scanner.close();
// もう一度Scanner使いたい
scanner = new Scanner(System.in);
scanner.nextLine(); // ここで落ちる

実行結果

Exception in thread "main" java.util.NoSuchElementException: No line found
	at java.util.Scanner.nextLine(Scanner.java:1540)
	at Hogehoge.main(Hogehoge.java:15)

System.inは標準入力と言い、Javaのプログラムが入力を受け取るためのデータの通り道のようなものです。eclipseで言うところの「コンソール」とJavaの橋渡し的存在です。

scanner.close(); すると、その橋(System.in)自体の使用をやめる処理が走ります。そしてそのSystem.inは、一度closeするとそのプログラムの実行中はもう一度開くことができません

It is not possible to reopen System.in, System.out or System.err. … it is not possible to reopen them.

Stack Overflow – In Java is it possible to re-open System.in after closing it

プログラムの最後に「作法」としてclose();を書くのは良いことですが(後述)、Javaの練習で書くnew Scanner(System.in); に関してはプログラムの途中でcloseするメリットは無いように思います。

理由2: Javaのプログラム自体の実行終了はclose()を呼んだのと大体同じ

Javaのプログラム自体が終了すると、橋渡しするそのSystem.in含め全てがメモリから破棄されます。練習で書くプログラムは長時間動き続けるものではなく、System.inも自分でcloseしなくても他のプログラムに悪影響はありません。


じゃあclose()書かなくていい?

いいえ、個人的な意見ですが一言で言えば、「new Scanner(System.in); に関しては事実上必要ないが、今後必須の考え方なので書いて練習の機会に」です。

Javaの練習で書くnew Scanner(System.in); に関しては 書かなくても動くといえば動きますが、closeはScannerだけでなくプログラミング全般に通じる考え方, 作法の一つです。そのマインド作り, 練習としてぜひ書いていただきたいと考えます。

Scannerはcloseするべきか について調べると「書くようにしましょう」という文献を見かけますが、この重要な概念を覚えてもらいたいという願いからと思います。


close()の意義

closeは広義には資源を開放して他のプログラムでも使えるようにすることです。

借りたものは返すという作法

「(図書館の本のような)公共のものを借りたら早めに返して他の人も使えるようにする」一般常識はプログラミングの世界でもあり、「借りる」がopen (new Scanner(…);など)、「返す」が close に相当します。

例えばファイルの書き込みで言えば以下のような処理が一般に行われています。

  1. 「自分がこのファイルに書き込むので、終わるまで他のプログラムが書き込めないようロックする」(open)
  2. 「書き込み終わったから他のプログラムも書き込めるようにファイルを開放する」(close)

closeしないとどうなるのか

自分がファイルを使う権利を握り続けるので、ほかのプログラムがいつまで経ってもそのファイルを操作できません。

ファイルに限らず例えばデータベースに接続するプログラムがあるとして、closeしないで放置すると他のプログラムがデータベースに接続できなくなったりシステムの性能が落ちたりします。

Scannerの場合は?

Scannerはキーボード入力を受け付けるSystem.inだけでなく、テキストファイルなど他の入力も扱うことができます。その場合、scannerを使い終わった時点でcloseをすぐ実行するのは作法として適切と言えます。

Scanner scanner = null;
// 必要なtry-catch文は省略しています。
scanner = new Scanner(new File("hoge.txt"));
System.out.println(scanner.nextLine());
scanner.close(); // 使い終わったのでclose
// いろんな処理
// System.inと違い、一度closeしても再度openできる。
// ファイルに読み込みのロックが掛かっていなければエラーにはならない
scanner = new Scanner(new File("hoge.txt"));
System.out.println(scanner.nextLine());
scanner.close(); // 使い終わったのでclose
/*
hogehoge
hogehoge
と出力されます。(hoge.txtには hogehoge と書いてある)
*/

bookmark_border【eclipse】謎の不具合に遭遇したときに試すclean【Java】

次のような謎の不具合に遭遇したときまず試す操作のメモです。

  • コードの文法は合っているのに文法エラーの指摘がある
  • パッケージ・エクスプローラーなどで表示されるディレクトリ構造が変
  • サーブレットでサーバーが起動できない

まず試す:プロジェクトのクリーン

[プロジェクト]→[クリーン]→[すべてのプロジェクトをクリーン]→[Enter]

これは何

生成済のclassファイル(プロジェクトのビルド時に出力されるファイル)などを全部消します。

eclipseはビルド時間の短縮を目的に、コードに変更があったとき変更箇所だけをビルドし直しています。この操作でその生成済みの成果物を一度全て消すことで、eclipseはサーブレットやJavaプロジェクト全体を次回 一からビルドし直してくれます

奥の手:eclipseのclean起動

Windows

  1. eclipseを開いているなら終了する
  2. eclipseが入っているフォルダを開く
  3. eclipse.exe -clean.cmd をダブルクリック (eclipse.exeではなく)

Mac

  1. eclipseを開いているなら終了する。(command + Q などで完全に)
  2. 「ターミナル」を起動する
  3. 次のコマンドを実行する
    • open eclipse.appのフルパス --args -clean
コマンドの例:
open /Applications/Eclipse_2019-09.app --args -clean

eclipse.appのフルパスを入力する楽な方法:

  1. eclipse.appが入っているフォルダをFinderで開く
  2. eclipse.appをFinderからターミナルにドラッグ&ドロップ

これは何

eclipseが保持しているキャッシュを削除+再作成してeclipseを起動します。

キャッシュにはプロジェクト内のファイルやディレクトリ構造の情報が含まれており、何かの拍子にそれが壊れると実際の構造とeclipse上での表示がずれたり、他の問題を引き起こしたりします。

パッケージ・エクスプローラーでF5キーを押してファイルシステム側の構造を再読み込みする方法もありますが、キャッシュを一旦作り直してもらったほうが起こりうる他の何らかの問題も回避できると考えて何かがおかしいときはこれを実行しています。



bookmark_border「サーバーで実行」時にサーバー再起動ダイアログを再表示させる【eclipse】

eclipseでサーブレットを「サーバーで実行」するとき、サーバーを再起動するか尋ねるダイアログが出て欲しいのに出てきてくれないときの対処をメモ。

[Windows: ウインドウ/ Mac: Eclipse] → [設定] → [サーバー] → [起動] → [必要な場合はサーバーの再起動]

ダイアログが出てきてくれないときはここが「なし」になっている可能性。

bookmark_border【Mac】サーバーで必要なポート8080はすでに使用中 の一発解決コマンド

下のコマンドを実行する

8080ポートを使用しているPIDをそのままkillに渡しているだけです。

lsof -t -i:8080 | xargs kill -9 

これは何

lsof -i:8080 → 8080を使用しているプロセスが誰か調べる
-t → そのPIDだけもらう

| xargs kill -9 パイプで受け取ったPIDをkill (-9:強制終了)

bookmark_border【Tomcat, Windows】「サーバーで必要なポート8080はすでに使用中です」を一発解決するスクリプト【eclipse】

Macだとアクティビティモニタで簡単にプロセスをkillできますが(自分はそうしてた)、Windowsはnetstatしたりサービス一覧を調べたりする方法が出てきて、なんか大変そうだったので。

このスクリプトを実行する

PowerShellでこのスクリプトを実行します。

$processes = Get-Process -Id (Get-NetTCPConnection -LocalPort 8080,8005,8009).OwningProcess
foreach($process in $processes) {
  Stop-Process $process.Id
}

実行方法

  1. Windows PowerShellを立ち上げる
    スタートメニュー(Cortana)の検索で powershell とか打つと候補に出てくる
  2. 上記のスクリプトをコピペしてEnterで実行

エラーっぽい赤い文字が出るかもしれませんが、8005やか8009番ポートを使用しているアプリケーションが存在していないだけで特に問題ありません。

これは何

8080, 8005, 8009番ポートを使用しているプロセスを一括で終了しています。

「ローカルホストのtomcat vXX サーバーで必要なポート8080はすでに使用中です」 is 何

8080番ポートを使うサーバーを起動したいけど、8080番ポートを使用している先客(プロセス)がすでに実行中なので、起動できない。
(すでに実行中のそれは、大抵はeclipseで自分が起動したプロセスの残骸だったりする)

ポート番号の変更は根本解決ではない

eclipseでtomcatのポート番号を変更する方法もありますが、このエラーは前述の通りプロセスの残骸が原因なので、そのプロセスを終了させるほうが正攻法と思われます。(最終手段のPC再起動も事実上はその残骸プロセスを終了させる方法)

bookmark_borderTomcat+Log4j2でコンソール出力 & 任意のファイルに書き込み

Log4j2 + サーブレット(Tomcat)でとりあえず任意のファイルとコンソールへログ出力する上で最低限すべきことをメモ。ログレベルの制御といったことまでは書いていません。

1. Log4j2のダウンロード

Apache ソフトウェア財団から最新版をダウンロード

2. Log4j2のjarをビルドパスに追加

ダウンロードしたzipにいっぱいjarが入っています。そのうち以下のファイルをWEB-INF/libにコピー。
大抵はコピーした段階でeclipseが自動でビルドパスに追加してくれるはずです。

  • log4j-XXX-api-XXX.jar
  • log4j-api-XXX.jar
  • log4j-core-XXX.jar
  • log4j-web-XXX.jar

※XXXの部分にはバージョン番号が入ります。

3. log4j2.xmlの編集

WEB-INF直下に「log4j2.xml」を以下の内容で記述しつつ、ハイライトされている部分を適宜書き換えます。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="OFF">
  <Appenders>
        <RollingFile name="file"
                fileName="C:\XXX\YYY\hogeapp.log" <!-- 書き込んでほしいログファイルの場所 -->
                filePattern="C:\XXX\YYY\hogeapp-%d{yyyy-MM-dd}-%i.log">
            <PatternLayout pattern="[%date] %-5p %location %m%n"/>
            <Policies>
                <OnStartupTriggeringPolicy />
                <SizeBasedTriggeringPolicy size="256 KB" />
            </Policies>
            <DefaultRolloverStrategy max="100"/>
        </RollingFile>
    </Appenders>
    <Loggers>
        <Logger name="自分のアプリケーションのパッケージ名" level="trace">
            <AppenderRef ref="file" />
        </Logger>
    </Loggers>
</Configuration>

4. ログ出力のコードを書く

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@WebServlet("/Main")
public class Main extends HttpServlet {
    private static Logger logger = LogManager.getLogger();
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        logger.warn("まずい");
        logger.error("かなりまずい");
    }
}