Programming Diary.

ソフトウェアエンジニアの技術blog

VirtualBox上のUbuntuにSSHで接続するための設定

前回は、Linux学習用にVirtualBox仮想マシンを作成しUbuntuをインストールする手順を確認しました。今回は、そのVirtualBox上のゲストOSにホストOSからSSHで接続するための設定手順を確認します。

環境

SSHサーバのインストール

ゲストOS(Ubuntu)にSSHサーバがインストールされていない場合は、まずはインストールが必要です。下記コマンドを実行しSSHサーバをインストールします。

$ sudo apt-get install openssh-server

インストール後は自動でSSHデーモンが起動します。下記コマンドでsshdプロセスが起動していることを確認します。

$ ps aux | grep sshd | grep -v grep

Virtual Boxのネットワーク設定

今回はホストOSからゲストOSへ接続するためにホストオンリーアダプター(ゲストOSとホストOS間のみ通信可能なネットワーク)を使用します。VirtualBoxのメニューから「ファイル」>「環境設定」>「ネットワーク」を選択し、下図のようにホストオンリーネットワークが設定されていることを確認します。 f:id:smys0515:20160228031528p:plain

さらに、右側の編集アイコンを選択し、IPアドレスが設定されていることを確認します。DHCPサーバは有効化されていないままで問題ありません。

f:id:smys0515:20160228031534p:plain f:id:smys0515:20160228221059p:plain

仮想マシンのネットワーク設定

続いて、VirtualBoxのメニューから「仮想マシン」>「設定」>「ネットワーク」を選択し、アダプター1にNATが割り当てられていることを確認します。さらに、アダプター2を有効化しホストオンリーアダプターを割り当てます。 f:id:smys0515:20160228221747p:plain f:id:smys0515:20160228221746p:plain

また、ゲストOS上で「System Settings」>「Network」より対象のネットワーク(上記で設定したアダプター2のMACアドレスを確認)を選択し、「Options」ボタンをクリックし編集画面を表示します。 f:id:smys0515:20160228222945p:plain

「General」タブで「Automatically connect to this network when it is available」にチェックが入っていることを確認し、「IPv4 Settings」タブでManualを選択して固定のIPアドレスを設定します。 f:id:smys0515:20160228164304p:plain f:id:smys0515:20160229002935p:plain

ホストOSからゲストOSへ接続

以上で必要な設定はすべて完了です。ホストOSのSSHクライアントからIPアドレスを指定してゲストOSへ接続し、ユーザ名とパスフレーズを入力してログインできることを確認します。 f:id:smys0515:20160228165441p:plain f:id:smys0515:20160228165444p:plain

※今回はVirtualBoxのネットワーク設定でホストオンリーアダプターを追加してゲストOSへSSHで接続する手順を確認しましたが、他にもポートフォワーディング機能を利用してゲストOSへSSHで接続することも可能です。詳しい手順は下記『新しいLinuxの教科書』で紹介されています。

新しいLinuxの教科書

新しいLinuxの教科書

Oracle VM VirtualBoxでLinux学習環境の構築

はじめに

Linuxを学び始める初心者にとって、まずは学習用の環境を構築することが重要になるかと思います。その際に、マシンを新たに1台用意してLinuxをインストールするという作業は手間もコストもかかりますが、仮想化ソフトウェアを利用することによって、仮想マシン上にLinux学習用環境を簡単に構築することが可能となります。

今回はWindows上に仮想化ソフトウェアOracle VM VirtualBox仮想マシンを作成し、Ubuntuをインストールする手順を確認します。

環境

Oracle VM VirtualBoxのインストール

まずは仮想化ソフトウェアのOracle VM VirtualBoxをインストールします。公式ページの「Downloads」メニューより各ホスト向けのインストーラをダウンロードし、セットアップウィザードを起動します。今回はデフォルトの設定のままインストールを進めます。インストール完了後にVirtualBoxを起動すると、下図のような画面が表示されます。 f:id:smys0515:20160207013211p:plain

Linuxディストリビューションの入手

Linuxディストリビューションは、CDやDVDの物理メディアとして、あるいはインターネットを通じてISOファイル(イメージファイル)として手軽に入手することができます。今回はISOファイルとしてLinuxディストリビューションを入手します。ただし、このイメージファイルは数GBという容量のためダウンロードに時間がかかります。

