关于java:字符串格式化中的命名占位符

Named placeholders in string formatting

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

1
2
print"There's an incorrect value '%(value)s' in column # %(column)d" % \
  { 'value': x, 'column': y }

我想知道Java中是否有可能(希望没有外部库)?


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

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

1
2
3
4
5
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)");

上述结果导致:

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

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

1
2
3
4
5
<dependency>
    <groupId>org.apache.commons</groupId>
    commons-lang3</artifactId>
    <version>3.4</version>
</dependency>


不完全正确,但可以使用messageformat多次引用一个值:

1
MessageFormat.format("There's an incorrect value "{0}" in column # {1}", x, y);

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


您可以使用StringTemplate库,它提供了您想要的以及更多。

1
2
3
4
5
import org.antlr.stringtemplate.*;

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


apache common strsubstitutor的另一个简单命名占位符示例。

1
2
3
4
5
6
7
8
9
10
11
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 = StrSubstitutor.replace(template, values,"{","}");

System.out.println(message);

// Welcome to Stackoverflow. My name is Thanos.


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

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

警告:我只想显示最简单的代码。当然,在安全性很重要的情况下,不要将其用于严重的生产代码,如注释中所述:此处存在转义、错误处理和安全性问题。但在最坏的情况下,您现在知道为什么需要使用"良好"的lib:-)


谢谢你的帮助!利用所有的线索,我编写了一个程序来做我想要做的事情——使用字典进行类似于Python的字符串格式设置。由于我是Java新手,任何提示都是值得赞赏的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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());
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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());
}

例子:

1
2
3
4
5
6
7
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.


这是一个老线程,但就记录而言,您也可以使用Java 8风格,如下所示:

1
2
3
4
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);
}

用途:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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."));
}

输出将是:

1
2
3
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.


我是一家小型图书馆的作者,该图书馆完全按照您的要求行事:

1
2
3
4
5
6
7
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);

或者您可以链接参数:

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

// Output:"5 + 10 = 15"


我还创建了一个util/helper类(使用JDK8),它可以格式化一个字符串,并替换出现的变量。

为此,我使用了matchers"appendreplacement"方法,该方法执行所有替换,并且只循环格式字符串的受影响部分。

