「お題:文字列を先頭から見て同じところまで除去」をJavaで挑戦してみた

お題:文字列を先頭から見て同じところまで除去 - No Programming, No Life

先にソートしたら楽じゃね?と思ったのでやってみた。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Odai{
    
    public static List<String> hoge( final List<String> arg ){
        List<String> copy = new ArrayList<String>( arg );
        Collections.sort( copy );
        String first = copy.get( 0 );
        String last = copy.get( copy.size() - 1 );
        int length = first.length() < last.length() ? first.length() : last.length();
        int index = 0;
        while( index < length && first.charAt( index ) == last.charAt( index ) )index++;
        List<String> res = new ArrayList<String>(arg.size());
        for( String s : arg )res.add( s.substring( index ) );
        return res;    
    }
}

ふつうに書くとこう?

import java.util.ArrayList;
import java.util.List;

public class Odai{

    public static List<String> hoge( final List<String> arg ){
        int length = arg.get( 0 ).length();
        for( int i = 1; i < arg.size(); i++ ){
            length = length < arg.get( i ).length() ? length : arg.get( i ).length();
            int index = 0;
            for( ; index < length && arg.get( 0 ).charAt( index ) == arg.get( i ).charAt( index ); index++ ){}
            length = index;
        }
        List<String> res = new ArrayList<String>(arg.size());
        for( String s : arg )res.add( s.substring( length ) );
        return res; 
    }
}

テストコード

import java.util.Arrays;
import java.util.List;
import junit.framework.TestCase;

public class OdaiTest extends TestCase{
    
    public void testHoge(){
        List<String> res;
        res = Odai.hoge( Arrays.asList( "abcdef", "abc123" ) );
        
        assertEquals( "def", res.get( 0 ) );
        assertEquals( "123", res.get( 1 ) );
        
        res = Odai.hoge( Arrays.asList( "あいうえお", "あいさんさん", "あいどる" ) );
        
        assertEquals( "うえお", res.get( 0 ) );
        assertEquals( "さんさん", res.get( 1 ) );
        assertEquals( "どる", res.get( 2 ) );
        
        res = Odai.hoge( Arrays.asList( "12345", "67890", "12abc" ) );
        
        assertEquals( "12345", res.get( 0 ) );
        assertEquals( "67890", res.get( 1 ) );
        assertEquals( "12abc", res.get( 2 ) );
    }
}

Javaの文字列結合について

第3回Effective Java読書会で項目51「文字列結合のパフォーマンスに用心する」がテーマに上がったのでそのあたりのお話を少しまとめておこうかと思います。

まず、Effective Javaの項目51には

  1. 文字列結合演算子は(+演算子)は便利だけど、使いどころを間違えるとパフォーマンスに影響を与える
  2. そんな場合はStringBuilderを使用する

ということがかかれています。


1に関して、どのような場合にパフォーマンスに影響を与えるかというと、主に以下のようなループ内で文字列を+演算子で結合する場合があげられると思います。

String result = "";
for( int i = 0; i < 10; i++ ){
    result += i;  //String結合
}


なぜこのような場合にパフォーマンスに影響を与えるかというと、文字列結合に+演算子を使用した場合にはコンパイル時にStringBuilderのappendメソッドを使用した処理に置き換えられ、実質的に以下のようなコードに変換されます。*1

String result = "";
for( int i = 0; i < 10; i++ ){
    StringBuilder sb = new StringBuilder( result );
    sb.append( i );
    result = sb.toString();
}

n回のループに対してn個のStringBuilderのインスタンスが生成されてしまうことになり、その分処理が遅くなってしまうのは明らかです。


このような場合には2に書かれているようにStringBuilderを使用した処理にしておくのが妥当です。これであればStringBuilderは最初に一つ作るだけで、無駄なStringBuilderのインスタンス生成処理でCPUやメモリ領域が使用されません。

StringBuilder sb = new StringBuilder();
for( int i = 0; i < 10; i++ ){
    sb.append( i );
}
String result = sb.toString();


ちなみに、StringBuilderとよく似たクラスにStringBufferというクラスがあり、こちらは同期化処理を含んだ実装になっています。しかし、StringBuilder(StringBuffer)を複数のスレッド間で共有するということはまず無いと思うので、基本的にStringBuilderを使っておけば良いと思います。*2


ただし、文字列リテラル(と文字列定数)だけを結合する場合は+演算子を使用したうがパフォーマンスが良かったりします。例えば以下のようなコード