Ubuntuイメージファイルのダウンロード

Ubuntu公式ページの「Download」メニューよりダウンロードします。今回はUbuntu Desktopを選択し、「Download」ボタンをクリックしてダウンロードを開始し、完了するまでしばらく待ちます。

VirtualBox仮想マシンの構築

続いて、VirtualBox仮想マシンを作成します。

まずはVirtualBoxを起動し、左上の「新規」ボタンをクリックします。任意の名前を入力し、タイプは「Linux」、バージョンは「Ubuntu(64-bit)」を選択します。さらに、「メモリーサイズ」は1024MB程度、「ハードディスク」は「仮想ハードディスクを作成する」、「ハードディスクのファイルタイプ」は「VDI」、「物理ハードディスクにあるストレージ」は「可変サイズ」、「ファイルの場所とサイズ」はデフォルトの場所で容量を16GB程度に設定します。これで仮想マシンが作成されました。

起動すると下図のような「起動ハードディスクを選択」画面が表示されるので、ダウンロードしたISOファイルを選択して「起動」ボタンをクリックします。

f:id:smys0515:20160207195244p:plain

Ubuntuのインストール

仮想マシンが起動すると、下図のような「Welcome」画面が表示されます。「Install Ubuntu」ボタンをクリックし、Ubuntuのインストールを開始します。

f:id:smys0515:20160207195915p:plain

Ubuntuインストーラに従いインストールを進めます。「Installation type」画面では「Erase disk and install Ubuntu」を選択し、「Who are you?」画面でユーザ名やパスワードを設定します。インストールが完了すると一度再起動します。

f:id:smys0515:20160207200318p:plain

再起動完了後はログイン画面が表示されるので、ログインできることを確認したら無事にインストール完了です。Terminalを起動し、各種コマンドを実行できます。

まとめ

今回はLinux初心者が学習を進めていくための準備として、Oracle VM VirtualBoxを使用してUbuntu仮想マシンを構築しました。まずはこの学習環境を使用して、Linuxの各種機能について理解を深めていければと思います。

また、今回はUbuntuをインストールしましたが、同様の手順で他のLinuxディストリビューションの環境構築も可能です。

※『新しいLinuxの教科書』の第1章を参考にしました。

新しいLinuxの教科書

新しいLinuxの教科書

JJUGナイトセミナー「GS Collections 道場」に参加しました

昨日、JJUGナイトセミナー「GS Collections 道場」のGS Collectionsハンズオンに参加しました。

内容

ゴールドマン・サックスの講師の方による説明と資料に基づき、GS Collections KataのExercise1〜5を進めていきました。*1

各Exerciseで学んだ点を簡単にまとめておきます。

  • Exercise1

    • collect, selectの使用
  • Exercise2

    • Predicateを引数に取る各種メソッドの使用(anySatisfy, allSatisfy, count, reject, partition, detect)
    • PredicatesのstaticメソッドからPredicateの生成
  • Exercise3

    • flatCollectの使用
    • targetを指定するcollectの使用(collectの結果を指定したtargetに格納)
  • Exercise4・5

    • ArrayIterateを用いて配列からGS Collectionsの各種メソッドの使用
    • ListIterateを用いてJDKのListからGS Collectionsの各種メソッドの使用
    • ArrayAdapterを用いて配列をMutableListでラップして、GS Collectionsの各種メソッドの使用
    • ListAdapterを用いてJDKのListをMutableListでラップして、GS Collectionsの各種メソッドの使用
感想

2時間にしては内容的にはたくさん詰め込まれているようで、わりと早いスピードで説明されていたので付いていくのがけっこう大変でした。ただ、各Exerciseで演習の時間も10分程度取られたのでその間に頭の中を整理したり、質問したりもできるのでよかったです。

個人的には、Exercise5の演習中に全然関係ないjdk.nashorn.internal.runtimeパッケージのListAdapterを使用していたことになかなか気付かず、うまくいかなくて少し焦りました。

全体的に、説明+演習+解説のセットで進められるので理解しやすく、知識の定着にもつながると思いました。今回のセミナーで、以前の記事(Javaコレクションフレームワークの比較 (JDK8/GS Collections/Guava) - Programming Diary.)で簡単に使用してみたときよりも理解が深まったのでよかったです。

