Конкатенация строк в Java — быстрее?
За последнее время появилось несколько постов связанных с измерениями скорости работы со строками в Java. Хотелось поделиться полученным несколько месяцев назад методом «ускорения» приложения, а именно, ускорением конкатенации строк. Это обычно одна из наиболее используемых строковых операций в общего рода приложениях и ее улучшение часто повышает производительность в целом.
В статье описывается несколько методов ускорения работы конкатенации и в конце будут выбраны лучшие по времени работы исходники.
Как можно соединить одну или несколько строк?
Способ #1: плюс
1 | String result = a + b; |
Самый очевидный для многих начинающих программистов, особенно перешедших в Java из других ЯП, где строки тоже можно «плюсовать».
На самом деле за этой конструкцией скрывается достаточно емкая операция. Тут создается новый StringBuilder и в него добавляются все полученные на вход строки. И не забываем про .toString(), на него тоже идет время.
Кстати, эта скрытая возможность может смущать начинающих кодеров. Компилируем код javac версии 1.5+ и запускаем на JVM без StringBuilder-а, например, BlackBerry, J2ME или других урезанных системах и получаем ошибку ClassNotFoundException, хотя явно нигде этот класс не используется.
Способ #2: String.concat(String)
1 | String result = a.concat(b); |
Использование «родных» методов String-а даст ощутимый прирост в производительности — они писались с расчетом на скорость и часто дают хорошие результаты. Впрочем, почему тогда не заменять одинарный “+” на .concat хотя бы на этапе компиляции?
Способ #3: StringBuffer
1 2 3 4 | StringBuffer sb = new StringBuffer(); sb.append(a); sb.append(b); String result = sb.toString; |
Хороший способ для конкатенации многих строк, допустим, в цикле. Впрочем, эту конструкцию можно еще оптимизировать:
1 | String result = new StringBuffer(a).append(b).toString(); |
и за счет того, что в конструктор первая строка уже будет передаваться, этот способ будет работать немного быстрее.
Способ #4: StringBuilder
Использование аналогично предыдущему пункту, вместо StringBuffer — StringBuilder. Будет работать быстрее из-за не-синхронизированных методов последнего. Правда, проверка на java6 показала, что время почти не отличается, хотя на 5-й — почти на треть. Оптимизировали значит.
Способ #5: Re-using variables
Часто в советах оптимизации проскакивает — не плодите сущностей (с), то-есть используйте уже созданные экземпляры классов. Можем применить это для нашей конкатенации:
1 2 3 4 | private / static private StringBuilder staticSb = new StringBuilder(); ... staticSb.delete( 0 , staticSb.length()); String result = staticSb.append(a).append(b).toString; |
Такой метод очищения буфера, кстати, быстрее чем, например, StringBuilder.setLength(0);
Результаты
Тестирование проводилось на jre1.6.0_21 не так быстро как хотелось бы. Так же соединялись 3 и больше подстрок. Лучшие результаты выделены цветом.
Цифры в наносекундах за одно исполнение, исполнений каждого блока было по 100000. Выполнялось 5 измерений, лучшее и худшее отбрасывалось.
Аргументы виртуальной машины: -Xmx1024M -Xms1024M для уменьшения влияния GarbageCollector-а на время.
2 | 3 | 4 | 5 | |
---|---|---|---|---|
a+b+c... | 1109 | 1534 | 1749 | 2751 |
a.concat(b)… | 395 | 768 | 1295 | 1885 |
StringBuffer | 1263 | 1643 | 2504 | 2740 |
StringBuilder | 1042 | 1482 | 2371 | 2397 |
Pre-allocated StringBuilder | 552 | 617 | 785 | 967 |
Исходный код программы-мерялки:
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | package com.stringtest; public class StringTimeTest { @SuppressWarnings ( "unused" ) public static void main(String[] args) { String a = "first looooooooooooong striiiiiiiiiing test" ; String b = "second loooooooooooong striiiiiiiiiing test123" ; String c = "third looooooooooooong striiiiiiiiiing test123456" ; String d = "fourth looooooooooooong striiiiiiiiiing test12345678" ; String e = "fifth looooooooooooong striiiiiiiiiing test12345678901" ; N= 100000 ; t = System.nanoTime(); System.out.println( " ===== 2 concatenation ==== " ); for ( int i= 0 ;i<N;i++) { String res = a + b; } t(); for ( int i= 0 ;i<N;i++) { String res = a.concat(b); } t(); for ( int i= 0 ;i<N;i++) { StringBuffer sb = new StringBuffer(); String res = sb.append(a).append(b).toString(); } t(); for ( int i= 0 ;i<N;i++) { StringBuilder sb = new StringBuilder(); String res = sb.append(a).append(b).toString(); } t(); for ( int i= 0 ;i<N;i++) { StringBuilder sb = new StringBuilder( 200 ); String res = sb.append(a).append(b).toString(); } t(); StringBuilder sbGlob = new StringBuilder(); for ( int i= 0 ;i<N;i++) { sbGlob.delete( 0 , sbGlob.length()); String res = sbGlob.append(a).append(b).toString(); } t(); // 3 System.out.println( " ===== 3 concatenation ==== " ); for ( int i= 0 ;i<N;i++) { String res = a + b + c; } t(); for ( int i= 0 ;i<N;i++) { String res = a.concat(b).concat(c); } t(); for ( int i= 0 ;i<N;i++) { StringBuffer sb = new StringBuffer(); String res = sb.append(a).append(b).append(c).toString(); } t(); for ( int i= 0 ;i<N;i++) { StringBuilder sb = new StringBuilder(); String res = sb.append(a).append(b).append(c).toString(); } t(); for ( int i= 0 ;i<N;i++) { StringBuilder sb = new StringBuilder( 200 ); String res = sb.append(a).append(b).append(c).toString(); } t(); //StringBuilder sbGlob = new StringBuilder(); for ( int i= 0 ;i<N;i++) { sbGlob.delete( 0 , sbGlob.length()); String res = sbGlob.append(a).append(b).append(c).toString(); } t(); // 4 System.out.println( " ===== 4 concatenation ==== " ); for ( int i= 0 ;i<N;i++) { String res = a + b + c + d; } t(); for ( int i= 0 ;i<N;i++) { String res = a.concat(b).concat(c).concat(d); } t(); for ( int i= 0 ;i<N;i++) { StringBuffer sb = new StringBuffer(); String res = sb.append(a).append(b).append(c).append(d).toString(); } t(); for ( int i= 0 ;i<N;i++) { StringBuilder sb = new StringBuilder(); String res = sb.append(a).append(b).append(c).append(d).toString(); } t(); for ( int i= 0 ;i<N;i++) { StringBuilder sb = new StringBuilder( 200 ); String res = sb.append(a).append(b).append(c).append(d).toString(); } t(); //StringBuilder sbGlob = new StringBuilder(); for ( int i= 0 ;i<N;i++) { sbGlob.delete( 0 , sbGlob.length()); String res = sbGlob.append(a).append(b).append(c).append(d).toString(); } t(); // 5 System.out.println( " ===== 5 concatenation ==== " ); for ( int i= 0 ;i<N;i++) { String res = a + b + c + d + e; } t(); for ( int i= 0 ;i<N;i++) { String res = a.concat(b).concat(c).concat(d).concat(e); } t(); for ( int i= 0 ;i<N;i++) { StringBuffer sb = new StringBuffer(); String res = sb.append(a).append(b).append(c).append(d).append(e).toString(); } t(); for ( int i= 0 ;i<N;i++) { StringBuilder sb = new StringBuilder(); String res = sb.append(a).append(b).append(c).append(d).append(e).toString(); } t(); for ( int i= 0 ;i<N;i++) { StringBuilder sb = new StringBuilder( 200 ); String res = sb.append(a).append(b).append(c).append(d).append(e).toString(); } t(); //StringBuilder sbGlob = new StringBuilder(); for ( int i= 0 ;i<N;i++) { sbGlob.delete( 0 , sbGlob.length()); String res = sbGlob.append(a).append(b).append(c).append(d).append(e).toString(); } t(); } static long t; static long N; static void t() { System.out.println((System.nanoTime()-t)/ 1.0 /N); t = System.nanoTime(); } } |
Выводы
- Выбор метода зависит от количества строк
- Для соединения двух строк лучше использовать родной String.concat()
- Для часто вызываемого метода, в котором будут соединяться больше двух строк, лучше использовать последний метод с очищением буфера и повторного его использования. Для thread-safety можно делать это поле ThreadLocal, например. Или вообще не-статичным членом класса.
- В ходе экспериментов так же выяснилось, что одиночные a+b(+c) лучше заменять на a.concat, при большем количестве — StringBuilder.
Впрочем, не забываем, что Premature optimization is the root of all evil