Skip to content

Commit b30b162

Browse files
Avoid String allocations in Version (#6317)
* Avoid `String` allocations in `Version` By not calling `String#substring()` we can avoid unnecessary `String` allocations. As `Integer#parseInt(CharSequence, int, int, int)` is only available in Java 9, I inlined a simplified version of it. * More minor optimizations
1 parent 8986f6f commit b30b162

1 file changed

Lines changed: 108 additions & 28 deletions

File tree

  • rewrite-maven/src/main/java/org/openrewrite/maven/tree

rewrite-maven/src/main/java/org/openrewrite/maven/tree/Version.java

Lines changed: 108 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -218,15 +218,15 @@ static final class Tokenizer {
218218
private static final Integer QUALIFIER_ALPHA = -5;
219219
private static final Integer QUALIFIER_BETA = -4;
220220
private static final Integer QUALIFIER_MILESTONE = -3;
221-
private static final Map<String, Integer> QUALIFIERS;
222221
private final String version;
223222
private int index;
224-
private String token;
223+
private int tokenStart;
224+
private int tokenEnd;
225225
private boolean number;
226226
private boolean terminatedByNumber;
227227

228228
Tokenizer(String version) {
229-
this.version = version.length() > 0 ? version : "0";
229+
this.version = !version.isEmpty() ? version : "0";
230230
}
231231

232232
public boolean next() {
@@ -269,10 +269,12 @@ public boolean next() {
269269
}
270270

271271
if (end > start) {
272-
this.token = this.version.substring(start, end);
272+
this.tokenStart = start;
273+
this.tokenEnd = end;
273274
this.number = state >= 0;
274275
} else {
275-
this.token = "0";
276+
this.tokenStart = 0;
277+
this.tokenEnd = 1;
276278
this.number = true;
277279
}
278280

@@ -282,29 +284,33 @@ public boolean next() {
282284

283285
@Override
284286
public String toString() {
285-
return String.valueOf(this.token);
287+
return this.version.substring(this.tokenStart, this.tokenEnd);
286288
}
287289

288290
public Version.Item toItem() {
289291
if (this.number) {
290292
try {
291-
return this.token.length() < 10 ? new Version.Item(4, Integer.parseInt(this.token)) : new Version.Item(5, new BigInteger(this.token));
292-
} catch (NumberFormatException var2) {
293-
throw new IllegalStateException(var2);
293+
int tokenLength = this.tokenEnd - this.tokenStart;
294+
return tokenLength < 10 ? new Version.Item(4, parseInt(version, tokenStart, tokenEnd)) :
295+
new Version.Item(5, new BigInteger(version.substring(this.tokenStart, this.tokenEnd)));
296+
} catch (NumberFormatException e) {
297+
throw new IllegalArgumentException("Illegal version: " + version.substring(this.tokenStart, this.tokenEnd), e);
294298
}
295299
} else {
300+
int tokenLength = this.tokenEnd - this.tokenStart;
296301
if (this.index >= this.version.length()) {
297-
if ("min".equalsIgnoreCase(this.token)) {
302+
if (tokenLength == 3 && matches("min")) {
298303
return Version.Item.MIN;
299304
}
300305

301-
if ("max".equalsIgnoreCase(this.token)) {
306+
if (tokenLength == 3 && matches("max")) {
302307
return Version.Item.MAX;
303308
}
304309
}
305310

306-
if (this.terminatedByNumber && this.token.length() == 1) {
307-
switch (this.token.charAt(0)) {
311+
if (this.terminatedByNumber && tokenLength == 1) {
312+
char c = this.version.charAt(this.tokenStart);
313+
switch (c) {
308314
case 'A':
309315
case 'a':
310316
return new Version.Item(2, QUALIFIER_ALPHA);
@@ -317,24 +323,98 @@ public Version.Item toItem() {
317323
}
318324
}
319325

320-
Integer qualifier = QUALIFIERS.get(this.token);
321-
return qualifier != null ? new Version.Item(2, qualifier) : new Version.Item(3, this.token.toLowerCase(Locale.ENGLISH));
326+
// Fast path for common qualifiers (avoiding substring allocation and map lookup)
327+
switch (tokenLength) {
328+
case 0:
329+
return new Version.Item(2, 0);
330+
case 2:
331+
if (matches("ga")) {
332+
return new Version.Item(2, 0);
333+
}
334+
if (matches("rc")) {
335+
return new Version.Item(2, -2);
336+
}
337+
if (matches("cr")) {
338+
return new Version.Item(2, -2);
339+
}
340+
if (matches("sp")) {
341+
return new Version.Item(2, 1);
342+
}
343+
break;
344+
case 4:
345+
if (matches("beta")) {
346+
return new Version.Item(2, QUALIFIER_BETA);
347+
}
348+
break;
349+
case 5:
350+
if (matches("alpha")) {
351+
return new Version.Item(2, QUALIFIER_ALPHA);
352+
}
353+
if (matches("final")) {
354+
return new Version.Item(2, 0);
355+
}
356+
break;
357+
case 7:
358+
if (matches("release")) {
359+
return new Version.Item(2, 0);
360+
}
361+
break;
362+
case 8:
363+
if (matches("snapshot")) {
364+
return new Version.Item(2, -1);
365+
}
366+
break;
367+
case 9:
368+
if (matches("milestone")) {
369+
return new Version.Item(2, QUALIFIER_MILESTONE);
370+
}
371+
break;
372+
}
373+
374+
// Unknown qualifier - treat as string
375+
String token = this.version.substring(this.tokenStart, this.tokenEnd);
376+
return new Version.Item(3, token.toLowerCase(Locale.ENGLISH));
377+
}
378+
}
379+
380+
// Adapted from Java 9's `Integer#parseInt(CharSequence, int, int, int)`
381+
private static int parseInt(String s, int beginIndex, int endIndex)
382+
throws NumberFormatException {
383+
384+
if (beginIndex >= endIndex) {
385+
throw new NumberFormatException("Empty string");
386+
}
387+
388+
int result = 0;
389+
int i = beginIndex;
390+
int limit = -Integer.MAX_VALUE;
391+
int multmin = limit / 10;
392+
393+
char firstChar = s.charAt(i);
394+
if (firstChar < '0' || firstChar > '9') {
395+
throw new NumberFormatException("Invalid character");
396+
}
397+
398+
// Accumulating negatively avoids surprises near MAX_VALUE
399+
while (i < endIndex) {
400+
int digit = s.charAt(i++) - '0';
401+
if (digit < 0 || digit > 9) {
402+
throw new NumberFormatException("Invalid character");
403+
}
404+
if (result < multmin) {
405+
throw new NumberFormatException("Value out of range");
406+
}
407+
result *= 10;
408+
if (result < limit + digit) {
409+
throw new NumberFormatException("Value out of range");
410+
}
411+
result -= digit;
322412
}
413+
return -result;
323414
}
324415

325-
static {
326-
QUALIFIERS = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
327-
QUALIFIERS.put("alpha", QUALIFIER_ALPHA);
328-
QUALIFIERS.put("beta", QUALIFIER_BETA);
329-
QUALIFIERS.put("milestone", QUALIFIER_MILESTONE);
330-
QUALIFIERS.put("cr", -2);
331-
QUALIFIERS.put("rc", -2);
332-
QUALIFIERS.put("snapshot", -1);
333-
QUALIFIERS.put("ga", 0);
334-
QUALIFIERS.put("final", 0);
335-
QUALIFIERS.put("release", 0);
336-
QUALIFIERS.put("", 0);
337-
QUALIFIERS.put("sp", 1);
416+
private boolean matches(String target) {
417+
return this.version.regionMatches(true, this.tokenStart, target, 0, target.length());
338418
}
339419
}
340420
}

0 commit comments

Comments
 (0)