【Java8】 .propertiesファイルからキャストしつつ値を取得する

環境

$ java -version
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (build 1.8.0_212-8u212-b03-0ubuntu1.18.04.1-b03)
OpenJDK 64-Bit Server VM (build 25.212-b03, mixed mode)

結論

以下のPropertiesWrapperがそれ用のクラス。

import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.Double;
import java.lang.Integer;
import java.util.Properties;
import java.util.function.Function;

public class PropertiesTest {
    public static void main(String[] args) {
        PropertiesWrapper props = new PropertiesWrapper("./test.properties");
        
        String foo = props.getValue("foo", "defaultValue", (s) -> s);
        int bar = props.<Integer>getValue("bar", 0, (s) -> Integer.parseInt(s));
        double baz = props.<Double>getValue("baz", 0.1, (s) -> Double.parseDouble(s));
        
        System.out.println(foo);
        System.out.println(bar);
        System.out.println(baz);
    }
}

class PropertiesWrapper {
    private Properties props;
    
    public PropertiesWrapper(String filepath) {
        this.props = new Properties();
        
        try (InputStream inputStream = new FileInputStream(filepath)){
            props.load(inputStream);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public <T> T getValue(String key, T defaultValue, Function<String, T> castFunc) {
        String value = props.getProperty(key);
        T cast;
        try{
            cast = castFunc.apply(value);
        }catch(Exception e){
            cast = defaultValue;
        }
        return cast;
    }
}

過程

↓みたいに色んな型の値がある.propertiesファイルがあるとする。

foo=x
bar=100
baz=5.0

Javaで.propertiesファイルから読んだ値はString型で取得されるため、数値を読み込むにはキャストする必要がある。

Properties props = new Properties();
String value = props.getProperty("key");

int x = Integer.parseInt(value);
double y = Double.parseDouble(value);
float z = Float.parseFloat(value);

取得した値がキャストできないとNumberFormatExceptionが発生するので変換処理を集約したい。そこで以下のようなjava.util.Propertiesクラスのラッパークラスを作る。getValue()で.propertiesファイルから値を取得する。

class PropertiesWrapper {
    private Properties props;
    
    public PropertiesWrapper(String filepath) {
        this.props = new Properties();
        
        try (InputStream inputStream = new FileInputStream(filepath)){
            props.load(inputStream);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public Object getValue(String key) {
        // このメソッドにキャスト処理を集約したい
        return props.getProperty(key);
    }
}

getValue()の中でキャストしたりエラーハンドリングしたりしたい。先に書いたように、数値型へのキャストはprimitive型のラッパークラスを用いる。なのでジェネリクスを使ってみる。長いのでメソッド部分だけ。

public <T> T getValue(String key) {
    String value = props.getProperty(key);
    return (T) value;
}

.propertiesファイルから取得した値を型パラメータTでキャストして返している(コンパイル時に未検査のキャストの警告が出る)。呼ぶときは例えばpropertiesWrapper.<Integer>getValue("bar")などとして呼ぶ。実行すると

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
    at PropertiesTest.main(PropertiesTest.java:12)

みたいなエラーが出る。java.lang.Stringクラスとjava.lang.Numberクラスには何の関連もないので(T)みたいなキャストは無効。なので、キャストする方法も一緒に渡してみる。

public <T> T getValue(String key, Function<String, T> castFunc) {
    String value = props.getProperty(key);
    T cast = castFunc.apply(value);
    return cast;
}

String型を受け取りT型の値を返すFunction型の関数型インターフェースcastFuncを受け取るようにした。実際に動かしてみる。

public class PropertiesTest {
    public static void main(String[] args) {
        PropertiesWrapper props = new PropertiesWrapper("./test.properties");
        
        int bar = props.<Integer>getValue("bar", (s) -> Integer.parseInt(s));
        System.out.println(bar); // -> 100
    }
}

test.propertiesにあるbarの値をInteger.parseIntでキャストする。ラムダ式の部分はメソッド参照(Integer::parseInt)でもいいけど、メソッド参照はラムダ式よりも遅いってどっかで見た。やったら実際遅かった。ちなみにちゃんとしたT型を返すようになったので、さっき出ていた未検査のキャストの警告は消えている。

キャストにミスったときはデフォルトの値を返すことにして、エラーハンドリングも加味した結果、以下のようになった。

public <T> T getValue(String key, T defaultValue, Function<String, T> castFunc) {
    String value = props.getProperty(key);
    T cast;
    try{
        cast = castFunc.apply(value);
    }catch(Exception e){
        cast = defaultValue;
    }
    return cast;
}

感想

結局.propertiesファイルから取得するときにキャスト用の関数をその都度渡しているのがなんか…。でもキャストと言いつつ、castFuncの実装によっては適当なエラーチェックなどを挟めるのはいいかもしれない。まあこのタイミングでやることか?という疑問はある。