20201023のJavaに関する記事は8件です。

期間の重複チェック(LocalTime, LocalDate, LocalDateTime)

毎回ロジックを考えている気がするのでメモしておく。

  • 期間の終了がnull (つまり開始からこの先ずっと対象期間) というパターンもよくあるので、それにも対応。
  • 「10:00 ~ 11:00」と「11:00 ~ 12:00」のような、期間1の終了と期間2の開始がぴったり一致している時も重複あり判定にしている。このパターンを重複なし判定にする場合は、 !start2.isAfter(end1)start2.isBefore(end1)に、 !end2.isBefore(start1)end2.isAfter(start1) にする。
 /**
   * 期間の重複チェック(LocalTime版)
   * 単一のポイントで期間が重なったときも重複したとみなす。
   * e.g.)
   *  期間1 => 10:00 ~ 11:00
   *  期間2 => 11:00 ~ 12:00
   *  結果 => 重複あり(true)
   *
   * @param start1 期間1の開始
   * @param end1 期間1の終了
   * @param start2 期間2の開始
   * @param end2 期間2の終了
   * @return 重複してたらtrue
   */
  def isOverlapped(start1: LocalTime, end1: LocalTime, start2: LocalTime, end2: LocalTime): Boolean =
    !start2.isAfter(end1) && !end2.isBefore(start1)

  /**
   * 期間の重複チェック(LocalTime版, 期間終了Option可)
   * 単一のポイントで期間が重なったときも重複したとみなす。
   * e.g.)
   *  期間1 => 10:00 ~ 11:00
   *  期間2 => 11:00 ~ 12:00
   *  結果 => 重複あり(true)
   *
   * @param start1 期間1の開始
   * @param end1Opt 期間1の終了 (Noneなら開始以降ずっと対象期間)
   * @param start2 期間2の開始
   * @param end2Opt 期間2の終了 (Noneなら開始以降ずっと対象期間)
   * @return 重複してたらtrue
   */
  def isOverlapped(start1: LocalTime, end1Opt: Option[LocalTime], start2: LocalTime, end2Opt: Option[LocalTime]): Boolean =
    end1Opt.fold(true)(!start2.isAfter(_)) && end2Opt.fold(true)(!_.isBefore(start1))

  /**
   * 期間の重複チェック(LocalDate版)
   * 単一のポイントで期間が重なったときも重複したとみなす。
   * e.g.)
   *  期間1 => 2020-10-01 ~ 2020-10-10
   *  期間2 => 2020-10-10 ~ 2020-10-20
   *  結果 => 重複あり(true)
   *
   * @param start1 期間1の開始
   * @param end1 期間1の終了
   * @param start2 期間2の開始
   * @param end2 期間2の終了
   * @return 重複してたらtrue
   */
  def isOverlapped(start1: LocalDate, end1: LocalDate, start2: LocalDate, end2: LocalDate): Boolean =
    !start2.isAfter(end1) && !end2.isBefore(start1)

  /**
   * 期間の重複チェック(LocalDate版, 期間終了Option可)
   * 単一のポイントで期間が重なったときも重複したとみなす。
   * e.g.)
   *  期間1 => 2020-10-01 ~ 2020-10-10
   *  期間2 => 2020-10-10 ~ 2020-10-20
   *  結果 => 重複あり(true)
   *
   * @param start1 期間1の開始
   * @param end1Opt 期間1の終了 (Noneなら開始以降ずっと対象期間)
   * @param start2 期間2の開始
   * @param end2Opt 期間2の終了 (Noneなら開始以降ずっと対象期間)
   * @return 重複してたらtrue
   */
  def isOverlapped(start1: LocalDate, end1Opt: Option[LocalDate], start2: LocalDate, end2Opt: Option[LocalDate]): Boolean =
    end1Opt.fold(true)(!start2.isAfter(_)) && end2Opt.fold(true)(!_.isBefore(start1))

  /**
   * 期間の重複チェック(LocalDateTime版)
   * 単一のポイントで期間が重なったときも重複したとみなす。
   * e.g.)
   *  期間1 => 2020-10-01 10:00:00 ~ 2020-10-10 20:00:00
   *  期間2 => 2020-10-10 20:00:00 ~ 2020-10-20 20:00:00
   *  結果 => 重複あり(true)
   *
   * @param start1 期間1の開始
   * @param end1 期間1の終了
   * @param start2 期間2の開始
   * @param end2 期間2の終了
   * @return 重複してたらtrue
   */
  def isOverlapped(start1: LocalDateTime, end1: LocalDateTime, start2: LocalDateTime, end2: LocalDateTime): Boolean =
    !start2.isAfter(end1) && !end2.isBefore(start1)

  /**
   * 期間の重複チェック(LocalDateTime版, 期間終了Option可)
   * 単一のポイントで期間が重なったときも重複したとみなす。
   * e.g.)
   *  期間1 => 2020-10-01 10:00:00 ~ 2020-10-10 20:00:00
   *  期間2 => 2020-10-10 20:00:00 ~ 2020-10-20 20:00:00
   *  結果 => 重複あり(true)
   *
   * @param start1 期間1の開始
   * @param end1Opt 期間1の終了 (Noneなら開始以降ずっと対象期間)
   * @param start2 期間2の開始
   * @param end2Opt 期間2の終了 (Noneなら開始以降ずっと対象期間)
   * @return 重複してたらtrue
   */
  def isOverlapped(start1: LocalDateTime, end1Opt: Option[LocalDateTime], start2: LocalDateTime, end2Opt: Option[LocalDateTime]): Boolean =
    end1Opt.fold(true)(!start2.isAfter(_)) && end2Opt.fold(true)(!_.isBefore(start1))
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaで、前後の連続する改行やタブを取り除く

