在Java中,最快的连接两个字符串的方法是什么?

46

在Java中,最快的连接两个字符串的方法是什么?

即:

String ccyPair = ccy1 + ccy2;

我正在使用cyPair作为HashMap中的键,并在一个非常紧密的循环中调用它以检索值。

当我进行分析时,这是瓶颈所在。

java.lang.StringBuilder.append(StringBuilder.java:119)  
java.lang.StringBuilder.(StringBuilder.java:93)

1
字符串拼接成瓶颈?这意味着所有的Java程序都存在性能问题。不要过度优化。 - Bozho
12
但他已经对代码进行了剖析,这是瓶颈所在。这不是微观优化,也不是过早优化,只是优化。 - Duncan McGregor
@Duncan,事实上这是其中一个问题。真正的问题在于循环中的ccy代码生成。它包含多个分配+内存屏障,不太快的哈希码(14 mul + add;假设ccy对像“eur / usdusd / jpy”),然后是等于号。使用持有对两个字符串的引用的持有对将是更好的解决方案。 - bestsss
17个回答

51

有很多理论 - 是时候实践一下了!

private final String s1 = new String("1234567890");
private final String s2 = new String("1234567890");

在经过热身的64位Hotspot,1.6.0_22上,在Intel Mac OS上使用1000万次普通for循环。

例如:

@Test public void testConcatenation() {
    for (int i = 0; i < COUNT; i++) {
        String s3 = s1 + s2;
    }
}
在循环中使用以下语句
String s3 = s1 + s2; 

1.33秒

String s3 = new StringBuilder(s1).append(s2).toString();

1.28秒

String s3 = new StringBuffer(s1).append(s2).toString();

1.92秒

String s3 = s1.concat(s2);

0.70秒

String s3 = "1234567890" + "1234567890";

concat函数是最优选择,除非你有静态字符串,在这种情况下,编译器已经为你处理了。


6
代码将进行优化,因此您实际上正在测试未经优化的代码。这就是您不编写微基准测试的方式。尽管如此,对于两个字符串的情况,String.contact 应该是最快的。 - bestsss
1
我承认没有进一步检查结果,因为它们正是我所期望的!但我不明白我如何测试未经优化的代码。如果 Hotspot 删除没有副作用的代码,所有这些循环将花费相同的时间,如果不是,则我正在测试运行语句的时间(加上循环)。我们不知道的是循环所需的时间,但是由于手头没有太多时间,我没有考虑到这一点;-) - Duncan McGregor
@DuncanMcGregor 在JVM优化代码之前需要一些时间。 - Thorbjørn Ravn Andersen
1
StringBuilder是一种处理大字符串快速的方式,但对于小字符串则较慢。 - Georgi Peev

24

我相信答案可能已经确定,但我发布分享代码。

简短的回答,如果你只是想要纯粹的连接字符串,那就是:String.concat(...)

输出:

ITERATION_LIMIT1: 1
ITERATION_LIMIT2: 10000000
s1: STRING1-1111111111111111111111
s2: STRING2-2222222222222222222222

iteration: 1
                                          null:    1.7 nanos
                                 s1.concat(s2):  106.1 nanos
                                       s1 + s2:  251.7 nanos
   new StringBuilder(s1).append(s2).toString():  246.6 nanos
    new StringBuffer(s1).append(s2).toString():  404.7 nanos
                 String.format("%s%s", s1, s2): 3276.0 nanos

Tests complete

样例代码:

package net.fosdal.scratch;

public class StringConcatenationPerformance {
    private static final int    ITERATION_LIMIT1    = 1;
    private static final int    ITERATION_LIMIT2    = 10000000;

