20200520のJavaに関する記事は11件です。

Javaの例外処理?

0. 始めに

例によって、Javaの例外が分かっていなかったので、まとめてみた。
今回は(こそ?)自分用メモとして記事を書く。

1. 例外処理とは?

 例外の定義も色々あったので、参考1 p.264 から引用したところ、

 Javaにおけるプログラムの実行中に発生するトラブルには、大きく分けて2つの種類があります。
実行環境のトラブルなど、プログラムからは対処しようのない事態を表すエラーと、プログラムが対処できる例外の2つです。

 例外はさらに、検査例外と非検査例外に分かれます。検査例外とは、例外処理を記述したかをコンパイラが検査する例外を指します。
もう一方の非検査例外は、例外処理を記述したかどうかをコンパイラが検査しない例外を指します。

とのこと。要するに3つに分けられる。
 さらにそれぞれの例外に応じた例外クラスが存在し、その関係は下記の通り。

スライド1.PNG

(サブクラスも載せたい)

2. 各例外の基本

①検査例外

 検査例外のクラスであるExceptionを継承している例外クラスは、try-catchやthrows句で宣言するなどの処理が必須になる。
ここで言う検査とは、catchやthrowのこと。検査例外を検査していない場合はコンパイルエラーになる。

②非検査例外

 Exceptionのサブクラスでも、RuntimeExceptionを継承していれば非検査例外として扱われ、try-catchなどの処理は不要になる。
もちろん、例外処理を記載しても問題ない。

③エラー

 メモリ不足やファイルの読み込みに失敗したなどの例外。非検査例外と同じく、例外処理とかしなくていい。

3. 例外処理書き方あれこれ

3.1 標準的な書き方

①Exceptionをキャッチする
明示的に例外をスローするときは、throwを付ける(検査例外のみ?検証する)
catchブロックのパラメータはThrowableなので、全ての例外をキャッチできる。

try {  // 例外が発生するかもしれない処理をくくる
  throw new Exception();
} catch (Exception e) {
  ...  // try 中で例外が発生した時に処理される
} finally {
  // 例外が発生してもしなくても実行される
}

②Exceptionをスローする メソッド宣言にthrowsを使う。

private void hogehoge() throws Exception {
  throw new Exception();
}

3.2 マルチキャッチ

キャッチする例外クラスを複数指定できる。参考2より引用させていただきました。

// Java6以前の場合
try {
  . . . 
} catch (IOException ex) {
     logger.log(ex);
     throw ex;
} catch (SQLException ex) {
     logger.log(ex);
     throw ex;
}

// Java7以降の場合
try {
  . . . 
} catch (IOException|SQLException ex) {
    logger.log(ex);
    throw ex;
}

3.3 検査例外のスローを記載したら、絶対にキャッチも書く

検査例外をスロー宣言しているメソッドは、キャッチするか再スローしないとコンパイルエラーになる。。。

public void method() throws Exception {}

// case1
try {
  method();
} catch (Exception e) {  // catchが無いとエラー
  ...
} 

// case 2
public void method2() throws Exception {  // 再スローしないとエラー
  method();
}

4. 例外発生後の挙動

4.1 for文中で例外が発生したら、それ以降のループ処理はされない

参考1 p.349より引用。
i = 1の時にStirngIndexOutOfboundsExceptionが発生するので、i > 1のループ処理は実行されない。

String[] array = {"abcde", "fgh", "ijk"};
String[] array2 = new String[3];
int i = 0;
try {
  for (String s: array) {
    array2[i] = s.substring(1, 4);
    i++;
  }
} catch (Exception e) {
  System.out.println("Error");
}

for (String s: array2) {
  System.out.println(s);
}

// 実行結果
// Error   エラーをキャッチしたことによる出力
// bcd     array2[0]
// null    array2[1]
// null    array2[2]

4.2 例外をスローしているメソッドをオーバーライドする時の注意

 例外をスローしているメソッドをオーバーライドした場合、そのメソッドには以下の制約が追加される(参考3 p. 299を元に一部表現を変えて引用)。

 ①サブクラスのメソッドでスローできる例外は、スーパークラスのメソッドがスローする例外と同じかそのサブクラスの例外
 ②スーパークラスのメソッドがどんな例外をスローしていようと、サブクラスのメソッドではRuntimeExceptionかそのサブクラスの例外としてスロー出来る(実行時例外に倒せる)
 ③そもそも、スーパークラスのメソッドが例外をスローしているからと言って、サブクラスのメソッドでスローするのが必須ではない

具体例を出す(参考3 p.300より引用)。ここで、NG:コンパイルエラーとする。

class Super { void method() throws IOException {} }

