字符串格式化中的命名占位符

And*_*ndy 153 java string-formatting

在Python中,当格式化字符串时,我可以按名称而不是按位置填充占位符,如下所示:

print "There's an incorrect value '%(value)s' in column # %(column)d" % \
  { 'value': x, 'column': y }
Run Code Online (Sandbox Code Playgroud)

我想知道这是否可以用Java(希望没有外部库)?

sch*_*hup 132

如果您的值已经正确格式化,那么jakarta commons lang的StrSubstitutor是一种轻量级的方法.

http://commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/apache/commons/lang3/text/StrSubstitutor.html

Map<String, String> values = new HashMap<String, String>();
values.put("value", x);
values.put("column", y);
StrSubstitutor sub = new StrSubstitutor(values, "%(", ")");
String result = sub.replace("There's an incorrect value '%(value)' in column # %(column)");
Run Code Online (Sandbox Code Playgroud)

以上结果如下:

"第2列中的值'1'不正确"

使用Maven时,您可以将此依赖项添加到pom.xml:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
</dependency>
Run Code Online (Sandbox Code Playgroud)

  • 旧线程,但从3.6开始,文本包已弃用,以支持commons-text.https://commons.apache.org/proper/commons-text/ (6认同)
  • 我发现令人失望的是,如果找不到键,库不会抛出,但是,如果你使用默认语法(`$ {arg}`)而不是上面的自定义语法(`%(arg)`)那么正则表达式不会编译,这是期望的效果. (2认同)
  • 如果地图中不存在该键,您可以设置自定义VariableResolver并抛出异常. (2认同)

gil*_*dbu 65

不完全,但您可以使用MessageFormat多次引用一个值:

MessageFormat.format("There's an incorrect value \"{0}\" in column # {1}", x, y);
Run Code Online (Sandbox Code Playgroud)

以上也可以使用String.format()完成,但是如果你需要构建复杂的表达式,我发现messageFormat语法更清晰,而且你不需要关心你放入字符串的对象的类型

  • 谢谢,但是重点是我无法定位参数。 (3认同)

Nin*_*ham 18

简单命名占位符的Apache Common StringSubstitutor的另一个示例。

String template = "Welcome to {theWorld}. My name is {myName}.";

Map<String, String> values = new HashMap<>();
values.put("theWorld", "Stackoverflow");
values.put("myName", "Thanos");

String message = StringSubstitutor.replace(template, values, "{", "}");

System.out.println(message);

// Welcome to Stackoverflow. My name is Thanos.
Run Code Online (Sandbox Code Playgroud)


Fix*_*int 15

您可以使用StringTemplate库,它提供您想要的东西等等.

import org.antlr.stringtemplate.*;

final StringTemplate hello = new StringTemplate("Hello, $name$");
hello.setAttribute("name", "World");
System.out.println(hello.toString());
Run Code Online (Sandbox Code Playgroud)


Chr*_*ssy 9

对于简单的情况,您可以简单地使用硬编码的String替换,不需要在那里使用库:

    String url = "There's an incorrect value '%(value)' in column # %(column)";
    url = url.replace("%(value)", x); // 1
    url = url.replace("%(column)", y); // 2
Run Code Online (Sandbox Code Playgroud)

