如果只使用一次,是否应该定义一个字符串常量?


24

我们正在为Jaxen(Java的XPath库)实现一个适配器,该适配器允许我们使用XPath来访问应用程序的数据模型。

这是通过实现将字符串(从Jaxen传递给我们的)映射到数据模型的元素的类来完成的。我们估计我们将需要大约100个类,总共超过1000个字符串比较。

我认为,执行此操作的最佳方法是将字符串直接写入代码的简单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()方法中,该常数将仅使用一次。直接使用字符串与使用常量一样容易阅读和理解,并节省了我们编写至少一千行代码的时间。

那么,是否有任何理由一次定义字符串常量?还是直接使用字符串是否可以接受?


PS。在有人建议使用枚举代替之前,我们对它进行了原型设计,但是枚举转换比简单的字符串比较要慢15倍,因此我们不予考虑。


结论: 下面的答案扩展了这个问题的范围,不仅仅是字符串常量,所以我有两个结论:

  • 在这种情况下,直接使用字符串而不是字符串常量可能是可以的,但是
  • 有一些方法可以避免使用字符串,这可能更好。

因此,我将尝试完全避免使用字符串的包装技术。不幸的是,我们还不能使用字符串切换语句,因为我们还没有使用Java 7。但最终,我认为对我们来说最好的答案是尝试每种技术并评估其性能。现实情况是,如果一种技术明显更快,那么我们可能会选择它,而不管它的美观或遵循惯例如何。


3
您不打算手动输入1000 if语句吗?
JeffO 2012年

1
我感到很难过,这种简单的语言在某些语言中是多么令人不快……
Jon Purdy 2012年

5
Java 7允许使用字符串作为switch标签。使用开关而不是if级联。
恢复莫妮卡-M.Schröder'12

3
如果将字符串转换为枚举值,枚举转换将慢15倍!直接传递枚举,然后与另一个相同类型的枚举值进行比较!
尼尔

2
像HashMap这样的气味可以解决。
MarioDS

Answers:


5

尝试这个。最初的反射当然很昂贵,但是如果您打算多次使用它(我想您会使用),那么这无疑是您所提议的更好的解决方案。我不喜欢使用反射,但是当我不喜欢反射的替代方法时,我发现自己正在使用它。我确实认为这会为您的团队节省很多麻烦,但是您必须传递方法的名称(小写)。

换句话说,您将传递“ fullname”,而不是传递“ name”,因为get方法的名称是“ getFullName()”。

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()并且您使用全名进行调用,则即使联系人的getFullName()已重命名,它也将始终有效—会导致编译错误,然后再让您执行此操作)。

public class ContactWrapper {
    private Contact contact;

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

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

该解决方案为我节省了很多时间,即当我想在jsf数据表中使用单个数据表示形式时,以及当需要使用jasper将数据导出到报表中时(根据我的经验,这种方式不能很好地处理复杂的对象访问器) 。


我喜欢将包装器对象称为via的方法.invoke(),因为它完全消除了字符串常量。我不太热衷于运行时反射来设置映射,尽管也许可以getMethodMapping()在一个static块中执行可以在启动时而不是在系统运行时执行。
gutch

@gutch,包装模式是我经常使用的模式,因为它可以解决很多与接口/控制器有关的问题。接口总是可以使用包装器并且对此感到满意,同时控制器可以被内翻。您只需要知道界面中要提供哪些数据即可。再说一次,我要强调一点,我通常不喜欢反射,但是如果它是一个Web应用程序,那么在启动时就可以接受,因为客户端不会看到任何等待时间。
尼尔

@Neil为什么不使用Apache Commons的BeanUtils?它还支持嵌入式对象。您可以遍历整个数据结构obj.attrA.attrB.attrN,它还有许多其他可能:-)
Laiv

我会使用@Annotations而不是使用Maps进行映射。像JPA一样。定义我自己的注释,以使用特定的attr或getter映射控制器条目(字符串)。使用Annotation非常容易,并且可以从Java 1.6(我认为)获得它
Laiv

5

尽可能使用Java 7,它允许您在switch语句中使用字符串。

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
关于跳转表。将字符串开关替换为开关,首先基于哈希码(检查具有相同哈希码的所有常量是否相等),然后选择分支索引,第二步在分支索引上切换并选择原始分支代码。后者显然适用于分支表,前者不是由于散列函数的分布。因此,任何性能优势都可能归因于基于哈希的实现。
scarfridge '04 -4-24

很好的一点;如果性能良好,就应该为此而转向Java 7 ...
gutch

4

如果您要保持这一点(曾经做过任何不重要的更改),我实际上可能会考虑使用某种注释驱动的代码生成方式(也许通过CGLib),甚至只是使用一个脚本为您编写所有代码。想象一下您正在考虑的方法中可能出现的错别字和错误的数量...


我们考虑了对现有方法的注释,但是有些映射遍历了多个对象(例如,映射到的“国家” object.getAddress().getCountry()),而这些对象很难用注释来表示。if / else字符串比较不是很漂亮,但是它们是快速,灵活,易于理解和易于进行单元测试的。
gutch 2012年

1
您对错别字和错误的可能性是正确的;我唯一的防御就是单元测试。当然,这意味着还有更多代码……
gutch

2

我仍将使用在类顶部定义的常量。它使您的代码更具可维护性,因为在以后(如有必要)更容易看到可以更改的内容。例如,"first_name"可能会"firstName"在以后出现。


但是,我同意,如果该代码将自动生成并且常量未在其他地方使用,则没关系(OP表示他们需要在100个类中进行此操作)。
NoChance 2012年

5
我只是看不到“可维护性”的角度,在两种情况下都一次将“ first_name”更改为“ givenName”,但是在命名常量的情况下,现在剩下一个凌乱的变量“ first_name”,该变量引用字符串“ givenName”,因此您可能还希望更改它,因此现在在两个地方进行了三个更改
James Anderson

1
使用正确的IDE,这些更改是微不足道的。我要提倡的是,在哪里进行这些更改会更明显因为您已经花了时间在类的顶部声明常量,而不必通读类中的其余代码即可进行这些更改。
伯纳德

但是,当您阅读if语句时,您必须返回并检查常量是否包含您认为包含的字符串-此处未保存任何内容。
詹姆斯·安德森

1
也许,但这就是为什么我很好地命名常数的原因。
伯纳德'04

1

如果您的命名是一致的(aka "some_whatever"始终映射到getSomeWhatever()),则可以使用反射来确定并执行get方法。


更好的getSome_whatever()。可能会破坏骆驼的情况,但确保反射有效更为重要。加上它还有一个额外的优点,它使您说:“为什么我们这样做了。哦,等等。等等!乔治不要更改该方法的名称!”
尼尔

0

我想,即使没有注释,注释处理也可能是解决方案。可以为您生成所有无聊代码的东西。缺点是您将获得N个模型类的N个生成的类。您也不能将任何内容添加到现有的类中,而是编写类似

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

每个班一次应该不是问题。或者,您可以编写类似

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

在一个普通的超类中。


您可以使用反射代替注释处理来生成代码。缺点是您需要先编译代码,然后才能对其进行反射。这意味着除非生成一些存根,否则您不能依赖模型类中生成的代码。


我也考虑直接使用反射。当然,反射很慢,但是为什么会慢呢?这是因为它必须完成您需要做的所有事情,例如,打开字段名称。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.