class SubA { void method() }  // OK 例外をスローしなくても良い
class SubB { void method() throws FileNotFoundException {} }  // OK IOExceptionのサブクラスでスローしている
class SubC { void method() throws Exception {} }  // NG IOExceptionのスーパークラスでスローしている
class SubD { void method() throws ClassNotFoundException {} }  // NG IOExceptionと継承関係にない例外クラスでスローしている
class SubE { void method() throws RuntimeException {} }  // OK 実行時例外に倒している

5. 参考

1.志賀澄人 (2019) 「徹底攻略 Java SE 8 Silver 問題集 [1Z0-808] 対応」 ㈱インプレス 発行
2.【社内勉強会】Javaの例外処理(2017/04/26)
3.山本道子 (2015) Java プログラマ Silver SE 7(第5刷発行) ㈱翔泳社 発行

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

部分和問題から再帰的なプロセスを網羅する

0. はじめに

深さ優先探索を行うときには、再帰的な書き方をしなければいけない場面が多々あると思います。
実際、他人の書いたコードを見てみると中でreurn文が多用されており、どの順番でコードが動いているのか私には全然分かりませんでした。そこで今回は、具体的にコードの動きを観察します。

1 . 部分和問題

実際の問題はけんちょんさんの再帰関数を学ぶと、どんな世界が広がるかの記事の3-1を参考にしてください。

ちなみにjavaでのコードはこうなります。

import java.util.Scanner;
public class Main {
    static int indexA = 0;
    static int indexB = 0;
    static boolean func(int i, int x, int[] a) {


        //ベースケース

        if(i == 0) {
            if(x == 0) {
                return true;
            }else {
                return false;
            }
        }


        //a[i-1]を選ばない場合(func(i-1,x,a)がokならok)

        if(func(i-1,x,a)) {
            return true;
        }

        System.out.println("indexA:"+(++indexA)+" i:"+i+" x:"+x);

        //a[i-1]を選ぶ場合(func(i-1,x-a[i-1],a)がokならok)
        if(func(i-1,x-a[i-1],a)) {
            return true;
        }

        System.out.println("indexB:"+(++indexB)+" i:"+i+" x:"+x);

        //どっちもダメだったらダメ
        return false;



    }


    public static void main(String[] args) {
        Scanner stdIn = new Scanner(System.in);

        System.out.print("コインの枚数:");

        int n = stdIn.nextInt();


        int[] a = new int[n];

        System.out.println("コインの価値:");

        for(int i = 0; i < n; i++) {
            a[i] = stdIn.nextInt();
        }

        System.out.print("金額の設定:");
        int x = stdIn.nextInt();

        //再帰的に書く

        if(func(n,x,a)) {
            System.out.println("Yes");
        }else {
            System.out.println("No");
        }



    }

}

1.1 n = 3, a =(2,3,4) x = 6 のとき

image.png

なぜこのような順番でコードが出てきたかを以下の図で確認できます。
再帰図.jpg

〇の中に書かれた順番で動いているのが分かると思います。

※ここからさらに詳細な動きを書くので、理解できた方は次の見出しまでスキップしてください。

iが0のとき、xが0でない場合は、すべてfalseが返るので、①のfunc(0,6)はfalseが返る、そのためfunc(1,6)のとき、の最初の1個目のif文はif(false)であるため、何も何も表示されない。

if(func(i-1,x,a)) {  //ここが表示されない

            System.out.println("選ばない"+" i:"+i+" x:"+x);

            return true;
        }

次に, i==1,x == 6を今見ているので、表示されているのが分かる。

System.out.println("indexA:"+(++indexA)+" i:"+i+" x:"+x+"\n"); //1行目に ""indexA:1 i:1 x:6""と表示されている

この後、func(1,6)の2つ目のif文の②が動作しており、func(0,4)はfalseであるため、このif文も表示されない。

if(func(i-1,x-a[i-1],a)) { //ここが表示されない

            System.out.println("選ぶ"+" i:"+i+" x:"+x);

            return true;
        }

そして、そのif文の後のprintlnが動作していることが分かる。(②のindexB:1 i:1 x:6が表示されている)

System.out.println("indexB:"+(++indexB)+" i:"+i+" x:"+x+"\n"); 

func(1,6)のどちらのif文もfalseであったため、//どっちもダメだったらダメと書かれた行のreturn falseが返ってくる。

ここでやっとfunc(2,6)の最初のif文が終わったことを意味するのである。

再帰はベースケースまでたどり着いてから戻り値がどんどん返ってくるイメージを持つと分かりやすいだろう。

1.2 n = 3, a =(2,3,4) x = 10 のとき

ここで絶対x=10の目標には達しないときの動きも確認する。

image.png

この画像から以下のような順番で動いていることが分かる。
再帰図 (1).jpg

深さ優先探索が行われていることがよくわかると思う。先程と同じようなコードの動きをしているので説明は省略する。

2 まとめ