*1:資料中のExercise2は現在のGS Collections Kataではなくなっているので、以降は番号がずれています

JNIによるJavaからネイティブコードの呼び出しとProject PanamaのJava FFI概要

はじめに

JavaコードとC/C++などのネイティブコードを連携させる仕組みに、JNI(Java Native Interface)が挙げられますが、実装上の手続きが煩雑だったりパフォーマンスが良くなかったりして問題点が多いとされています。*1

そこで、Project PanamaではJavaとネイティブ間のやり取りにおける新たな枠組みとしてJEP 191: Foreign Function Interface(Java FFI)が検討されています。今回は、JNIでJavaからC++のコードを呼び出すサンプルを実装後、Java FFIの概要を確認します。

JNIでJavaからC++を呼び出すための実装手順

今回は、Java側から引数が文字列のC++の関数を呼び出し、C++側でその文字列に"Hello World" を結合して返すという例を取り上げます。

環境

実装手順の概要は以下の通りです。
1. Javaでnative修飾子を付けたメソッドを宣言する
2. Javaソースファイルをコンパイルし、classファイルを生成する
3. classファイルからCヘッダーファイルを生成する
4. C++ソースファイルを実装する
5. C++ソースファイルをコンパイルし、共有ライブラリを作成する
6. Javaで共有ライブラリを読み込み、コンパイル・実行する

Javaでnative修飾子を付けたメソッド宣言

まずはJavaでnativeメソッドを宣言します。処理の中身はC++で実装するため定義のみ記述します。

public class HelloWorldJNI {

    private native String helloWorld(String string);

}
Javaソースファイルからclassファイル生成

ここで一旦Javaソースファイルをコンパイルし、classファイルを生成します。

javac HelloWorldJNI.java
classファイルからCヘッダーファイル生成

classファイルが生成されたことを確認し、javahコマンドでCヘッダーファイルを生成します。

javah HelloWorldJNI

下記のようなヘッダーファイル(HelloWorldJNI.h)が自動生成されます。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorldJNI */

#ifndef _Included_HelloWorldJNI
#define _Included_HelloWorldJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloWorldJNI
 * Method:    helloWorld
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_HelloWorldJNI_helloWorld
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif
C++ソースファイル実装

生成されたヘッダーファイルを読み込み、C++のソースファイルを実装します。JNIEnv構造体に定義された関数でC++に渡されたJavaオブジェクトを扱うことができます。今回は、"Hello World "の末尾にJavaから渡された文字列を結合して返します。なお、GetStringUTFChars関数で取得した文字列は不要になった段階でReleaseStringUTFChars関数で解放する必要があります。

#include "HelloWorldJNI.h"
#include <cstring>

JNIEXPORT jstring JNICALL Java_HelloWorldJNI_helloWorld
  (JNIEnv *env, jobject obj, jstring string) {
      char buf[256] = "Hello World ";
      const char* src = env->GetStringUTFChars(string, 0);
      
      strcat(buf, src);
      
      env->ReleaseStringUTFChars(string, src);
      
      return env->NewStringUTF(buf);
}
C++ソースファイルから共有ライブラリ作成

C++のソースファイルをコンパイルし、共有ライブラリ(OS Xでは.dylib, Windowsでは.dll, Linuxでは.so)を作成します。下記のように、-Iオプションでjni.hおよびjni_md.hのパスを指定する必要があります。

g++ -shared -I /Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/include  -I /Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/include/darwin  HelloWorldJNI.cpp -o libHelloWorldJNI.dylib
Javaで共有ライブラリを読み込み、コンパイル・実行