String text2 = "1234" + "5678";
}

であれば、コンパイル時にコンパイラがあらかじめ文字列結合した結果に変換してくれるため、実質的に以下のようなコードに変換されます。

String text2 = "12345678";
}

このような場合なら、わざわざ実行時にStringBuilderを使用して結合するよりかは、+演算子を使用してコンパイル時に結合してしまうほうがパフォーマンスが良いでしょう。


まとめ

  1. パフォーマンスが重要でない場合以外は、文字列の結合にStringBuilderを使用する
  2. 文字列リテラルだけを結合する場合は文字列結合演算子(+演算子)を使用する

Effective Java 第2版 (The Java Series)

Effective Java 第2版 (The Java Series)

*1:Javaのバージョンによっては挙動が異なるかもしれません。確認に使用したJavaのバージョンは1.6.0_24

*2:ちなみにStringBuilderはJava1.5から導入されたクラスなので、それ以前はStringBufferしかありません

MavenプロジェクトでGroovyを使う

Groovyを使ってJavaプログラムのテストコードを、と言われる割にはどのように設定すればgroovyファイルをビルド出来るのかという詳しい情報が見当たらなかったので調べたことをまとめます。

今回の環境
Maven 3.0.3
NetBeans7.0
Groovy 1.8


mavenプロジェクトでGroovyファイルをコンパイルするには、gmavenを使用しますが別途インストールなどをする必要はありません。
まずは、Mavenのpom.xmlファイルに以下の設定を追加します。

/project/build/plugins下

<plugin>
  <groupId>org.codehaus.gmaven</groupId>
  <artifactId>gmaven-plugin</artifactId>
  <version>1.3</version>
  <executions>
    <execution>
      <goals>
        <goal>generateStubs</goal>
        <goal>compile</goal>
        <goal>generateTestStubs</goal>
        <goal>testCompile</goal>
      </goals>      
      <configuration>
        <providerSelection>1.7</providerSelection>
      </configuration>
    </execution>
  </executions>      
  <dependencies>
    <dependency>
      <groupId>org.codehaus.groovy</groupId>
      <artifactId>groovy-all</artifactId>
      <version>1.8.0</version>
    </dependency>
  </dependencies>
</plugin>

/project/dependencies下

<dependency>
  <groupId>org.codehaus.groovy</groupId>
  <artifactId>groovy-all</artifactId>
  <version>1.8</version>
</dependency>

これでpom.xmlの設定は完了です。
動作の確認のため、試しにgroovyクラスを作成します。
まずは、groovyファイルを置くためにsrcディレクトリの下にgroovyディレクトリを追加します。基本的に、groovyファイルはsrc/groovyディレクトリ内に置いておかないとコンパイルされないので注意が必要です。(テストの場合はtest/groovy)
groovyディレクトリを追加すると、NetBeansの場合はGroovyパッケージが表示されるはずなので、ここにgroovyファイルを追加していきます。


Groovyプラグインをインストールしてあれば、Groovyクラスが作成できるので、取り敢えず"sample"という名前のフォルダを追加し、その下に新規Groovyクラスを追加します。

試しに追加したファイルに"Hello Groovy"と出力する簡単なプログラムを記述して、Shift+F6でファイルを実行してみましょう。
NewGrrovyClass.groovy

package sample

class Test {
    
  def hello(){
    println "Hello Groovy"
  }
}    

def instance = new Test()
instance.hello()

コンパイルが実行され、"Hello Groovy"と出力されれば成功です。

Scanning for projects...
                                                                        
------------------------------------------------------------------------
Building groovyinjava 1.0-SNAPSHOT
------------------------------------------------------------------------

[gmaven-plugin:generateStubs]
Generated 2 Java stubs

[resources:resources]
Using 'UTF-8' encoding to copy filtered resources.
skip non existing resourceDirectory /Volumes/WB1T/valiant/NetBeansProjects/groovyinjava/src/main/resources

[compiler:compile]
Compiling 1 source file to /Volumes/WB1T/valiant/NetBeansProjects/groovyinjava/target/classes

[gmaven-plugin:compile]
Compiled 2 Groovy classes

[exec:exec]
Hello Groovy
------------------------------------------------------------------------
BUILD SUCCESS
------------------------------------------------------------------------
Total time: 4.319s
Finished at: Mon Jun 06 01:13:20 JST 2011
Final Memory: 11M/81M
------------------------------------------------------------------------