再起関数を用いるときは、まず ベースとなるケースを考えて循環を止める。
その後、if文内で再帰を行い、trueになるケースが一つでも存在するかを確かめた。

bit全探索もとる、とらないの2^n通りのケースを考えられるため、今回の問題であればbit全探索でも再帰的に書いても良いとは思います。
しかし、if文でさらに分岐した条件を書くことができる点が再帰のさらなる魅力だと思います。(まだ使ったことはありませんが...。)

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

JavaのBigDecimal

BigDecimalについて少しまとめた

BigDecimalはJavaのAPIの一つで、
普通の数値型は2進数で処理されるため、意図しない数値が返って来ることがある。
しかし、BigDecimalを使うことで、10進数として扱うことができる。

Number.java
import java.math.BigDecimal;
import java.math.RoundingMode;

public class Number {
  public static void main(String[] args) {
    BigDecimal number1 = new BigDecimal("0.2");     //引数は""で囲む
    BigDecimal number2 = new BigDecimal("4");
    System.out.println(number1);

    System.out.println(BigDecimal.ZERO);    //0
    System.out.println(BigDecimal.ONE);     //1
    System.out.println(BigDecimal.TEN);     //10

    System.out.println(number1.add(number2));         
    //加算 number1 + number2
    System.out.println(number1.subtract(number2));  
    //減算 number1 - number2
    System.out.println(number1.multiply(number2));  
    //乗算 number1 * number2
    System.out.println(number1.divide(number2, 3, RoundingMode.UP));  
    //除算 number1 / number2, 小数点第3位まで表示, 切り上げ
    //RoundingMode.は切り上げ、切り捨て、四捨五入などがある。

    BigDecimal number3 = new BigDecimal("0.22");
    BigDecimal value1 = number3.scaleByPowerOfTen(2);  //10の2乗
    System.out.println(value1);  //22

    BigDecimal value2 = number3.scaleByPowerOfTen(-2);  //10の-2乗
    System.out.println(value2);  //0.0022

    BigDecimal value3 = number3.negate();  //マイナス化
    System.out.println(value3);   //-0.22


  }
}

以上。
まだまだたくさんの機能があるので試してみたいです。

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

Javaで1件だけ検索するSQL文の結果の条件分岐

最近 JDBC (Java Database Connectivity) の勉強を少しだけしました。
データベースから1件だけ検索し、その結果を取得するときの条件分岐の仕方で困ったのでメモとして残しておきます。

Connection con = null;
Statement stmt = null;
ResultSet res = null;
String sql = "SELECT * FROM DATABASE WHERE COLUMN = 〇〇";