追記
1文字だとマッチしないことに気づき
"^[\t\n]*([^\t\n]+(.|\\s)*[^\t\n]+)[\t\n]*$"から
"^[\t\n]*([^\t\n]*(.|\\s)*[^\t\n]+)[\t\n]*$"に修正した。
最初のプラスがアスタリスクになった。


大嫌いな正規表現。
前後以外の改行やタブはそのままにするとこがポイント。
めちゃくちゃハマったので記事にしときます。

    public static String trimNewline(String str) {
        String regex = "^[\t\n]*([^\t\n]*(.|\\s)*[^\t\n]+)[\t\n]*$";
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(str);
        if (m.find()) {
            return m.group(1);
        } else { // 「空文字」や「タブや改行だけで構成された文字列」はマッチせずここにくる
            return "";
        }
    }

Javaの正規表現を調べると
「.」は任意の文字にマッチ
とだけ書いてあるものばかりですが、実は、
改行以外の任意の文字列
だったので、めちゃくちゃハマりました。

System.out.println(trimNewline("\t\n\t\nhoge\t\n\t\n")); // hoge
System.out.println(trimNewline("\t\n\t\nhoge")); // hoge
System.out.println(trimNewline("hoge\t\n\t\n")); // hoge

また、途中に改行やタブが入った、
"\t\n\n\n\t\tho\t\t\nhoge\nge\t\n\t\n"
こちらも、
image.png

無事に、前後のみ取り除かれました。

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

【Java】joinメソッドの使い方

プログラミング勉強日記

2020年10月23日
Javaで文字列を連結するときに便利なjoinメソッドを使ったので、まとめる。

joinメソッドとは

 指定された区切り文字で、文字列を連結するためのメソッド。第一引数を区切り文字(デリミタ)として、第二引数を接頭辞として以降指定された接頭辞の終わりまでの文字列が区切られる。
 デリミタは,(カンマ)、.(ピリオド)、:(コロン)、を指定することができる。