Javaソースファイルにライブラリの読み込み処理(System#loadLibrary)を記述します。

public class HelloWorldJNI {
    private native String helloWorld(String string);

    static {
        System.loadLibrary("HelloWorldJNI");
    }

    public static void main(String... args) {
        System.out.println(new HelloWorldJNI().helloWorld("Java"));
        System.out.println(new HelloWorldJNI().helloWorld("C++"));
    }
}

再度コンパイル後、-Djava.library.pathで共有ライブラリのパスを指定し、実行します。

javac HelloWorldJNI.java
java -Djava.library.path=. HelloWorldJNI

実行結果は下記の通りです。

Hello World Java
Hello World C++

JNIの問題点

上記のJNIによるJavaからネイティブコードの呼び出しについては、次のような実装上および性能上の問題点があります。

  • C/C++のコードを書く必要がある
  • ネイティブコードにおけるメモリ管理を適切に実施する必要がある
  • ネイティブのアプリケーションに比べて一般的にパフォーマンスが著しく劣る

そこで、Project Panamaが提唱され、Javaコードとネイティブコード間の連携の新たな仕組みとしてJava FFIが検討されています。

Project PanamaのJava FFI概要

Java FFIではJDKレベルにおけるネイティブコードとの連携について検討されています。具体的にはJNR(Java Native Runtime)として実装が進められており、ネイティブコードの呼び出しやネイティブメモリの管理などについて最適化が図られています。将来的にはJSR(Java Specification Requests)として検討され、Java SE 10で導入予定です。

下記でサンプルが公開されているので、改めて詳細に確認したいと思います。

jnr · GitHub

*1:他にJNA(Java Native Access)というライブラリを使用することもできますが、これもパフォーマンスの面で問題があるとされています

MySQLサンプルデータを用いたJava EEアプリケーション開発入門

はじめに

Java EE初学者が入門として簡単なWebアプリケーションを作成して学習を進めていきます。今回は、MySQL公式サイトで配布されているworldデータベースを使用して、各大陸の国一覧表示アプリを作成します。

開発環境

※現状ではJava EEアプリケーション開発を最も容易に始められると思われるNetBeans + GlassFishの組み合わせを選択しました。(ネットや書籍の情報が豊富なうえに、NetBeansの自動生成機能が便利なようです)

開発するアプリの概要

今回は、MySQLのサンプルworldデータベースを用いて、各大陸の国の情報を一覧で表示するアプリを開発します。

初期表示画面 f:id:smys0515:20150517202312p:plain

プルダウンで大陸名を選択すると、該当する国名、人口、首都が表示されます。 f:id:smys0515:20150517202117p:plain

MySQL worldデータベースの導入

まずは、MySQL公式サイトのExample Databasesからworld databaseをダウンロードします。ストレージエンジンがMyISAM versionとInnoDB versionが存在しますが、今回はInnoDB versionを使用することにしました。コマンドラインからMySQLへ接続し、データベースを作成後、ダウンロードしたファイルを読み込みます。

mysql> create database world;
mysql> use world;
mysql> source world_innodb.sql;

下記テーブルが作成されていることを確認します。

mysql> show tables;
+-----------------+
| Tables_in_world |
+-----------------+
| City            |
| Country         |
| CountryLanguage |
+-----------------+

ERMasterのインポート機能でER図を自動生成すると下図のようになります。CountryテーブルのContinentカラムに格納されうる値が、定められた文字列定数のいずれかであることがわかります。(今回はそれらの値をプルダウンに用います) f:id:smys0515:20150517115422p:plain

ちなみにMySQL公式サイトによると、当サンプルデータはフィンランドの国家統計局によるデータとのことです。

NetBeansMySQL接続設定

続いて、NetBeansからMySQLへ接続するための設定を行います。NetBeansを起動し、「サービス」タブの「データベース」を右クリックで「MySQLサーバーを登録」を選択します。すると、「MySQLサーバー・プロパティ」ダイアログが開くので、適切な値を入力し「OK」をクリックします。

f:id:smys0515:20150517204428p:plain f:id:smys0515:20150517204433p:plain

問題なく接続できればデータベースの設定は完了です。引き続き、Webアプリケーションプロジェクトを作成していきます。

Webアプリケーションプロジェクトの作成

NetBeansの「ファイル」>「新規プロジェクト」から、カテゴリ:[Maven]、プロジェクト:[Webアプリケーション]を選択し「次へ」をクリックします。 f:id:smys0515:20150517234614p:plain プロジェクト名(今回はWorldDataApp)等を入力して、「次へ」をクリックし、サーバ:[GlassFish Server]、Java EEバージョン:[Java EE 7 Web]となっていることを確認して、「終了」をクリックするとプロジェクトが作成されます。

さらに、今回使用する各パッケージを作成しておきます。「プロジェクト」タブの「ソース・パッケージ」を右クリックで「新規」>「その他」を選択します。カテゴリ:[Java]、ファイル・タイプ:[Javaパッケージ]を選択、「次へ」をクリックし、パッケージ名を入力して下記パッケージを作成します。

  • sample.javaee.worlddataapp.bean(CDI管理用パッケージ)
  • sample.javaee.worlddataapp.ejbEJB用パッケージ)
  • sample.javaee.worlddataapp.entity(JPAエンティティ用パッケージ)

