Конкатенация строк в Java — быстрее?
За последнее время появилось несколько постов связанных с измерениями скорости работы со строками в Java. Хотелось поделиться полученным несколько месяцев назад методом «ускорения» приложения, а именно, ускорением конкатенации строк. Это обычно одна из наиболее используемых строковых операций в общего рода приложениях и ее улучшение часто повышает производительность в целом.
В статье описывается несколько методов ускорения работы конкатенации и в конце будут выбраны лучшие по времени работы исходники.
Как можно соединить одну или несколько строк?
Способ #1: плюс
String result = a + b;
Самый очевидный для многих начинающих программистов, особенно перешедших в Java из других ЯП, где строки тоже можно «плюсовать».
На самом деле за этой конструкцией скрывается достаточно емкая операция. Тут создается новый StringBuilder и в него добавляются все полученные на вход строки. И не забываем про .toString(), на него тоже идет время.
Кстати, эта скрытая возможность может смущать начинающих кодеров. Компилируем код javac версии 1.5+ и запускаем на JVM без StringBuilder-а, например, BlackBerry, J2ME или других урезанных системах и получаем ошибку ClassNotFoundException, хотя явно нигде этот класс не используется.
Способ #2: String.concat(String)
String result = a.concat(b);
Использование «родных» методов String-а даст ощутимый прирост в производительности — они писались с расчетом на скорость и часто дают хорошие результаты. Впрочем, почему тогда не заменять одинарный “+” на .concat хотя бы на этапе компиляции?
Способ #3: StringBuffer
StringBuffer sb = new StringBuffer(); sb.append(a); sb.append(b); String result = sb.toString;
Хороший способ для конкатенации многих строк, допустим, в цикле. Впрочем, эту конструкцию можно еще оптимизировать:
String result = new StringBuffer(a).append(b).toString();
и за счет того, что в конструктор первая строка уже будет передаваться, этот способ будет работать немного быстрее.
Способ #4: StringBuilder
Использование аналогично предыдущему пункту, вместо StringBuffer — StringBuilder. Будет работать быстрее из-за не-синхронизированных методов последнего. Правда, проверка на java6 показала, что время почти не отличается, хотя на 5-й — почти на треть. Оптимизировали значит.
Способ #5: Re-using variables
Часто в советах оптимизации проскакивает — не плодите сущностей (с), то-есть используйте уже созданные экземпляры классов. Можем применить это для нашей конкатенации:
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 |
Исходный код программы-мерялки:
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