public class Main {
  public static void main(String[] args) throws Exception {
    String str = String.join(",", "apple", "orange", "melon");
    System.out.println(str);
  }
}
実行結果
apple,orange,melon
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

if文の中括弧の省略(Javaシルバー)

if文の中括弧の省略

Javaの制御文(if、for、while等)の中括弧は省略しても実行される場合がある。
{}が省略されている場合は、true時の処理対象が1行のみである。

以下の例①のコードでは、条件式(i < 3)の結果がtrueの場合、実行される文は処理1の文のみである。
処理2の文は条件式に関わらず必ず実行される。

例①
if(i < 3)
  System.out.println("処理1");
  System.out.println("処理2");

つまり、例①は以下の例②のコードと同じ動きになる。

例②
if(i < 3){
  System.out.println("処理1");
}
  System.out.println("処理2");

今回、Javaの資格試験にでる可能性があったためメモした。
しかし、文法的には問題ないが可読性と保守性は下がるため、基本的には省略せず、例②のようなブロック{}の形にする。

※コメントで教えていただいたので追記する。ifやwhileなどは元々「直後」の文のみ対象となる。複数の文を対象にするための方法として、中括弧{}を使っている。

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

Javaで、前後の連続する改行を取り除く

苦手な正規表現で苦労したので。

public static String trimNewline(String str) {
        String regex = "^[\t\n\r]*(.*[^\t\n\r])[\t\n\r]*$";
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(str);
        if (m.find()) {
            return m.group(1);
        } else { // ここには、空文字しかいかない
            return str;
        }
    }


(.*[^\t\n\r])ではなく、(.+)としてしまうと、

hoge\t\tが hoge\t + \tと解釈されて、
hoge\tが返ってきてしまうのに時間食いました。

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

MyBatisでMapperに引数を経由せずにオブジェクトを渡す方法

記事投稿時点で見つけきらなかったので調べてみました。

やりたかったこと

  • フィーチャートグルの状態で発行するSQLを一部変更したい
  • でも、フィーチャートグルを使うかどうかはマッパー内部に閉じたい(引数にしたくない)

結論だけみたい人へ

サンプルはこちらにあります。
このソースコードの66行目のaddParameterメソッドを書き換えて渡したいオブジェクトを指定すれば引数で渡したのと同様の方法でアクセスできるようになります。

コードの説明

全体像

まずは該当のソースコードを以下に示します。

ソースコード
CustomSqlSessionConfig.java
package mybatis_implicit_parameter.config;

import lombok.experimental.Delegate;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.Map;

@Configuration
public class CustomSqlSessionConfig {
    @Bean
    public SqlSessionTemplate sqlSession(SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplateExt(sqlSessionFactory);
    }

    static class SqlSessionTemplateExt extends SqlSessionTemplate {
        public SqlSessionTemplateExt(SqlSessionFactory sqlSessionFactory) {
            super(sqlSessionFactory);
        }

        Object invokeParentMethod(Method method, Object[] args) throws Throwable {
            return makeParentMethodHandle(method).bindTo(this).invokeWithArguments(args);
        }

        MethodHandle makeParentMethodHandle(Method method) throws Exception {
            return MethodHandles.lookup().findSpecial(SqlSessionTemplate.class, method.getName(),
                    MethodType.methodType(method.getReturnType(), method.getParameterTypes()), SqlSessionTemplateExt.class);
        }

        @Delegate
        final SqlSession proxy = (SqlSession)Proxy.newProxyInstance(SqlSessionTemplateExt.class.getClassLoader(),
                new Class[]{SqlSession.class}, new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        if(hasParameter(method)) {
                            args[1] = addParameter(args[1]);
                        }
                        return SqlSessionTemplateExt.this.invokeParentMethod(method, args);
                    }

                    boolean hasParameter(Method method) {
                        Parameter[] params = method.getParameters();
                        return (params.length >= 2 && params[1].getType().equals(Object.class));
                    }