try{
    con.DriveManager.getConnection(url, user, password);
    stmt.con.createStatement();
    res.stmt.executeQuery(sql);

    if(res.next()){
        //結果があるときの処理
    }else{
        //結果がない時の処理
}catch(SQLException e){
    //SQL文が間違っていた時などの処理
}finally{
    //クローズ処理など
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

java(コンストラクタ)

コンストラクの定義方法

①メソッド名がクラス名と完全に等しい
②メソッド宣言に戻り値が記述されていない(voidもダメ)

生まれた直後の動作を定義したHeroクラス

public class Hero {
  String name;
  int hp;
  public Hero() {   //コントラストの定義
    this.hp = 100;  // hpフィールドを100で初期化
  }
}

newされた直後に自動的に実行される処理を書いたメソッド

コストラクタが定義されたHeroを生み出す

public class Main {
  public static void main(String[] args) {
    Hero h = new Hero();      //インスタン生成と同時にHPに100が代入されている
    h.hp = 100;               //いらない

    System.out.println(h.hp); //100と表示される
  }
}

コンストラクタで引数を追加情報として受け取る

public class Hero {
  String name;
  int hp;
  public Hero(String name) {   //引数として文字列を1つ受け取る
    this.hp = 100;             // hpフィールドを100で初期化
    this.name = name;          //引数の値でnameフィールドを初期化
  }
}

newで引数を渡す

public class Main {
  public static void main(String[] args) {
    Hero h = new Hero("勇者");      //インスタン生成後、JVMによって自動的にコンストラクタが実行され、引数として"勇者"が利用される

    System.out.println(h.hp);   //100と表示される
    System.out.println(h.name); //勇者と表示される
  }
}

コンストラクタのオーバーロード

public class Hero {
  String name;
  int hp;
  public Hero(String name) {   //コンストラスタ①           
    this.name = name;          
  }
  public Hero() {              //コンストラクタ②
    this.name = "勇者2"        //"勇者2"を設定
  }
}

コンストラスタをオーバーロードしたクラスの使用

public class Main {
  public static void main(String[] args) {
    Hero h = new Hero("勇者");      //文字列引数があるのでコンストラスタ①が呼び出される
    System.out.println(h.name);    //勇者と表示される

    Hero h2 = new Hero();          //引数がないのでコンストラクタ②が呼び出される
    System.out.println(h2.name);   //勇者2と表示される
  }
}

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

JavaはExcelの隣接する行に異なる背景色を設定します

Excelテーブルを作成するとき、データテーブルの2つの隣接する行を異なる背景色で塗りつぶすことにより、各行のデータをより明確に見せ、行の読み違いを避け、Excelテーブルの美観を向上させることがで。この記事では、JavaプログラムでExcelの奇数行と偶数行に代替の背景色を設定する方法を紹介します。

使用ツール: Free Spire.XLS for Java(無料版)

JARファイルのインポート方法
方法1: Free Spire.XLS for Javaパッケージをダウンロードして解凍し、Spire.Xls.jarパッケージをlibフォルダーからJavaアプリケーションにインポートします。

方法2: mavenを使用している場合は、pom.xmlファイルに次の依存関係を追加する必要があります。

<repositories>
        <repository>
            <id>com.e-iceblue</id>
            <name>e-iceblue</name>
            <url>http://repo.e-iceblue.com/nexus/content/groups/public/</url>
        </repository>
</repositories>
<dependencies>
    <dependency>
        <groupId>e-iceblue</groupId>
        <artifactId>spire.xls.free</artifactId>
        <version>2.2.0</version>
    </dependency>
</dependencies>

Javaコード例:

import com.spire.xls.*;

import java.awt.*;

public class ConditionalFormatting {

    public static void main(String[] args) {

        //Workbookオブジェクトを作成する
        Workbook workbook = new Workbook();

        //Excelドキュメントを読み込む
        workbook.loadFromFile("test.xlsx");

        //ワークシートを入手する
        Worksheet sheet = workbook.getWorksheets().get(0);

        //データ領域を取得する
        CellRange dataRange = sheet.getAllocatedRange();

        //条件付き書式を使用して、偶数行の背景色を薄い灰色に設定します
        ConditionalFormatWrapper format1 = dataRange.getConditionalFormats().addCondition();
        format1.setFirstFormula("=MOD(ROW(),2)=0");
        format1.setFormatType(ConditionalFormatType.Formula);
        format1.setBackColor(Color.lightGray);

        //条件付き書式を使用して、奇数行の背景色を黄色に設定します
        ConditionalFormatWrapper format2 = dataRange.getConditionalFormats().addCondition();
        format2.setFirstFormula("=MOD(ROW(),2)=1");
        format2.setFormatType(ConditionalFormatType.Formula);
        format2.setBackColor(Color.yellow);

        //ドキュメントを保存します
        workbook.saveToFile("AlternateColor.xlsx", ExcelVersion.Version2016);
    }
}

出力結果:
xl.jpg

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

java(クラス型をフィールドに用いる)

has-aの関係

Swordクラスを定義

public class Sword {
  String name;
  int damage;
}

Heroクラスを定義

public class Hero {
  String name;
  int hp;
  Sword sword;                //剣の情報
  public void attack() {
    System.out.println(this.name + "攻撃した!");
    System.out.println("敵に5ポイントのダメージを与えた!");
  } 
}

int型、String型ではなく、Sword型をフィールドにクラス型の変数を宣言することもできる。
has-aの関係
Hero has-a Sword(勇者は剣を持っている)

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

【JPA】ManyToMany関係の交差テーブルにキー以外の項目を持った場合の保存方法

はじめに

交差テーブルを利用したマッピングには毎回、頭を悩まされます。
今回は、交差テーブルに複合キー以外を持った場合の登録方法をご紹介します。

複合キーのみの登録

まず、以下のような関係をもったエンティティがあるとします。

1.png

この場合の保存方法は以下のようになります。

ユーザマスタ
@Getter
@NoArgsConstructor
@EqualsAndHashCode(of = {"id"})
@Entity
@Table
public class User implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String status;

    @ManyToMany
    @JoinTable(name = "user_service",joinColumns = @JoinColumn(name="user_id"), referencedColumnName="id"
              inverseJoinColumns = @JoinColumn(name="service_id", referencedColumnName="id"))
    private List<Service> services = new ArrayList<>();

    public User(String name,String status){
        this.name = name;
        this.status = status;
    }

    // 利用するサービスを追加します
    public void addService(Service service){
        services.add(service);
    }
}
サービスマスタ
@Getter
@NoArgsConstructor
@EqualsAndHashCode(of = {"id"})
@Entity
@Table
public class Service implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String status;

    public Service(String name,String status) {
        this.name = name;
        this.status = status;
    }
}
登録処理
User user = entityManager.find(User.class,<ユーザID>);
Service service = entityManager.find(Service.class,<利用するサービスID>);
user.addService(service);
entityManager.persist(user);

複合キー以外の項目がある場合の登録