警告:我只想展示最简单的代码.当然,请不要将此用于严重的安全问题的生产代码,如评论中所述:转义,错误处理和安全性是一个问题.但在最坏的情况下,你现在知道为什么需要使用'好'的lib :-)

  • 这个简单易行,但缺点是当找不到值时它会默默地失败。它只是将占位符留在原始字符串中。 (2认同)
  • **警告:**因为这种技术将中间替换结果视为自己的格式字符串,[此解决方案容易受到格式字符串攻击](https://codereview.stackexchange.com/a/194454/9357).任何正确的解决方案都应该通过格式字符串进行一次传递. (2认同)

And*_*ndy 8

感谢你的帮助!使用你所有的线索,我写了例行程序来做我想要的 - 使用字典的类似python的字符串格式.由于我是Java新手,任何提示都受到赞赏.

public static String dictFormat(String format, Hashtable<String, Object> values) {
    StringBuilder convFormat = new StringBuilder(format);
    Enumeration<String> keys = values.keys();
    ArrayList valueList = new ArrayList();
    int currentPos = 1;
    while (keys.hasMoreElements()) {
        String key = keys.nextElement(),
        formatKey = "%(" + key + ")",
        formatPos = "%" + Integer.toString(currentPos) + "$";
        int index = -1;
        while ((index = convFormat.indexOf(formatKey, index)) != -1) {
            convFormat.replace(index, index + formatKey.length(), formatPos);
            index += formatPos.length();
        }
        valueList.add(values.get(key));
        ++currentPos;
    }
    return String.format(convFormat.toString(), valueList.toArray());
}
Run Code Online (Sandbox Code Playgroud)

  • **警告:**因为循环将中间替换结果视为自己的格式字符串,所以[此解决方案容易受到格式字符串攻击](https://codereview.stackexchange.com/a/194454/9357)。任何正确的解决方案都应一次通过格式字符串。 (5认同)

kay*_*yz1 6

public static String format(String format, Map<String, Object> values) {
    StringBuilder formatter = new StringBuilder(format);
    List<Object> valueList = new ArrayList<Object>();

    Matcher matcher = Pattern.compile("\\$\\{(\\w+)}").matcher(format);

    while (matcher.find()) {
        String key = matcher.group(1);

        String formatKey = String.format("${%s}", key);
        int index = formatter.indexOf(formatKey);

        if (index != -1) {
            formatter.replace(index, index + formatKey.length(), "%s");
            valueList.add(values.get(key));
        }
    }

    return String.format(formatter.toString(), valueList.toArray());
}
Run Code Online (Sandbox Code Playgroud)

例:

String format = "My name is ${1}. ${0} ${1}.";

Map<String, Object> values = new HashMap<String, Object>();
values.put("0", "James");
values.put("1", "Bond");

System.out.println(format(format, values)); // My name is Bond. James Bond.
Run Code Online (Sandbox Code Playgroud)

  • 这应该是答案,因为它避免了此处大多数其他解决方案都容易受到的格式字符串攻击。请注意,Java 9通过支持[`.replaceAll()`字符串替换回调](https://docs.oracle.com/javase/9​​/docs/api/java/util/regex/Matcher。 html#replaceAll%28java.util.function.Function%29)。 (2认同)

gil*_*des 6

这是一个旧线程,但是为了记录起见,您还可以使用Java 8样式,如下所示:

public static String replaceParams(Map<String, String> hashMap, String template) {
    return hashMap.entrySet().stream().reduce(template, (s, e) -> s.replace("%(" + e.getKey() + ")", e.getValue()),
            (s, s2) -> s);
}
Run Code Online (Sandbox Code Playgroud)

用法:

public static void main(String[] args) {
    final HashMap<String, String> hashMap = new HashMap<String, String>() {
        {
            put("foo", "foo1");
            put("bar", "bar1");
            put("car", "BMW");
            put("truck", "MAN");
        }
    };
    String res = replaceParams(hashMap, "This is '%(foo)' and '%(foo)', but also '%(bar)' '%(bar)' indeed.");
    System.out.println(res);
    System.out.println(replaceParams(hashMap, "This is '%(car)' and '%(foo)', but also '%(bar)' '%(bar)' indeed."));
    System.out.println(replaceParams(hashMap, "This is '%(car)' and '%(truck)', but also '%(foo)' '%(bar)' + '%(truck)' indeed."));
}
Run Code Online (Sandbox Code Playgroud)

输出将是:

This is 'foo1' and 'foo1', but also 'bar1' 'bar1' indeed.
This is 'BMW' and 'foo1', but also 'bar1' 'bar1' indeed.
This is 'BMW' and 'MAN', but also 'foo1' 'bar1' + 'MAN' indeed.
Run Code Online (Sandbox Code Playgroud)

  • **警告:**由于`.reduce()`将中间替换结果视为其自身的格式字符串,因此[此解决方案容易受到格式字符串攻击](https://codereview.stackexchange.com/a/194454/ 9357)。任何正确的解决方案都应一次通过格式字符串。 (4认同)

hev*_*ev1 6

StringSubstitutor可以使用Apache Commons Text。(有关如何将其包含在项目中的信息,请参阅依赖项信息StrSubstitutor。)请注意,已弃用。

import org.apache.commons.text.StringSubstitutor;
// ...
Map<String, String> values = new HashMap<>();
values.put("animal", "quick brown fox");
values.put("target", "lazy dog");
StringSubstitutor sub = new StringSubstitutor(values);
String result = sub.replace("The ${animal} jumped over the ${target}.");
// "The quick brown fox jumped over the lazy dog."
Run Code Online (Sandbox Code Playgroud)

此类支持为变量提供默认值。

String result = sub.replace("The number is ${undefined.property:-42}.");
// "The number is 42."
Run Code Online (Sandbox Code Playgroud)

要使用递归变量替换,请调用setEnableSubstitutionInVariables(true);.

Map<String, String> values = new HashMap<>();
values.put("b", "c");
values.put("ac", "Test");
StringSubstitutor sub = new StringSubstitutor(values);
sub.setEnableSubstitutionInVariables(true);
String result = sub.replace("${a${b}}");
// "Test"
Run Code Online (Sandbox Code Playgroud)


And*_*anu 5

我是一个小型库的作者,它完全可以满足您的需求:

Student student = new Student("Andrei", 30, "Male");

String studStr = template("#{id}\tName: #{st.getName}, Age: #{st.getAge}, Gender: #{st.getGender}")
                    .arg("id", 10)
                    .arg("st", student)
                    .format();
System.out.println(studStr);
Run Code Online (Sandbox Code Playgroud)

或者你可以链接参数:

String result = template("#{x} + #{y} = #{z}")
                    .args("x", 5, "y", 10, "z", 15)
                    .format();
System.out.println(result);

// Output: "5 + 10 = 15"
Run Code Online (Sandbox Code Playgroud)