Optimize allocation in StringUtils#cleanPath
This commit applies several optimizations to StringUtils#cleanPath and related methods: * pre-size pathElements deque in StringUtils#cleanPath with pathElements.length elements, since this this is the maximum size and the most likely case. * optimize StringUtils#collectionToDelimitedString to calculate the size of the resulting String and avoid array auto-resizing in the StringBuilder. * If the path did not contain any components that required cleaning, return the (normalized) path as-is. No need to concatenate the prefix and the trailing path. See gh-26316
This commit is contained in:
parent
ae56f2ac09
commit
8d3e8ca3a2
|
@ -667,7 +667,9 @@ public abstract class StringUtils {
|
||||||
if (!hasLength(path)) {
|
if (!hasLength(path)) {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
String pathToUse = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR);
|
|
||||||
|
String normalizedPath = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR);
|
||||||
|
String pathToUse = normalizedPath;
|
||||||
|
|
||||||
// Shortcut if there is no work to do
|
// Shortcut if there is no work to do
|
||||||
if (pathToUse.indexOf('.') == -1) {
|
if (pathToUse.indexOf('.') == -1) {
|
||||||
|
@ -695,7 +697,8 @@ public abstract class StringUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR);
|
String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR);
|
||||||
Deque<String> pathElements = new ArrayDeque<>();
|
// we never require more elements than pathArray and in the common case the same number
|
||||||
|
Deque<String> pathElements = new ArrayDeque<>(pathArray.length);
|
||||||
int tops = 0;
|
int tops = 0;
|
||||||
|
|
||||||
for (int i = pathArray.length - 1; i >= 0; i--) {
|
for (int i = pathArray.length - 1; i >= 0; i--) {
|
||||||
|
@ -721,7 +724,7 @@ public abstract class StringUtils {
|
||||||
|
|
||||||
// All path elements stayed the same - shortcut
|
// All path elements stayed the same - shortcut
|
||||||
if (pathArray.length == pathElements.size()) {
|
if (pathArray.length == pathElements.size()) {
|
||||||
return prefix + pathToUse;
|
return normalizedPath;
|
||||||
}
|
}
|
||||||
// Remaining top paths need to be retained.
|
// Remaining top paths need to be retained.
|
||||||
for (int i = 0; i < tops; i++) {
|
for (int i = 0; i < tops; i++) {
|
||||||
|
@ -732,7 +735,40 @@ public abstract class StringUtils {
|
||||||
pathElements.addFirst(CURRENT_PATH);
|
pathElements.addFirst(CURRENT_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR);
|
final String joined = joinStrings(pathElements, FOLDER_SEPARATOR);
|
||||||
|
// avoid string concatenation with empty prefix
|
||||||
|
return prefix.isEmpty() ? joined : prefix + joined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a {@link Collection Collection<String>} to a delimited {@code String} (e.g. CSV).
|
||||||
|
* <p>This is an optimized variant of {@link #collectionToDelimitedString(Collection, String)}, which does not
|
||||||
|
* require dynamic resizing of the StringBuilder's backing array.
|
||||||
|
* @param coll the {@code Collection Collection<String>} to convert (potentially {@code null} or empty)
|
||||||
|
* @param delim the delimiter to use (typically a ",")
|
||||||
|
* @return the delimited {@code String}
|
||||||
|
*/
|
||||||
|
private static String joinStrings(@Nullable Collection<String> coll, String delim) {
|
||||||
|
|
||||||
|
if (CollectionUtils.isEmpty(coll)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// precompute total length of resulting string
|
||||||
|
int totalLength = (coll.size() - 1) * delim.length();
|
||||||
|
for (String str : coll) {
|
||||||
|
totalLength += str.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder(totalLength);
|
||||||
|
Iterator<?> it = coll.iterator();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
sb.append(it.next());
|
||||||
|
if (it.hasNext()) {
|
||||||
|
sb.append(delim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -402,6 +402,8 @@ class StringUtilsTests {
|
||||||
assertThat(StringUtils.cleanPath("file:.././")).isEqualTo("file:../");
|
assertThat(StringUtils.cleanPath("file:.././")).isEqualTo("file:../");
|
||||||
assertThat(StringUtils.cleanPath("file:/mypath/spring.factories")).isEqualTo("file:/mypath/spring.factories");
|
assertThat(StringUtils.cleanPath("file:/mypath/spring.factories")).isEqualTo("file:/mypath/spring.factories");
|
||||||
assertThat(StringUtils.cleanPath("file:///c:/some/../path/the%20file.txt")).isEqualTo("file:///c:/path/the%20file.txt");
|
assertThat(StringUtils.cleanPath("file:///c:/some/../path/the%20file.txt")).isEqualTo("file:///c:/path/the%20file.txt");
|
||||||
|
assertThat(StringUtils.cleanPath("jar:file:///c:\\some\\..\\path\\.\\the%20file.txt")).isEqualTo("jar:file:///c:/path/the%20file.txt");
|
||||||
|
assertThat(StringUtils.cleanPath("jar:file:///c:/some/../path/./the%20file.txt")).isEqualTo("jar:file:///c:/path/the%20file.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
Loading…
Reference in New Issue