以字符串格式命名的占位符


172

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

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

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


您可以扩展MessageFormat并在其中实现从变量到索引的映射功能。
vpram86


1
历史:Java在此问题上大多复制了C / C ++,因为它试图从C ++世界吸引开发人员,而C ++世界%s是惯例。en.wikipedia.org/wiki/Printf_format_string#History另外请注意,某些IDE和FindBugs可能会自动检测不匹配的%s和%d计数,但是我仍然希望使用命名字段。
Christophe Roussy

Answers:


143

雅加达公共语言的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)");

上面的结果是:

“第2列中的值'1'不正确”

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

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
</dependency>

2
我发现令人失望的是,如果找不到键,库不会抛出,但是,如果您使用默认语法(${arg})而不是上面的自定义语法()%(arg),则正则表达式将无法编译,这是理想的效果。
约翰·莱曼

2
您可以设置自定义VariableResolver并在地图中不存在该键时引发Exception。
Mene 2016年

7
旧线程,但从3.6版本开始,不推荐使用文本包,而推荐使用commons-text。commons.apache.org/proper/commons-text
Jeff Walker

73

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

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

上面的操作也可以用String.format()完成,但是如果您需要构建复杂的表达式,并且不需要关心要放入字符串中的对象的类型,我可以使用messageFormat语法更清洁的方法。


不知道为什么不能,字符串中的位置并不重要,只有args列表中的位置不重要,这使它成为重命名问题。您知道键的名称,这意味着您可以确定参数在参数列表中的位置。从现在开始,值将被称为0,列将被称为1:MessageeFormat.format(“列#{1}中存在错误的值\” {0} \“,使用{0}作为值会导致很多问题”,valueMap .get('value'),valueMap.get('column'));
giladbu 2010年

1
感谢您提供的线索,它帮助我编写了完全符合我想要的功能的简单函数(已将其放在下面)。
安迪

1
同意,语法要简洁得多。太糟糕了,MessageFormat在格式化数字值时有自己的想法。
Kees de Kooter '16

它似乎忽略了用单引号引起来的占位符。
Kees de Kooter '16

MessageFormat相对较大的json内容很棒但很麻烦
EliuX

32

简单命名占位符的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.

如果您希望加载非常大的文件,我发现此库还支持replaceIn将值替换为缓冲区的内容:StringBuilder或TextStringBuilder。使用这种方法,文件的全部内容将不会加载到内存中。
爱德华·科里加尔

15

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

import org.antlr.stringtemplate.*;

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

'字符有麻烦:unexpected char: '''
AlikElzin-kilaka

11

对于非常简单的情况,您可以只使用硬编码的String替换,而无需那里的库:

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

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


1
这很简单,但是缺点是当找不到该值时,它会默默地失败。它只是将占位符留在原始字符串中。
kiedysktos 17-4-27的

@kiedysktos,您可以通过检查来改进它,但如果您想要完整的内容,请使用lib :)
Christophe Roussy

2
警告:由于此技术将中间替换结果视为自己的格式字符串,因此此解决方案容易受到格式字符串攻击。任何正确的解决方案都应一次通过格式字符串。
200_success,

@ 200_success是讨论安全性的好方法,当然,此代码不适用于严重的生产使用……
Christophe Roussy

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());
}

与Lombo的答案不同,它不能陷入无限循环,因为formatPos不能包含formatKey
亚伦·迪富

6
警告:由于循环将中间替换结果视为自己的格式字符串,因此此解决方案容易受到格式字符串攻击。任何正确的解决方案都应一次通过格式字符串。
200_success,

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);
}

用法:

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."));
}

输出将是:

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.

这很棒,但是很遗憾,它违反了此处的规范docs.oracle.com/javase/8/docs/api/java/util/stream/…如果第一个参数是Identity,则合并器函数必须返回第二个参数。上面的那个将返回身份。它还违反了以下规则:Combiner.apply(u,accumulator.apply(identity,t))== accumulator.apply(u,t)
Ali Cheaito

有趣的是……但前提是您提出了一种更好的方式来传递地图,并且尽可能在模板之后(如大多数格式代码一样)进行传递。
Christophe Roussy

4
警告:由于.reduce()将中间替换结果视为自己的格式字符串,因此此解决方案容易受到格式字符串攻击。任何正确的解决方案都应一次通过格式字符串。
200_success,

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());
}

例:

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.

2
这应该是答案,因为它避免了此处大多数其他解决方案容易受到的格式字符串攻击。请注意,Java 9使它更加简单,并支持.replaceAll()字符串替换回调
200_success,

这应该是答案,因为它不使用任何外部库。
李伯浩

3

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

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);

或者,您可以链接参数:

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

// Output: "5 + 10 = 15"

是否可以对库进行基于条件的格式化?
gaurav '19

@gaurav不太正确。如果需要,则需要功能齐全的模板库。
安德烈·乔巴努

1

您可能在字符串帮助程序类中有类似的内容

