【问题标题】:Correct way to use StringBuilder in SQL在 SQL 中使用 StringBuilder 的正确方法
【发布时间】:2012-02-02 06:54:37
【问题描述】:

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

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

StringBuilder 是否实现了它的目标,即减少内存使用?

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

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

两个语句的内存使用量是否相等?请说清楚。

提前致谢!

编辑:

顺便说一句,这不是我的代码。在一个旧项目中找到它。此外,查询并不像我的示例中的那么小。 :)

【问题讨论】:

标签: java string stringbuilder


【解决方案1】:

使用StringBuilder的目的,即减少内存。实现了吗?

不,一点也不。该代码未正确使用StringBuilder。 (不过,我认为您引用错误了;在id2table 周围肯定没有引号吗?)

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

这会占用内存等于使用下面的字符串吗?

不,它会导致 更多 内存流失,而不仅仅是您引用的直接连接。 (直到/除非 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 characters,它通常太小,导致StringBuilder 必须重新分配以使其更大(IIRC,在 Sun/Oracle JDK 中,它会增加一倍 [或更多,如果它知道每次空间不足时都需要更多来满足特定的append]。

如果使用 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();

没关系,虽然默认构造函数和后续的重新分配并不理想,但很有可能它已经足够好了——并且串联很多更具可读性。

但这仅适用于单个表达式。多个StringBuilders 用于此:

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 行。这样好吗?
  • @Vanathi:(“问题”,而不是“怀疑”——这是一种常见的误译。)这很好,但可能会导致多次重新分配,因为 StringBuilder 最初会被分配足够的空间您传递给构造函数的字符串加上 16 个字符。因此,如果您追加超过 16 个字符(我敢说您是,如果有 60 个追加!),StringBuilder 将不得不重新分配至少一次,可能多次。如果你有一个合理的想法最终结果会有多大(比如 400 个字符),最好先做sql = new StringBuilder(400);(或其他)然后再做appends。
  • @Vanathi:很高兴有帮助。是的,如果它将是 6,000 个字符,提前告诉StringBuilder 将节省大约 8 个内存重新分配(假设初始字符串约为 10 个字符,SB 本来是 26,然后加倍到 52,然后是 104 、208、416、832、1664、3328,最后是 6656)。仅当这是一个热点时才有意义,但如果您提前知道... :-)
  • @T.J.克劳德你的意思是说我不能使用“+”运算符来获得更好的性能。正确的?那么为什么 Oracal 在他们的语言中添加了“+”运算符,您能详细说明一下吗?无论如何我都赞成您的回答。
【解决方案2】:

当您已经拥有了所有想要附加的“片段”时,使用StringBuilder 根本没有意义。根据您的示例代码在同一调用中使用StringBuilder 字符串连接更糟糕。

这样会更好:

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

在这种情况下,字符串连接实际上是在编译时发生的,所以它相当于更简单的:

return "select id1, id2 from table";

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

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

我有一个 article on String / StringBuffer 不久前写的 - 在 StringBuilder 出现之前。不过,这些原则同样适用于StringBuilder

【讨论】:

    【解决方案3】:

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

    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";
    

    但是,即使您附加任何字段或其他非常量,编译器也会使用 internal 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 并附加到它的代码,因此最终结果毕竟不是那么糟糕。

      当然,任何看到该代码的人都会认为它有点迟钝。

      【讨论】:

        【解决方案5】:

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

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

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

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

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

        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

        【讨论】:

        • 不,你不应该。它比使用+ 的可读性差,无论如何它都会被转换成相同的代码。 StringBuilder 在您无法在单个表达式中执行所有连接时很有用,但在这种情况下则不然。
        • 我知道问题中的字符串是作为示例发布的。既不使用 StringBuilder 也不添加不同的片段来构建这样的“固定”字符串是没有意义的,因为您可以在单个常量“select id1, id2 from table”中定义它
        • 但是,即使变量中有非常量值,如果您要使用 StringBuilder,它仍然会使用单个 StringBuilder - 那么为什么不这样做呢?这个问题没有表明任何事情都需要通过StringBuilder
        【解决方案6】:

        你也可以使用MessageFormat

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-12-19
          • 1970-01-01
          • 1970-01-01
          • 2021-05-03
          • 2012-10-31
          • 2023-03-21
          • 2015-08-13
          • 1970-01-01
          相关资源
          最近更新 更多