    public static void main(String[] args) {
        String s1 = "STRING1-1111111111111111111111";
        String s2 = "STRING2-2222222222222222222222";
        String methodName;
        long startNanos, durationNanos;
        int iteration2;

        System.out.println("ITERATION_LIMIT1: " + ITERATION_LIMIT1);
        System.out.println("ITERATION_LIMIT2: " + ITERATION_LIMIT2);
        System.out.println("s1: " + s1);
        System.out.println("s2: " + s2);
        int iteration1 = 0;
        while (iteration1++ < ITERATION_LIMIT1) {
            System.out.println();
            System.out.println("iteration: " + iteration1);

            // method #0
            methodName = "null";
            iteration2 = 0;
            startNanos = System.nanoTime();
            while (iteration2++ < ITERATION_LIMIT2) {
                method0(s1, s2);
            }
            durationNanos = System.nanoTime() - startNanos;
            System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));

            // method #1
            methodName = "s1.concat(s2)";
            iteration2 = 0;
            startNanos = System.nanoTime();
            while (iteration2++ < ITERATION_LIMIT2) {
                method1(s1, s2);
            }
            durationNanos = System.nanoTime() - startNanos;
            System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));

            // method #2
            iteration2 = 0;
            startNanos = System.nanoTime();
            methodName = "s1 + s2";
            while (iteration2++ < ITERATION_LIMIT2) {
                method2(s1, s2);
            }
            durationNanos = System.nanoTime() - startNanos;
            System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));

            // method #3
            iteration2 = 0;
            startNanos = System.nanoTime();
            methodName = "new StringBuilder(s1).append(s2).toString()";
            while (iteration2++ < ITERATION_LIMIT2) {
                method3(s1, s2);
            }
            durationNanos = System.nanoTime() - startNanos;
            System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));

            // method #4
            iteration2 = 0;
            startNanos = System.nanoTime();
            methodName = "new StringBuffer(s1).append(s2).toString()";
            while (iteration2++ < ITERATION_LIMIT2) {
                method4(s1, s2);
            }
            durationNanos = System.nanoTime() - startNanos;
            System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));

            // method #5
            iteration2 = 0;
            startNanos = System.nanoTime();
            methodName = "String.format(\"%s%s\", s1, s2)";
            while (iteration2++ < ITERATION_LIMIT2) {
                method5(s1, s2);
            }
            durationNanos = System.nanoTime() - startNanos;
            System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));

        }
        System.out.println();
        System.out.println("Tests complete");

    }

    public static String method0(String s1, String s2) {
        return "";
    }

    public static String method1(String s1, String s2) {
        return s1.concat(s2);
    }

    public static String method2(String s1, String s2) {
        return s1 + s2;
    }

    public static String method3(String s1, String s2) {
        return new StringBuilder(s1).append(s2).toString();
    }

    public static String method4(String s1, String s2) {
        return new StringBuffer(s1).append(s2).toString();
    }

    public static String method5(String s1, String s2) {
        return String.format("%s%s", s1, s2);
    }

}

不错的评论。我一直在寻找string.format的速度,现在我看到它有点慢了 :-) 我会使用concat代替。 - Martin Podval

23
这些例程出现在基准测试中的原因是因为编译器在幕后实现了你的"+"操作符。
如果你真的需要拼接的字符串,应该让编译器用"+"做它的魔法。如果你只需要一个用于映射查找的键,一个包含两个字符串并具有适当的equals和hashMap实现的键类可能是一个好主意,因为它避免了复制步骤。
2023年编辑:Java 16引入了记录类型,编译器会自动在幕后完成繁重的工作。只需创建一个包含两个字符串字段的记录即可。

1
你有没有任何示例代码可以防止瓶颈,因为你可能知道实现部分? - Deepak
2
@Deepak,我不认为这是一个瓶颈,但在Eclipse 3.6中创建这样一个类的最简单方法是创建一个新类,给它字段ccy1和ccy2,要求Eclipse基于字段创建构造函数,并生成hashCode()和equals()方法。 - Thorbjørn Ravn Andersen

7
您应该使用在运行时生成的字符串进行测试(例如UUID.randomUUID().toString()),而不是在编译时使用字符串(例如"my string")。我的结果是:
plus:     118 ns
concat:    52 ns
builder1: 102 ns
builder2:  66 ns
buffer1:  119 ns
buffer2:   87 ns