helper类目前没有很好的javadoc文档。我以后会改变这一点;)不管怎样,我评论了最重要的几行(希望如此)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
    public class FormatHelper {

    //Prefix and suffix for the enclosing variable name in the format string.
    //Replace the default values with any you need.
    public static final String DEFAULT_PREFIX ="${";
    public static final String DEFAULT_SUFFIX ="}";

    //Define dynamic function what happens if a key is not found.
    //Replace the defualt exception with any"unchecked" exception type you need or any other behavior.
    public static final BiFunction<String, String, String> DEFAULT_NO_KEY_FUNCTION =
            (fullMatch, variableName) -> {
                throw new RuntimeException(String.format("Key: %s for variable %s not found.",
                                                         variableName,
                                                         fullMatch));
            };
    private final Pattern variablePattern;
    private final Map<String, String> values;
    private final BiFunction<String, String, String> noKeyFunction;
    private final String prefix;
    private final String suffix;

    public FormatHelper(Map<String, String> values) {
        this(DEFAULT_NO_KEY_FUNCTION, values);
    }

    public FormatHelper(
            BiFunction<String, String, String> noKeyFunction, Map<String, String> values) {
        this(DEFAULT_PREFIX, DEFAULT_SUFFIX, noKeyFunction, values);
    }

    public FormatHelper(String prefix, String suffix, Map<String, String> values) {
        this(prefix, suffix, DEFAULT_NO_KEY_FUNCTION, values);
    }

    public FormatHelper(
            String prefix,
            String suffix,
            BiFunction<String, String, String> noKeyFunction,
            Map<String, String> values) {
        this.prefix = prefix;
        this.suffix = suffix;
        this.values = values;
        this.noKeyFunction = noKeyFunction;

        //Create the Pattern and quote the prefix and suffix so that the regex don't interpret special chars.
        //The variable name is a"\w+" in an extra capture group.
        variablePattern = Pattern.compile(Pattern.quote(prefix) +"(\\w+)" + Pattern.quote(suffix));
    }

    public static String format(CharSequence format, Map<String, String> values) {
        return new FormatHelper(values).format(format);
    }

    public static String format(
            CharSequence format,
            BiFunction<String, String, String> noKeyFunction,
            Map<String, String> values) {
        return new FormatHelper(noKeyFunction, values).format(format);
    }

    public static String format(
            String prefix, String suffix, CharSequence format, Map<String, String> values) {
        return new FormatHelper(prefix, suffix, values).format(format);
    }

    public static String format(
            String prefix,
            String suffix,
            BiFunction<String, String, String> noKeyFunction,
            CharSequence format,
            Map<String, String> values) {
        return new FormatHelper(prefix, suffix, noKeyFunction, values).format(format);
    }

    public String format(CharSequence format) {

        //Create matcher based on the init pattern for variable names.
        Matcher matcher = variablePattern.matcher(format);

        //This buffer will hold all parts of the formatted finished string.
        StringBuffer formatBuffer = new StringBuffer();

        //loop while the matcher finds another variable (prefix -> name <- suffix) match
        while (matcher.find()) {

            //The root capture group with the full match e.g ${variableName}
            String fullMatch = matcher.group();

            //The capture group for the variable name resulting from"(\w+)" e.g. variableName
            String variableName = matcher.group(1);

            //Get the value in our Map so the Key is the used variable name in our"format" string. The associated value will replace the variable.
            //If key is missing (absent) call the noKeyFunction with parameters"fullMatch" and"variableName" else return the value.
            String value = values.computeIfAbsent(variableName, key -> noKeyFunction.apply(fullMatch, key));

            //Escape the Map value because the"appendReplacement" method interprets the $ and \ as special chars.
            String escapedValue = Matcher.quoteReplacement(value);

            //The"appendReplacement" method replaces the current"full" match (e.g. ${variableName}) with the value from the"values" Map.
            //The replaced part of the"format" string is appended to the StringBuffer"formatBuffer".
            matcher.appendReplacement(formatBuffer, escapedValue);
        }

        //The"appendTail" method appends the last part of the"format" String which has no regex match.
        //That means if e.g. our"format" string has no matches the whole untouched"format" string is appended to the StringBuffer"formatBuffer".
        //Further more the method return the buffer.
        return matcher.appendTail(formatBuffer)
                      .toString();
    }

    public String getPrefix() {
        return prefix;
    }

    public String getSuffix() {
        return suffix;
    }

    public Map<String, String> getValues() {
        return values;
    }
}

可以使用值(或后缀前缀或nokeyfunction)为特定映射创建类实例。像:

1
2
3
4
5
6
7
8
9
10
11
    Map<String, String> values = new HashMap<>();
    values.put("firstName","Peter");
    values.put("lastName","Parker");


    FormatHelper formatHelper = new FormatHelper(values);
    formatHelper.format("${firstName} ${lastName} is Spiderman!");
    // Result:"Peter Parker is Spiderman!"
    // Next format:
    formatHelper.format("Does ${firstName} ${lastName} works as photographer?");
    //Result:"Does Peter Parker works as photographer?"

此外,您还可以定义如果缺少值映射中的键(这两种方式都适用,例如格式字符串中的变量名错误或映射中的键丢失)。默认行为是引发未选中异常(未选中是因为我使用了无法处理选中异常的默认JDK8函数),如:

1
2
3
4
5
6
7
8
    Map<String, String> map = new HashMap<>();
    map.put("firstName","Peter");
    map.put("lastName","Parker");


    FormatHelper formatHelper = new FormatHelper(map);
    formatHelper.format("${missingName} ${lastName} is Spiderman!");
    //Result: RuntimeException: Key: missingName for variable ${missingName} not found.

可以在构造函数调用中定义自定义行为,如:

1
2
3
4
5
6
7
8
Map<String, String> values = new HashMap<>();
values.put("firstName","Peter");
values.put("lastName","Parker");