利用サービステーブルに「利用開始日」を追加します。

1.png

この場合、@JoinTableのみでは利用開始日の保存ができないので、利用サービスとしてエンティティクラスを作成することになります。

利用サービス
@Getter
@NoArgsConstructor
@EqualsAndHashCode(of = {"id"})
@Entity
@Table(name = "user_service")
public class UserService implements Serializable {

    @EmbeddedId
    private PrimaryKey pk;

    @Column(name = "begin_date")
    private LocalDate beginDate;

    @ManyToOne
    @MapsId("userId")
    private User user;

    @ManyToOne
    @MapsId("serviceId")
    private Service service;

    public UserService(User user, Service service,LocalDate beginDate) {
        this.user = user;
        this.service = service;
        this.beginDate = beginDate;
    }

    @Getter
    @NoArgsConstructor
    @Embeddable
    public static class PrimaryKey implements Serializable {

        @Column(name = "user_id")
        private Long userId;

        @Column(name = "service_id")
        private Long serviceId;
    }
}

ここで重要なのは@MapsIdです。引数にマッピングしたいPrimaryKeyクラスで定義したフィールド名を指定します。これによって、エンティティクラスと複合キーが関連づけられます。

ユーザマスタも修正します。元々、ユーザマスタから利用しているサービスを取得できたのですが、
利用サービスクラス経由で取得するようにします。

ユーザマスタ
@Getter
@NoArgsConstructor
@EqualsAndHashCode(of = {"id"})
@Entity
@Table
public class User implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String status;

    @OneToMany(mappedBy = "user")
    private List<UserService> services = new ArrayList<>();

    public User(String name,String status){
        this.name = name;
        this.status = status;
    }

    // 利用するサービスを追加します
    public void addService(Service service){
        services.add(new UserService(this, service, LocalDate.now()));
    }

    // 交差テーブルのリストではなく、サービスのリストを返却してあげる
    public List<Service> getServices(){
        return services.stream().map(UserService::getService).collect(Collectors.toList());
    }
}
登録処理サンプル
User user = entityManager.find(User.class,<ユーザID>);
Service service = entityManager.find(Service.class,<利用するサービスID>);
user.addServices(service);
entityManager.persist(user);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

単体テストを書くためにstaticメソッドから脱却しよう ~DI編~

概要

前回の単体テストを書くためにstaticメソッドから脱却しよう~原則編~では、下記の2点を説明した。

  1. 実行環境により結果が変わるメソッドをstaticにしてはいけない
  2. 原則を破っている既存コードも、ステップを分けて影響範囲を小さくしながら修正できる
    • たとえば、メソッドをラップすることで解決できる

今回の記事では、既存コードを修正するための他の方法として、他のクラスのstaticメソッドを呼び出している場合を例に、DI(Dependency Injection - 依存性の注入)という手法を紹介する。

なお、この記事では、Spring FrameworkなどのDIコンテナのフレームワークには言及しない。

問題

下記のLegacyClient#execute()のようなメソッドに対して、うまく単体テストを書くことができない。

メソッド内の変数のhostportの値を代入する処理では、GlobalControl クラスのインスタンスメソッドにアクセスするのだが、データベースへのアクセスを必要としている。このため、アクセスが不可能な環境から実行した際には、例外が発生してしまう。

LegacyClient.java
public class LegacyClient {
    public void execute() {
        // ここにテスト対象のコードが入る

        // 問題のコード
        String host = GlobalControl.getInstance().get("host", "localhost");
        int port = GlobalControl.getInstance().getInt("port", 8080);

        // ここにテスト対象のコードが入る
    }
}
GlobalControl.java
public final class GlobalControl {
    private static final GlobalControl instance = new GlobalControl();

    public static GlobalControl getInstance() {
        return instance;
    }

    public String get(String key, String defaultValue) {
        // データベースにアクセスするような処理
    }

    public int getInt(String key, int defaultValue) {
        // データベースにアクセスするような処理
    }
}

ラップメソッドには欠点がある

前回の記事では、メソッドをラップして解決する方法を紹介した。今回のケースも、その方法を適用することはできる。

しかし、この方法だと、複数のラップメソッドを作る必要があり、コードが冗長になる。

複数のラップメソッドを作る必要がある
import com.google.common.annotations.VisibleForTesting;

public class LegacyClient {
    public void execute() {
        // ここにテスト対象のコードが入る

        // 問題のメソッドをラップした
        String host = get("host", "localhost");
        int port = getInt("port", 8080);

        // ここにテスト対象のコードが入る
    }

    // テストクラスで上書きするラップメソッド1
    @VisibleForTesting
    String get(String key, String defaultValue) {
        return GlobalControl.getInstance().get(key, defaultValue);
    }