使用这个实现:

private static long COUNT = 10000000;

public static void main(String[] args) throws Exception {
    String s1 = UUID.randomUUID().toString();
    String s2 = UUID.randomUUID().toString();
    for(String methodName : new String[] {
            "none", "plus", "concat", "builder1", "builder2", "buffer1", "buffer2"
    }) {
        Method method = ConcatPerformanceTest.class.getMethod(methodName, String.class, String.class);
        long time = System.nanoTime();
        for(int i = 0; i < COUNT; i++) {
            method.invoke((Object) null, s1, s2);
        }
        System.out.println(methodName + ": " + (System.nanoTime() - time)/COUNT + " ns");
    }
}

public static String none(String s1, String s2) {
    return null;
}

public static String plus(String s1, String s2) {
    return s1 + s2;
}

public static String concat(String s1, String s2) {
    return s1.concat(s2);
}

public static String builder1(String s1, String s2) {
    return new StringBuilder(s1).append(s2).toString();
}

public static String builder2(String s1, String s2) {
    return new StringBuilder(s1.length() + s2.length()).append(s1).append(s2).toString();
}

public static String buffer1(String s1, String s2) {
    return new StringBuffer(s1).append(s2).toString();
}

public static String buffer2(String s1, String s2) {
    return new StringBuffer(s1.length() + s2.length()).append(s1).append(s2).toString();
}

5
对于标题中的问题:String.concat通常是连接两个String的最快方法(但请注意null)。没有[过大]中间缓冲区或其他对象参与。奇怪的是,+编译成相对低效的代码,涉及StringBuilder
然而,您问题的主体指向其他问题。通过字符串连接生成地图键是一种常见的“反习惯用法”。这是一种hack且容易出错。您确定生成的键是唯一的吗?在您的代码为某些尚未知道的要求进行维护后,它是否仍将保持唯一性?最好的方法是为键创建一个不可变值类。使用List和通用元组类是一种懒散的hack。

StringBuilder变量比concat方法真的低效很多吗? - Paŭlo Ebermann

3

在我的Windows和远程Linux机器上进行基准测试后,我发现以下concat3方法是最快的方式: 尽管我相信concat1的性能取决于JVM的实现和优化,并且在未来版本中可能表现更好。

    public class StringConcat {

    public static void main(String[] args) {
        int run = 100 * 100 * 1000;
        long startTime, total = 0;

        final String a = "a";
        final String b = "assdfsaf";
        final String c = "aasfasfsaf";
        final String d = "afafafdaa";
        final String e = "afdassadf";

        startTime = System.currentTimeMillis();
        concat1(run, a, b, c, d, e);
        total = System.currentTimeMillis() - startTime;
        System.out.println(total);

        startTime = System.currentTimeMillis();
        concat2(run, a, b, c, d, e);
        total = System.currentTimeMillis() - startTime;
        System.out.println(total);

        startTime = System.currentTimeMillis();
        concat3(run, a, b, c, d, e);
        total = System.currentTimeMillis() - startTime;
        System.out.println(total);
    }

    private static void concat3(int run, String a, String b, String c, String d, String e) {
        for (int i = 0; i < run; i++) {
            String str = new StringBuilder(a.length() + b.length() + c.length() + d.length() + e.length()).append(a)
                    .append(b).append(c).append(d).append(e).toString();
        }
    }

    private static void concat2(int run, String a, String b, String c, String d, String e) {
        for (int i = 0; i < run; i++) {
            String str = new StringBuilder(a).append(b).append(c).append(d).append(e).toString();
        }
    }

    private static void concat1(int run, String a, String b, String c, String d, String e) {
        for (int i = 0; i < run; i++) {
            String str = a + b + c + d + e;
        }
    }
}

你能提供一下你测试时所使用的JVM的详细信息吗? - Redandwhite
2
java版本号 "1.6.0_31" Java(TM) SE 运行环境 (构建 1.6.0_31-b05) Java HotSpot(TM) Client VM (构建 20.6-b01, 混合模式, 共享) - leoismyname