エンティティクラスの自動生成

はじめにエンティティクラスを作成します。JPAエンティティ用パッケージ(sample.javaee.worlddataapp.entity)を右クリックで、「新規」>「その他」を選択して、カテゴリ:[持続性]、ファイル・タイプ:[データベースからのエンティティ・クラス]を選択し、「次へ」をクリックします。データ・ソースで新しいデータソースを選択し、JNDI名を入力して、データベース接続を選択し、「OK」をクリックします。

すると、下図の画面で使用可能な表が表示されるので、今回はCityとCountryを追加し、「次へ」で進めていき「終了」をクリックします。 f:id:smys0515:20150518014725p:plain これで、City.javaおよびCountry.javaが自動生成されます。また、JPA設定ファイルsrc/main/resources/META-INF/persistence.xmlも生成されています。

ただし、今回はデフォルトで自動生成されたCountry.javaのフィールドの

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "countryCode")
    private Collection<City> cityCollection;

となっている箇所を下記のように変更し、首都名の取得メソッドを追加しました。

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "countryCode")
    @MapKey(name = "id")
    private Map<Integer, City> cityMap;

    public String getCapitalName() {
        if (cityMap == null || capital == null) {
            return "";
        }
        return cityMap.get(capital).getName();
    }

これは、Countryに紐づくCityをCollectionで保持していたのを、City.idをキーとするMapで保持するよう変更し、Country.capital(=City.id)からCity.nameを取得できるようにするためです。

続いて、EJB用パッケージ(sample.javaee.worlddataapp.ejb)を右クリックで、「新規」>「その他」を選択して、カテゴリ:[持続性]、ファイル・タイプ:[エンティティ・クラスのセッションBean]を選択し、「次へ」をクリックします。使用可能なエンティティ・クラスに先ほど作成したクラスが表示されるので、追加して「次へ」「終了」をクリックします。 f:id:smys0515:20150518021658p:plain

すると、データアクセス用のAbstractFacade.java, CityFacade.java, CountryFacade.javaが生成されています。今回は、CountryFacade.javaに下記メソッドを追加して、continentからも取得できるようにしました。

    public List<Country> findByContinent(String continent) {
        return em.createNamedQuery("Country.findByContinent").setParameter("continent", continent).getResultList();
    }

CDI管理Beanの生成

つぎに、CDI管理Beanを作成します。CDI管理Bean用パッケージ(sample.javaee.worlddataapp.bean)を右クリックで、「新規」>「その他」を選択して、カテゴリ:[JavaServer Faces]、ファイル・タイプ:[JSF管理対象Bean]を選択し、「次へ」をクリックします。クラス名を入力し、スコープにviewを選択して「終了」をクリックします。

下記のように、大陸プルダウンの生成処理およびプルダウン選択時の国一覧情報の取得処理などを記述します。

// package・importは省略

/**
 * 国一覧表示画面 管理Bean
 */
@Named(value = "worldDataBean")
@ViewScoped
public class WorldDataBean implements Serializable {

    /* 選択された大陸 */
    private Continent continent;

    /* 大陸プルダウン */
    private List<Continent> continentList;

    /* 表示する国一覧 */
    private List<Country> countryList;

    /* データベースアクセス用EJB */
    @Inject
    private CountryFacade countryFacade;

    /**
     * 初期化処理
     */
    @PostConstruct
    public void init() {
        this.continent = Continent.NONE;
        this.continentList = Arrays.asList(Continent.values());
        this.countryList = getCountryByContinent();
    }

    /**
     * 大陸から国一覧を取得
     * @return 
     */
    public List<Country> getCountryByContinent() {
        if (this.continent == Continent.NONE) {
            return new ArrayList<>();
        } else {
            return countryFacade.findByContinent(this.continent.getLabelName());
        }
    }
    // getter・setterは省略

    /**
     * 大陸を表す列挙型
     */
    public static enum Continent {