                    Object addParameter(Object parameter) {
                        parameter = (parameter == null) ? new MapperMethod.ParamMap() : parameter;
                        if (parameter instanceof Map) {
                            return addParameter((Map) parameter);
                        } else {
                            throw new RuntimeException("想定してないパターンここに来るなら教えて〜:" + parameter.getClass().getName());
                        }
                    }

                    Object addParameter(Map map) {
                        //暗黙のパラメーターはここでセットする
                        map.put("insertValue", "foo");
                        map.put("updateValue", "bar");
                        return map;
                    }
                });
    }
}

やってることをかいつまんで説明すると、、、

MyBatisは SqlSessionクラス のinsert/select/update/deleteという名の第1引数にString、第2引数にObjectを持つメソッドを経由してMapperの内容を解析してSQLを発行します。
CustomSqlSessionConfigでは、SQLSessionTemplateを拡張してそれぞれのメソッドをオーバーライドして引数経由で渡すのが不適切だと思われるオブジェクトを第2引数のパラメーター(実体はMap)に追加して実行するようにしています。

こだわりポイント:lombokの@Delegateとダイナミックプロキシを組み合わせる

このコードは自己満足的なコードで本当はもっとシンプルに実現できます。
上に書いたことを愚直に実装しようとすればこのようなコードではなくても、↓のように対象のメソッドをオーバーライドすれば実現できます。

愚直なコード
CustomSqlSessionConfig.java
(省略)
@Override 
public int insert(String statement,Object parameter) {
    return super.insert(statement, addParameter(parameter));
}
(省略)

でも、同じことを何度も書くのが嫌だってのでちょっと拘ってみました。

ダイナミックプロキシは、Javaの古いバージョンから実装されてる機能で、引数で指定されたinterface(群)のメソッドを持つオブジェクトを動的に作成し、指定したInvocationHandlerに処理を集約することができる機能です。

一方lombokの@Delegateはアノテートされたフィールドの全てのpublicなメソッドをそのフィールドを保持しているクラスのメソッドとして展開するアノテーションです(細かく調整もできます)。ぱっとイメージが付きにくいかも知れませが、このサイトを見ていただけるとイメージがつき易いかと思います。

この2つを組み合わせると、自身のクラスの任意のメソッド(今回はinsertだったりselectだったり)をひとまとめに処理する事が出来るんです。
ちょっと黒魔術的なコードにはなりがちですが、好きな人は溜まらなく好きなはず!知らんけどw

この方法の欠点は、InvocationHandlerから親クラスの同一メソッドを呼び出す(super.xxx的な呼び出し)のが困難になる点です。InvocationHandlerはMethodオブジェクトを引数として受け取るので、method.invoke(...)と出来ると良いのですが、@Delegateと組み合わせると、StackOverflowを引き起こしてしまいます。これは、親クラスを指定して同一のメソッドシグネチャを持つMethodオブジェクトを取得してきても同じ結果になります。これは、Method#invokeのAPIリファレンスに書かれてるとおりメソッド呼び出しをinvokeVirtualで実現しているためです。

If the underlying method is an instance method, it is invoked using dynamic method lookup as documented in The Java Language Specification, section 15.12.4.4; in particular, overriding based on the runtime type of the target object may occur.
(基礎となるメソッドがインスタンス・メソッドの場合、Java言語仕様セクションに記載されている動的メソッド参照を使用して起動され、特に、ターゲット・オブジェクトの実行時のタイプに基づいてオーバーライドされます。)

super.xxxをリフレクションで実現するためには

MethodHandles.lookup().findSpecial(SqlSessionTemplate.class, method.getName(),
    MethodType.methodType(method.getReturnType(), 
    method.getParameterTypes()), SqlSessionTemplateExt.class);