/**
 * An interpreter for strings with named placeholders.
 *
 * For example given the string "hello %(myName)" and the map <code>
 *      <p>Map<String, Object> map = new HashMap<String, Object>();</p>
 *      <p>map.put("myName", "world");</p>
 * </code>
 *
 * 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();
}

非常感谢,它几乎满足了我的要求,但是唯一的事情是它没有帐户修饰符(考虑“%(key)08d”)
Andy 2010年

1
还请注意,如果所使用的任何值包含相应的条目,则这将进入无限循环。
亚伦·杜福

1
警告:由于循环将中间替换结果视为自己的格式字符串,因此此解决方案容易受到格式字符串攻击。任何正确的解决方案都应一次通过格式字符串。
200_success,

1

我的答案是:

a)尽可能使用StringBuilder

b)保持(以任何形式:整数是最好的,特殊的字符,如美元宏等)“占位符”的位置,然后使用StringBuilder.insert()(少量参数版本)。

当内部将StringBuilder转换为String时,使用外部库似乎过于矫kill过正,并且我相信性能会显着下降。


1

根据答案,我创建了MapBuilder课程:

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了String格式的类:

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;
    }

}

您可以这样使用:

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

1
警告:由于循环将中间替换结果视为自己的格式字符串,因此此解决方案容易受到格式字符串攻击。任何正确的解决方案都应一次通过格式字符串。
200_success,

1

根据您的特定需求,Apache Commons Lang的replaceEach方法可能派上用场。您可以轻松地使用它通过以下单个方法调用按名称替换占位符:

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

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


1

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

为此,我使用Matchers的“ appendReplacement”方法进行所有替换,并仅在格式字符串的受影响部分上循环。

目前尚无关于javadoc的帮助类。我将在将来对此进行更改;)无论如何,我评论了最重要的几行(我希望如此)。

    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)为特定Map创建类实例:

    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?"

此外,您还可以定义如果缺少值Map中的键(以两种方式工作,例如格式字符串中的变量名错误或Map中的键丢失)时会发生什么。默认行为是引发未检查的异常(未检查,因为我使用了无法处理已检查异常的默认jdk8函数),例如:

    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.

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

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!"

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

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

为了更好地处理,还有静态方法表示形式,例如:

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!"

0

试试Freemarker,模板库。

替代文字


4
Freemarker?我想他愿意知道,如何在纯Java中做到这一点。无论如何,如果Freemarker是可能的答案,那么我还能说JSP也是正确的答案吗?
Rakesh Juyal 2010年

1
谢谢,但是对于我手头的任务来说,这似乎有些过头了。但是,谢谢。
安迪

1
@Rakesh JSP是非常“视图/ FE”特定的东西。我过去曾使用FreeMarker生成XML,有时甚至生成了JAVA文件。安迪(Andy)担心您将不得不自己编写一个实用程序(或像上面规定的那样)
Kannan Ekanath 2010年

@鲍里斯哪一个是更好的freemarker vs速度vs stringtemplate?
gaurav '19


0

撰写本文时,Java没有内置任何内容。我建议编写自己的实现。我更喜欢一个简单的流利的生成器界面,而不是创建一个映射并将其传递给函数-您最终得到了一个漂亮的连续代码块,例如:

String result = new TemplatedStringBuilder("My name is {{name}} and I from {{town}}")
   .replace("name", "John Doe")
   .replace("town", "Sydney")
   .finish();

这是一个简单的实现:

class TemplatedStringBuilder {

    private final static String TEMPLATE_START_TOKEN = "{{";
    private final static String TEMPLATE_CLOSE_TOKEN = "}}";

    private final String template;
    private final Map<String, String> parameters = new HashMap<>();

    public TemplatedStringBuilder(String template) {
        if (template == null) throw new NullPointerException();
        this.template = template;
    }

    public TemplatedStringBuilder replace(String key, String value){
        parameters.put(key, value);
        return this;
    }

    public String finish(){

        StringBuilder result = new StringBuilder();

        int startIndex = 0;

        while (startIndex < template.length()){

            int openIndex  = template.indexOf(TEMPLATE_START_TOKEN, startIndex);

            if (openIndex < 0){
                result.append(template.substring(startIndex));
                break;
            }

            int closeIndex = template.indexOf(TEMPLATE_CLOSE_TOKEN, openIndex);

            if(closeIndex < 0){
                result.append(template.substring(startIndex));
                break;
            }

            String key = template.substring(openIndex + TEMPLATE_START_TOKEN.length(), closeIndex);

            if (!parameters.containsKey(key)) throw new RuntimeException("missing value for key: " + key);

            result.append(template.substring(startIndex, openIndex));
            result.append(parameters.get(key));

            startIndex = closeIndex + TEMPLATE_CLOSE_TOKEN.length();
        }

        return result.toString();
    }
}


0

您应该看一下ICU4J官方。它提供了一个类似于JDK可用的MessageFormat类,但该类支持命名的占位符。

与本页上提供的其他解决方案不同。ICU4j是ICU项目的一部分,该项目由IBM维护并定期更新。此外,它还支持高级用例,例如复数形式等等。

这是一个代码示例:

MessageFormat messageFormat =
        new MessageFormat("Publication written by {author}.");

Map<String, String> args = Map.of("author", "John Doe");

System.out.println(messageFormat.format(args));

0

有Java插件可以在Java中使用字符串插值(例如Kotlin,JavaScript)。支持Java 8,9,10,11 ... https://github.com/antkorwin/better-strings

在字符串文字中使用变量:

int a = 3;
int b = 4;
System.out.println("${a} + ${b} = ${a+b}");

使用表达式:

int a = 3;
int b = 4;
System.out.println("pow = ${a * a}");
System.out.println("flag = ${a > b ? true : false}");

使用功能:

@Test
void functionCall() {
    System.out.println("fact(5) = ${factorial(5)}");
}

long factorial(int n) {
    long fact = 1;
    for (int i = 2; i <= n; i++) {
        fact = fact * i;
    }
    return fact;
}

有关更多信息,请阅读项目README。

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.