在SQL中使用StringBuilder的正确方法


88

我刚刚在我的项目中发现了一些这样的sql查询构建:

return (new StringBuilder("select id1, " + " id2 " + " from " + " table")).toString();

这是否StringBuilder达到其目标,即减少内存使用量?

我对此表示怀疑,因为在构造函数中使用了“ +”(字符串concat运算符)。像下面的代码那样,将占用与使用String相同的内存量吗?我了解,使用时有所不同StringBuilder.append()

return "select id1, " + " id2 " + " from " + " table";

两条语句的内存使用量是否相等?请澄清。

提前致谢!

编辑:

顺便说一句,这不是我的代码。在一个旧项目中发现它。而且,查询的大小不如我的示例中的查询小。:)


1
SQL安全性:始终使用PreparedStatement或类似的方式:docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html
Christophe Roussy

除了使用内存之外,为什么不使用SQL Builder库呢?stackoverflow.com/q/370818/521799
Lukas Eder

Answers:


182

使用StringBuilder的目的是减少内存。完成了吗?

一点都不。该代码未StringBuilder正确使用。(不过,我认为您的报价有误;id2和的周围肯定没有引号table吗?)

请注意,通常的目的是减少内存流失,而不是减少使用的总内存,以使垃圾收集器的工作更加轻松。

这样占用的内存等于使用如下所示的String吗?

不,这将导致更多的内存混乱,而不仅仅是您引用的直接连接。(除非/除非JVM优化器认为StringBuilder代码中的显式内容是不必要的,否则将其优化)。

如果该代码的作者想要使用StringBuilder(有参数支持,但也有反对;请参见此答案末尾的注释),则最好做得好(在此我假设id2和之间实际上没有引号table):

StringBuilder sb = new StringBuilder(some_appropriate_size);
sb.append("select id1, ");
sb.append(id2);
sb.append(" from ");
sb.append(table);
return sb.toString();