FormatHelper formatHelper = new FormatHelper(fullMatch, variableName) -> variableName.equals("missingName") ?"John":"SOMETHING_WRONG", values);
formatHelper.format("${missingName} ${lastName} is Spiderman!");
// Result:"John Parker is Spiderman!"

或者将其委托回默认的无键行为:

1
2
3
4
5
...
    FormatHelper formatHelper = new FormatHelper((fullMatch, variableName) ->   variableName.equals("missingName") ?"John" :
            FormatHelper.DEFAULT_NO_KEY_FUNCTION.apply(fullMatch,
                                                       variableName), map);
...

为了更好地处理,还存在静态方法表示,如:

1
2
3
4
5
6
Map<String, String> values = new HashMap<>();
values.put("firstName","Peter");
values.put("lastName","Parker");

FormatHelper.format("${firstName} ${lastName} is Spiderman!", map);
// Result:"Peter Parker is Spiderman!"


ApacheCommonsLang的replace根据您的特定需求,每个方法都可以派上用场。您可以很容易地使用它用这个方法调用按名称替换占位符:

1
2
StringUtils.replaceEach("There's an incorrect value '%(value)' in column # %(column)",
            new String[] {"%(value)","%(column)" }, new String[] { x, y });

给定一些输入文本,这将用第二个字符串数组中的相应值替换第一个字符串数组中出现的所有占位符。


根据答案,我创建了MapBuilder类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class MapBuilder {

    public static Map<String, Object> build(Object... data) {
        Map<String, Object> result = new LinkedHashMap<>();

        if (data.length % 2 != 0) {
            throw new IllegalArgumentException("Odd number of arguments");
        }

        String key = null;
        Integer step = -1;

        for (Object value : data) {
            step++;
            switch (step % 2) {
                case 0:
                    if (value == null) {
                        throw new IllegalArgumentException("Null key value");
                    }
                    key = (String) value;
                    continue;
                case 1:
                    result.put(key, value);
                    break;
            }
        }

        return result;
    }

}

然后我为字符串格式创建了类StringFormat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class StringFormat {

    public static String format(String format, Object... args) {
        Map<String, Object> values = MapBuilder.build(args);

        for (Map.Entry<String, Object> entry : values.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            format = format.replace("$" + key, value.toString());
        }

        return format;
    }

}

你可以这样使用:

1
2
3
4
String bookingDate = StringFormat.format("From $startDate to $endDate"),
       "$startDate", formattedStartDate,
       "$endDate", formattedEndDate
);


我的答案是:

a)尽可能使用StringBuilder

b)保持"placeholder"的位置(任何形式:integer是最好的,特殊字符如dollar macro等),然后使用StringBuilder.insert()(参数的几个版本)。

当StringBuilder在内部转换为String时,使用外部库似乎有点过头了,而且我认为这会显著降低性能。


在字符串帮助器类中可以有类似这样的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
 * An interpreter for strings with named placeholders.
 *
 * For example given the string"hello %(myName)" and the map <wyn>
 *      <p>
Map<String, Object> map = new HashMap<String, Object>();
</p>
 *      <p>
map.put("myName","world");
</p>
 * </wyn>
 *
 * the call {@code format("hello %(myName)", map)} returns"hello world"
 *
 * It replaces every occurrence of a named placeholder with its given value
 * in the map. If there is a named place holder which is not found in the
 * map then the string will retain that placeholder. Likewise, if there is
 * an entry in the map that does not have its respective placeholder, it is
 * ignored.
 *
 * @param str
 *            string to format
 * @param values
 *            to replace
 * @return formatted string
 */

public static String format(String str, Map<String, Object> values) {

    StringBuilder builder = new StringBuilder(str);

    for (Entry<String, Object> entry : values.entrySet()) {

        int start;
        String pattern ="%(" + entry.getKey() +")";
        String value = entry.getValue().toString();

        // Replace every occurence of %(key) with value
        while ((start = builder.indexOf(pattern)) != -1) {
            builder.replace(start, start + pattern.length(), value);
        }
    }

    return builder.toString();
}


尝试FreeMarker,模板化库。

alt text