在SQL中正确使用StringBuilder的方法

88

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

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

这个StringBuilder能够达到它的目的,即减少内存使用吗?

我怀疑它不能,因为在构造函数中使用了'+'(字符串连接操作符)。它会像下面的代码那样占用相同的内存吗?据我所知,在使用StringBuilder.append()时会有所不同。

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

这两个语句在内存使用方面是否相等?请澄清。

编辑:

顺便说一下,这不是我的代码。我是在一个旧项目中发现它的。而且查询不像我示例中那样简单:)


1
SQL安全性:始终使用PreparedStatement或类似的东西:https://docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html - Christophe Roussy
除了内存使用方面的问题,为什么不使用 SQL 构建器库呢:https://dev59.com/EnRC5IYBdhLWcg3wOeWB - Lukas Eder
6个回答

182

使用StringBuilder的目的是减少内存,这个目的实现了吗?

不,完全没有。那段代码并没有正确地使用StringBuilder。(不过我认为你引用的时候可能出错了,id2table周围应该没有引号吧?)

请注意,通常的目的是减少内存波动而不是总内存使用量,以便减轻垃圾收集器的负担。

这会占用和使用String一样多的内存吗?

不,这将导致比您引用的直接连接更多的内存波动。(除非JVM优化器发现代码中的显示StringBuilder是不必要的并将其优化掉,如果它可以的话。)

如果代码作者想要使用StringBuilder(有正反两种观点,请参见本答案末尾的说明),最好正确地使用它(在这里,我假设id2table周围实际上没有引号):

StringBuilder sb = new StringBuilder(some_appropriate_size);
sb.append("select id1, ");
sb.append(id2);
sb.append(" from ");
sb.append(table);
return sb.toString();
请注意,我已经在StringBuilder构造函数中列出了适当的大小“some_appropriate_size”,以便它具有足够的容量来附加我们要附加的完整内容。如果您没有指定默认大小,则使用的默认大小为16个字符,这通常太小了,会导致StringBuilder必须重新分配空间使自己变得更大(如果我没记错,在Sun / Oracle JDK中,它每次用完空间时都会将其倍增[或更多,如果它知道需要更多来满足特定的附加需求])。
您可能已经听说过,如果使用Sun/Oracle编译器编译,则字符串连接会在后台使用一个StringBuilder。这是真的,它会为整个表达式使用一个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();

所以那样做是可以的,虽然默认构造函数和随后的重新分配并不理想,但很可能足够好—而且字符串拼接更易读。

但这仅适用于单个表达式。为此使用多个 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,除非我有充分的理由怀疑它会成为一个重大问题。 - Jon Skeet
@Crowder还有一个疑问。StringBuilder sql = new StringBuilder(" XXX); sql.append("nndmn");...。类似的sql.append行大约有60行。这样做可以吗? - Vaandu
1
@Vanathi:(“问题”,而不是“怀疑”——这是一个常见的误译。)这样做没问题,但可能会导致多次重新分配内存,因为 StringBuilder 最初分配的空间仅够容纳你传递给构造函数的字符串加上 16 个字符。所以如果你添加的字符数超过了 16 个(如果有 60 次添加,我敢说你肯定超过了!),那么 StringBuilder 就必须至少重新分配一次内存,甚至可能需要多次重新分配。如果你对最终结果的大小有一个合理的估计(比如说,400 个字符),最好先使用 sql = new StringBuilder(400);(或其他大小),然后再进行 append 操作。 - T.J. Crowder
@Vanathi:很高兴能帮到你。是的,如果要达到6,000个字符,提前告诉StringBuilder可以节省大约8次内存重新分配(假设初始字符串大约有10个字符,则SB一开始就是26,然后加倍到52,104,208,416,832,1664,3328,最后是6656)。只有在这是热点情况下才会显著,但是如果您事先知道... :-) - T.J. Crowder
@T.J. Crowder,您的意思是我不应该使用“+”运算符以获得更好的性能。对吗?那么为什么Oracle在他们的语言中添加了“+”运算符,您能否详细说明一下?无论如何,我会给您的答案点赞。 - Smit Patel
我想知道Java的实现者为什么决定使用StringBuilder,而不是让String包括一个类型为String[]的构造函数?对于任意数量和大小的参数,后一种方法的GC开销总和将是一个字符串引用数组。相比之下,使用StringBuilder保证了至少与结果字符串大小相等的开销。 - supercat

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();

我使用 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);

最后,正如 @T.J.rowder 所指出的那样,您应该始终猜测 StringBuilder 的大小。这将减少在扩展内部缓冲区大小时创建的 char[] 对象的数量。

4
您猜测使用字符串构建器的目的并没有完全实现,事实上是正确的。然而,当编译器看到表达式“select id1, ”+“ id2 ”+“ from ”+“ table”时,它会发出代码,实际上在幕后创建一个StringBuilder并进行附加,因此最终结果并不那么糟糕。但是,当然,任何查看该代码的人都会认为它有点愚蠢。

2
在您发布的代码中,没有任何优势,因为您误用了StringBuilder。您在两种情况下都构建相同的字符串。使用StringBuilder,您可以使用append方法避免对字符串进行+操作。您应该这样使用它:
return new StringBuilder("select id1, ").append(" id2 ").append(" from ").append(" table").toString();

在Java中,String类型是一个不可变的字符序列,因此当你添加两个字符串时,虚拟机会创建一个新的字符串值,其中包含连接的两个操作数。
StringBuilder提供了一个可变的字符序列,您可以使用它来连接不同的值或变量而不创建新的字符串对象,因此它有时比使用字符串更有效率。
这提供了一些有用的功能,例如更改作为参数传递给另一个方法的字符序列的内容,这是您无法使用字符串完成的。
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。该网页与Java数据缓冲区相关。

1
不,你不应该这样做。使用+更易读,而且最终会被转换为相同的代码。当你无法在单个表达式中执行所有连接操作时,StringBuilder很有用,但在这种情况下不需要使用它。 - Jon Skeet
1
我理解问题中的字符串是作为示例发布的。使用StringBuilder或添加不同的片段来构建这样一个“固定”的字符串没有任何意义,因为你可以在一个常量中定义它,如“select id1,id2 from table”。 - Tomas Narros
即使变量中有非常量值,如果您使用return "select id1, " + foo + "something else" + bar;,它仍将使用单个StringBuilder- 那么为什么不这样做呢?问题没有提供任何指示需要传递StringBuilder - Jon Skeet

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接