    // テストクラスで上書きするラップメソッド2
    @VisibleForTesting
    int getInt(String key, int defaultValue) {
        return GlobalControl.getInstance().getInt(key, defaultValue);
    }
}

DIによる解決方法

このため、今回はDIによる解決を試みる。

前回と同じように、commitを分けて、ステップを追って修正していこう。

ステップ0: 失敗する単体テストを用意する

失敗する単体テスト
public class LegacyClientTest {
    @Test
    public void test() {
        new LegacyClient().execute();
    }
}

ステップ1: 問題のクラスのインタフェースを抽出する

単体テストが用意できたところで、実際の修正に入る。

(IDEのリファクタリング機能などを利用して、)問題のGlobalControlクラスで宣言されているインスタンスメソッドをインタフェースに抽出する。

GlobalControlクラスから抽出したインタフェース
public interface GlobalControlHolder {
    public String get(String key, String defaultValue);
    public int getInt(String key, int defaultValue);
}
抽出したインタフェースを付与
public final class GlobalControl implements GlobalControlHolder {
    private static final GlobalControl instance = new GlobalControl();

    public static GlobalControl getInstance() {
        return instance;
    }

    @Override
    public String get(String key, String defaultValue) {
        // データベースにアクセスするような処理
    }

    @Override
    public int getInt(String key, int defaultValue) {
        // データベースにアクセスするような処理
    }
}

ステップ2: staticメソッドの呼び出しをインスタンス作成時に変更する

GlobalControl インスタンスをメソッド内で取得するのではなく、インスタンス作成時(コンストラクタ)に取得するようにする。また、このときのメンバ変数は先ほど抽出したインタフェースGlobalControlHolderで宣言するようにする。

そして、executeメソッドでは、メンバ変数globalControlを参照するように書き換える。

LegacyClient.java
public class LegacyClient {
    // インスタンスをローカルからメンバ変数に移し、保持する
    private final GlobalControlHolder globalControl;

    public LegacyClient() {
        this.globalControl = GlobalControl.getInstance();
    }

    public void execute() {
        // ここにテスト対象のコードが入る

        // メンバ変数 "globalControl" を参照するように書き換え
        String host = globalControl.get("host", "localhost");
        int port = globalControl.getInt("port", 8080);

        // ここにテスト対象のコードが入る
    }
}

ステップ3: DI用のコンストラクタを追加する

ステップ2で作成したコンストラクタは、コンストラクタ内でGlobalControlHolderインスタンスを取得していた。それに加えて、GlobalControlHolderを引数に取るような、別のコンストラクタを追加する。

LegacyClient.java
public class LegacyClient {
    private final GlobalControlHolder globalControl;

    public LegacyClient() {
        this.globalControl = GlobalControl.getInstance();
    }

    // DI用のコンストラクタ
    public LegacyClient(GlobalControlHolder globalControl) {
        this.globalControl = globalControl;
    }

    public void execute() {
        // ここにテスト対象のコードが入る

        String host = globalControl.get("host", "localhost");
        int port = globalControl.getInt("port", 8080);

        // ここにテスト対象のコードが入る
    }
}

これにより、呼び出し元からGlobalControlインスタンスを渡せるようになった。

コンストラクタを利用して、呼び出し元から依存モジュール(今回はGlobalControlがあたる)を注入することをコンストラクタインジェクションと呼ぶ。

ステップ4: テストコードで依存性を注入する

テストコードでは、GlobalControlHolderインタフェースを実装したMockクラスを作成し、それをnewしてLegacyClientのコンストラクタに渡すようにする。

テストコード用のMockクラス
public class MockGlobalControl implements GlobalControlHolder {
    @Override
    public String get(String key, String defaultValue) {
        // デフォルト値を返す
        return defaultValue;
    }
    @Override
    public int getInt(String key, int defaultValue) {
        // デフォルト値を返す
        return defaultValue;
    }
}
修正したテストコード
public class LegacyClientTest {
    @Test
    public void test() {
        // テストコード用のMockクラスを注入する
        new LegacyClient(new MockGlobalControl()).execute();
    }
}

これにより、単体テストでは、データベースにアクセスしない実装で依存モジュールを差し替えることができる。

まとめ

別クラスのstaticメソッドを呼び出している場合の解決は、DI(Dependency Injection - 依存性の注入)が有効である。

また、前回同様、ステップを分けて修正することで、リファクタリングによる影響範囲を限定しつつ単体テストを書くことができる。

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

単体テストを書くためにstaticメソッドから脱却しよう ~②DI編~

概要

前回の単体テストを書くためにstaticメソッドから脱却しよう ~①原則・ラップメソッド編~では、下記の2点を説明した。

  1. 実行環境により結果が変わるメソッドをstaticにしてはいけない
  2. 原則を破っている既存コードも、ステップを分けて影響範囲を小さくしながら修正できる
    • たとえば、メソッドをラップすることで解決できる

