que - Concatenación de StringBuilder vs String en toString() en Java
stringbuilder string java (18)
¿Puedo señalar que si va a iterar sobre una colección y usar StringBuilder, es posible que desee revisar Apache Commons Lang y StringUtils.join() (en diferentes sabores)?
Independientemente del rendimiento, le ahorrará tener que crear StringBuilders y bucles por lo que parece ser la millonésima vez.
Dadas las 2 implementaciones de toString()
continuación, cuál es la preferida:
public String toString(){
return "{a:"+ a + ", b:" + b + ", c: " + c +"}";
}
o
public String toString(){
StringBuilder sb = new StringBuilder(100);
return sb.append("{a:").append(a)
.append(", b:").append(b)
.append(", c:").append(c)
.append("}")
.toString();
}
?
Más importante aún, dado que solo tenemos 3 propiedades, es posible que no haya una diferencia, pero ¿en qué momento cambiaría de +
concat a StringBuilder
?
Apache Commons-Lang tiene una clase ToStringBuilder que es muy fácil de usar. Hace un buen trabajo tanto en el manejo de la lógica de apéndice como en el formato de cómo desea que se vea su toString.
public void toString() {
ToStringBuilder tsb = new ToStringBuilder(this);
tsb.append("a", a);
tsb.append("b", b)
return tsb.toString();
}
com.blah.YourClass@abc1321f[a=whatever, b=foo]
salida que se ve como com.blah.YourClass@abc1321f[a=whatever, b=foo]
.
O en una forma más condensada utilizando el encadenamiento:
public void toString() {
return new ToStringBuilder(this).append("a", a).append("b", b").toString();
}
O si quieres usar la reflexión para incluir todos los campos de la clase:
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
También puede personalizar el estilo de ToString si lo desea.
Comparé cuatro enfoques diferentes para comparar el rendimiento. No sé exactamente qué le pasa a gc, pero lo importante para mí es el tiempo. El compilador es un factor importante aquí. Usé jdk1.8.0_45 en la plataforma window8.1.
concatWithPlusOperator = 8
concatWithBuilder = 130
concatWithConcat = 127
concatStringFormat = 3737
concatWithBuilder2 = 46
public class StringConcatenationBenchmark {
private static final int MAX_LOOP_COUNT = 1000000;
public static void main(String[] args) {
int loopCount = 0;
long t1 = System.currentTimeMillis();
while (loopCount < MAX_LOOP_COUNT) {
concatWithPlusOperator();
loopCount++;
}
long t2 = System.currentTimeMillis();
System.out.println("concatWithPlusOperator = " + (t2 - t1));
long t3 = System.currentTimeMillis();
loopCount = 0;
while (loopCount < MAX_LOOP_COUNT) {
concatWithBuilder();
loopCount++;
}
long t4 = System.currentTimeMillis();
System.out.println("concatWithBuilder = " + (t4 - t3));
long t5 = System.currentTimeMillis();
loopCount = 0;
while (loopCount < MAX_LOOP_COUNT) {
concatWithConcat();
loopCount++;
}
long t6 = System.currentTimeMillis();
System.out.println("concatWithConcat = " + (t6 - t5));
long t7 = System.currentTimeMillis();
loopCount = 0;
while (loopCount < MAX_LOOP_COUNT) {
concatStringFormat();
loopCount++;
}
long t8 = System.currentTimeMillis();
System.out.println("concatStringFormat = " + (t8 - t7));
long t9 = System.currentTimeMillis();
loopCount = 0;
while (loopCount < MAX_LOOP_COUNT) {
concatWithBuilder2();
loopCount++;
}
long t10 = System.currentTimeMillis();
System.out.println("concatWithBuilder2 = " + (t10 - t9));
}
private static void concatStringFormat() {
String s = String.format("%s %s %s %s ", "String", "String", "String", "String");
}
private static void concatWithConcat() {
String s = "String".concat("String").concat("String").concat("String");
}
private static void concatWithBuilder() {
StringBuilder builder=new StringBuilder("String");
builder.append("String").append("String").append("String");
String s = builder.toString();
}
private static void concatWithBuilder2() {
String s = new StringBuilder("String").append("String").append("String").append("String").toString();
}
private static void concatWithPlusOperator() {
String s = "String" + "String" + "String" + "String";
}
}
Creo que deberíamos ir con el enfoque de apéndice de StringBuilder La razón es
La concatenación de cadenas creará un nuevo objeto de cadena cada vez (ya que la cadena es un objeto inmutable), por lo que creará 3 objetos.
Con el generador de cadenas solo se creará un objeto [StringBuilder es muttable] y la cadena adicional se le agrega.
Desde Java 1.5, la concatenación simple de una línea con "+" y StringBuilder.append () generan exactamente el mismo bytecode.
Entonces, por el bien de la legibilidad del código, use "+".
2 excepciones:
- entorno multihilo: StringBuffer
- concatenación en bucles: StringBuilder / StringBuffer
En Java 9, la versión 1 debería ser más rápida porque se convierte en llamada invokedynamic
. Más detalles se pueden encontrar en JEP-280 :
La idea es reemplazar la danza de apéndice de StringBuilder por una simple llamada invocada a java.lang.invoke.StringConcatFactory, que aceptará los valores en la necesidad de concatenación.
En cuanto al rendimiento, la concatenación de cadenas con ''+'' es más costosa porque tiene que hacer una copia completamente nueva de la cadena, ya que las cadenas son inmutables en java. Esto juega un papel particular si la concatenación es muy frecuente, por ejemplo: dentro de un bucle. Lo siguiente es lo que sugiere mi IDEA cuando intento hacer tal cosa:
Reglas generales:
- Dentro de una sola asignación de cadena, usar la concatenación de cadenas está bien.
- Si estás haciendo un bucle para crear un gran bloque de datos de caracteres, ve a StringBuffer.
- El uso de + = en una Cadena siempre será menos eficiente que el uso de un StringBuffer, por lo que debe sonar campanas de advertencia, pero en ciertos casos la optimización obtenida será insignificante en comparación con los problemas de legibilidad, así que use su sentido común.
Aquí hay un buen blog de Jon Skeet sobre este tema.
En la mayoría de los casos, no verá una diferencia real entre los dos enfoques, pero es fácil construir un escenario del peor de los casos como este:
public class Main
{
public static void main(String[] args)
{
long now = System.currentTimeMillis();
slow();
System.out.println("slow elapsed " + (System.currentTimeMillis() - now) + " ms");
now = System.currentTimeMillis();
fast();
System.out.println("fast elapsed " + (System.currentTimeMillis() - now) + " ms");
}
private static void fast()
{
StringBuilder s = new StringBuilder();
for(int i=0;i<100000;i++)
s.append("*");
}
private static void slow()
{
String s = "";
for(int i=0;i<100000;i++)
s+="*";
}
}
La salida es:
slow elapsed 11741 ms
fast elapsed 7 ms
El problema es que agregar + a una cadena reconstruye una cadena nueva, por lo que cuesta algo lineal a la longitud de las cadenas (suma de ambas).
Entonces, a tu pregunta:
El segundo enfoque sería más rápido, pero es menos legible y más difícil de mantener. Como dije, en su caso específico probablemente no vería la diferencia.
Haga que el método toString sea lo más legible posible.
La única excepción para esto en mi libro es si puedes probarme que consume recursos significativos :) (Sí, esto significa perfilar)
También tenga en cuenta que el compilador Java 5 genera un código más rápido que el enfoque "StringBuffer" escrito a mano utilizado en versiones anteriores de Java. Si usas "+", estas mejoras y las futuras vienen gratis.
La clave es si está escribiendo una sola concatenación en un solo lugar o acumulando con el tiempo.
Para el ejemplo que dio, no tiene sentido usar StringBuilder explícitamente. (Mire el código compilado para su primer caso).
Pero si está creando una cadena, por ejemplo, dentro de un bucle, use StringBuilder.
Para aclarar, asumiendo que hugeArray contiene miles de cadenas, código como este:
...
String result = "";
for (String s : hugeArray) {
result = result + s;
}
es una pérdida de tiempo y de memoria en comparación con:
...
StringBuilder sb = new StringBuilder();
for (String s : hugeArray) {
sb.append(s);
}
String result = sb.toString();
La versión 1 es preferible porque es más corta y , de hecho, el compilador la convertirá en la versión 2 , sin ninguna diferencia de rendimiento.
Y lo que es más importante, dado que solo tenemos 3 propiedades, es posible que no haya una diferencia, pero ¿en qué momento cambia de concat a constructor?
En el punto en el que estás concatenando en un bucle, eso ocurre cuando el compilador no puede sustituir a StringBuilder
por sí mismo.
Para cuerdas simples como esa prefiero usar
"string".concat("string").concat("string");
En orden, diría que el método preferido para construir una cadena es usar StringBuilder, String # concat (), luego el operador sobrecargado +. StringBuilder es un aumento significativo en el rendimiento cuando se trabajan cadenas de gran tamaño, al igual que usar el operador +, hay una gran disminución en el rendimiento (una disminución exponencialmente grande a medida que aumenta el tamaño de la Cadena). El único problema con el uso de .concat () es que puede lanzar NullPointerExceptions.
Parece que hay cierto debate sobre si StringBuilder sigue siendo necesario con los compiladores actuales. Así que pensé que iba a dar mis 2 centavos de experiencia.
Tengo un conjunto de resultados de JDBC
de 10k registros (sí, los necesito todos en un solo lote). El uso del operador + lleva aproximadamente 5 minutos en mi máquina con Java 1.8
. El uso de stringBuilder.append("")
toma menos de un segundo para la misma consulta.
Así que la diferencia es enorme. Dentro de un bucle, StringBuilder
es mucho más rápido.
Por razones de rendimiento, se desaconseja el uso de +=
(concatenación de String
). La razón por la cual es: Java String
es un sistema inmutable, cada vez que se realiza una nueva concatenación, se crea una nueva String
(la nueva tiene una huella digital diferente a la anterior en el pool de String ). Crear nuevas cadenas ejerce presión sobre el GC y ralentiza el programa: la creación de objetos es costosa.
Debajo del código debería hacerlo más práctico y claro al mismo tiempo.
public static void main(String[] args)
{
// warming up
for(int i = 0; i < 100; i++)
RandomStringUtils.randomAlphanumeric(1024);
final StringBuilder appender = new StringBuilder();
for(int i = 0; i < 100; i++)
appender.append(RandomStringUtils.randomAlphanumeric(i));
// testing
for(int i = 1; i <= 10000; i*=10)
test(i);
}
public static void test(final int howMany)
{
List<String> samples = new ArrayList<>(howMany);
for(int i = 0; i < howMany; i++)
samples.add(RandomStringUtils.randomAlphabetic(128));
final StringBuilder builder = new StringBuilder();
long start = System.nanoTime();
for(String sample: samples)
builder.append(sample);
builder.toString();
long elapsed = System.nanoTime() - start;
System.out.printf("builder - %d - elapsed: %dus/n", howMany, elapsed / 1000);
String accumulator = "";
start = System.nanoTime();
for(String sample: samples)
accumulator += sample;
elapsed = System.nanoTime() - start;
System.out.printf("concatenation - %d - elapsed: %dus/n", howMany, elapsed / (int) 1e3);
start = System.nanoTime();
String newOne = null;
for(String sample: samples)
newOne = new String(sample);
elapsed = System.nanoTime() - start;
System.out.printf("creation - %d - elapsed: %dus/n/n", howMany, elapsed / 1000);
}
Los resultados de una carrera se informan a continuación.
builder - 1 - elapsed: 132us
concatenation - 1 - elapsed: 4us
creation - 1 - elapsed: 5us
builder - 10 - elapsed: 9us
concatenation - 10 - elapsed: 26us
creation - 10 - elapsed: 5us
builder - 100 - elapsed: 77us
concatenation - 100 - elapsed: 1669us
creation - 100 - elapsed: 43us
builder - 1000 - elapsed: 511us
concatenation - 1000 - elapsed: 111504us
creation - 1000 - elapsed: 282us
builder - 10000 - elapsed: 3364us
concatenation - 10000 - elapsed: 5709793us
creation - 10000 - elapsed: 972us
Sin considerar los resultados de 1 concatenación (JIT aún no estaba haciendo su trabajo), incluso para 10 concatenaciones la penalización de rendimiento es relevante; Para miles de concatenaciones, la diferencia es enorme.
Lecciones aprendidas de este experimento muy rápido (fácilmente reproducible con el código anterior): nunca use el +=
para concatenar cadenas, incluso en casos muy básicos donde se necesitan algunas concatenaciones (como se dijo, crear cadenas nuevas es costoso y pone presión) en el GC).
También tuve un conflicto con mi jefe sobre el hecho de si usar append o + .Como están usando Append (todavía no puedo entender lo que dicen cada vez que se crea un nuevo objeto). Así que pensé en hacer algo de I + D. Aunque me encanta la explicación de Michael Borgwardt, pero solo quería mostrar una explicación si alguien realmente necesita saber en el futuro.
/**
*
* @author Perilbrain
*/
public class Appc {
public Appc() {
String x = "no name";
x += "I have Added a name" + "We May need few more names" + Appc.this;
x.concat(x);
// x+=x.toString(); --It creates new StringBuilder object before concatenation so avoid if possible
//System.out.println(x);
}
public void Sb() {
StringBuilder sbb = new StringBuilder("no name");
sbb.append("I have Added a name");
sbb.append("We May need few more names");
sbb.append(Appc.this);
sbb.append(sbb.toString());
// System.out.println(sbb.toString());
}
}
y el desmontaje de la clase anterior sale como
.method public <init>()V //public Appc()
.limit stack 2
.limit locals 2
met001_begin: ; DATA XREF: met001_slot000i
.line 12
aload_0 ; met001_slot000
invokespecial java/lang/Object.<init>()V
.line 13
ldc "no name"
astore_1 ; met001_slot001
.line 14
met001_7: ; DATA XREF: met001_slot001i
new java/lang/StringBuilder //1st object of SB
dup
invokespecial java/lang/StringBuilder.<init>()V
aload_1 ; met001_slot001
invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan/
g/StringBuilder;
ldc "I have Added a nameWe May need few more names"
invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan/
g/StringBuilder;
aload_0 ; met001_slot000
invokevirtual java/lang/StringBuilder.append(Ljava/lang/Object;)Ljava/lan/
g/StringBuilder;
invokevirtual java/lang/StringBuilder.toString()Ljava/lang/String;
astore_1 ; met001_slot001
.line 15
aload_1 ; met001_slot001
aload_1 ; met001_slot001
invokevirtual java/lang/String.concat(Ljava/lang/String;)Ljava/lang/Strin/
g;
pop
.line 18
return //no more SB created
met001_end: ; DATA XREF: met001_slot000i ...
; ===========================================================================
;met001_slot000 ; DATA XREF: <init>r ...
.var 0 is this LAppc; from met001_begin to met001_end
;met001_slot001 ; DATA XREF: <init>+6w ...
.var 1 is x Ljava/lang/String; from met001_7 to met001_end
.end method
;44-1=44
; ---------------------------------------------------------------------------
; Segment type: Pure code
.method public Sb()V //public void Sb
.limit stack 3
.limit locals 2
met002_begin: ; DATA XREF: met002_slot000i
.line 21
new java/lang/StringBuilder
dup
ldc "no name"
invokespecial java/lang/StringBuilder.<init>(Ljava/lang/String;)V
astore_1 ; met002_slot001
.line 22
met002_10: ; DATA XREF: met002_slot001i
aload_1 ; met002_slot001
ldc "I have Added a name"
invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan/
g/StringBuilder;
pop
.line 23
aload_1 ; met002_slot001
ldc "We May need few more names"
invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan/
g/StringBuilder;
pop
.line 24
aload_1 ; met002_slot001
aload_0 ; met002_slot000
invokevirtual java/lang/StringBuilder.append(Ljava/lang/Object;)Ljava/lan/
g/StringBuilder;
pop
.line 25
aload_1 ; met002_slot001
aload_1 ; met002_slot001
invokevirtual java/lang/StringBuilder.toString()Ljava/lang/String;
invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan/
g/StringBuilder;
pop
.line 28
return
met002_end: ; DATA XREF: met002_slot000i ...
;met002_slot000 ; DATA XREF: Sb+25r
.var 0 is this LAppc; from met002_begin to met002_end
;met002_slot001 ; DATA XREF: Sb+9w ...
.var 1 is sbb Ljava/lang/StringBuilder; from met002_10 to met002_end
.end method
;96-49=48
; ---------------------------------------------------------------------------
De los dos códigos anteriores puede ver que Michael tiene razón. En cada caso, solo se crea un objeto SB.
Usando la última versión de Java (1.8), el desensamblaje ( javap -c
) muestra la optimización introducida por el compilador. +
también sb.append()
generará un código muy similar. Sin embargo, valdrá la pena inspeccionar el comportamiento si estamos usando +
en un bucle for.
Añadiendo cadenas usando + en un bucle for
Java:
public String myCatPlus(String[] vals) {
String result = "";
for (String val : vals) {
result = result + val;
}
return result;
}
ByteCode :( for
extracto del bucle)
12: iload 5
14: iload 4
16: if_icmpge 51
19: aload_3
20: iload 5
22: aaload
23: astore 6
25: new #3 // class java/lang/StringBuilder
28: dup
29: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
32: aload_2
33: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: aload 6
38: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
41: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
44: astore_2
45: iinc 5, 1
48: goto 12
Añadiendo cadenas usando stringbuilder.append
Java:
public String myCatSb(String[] vals) {
StringBuilder sb = new StringBuilder();
for(String val : vals) {
sb.append(val);
}
return sb.toString();
}
ByteCdoe :( for
extracto de bucle)
17: iload 5
19: iload 4
21: if_icmpge 43
24: aload_3
25: iload 5
27: aaload
28: astore 6
30: aload_2
31: aload 6
33: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: pop
37: iinc 5, 1
40: goto 17
43: aload_2
Sin embargo, hay un poco de diferencia evidente . En el primer caso, donde se usó +
, se crea un nuevo StringBuilder
para cada iteración de bucle y el resultado generado se almacena haciendo una llamada a toString()
(del 29 al 41). Por lo tanto, está generando cadenas intermedias que realmente no necesita mientras usa el operador +
en el bucle for
.
Vea el ejemplo a continuación:
//java8
static void main(String[] args) {
case1();//str.concat
case2();//+=
case3();//StringBuilder
}
static void case1() {
List<Long> savedTimes = new ArrayList();
long startTimeAll = System.currentTimeMillis();
String str = "";
for (int i = 0; i < MAX_ITERATIONS; i++) {
long startTime = System.currentTimeMillis();
str = str.concat(UUID.randomUUID()+"---");
saveTime(savedTimes, startTime);
}
System.out.println("Created string of length:"+str.length()+" in "+(System.currentTimeMillis()-startTimeAll)+" ms");
}
static void case2() {
List<Long> savedTimes = new ArrayList();
long startTimeAll = System.currentTimeMillis();
String str = "";
for (int i = 0; i < MAX_ITERATIONS; i++) {
long startTime = System.currentTimeMillis();
str+=UUID.randomUUID()+"---";
saveTime(savedTimes, startTime);
}
System.out.println("Created string of length:"+str.length()+" in "+(System.currentTimeMillis()-startTimeAll)+" ms");
}
static void case3() {
List<Long> savedTimes = new ArrayList();
long startTimeAll = System.currentTimeMillis();
StringBuilder str = new StringBuilder("");
for (int i = 0; i < MAX_ITERATIONS; i++) {
long startTime = System.currentTimeMillis();
str.append(UUID.randomUUID()+"---");
saveTime(savedTimes, startTime);
}
System.out.println("Created string of length:"+str.length()+" in "+(System.currentTimeMillis()-startTimeAll)+" ms");
}
static void saveTime(List<Long> executionTimes, long startTime) {
executionTimes.add(System.currentTimeMillis()-startTime);
if(executionTimes.size()%CALC_AVG_EVERY == 0) {
out.println("average time for "+executionTimes.size()+" concatenations: "+
NumberFormat.getInstance().format(executionTimes.stream().mapToLong(Long::longValue).average().orElseGet(()->0))+
" ms avg");
executionTimes.clear();
}
}
Salida:
tiempo promedio para 10000 concatenaciones: 0.096 ms avg
tiempo promedio para 10000 concatenaciones: 0.185 ms avg
tiempo promedio para 10000 concatenaciones: 0.327 ms avg
tiempo promedio para 10000 concatenaciones: 0.501 ms avg
tiempo promedio para 10000 concatenaciones: 0.656 ms avg
Cadena de longitud creada: 1950000 en 17745 ms.
tiempo promedio para 10000 concatenaciones: 0.21 ms avg
tiempo promedio para 10000 concatenaciones: 0.652 ms avg
tiempo promedio para 10000 concatenaciones: 1.129 ms avg
tiempo promedio para 10000 concatenaciones: 1.727 ms avg
tiempo promedio para 10000 concatenaciones: 2.302 ms avg
Cadena de longitud creada: 1950000 en 60279 ms.
tiempo promedio para 10000 concatenaciones: 0.002 ms avg
tiempo promedio para 10000 concatenaciones: 0.002 ms avg
tiempo promedio para 10000 concatenaciones: 0.002 ms avg
tiempo promedio para 10000 concatenaciones: 0.002 ms avg
tiempo promedio para 10000 concatenaciones: 0.002 ms avg
Cadena de longitud creada: 1950000 en 100 ms.
A medida que aumenta la longitud de la cadena, también lo hace el tiempo de concatenación.
Ahí es donde definitivamente se necesita el StringBuilder
.
Como ves, la concatenación: UUID.randomUUID()+"---"
, no afecta realmente el tiempo.
PD: no creo que cuándo usar StringBuilder en Java sea realmente un duplicado de esto.
Esta pregunta se refiere a toString()
que la mayoría de las veces no realiza concatenaciones de cadenas inmensas.
Yo prefiero:
String.format( "{a: %s, b: %s, c: %s}", a, b, c );
... porque es corto y legible.
No optimizaría esto para la velocidad a menos que lo use dentro de un bucle con un número de repeticiones muy alto y haya medido la diferencia de rendimiento.
Estoy de acuerdo, que si tiene que generar una gran cantidad de parámetros, este formulario puede ser confuso (como lo dice uno de los comentarios). En este caso, cambiaría a una forma más legible (tal vez utilizando ToStringBuilder de apache-commons, tomado de la respuesta de matt b) e ignorar el rendimiento de nuevo.