もちろんJava側からも呼べますし、入力補完も効きます。
App.java

package jp.navalvessel.groovyinjava;

import sample.Test;

public class App 
{
    public static void main( String[] args )
    {
        Test instance = new Test();
        instance.hello();
    }
}

Mac上のNetBeansにScalaの開発環境を構築する

今回構築した環境は以下の通り
MacOSX 10.6
NetBeans 7.0
Scala 2.8
MacPorts 1.9.2

MacPortsをインストールする

オライリーのプログラミングScalaに「ScalaのインストールはMacPorts使うと簡単だよ!」と書いてあったので、まずはMacPortsのサイトからパッケージのインストーラをダウンロードし、インストールします。

Scalaをインストールする

MacPortsのインストールが終わった後、ターミナル上で次のコマンドを実行してScalaをインストールします。

sudo port install scala28

このままだと、ターミナル上でscalaコマンドなどを実行できないので、/usr/local/binにシンボリックリンクを作成します。
必要に応じて他のファイルのシンボリックリンクを作成してください。

sudo ln -s /opt/local/bin/scala-2.8 /usr/local/bin/scala
sudo ln -s /opt/local/bin/scalac-2.8 /usr/local/bin/scalac

シンボリックリンクの作成後、ターミナル上で「scala -version」を実行して以下のような出力がされればOK。

Scala code runner version 2.8.1.final -- Copyright 2002-2010, LAMP/EPFL

次に、「~/.MacOSX/environment.plist」に環境変数SCALA}HOMEを設定します。
ディレクトリ及びファイルが存在しない場合は、新しく作成してください。
environment.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>SCALA_HOME</key>
    <string>/opt/local/share/scala-2.8</string>
  </dict>
</plist>

environment.plistを編集して保存したら、設定を有効にするためログインし直してください。
以上で、Scalaのインストールは終了です。

NetBeansScalaプラグインをインストールする

NetBeansのサイトからプラグインのzipファイルをダウンロードして解凍します。
NetBeansのプラグインダイアログを開き、ダウンロード済みタブのプラグインの追加をクリックし、解凍したディレクトリ内のnbmファイルをすべて選択し、インストールします。
以上で、Scalaプラグインのインストールは終了です。

大阪EffectiveJava読書会 第1回

本日、初めての読書会として大阪で行われたEffectiveJava読書会 第1回に行ってきました。
一言に読書会と言っても、一人ずつ音読したり、各々が黙々と読んで質問したりすると色々とスタイルがあるらしく、今回の私が参加したこの読書会は、3人のチーム*4組に別れてEffectiveJavaの各章どれか一つについて1時間程度でまとめて発表するというスタイルでした。
そんなわけで私は主催者の@irofさん、@meganiiさんと共に第六章、主に項目30のenumについて発表したので、そのあたりを中心にまとめました。

  • 項目30 int定数の代わりにenumを使用する

javaにおけるenumの大きな特徴として、主に次の2点が挙げられます。

  1. 型安全性の保証
  2. データ・振る舞いを持つことができる

まずは型安全性の保障の保証について。

enumが実装されるJava1.4以前ではint enumパターンと呼ばれる技法が使用されていました。

//int enumパターン
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

このパターンをにはどのような問題があるかというと、定数値がただのint型であるために異なるドメインの定数値を誤って混在させてしまっても、コンパイラがこの誤りを検出することが出来ません。

//example
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

public static final int ORANGE_NAVAL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;

//コンパイル可能
private int apple = ORANGE_NAVAL;


一方、enumを使用することでこのような誤りをコンパイラによって検出することが可能になり、型安全性が保証されます。

//example
public enum Apple{ FUJI, PIPIN, GRANNY_SMITH };
public enum Orange{ NAVEL, TEMPLE, BLOOD };

//コンパイルエラー!
public Apple apple = Orange.NAVAL;


static importを使用すると頭のOrangeやAppleを省略できるため、int enumパターンと同じように記述できます。

import static Apple.*;

public Apple apple = FUJI;


次に、javaenum型はC言語C++enumとは異なり、データ・振る舞いを持つことが可能です。

//データを持つenum
public enum Operation{
    PLUS( "+" ),
    MINUS( "-" );
    private String symbol;
    Operation( String symbol ){ this.symbol = symbol };
    @override
    public String toString(){ return symbol };
}
//振る舞いを持つenum
public enum Operation{
    PLUS { double apply( double x, double y ){ return x + y;}},
    MINUS { double apply( double x, double y ){ return x - y;}};
    abstract double apply( double x, double y );
}