今回の記事では、既存コードを修正するための他の方法として、他のクラスのstaticメソッドを呼び出している場合を例に、DI(Dependency Injection - 依存性の注入)という手法を紹介する。

なお、この記事では、Spring FrameworkなどのDIコンテナのフレームワークには言及しない。

問題

下記のLegacyClient#execute()のようなメソッドに対して、うまく単体テストを書くことができない。

メソッド内の変数のhostportの値を代入する処理では、GlobalControl クラスのインスタンスメソッドにアクセスするのだが、データベースへのアクセスを必要としている。このため、アクセスが不可能な環境から実行した際には、例外が発生してしまう。

LegacyClient.java
public class LegacyClient {
    public void execute() {
        // ここにテスト対象のコードが入る

        // 問題のコード
        String host = GlobalControl.getInstance().get("host", "localhost");
        int port = GlobalControl.getInstance().getInt("port", 8080);

        // ここにテスト対象のコードが入る
    }
}
GlobalControl.java
public final class GlobalControl {
    private static final GlobalControl instance = new GlobalControl();

    public static GlobalControl getInstance() {
        return instance;
    }

    public String get(String key, String defaultValue) {
        // データベースにアクセスするような処理
    }

    public int getInt(String key, int defaultValue) {
        // データベースにアクセスするような処理
    }
}

ラップメソッドには欠点がある

前回の記事では、メソッドをラップして解決する方法を紹介した。今回のケースも、その方法を適用することはできる。

しかし、この方法だと、複数のラップメソッドを作る必要があり、コードが冗長になる。

複数のラップメソッドを作る必要がある
import com.google.common.annotations.VisibleForTesting;

public class LegacyClient {
    public void execute() {
        // ここにテスト対象のコードが入る

        // 問題のメソッドをラップした
        String host = get("host", "localhost");
        int port = getInt("port", 8080);

        // ここにテスト対象のコードが入る
    }

    // テストクラスで上書きするラップメソッド1
    @VisibleForTesting
    String get(String key, String defaultValue) {
        return GlobalControl.getInstance().get(key, defaultValue);
    }

    // テストクラスで上書きするラップメソッド2
    @VisibleForTesting
    int getInt(String key, int defaultValue) {
        return GlobalControl.getInstance().getInt(key, defaultValue);
    }
}

DIによる解決方法

このため、今回はDIによる解決を試みる。

前回と同じように、commitを分けて、ステップを追って修正していこう。

ステップ0: 失敗する単体テストを用意する

失敗する単体テスト
public class LegacyClientTest {
    @Test
    public void test() {
        new LegacyClient().execute();
    }
}

ステップ1: 問題のクラスのインタフェースを抽出する

単体テストが用意できたところで、実際の修正に入る。

(IDEのリファクタリング機能などを利用して、)問題のGlobalControlクラスで宣言されているインスタンスメソッドをインタフェースに抽出する。

GlobalControlクラスから抽出したインタフェース
public interface GlobalControlHolder {
    public String get(String key, String defaultValue);
    public int getInt(String key, int defaultValue);
}
抽出したインタフェースを付与
public final class GlobalControl implements GlobalControlHolder {
    private static final GlobalControl instance = new GlobalControl();

    public static GlobalControl getInstance() {
        return instance;
    }

    @Override
    public String get(String key, String defaultValue) {
        // データベースにアクセスするような処理
    }

    @Override
    public int getInt(String key, int defaultValue) {
        // データベースにアクセスするような処理
    }
}

ステップ2: staticメソッドの呼び出しをインスタンス作成時に変更する

GlobalControl インスタンスをメソッド内で取得するのではなく、インスタンス作成時(コンストラクタ)に取得するようにする。また、このときのメンバ変数は先ほど抽出したインタフェースGlobalControlHolderで宣言するようにする。

そして、executeメソッドでは、メンバ変数globalControlを参照するように書き換える。

LegacyClient.java
public class LegacyClient {
    // インスタンスをローカルからメンバ変数に移し、保持する
    private final GlobalControlHolder globalControl;

    public LegacyClient() {
        this.globalControl = GlobalControl.getInstance();
    }

    public void execute() {
        // ここにテスト対象のコードが入る

        // メンバ変数 "globalControl" を参照するように書き換え
        String host = globalControl.get("host", "localhost");
        int port = globalControl.getInt("port", 8080);

        // ここにテスト対象のコードが入る
    }
}

ステップ3: DI用のコンストラクタを追加する

ステップ2で作成したコンストラクタは、コンストラクタ内でGlobalControlHolderインスタンスを取得していた。それに加えて、GlobalControlHolderを引数に取るような、別のコンストラクタを追加する。

LegacyClient.java
public class LegacyClient {
    private final GlobalControlHolder globalControl;

    public LegacyClient() {
        this.globalControl = GlobalControl.getInstance();
    }

