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

StringUtilsのisEmptyとisBlankの違い

StringUtilsでよく使う関数でisEmptyとisBlankがあるんだけど、違いをたまに忘れてしまうのでメモ。
isEmptyは空文字だけをtrueで返すようだ。

qiita.java
package com.example.test;

import java.util.ArrayList;
import java.util.List;
import org.apache.commons.collections4.*;

import org.apache.commons.lang3.StringUtils;
import org.junit.Test;

public class BlankTest {

    @Test
    public void test() {
        System.out.println("空文字= " + StringUtils.isBlank(""));
        System.out.println("空白文字= " + StringUtils.isBlank(" "));
        System.out.println("全角空白文字= " + StringUtils.isBlank(" "));
        System.out.println("タブ空白文字= " + StringUtils.isBlank("\t"));
        System.out.println("空文字= " + StringUtils.isEmpty(""));
        System.out.println("空白文字=" + StringUtils.isEmpty(" "));
        System.out.println("全角空白文字=" + StringUtils.isEmpty(" "));
        System.out.println("タブ空白文字=" + StringUtils.isEmpty("\t"));              
    }

}

実行結果
空文字= true
空白文字= true
全角空白文字= true
タブ空白文字= true
空文字= true
空白文字=false
全角空白文字=false
タブ空白文字=false

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javax.batch.operations.JobStartExceptionとなったときの対応方法

  • 環境
    • Windows 10 Pro 64bit バージョン1909
    • Payara Server 5.194
    • Eclipse Version 2020-03
    • openjdk version 11

事象 : バッチ処理が起動せずにエラーになる

デバックしても何がnullになっていて問題なのかよくわかんない。

ログ
javax.batch.operations.JobStartException: java.lang.NullPointerException  
    at com.ibm.jbatch.container.api.impl.JobOperatorImpl.start(JobOperatorImpl.java:92)  
...省略... 
Caused by: java.lang.NullPointerException: null  
    at com.ibm.jbatch.container.jobinstance.JobExecutionHelper.startJob(JobExecutionHelper.java:125)  
    at com.ibm.jbatch.container.impl.BatchKernelImpl.startJob(BatchKernelImpl.java:124)  
    at com.ibm.jbatch.container.api.impl.JobOperatorImpl.startInternal(JobOperatorImpl.java:123)  
    at com.ibm.jbatch.container.api.impl.JobOperatorImpl.start(JobOperatorImpl.java:88)  
    ... 161 common frames omitted 

原因 : Payaraがちゃんとクリーンできてないから

どうもJobOperatorImplはPayara関連くさいということで対応したらなおった。
GlassFishでよくわからないエラーになるとおんなじような対応をしていたが、Payaraになっても・・・。

対応 : Payara配下のディレクトリを削除する

# この3つのディレクトリを削除する。eclipseApps~ディレクトリはEclipseでないとない・・・と思う。
$ ls -la /c/apps/payara5/glassfish/domains/domain1/ | grep -e eclipse -e generated 
drwxr-xr-x 1 ponsuke 1049089   0  9月 18 17:41 eclipseApps/ 
drwxr-xr-x 1 ponsuke 1049089   0  9月 18 17:24 eclipseAppsTmp/ 
drwxr-xr-x 1 ponsuke 1049089   0  9月 18 17:26 generated/
  1. 上記のディレクトリを削除する
  2. EclipseでPayaraにデプロイしているアプリを削除
  3. Payaraをクリーンする
  4. プロジェクトをクリーンする
  5. アプリをPayaraにデプロイして起動する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Mediatorパターン

Mediatorパターンとは

「相談役」を用意するパターン。
仕事を行うメンバーは、全体に波及する出来事がおきた時相談役に知らせる。
また、相談役からの指示には素直に応じる。
相談役は、メンバーからの知らせを元に大局的な判断を行い、
各メンバーに指示をだす。

Mediator(調停者、仲介者)の役

Colleague役と通信を行って、調整を行うためのインターフェースを定める。

package mediator;

public interface Mediator {
    // このMediatorが管理するメンバーを生成する
    public abstract void createColleagues();
    // 相談役への相談
    public abstract void collegueChanged();
}

ConcreteMediator(具体的な調停者、仲介者)の役

Mediator役のインターフェースを実装し、実際の調整を行う。

package mediator;

import java.awt.CheckboxGroup;
import java.awt.Color;
import java.awt.Frame;
import java.awt.GridLayout;
import java.awt.Label;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class LoginFrame extends Frame implements ActionListener, Mediator {
    private ColleagueCheckBox checkGuest;
    private ColleagueCheckBox checkLogin;
    private ColleagueTextField textUser;
    private ColleagueTextField textPass;
    private ColleagueButton buttonOk;
    private ColleagueButton buttonCancel;

     public LoginFrame(String title) {
         super(title);
         setBackground(Color.lightGray);
         // レイアウトマネージャーを使って4×2のグリッドを作る
         setLayout(new GridLayout(4, 2));
         // Colleagueたちの生成
         createColleagues();
         // 配置
         add(checkGuest);
         add(checkLogin);
         add(new Label("User name:"));
         add(textUser);
         add(new Label("Password:"));
         add(textPass);
         add(buttonOk);
         add(buttonCancel);
         // 有効/無効の初期設定
         collegueChanged();
         // 表示
         pack();
         show();
    }

    @Override
    public void createColleagues() {
        // 生成
        CheckboxGroup g = new CheckboxGroup();
        checkGuest = new ColleagueCheckBox("Guest", g, true);
        checkLogin = new ColleagueCheckBox("Login", g, false);
        textUser = new ColleagueTextField("", 10);
        textPass = new ColleagueTextField("", 10);
        textPass.setEchoChar('*');
        buttonOk = new ColleagueButton("OK");
        buttonCancel = new ColleagueButton("Cancel");
        // Mediator(相談役)のセット
        checkGuest.setMediator(this);
        checkLogin.setMediator(this);
        textUser.setMediator(this);
        textPass.setMediator(this);
        buttonOk.setMediator(this);
        buttonCancel.setMediator(this);
        // Listenerのセット
        checkGuest.addItemListener(checkGuest);
        checkLogin.addItemListener(checkLogin);
        textUser.addTextListener(textUser);
        textPass.addTextListener(textPass);
        buttonOk.addActionListener(this);
        buttonCancel.addActionListener(this);
    }

    // Colleagueからの通知で有効/無効を判定する
    @Override
    public void collegueChanged() {
        if (checkGuest.getState()) {
            // Guestモード
            textPass.setColleagueEnabled(true);
            if (textPass.getText().length() > 0) {
                buttonOk.setEnabled(true);
            } else {
                buttonOk.setEnabled(false);
            }
        } else {
            // Loginモード
            textPass.setColleagueEnabled(false);
            buttonOk.setColleagueEnabled(false);
        }
    }

    // textUserまたはtextPassの変更があった
    // 各Colleagueの有効/無効を判定する
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println(e);
        System.exit(0);
    }
}

Colleague(同僚)の役

Mediator役と通信を行うインターフェースを定める。

package mediator;

public interface Colleague {
    public abstract void setMediator(Mediator mediator);
    // 相談役からやってくる指示に該当
    public abstract void setColleagueEnabled(boolean enabled);
}

ConcretrColleague(具体的同僚)の役

Colleague役のインターフェースを実装する。

package mediator;

import java.awt.Button;
/**
 * LoginFrameクラス(Mediatorインターフェース)と協調動作を行う
 *
 */
public class ColleagueButton extends Button implements Colleague{
    private Mediator mediator;

    public ColleagueButton(String caption) {
        super(caption);
    }

    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    // Mediatorから有効/無効が指示される
    @Override
    public void setColleagueEnabled(boolean enabled) {
        setEnabled(enabled);
    }
}
package mediator;

import java.awt.Color;
import java.awt.TextField;
import java.awt.event.TextEvent;
import java.awt.event.TextListener;

public class ColleagueTextField extends TextField implements TextListener, Colleague {
    private Mediator mediator;

    public ColleagueTextField(String text, int columns) {
        super(text, columns);
    }

    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    @Override
    public void setColleagueEnabled(boolean enabled) {
        setEnabled(enabled);
        setBackground(enabled ? Color.white : Color.lightGray);
    }

    // 文字列が変化した場合Mediatorに通知
    public void textValueChanged(TextEvent e) {
        mediator.collegueChanged();
    }

}
package mediator;

import java.awt.Checkbox;
import java.awt.CheckboxGroup;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;

public class ColleagueCheckBox extends Checkbox implements ItemListener, Colleague {
    private Mediator mediator;

     public ColleagueCheckBox(String caption, CheckboxGroup group, boolean state) {
         super(caption, group, state);
    }

    @Override
    public void setMediator(Mediator mediator) {
        this.mediator = mediator;
    }

    @Override
    public void setColleagueEnabled(boolean enabled) {
        setEnabled(enabled);
    }

    @Override
    public void itemStateChanged(ItemEvent e) {
        mediator.collegueChanged();
    }
}

上記のプログラムでは、ボタンの活性、非活性の判断を相談役であるLoginFrameクラスが行っている。
各メンバー(ColleagueButton,TextField,CheckBox)は相談役に知らせているだけで、判断は相談役に任せている。

https://github.com/aki0207/mediator

こちらを参考にさせていただきました。
増補改訂版Java言語で学ぶデザインパターン入門

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

switchとEnumMap

switch

switchはbyte、short、int型の値やString型の値だけでなく列挙型も式に取れるので、サクッと条件分岐させたいときには便利ですよね。

ただ、その主張が激しくなってくると・・・。

DayOfWeek列挙体
enum DayOfWeek {
    SUNDAY    ("日曜日"),
    MONDAY    ("月曜日"),
    TUESDAY   ("火曜日"),
    WEDNESDAY ("水曜日"),
    THURSDAY  ("木曜日"),
    FRIDAY    ("金曜日"),
    SATURDAY  ("土曜日");

    private String name;

    private DayOfWeek(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
};
Personクラス
class Person {
    private DayOfWeek dayOfWeek;
    private String lyrics;

    private Person(DayOfWeek dayOfWeek) {
        setDayOfWeek(dayOfWeek);
    }

    // インスタンスを生成するstaticメソッド
    static Person todayIs(DayOfWeek dayOfWeek) {
        return new Person(dayOfWeek).doSometing();
    }

    // 曜日を設定するメソッド
    Person setDayOfWeek(DayOfWeek dayOfWeek) {
        Objects.requireNonNull(dayOfWeek, "帰れや");
        this.dayOfWeek = dayOfWeek;
        return this;
    }

