Effective Javaを勉強します【第2章】
入社3年目になりましたが、全然Javaの基礎できてないなと思いまして、 effective javaの学習を始めました。まず第1章から。
項目1. コンストラクタの代わりにstaticファクトリーメソッドを検討する
staticファクトリーメソッドとは
クラスのインスタンスを返す単なるstaticのメソッド。
- 例) 基本データ型booleanに対応するボクシングされた基本データクラスBooleanを返す
- ボクシング:基本データ型の変数をそのラッパークラスのインスタンスに変換(逆はアンボクシング)
public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }
長所
コンストラクタと異なり名前を持つ
同じシグニチャを持つコンストラクタが複数必要な時は、シグニチャの順番を変えるしかない。
- Hoge(int num, String str)とHoge(String str, int num)
- 外部からはコードが何をしているのか分からない
- Hoge.methodA(int num, String str)とHoge.methodB(int num, String str)
- メソッド名で内部処理の違いを区別できる
メソッドが呼び出されるごとに新たなオブジェクトを生成する必要がない
あらかじめメンバーとして生成しておいたインスタンスや、オブジェクト生成時にキャッシュしておいたインスタンスを返すことで 不必要に重複したオブジェクトの生成を回避可能。
→オブジェクト生成のコストが高く、かつ頻繁に要求される場合は、パフォーマンスの大幅な向上につながる
- 例) Boolean.valueOf(boolean)メソッド
- Flyweight(フライ級)パターンと似ている(同じ?)
- 不必要なインスタンス化を防ぐ = 「インスタンス制御されている」状態にする
- シングルトンやインスタンス化不可能を保証できる
メソッドの戻り値型の任意のサブタイプのオブジェクトを返却可能
- staticファクトリーメソッドの戻り値型を、
とか抽象インタフェースとかにする - 上記メソッドから返すクラスの実装を内部クラスとして持つ
- 実装の隠蔽が可能
- 複数のクラスを返却可能なので、柔軟なAPIの設計が可能になる
// java.util.Collectionsの抜粋 ... public class Collections { // インスタンス化不可能 private Collections { ... // staticファクトリ〜メソッド public static <K,V> Map<K,V> More ...unmodifiableMap(Map<? extends K, ? extends V> m) { return new UnmodifiableMap<K,V>(m); } // 返却するクラスの実装 private static class More ...UnmodifiableMap<K,V> implements Map<K,V>, Serializable { }
パラメータ化された型のインスタンス生成の面倒さを低減する
Java7からはダイヤモンド構文が導入されたので、おそらく解決した
// ダイヤモンド構文 Map<String, List<String>> m = newHasMap<>();
以前は以下。
// 冗長で面倒 Map<String, List<String>> m = newHasMap<String, List<String>>();
public static <K, V> HashMap<K, V> newInstance() { return new HashMap<K, V>(); } // 簡潔になる Map<String, List<String>> m = HashMap.newInstance();
短所
publicあるいはprotectedのコンストラクタを持たないクラスのサブクラスを作れない
容易に他のstaticメソッドと区別がつかない
- コンストラクタほど目立たない(その他のメソッドと同列扱い)
- valueOfなどstaticファクトリーメソッドでよく利用される命名法に従うことで軽減する
項目2. 数多くのコンストラクタパラメータに直面した時にはビルダーを検討する
数多くのコンストラクタパラメータが必要な時、実現方法としては以下の3つが考えられるが、 読みやすさ、書きやすさ、安全性の観点からビルダーパターンを検討すべきである。
- テレスコーピングコンストラクタ・パターン
- JavaBeansパターン
- ビルダーパターン
テレスコーピングコンストラクタ・パターン
いくつかの必須パラメータとn個のオプションパラメータが存在する時 以下のように複数のコンストラクタをオーバーロードする。
- 必須パラメータだけを受け取るコンストラクタ
- 必須パラメータ+0個のオプションパラメータを受け取るコンストラクタ
- 必須パラメータ+1個のオプションパラメータを受け取るコンストラクタ
- …
- 必須パラメータ+n個のオプションパラメータを受け取るコンストラクタ
上記のうち、設定したいパラメータを全て含む最も短いパラメータリストを持つ コンストラクタを利用していくことになるが、大抵不要なパラメータも含まれることになる。
→可読性の低下、パラメータの順序を間違える恐れ
JavaBeansパターン
パラメータなしのコンストラクタを呼び出し、その後必要なパラメータと対応したセッターを呼び出し パラメータを設定する。
→生成が複数の呼び出しに分割されるので、その生成過程の途中で不整合な状態にある恐れ
ビルダーパターン
- 対象のオブジェクトを直接生成する代わりに、必須パラメータをすべてもつビルダーオブジェクトを生成する。
- ビルダーは生成するクラスのstaticメンバークラス
- オプションパラメータはビルダーオブジェクトの持つセッターのようなメソッドから追加する。
- ビルダーオブジェクトの持つbuild()メソッドから生成するクラスのコンストラクタを呼び出し、対象のオブジェクトを生成する。
// 例: Member member = new Member.Builder("Taro").age(25).build(); public class Member { private final String name; private final int age; public static class Builder { private final String name; //必須パラメータ private int age = 20; //オプションパラメータはデフォルト値で初期化 public Builder(String name) { this.name = name; } public Builder age(int age) { this.age = age; return this; } public Member build() { return new Member(this); } } private Member(Builder builder) { this.name = Builder.name; this.age = Builder.age; } }
欠点
- オブジェクト生成時に必ずビルダーオブジェクトの生成が必要
- シビアなパフォーマンスが要求される場合は注意する
- テレスコーピングコンストラクタ・パターンより長くなりがち
- 例えばパラメータ数4以上など、パラメータが多い場合にだけ利用すべき
- 途中からビルダーパターンへ変更は難しいため、将来的にパラメータが増えそうな場合は 最初からビルダーパターンを検討すべき
項目3. privateのコンストラクタかenum型でシングルトンを強制する
シングルトンとは
一度しかインスタンスが作成されないクラス
→コード中の同じクラスのインスタンスは全て同一であることが保証される
実現方法1:privateなコンストラクタ + finalのフィールド (+ staticファクトリーメソッド)
以下ではfinalのフィールド初期化時にのみ、コンストラクタが呼び出される
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } ... }
または、staticファクトリーメソッドから返す。
public class Elvis { private static final Elvis INSTANCE = new Elvis(); private Elvis() { ... } public static Elvis getInstance { return INSTANCE; } ... }
上記はAccessibleObject.setAccessibleメソッドを使用して、リフレクションにより privateなコンストラクタを呼び出すことが可能。
- コンストラクタを修正して2つ目のインスタンス生成時は例外を返すなどして回避
また、シリアライズ可能にする場合、ディシリアライズ毎に新たなインスタンスが生成されることを防ぐために 次のメソッドを追加する。
private Object readResolve() throws ObjectStreamException { // ディシリアライズ時にINSTANCEを返却して、重複生成を防ぐ return INSTANCE; }
リフレクションとは
「実行時にプログラム自身の情報を取得/振る舞いを変更する」仕組み。 (リフレクションがどうしても必要なケースがいまいち分からない)
Class cl = Class.forName("Foo"); //オブジェクト取得 Method method = cl.getMethod("hello"); //メソッド取得 method.invoke(cl.newInstance()); //メソッド実行
- JPCERTのセキュアコーディングスタンダードには 「リフレクションを使ってクラス、メソッド、フィールドのアクセス範囲を広げない」という項目がある
実現方法2:Enum
実現方法1よりこちらを選ぶべき。
public enum Elvis { INSTANCE; ... }
項目4. privateのコンストラクタでインスタンス化不可能を強制する
1つの明示的なprivateなコンストラクタを含むことでインスタンス化を防ぐ。
public class UtilityClass { private UtilityClass() { throw new AssertionError(); } ... }
項目5. 不必要なオブジェクトの生成を避ける
機能的に同じオブジェクトが必要なときは、1つのオブジェクトを再利用するほうが大抵適切である。
極端にだめな例
String s = new String("stringette") //bad String s = "stringette"
bad:
“stringette"自体がStringオブジェクトであり、Stringコンストラクタで生成されるオブジェクトと同等
→二重にオブジェクトが生成されてしまう
good:
言語仕様上、同じ内容の文字列リテラルで生成されるインスタンスは再利用することが保証されている。(同一JVM上のみ)
→"stringette"は一度しか生成されず、再利用される
不変クラスの再利用
- staticファクトリーメソッドを利用すれば大抵、不必要なオブジェクト生成は回避可能
- 例) Boolean.valeOf(String)
変更されない可変オブジェクトの再利用
- static初期化子で回避可能
- 例) 本文中例のDate/Calender/TimeZoneインスタンス
オートボクシングに注意
オートボクシングにより、基本データ型が自動で対応するラッパークラスに変換される
→不要なオブジェクトが増える可能性
Long sum = 0L; for (long i = 0; i <= Integer.MAX_VALUE; i++) { sum += i; //iがLong型にオートボクシングされる }
防御的コピーは別
何でも再利用すればよいわけではない。 防御的コピーが求められる場合、オブジェクトの再利用が悪質なバグやセキュリティホールにつながる可能性があるので注意。(項目39)
項目6. 廃れたオブジェクト参照を取り除く
廃れた参照
今後それを通してオブジェクトが参照されることのない単なる参照
- 本文中の例ではelements配列のsize外にある要素
→いらなくなったらnullを代入することで、スタックの利用クライアントで参照がなくなればGCされるようになる - 参照が間違っている場合、NullPointerExceptionでエラーを伝えることもできる
メモリリークを気をつけるべきとき
とにかくnullを入れればよいわけではない。廃れた参照に対する最善の方法は、参照が含まれていた変数を スコープの外に出すこと。
以下のような場合にメモリリークを注意すべき。
- クラスが独自のメモリを管理しているとき
- 本文中コードだとStackのelements
- キャッシュ機構をもつとき
- WeakHashMapでキャッシュを表現するのが望ましい
- エントリーが廃れると(キーへの外部からの参照がなくなると)自動的に取り除かれる
- WeakHashMapでキャッシュを表現するのが望ましい
- リスナーやコールバックを持つクラス
- キャッシュと同じくWeakHashMapで解決可能
項目7. ファイナライザを避ける
ファイナライザは予測不可能で、大抵危険であり、一般には必要ない
- JPCERTのセキュアコーディングスタンダードには 「ファイナライザは使わない」という項目がある
ファイナライザ: GCによりインスタンスが破棄されるタイミングで実行されるメソッド
- 即座にファイナライザが実行される保証がない
- 実行されるかどうかも分からない
- 重要な処理をファイナライザに頼ると実行されない可能性がある
- 例1) データベースの共有ロックの解除
- 例2) ファイナライザ中の例外は無視されて、そのままファイナライズは終了する
- 他のオブジェクトへの悪影響を与える可能性もある
- パフォーマンスも悪い
- サブクラスのファイナライザは手作業でスーパークラスのファイナライザを呼び出す必要あり
→リソースの開放はファイナライザに頼らず、try-finallyのfinallyブロックで明示的に終了させる
ファイナライザが有効な場合
以下に対しては有効ではあるが、明示的終了のほうがよりよい。
- 安全ネットとして
- 明示的な終了を忘れた場合に備えて、保険的にファイナライザを用いる
- ただし明示的終了がされずファイナライザが呼ばれたときは警告ログを出力すべき
- 明示的な終了を忘れた場合に備えて、保険的にファイナライザを用いる
- ネイティブピアに対して
ファイナライズ連鎖とファイナライザガーディアン
サブクラスから、スーパークラスのファイナライザの呼び出しは手動で実施する必要がある。
→継承される可能性のあるクラスでファイナライズを用いる場合、 ファイナライザガーディアンと呼ばれる無名クラスを検討したほうがよい。
ファイナライザガーディアンとは
macでbashからzshへ乗り換えた話
今までずっとbashを使ってきましたが、補完が強力とか作業がいろいろ捗るとか聞くので、重い腰を持ち上げてzshに乗り換えることを決めました。
環境
- MacOS Sierra 10.12.3
- Homebrew 0.9.9
zshインストール
Homebrewは入れている前提です。
以下のコマンドでzshをインストールします。zsh-completionsは、zshの補完機能を強化するプラグインです。
$ brew install zsh $ brew install zsh-completions
ログインシェルの変更
/etc/shell
にログインシェルにできるプログラムがフルパスで記述されています。インストールしたzshのパスを追記します。
$ sudo sh -c "echo '/usr/local/bin/zsh' >> /etc/shells"
シェルを変更します。パスワードを入力後、ターミナルを再起動します。
$ chsh -s /usr/local/bin/zsh
初期設定
再起動後、以下のようなメッセージが表示されます。
This is the Z Shell configuration function for new users, zsh-newuser-install. You are seeing this message because you have no zsh startup files (the files .zshenv, .zprofile, .zshrc, .zlogin in the directory ~). This function can help you with a few settings that should make your use of the shell easier. You can: (q) Quit and do nothing. The function will be run again next time. (0) Exit, creating the file ~/.zshrc containing just a comment. That will prevent this function being run again. (1) Continue to the main menu. --- Type one of the keys in parentheses ---
- (q): 何もしません。次回ターミナル起動時に同じメッセージが再度表示されます。
- (0): ホームディレクトリ以下に.zshrcが作成されます。ここに設定を自分で書き込む方式です。
- (1): 対話式メニューで.zshrcの設定を決定していく方式です。
0を選択します。
zplugのインストール
zshのプラグインマネージャであるzplugをインストールします。
zshのプラグインマネージャといえばoh-my-zshやantigenも有名みたいですが、 参考にもあるようにパフォーマンス等の面でzplugが最近は流行っているよう。
$ brew install zplug
zplugのREADMEに従って~/.zshrcに設定を追加します。
# brewのインストールパスを設定する export ZPLUG_HOME=/usr/local/opt/zplug source $ZPLUG_HOME/init.zsh # pluginの追加 ## zsh-completions zplug "zsh-users/zsh-completions"¬ fpath=(/usr/local/opt/zplug/repos/zsh-users/zsh-completions $fpath)¬ ## git zplug "plugins/git", from:oh-my-zsh ## 未インストール項目をインストールする if ! zplug check --verbose; then printf "Install? [y/N]: " if read -q; then echo; zplug install fi fi ## コマンドをリンクして、PATH に追加し、プラグインは読み込む zplug load --verbose
上記で追加しているのは、git-completionsとoh-my-zshのgitプラグインです。
git-completionsは、vagrantコマンドやrailsコマンドなど、zshの補完を強化するプラグインです。 上記のみで補完が有効にならない場合は以下のコマンドも実行します。
$ rm -f ~/.zcompdump; compinit
oh-my-zshのgitプラグインは大量のgitのaliasや関数を読み込みます。(例:git status -s > gss) 全部はなかなか覚えられませんが、仕様頻度の高いaliasを使うだけでも意外と快適です。
vcs_infoの設定
vcsとはVersion Control Systemsの略です。 Gitとかバージョン管理システムの情報をターミナルに表示することが可能になります。
今回はシンプルにgitレポジトリ内ではブランチ名を表示するようにします。
# vcs_info autoload -Uz vcs_info ## branchの表示 zstyle ':vcs_info:*' formats '(%b)' precmd() { vcs_info } #prompt PROMPT='%n@%m: %~ ${vcs_info_msg_0}'
その他設定
上記に加えて、lessやgrepのグローバルエイリアス、historyの端末間での共有など 基本的な設定を追加したものが以下になります。
参考
RailsでBootstrapのmodalが開いてすぐ閉じてしまう問題
開発していて地味にハマったのでメモ。
概要
削除・更新系の処理実行前に確認画面を表示するために、bootstrapのmodalを利用。 ボタンを押すとmodalが一瞬表示されるが、何もUI操作をしていないのにmodalダイアログが閉じてしまう。
原因
異なる2つのgemの中でbootstrapを読み込んでいたことが原因。私の場合は”sass-rails”に加えて、"jquery-datatables-rails"のbootstrapオプションを利用していたからでした。
似たような事象が以下で報告されているので共有。どうやらmodal起動時に2回toggle()が呼ばれて、1回目の呼び出しでmodal起動、即座に2回目が呼び出されて閉じているっぽいですね。
Modal Disappears Immediately · Issue #1611 · twbs/bootstrap · GitHub
Bootstrap Modal immediately disappearing - Stack Overflow
解決方法
bootstrapを呼び出しているGemのうち、片方の代替となるGemを探してそれを利用するとかでしょうか。もしくは、片方のGemでbootstrapを呼び出さないように、何かしら工夫をするとか(Gemによって対処方法は違うと思うので具体的な解決策を示すことができなくて申し訳ないのですが)
私の場合は"dataconfirm-modal"という別のgemを利用することで正常に動作するようになったので、そのまま放置してあります。結局、sass-railsとjquery-datatables-railsの両方を利用しているので、また再発するかもしれないです。が、現状は問題ないので、しばらくこのままでいこうと思います・・・。
macOS Sierra + Vagrant + CentOS7でRuby On Railsの開発環境を構築する
railsの開発環境をvagrant上のcentos7に構築するスクリプトを作成し、GItHubに公開しました。利用手順をREADME.mdに記載してあります。postgresqlの設定など、手順の一部は手動で実施する必要があります。
以下の環境で動作確認。
- OS: macOS Sierra 10.12.3
- Vagrant 1.8.1
- Virtualbox 5.0.18
- ruby 2.3.1
- rails 4.2.2
- postgresql 9.2.18
- nodejs 4.7.3
本記事は上記スクリプトの作成課程やスクリプトの実行内容をメモしたものです。
事前準備
- vagrantのインストール
- virtualboxのインストール
- virtualboxのpluginのインストール
$ vagrant plugin install vagrant-vbguest
VMの生成
- VagrantFileを生成
$ vagrant init centos/7
- VagrantFileを以下のように書き換える
# -*- mode: ruby -*- # vi: set ft=ruby : Vagrant.configure(2) do |config| # https://atlas.hashicorp.com/centos/boxes/7 config.vm.box = "centos/7" config.vm.box_version = "1611.01" config.vm.netowrk "forwarded_port", guest: 3000, host: 3000 config.vm.provider "virtualbox" do |vb| vb.memory = "2048" vb.name = "centos/7" end # vagrant up時にbin/setup.shを自動実行する config.vm.provision "shell", privileged: false, path: "bin/setup.sh" end
- Vagrantfileの設定内容に沿ってVMを作成する
$ vagrant up --provider virtualbox
localeなどの初期設定、ruby、nodejs、postgresqlのインストール
- bin/setup.sh中で実行する
- rbenvでrubyのバージョン管理する
- nodejsはuglifierなどのgemで必要
#!/bin/sh RUBY_VERSION='2.3.1' HOME_DIR='/home/vagrant' # set locale, keymap, timezone sudo localectl set-locale LANG=ja_JP.utf8 sudo localectl set-keymap jp106 sudo timedatectl set-timezone Asia/Tokyo # install ruby sudo yum -y update sudo yum -y install git-all openssl-devel readline-devel sqlite gcc gcc-c++ git clone git://github.com/sstephenson/rbenv.git ${HOME_DIR}/.rbenv git clone git://github.com/sstephenson/ruby-build.git ${HOME_DIR}/.rbenv/plugins/ruby-build echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ${HOME_DIR}/.bashrc echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ${HOME_DIR}/.bashrc echo 'eval "$(rbenv init -)"' >> ${HOME_DIR}/.bashrc echo 'gem: --no-ri --no-rdoc' > ${HOME_DIR}/.gemrc source ${HOME_DIR}/.bashrc rbenv install $RUBY_VERSION rbenv global $RUBY_VERSION gem install bundler # install nodejs sudo rpm -Uvh https://rpm.nodesource.com/pub_4.x/el/7/x86_64/nodesource-release-el7-1.noarch.rpm sudo yum -y install nodejs # install postgresql # https://www.postgresql.org/download/linux/redhat/#yum sudo yum -y install http://yum.postgresql.org/9.5/redhat/rhel-7-x86_64/pgdg-redhat95-9.5-2.noarch.rpm sudo yum -y install postgresql-devel postgresql95-server postgresql95-contrib sudo /usr/pgsql-9.5/bin/postgresql95-setup initdb sudo systemctl start postgresql-9.5.service sudo systemctl enable postgresql-9.5.service
postgresqlの設定
これ以降は自動化できていないため、手動実行の必要がある。
- postgresユーザーのパスワード変更
$ sudo passwd postgres Changing password for user postgres. New password: #パスワードを適当に設定する Retype new password:
- postgresqlにvagrantユーザーを追加
$ su - postgres $ createuser vagrant -s $ psql postgres=# \password vagrant # vagrantユーザにパスワードを設定 Enter new password: #パスワードを適当に設定する Enter it again: postgres=# \q # psqlプロンプト終了 $ exit
- pg_hba.confの81行目付近を以下のように書き換える (peer → md5)
$ sudo vi /var/lib/pgsql/9.5/data/pg_hba.conf # "local" is for Unix domain socket connections only # local all all peer local all all md5 $ sudo systemctl restart postgresql-9.5.service
Railsプロジェクトの作成
- 作業ディレクトリの作成
$ mkdir /vagrant/hello_app $ cd /vagrant/hello_app
$ bundle init $ vi Gemfile # Gemfileを以下のように編集 source "https://rubygems.org" gem "rails", "4.2.2" gem 'pg', '0.17.1' # ホストOSとの共有フォルダ内にinstallするとrails関係のコマンドの実行速度が遅くなる(原因不明) $ bundle install --path ~/bundler/hello_app/vendor/bundle
- railsプロジェクトの作成とDB設定
$ bundle exec rails new . --database=postgresql $ vi config/database.yml # database.ymlを編集し、defaultにusernameとpasswordを設定 default: ... username: vagrant password: # 先ほど設定したパスワード $ bin/rake db:create
- サーバーの起動
bin/rails server -b 0.0.0.0 -p 3000
http://localhost:3000/
にアクセスし、以下の画面が表示されることを確認する
課題など
- postgresqlの設定を自動化したい
- ansibleとか使ってみたい