    // DI用のコンストラクタ
    public LegacyClient(GlobalControlHolder globalControl) {
        this.globalControl = globalControl;
    }

    public void execute() {
        // ここにテスト対象のコードが入る

        String host = globalControl.get("host", "localhost");
        int port = globalControl.getInt("port", 8080);

        // ここにテスト対象のコードが入る
    }
}

これにより、呼び出し元からGlobalControlインスタンスを渡せるようになった。

コンストラクタを利用して、呼び出し元から依存モジュール(今回はGlobalControlがあたる)を注入することをコンストラクタインジェクションと呼ぶ。

ステップ4: テストコードで依存性を注入する

テストコードでは、GlobalControlHolderインタフェースを実装したMockクラスを作成し、それをnewしてLegacyClientのコンストラクタに渡すようにする。

テストコード用のMockクラス
public class MockGlobalControl implements GlobalControlHolder {
    @Override
    public String get(String key, String defaultValue) {
        // デフォルト値を返す
        return defaultValue;
    }
    @Override
    public int getInt(String key, int defaultValue) {
        // デフォルト値を返す
        return defaultValue;
    }
}
修正したテストコード
public class LegacyClientTest {
    @Test
    public void test() {
        // テストコード用のMockクラスを注入する
        new LegacyClient(new MockGlobalControl()).execute();
    }
}

これにより、単体テストでは、データベースにアクセスしない実装で依存モジュールを差し替えることができる。

まとめ

別クラスのstaticメソッドを呼び出している場合の解決は、DI(Dependency Injection - 依存性の注入)が有効である。

また、前回同様、ステップを分けて修正することで、リファクタリングによる影響範囲を限定しつつ単体テストを書くことができる。

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

サーブレットでiPhoneアプリからのリクエストパラメータをうまく取得できなかった話

個人でiPhoneアプリを作成していたところ、getAttribute()とgetParameter()の違いを理解しておらず、
サーブレットでアプリからのリクエストをうまく取得できなかったのでメモとして残しておきます。

今回発生した事象はこんな感じ。

iPhoneアプリからリクエスト送信 
    ↓
サーブレットで受信(サーブレットに到達はするが、リクエスト内のパラメータが取得できない。。)

原因

request.getAttributeで値を取得しようとしていたのがダメでした。
getParameterで取得しないとダメみたいですね。。
(jspはgetAttributeで取得できていたので完全に盲点でした)

詳しい内容はこちらのサイトに記載してありました。
・getAttribute()とgetParameter()の違い

検証

理解を深めるためにサンプルプログラムで検証してみました。
iPhoneのtextFieldに入力された文字列をコンソールに表示するサンプルプログラムです。

・iPhone画面

 input.jpeg  result.png

結果

・コンソール画面

コンソール結果.png
やはりgetAttributeだと取得できていませんでした

ソース

・iPhone画面 (入力画面)

ViewController
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var testField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    //Servletに送信ボタンを押した際に実行
    @IBAction func goServlet(_ sender: Any) {
        self.performSegue(withIdentifier: "goResultView", sender: nil)
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

        print("prepare動作開始")
        //URLを設定
        guard let req_url = URL(string: "http://localhost:8080/Test/TestServlet")
            else{return}
        print("urlセット完了")

        //リクエストに必要な情報を宣言
        var req = URLRequest(url: req_url)
        print("リクエストの宣言")
        //POSTを指定
        req.httpMethod = "POST"
        //POSTするデータをBODYとして設定
        req.httpBody = "test=\(self.testField.text!)".data(using: .utf8)

        //sessionの作成
        let session = URLSession(configuration: .default,delegate: nil, delegateQueue: OperationQueue.main)
        print("sessionの作成")

                //リクエストをタスクとして登録
                let task = session.dataTask(with: req, completionHandler: {
                    (data, response ,error) in


                })
                //request送信
                task.resume()
            }

}

・iPhone画面 (結果画面)
※iPhoneの結果画面にはswiftコードを定義してません

・Java Servlet

TestServlet
package servlet;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet implementation class testServlet
 */
@WebServlet("/TestServlet")
public class TestServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    /**
     * @see HttpServlet#HttpServlet()
     */
    public TestServlet() {
        super();
        // TODO Auto-generated constructor stub
    }

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub
        response.getWriter().append("Served at: ").append(request.getContextPath());
    }

    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // TODO Auto-generated method stub

        request.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=UTF-8");

        System.out.println("サーブレットのPOSTメソッドに到達");
        System.out.println("getAttributeで受け取った場合:" + "iPhoneアプリから送られた文字列は" + request.getAttribute("test") + "です。");
        System.out.println("getParameterで受け取った場合:" + "iPhoneアプリから送られた文字列は" + request.getParameter("test") + "です。");
    }

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