    // 何かするメソッド
    Person doSometing() {
        lyrics = dayOfWeek.getName();
        switch (dayOfWeek) {
            case SUNDAY:
                lyrics += "に市場へでかけ";
                break;
            case MONDAY:
                lyrics += "におふろをたいて";
                break;
            case TUESDAY:
                lyrics += "はおふろにはいり";
                break;
            case WEDNESDAY:
                lyrics += "にともだちが来て";
                break;
            case THURSDAY:
                lyrics += "は送っていった";
                break;
            case FRIDAY:
                lyrics += "は糸まきもせず";
                break;
            case SATURDAY:
                lyrics += "はおしゃべりばかり";
                break;
        }
        return this;
    }

    // ギャンブルをするメソッド
    Person gambling(int amount) {
        lyrics = dayOfWeek.getName();
        switch (dayOfWeek) {
            case SUNDAY:
            case TUESDAY:
            case THURSDAY:
            case FRIDAY:
            case SATURDAY:
                lyrics += "に" + amount + "万負けて";
                break;
            case MONDAY:
            case WEDNESDAY:
                lyrics += "に" + amount + "万勝って";
                break;
        }
        return this;
    }

    // テュリャるメソッド
    Person turya() {
        switch (dayOfWeek) {
            case SUNDAY:
                lyrics += "テュリャ";
                break;
            case MONDAY:
                lyrics += "テュリャ リャ";
                break;
            case TUESDAY:
                lyrics += "テュリャ テュリャ テュリャ";
                break;
            case WEDNESDAY:
                lyrics += "テュリャ テュリャ テュリャリャ";
                break;
            case THURSDAY:
                lyrics += "、あ?";
                break;
            case FRIDAY:
                lyrics += "てゆりや";
                break;
            case SATURDAY:
                lyrics += "turya";
                break;
            default:
                break;
        }
        return this;
    }

    // 歌うメソッド
    Person sing() {
        System.out.println(lyrics);
        lyrics = "";
        return this;
    }
}

このように、くどさがでてきます。
breakの記述漏れにより予期せぬフォールスルーを生む危険性もでてきます。

Personクラスの使用例
Person.todayIs(MONDAY).sing()
    .setDayOfWeek(TUESDAY).doSometing().turya().sing()
    .setDayOfWeek(THURSDAY).gambling(100).turya().sing()
    .setDayOfWeek(SATURDAY).doSometing().turya().sing();
出力
月曜日におふろをたいて
火曜日はおふろにはいりテュリャ テュリャ テュリャ
木曜日に100万負けて、あ?
土曜日はおしゃべりばかりturya

EnumMap

列挙体と紐付けてデータを管理できるEnumMapというものがあります。
これを使って書き換えると、

Personクラス(修正)
class Person {
    ... // フィールドから曜日を設定するメソッドまで省略

    // 何かするメソッド
    Person doSometing() {
        lyrics = dayOfWeek.getName() + SOMETHING_MAP.get(dayOfWeek);
        return this;
    }

    // ギャンブルをするメソッド
    Person gambling(int amount) {
        lyrics = dayOfWeek.getName() + "に" + amount + "万" + GAMBLING_MAP.get(dayOfWeek);
        return this;
    }

    // テュリャるメソッド
    Person turya() {
        lyrics += TURYA_MAP.get(dayOfWeek);
        return this;
    }

    // 歌うメソッド
    Person sing() {
        System.out.println(lyrics);
        lyrics = "";
        return this;
    }

    // EnumMap

    /** 何か */
    private static final EnumMap<DayOfWeek, String> SOMETHING_MAP =
        new EnumMap<>(DayOfWeek.class) {{
            put(SUNDAY, "に市場へでかけ");
            put(MONDAY, "におふろをたいて");
            put(TUESDAY, "はおふろにはいり");
            put(WEDNESDAY, "にともだちが来て");
            put(THURSDAY, "は送っていった");
            put(FRIDAY, "は糸まきもせず");
            put(SATURDAY, "はおしゃべりばかり");
        }};

    /** ギャンブル */
    private static final EnumMap<DayOfWeek, String> GAMBLING_MAP =
        new EnumMap<>(DayOfWeek.class) {{
            final String WIN = "勝って";
            final String LOSE = "負けて";
            put(SUNDAY, LOSE);
            put(MONDAY, WIN);
            put(TUESDAY, LOSE);
            put(WEDNESDAY, WIN);
            put(THURSDAY, LOSE);
            put(FRIDAY, LOSE);
            put(SATURDAY, LOSE);
        }};

    /** テュリャ */
    private static final EnumMap<DayOfWeek, String> TURYA_MAP =
        new EnumMap<>(DayOfWeek.class) {{
            put(SUNDAY, "テュリャ");
            put(MONDAY, "テュリャ リャ");
            put(TUESDAY, "テュリャ テュリャ テュリャ");
            put(WEDNESDAY, "テュリャ テュリャ テュリャリャ");
            put(THURSDAY, "、あ?");
            put(FRIDAY, "てゆりや");
            put(SATURDAY, "turya");
        }};
}

とりあえず管理はしやすくなった・・・はず。

いまいちコレ!という使いどころに自信がありませんが、列挙体と直接紐付けるデータならば列挙体内で直接定義し、そうでなければ個々のクラスでEnumMapを活用するというような使い方なのでしょうかね1


  1. 今回は後者を想定して(DayOfWeek列挙体を他クラスでも使い回すと仮定して)、列挙体には曜日の名前以外のデータを紐付けませんでした。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaに初めて触れてみた

Javaに触れてみた

自己満の備忘録になりますので、ご容赦ください。

文字列を表示する

class Hello {
  public static void main(String[] args) {
    System.out.println("hello IKEMEN");
  }
}

ターミナル
hello IKEMEN

" "で囲んだ文字列が表示されます。

変数を使って文字列を表示する

class Hello {
  public static void main(String[] args) {
    String name = "IKEMEN"

    System.out.println("hello" + name);
  }
}

ターミナル
helloIKEMEN

このままでは余白がないので訂正します。

class Hello {
  public static void main(String[] args) {
    String name = "IKEMEN"

    System.out.println("hello " + name);
  }
}

System.out.println("hello " + name);のhelloの後に余白を入れます。

ターミナル
hello IKEMEN

余白ができて綺麗になりました。

ユーザーからの入力を受け取る

①文字列

import java.util.Scanner;

class Hello {
  public static void main(String[] args) {
    System.out.print("Your name? ");
    String name = new Scanner(System.in).next();

    System.out.println("hello " + name);
  }
}

new Scanner(System.in).next();でキーボードからの入力を受け取れるみたいです。

ターミナル
Your name?

と表示されるので、文字列(IKEMEN)を打ち込みます

ターミナル
Your name? IKEMEN
hello IKEMEN

②数値

import java.util.Scanner;

class Hello {
  public static void main(String[] args) {
    System.out.print("Your guess? ");
    Integer guess = new Scanner(System.in).nextInt();

    System.out.println("Your guess: " + guess);
  }
}

new Scanner(System.in).nextInt();で入力値をintegerとして受け取るみたいです。

ターミナル
Your guess?

と表示されるので、数値(6)を打ち込みます

ターミナル
Your guess? 6
Your guess: 6

条件に応じて表示を変える

import java.util.Scanner;

class Hello {
  public static void main(String[] args) {
    Integer answer = 6;

    System.out.print("Your guess? ");
    Integer guess = new Scanner(System.in).nextInt();

    if (answer == guess) {
      System.out.println("正解!");
    } else {
      System.out.print("ハズレ");
    }
  }
}

入力した数値が6の場合は正解!と表示され、6以外の場合はハズレと表示されます。

条件を追加する

import java.util.Scanner;

class Hello {
  public static void main(String[] args) {
    Integer answer = 6;

    System.out.print("Your guess? ");
    Integer guess = new Scanner(System.in).nextInt();

    if (answer == guess) {
      System.out.println("正解!");
    } else if (answer > guess) {
      System.out.println("もっと大きい");
    } else {
      System.out.println("もっと小さい");
    }
  }
}

入力した数値が6の場合は正解!と表示され、6より小さい場合はもっと大きいと表示され、6より大きい場合はもっと小さいと表示されます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Facadeパターン

Facadeパターンとは

処理を行うための「窓口」を用意する。
プログラムは大きくなるにつれ、たくさんのクラスが作られて行く。
それらのクラスを使用するためには、クラス間の関係を理解し、
正しい順番でメソッドを呼び出す必要がある。
Facadeパターンでは、「窓口」を用意し、利用者はその「窓口」に対し要求を出すだけで良い。

Facade(正面)の役

システムを構成しているその他大勢の役のシンプルな窓口となる。
高レベルでシンプルなインターフェースを外部に提供する。

package facade;

import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;

public class PageMaker {
    private PageMaker() {
    }