请注意,我已经some_appropriate_sizeStringBuilder构造函数中列出了它,以便它以足够的容量开始要添加的全部内容。如果不指定一个字符,则使用的默认大小为16个字符,通常太小,导致StringBuilder必须进行重新分配以使其自身变大(IIRC,在Sun / Oracle JDK中,它会自身加倍[或更多,如果它知道append每次用完房间都需要更多满足特定的条件。

您可能已经听说过,如果使用Sun / Oracle编译器进行编译,则字符串连接StringBuilder在幕后使用a 。的确如此,它将StringBuilder为整体表达使用一个。但是它将使用默认构造函数,这意味着在大多数情况下,它将不得不进行重新分配。不过,它更容易阅读。请注意,这对于一系列串联是正确的。因此,例如,它使用一个:StringBuilder

return "prefix " + variable1 + " middle " + variable2 + " end";

它大致翻译为:

StringBuilder tmp = new StringBuilder(); // Using default 16 character size
tmp.append("prefix ");
tmp.append(variable1);
tmp.append(" middle ");
tmp.append(variable2);
tmp.append(" end");
return tmp.toString();

所以没关系,虽然默认的构造函数和随后的重新分配(S)不理想,赔率是它不够好-以及级联是一个很大的可读性。

但这仅适用于单个表达式。StringBuilder为此使用了多个:

String s;
s = "prefix ";
s += variable1;
s += " middle ";
s += variable2;
s += " end";
return s;

最终变成这样的东西:

String s;
StringBuilder tmp;
s = "prefix ";
tmp = new StringBuilder();
tmp.append(s);
tmp.append(variable1);
s = tmp.toString();
tmp = new StringBuilder();
tmp.append(s);
tmp.append(" middle ");
s = tmp.toString();
tmp = new StringBuilder();
tmp.append(s);
tmp.append(variable2);
s = tmp.toString();
tmp = new StringBuilder();
tmp.append(s);
tmp.append(" end");
s = tmp.toString();
return s;

...这很丑。

但是要记住,重要的是,在几乎所有情况下,这都无关紧要,除非具有特定的性能问题,否则最好选择可读性(增强可维护性)。


是的,那更好。无参数构造函数的使用有点不幸,但不太可能有意义。我仍然会使用单个x + y + z表达式,而不是StringBuilder除非有充分的理由怀疑这将是一个重要问题。
乔恩·斯基特

@Crowder还有一个疑问。StringBuilder sql = new StringBuilder(" XXX); sql.append("nndmn");...。类似的sql.append线是大约60条线。这样好吗
Vaandu 2012年

1
@Vanathi :(“问题”,而不是“疑问”,这是常见的误翻译。)很好,但可能会导致多次重新分配,因为StringBuilder最初将为您通过构造函数的字符串加上16个字符分配足够的空间。因此,如果您追加16个以上的字符(我敢说您是,如果有60个追加!),StringBuilder则必须至少重新分配一次,甚至可能多次。如果您有一个合理的想法,最终结果将是多大(例如400个字符),则最好执行sql = new StringBuilder(400);(或执行其他任何操作),然后再执行appends。
TJ Crowder 2012年

@Vanathi:很高兴。是的,如果它将是6,000个字符,则告诉您StringBuilder事先将节省大约8个内存重新分配(假设初始字符串大约是10个字符,则SB开头应该是26,然后翻倍到52,然后是104、208, 416、832、1664、3328,最后是6656)。仅在这是一个热点时才有意义,但是如果您提前知道的话,它仍然......-)
TJ Crowder 2012年

@TJ Crowder,您的意思是说我一定不能使用“ +”运算符来获得更好的性能。对?那么,为什么Oracal用他们的语言添加了“ +”运算符,您能详细说明一下吗?
Smit Patel 2014年

38

当您已经拥有要附加的所有“片段”时,根本没有必要使用StringBuilder。按照示例代码在同一调用中使用StringBuilder 字符串串联甚至更糟。

这样会更好:

return "select id1, " + " id2 " + " from " + " table";

在这种情况下,字符串串联实际上是在编译时发生的,因此它等效于偶数:

return "select id1, id2 from table";

在这种情况下,使用new StringBuilder().append("select id1, ").append(" id2 ")....toString()实际上会影响性能,因为这会强制在执行时而不是在编译执行连接。哎呀。

如果实际代码通过在查询中包含来构建SQL查询,那么这是另一个单独的问题,那就是您应该使用参数化查询,在参数中而不是在SQL中指定值。

我前一段时间写过一篇关于String/StringBuffer文章 -在此之前StringBuilder。这些原则StringBuilder同样适用。


10

[[这里有一些很好的答案,但我发现他们仍然缺少一些信息。]]

return (new StringBuilder("select id1, " + " id2 " + " from " + " table"))
     .toString();

因此,正如您所指出的那样,您给出的示例过于简单,但无论如何我们还是要对其进行分析。这里发生的是编译器实际上是在+这里工作,因为"select id1, " + " id2 " + " from " + " table"它们都是常量。这样就变成了:

return new StringBuilder("select id1,  id2  from  table").toString();

在这种情况下,显然没有必要使用StringBuilder。您也可以这样做:

// the compiler combines these constant strings
return "select id1, " + " id2 " + " from " + " table";

但是,即使您要追加任何字段或其他非常量,编译器也将使用内部 字段StringBuilder-无需定义以下内容:

// an internal StringBuilder is used here
return "select id1, " + fieldName + " from " + tableName;

在幕后,这变成了近似等于的代码:

StringBuilder sb = new StringBuilder("select id1, ");
sb.append(fieldName).append(" from ").append(tableName);
return sb.toString();

实际上,唯一需要StringBuilder 直接使用的时间就是拥有条件代码时。例如,看起来像下面的代码非常需要StringBuilder

// 1 StringBuilder used in this line
String query = "select id1, " + fieldName + " from " + tableName;
if (where != null) {
   // another StringBuilder used here
   query += ' ' + where;
}

+在第一行使用一个StringBuilder实例。然后+=使用另一个StringBuilder实例。这样做效率更高:

// choose a good starting size to lower chances of reallocation
StringBuilder sb = new StringBuilder(64);
sb.append("select id1, ").append(fieldName).append(" from ").append(tableName);
// conditional code
if (where != null) {
   sb.append(' ').append(where);
}
return sb.toString();

我使用a的另一时间StringBuilder是当我从许多方法调用中构建字符串时。然后,我可以创建带有StringBuilder参数的方法:

private void addWhere(StringBuilder sb) {
   if (where != null) {
      sb.append(' ').append(where);
   }
}

使用时StringBuilder,您应同时注意的使用情况+

sb.append("select " + fieldName);

+将导致StringBuilder创建另一个内部。这当然应该是:

sb.append("select ").append(fieldName);

最后,正如@TJrowder指出的那样,您应该始终对的大小进行猜测StringBuilderchar[]当增加内部缓冲区的大小时,这将节省创建的对象数量。


4

您猜对了,至少在最大程度上没有达到使用字符串生成器的目的,这是正确的。

但是,当编译器看到该表达式时,"select id1, " + " id2 " + " from " + " table"它发出的代码实际上会在StringBuilder后台创建一个代码并将其追加到后面,因此最终结果并不是那么糟糕。

但是,当然,任何查看该代码的人都必然认为它有些迟钝。


2

在您发布的代码中,由于您滥用StringBuilder,因此没有任何优势。在两种情况下,您都构建相同的String。使用StringBuilder可以避免+使用append方法对String进行操作。您应该以这种方式使用它:

return new StringBuilder("select id1, ").append(" id2 ").append(" from ").append(" table").toString();

在Java中,String类型是一个不可变的字符序列,因此当您添加两个Strings时,VM会创建一个新的String值,并将两个操作数串联在一起。

StringBuilder提供了可变的字符序列,您可以使用它们来连接不同的值或变量,而无需创建新的String对象,因此有时它比处理字符串更有效。

这提供了一些有用的功能,例如更改在另一个方法中作为参数传递的char序列的内容,而String是无法做到的。

private void addWhereClause(StringBuilder sql, String column, String value) {
   //WARNING: only as an example, never append directly a value to a SQL String, or you'll be exposed to SQL Injection
   sql.append(" where ").append(column).append(" = ").append(value);
}

有关更多信息,请访问http://docs.oracle.com/javase/tutorial/java/data/buffers.html


1
不,你不应该。与使用相比+,它的可读性较差,无论如何都将被转换为相同的代码。StringBuilder当您无法在单个表达式中执行所有串联操作时很有用,但在这种情况下则不然。
乔恩·斯基特

1
我了解问题中的字符串是作为示例发布的。用StringBuilder或添加不同的片段来构建这样的“固定”字符串是没有意义的,因为您可以在单个常量“从表中选择id1,id2”中定义它
Tomas Narros,2012年

但是,即使有来自变量的非恒定值,也仍然使用单一的StringBuilder,如果你使用return "select id1, " + foo + "something else" + bar;-那么为什么不这样做呢?该问题没有提供任何迹象表明需要通过StringBuilder
乔恩·斯基特

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.