1
也许你应该创建一个Pair类,而不是使用字符串连接?
public class Pair<T1, T2> {
    private T1 first;
    private T2 second;

    public static <U1,U2> Pair<U1,U2> create(U1 first, U2 second) {
        return new Pair<U1,U2>(U1,U2);
    }

    public Pair( ) {}

    public Pair( T1 first, T2 second ) {
        this.first = first;
        this.second = second;
    }

    public T1 getFirst( ) {
        return first;
    }

    public void setFirst( T1 first ) {
        this.first = first;
    }

    public T2 getSecond( ) {
        return second;
    }

    public void setSecond( T2 second ) {
        this.second = second;
    }

    @Override
    public String toString( ) {
        return "Pair [first=" + first + ", second=" + second + "]";
    }

    @Override
    public int hashCode( ) {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((first == null)?0:first.hashCode());
        result = prime * result + ((second == null)?0:second.hashCode());
        return result;
    }

    @Override
    public boolean equals( Object obj ) {
        if ( this == obj )
            return true;
        if ( obj == null )
            return false;
        if ( getClass() != obj.getClass() )
            return false;
        Pair<?, ?> other = (Pair<?, ?>) obj;
        if ( first == null ) {
            if ( other.first != null )
                return false;
        }
        else if ( !first.equals(other.first) )
            return false;
        if ( second == null ) {
            if ( other.second != null )
                return false;
        }
        else if ( !second.equals(other.second) )
            return false;
        return true;
    }

}

将此作为您的HashMap键

使用HashMap<Pair<String,String>,Whatever>而不是HashMap<String,Whatever>

在紧密循环中,您将使用map.get(Pair.create(str1,str2))而不是map.get(str1 + str2)


1
@KitsuneYMG,您能否发布一个完整的工作示例,以便将来处理这些问题时方便使用。 - Deepak
@Deepak,请查看修改。如果您需要三元组、四元组等,使用此代码作为基础添加更多非常容易。 - KitsuneYMG
@KitsuneYMG,你能否发布你的pair类中的public static void main方法,以便于进一步参考。 - Deepak
我很想知道这样做是否实际上更快,因为它不缓存Pair的hashCode,而连接的字符串的hashCode是被缓存的。 - Duncan McGregor
@Duncan,你可以轻松地缓存哈希码并在set*后丢弃它。这应该比连接两个字符串更快,因为连接需要两个memcpy(除非特定的JVM使用绳索)。 - KitsuneYMG
旁注:Eclipse生成的哈希码/相等性不好(尤其是在视觉上),您可能需要将类序列化;哈希码缓存可能不需要(由于HashMap的工作方式)。此外,声明T1和T2为“String”将有助于编译器静态链接equals和hashcode(现在它可能会回退到内联缓存,虽然不错但仍然不够好)。 - bestsss

1

我建议尝试Thorbjørn Ravn Andersen的建议。

如果您需要根据两个部分的长度连接字符串,则创建具有所需大小的StringBuilder实例可能会稍微更好,以避免重新分配。默认的StringBuilder构造函数在当前实现中保留16个字符 - 至少在我的机器上是这样。因此,如果连接的字符串比初始缓冲区大小长,StringBuilder必须重新分配。

试试这个,并告诉我们您的分析器对此的看法:

StringBuilder ccyPair = new StringBuilder(ccy1.length()+ccy2.length());
ccyPair.append(ccy1); 
ccyPair.append(ccy2); 

1
根据Java规范自Java的第一个版本以来),在“字符串连接运算符+”一节中指出:

为了提高重复字符串连接的性能,Java编译器可以使用StringBuffer类或类似技术来减少通过表达式求值创建的中间String对象的数量。

因此,基本上对于变量,使用+运算符StringBuilder.append是基本相同的。


其他事情,我知道你在问题中提到只添加了2个字符串,但请注意,添加3个或更多的字符串会导致不同的结果:

我使用了稍微修改过的@Duncan McGregor示例。我有5种方法使用concat连接2到6个字符串,并且有5种方法使用StringBuilder连接2到6个字符串:

// Initialization
    private final String s1 = new String("1234567890");
    private final String s2 = new String("1234567890");
    private final String s3 = new String("1234567890");
    private final String s4 = new String("1234567890");
    private final String s5 = new String("1234567890");
    private final String s6 = new String("1234567890");

// testing the concat
    public void testConcatenation2stringsConcat(int count) {
        for (int i = 0; i < count; i++) {
            String s100 = s1.concat(s2);
        }
    }
    public void testConcatenation3stringsConcat(int count) {
        for (int i = 0; i < count; i++) {
            String s100 = s1.concat(s2).concat(s3);
        }
    }
    public void testConcatenation4stringsConcat(int count) {
        for (int i = 0; i < count; i++) {
            String s100 = s1.concat(s2).concat(s3).concat(s4);
        }
    }
    public void testConcatenation5stringsConcat(int count) {
        for (int i = 0; i < count; i++) {
            String s100 = s1.concat(s2).concat(s3).concat(s4).concat(s5);
        }
    }
    public void testConcatenation6stringsConcat(int count) {
        for (int i = 0; i < count; i++) {
            String s100 = s1.concat(s2).concat(s3).concat(s4).concat(s5).concat(s6);
        }
    }

//testing the StringBuilder
    public void testConcatenation2stringsSB(int count) {
        for (int i = 0; i < count; i++) {
            String s100 = new StringBuilder(s1).append(s2).toString();
        }
    }
    public void testConcatenation3stringsSB(int count) {
        for (int i = 0; i < count; i++) {
            String s100 = new StringBuilder(s1).append(s2).append(s3).toString();
        }
    }
    public void testConcatenation4stringsSB(int count) {
        for (int i = 0; i < count; i++) {
            String s100 = new StringBuilder(s1).append(s2).append(s3).append(s4).toString();
        }
    }
    public void testConcatenation5stringsSB(int count) {
        for (int i = 0; i < count; i++) {
            String s100 = new StringBuilder(s1).append(s2).append(s3).append(s4).append(s5).toString();
        }
    }
    public void testConcatenation6stringsSB(int count) {
        for (int i = 0; i < count; i++) {
            String s100 = new StringBuilder(s1).append(s2).append(s3).append(s4).append(s5).append(s6).toString();
        }
    }

我得到了以下结果(以秒为单位):
testConcatenation2stringsConcat:0.018 |||||||||||||||| testConcatenation2stringsSB:0.2 testConcatenation3stringsConcat:0.35 ||||||||||||||||||| testConcatenation3stringsSB:0.25 testConcatenation4stringsConcat:0.5 |||||||||||||||||||||| testConcatenation4stringsSB:0.3 testConcatenation5stringsConcat:0.67 ||||||||||||||||||| testConcatenation5stringsSB:0.38 testConcatenation5stringsConcat:0.9 |||||||||||||||||||||| testConcatenation5stringsSB:0.43
您可以看到,仅在连接两个字符串时,concat比StringBuilder更快。 请注意,随着添加更多字符串,使用StringBuilder的结果时间增加得更慢。 请注意,当字符串非常长时,差异将更加显着。

1

有趣的是,StringJoiner在这里没有被提到...

通常需要在字符串之间插入分隔符,例如", "
使用StringJoiner比使用StringBuilder更易于阅读,速度也相同。

StringJoiner joiner = new StringJoiner( ", " );
joiner.add( ccy1 ).add( ccy2 );

这个如何表现,我以前没有用过(相对于 StringBuilder 或简单的字符串连接呢?) - ggb667
@ggb667 当无缝连接两个字符串时,StringBuilder 明显比 StringJoiner("") 更快。一旦你想在字符串之间加入分隔符,速度就不会有明显的差异了。在后一种情况下,我更喜欢使用 StringJoiner 来提高可读性。当然,进行基准测试肯定是很有趣的。 - Kaplan

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