Оптимизация возможностей с потоками Java
Я просматривал какой-то код и наткнулся на этот метод, который принимает значение заголовка HTML (то есть Content-Disposition = inline; filename = foo.bar) и анализирует его на карте, разделенной точкой с запятой на ключ = значение пар. Сначала это выглядело как хороший кандидат на оптимизацию с использованием потока, но после того, как я его реализовал, тот факт, что я не могу повторно использовать вычисленное значение String.indexOf('='), означает, что строка должна сканироваться 3 раза, что фактически менее оптимальным, чем оригинал. Я прекрасно понимаю, что существует много случаев, когда Streams не являются подходящим инструментом для этой работы, но мне было интересно, не пропустил ли я какой-либо метод, который мог бы позволить Stream быть более результативным или более эффективным, чем исходный код.
/**
* Convert a Header Value String into a Map
*
* @param value The Header Value
* @return The data Map
*/
private static Map<String,String> headerMap (String value) {
int eq;
Map<String,String> map = new HashMap<>();
for(String entry : value.split(";")) {
if((eq = entry.indexOf('=')) != -1) {
map.put(entry.substring(0,eq),entry.substring(eq + 1));
}
}
return map;
return Stream.of(value.split(";")).filter(entry -> entry.indexOf('=') != -1).collect(Collectors.));
} //headerMap
Моя попытка потоковой передачи:
/**
* Convert a Header Value String into a Map
*
* @param value The Header Value
* @return The data Map
*/
private static Map<String,String> headerMap (String value) {
return Stream.of(value.split(";")).filter(entry -> entry.indexOf('=') != -1).collect(Collectors.toMap(entry -> entry.substring(0,entry.indexOf('=')),entry -> entry.substring(entry.substring(entry.indexOf('=') + 1))));
} //headerMap
Ответы
Ответ 1
Это решение ищет '='
только один раз:
private static Map<String, String> headerMap(String value) {
return Stream.of(value.split(";"))
.map(s -> s.split("=", 2))
.filter(arr -> arr.length == 2)
.collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
}
Обратите внимание, что здесь используется быстрый путь для String.split
, поэтому регулярное выражение фактически не создается.
Обратите внимание, что с помощью Guava вы можете сделать это довольно чистым способом даже до Java-8:
private static Map<String, String> headerMap(String value) {
return Splitter.on( ';' ).withKeyValueSeparator( '=' ).split( value );
}
В общем, я бы посоветовал вам против ручного разбора HTTP-заголовков. Там много оговорок. См., Например, то, как реализовано в HTTP-библиотеке Apache. Использовать библиотеки.
Ответ 2
Я придумал следующий код:
private static Map<String, String> headerMap(String value) {
return Stream.of(value.split(";"))
.filter(entry -> entry.indexOf('=') != -1)
.map(entry -> {
int i = entry.indexOf('=');
return new String[] { entry.substring(0, i), entry.substring(i + 1) };
})
.collect(Collectors.toMap(array -> array[0], array -> array[1]));
}
Он сканирует только entry
два раза, сохраняя ключ и значение внутри массива размера 2. Я не уверен, что он будет таким же эффективным, как цикл for
, поскольку мы создаем другой объект для служат как владелец.
Другим решением, которое сканирует entry
только один раз, является это, хотя я его не очень понял:
private static Map<String, String> headerMap(String value) {
return Stream.of(value.split(";"))
.map(entry -> {
int i = entry.indexOf('=');
if (i == -1) {
return null;
}
return new String[] { entry.substring(0, i), entry.substring(i + 1) };
})
.filter(Objects::nonNull)
.collect(Collectors.toMap(array -> array[0], array -> array[1]));
}
Я понял тест JMH, чтобы проверить это. Ниже приведен сравнительный код:
@Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 1000, timeUnit = TimeUnit.MILLISECONDS)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(3)
@State(Scope.Benchmark)
public class StreamTest {
private static final String VALUE = "Accept=text/plain;"
+ "Accept-Charset=utf-8;"
+ "Accept-Encoding=gzip, deflate;"
+ "Accept-Language=en-US;"
+ "Accept-Datetime=Thu, 31 May 2007 20:35:00 GMT;"
+ "Cache-Control=no-cache;"
+ "Connection=keep-alive;"
+ "Content-Length=348;"
+ "Content-Type=application/x-www-form-urlencoded;"
+ "Date=Tue, 15 Nov 1994 08:12:31 GMT;"
+ "Expect=100-continue;"
+ "Max-Forwards=10;"
+ "Pragma=no-cache";
@Benchmark
public void loop() {
int eq;
Map<String, String> map = new HashMap<>();
for (String entry : VALUE.split(";")) {
if ((eq = entry.indexOf('=')) != -1) {
map.put(entry.substring(0, eq), entry.substring(eq + 1));
}
}
}
@Benchmark
public void stream1() {
Stream.of(VALUE.split(";"))
.filter(entry -> entry.indexOf('=') != -1)
.map(entry -> {
int i = entry.indexOf('=');
return new String[] { entry.substring(0, i), entry.substring(i + 1) };
})
.collect(Collectors.toMap(array -> array[0], array -> array[1]));
}
@Benchmark
public void stream2() {
Stream.of(VALUE.split(";"))
.map(entry -> {
int i = entry.indexOf('=');
if (i == -1) {
return null;
}
return new String[] { entry.substring(0, i), entry.substring(i + 1) };
})
.filter(Objects::nonNull)
.collect(Collectors.toMap(array -> array[0], array -> array[1]));
}
public static void main(String[] args) throws Exception {
Main.main(args);
}
}
и это результат (код i5 3230M CPU @2,60 ГГц, Windows 10, Oracle JDK 1.8.0_25):
Benchmark Mode Cnt Score Error Units
StreamTest.loop avgt 30 1,541 ± 0,038 us/op
StreamTest.stream1 avgt 30 1,633 ± 0,042 us/op
StreamTest.stream2 avgt 30 1,604 ± 0,058 us/op
Что это демонстрирует, так это то, что как потоковое решение, так и цикл for фактически эквивалентны производительности.