一度だけ使用する場合、文字列定数を定義する必要がありますか?


24

Xaxを使用してアプリケーションのデータモデルにアクセスできるようにするJaxen(JavaのXPathライブラリ)のアダプターを実装しています。

これは、文字列(Jaxenから渡された)をデータモデルの要素にマップするクラスを実装することによって行われます。合計で1000以上の文字列比較を行う約100のクラスが必要になると推定されます。

これを行う最良の方法は、各文字列を定数として定義するのではなく、コードに直接書き込まれた文字列を使用したif / elseステートメントを単純にすることだと思います。例えば:

public Object getNode(String name) {
    if ("name".equals(name)) {
        return contact.getFullName();
    } else if ("title".equals(name)) {
        return contact.getTitle();
    } else if ("first_name".equals(name)) {
        return contact.getFirstName();
    } else if ("last_name".equals(name)) {
        return contact.getLastName();
    ...

ただし、文字列値をコードに直接埋め込むのではなく、文字列定数を作成することを常に教えられました。これは次のようになります。

private static final String NAME = "name";
private static final String TITLE = "title";
private static final String FIRST_NAME = "first_name";
private static final String LAST_NAME = "last_name";

public Object getNode(String name) {
    if (NAME.equals(name)) {
        return contact.getFullName();
    } else if (TITLE.equals(name)) {
        return contact.getTitle();
    } else if (FIRST_NAME.equals(name)) {
        return contact.getFirstName();
    } else if (LAST_NAME.equals(name)) {
        return contact.getLastName();
    ...

この場合、それは悪い考えだと思います。定数は、getNode()メソッド内で一度だけ使用されます。文字列を直接使用することは、定数を使用するのと同じくらい読みやすく理解しやすく、少なくとも1,000行のコードを書く手間を省きます。

それで、1回の使用のために文字列定数を定義する理由はありますか?または、文字列を直接使用しても問題ありませんか?


PS。代わりに列挙型を使用することを提案する前に、プロトタイプを作成しましたが、列挙型変換は単純な文字列比較よりも15倍遅いため、考慮されていません。


結論: 以下の回答は、この質問の範囲を単なる文字列定数を超えて拡大したため、2つの結論があります。

  • このシナリオでは、文字列定数ではなく文字列を直接使用しても問題ありませんが、
  • 文字列をまったく使用しないようにする方法があります。

そこで、文字列を完全に回避するラッパーテクニックを試します。残念ながら、まだJava 7を使用していないため、文字列switchステートメントを使用できません。ただし、最終的には、各手法を試し、そのパフォーマンスを評価することが最善の答えだと思います。現実には、1つの手法が明らかに速い場合は、その美しさや慣習の順守に関係なく、おそらくそれを選択します。


3
ステートメントを使用する場合、1000を手動でキーボード入力する予定はありませんか?
-JeffO

1
一部の言語ではこの単純なことがどれほど不快なことか、とても悲しいと思います…
ジョンパーディ

5
Java 7では、文字列をswitchラベルとして使用できます。ifカスケードの代わりにスイッチを使用します。
モニカの復活-M.シュレーダー

3
文字列を列挙値に変換すると、列挙変換は15倍遅くなります!enumを直接渡し、同じタイプの別のenum値と比較してください!
ニール

2
HashMapのような匂いが解決策になります。
MarioDS

回答:


5

これを試して。最初のリフレクションは確かに費用がかかりますが、何度も使用する場合は、これは間違いなく、提案しているものよりも優れたソリューションです。私はリフレクションを使用するのは好きではありませんが、リフレクションの代替手段が気に入らないときは自分でそれを使用していることに気づきます。これにより、チームの頭痛の種が大幅に軽減されると思いますが、メソッド名を小文字で渡す必要があります。

つまり、getメソッドの名前は「getFullName()」であるため、「name」を渡すのではなく、「fullname」を渡すことになります。

Map<String, Method> methodMapping = null;

public Object getNode(String name) {
    Map<String, Method> methods = getMethodMapping(contact.getClass());
    return methods.get(name).invoke(contact);
}

public Map<String, Method> getMethodMapping(Class<?> contact) {
    if(methodMapping == null) {
        Map<String, Method> mapping = new HashMap<String, Method>();
        Method[] methods = contact.getDeclaredMethods();
        for(Method method : methods) {
            if(method.getParameterTypes().length() == 0) {
                if(method.getName().startsWith("get")) {
                    mapping.put(method.getName().substring(3).toLower(), method);
                } else if (method.getName().startsWith("is"))) {
                    mapping.put(method.getName().substring(2).toLower(), method);
                }
            }
        }
        methodMapping = mapping;
    }
    return methodMapping;
}

連絡先のメンバーに含まれるデータにアクセスする必要がある場合は、必要な情報にアクセスするためのすべてのメソッドを持つ連絡先のラッパークラスを構築することを検討してください。これは、アクセスフィールドの名前が常に同じであることを保証するのにも役立ちます(つまり、ラッパークラスにgetFullName()があり、fullnameで呼び出す場合、連絡先のgetFullName()の名前が変更されていても常に機能します-itそれを行う前にコンパイルエラーが発生します)。

public class ContactWrapper {
    private Contact contact;

    public ContactWrapper(Contact contact) {
        this.contact = contact;
    }

    public String getFullName() {
        return contact.getFullName();
    }
    ...
}

このソリューションにより、jsfデータテーブルで使用する単一のデータ表現が必要な場合や、そのデータをジャスパーを使用してレポートにエクスポートする必要がある場合(経験上、複雑なオブジェクトアクセサーをうまく処理できない場合) 。


viaというメソッドを持つラッパーオブジェクトのアイデアが気に入っ.invoke()ています。文字列定数を完全になくすからです。マップをセットアップするためにランタイムリフレクションにあまり熱心ではありませんgetMethodMapping()が、staticブロックで実行することでシステムが実行されるのではなく、起動時に発生するようになるかもしれません。
ガッチ

@gutch、ラッパーパターンは、インターフェイス/コントローラーに関連する多くの問題を解決する傾向があるため、私がよく使用するパターンです。インターフェースは常にラッパーを使用でき、それに満足することができ、その間にコントローラーを裏返すことができます。知っておく必要があるのは、インターフェイスで使用できるデータだけです。繰り返しになりますが、私は通常、リフレクションは好きではありませんが、それがWebアプリケーションの場合、クライアントがその待機時間をまったく表示しないため、起動時に実行することは完全に受け入れられます。
ニール

@Neil Apache commonsのBeanUtilsを使用しないのはなぜですか?また、埋め込みオブジェクトもサポートしています。データ構造全体obj.attrA.attrB.attrNを通過できます。他にも多くの可能性があります
:

Mapsでのマッピングの代わりに、@ Annotationsを使用します。JPAのようなものです。特定のattrまたはgetterを使用してコントローラーエントリ(文字列)をマッピングするための独自の注釈を定義します。注釈を使用するのは非常に簡単で、Java 1.6から入手できます(私は思う)
-Laiv

5

可能な場合は、switch文で文字列を使用できるJava 7を使用してください。

http://docs.oracle.com/javase/tutorial/java/nutsandbolts/switch.htmlから

public class StringSwitchDemo {

    public static int getMonthNumber(String month) {

        int monthNumber = 0;

        if (month == null) {
            return monthNumber;
        }

        switch (month.toLowerCase()) {
            case "january":
                monthNumber = 1;
                break;
            case "february":
                monthNumber = 2;
                break;
            case "march":
                monthNumber = 3;
                break;
            case "april":
                monthNumber = 4;
                break;
            case "may":
                monthNumber = 5;
                break;
            case "june":
                monthNumber = 6;
                break;
            case "july":
                monthNumber = 7;
                break;
            case "august":
                monthNumber = 8;
                break;
            case "september":
                monthNumber = 9;
                break;
            case "october":
                monthNumber = 10;
                break;
            case "november":
                monthNumber = 11;
                break;
            case "december":
                monthNumber = 12;
                break;
            default: 
                monthNumber = 0;
                break;
        }

        return monthNumber;
    }

    public static void main(String[] args) {

        String month = "August";

        int returnedMonthNumber =
            StringSwitchDemo.getMonthNumber(month);

        if (returnedMonthNumber == 0) {
            System.out.println("Invalid month");
        } else {
            System.out.println(returnedMonthNumber);
        }
    }
}

私は測定していませんが、switchステートメントは比較の長いリストではなくジャンプテーブルにコンパイルされると信じています。これはさらに速いはずです。

実際の質問に関して:一度だけ使用する場合は、定数にする必要はありません。ただし、定数を文書化してJavadocに表示できることを考慮してください。これは重要な文字列値にとって重要です。


2
ジャンプテーブルについて。文字列スイッチはスイッチに置き換えられ、最初はハッシュコードに基づいて(同じハッシュコードを持つすべての定数の等式がチェックされます)、ブランチインデックスを選択し、2番目のスイッチはブランチインデックスを切り替えて元のブランチコードを選択します。後者は明らかにブランチテーブルに適していますが、前者はハッシュ関数の分布によるものではありません。したがって、パフォーマンス上の利点は、おそらくハッシュベースの実現によるものです。
スカーフリッジ

非常に良い点。うまく機能する場合は、このためだけにJava 7に移行する価値があるかもしれません
...-gutch

4

これを維持する(これまでに重要な変更を行う)場合は、何らかの注釈駆動型コード生成(おそらくCGLib使用)またはすべてのコードを作成するスクリプトを使用することを検討するかもしれません。あなたが検討しているアプローチで忍び込む可能性のあるタイプミスとエラーの数を想像してください...


既存のメソッドに注釈を付けることを検討しましたが、一部のマッピングは複数のオブジェクトをトラバースします(たとえば、「country」へのマッピングobject.getAddress().getCountry())。これは注釈で表すのが困難です。if / else文字列比較はきれいではありませんが、高速で柔軟性があり、理解しやすく、単体テストが簡単です。
ガッチ

1
あなたはタイプミスやエラーの可能性について正しいです。私の唯一の防御はユニットテストです。もちろん、それはさらに多くのコードを意味します
...-gutch

2

クラスの上部で定義された定数を引き続き使用します。必要に応じて後で変更できるものを簡単に確認できるため、コードのメンテナンス性が向上します。たとえば、後に"first_name"なってしまう可能性があります"firstName"


ただし、このコードが自動的に生成され、定数が他の場所で使用されない場合、それは問題ではありません(OPは、100クラスでこれを行う必要があると言います)。
-NoChance

5
どちらの場合でも、「維持可能性」の角度がここで一度だけ「first_name」から「givenName」に変更されることはありません。あなたは今、二つの場所で3つの変更持っているので、あなたはおそらく同様に変更するように文字列「givenName属性」
ジェームズ・アンダーソン

1
適切なIDEでは、これらの変更は簡単です。私が提唱しているのは、クラスの最上部で定数を宣言するのに時間をかけ、クラス内の残りのコードを読む必要がないため、これらの変更を行う場所がより明確であることですこれらの変更を行います。
バーナード

ただし、ifステートメントを読んでいるときは、戻って定数に含まれていると思われる文字列が含まれていることを確認する必要があります。ここには何も保存されていません。
ジェームズアンダーソン

1
おそらく、しかし、これは私が私の定数によく名前を付ける理由です。
バーナード

1

名前が一貫している場合(別名"some_whatever"は常ににマップされるgetSomeWhatever())、リフレクションを使用してgetメソッドを決定および実行できます。


より良いgetSome_whatever()。ラクダのケースを壊しているかもしれませんが、反射が機能することを確認することがはるかに重要です。さらに、「どうしてこんなやり方でやったのか・・・」と言うことができるという追加の利点があります。
ニール

0

注釈がなくても、注釈処理が解決策になると思います。それはあなたのためにすべての退屈なコードを生成できるものです。欠点は、N個のモデルクラスに対してN個の生成クラスが得られることです。また、既存のクラスには何も追加できませんが、

public Object getNode(String name) {
    return SomeModelClassHelper.getNode(this, name);
}

クラスごとに1回は問題になりません。あるいは、次のように書くこともできます

public Object getNode(String name) {
    return getHelper(getClass()).getNode(this, name);
}

共通のスーパークラスで。


コード生成に注釈処理の代わりにリフレクションを使用できます。欠点は、リフレクションを使用する前にコードをコンパイルする必要があることです。これは、スタブを生成しない限り、モデルクラスで生成されたコードに依存できないことを意味します。


また、リフレクションを直接使用することも検討します。確かに、反射は遅いですが、なぜ遅いのですか?フィールド名をオンにするなど、必要なすべてのことを行う必要があるためです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.