このようにすることでswitch文を使用することなく定数と紐づいたメソッドを呼ぶことができます。まあストラテジパターンの簡単な実装といった所ですね。

//example
public double apply( double x, double y, Operation operation ){
    return operation.apply( x, y );
}


また、enum自身に振る舞いが実装されている場合、定数値が増えた場合にも switchブロックの修正も不要になります。

//振る舞いを持つenum
public enum Operation{
    PLUS { double apply( double x, double y ){ return x + y;}},
    MINUS { double apply( double x, double y ){ return x - y;}},
    TIMES { double apply( double x, double y ){ return x - y;}},
    DIVEDE { double apply( double x, double y ){ return x - y;}};
    abstract double apply( double x, double y );
}

//TIMESとDIVEDEが増えたけど変更しなくて良い
public double apply( double x, double y, Operation operation ){
    return operation.apply( x, y );
}

上のようなenum内で定義されたabstractメソッドの場合、実装しなければコンパイルエラーになるため、実装し忘れることはありません。
しかし、switch文による分岐で実装した場合、定数に対する実装を忘れてしまってもコンパイラは知らせてくれません。

//caseブロックを追加し忘れてもコンパイルは通る
public double apply( double x, double y, Operation operation ){
    switch( operation ){
        case PLUS: return x + y;
        case MINUS: return x - y;
    }
}

ただ、メソッドを定義できるのが便利と言っても、大量の処理を突っ込むなどのやり過ぎは良くないんじゃないかという話も。
そういう場合は素直にインターフェイスを実装したクラスに分ければいいのではないかと思います。


とまあ、発表では後半の具体的なコード*1まで出せませんでしたが、大体こんなような話だったような気がします。
このように便利なenum、どんどん使っていってもらいたいものです。

しかし

  • javaのバージョンがいまだに1.4
  • 誰もenum使ってない
  • enumを使った他人の分からないコードを書いちゃだめ
  • enumはコード規約で禁止

という話も・・・。

*1:このエントリの物は主にEffectiveJavaより抜粋

イベントを伝播するJScrollPane

JScrollPaneを入れ子構造にした場合、マウス位置の最も上にあるJScrollPaneがイベントを消費してしまい、結果スクロール途中のマウスの下に入れ子のJScrollPaneが来てしまうとスクロールを止められてしまいとても不便です。
そんなわで、上もしくは下までスクロールしきったJScrollPaneのイベントを下のコンポーネントにスルーしてくれるMouseWheelListenerを作ったのでメモ。

JScrollPaneをわざわざメンバー変数に保持してるけど、MouseWheelEventから取り出したほうがよかったかも。

import java.awt.Component;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;

/**
 * JScrollPaneがスクロール方向の限界値までスクロールしている場合に
 * 親コンポーネントへイベントを伝播させるMouseWheelListenerの実装です。
 *
 */
public class JScrollPaneHandler implements MouseWheelListener{

    private JScrollPane mTarget;

    /**
     * MouseWheelListenerExを生成します。
     * @param target このリスナを登録するJScrollPane
     */
    public JScrollPaneHandler( JScrollPane target ){
        mTarget = target;
    }

    /**
     * 登録されているJScrollPaneがスクロール方向の限界値までスクロールしている場合に
     * イベントを親コンポーネントに伝播させます。
     * @param e MouseWheelEvent
     */
    @Override
    public void mouseWheelMoved( MouseWheelEvent e ){
        JScrollBar vBar = mTarget.getVerticalScrollBar();

        int rotation = e.getWheelRotation();
        int current = vBar.getValue();
        int height = mTarget.getHeight();

        if( rotation > 0 ){         //下にスクロール
            int max = vBar.getMaximum() - height;
            if( current >= max ){   //限界値までスクロールしている
                dispatchParent( e );
            }
        }else if( rotation < 0 ){   //上にスクロール
            int min = vBar.getMinimum();
            if( current <= min ){   //限界値までスクロールしている
                dispatchParent( e );
            }
        }
    }

    /**
     * 登録されているJComponentの親コンポーネントにMouseWheelイベントを発行します。
     * 親コンポーネントが存在しない場合は何もしません。
     * @param e 発行するMouseWheelEvent
     */
    private void dispatchParent( MouseWheelEvent e ){
        Component parent = mTarget.getParent();
        if( parent != null ){
            parent.dispatchEvent( e );
        }
    }
}