        NONE("-"), ASIA("Asia"), EUROPE("Europe"), NORTH_AMERICA("North America"), //
        AFRICA("Africa"), OCEANIA("Oceania"), ANTARCTICA("Antarctica"), SOUTH_AMERICA("South America");

        /* ラベル名 */
        private String labelName;

        Continent(String labelName) {
            this.labelName = labelName;
        }
        // getter・setterは省略
    }

}

JSFページの作成

最後にJSFページを作成します。Webページフォルダを右クリックで、「新規」>「その他」を選択して、カテゴリ:[JavaServer Faces]、ファイル・タイプ:[JSFページ]を選択し、「次へ」をクリックします。ファイル名(world)を入力し、「終了」をクリックします。ここで、デフォルトで生成されたindex.htmlは不要なため削除し、WEB-INF/web.xmlのwelcome-fileのindex.htmlはworld.xhtmlに変更します。

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:f="http://xmlns.jcp.org/jsf/core">
    <h:head>
        <title>World Data</title>
        <style type="text/css">
            .numberColumn  {text-align: right;}
            .oddRow  {background-color: lightcyan;}
            .evenRow  {background-color: lightyellow}
        </style>
    </h:head>
    <h:body>
        <h:form prependId="false">
            <h:outputText value="continent: " />
            <h:selectOneMenu value="#{worldDataBean.continent}">
                <f:selectItems value="#{worldDataBean.continentList}" var="continent" itemValue="#{continent}" itemLabel="#{continent.labelName}"/>
                <f:ajax execute="txt" event="change" render="countryTable" listener="#{worldDataBean.getCountryByContinent()}" />
            </h:selectOneMenu>
            <h:dataTable id="countryTable" var="country" value="#{worldDataBean.countryList}" rowClasses="oddRow, evenRow" columnClasses="numberColumn, ,numberColumn, ">
                <h:column>
                    <f:facet name="header">
                        <h:outputText value="#" rendered="#{worldDataBean.countryList.size() > 0}" />
                    </f:facet>
                    <h:outputText value="#{worldDataBean.countryList.indexOf(country) + 1} " />
                </h:column>
                <h:column>
                    <f:facet name="header">
                        <h:outputText value="name" rendered="#{worldDataBean.countryList.size() > 0}" />
                    </f:facet>
                    <h:outputText value="#{country.name} " />
                </h:column>
                <h:column>
                    <f:facet name="header">
                        <h:outputText value="population" rendered="#{worldDataBean.countryList.size() > 0}" />
                    </f:facet>
                    <h:outputText value="#{country.population}" />
                </h:column>
                <h:column>
                    <f:facet name="header">
                        <h:outputText value="capital" rendered="#{worldDataBean.countryList.size() > 0}" />
                    </f:facet>
                    <h:outputText value="#{country.capitalName}" />
                </h:column>
            </h:dataTable>
        </h:form>
    </h:body>
</html>

主なポイントは以下の通りです。

  • <h:dataTable>タグのrowClassesおよびcolumnClassesでスタイルの指定が可能。それぞれカンマ区切りで複数指定すると、行ごとまたは列ごとに適用するスタイルを変えることができる。(今回のように奇数行と偶数行の背景色の色分けや特定列のみ右寄せで表示するなど)
  • <f:ajax>タグを用いることで画面の一部のみを更新することが可能。event属性でAjaxが実行されるタイミングを、render属性でレンダリング対象を指定し、listener属性に記述したCDI管理Beanのメソッドが実行される。

以上で完成です。アプリケーションを実行し、想定通りの動作となることを確認します。

まとめ

MySQLサンプルデータを用いたJava EEアプリケーション開発を通じて、JSF, EJB, JPA, CDIなどについて簡単に確認しました。NetBeans + GlassFishで意外と手軽に始められると感じたので、引き続き学習を続けていきたいと思います。

※『Javaエンジニア養成読本』特集3「現場で役立つJava EE」を参考にしました。

Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] (Software Design plus)

Javaエンジニア養成読本 [現場で役立つ最新知識、満載!] (Software Design plus)

Javaコレクションフレームワークの比較 (JDK8/GS Collections/Guava)

はじめに

Java Day Tokyo 2015JJUG CCC 2015 Springで紹介されていたGS Collectionsを使用してみました。実例を用いてJava8のStream APIGuavaの使用方法と比較します。

