There's another option answered here. Will repeat it for convenience.
There's a library org.apache.commons:commons-text:1.9 with class StringSubstitutor. That's how it works:
 // Build map
 Map<String, String> valuesMap = new HashMap<>();
 valuesMap.put("animal", "quick brown fox");
 valuesMap.put("target", "lazy dog");
 String templateString = "The ${animal} jumped over the ${target}.";
 // Build StringSubstitutor
 StringSubstitutor sub = new StringSubstitutor(valuesMap);
 // Replace
 String resolvedString = sub.replace(templateString);
Still there's a remark. StringSubstitutor instance is created with a substitution map and then parses template strings with its replace method. That means it cannot pre-parse the template string, so processing the same template with different substitution maps may be less efficient.
The Python's string.Template works the opposite way. It's created with the template string and then processes substitution maps with its substitute or safe_substitute methods. So theoretically it can pre-parse the template string that may give some performance gain.
Also the Python's string.Template will process ether $variable or ${variable} by default. Couldn't find so far how to adjust the StringSubstitutor to do this way.
By default StringSubstitutor parses placeholders in the values that may cause infinite loops. stringSubstitutor.setDisableSubstitutionInValues(true) will disable this behavior.