    public static void makeWelcomePage(String mailAddress, String fileName) {
        try {
            Properties mailProp = Database.getProperties("mailData");
            String userName = mailProp.getProperty(mailAddress);
            HtmlWriter writer = new HtmlWriter(new FileWriter(fileName));
            writer.title("Welcome to " + userName + "'s page!");
            writer.paragraph(userName + "のページへようこそ。");
            writer.paragraph("メール待っていますね。");
            writer.mailTo(mailAddress, userName);
            writer.close();
            System.out.println(fileName + " is created for " + mailAddress + " (" + userName + ")");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

システムを構成しているその他大勢の役

それぞれの仕事を行うが、Facade役のことは意識しない。
Facade役から呼び出されて仕事を行うが、その他大勢の役からFacade役を呼び出すことはない。

package facade;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class Database {
    private Database() {
    }

    public static Properties getProperties(String dbName) {
        String fileName = dbName + ".txt";
        Properties prop = new Properties();
        try {
            prop.load(new FileInputStream(fileName));
        } catch (IOException e) {
            System.out.println("Warning:" + fileName + " is not found");
        }
        return prop;
    }
}
package facade;

import java.io.IOException;
import java.io.Writer;

public class HtmlWriter {
    private Writer writer;

    public HtmlWriter(Writer writer) {
        this.writer = writer;
    }

    public void title(String title) throws IOException {
        writer.write("<html>");
        writer.write("<head>");
        writer.write("<title>" + title + "</title>");
        writer.write("</head>");
        writer.write("<body>\n");
        writer.write("<h1>" + title + "</h1>\n");
    }

    public void paragraph(String msg) throws IOException {
        writer.write("<p>" + msg + "</p>\n");
    }

    public void link(String href, String caption) throws IOException {
        paragraph("<a href=\"" + href + "\">" + caption + "</a>");
    }

    public void mailTo(String mailAddress, String userName) throws IOException {
        link("mailTo:" + mailAddress, userName);
    }

    public void close() throws IOException {
        writer.write("</body>");
        writer.write("</html>\n");
        writer.close();
    }
}

Client(依頼者)の役

Facadeパターンを利用する役

package facade;

public class Main {
    public static void main(String[] args) {
        PageMaker.makeWelcomePage("hyuki@hyuki.com", "welcome.html");
    }
}

実行して作成されたHTML

スクリーンショット 2020-09-18 14.59.53.png

https://github.com/aki0207/facade

こちらを参考にさせていただきました。
増補改訂版Java言語で学ぶデザインパターン入門

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コーディングの方法とリファクタリングの原理

Chanyiに、リファクタリング原理の問題点を踏まえて、コーディングに使えるメソッドやコーディングの現状についての考えを語っていただきました。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

方法1:手書きでコードを書く

ほとんどの初心者JAVAプログラマーは、開発ツール上で以下のコードを筋肉の記憶力だけでタイプアウトしてしまうことがあります。

public class Test {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

そう、これは誰もが最初にコードを入力したときに覚える古典的な「Hello world」文です。

当然のことながら、手動でコードを入力することも選択肢の一つです。そして実際には、コードを手入力することは、プログラマーとしての能力をテストするのに適しています。実際、多くの企業では、面接の過程でコンピュータプログラミングの試験の一部として手動コーディングを使用しています。このような試験では、面接官は試験の要件に基づいてプログラミングツール(Eclipseなど)を選択し、特定のコードを手書きで書いたり、デバッグしたり、実行したりする必要があります。コーディングプロセス全体の間、面接官はインターネットで答えを検索したり、オンラインのヘルプ文書を見たりすることはできません。一般的には、面接官が完全に自分でコードを書くことができることが要件とされています。このような試験では、構文、関数、論理、思考力、アルゴリズム、実地能力など、面接官のコーディング能力が試されます。

マニュアルコーディングは、優秀なプログラマーなら誰でも持っているべき、本当に持っていなければならない基本的なスキルです。マニュアルコーディングは、記事を書くために必要な基本的なライティングスキルのようなものです。構文とは、コードの中の文章を作るための方法です。関数とは、記事の中の単語や文章のことです。クラスライブラリとは、引用するための逸話、アーキテクチャとは、表現のジャンルです。機能は記事を書くための主な目的です。アルゴリズムは言語を整理するためのロジックです。したがって、プログラミング言語の構文をマスターし、基本的なクラスライブラリの機能の束を覚え、必要なサードパーティのクラスライブラリをいくつか引用し、成熟した安定したアーキテクチャを選択し、製品要求の機能を明確にし、ロジックを実現するアルゴリズムを選択する必要があります。そして、マニュアルコーディングは記事を書くのと同じくらい簡単です。

方法2:コードをコピーして貼り付ける

中国の諺で "唐の詩300首をよく勉強すれば、書けなくても詠めるようになる。"とあるように、 コーディングも同じことが言えます。コーディングの第一歩は真似をすることです。つまり、コードをコピー&ペーストすることです。コードのコピー&ペーストは芸術です。この方法を正しく使えば、半分の労力でコーディングを完成させることができます。必要なコードが出てきたら、コピー&ペーストする前に慎重にチェックしましょう。あるシナリオに適したコードが、必ずしも別のシナリオに適しているとは限りません。有資格者であるプログラマーは、コードを確認せずに単純にコードを取って使うことはしません。

1. なぜコードをコピーして貼り付けるのか

1、既存のコードをコピー&ペーストすることで、開発時間を短縮することができます。
2、安定したコードをコピー&ペーストすることで、システム障害のリスクを減らすことができます。
3、ネットワークコードをコピー&ペーストすることで、他の人の実績を自分の実績に変換することができます。

2. コードのコピー&ペーストで発生する問題点

1、コピーしたコードをどの程度理解していますか?実装ロジックは妥当ですか?コードは安定して動作しますか?潜在的なバグはどれくらいありますか?
2、プロジェクト内で何回コードをコピー&ペーストしましたか?3回目に同じことをしたらリファクタリングの原則に基づいて、同じコードをリファクタリングする必要がありますか?
3、コードをコピー&ペーストする回数が増えれば増えるほど、コードメンテナンスの問題が出てきます。複数のバージョンでコードを更新した後もコードの同期を保つためには、それぞれの場所で同じ変更をしなければなりません。そのため、メンテナンスコストやリスクが増大してしまいます。

要するに、コードのコピー&ペーストは他のコーディング方法と同じで、どの方法が優れているということはありません。あくまでも、適切にも不適切にも使える方法です。コードをコピー&ペーストする場合は、その結果に責任を持たなければなりません。

方法3:テキスト置換によるコードの生成

1. 生成されたコードの例

以下は、ユーザークエリを実装するために書かれたコードの一部です。

/** Query user service function */
public PageData<UserVO> queryUser(QueryUserParameterVO parameter) {
    Long totalCount = userDAO.countByParameter(parameter);
    List<UserVO> userList = null;
    if (Objects.nonNull(totalCount) && totalCount.compareTo(0L) > 0) {
        userList = userDAO.queryByParameter(parameter);
    }
    return new PageData<>(totalCount, userList);
}

/** Query user controller function */
@RequestMapping(path = "/queryUser", method = RequestMethod.POST)
public Result<PageData<UserVO>> queryUser(@Valid @RequestBody QueryUserParameterVO parameter) {
    PageData<UserVO> pageData = userService.queryUser(parameter);
    return Result.success(pageData);
}

企業クエリを実装するコードを書く場合、コード形式はユーザークエリのコードと似ています。置換関係は以下のように整理できます。

1、"User "を "Company "に置き換えてください。
2、"user "を "company "に置き換えてください。
NotepadEditPlusなどのテキストエディタを使って、共通テキストを大文字小文字を区別して置換することができます。最終的な結果は以下のようになります。

/** Query company service function */
public PageData<CompanyVO> queryCompany(QueryCompanyParameterVO parameter) {
    Long totalCount = companyDAO.countByParameter(parameter);
    List<CompanyVO> companyList = null;
    if (Objects.nonNull(totalCount) && totalCount.compareTo(0L) > 0) {
        companyList = companyDAO.queryByParameter(parameter);
    }
    return new PageData<>(totalCount, companyList);
}

/** Query company controller function */
@RequestMapping(path = "/queryCompany", method = RequestMethod.POST)
public Result<PageData<CompanyVO>> queryCompany(@Valid @RequestBody QueryCompanyParameterVO parameter) {
    PageData<CompanyVO> pageData = companyService.queryCompany(parameter);
    return Result.success(pageData);
}

テキスト置換でコードを生成する場合は、コード生成時間が1分を超えないようにしてください。

2. メリットとデメリット

メリット:
1、コード生成速度が速い。
デメリット:
1、サンプルコードをコンパイルする必要があります。
2、この方法は、テキスト置換のシナリオにのみ適用されます。

方法 4: Excel の数式を使用してコードを生成する

エクセルの数式は非常に強力で、いくつかの定式化されたコードをコンパイルするために使用することができます。

1. Excel の数式を使用してモデルクラスを生成する

インターフェースモデルの定義をWikiからExcelにコピーします。サンプルデータは以下の通りです。

image.png

以下のようにエクセルの計算式を書きます。

= "/** "&D6&IF(ISBLANK(F6), "", "("&F6&")")&" */ "&IF(E6 = "否", IF(C6 = "String", "@NotBlank", "@NotNull"), "")&" private "&C6&" "&B6&";"

次のように数式を使ってコードを生成します。

/** User ID */ @NotNull private Long id;
/** Username */ @NotBlank private String name;
/** User gender (0:unknown;1:male;2:female) */ @NotNull private Integer sex;
/** User description */  private String description;

モデルクラスを作成し、以下のようにコードを整理します。

/** User DO Class */
public class UserDO {
    /** User ID */
    @NotNull
    private Long id;
    /** User Name */
    @NotBlank
    private String name;
    /** User gender (0:unknown;1:male;2:female) */
    @NotNull
    private Integer sex;
    /** User description */
    private String description;
    ......
}

2. Excelの式を使用して列挙クラスを生成する

Wikiから列挙定義をExcelにコピーします。サンプルデータは以下の通りです。

image.png

以下のようにエクセルの計算式を書きます。

="/** "&D2&"("&B2&") */"&C2&"("&B2&", """&D2&"""),"

次のように数式を使ってコードを生成します。

/** empty(0) */NONE(0, "Unknown"),
/** male(1) */MAN(1, "Male"),
/** female(2) */WOMAN(2, "Female"),

列挙クラスを作成し、以下のようにコードを整理します。

/** User gender enumeration class */
public enum UserSex {
    /** enumeration definition */
    /** empty(0) */
    NONE(0, "unknown"),
    /** male(1) */
    MAN(1, "male"),
    /** female(2) */
    WOMAN(2, "female");
    ......
}

3. Excelの数式を使ってデータベース・ステートメントを生成する

会社リストは、エクセルでは以下のようにソートされています。このリストに基づいて、レコードを直接データベースに挿入するためのSQL文を書く必要があります。

image.png

以下のようにエクセルの計算式を書きます。

= "('"&B2&"', '"&C2&"', '"&D2&"', '"&E2&"'),"

式を使用して、以下のようなSQL文を生成します。

('AutoNavi', 'First Tower', '(010)11111111', 'gaode@xxx.com'),
('Alibaba Cloud', 'Green village', '(010)22222222', 'aliyun@xxx.com'),
('Cainiao', 'Alibaba offices', '(010)33333333', 'cainiao@xxx.com'),

into文ヘッダを追加し、SQL文を以下のように並べ替えます。

insert into t_company(name, address, phone, email) values
('AutoNavi', 'First Tower', '(010)11111111', 'gaode@xxx.com'),
('Alibaba Cloud', 'Green village', '(010)22222222', 'aliyun@xxx.com'),
('Cainiao', 'Alibaba offices', '(010)33333333', 'cainiao@xxx.com');

4. メリットとデメリット

メリット:
1、この方法は、テーブルベースのデータのコード生成に適用できます。
2、式を書いた後、ドラッグ&ドロップでコードを生成することができます。そのため、コード生成速度が速いです。
デメリット:
1、この方法は複雑な関数を使ったコードの生成には適用できません。

方法5:ツールを使ってコードを生成する

つまり、既存のツールを使ってコードを生成します。多くの開発ツールはコードを生成するツールを提供しています。例えば、コンストラクタの生成、基底クラスやインターフェイス関数のリロード、ゲッター/セッター関数の生成、toString関数の生成などです。これらのツールを使用することで、手動でコーディングする手間を省くことができます。また、いくつかのコード生成プラグインを使用して、特定のアプリケーション・シナリオを満たすコードを生成することもできます。

以下では、MyBatis-Generatorプラグインを例に、コード生成ツールの使い方を説明します。

1. プラグインのインストールと実行

詳しくは、インターネットで関連資料を検索してください。

2. 生成されたコードの例

2.1. モデルクラスのコードを生成する
User.javaファイルの内容は以下の通りです。

......
public class User {
    private Long id;
    private String user;
    private String password;
    private Integer age;
    ......
}

2.2 マッパーインターフェースコードの生成
UserMapper.javaファイルの内容は以下の通りです。

......
public interface UserMapper {
    User selectByPrimaryKey(Long id);
    ......
}

2.3. マッパー XML コードの生成
UserMapper.xmlファイルの内容は以下の通りです。

......
<mapper namespace="com.test.dao.UserMapper" >
  <resultMap id="BaseResultMap" type="com.test.pojo.User" >
    <id column="id" property="id" jdbcType="BIGINT" />
    <result column="user" property="user" jdbcType="VARCHAR" />
    <result column="password" property="password" jdbcType="VARCHAR" />
    <result column="age" property="age" jdbcType="INTEGER" />
  </resultMap>
  <sql id="Base_Column_List" >
    id, user, password, age
  </sql>
  <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Long" >
    select 
    <include refid="Base_Column_List" />
    from test_user
    where id = #{id,jdbcType=BIGINT}
  </select>
  ......
</mapper>

3. メリットとデメリット

メリット:
1、コード生成プラグインを使用してコードを生成します。そのため、コード生成速度が速いこと。
2、プラグインの設定ファイルを使用して、所望の機能コードの生成を制御します。
デメリット:
1、コード生成プラグインの使用方法を勉強して慣れるまでに時間がかかること。
2、生成されたコードが必ずしもコード仕様を満たしているとは限らない。生成のたびにコードの適合性チェックが必要になります。
3、コードを再生成した後は、カスタムコードが上書きされる可能性が高い為、別のコードライブラリを管理し、DIFFツールを使用してコードの違いを識別し、値を割り当ててから、異なるコードを貼り付けることをお勧めします。

方法6:コードを使ってコードを生成する

つまり、自分で書いて、自分流にコードを生成します。以下では、MyBatisベースのデータベースアクセスのコードを例にとり、この方法を説明します。

1. テーブル情報の問い合わせ

まず、コード生成に必要なテーブルとカラムの情報をデータベースから取得します。

1.1. テーブル情報を問い合わせる
テーブル情報を問い合わせるためのステートメントは以下の通りです。

select t.table_name as 'table name'
, t.table_comment as 'table remarks'
from information_schema.tables t
where t.table_schema = ?
and t.table_type = 'BASE TABLE'
and t.table_name = ?;

1つ目のクエスチョンマークはデータベース名に割り当てられた値を、2つ目のクエスチョンマークはテーブル名に割り当てられた値を示します。

テーブル情報のクエリ結果は以下のようになります。

SN. Table name Table remarks
1 org_company Organization company table

1.2. クエリカラム情報
カラム情報をクエリするためのステートメントは以下の通りです。

select c.column_name as 'column name'
, c.column_comment as 'column remarks'
, c.data_type as 'data type'
, c.character_maximum_length as 'character length'
, c.numeric_precision as 'numeric precision'
, c.numeric_scale as 'numeric scale'
, c.column_default as ''
, c.is_nullable as 'optional?'
, c.column_key as 'column key name'
from information_schema.columns c
where c.table_schema = ?
and c.table_name = ?
order by c.ordinal_position;

1つ目のクエスチョンマークはデータベース名に割り当てられた値を、2つ目のクエスチョンマークはテーブル名に割り当てられた値を示しています。

カラム情報のクエリ結果は以下のようになります。

image.png

2 コードのコンパイルと生成

2.1 モデル・クラス・コードのコンパイルと生成

/** Generate model class file function */
private void generateModelClassFile(File dir, Table table, List<Column> columnList) throws Exception {
try (PrintWriter writer = new PrintWriter(new File(dir, className + "DO.java"))) {
String className = getClassName(table.getTableName());
String classComments = getClassComment(table.getTableComment());
writer.println("package " + groupName + "." + systemName + ".database;");
......
writer.println("/** " + classComments + "DO class */");
writer.println("@Getter");
writer.println("@Setter");
writer.println("@ToString");
writer.println("public class " + className + "DO {");
for (Column column : columnList) {
String fieldType = getFieldType(column);
String fieldName = getFieldName(column.getColumnName());
String fieldComment = getFieldComment(column);
writer.println("\t/** " + fieldComment + " */");
writer.println("\tprivate " + fieldType + " " + fieldName + ";");
}
writer.println("}");
}
}

2.2 DAOインターフェースコードのコンパイルと生成

/** Generate DAO interface file function */
private void generateDaoInterfaceFile(File dir, Table table, List<Column> columnList, List<Column> pkColumnList) throws Exception {
    try (PrintWriter writer = new PrintWriter(new File(dir, className + "DAO.java"))) {
        String className = getClassName(table.getTableName());
        String classComments = getClassComment(table.getTableComment());
        writer.println("package " + groupName + "." + systemName + ".database;");
        ......
        writer.println("/** " + classComments + "DAO interface */");
        writer.println("public interface " + className + "DAO {");
        writer.println("\t/** get" + classComments + "function */");
        writer.print("\tpublic " + className + "DO get(");
        boolean isFirst = true;
        for (Column pkColumn : pkColumnList) {
            if (!isFirst) {
                writer.print(", ");
            } else {
                isFirst = false;
            }
            String fieldType = getFieldType(pkColumn);
            String fieldName = getFieldName(pkColumn.getColumnName());
            writer.print("@Param(\"" + fieldName + "\") " + fieldType + " " + fieldName);
        }
        writer.println(");");
        ......
        writer.println("}");
    }
}

2.3 DAO マッパーコードのコンパイルと生成

/** Generate DAO mapping file function */
private void generateDaoMapperFile(File dir, Table table, List<Column> columnList, List<Column> pkColumnList) throws Exception {
    try (PrintWriter writer = new PrintWriter(new File(dir, className + "DAO.xml"))) {
        String className = getClassName(table.getTableName());
        String classComments = getClassComment(table.getTableComment());
        writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
        ......
        writer.println("<!-- " + classComments + "Mapping -->");
        writer.println("<mapper namespace=\"" + groupName + "." + systemName + ".database." + className + "DAO\">");
        writer.println("\t<!—All field statements -->");
        writer.println("\t<sql id=\"fields\">");
        if (CollectionUtils.isNotEmpty(columnList)) {
            boolean isFirst = true;
            String columnName = getColumnName(pkColumn.getColumnName());
            for (Column column : columnList) {
                if (isFirst) {
                    isFirst = false;
                    writer.println("\t\t" + columnName);
                } else {
                    writer.println("\t\t, " + columnName);
                }
            }
        }
        writer.println("\t</sql>");
        writer.println("\t<!-- get" + classComments + "function statement -->");
        writer.println("\t<select id=\"get\" resultType=\"" + groupName + "." + systemName + ".database." + className + "DO\">");
        writer.println("\t\tselect");
        writer.println("\t\t<include refid=\"fields\"/>");
        writer.println("\t\tfrom " + table.getTableName());
        boolean isFirst = true;
        for (Column pkColumn : pkColumnList) {
            String columnName = getColumnName(pkColumn.getColumnName());
            String fieldName = getFieldName(pkColumn.getColumnName());
            writer.print("\t\t");
            if (isFirst) {
                writer.print("where");
                isFirst = false;
            } else {
                writer.print("and");
            }
            writer.println(" " + columnName + " = #{" + fieldName + "}");
        }
        writer.println("\t</select>");
        writer.println("</mapper>");
    }
}

3 関連するコードの生成

3.1 生成されたモデルクラスのコード

/** Organize company DO class */
@Getter
@Setter
@ToString
public class OrgCompanyDO {
    /** company logo */
    private Long id;
    /** company name */
    private String name;
    /** contact address */
    private String address;
    /** company description */
    private String description;
}

3.2 生成されたDAOインタフェースコード

/** Organize company DAO interface */
public interface OrgCompanyDAO {
    /** Get organization company function */
    public OrgCompanyDO get(@Param("id") Long id);
}

3.3 生成されたDAOマッパーコード

<!—Organize company mapping -->
<mapper namespace="xxx.database.OrgCompanyDAO">
    <!—All field statement -->
    <sql id="fields">
        id
        , name
        , address
        , description
    </sql>
    <!—Get organization company function statement -->
    <select id="get" resultType="xxx.database.OrgCompanyDO">
        select
        <include refid="fields"/>
        from org_company
        where id = #{id}
    </select>
</mapper>

4. メリットとデメリット

メリット:
1、コードフォーマットをカスタマイズして、生成されたコードのコンプライアンスを確保することができます。
2、コード機能をカスタマイズして、目的のコードだけを生成することができます。
3、事前のコード沈殿の後、コードは後で直接使用することができます。
デメリット:
1、コード生成に必要なデータを確実に取得するためにデータソースの検討が必要です。
2、データモデルの作成やコンパイル、コード生成に時間がかかります。

究極のメソッド:メソッドだけに固執しない

必要なものを直接コンピュータに伝えれば、コンピュータが自動的にコードを生成してくれるという究極のコーディング方法でしょうか。これは将来的に技術がある程度発展してから現実のものになるかもしれません。しかし、現在ではこの方法は非現実的です。現実には、上司やプロダクトマネージャー、テクニカルマネージャーでもない限り、「口を開けばすぐにコードを生成する」ということはできません。

コーディングの究極の方法は、方法だけに固執するのではなく、適切な方法であれば何でも使うことです。この記事で挙げたすべてのコーディング方法は、それぞれにメリットやデメリットがあり、様々なシナリオに適用可能です。そのため、様々なコーディング方法を柔軟に使い分けることが、本当の究極のコーディング方法と言えるでしょう。

コードの標準化

先行するコーディング方法の多くは、サンプルコードを手作業でコンパイルする必要があります。コードがコード仕様に準拠していない場合、コード間の共通点を見つけ出し、標準として機能するサンプルコードを抽象化することが困難になります。標準として機能するサンプルコードがコード仕様に準拠していなければ、生成されたコードもコード仕様に準拠しておらず、不準拠は数十倍、数百倍、数千倍に拡大してしまいます。そのため、コードの標準化はコーディングの最優先事項になります。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スタートアップ企業のJavaサーバで見られる、混乱について

この記事では、スタートアップ企業のJavaサーバで見られる混乱を招く状況について、これらのサービスをより良い方向に向けるためのいくつかの方法を見ていきます。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

Controller Base ClassとService Base Classを使用

ベースクラス(Base Class)の紹介

Controller Base Class(コントローラベースクラス)

/** Controller Base Classes */
public class BaseController {    
    /** Injection services related */
    /** User Service */
    @Autowired
    protected UserService userService;
    ...

    /** Static constant correlation */
    /** Phone number mode */
    protected static final String PHONE_PATTERN = "/^[1]([3-9])[0-9]{9}$/";
    ...

    /** Static function related */
    /** Verify phone number */
    protected static vaildPhone(String phone) {...}
    ...
}

共通のコントローラベースクラスには、主にインジェクションサービス、静的定数、静的関数が含まれており、すべてのコントローラがこれらのリソースをコントローラベースクラスから継承し、関数で直接使用できるようになっています。

Service Base Class(サービスベースクラス)
一般的なService Base Classは以下の通りです。

/** Service Base Classes */
public class BaseService {
    /** Injection DAO related */
    /** User DAO */
    @Autowired
    protected UserDAO userDAO;
    ...

    /** Injection services related */
    /** SMS service */
    @Autowired
    protected SmsService smsService;
    ...

    /** Injection parameters related */
    /** system name */
    @Value("${example.systemName}")
    protected String systemName;
    ...

    /** Injection constant related */
    /** super user ID */
    protected static final long SUPPER_USER_ID = 0L;
    ...

    /** Service function related */
    /** Get user function */
    protected UserDO getUser(Long userId) {...}
    ...

    /** Static function related */
    /** Get user name */
    protected static String getUserName(UserDO user) {...}
    ...
}

共通のサービスベースクラスは、主にインジェクションデータアクセスオブジェクト(DAO)、インジェクションサービス、インジェクションパラメータ、静的定数、サービス関数、静的関数を含んでおり、すべてのサービスがサービスベースクラスからこれらのリソースを継承し、直接関数で使用できるようになっています。

ベースクラスの必要性

まず、Liskov Substitution Principle (LSP)を見てみましょう。

LSPによると、ベースクラス(スーパークラス)を参照するすべての場所で、そのサブクラスのオブジェクトを透過的に使用できるようにしなければなりません。

次に、ベースクラスの利点を見てみましょう。

1、ベースクラスは、サブクラスがスーパークラスのすべてのメソッドと属性を持っているので、サブクラスを作成するための作業負荷を軽減します。
2、ベースクラスは、サブクラスがスーパークラスのすべての機能を持っているので、コードの再利用性を向上させます。
3、ベースクラスは、サブクラスが独自の関数を追加できるので、コードのスケーラビリティを向上させます。

したがって、次のような結論を導き出すことができます。

1、コントローラベースクラスとサービスベースクラスは、プロジェクトのどこにも直接使用されておらず、それらのサブクラスで置き換えられることはありません。したがって、それらはLSPに準拠していません。
2、コントローラベースクラスとサービスベースクラスは、抽象インターフェース関数や仮想関数を持ちません。つまり、ベースクラスを継承する全てのサブクラスは共通の特性を持ちません。その結果、プロジェクトで使用されるものはサブクラスのままです。
3、コントローラのベースクラスやサービスのベースクラスは、再利用性にのみ焦点を当てています。つまり、サブクラスは、インジェクションDAO、インジェクションサービス、インジェクションパラメータ、スタティック定数、サービス関数、スタティック関数など、ベースクラスのリソースを便利に使うことができます。しかし、コントローラベースクラスやサービスベースクラスは、これらのリソースの必要性を無視しています。つまり、これらのリソースは、サブクラスにとって必要不可欠なものではありません。そのため、サブクラスがロードされたときにパフォーマンスを低下させてしまいます。

結論から言うと、コントローラベースクラスもサービスベースクラスも雑多なクラスに分類されます。これらは本当の意味でのベースクラスではないので、分割する必要があります。

ベースクラスを分割する方法

コントローラのベースクラスよりもサービスベースクラスの方が代表的なので、この記事ではサービスベースクラスを例に「ベースクラス」を分割する方法を説明します。

インジェクションインスタンスを実装クラスに入れる
「使うときだけクラスを導入し、不要なときは削除する」という原則に従い、使用するDAOやサービス、パラメータなどを実装クラスに注入します。

/** Udser Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** SMS service */
    @Autowired
    private SmsService smsService;

    /** System name */
    @Value("${example.systemName}")
    private String systemName;
    ...
}

静的定数を定数クラスに入れる
静的定数を対応する定数クラスにカプセル化し、必要なときに直接使用します。

/** example constant class */
public class ExampleConstants {
    /** super user ID */
    public static final long SUPPER_USER_ID = 0L;
    ...
}

サービスクラスにサービス関数を入れる
サービス機能を対応するサービスクラスにカプセル化します。他のサービスクラスを使用する場合は、このサービスクラスのインスタンスを注入し、インスタンスを通してサービス機能を呼び出すことができます。

/** User service class */
@Service
public class UserService {
    /** Ger user function */
    public UserDO getUser(Long userId) {...}
    ...
}

/** Company service class */
@Service
public class CompanyService {
    /** User service */
    @Autowired
    private UserService userService;

    /** Get the administrator */
    public UserDO getManager(Long companyId) {
        CompanyDO company = ...;
        return userService.getUser(company.getManagerId());
    }
    ...
}

ツールクラスに静的関数を入れる
静的関数を対応するツールクラスにカプセル化し、必要なときに直接使用します。

/** User Aid Class */
public class UserHelper {
    /** Get the user name */
    public static String getUserName(UserDO user) {...}
    ...
}

ビジネスコードはコントローラクラスに記述あり

現象の説明

コントローラクラスで以下のようなコードをよく見かけます。

/** User Controller Class */
@Controller
@RequestMapping("/user")
public class UserController {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function */
    @ResponseBody
    @RequestMapping(path = "/getUser", method = RequestMethod.GET)
    public Result<UserVO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
        // Get user information
        UserDO userDO = userDAO.getUser(userId);
        if (Objects.isNull(userDO)) {
            return null;
        }

        // Copy and return the user
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userDO, userVO);
        return Result.success(userVO);
    }
    ...
}

コンパイラは、インタフェース関数がシンプルで、インタフェース関数をサービス関数にカプセル化する必要がないので、このように書いても良いと説明するかもしれませんが、実際には、インタフェース関数をサービス関数にカプセル化する必要はありません。

特殊な場合

この特殊なケースでは、コードは以下のようになります。

/** Test Controller Class */
@Controller
@RequestMapping("/test")
public class TestController {
    /** System name */
    @Value("${example.systemName}")
    private String systemName;

    /** Access function */
    @RequestMapping(path = "/access", method = RequestMethod.GET)
    public String access() {
        return String.format("You're accessing System (%s)!", systemName);
    }
}

アクセス結果は以下の通りです。

curl http://localhost:8080/test/access

System(null) にアクセスしています!

なぜsystemNameパラメータが注入されていないのかと質問されるかもしれません。さて、Springのドキュメントには次のような説明があります。

実際の@Valueアノテーションの処理はBeanPostProcessorによって実行されることに注意してください。

BeanPostProcessorインタフェースはコンテナごとにスコープされています。これはコンテナ階層を使用している場合にのみ関連します。1つのコンテナでBeanPostProcessorを定義した場合,そのコンテナ内のBeanに対してのみ、その作業を行います。1つのコンテナで定義されたBeanは、両方のコンテナが同じ階層の一部であっても、別のコンテナのBeanPostProcessorによって後処理されません。

これらの説明によれば、@ValueはBeanPostProcessorを介して処理され、WebApplicationContexApplicationContextは別々に処理されます。したがって、WebApplicationContexは、親コンテナの属性値を使用することができません。

コントローラはサービス要件を満たしていません。したがって,コントローラクラスにビジネスコードを記述することは不適切です。

三層サーバーアーキテクチャ

SpringMVCサーバーは、プレゼンテーション層、ビジネス層、パーシスタンス層からなる古典的な3層アーキテクチャを採用しており、クラスアノテーションに@Controller@Service@Repositoryを使用しています。

image.png

  • プレゼンテーション層:コントローラー層とも呼ばれています。このレイヤーは、クライアントからのリクエストを受信し、クライアントからの結果をクライアントに応答する役割を担っています。この層ではHTTPがよく使われています。
  • ビジネス層:サービス層とも呼ばれています。ビジネス関連のロジック処理を担当するレイヤーで、機能別にサービスとジョブに分かれています。
  • 永続性レイヤー:リポジトリ層とも呼ばれます。この層はデータの永続性を担当し、ビジネス層がキャッシュやデータベースにアクセスするために使用します。

そのため、コントローラークラスにビジネスコードを書くことは、SpringMVCサーバーの3層アーキテクチャ仕様に準拠していません。

永続性レイヤーのコードはサービスクラスへ

機能面では、サービスクラスの中にパーシスタンス層のコードを書いても良いと思います。そのため、多くのユーザーがこのコーディング方法を受け入れています。

主な問題点

1、ビジネス層とパーシスタンス層が混在しており、SpringMVCサーバの3層アーキテクチャ仕様に準拠していないこと。
2、ステートメントや主キーがビジネスロジックで組み立てられているため、ビジネスロジックの複雑さが増すこと。
3、サードパーティ製のミドルウェアがビジネスロジックで直接使用されているため、サードパーティ製のパーシステンスミドルウェアの置き換えが難しいこと。
4、また、同じオブジェクトの永続化レイヤのコードが様々なビジネスロジックに散らばってしまい、オブジェクト指向プログラミングの原則に反すること。
5、このコーディング方法でユニットテストケースを書いてしまうと、パーシスタンス層のインタフェース機能を直接テストすることが出来ないこと。

データベースのコードはサービスで書き込まれる

ここでは、データベース永続化ミドルウェアHibernateの直接問い合わせを例に説明します。

現象の説明

/** User Service Class */
@Service
public class UserService {
    /** Session factory */
    @Autowired
    private SessionFactory sessionFactory;

    /** Get user function based on job number */
    public UserVO getUserByEmpId(String empId) {
        // Assemble HQL statement
        String hql = "from t_user where emp_id = '" + empId + "'";

        // Perform database query
        Query query = sessionFactory.getCurrentSession().createQuery(hql);
        List<UserDO> userList = query.list();
        if (CollectionUtils.isEmpty(userList)) {
            return null;
        }

        // Convert and return user
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userList.get(0), userVO);
        return userVO;
    }
}

おすすめの解決策

/** User DAO CLass */
@Repository
public class UserDAO {
     /** Session factory */
    @Autowired
    private SessionFactory sessionFactory;

    /** Get user function based on job number */
    public UserDO getUserByEmpId(String empId) {
        // Assemble HQLstatement
        String hql = "from t_user where emp_id = '" + empId + "'";

        // Perform database query
        Query query = sessionFactory.getCurrentSession().createQuery(hql);
        List<UserDO> userList = query.list();
        if (CollectionUtils.isEmpty(userList)) {
            return null;
        }

        // Return user information
        return userList.get(0);
    }
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function based on job number */
    public UserVO getUserByEmpId(String empId) {
        // Query user based on job number
        UserDO userDO = userDAO.getUserByEmpId(empId);
        if (Objects.isNull(userDO)) {
            return null;
        }

        // Convert and return user
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userDO, userVO);
        return userVO;
    }
}

プラグインについて
AliGeneratorは、アリババが開発したMyBatis Generatorベースのツールで、DAO(データアクセスオブジェクト)層のコードを自動生成します。AliGeneratorで生成されたコードでは、複雑なクエリを実行する際に、ビジネスコード内でクエリ条件を組み立てる必要があります。そのため、ビジネスコードが特に肥大化してしまいます。

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function */
    public UserVO getUser(String companyId, String empId) {
        // Query database
        UserParam userParam = new UserParam();
        userParam.createCriteria().andCompanyIdEqualTo(companyId)
            .andEmpIdEqualTo(empId)
            .andStatusEqualTo(UserStatus.ENABLE.getValue());
        List<UserDO> userList = userDAO.selectByParam(userParam);
        if (CollectionUtils.isEmpty(userList)) {
            return null;
        }

        // Convert and return users
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(userList.get(0), userVO);
        return userVO;
    }
}

個人的には、プラグインを使って DAO レイヤーのコードを生成するのは好きではありません。代わりに、マッピングにオリジナルの MyBatis XML を使用する方が好きです。

  • プラグインは、プロジェクトに不適合なコードをインポートする可能性があります。
  • 単純なクエリを実行するには、複雑なコードの完全なセットをインポートする必要があります。
  • 複雑なクエリでは、条件を組み立てるためのコードが複雑で直感的ではありません。XMLで直接SQL文を書く方が良いでしょう。
  • テーブルを変更した後、コードを再生成して上書きする必要があり、その間に誤ってユーザー定義関数(UDF)を削除してしまう可能性があります。

プラグインの使用を選択する場合は、プラグインがもたらす利便性を享受しつつ、プラグインのデメリットも受け入れるべきです。

Redisのコードはサービスクラスへ

説明

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;
    /** Redistemplate */
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    /** User primary key mode */
    private static final String USER_KEY_PATTERN = "hash::user::%s";

    /** Save user function */
    public void saveUser(UserVO user) {
        // Convert user information
        UserDO userDO = transUser(user);

        // Save Redis user
        String userKey = MessageFormat.format(USER_KEY_PATTERN, userDO.getId());
        Map<String, String> fieldMap = new HashMap<>(8);
        fieldMap.put(UserDO.CONST_NAME, user.getName());
        fieldMap.put(UserDO.CONST_SEX, String.valueOf(user.getSex()));
        fieldMap.put(UserDO.CONST_AGE, String.valueOf(user.getAge()));
        redisTemplate.opsForHash().putAll(userKey, fieldMap);

        // Save database user
        userDAO.save(userDO);
    }
}

おすすめの解決策

/** User Redis Class */
@Repository
public class UserRedis {
    /** Redistemplate */
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    /** Primary key mode */
    private static final String KEY_PATTERN = "hash::user::%s";

    /** Save user function */
    public UserDO save(UserDO user) {
        String key = MessageFormat.format(KEY_PATTERN, userDO.getId());
        Map<String, String> fieldMap = new HashMap<>(8);
        fieldMap.put(UserDO.CONST_NAME, user.getName());
        fieldMap.put(UserDO.CONST_SEX, String.valueOf(user.getSex()));
        fieldMap.put(UserDO.CONST_AGE, String.valueOf(user.getAge()));
        redisTemplate.opsForHash().putAll(key, fieldMap);
    }
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;
    /** User Redis */
    @Autowired
    private UserRedis userRedis;

    /** Save user function */
    public void saveUser(UserVO user) {
        // 转化用户信息
        UserDO userDO = transUser(user);

        // Save Redis user
        userRedis.save(userDO);

        // Save database user
        userDAO.save(userDO);
    }
}

Redisのオブジェクト関連の操作インターフェースをDAOクラスにカプセル化します。これは、SpringMVCサーバのオブジェクト指向プログラミングの原則と3階層アーキテクチャの仕様に準拠しており、コードの管理・保守を容易にします。

データベースモデルクラスはインターフェースに晒されている

症状の説明

/** User DAO Class */
@Repository
public class UserDAO {
    /** Get user function */
    public UserDO getUser(Long userId) {...}
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Get user function */
    public UserDO getUser(Long userId) {
        return userDAO.getUser(userId);
    }
}

/** User Controller Class */
@Controller
@RequestMapping("/user")
public class UserController {
    /** User service */
    @Autowired
    private UserService userService;

    /** Get user function */
    @RequestMapping(path = "/getUser", method = RequestMethod.GET)
    public Result<UserDO> getUser(@RequestParam(name = "userId", required = true) Long userId) {
        UserDO user = userService.getUser(userId);
        return Result.success(user);
    }
}

先行コードはSpringMVCサーバの3層アーキテクチャに準拠しているようです。唯一の問題は、データベースモデルUserDOが外部インターフェースに直接さらされていることです。

既存の問題と解決策

既存の問題点
1、データベースのテーブル設計が間接的に露出しているため、競合製品の分析に便利です。
2、データベースクエリにフィールド制限が課されていない場合、インターフェースデータの量が膨大になり、ユーザーの貴重なトラフィックを無駄にします。
3、データベースクエリにフィールド制限が課されていない場合、機密性の高いフィールドがインターフェースに簡単に露出してしまい、データセキュリティの問題が生じます。
4、データベースモデルクラスがインターフェースの要件を満たすことができない場合、データベースモデルクラスに他のフィールドを追加する必要があり、その結果、データベースモデルクラスとデータベースのフィールドの間にミスマッチが発生します。
5、インターフェースのドキュメントが適切にメンテナンスされていない場合、コードを読んでも、データベースモデルクラス内のどのフィールドがインターフェースによって使用されているかを識別するのに役立ちません。そのため、コードの保守性が悪くなります。

ソリューション
1、管理システムの観点からは、データベースモデルクラスはインターフェースモデルクラスから完全に独立していなければなりません。
2、プロジェクトの構造上、開発者はデータベースモデルクラスをインターフェースに公開しないようにしなければなりません。

プロジェクト構築のための3つの方法

以下では、開発者がデータベースモデルクラスをインターフェースに公開することを効果的に防ぐために、より科学的にJavaプロジェクトを構築する方法を説明します。

方法1:共有モデルでプロジェクトを構築する
すべてのモデル・クラスを 1 つのモデル・プロジェクト(example-model)に配置します。他のプロジェクト(example-repository、example-service、example-website など)は、すべて example-model に依存しています。関係図は以下のようになります。

image.png

image.png

リスク
プレゼンテーション層プロジェクト(example-webapp)は、ビジネス層プロジェクト(example-service)の任意のサービス機能を呼び出すことができますし、ビジネス層をまたいでパーシスタンス層プロジェクト(example-repository)のDAO機能を直接呼び出すこともできます。

方法2:分離されたモデルでプロジェクトを構築
API プロジェクト(example-api)を別途構築し、外部インタフェースとそのモデル VO クラスを抽象化する。ビジネス層プロジェクト(example-service)はこれらのインタフェースを実装し、プレゼンテーション層プロジェクト(example-webapp)にサービスを提供します。プレゼンテーション層プロジェクト(example-webapp)は、API プロジェクト(example-api)で定義されたサービスインタフェースのみを呼び出します。

image.png

image.png

リスク
プレゼンテーション層プロジェクト(example-webapp)は、ビジネス層プロジェクト(example-service)の内部サービス機能とパーシステンス層プロジェクト(example-repository)のDAO機能をまだ呼び出すことができます。このような状況を避けるために、管理システムは、プレゼンテーション層プロジェクト(example-webapp)がAPIプロジェクト(example-api)によって定義されたサービスインターフェース関数のみを呼び出すことができるようにする必要があります。

方法3: サービス指向プロジェクトを構築する
ビジネス層プロジェクト(example-service)と永続化層プロジェクト(example-repository)をDubboプロジェクト(example-dubbo)を使ってサービスにパッケージ化します。ビジネスレイヤプロジェクト(example-webapp)または他のビジネスプロジェクト(other-service)に対して、APIプロジェクト(example-api)で定義されたインターフェース機能を提供します。

image.png

image.png

注意: Dubboプロジェクト(example-dubbo)は、APIプロジェクト(example-api)で定義されたサービス・インターフェースのみをリリースします。これにより、データベースモデルが公開されないことを保証します。ビジネスレイヤープロジェクト(例-webapp)や他のビジネスプロジェクト(他のサービス)は、APIプロジェクト(例-api)にのみ依存し、APIプロジェクトで定義されているサービスインターフェースのみを呼び出すことができます。

あまり推奨されない提案

ユーザーによっては、次のような配慮があるかもしれません。インタフェースモデルと永続層モデルが分離されていることを考えると、インタフェースモデルにデータ問い合わせモデルのVOクラスが定義されている場合、永続層モデルにもデータ問い合わせモデルのDOクラスが定義されている必要があることになります。また、インタフェースモデルにデータ戻りモデルのVOクラスが定義されている場合は、永続層モデルにもデータ戻りモデルのDOクラスが定義されている必要があります。しかし、これはプロジェクトの初期段階での迅速な反復開発にはあまり適していません。また、次のような疑問も出てきます。永続層のデータモデルをインターフェイスを通して公開せずに、インターフェイスのデータモデルを永続層に使わせることは可能なのでしょうか?

このメソッドは、SpringMVCサーバの3層アーキテクチャの独立性に影響を与えるため、受け入れられません。しかし、この方法はデータベースモデルクラスを公開しないので、迅速な反復開発には許容できます。したがって、あまりお勧めできない提案です。

/** User DAO Class */
@Repository
public class UserDAO {
    /** Calculate user function */
    public Long countByParameter(QueryUserParameterVO parameter) {...}
    /** Query user function */
    public List<UserVO> queryByParameter(QueryUserParameterVO parameter) {...}
}

/** User Service Class */
@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Query user function */
    public PageData<UserVO> queryUser(QueryUserParameterVO parameter) {
        Long totalCount = userDAO.countByParameter(parameter);
        List<UserVO> userList = null;
        if (Objects.nonNull(totalCount) && totalCount.compareTo(0L) > 0) {
            userList = userDAO.queryByParameter(parameter);
        }
        return new PageData<>(totalCount, userList);
    }
}

/** User Controller Class */
@Controller
@RequestMapping("/user")
public class UserController {
    /** User service */
    @Autowired
    private UserService userService;

    /** Query user function (with the page index parameters of startIndex and pageSize) */
    @RequestMapping(path = "/queryUser", method = RequestMethod.POST)
    public Result<PageData<UserVO>> queryUser(@Valid @RequestBody QueryUserParameterVO parameter) {
        PageData<UserVO> pageData = userService.queryUser(parameter);
        return Result.success(pageData);
    }
}

結論

Javaをどのように活用すべきかについては人それぞれの意見があり、もちろんこの記事では私の個人的な意見のみを述べています。しかし、私にとっては、私が以前働いていたいくつかのスタートアップ企業での経験に基づいて、自分の考えを表現することが重要だと考えました。なぜなら、私の理解では、これらのカオスな構成が修正されていれば、システム全体がより良いものになると考えているからです。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java 15新機能まとめ

Java 15が2020/9/15にリリースされました。
https://mail.openjdk.java.net/pipermail/announce/2020-September/000291.html

Java SE 15 Platform JSR 390

MacやLinuxでのインストールにはSDKMAN!をお勧めします

Oracle OpenJDK以外に無償で商用利用できるディストリビューションとしては、次のようなものがあります。

Amazon CorrettoではRCがリリースされています。
Amazon Corretto 15 Release Candidate (RC)

アップデートは10月に15.0.1が、翌年1月に15.0.2がリリースされることになります。

Oracle JDKは開発用途には利用できますが、商用利用にはJava SE Subscriptionを購入する必要があります。

JEP

大きめの変更はJEPでまとまっています。
http://openjdk.java.net/projects/jdk/15/

今回は14のJEPが取り込まれました。ただ、deprecateやremoveが4つ、すでにプレビューで出ていたものが6つあり、今回新たに取り込まれたのは実質4つとなります。
JEP 339: Edwards-Curve Digital Signature Algorithm (EdDSA)
JEP 360: Sealed Classes (Preview)
JEP 371: Hidden Classes
JEP 372: Remove the Nashorn JavaScript Engine
JEP 373: Reimplement the Legacy DatagramSocket API
JEP 374: Disable and Deprecate Biased Locking
JEP 375: Pattern Matching for instanceof (Second Preview)
JEP 377: ZGC: A Scalable Low-Latency Garbage Collector (Production)
JEP 378: Text Blocks (Standard)
JEP 379: Shenandoah: A Low-Pause-Time Garbage Collector (Production)
JEP 381: Remove the Solaris and SPARC Ports
JEP 383: Foreign-Memory Access API (Second Incubator)
JEP 384: Records (Second Preview)
JEP 385: Deprecate RMI Activation for Removal

分野ごとにまとめていきます。

言語機能

言語仕様にかかわる変更としては次のようなものがあります。
JEP 360: Sealed Classes (Preview)
JEP 375: Pattern Matching for instanceof (Second Preview)
JEP 384: Records (Second Preview)
JEP 378: Text Blocks (Standard)

JEP 360: Sealed Classes (Preview)

Sealedクラスは、継承できるクラスを限定する機能です。仕様の名前は「クラス」 となってますが、interfaceでも使えます。
クラス名のあとに permitsで継承クラスを指定していくという感じになります。

public abstract sealed class Shape
    permits Circle, Rectangle, Square {...}

そうするとShapeクラスを継承できるのはCircle、Rectangle、Squareに限定されます。Circle、Rectangle、Squareは必ず定義する必要があります。
これらのクラスは、モジュール化しているのであれば同じモジュール内、モジュール化していないのであれば同じパッケージ内である必要があります。
また、permitsされるクラスが同一のソースだけにあるのであれば、permitsは省略できます。
permitsするクラスは定義のときにsealedかnon-sealedかfinalをつける必要があります。recordは暗黙的にfinalになるのでなにもつける必要はありません。

現状ではほとんど使いどころがないです。
例えば、Shapeの継承先は確定しているので、次のコードはコンパイル通ってほしいところですが、エラーになります。

String getName(Shape s) {
  if (s instanceof Circle) {
    return "円";
  } else if (s instanceof Rectangle) {
    return "四角";
  } else if (s instanceof Square) {
    return "正方形";
  }
}

まだドラフトですがPattern matching for switch (Preview)が導入されてswitch式/文でパターンマッチが使えるようになれば、次のようにdefaultなしで書ける予定です。

String getName(Shape s) {
  switch (s) {
    case Circle c -> return "円";
    case Rectangle r -> return "四角";
    case Square q -> return "正方形";
  }
}

もちろん、ここでswitch式を使うこともできますね。

String getName(Shape s) {
  return switch (s) {
    case Circle c -> "円";
    case Rectangle r -> "四角";
    case Square q -> "正方形";
  }
}

ということで、現時点でのSealedクラスは今後の拡張のための下準備というところです。
背景にある考え方は、次の記事で言語設計者であるBrian Goetzが解説しています。
Java 注目の機能:Sealed クラス

JEP 384: Records (Second Preview)

データ保持用のクラスとしてrecordがJava 14にpreview機能として入ったRecordsですが、フィードバックを反映したSecond Previewになりました。Java 16で次のJEPで正式化という流れになりそうです。
JEP draft: Record Classes

変更点

・コンストラクタがpublicである必要があったものが、record本体に合わせた可視性で定義できるようになりました。
・引数リストを省略したコンストラクタでコンポーネントフィールドに値を割り当てるとコンパイルエラーになります。

定義

recordとして定義します。

record Foo(int x, int y) {}

他のクラスは継承できません。

record レコード名(状態) { 定義 }

これは次のようなクラスになります。

class Foo extends Record {
  // 状態がprivate finalとして定義
  private final int x;
  private final int y;
  // 追加のインスタンスフィールドは定義できない

  // 状態をフィールドに設定するコンストラクタが定義される
  public Foo(int x, int y) {
    this.x = x;
    this.y = y;
  }

  // 状態と同じ名前のメソッドが定義される
  public int x() {
    return x;
  }
  public int y() {
    return y;
  }

  // 状態を反映するhashCodeが定義される
  public int hashCode() { ... }
  // 状態を比較するequals が定義される
  public boolean equals() { ... }
  // 状態を表示するtoStringが定義される
  public String toString() { ... }
}

取得用メソッドは定義されますが、値設定用のメソッドは定義されません。つまり、イミュータブルなオブジェクトとなります。また、get/setではないことからJava Beanとの互換性もありません。
hashCode()メソッドやequals()メソッドなどは実際にはinvokeDynamicで実行時に実装コードが生成されます。

次のように、状態の検査や正規化にコンストラクタが定義できます。コンストラクタの引数リストは省略できます。

record Range(int lo, int hi) {
  Range {
    if (lo < 0) lo = 0; // 正規化
    if (lo > hi) throw IllegalArgumentException();
    // 設定されなかったフィールドはあとで設定される
  }
}

Java 14ではコンストラクタはpublicである必要がありましたが、Java 15ではrecordがパッケージプライベートであればコンストラクタもパッケージプライベートにできるというように、record本体の可視性にあわせれるようになりました。
また、コンストラクタの引数リストを省略した場合、Java 15からはコンポーネントフィールドに値を割り当てるとコンパイルエラーになります。

record Range(int lo, int hi) {
  Range {
    this.lo = 3; // コンパイルエラー
  }
}

staticではないinner classの中でrecordを定義することはできません。
次のようなコードをコンパイルするとrecord declarations not allowed in inner classesというエラーになります。

public class NestedRecord {
  class Foo {
      record Bar(int x){}
  }
}

この制約はJava 16で正式化されるときに外れる予定です。
次のようにstaticなクラスの中でのrecord定義はコンパイルできます。

public class NestedRecord {
  static class Foo {
      record Bar(int x){}
  }
}

APIの拡張

recordはjava.lang.Recordを継承したクラスになります。

次のようにRecordを継承するコードを書こうとするとrecords cannot directly extend java.lang.Recordというエラーになります。

class Foo extends Record {
}

Classクラスにはレコード関連のメソッドが追加されています。
isRecordメソッドで型がrecordかどうか判定できます。またgetRecordComponentsでrecordで定義されたコンポーネントを取得することができます。ただし、値の取得はRecordComponent経由では行えないので、Field経由で行うようです。

jshell> Foo.class.isRecord()
$9 ==> true

jshell> Foo.class.getRecordComponents()
$10 ==> RecordComponent[2] { int x, int y }

jshell> String.class.isRecord()
$11 ==> false

jshell> String.class.getRecordComponents()
$12 ==> null

型名としてのrecordの制限

recordという名前を型(クラス・インタフェース・レコード型など)につけることは制限されています。
--enable-previewを付けた状態ではエラーになります。

$ jshell --enable-preview
|  JShellへようこそ -- バージョン15
|  概要については、次を入力してください: /help intro

jshell> class record{}
|  エラー:
|  ここでは'record'は許可されません
|    リリース14から'record'は制限された型名であり、型の宣言に使用できません
|  class record{}
|        ^

jshell> String record=""
record ==> ""

jshell> record Rec(int record){}

jshell> Rec record=new Rec(3)
record ==> Rec[record=3]

enumと違いキーワードではないので、変数名やフィールド、recordのコンポーネント名にはrecordを使えます。
--enable-previewを付けない状態では、警告が表示されます。

$ jshell
|  JShellへようこそ -- バージョン15
|  概要については、次を入力してください: /help intro

jshell> class record{}
|  警告:
|  'record'は将来のリリースで制限された型名になる可能性があり、型の宣言での使用、または配列の要素タイプとしての使用はできなくなる可能性があります
|  class record{}
|        ^
|  次を作成しました: クラス record

今後の改善

standardになるときには、現状ではstaticではないinner classではrecordを入れ子にすることができないけど対応するようです。
java.lang.Recordをinline class対応のためにインタフェースにするのはどうだろう?という話があったのだけど、ドロップしたようです。
java.lang.Record as Interface

JEP 378: Text Blocks (Standard)

複数行文字列リテラルです。

歴史

Java 12でRaw String literalsとして提案されましたが、`(バッククオート)を使う仕様だったことから、記号の浪費という指摘もあって却下されました。
JEP 326: Raw String Literals (Preview)

その後さまざまな言語での複数行文字列リテラルを研究して、Java 13でText Blockとして"""で囲む仕様がPreviewとして入りました。
JEP 355: Text Blocks (Preview)

Java 14ではフィードバックを基に改行やスペースのエスケープが入ってPreview 2になりました。
JEP 368: Text Blocks (Second Preview)

そしてJava 15で標準機能になりました。

機能

改行などを含んだ文字列を定義できます。"""で囲みます。

// same as "You can write\ntwo line string.\n"
var str = """
  You can write
  two line string.
  """;

開始の"""のあとには文字列を続けれません。また、インデントは"""や内部の文字列で一番浅いところが基準になります。

var str = """
..You can write
..two line string.
  """;
var str = """
..  You can write
..  two line string.
  """;

改行をエスケープすることもできます。

var str = """
  You can write \
  two line string, \
  but this is single.
  """;

これは"You can write two line string, but this is single."になります。

行末のスペースは削除されます。
そこで、行末にスペースが必要なときは\sを入れてスペースが必要なことを示します。

var str = """
  test\s
  test \s
  """;

これは"test_\ntest__\n"になります。(Qiitaでは複数スペースをいれてもスペースひとつになってしまう)

文字列への変数の埋め込みはできません。その代わりにformattedメソッドがインスタンスメソッドとして用意されて、次のように書けるようになりました。

var str = """
  こんにちは、%sさん。
  今日はいい天気ですね。
  """.formatted("きしだ");

API

Text BlocksとともにいくつかのAPIが正式化されています。いままでにも入っていましたが、これらのAPIも--enable-previewなしで使えるようになりました。

stripIndent

stripIndentは複数行文字列から一番浅いインデント分の空白を取り除きます。
たとえば、次のような文字列があるとします。

  test
    test
  test

ここからインデント空白を取り除くと次のようになります。

test
  test
test

ただし、末尾に改行文字がある場合はインデント0が一番浅いとみなされます。

jshell> """
   ...>   test
   ...>     test""".stripIndent()
$28 ==> "test\n  test"

jshell> """
   ...>   test
   ...>     test
   ...> """.stripIndent()
$29 ==> "  test\n    test\n"
translateEscapes
jshell> "test\\n\\ttest"
$10 ==> "test\\n\\ttest"

jshell> System.out.println($10)
test\n\ttest

jshell> System.out.println($10.translateEscapes())
test
        test
formatted
jshell> "みかんは%dえん".formatted(100*11/10)
$14 ==> "みかんは110えん"

jshell> String.format("みかんは%dえん", 100*11/10)
$15 ==> "みかんは110えん"

JEP 375: Pattern Matching for instanceof (Second Preview)

パターンマッチングです。
instanceofを使ったパターンマッチが14でプレビューとして入りましたが、そのまま変更なしにセカンドプレビューとしてJava 15に入っています。

値 instanceof パターンで、値をマッチさせることができます。
パターンは、定数か変数定義です。変数定義の場合には、型が一致していた場合にtrueになりその変数に値が割り当てられます。

if (x instanceof Integer i) {
    // can use i here
}

switchで使えるようになれば便利ですが、これは別のJEPで定義されていて、Java 16以降に持ち越されています。
JEP draft: Pattern matching for switch (Preview)

API

JavaDocはこちら。
Overview (Java SE 15 & JDK 15)
変更点はこちら。
DRAFT: API Differences Between Java SE 14 (build 36) & Java SE 15 (build 36)

JEPでは次のようなものがあります。

JEP 371: Hidden Classes
JEP 374: Disable and Deprecate Biased Locking
JEP 373: Reimplement the Legacy DatagramSocket API
JEP 339: Edwards-Curve Digital Signature Algorithm (EdDSA)
JEP 383: Foreign-Memory Access API (Second Incubator)
JEP 372: Remove the Nashorn JavaScript Engine
JEP 385: Deprecate RMI Activation for Removal

JEPになってないものでは、Unicode 13への対応やMath.exactAbsの追加があります。

JEP 371: Hidden Classes

Javaでは実行時にクラスを作成することがよく行われます。
たとえばラムダ式では実行時に該当するインタフェースを実装したクラスが生成されます。ほかにもSpringなどのフレームワークではサブクラスを生成してAOPを実現するといったことが行われています。
現状ではそのように作成されたクラスでもリフレクション経由で外部から利用することができていますが、Hidden Classとすることで利用できないようにします。
そうすることで、生成されたクラスが利用される範囲を限定でき、クラスアンロードを効率よく行うことができるようになります。
また、UnsafeのdefineAnonymousClassの代替として使えるようになるので、このdefineAnonymousClassをdeprecatedにします。

JEP 374: Disable and Deprecate Biased Locking

競合のないロックのオーバーヘッドを削減する。Synchronizeされて多数のスレッドから呼ばれるCompare-and-Swap操作のパフォーマンスを改善する。
バイアスロッキングが導入された効果は今日では明らかではなく、最近のプロセッサではアトミックな操作のパフォーマンスもあがっている
バイアスロックが効果があるのは古いアプリケーションで、VectorやHashtableのようなsynchronizeを毎回行うコレクションを使っている。
スレッドプールキューやワーカースレッドを使うような最近のアプリケーションではバイアスロックを無効にしたほうがパフォーマンスがよい
バイアスロックには複雑なコードが必要で、他の部分にも影響している。
コードの理解が難しくなり、設計変更の障害になっている。
今回は無効にして、そのうち削除する
-XX:+UseBiasedLockingで有効にできます。

JEP 373: Reimplement the Legacy DatagramSocket API

Java 13でTCPソケット用のAPIが実装されなおしています。それに引き続いて、Java 15ではUDPソケットをより簡潔で現代的に実装しなおしました。
このことでメンテナンスが容易になるとともに、プロジェクトLoomで開発されている仮想スレッドに対応しやすくなっています。

JEP 339: Edwards-Curve Digital Signature Algorithm (EdDSA)

エドワーズ暗号デジタル署名の実装です。
TLS1.3での鍵交換に使えるのかな。
RFC 8032の実装ということになります。
https://tex2e.github.io/rfc-translater/html/rfc8032.html

JEP 372: Remove the Nashorn JavaScript Engine

NashornはJava 8でRhinoに代わるJavaScriptエンジンとして入りました。
JEP 174: Nashorn JavaScript Engine

しかし、JavaScriptエンジンのメンテナンスをOpenJDKとして行うのは重いということでJava 11でDeprecatedになっていましたが、15で削除されます。
JEP 335: Deprecate the Nashorn JavaScript Engine

代替は用意されていませんが、JavaScriptエンジンが必要になったときにはGraalVMで動くGraalJSを使いましょうということのようです。
JavaScriptエンジンが必要ならOpenJDKではなくGraalVMを使いましょうということになりますが、OpenJDKで動かすこともできます。
OpenJDKでGraalJSを動かす方法はこちら。
Improve React.js Server-Side Rendering by 150% with GraalVM – Logico Inside

JEP 383: Foreign-Memory Access API (Second Incubator)

ヒープ外のメモリをアクセスする方法としては、ByteBufferを使う方法やUnsafeを使う方法、JNIを使う方法がありますが、それぞれ一長一短があります。
ByteBufferでdirect bufferを使う場合、intで扱える範囲の2GBまでに制限されたり、メモリの解放がGCに依存したりします。
Unsafeの場合は、性能もいいのですが、名前が示すとおり安全ではなく、解放済みのメモリにアクセスすればJVMがクラッシュします。
JNIを使うとCコードを書く必要があり、性能もよくないです。

ということで、ヒープ外のメモリを直接扱うAPIがJava 14でインキュベータモジュールとして導入されたわけです。そしてJava 15でセカンドインキュベータになっています。
次のようなコードになります。

VarHandle intHandle = MemoryHandles.varHandle(int.class);

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
   MemoryAddress base = segment.baseAddress();
   for (int i = 0 ; i < 25 ; i++) {
        intHandle.set(base.offset(i * 4), i);
   }
}

コンパイル、実行では--add-modules jdk.incubator.Foreignを付けてモジュールを読み込む必要があります。

Java 14にくらべてMemoryHandlesからVarHandleを得るメソッドが拡充してますね。JShellでの補完は次のようになっています。

jshell> MemoryHandles.
asAddressVarHandle(   asUnsigned(           class                 collectCoordinates(   dropCoordinates(
filterCoordinates(    filterValue(          insertCoordinates(    permuteCoordinates(   varHandle(
withOffset(           withStride(

Java 14では次の3つのメソッドしかありませんでした。

jshell> MemoryHandles.
class         varHandle(    withOffset(   withStride(

JEP 385: Deprecate RMI Activation for Removal

RMI Activationは時代遅れってことで、削除に向けてまずはDeprecatedになりました。
Java Remote Method Invocation: 7 - Remote Object Activation

Math.absExact

絶対値を取得するMath.absInteger.MIN_VALUEを渡すと、そのままInteger.MIN_VALUEが返ってきてしまいます。
Math.absExactではInteger.MIN_VALUEを渡すと例外が投げられます。

jshell> Math.abs(Integer.MIN_VALUE)
$2 ==> -2147483648

jshell> Math.absExact(Integer.MIN_VALUE)
|  例外java.lang.ArithmeticException: Overflow to represent absolute value of Integer.MIN_VALUE
|        at Math.absExact (Math.java:1393)
|        at (#3:1)

http://mail.openjdk.java.net/pipermail/jdk-dev/2020-March/004175.html

UNICODE 13

UNICODE 13に対応しています。
[JDK-8239504] Support for Unicode 13.0 - Java Bug System

Java 11で導入されたUNICODE 10のときはJEPがあったけど、それ以来JEPは作られない感じ

JDK Flight Recorder

JFRでProcess Startイベントとかダイレクトメモリの統計がとれるようになった?
[JDK-8238665] Add JFR event for direct memory statistics - Java Bug System
[JDK-8222000] JFR: Process start event - Java Bug System

JVM

JEP 377: ZGC: A Scalable Low-Latency Garbage Collector (Production)
JEP 379: Shenandoah: A Low-Pause-Time Garbage Collector (Production)

JEP 377: ZGC: A Scalable Low-Latency Garbage Collector (Production)

テラバイト級のメモリに対応した低遅延のGCです。
Java 11でExperimentalとして導入されました。
JEP 333: ZGC: A Scalable Low-Latency Garbage Collector (Experimental)

当初はLinuxのみの対応でしたが、Java 14でmacOSとWindowsに対応しました。
JEP 364: ZGC on macOS (Experimental)
JEP 365: ZGC on Windows (Experimental)

そして今回プロダクションになりました。

JEP 379: Shenandoah: A Low-Pause-Time Garbage Collector (Production)

G1GCを拡張して数百GBに対応した少停止時間のGCです。
Java 12でExperimentalとして導入されました。
JEP 189: Shenandoah: A Low-Pause-Time Garbage Collector (Experimental)

今回プロダクションになりました。

ツール

JEPにはなっていませんが、rmicが削除されています。

rmicの削除

なぜかJEPになっていないですが、RMIのインタフェースからスタブクラスを作るためのrmicが削除されました。だいぶ前からスタブクラスを作る必要はなくなっていたので、基本的に使う必要なくなっていました。
Java 13のときにdeprecatedになっていたっぽい。
http://mail.openjdk.java.net/pipermail/core-libs-dev/2020-April/065622.html

OpenJDK

OpenJDK自体の変更としては次のJEPがあります。
JEP 381: Remove the Solaris and SPARC Ports

JEP 381: Remove the Solaris and SPARC Ports

Solaris/SPARC向けのソースコードを削除して、ビルドからも外されました。
OpenJDKではSolaris/SPARCに対応しなくなりますが、他のディストリビューションでは対応するかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む