環境
  • JDK 1.8.0_25
  • GS Collections 6.1.0
  • Guava 18.0
  • JUnit 4.8.1
問題設定
  1. 社員のリストから営業部に所属する社員を抽出し、社員番号の昇順で社員名を取得する
  2. 社員のリストから部署ごとの社員数を求める

※『Javaエンジニア養成読本』特集2の第3章「Stream API を使いこなすために」の例を参考にしました。

社員
public class Employee {
    private int id; // 社員番号
    private String name; // 名前
    private Dept dept; // 部署

    // 以下、コンストラクタ・getter
}
部署
public enum Dept {
    ACCOUNTS("経理部"), //
    GENERAL_AFFAIRS("総務部"), //
    HUMAN_RESOURCES("人事部"), //
    SALES("営業部");

    private String name; // 名前

    // 以下、コンストラクタ・getter
}
テストデータ
public abstract class EmployeeDomainForTest {
    protected List<Employee> employeeList;
    
    @Before
    public void setUp(){
        this.setUpEmployeeList();
    }

    private void setUpEmployeeList() {
        this.employeeList = new ArrayList<>();
        this.employeeList.add(new Employee(1003, "Bob", Dept.SALES));
        this.employeeList.add(new Employee(1007, "Fred", Dept.HUMAN_RESOURCES));
        this.employeeList.add(new Employee(1001, "Daniel", Dept.SALES));
        this.employeeList.add(new Employee(1010, "Randy", Dept.GENERAL_AFFAIRS));
        this.employeeList.add(new Employee(1004, "Matt", Dept.ACCOUNTS));
        this.employeeList.add(new Employee(1009, "Tim", Dept.SALES));
        this.employeeList.add(new Employee(1006, "Susan", Dept.GENERAL_AFFAIRS));
        this.employeeList.add(new Employee(1002, "Julia", Dept.HUMAN_RESOURCES));
        this.employeeList.add(new Employee(1005, "Brown", Dept.HUMAN_RESOURCES));
        this.employeeList.add(new Employee(1008, "Kevin", Dept.SALES));
    }
}

実例1

社員のリストから営業部に所属する社員を抽出し、社員番号の昇順で社員名を取得する

  • JDK8
public class JDK8Test extends EmployeeDomainForTest {
    @Test
    public void getEmployeeNamesInSalesSortedById() {
        List<String> result = this.employeeList.stream()
                .filter(e -> e.getDept() == Dept.SALES)
                .sorted(Comparator.comparingInt(Employee::getId))
                .map(Employee::getName).collect(Collectors.toList());

        assertThat(result, is(Arrays.asList("Daniel", "Bob", "Kevin", "Tim")));
    }
}
  • GS Collections
public class GSCollectionsTest extends EmployeeDomainForTest {
    @Test
    public void getEmployeeNamesInSalesSortedById() {
        MutableList<String> result = FastList
                .newList(this.employeeList)
                .selectWith(Predicates2.attributeEqual(Employee::getDept), Dept.SALES)
                .sortThisBy(Employee::getId).collect(Employee::getName);

        assertThat(result, is(Arrays.asList("Daniel", "Bob", "Kevin", "Tim")));
    }
}
  • Guava
public class GuavaTest extends EmployeeDomainForTest {
    @Test
    public void getEmployeeNamesInSalesSortedById() {
        List<Employee> filtered = FluentIterable.from(this.employeeList)
                .filter(e -> e.getDept() == Dept.SALES)
                .toSortedList(Comparator.comparingInt(Employee::getId));
        List<String> result = FluentIterable.from(filtered)
                .transform(Employee::getName).toList();

        assertThat(result, is(Arrays.asList("Daniel", "Bob", "Kevin", "Tim")));
    }
}
メソッド対応表

抽出

クラスメソッド
JDK8Streamfilter(Predicate<? super T> predicate)
GS CollectionsMutableListselect(Predicate<? super T> predicate)
selectWith(Predicate2<? super T,? super P> predicate,P parameter)
GuavaFluentIterablefilter(Predicate<? super E> predicate)
Iterablesfilter(Iterable<T> unfiltered, Predicate<? super T> predicate)

並び替え