子クラス内で、findSpecialで親クラスのメソッドのMethodHandleを取得する必要があります。
MethodHandleはこれまでのJavaのリフレクションと異なり、どのクラスで実効しているかで結果が異なります。InvocationHandler実装クラス内でこの処理を実行してもエラーが発生します。

あやしいポイント

  • パラメーターを追加するメソッドかどうかの判定は若干怪しい。
  • パラメーターオブジェクトの型がMap以外のケースがあると動かない。Map以外のケース(nullの場合がある事は確認済み)が来ると動作しません。Map以外が来るかはちゃんと追い切れてません。

動作確認済みライブラリバージョン

ライブラリ名 バージョン
Spring-Boot 2.3.4.RELEASE
mybatis-spring-boot-starter 2.1.3
lombok 5.1.0

サンプルコードについて

サンプルコードは好きに使っていただいて構いませんが、その結果においてはいかなる責任も負いませーん。

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

【備忘録】業務用Andoridアプリ開発への挑戦

前提情報

挨拶

乱筆乱文にて失礼いたします
業務改善のためAndroidアプリの開発に挑戦することに
開発手順を検討しつつ進める所存
都合により細部をぼかすことがあるはご容赦いただきたし
内容は随時追加予定

選定経緯

  • 紙に記入→PCへ入力の業務が非常に多く
    データ化されていない情報も多い
  • PC台数が少なく調べものもままならない
  • ネットワーク環境が弱いためオフラインでも使える
    →Androidでアプリ開発が最適では

投稿者スペック

  • システム管理者3年目(2020/10現在)
  • Android使用経験なし
  • Java経験なし

環境確認

  • Windows10 Pro + Java7
  • Android Studio 4.0
  • サーバー + SQL Server + Tomcat7
  • Androidタブレット(Android 9 Pie)

開発手順

要件定義

  • ターゲット補足
  • 理想形の構築
  • 現状の確認
  • すり合わせ

Androidアプリ

WebAPI

  • Tomcat7関連
  • http通信関連
  • 帳票印刷
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

備忘録No.2「ArrayListとHashSetで検索履歴を作る」[Java]

はじめに

paizaのスキルチェック入門編で苦戦したので記録。

やりたい事

1.任意の文字数の文字列を複数入力し、リストに持たせる。
2.その際、入力した文字列がすでにリストに存在していたら、以前の文字列を削除し、新しく追加する。
3.一番新しく入力されたものから順に上から出力する。

コード1

    Scanner sc = new Scanner(System.in);
    List<String> words = new ArrayList<>();
    while(sc.hasNext()) {
        String s = sc.nextLine();
        if(words.contains(s)) {//すでにリストにあるか調べ、存在していたら削除する
            words.remove(s);
        }
        words.add(0, s);//入力された文字列をリストの先頭に加える
    }

    for(String word : words) {
        System.out.println(word);
    }

コード1の問題点

・ArrayListは、remove()やadd()で値を増減させると、格納されている全ての値をずらさなければいけない為、処理に時間がかかる。
・ArrayListのcontains()も、リスト内の全ての値を検索する為、格納されている値が多いほど、処理時間が長くなる。

解決策

・文字列の検索にHashSetを使う。→ 要素をハッシュ値に変換して検索に使うことができ、素早く値を発見できる。

コード2

入力された文字列は全てリストに追加し、HashSetを使って、重複しているものは表示しないようにする。(実際に削除するのではなく、無かったことにする)

    Scanner sc = new Scanner(System.in);

    List<String> words = new ArrayList<>();
    while(sc.hasNext()) {
        String s = sc.nextLine();
        words.add(s);
    }

    Collections.reverse(words);//新しく入力されたものから出力する為にリストを反転

    Set<String> usedWords = new HashSet<>();//リスト内の文字列の重複の判定に使うHashSet

    for(String word : words) {
        if(!usedWords.contains(word)){//すでにusedWordに追加されているか(重複しているか)を判定し、していなかったら表示
            System.out.println(word);
        }
        usedWords.add(word);
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む