クラスメソッド
JDK8Streamsorted()
sorted(Comparator<? super T> comparator)
GS CollectionsMutableListsortThis()
sortThis(Comparator<? super T> comparator)
sortThisBy(Function<? super T,? extends V> function)
GuavaFluentIterabletoSortedList(Comparator<? super E> comparator)

変換

クラスメソッド
JDK8Streammap(Function<? super T,? extends R> mapper)
GS CollectionsMutableListcollect(Function<? super T,? extends V> function)
collectWith(Function2<? super T,? super P,? extends V> function, P parameter)
GuavaFluentIterabletransform(Function<? super E,T> function)
Iterablestransform(Iterable<F> fromIterable, Function<? super F,? extends T> function)
  • JDK8では、はじめにStreamを生成し、抽出・並び替え・変換の各種操作後、最後に結果を返すという処理の流れになる

  • GS Collectionsの場合は、抽出・並び替え・変換の各種メソッドはMutableListなどから直接呼べる(つまり、JDK8の場合のようにはじめにStreamを生成する必要はない。ただし、今回の例ではテストデータemployeeListが標準のListのため、はじめにMutableListを生成する必要がある)

    • GS Collectionsには、他にもImmutableListや遅延評価のLazyIterableなどもあり、一部のメソッドを除きMutableListと同様に使用できる
  • GuavaではFluentIterableまたはIterablesを用いることができるが、FluentIterableはメソッドチェーンでより簡潔に記述できる

実例2

社員のリストから部署ごとの社員数を求める

JDK8
public class JDK8Test extends EmployeeDomainForTest {
    @Test
    public void getEmployeeCountByDept() {
        Map<Dept, Long> result = this.employeeList.stream().collect(
                Collectors.groupingBy(Employee::getDept, Collectors.counting()));

        assertThat(result.get(Dept.ACCOUNTS), is(1L));
        assertThat(result.get(Dept.GENERAL_AFFAIRS), is(2L));
        assertThat(result.get(Dept.HUMAN_RESOURCES), is(3L));
        assertThat(result.get(Dept.SALES), is(4L));
    }
}
GS Collections
public class GSCollectionsTest extends EmployeeDomainForTest {
    @Test
    public void getEmployeeCountsByDept() {
        Bag<Dept> result = FastList.newList(this.employeeList)
                .collect(Employee::getDept).toBag();

        assertThat(result.occurrencesOf(Dept.ACCOUNTS), is(1));
        assertThat(result.occurrencesOf(Dept.GENERAL_AFFAIRS), is(2));
        assertThat(result.occurrencesOf(Dept.HUMAN_RESOURCES), is(3));
        assertThat(result.occurrencesOf(Dept.SALES), is(4));
    }
}
Guava
public class GuavaTest extends EmployeeDomainForTest {
    @Test
    public void getEmployeeCountsByDept() {
        Iterable<Dept> transformed = Iterables.transform(this.employeeList,
             Employee::getDept);
        Multiset<Dept> result = HashMultiset.create(transformed);

        assertThat(result.count(Dept.ACCOUNTS), is(1));
        assertThat(result.count(Dept.GENERAL_AFFAIRS), is(2));
        assertThat(result.count(Dept.HUMAN_RESOURCES), is(3));
        assertThat(result.count(Dept.SALES), is(4));
    }
}
メソッド対応表

要素の出現回数

クラスメソッド
JDK8--
GS CollectionsBagoccurrencesOf(Object item)
GuavaMultisetcount(Object element)
  • GS CollectionsとGuavaでは、それぞれBag, Multiset(要素の重複を許可するSet)を用いることで要素ごとの出現回数を求めることができる
  • JDKには上記に相当する型はないが、Collectors#groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)の第2引数にCollectors#counting()を渡すことによって、グループごとの出現回数をMapで取得できる

まとめ

実例を用いてJDK8, GS Collections, Guavaの使用方法の一部を確認しました。すべてを併用する状況はあまりないかもしれませんが、それぞれ適切に使い分けられるように違いを把握しておくことが大切だと思います。今回の例では使用しなかったメソッドもまだまだたくさんあるので、さらに理解を深めていきたいです。

Javaエンジニア養成読本[現場で役立つ最新知識、満載!]

Javaエンジニア養成読本[現場で役立つ最新知識、満載!]