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

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

String ccyPair = ccy1 + ccy2; 

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

当我描述然后这是瓶颈

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

这些例程出现在基准测试中的原因是因为这就是编译器在封面下实现“+”的方式。

如果你真的需要连接字符串,你应该让编译器用“+”来实现它的魔力。 如果你需要的是一个用于映射查找的键,一个包含两个字符串并且具有合适的equals和hashMap实现的键类,可能是一个好主意,因为它避免了复制步骤。

很多理论 – 一些练习的时间!

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

在一台热身的64位热点上使用plain for 10,000,000循环,在Intel Mac OS上使用1.6.0_22。

例如

 @Test public void testConcatenation() { for (int i = 0; i < COUNT; i++) { String s3 = s1 + s2; } } 

在循环中使用以下语句

 String s3 = s1 + s2; 

1.33s

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

1.28秒

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

1.92s

 String s3 = s1.concat(s2); 

0.70s

 String s3 = "1234567890" + "1234567890"; 

0.0S

所以concat是明显的胜利者,除非你有静态字符串,在这种情况下编译器已经照顾好你了。

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

简短的回答,如果您正在寻找纯粹的连接,那就是: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(如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(); } 

对于标题中的问题: String.concat通常是连接两个String的最快方法(但请注意null )。 不涉及[超大]中间缓冲区或其他对象。 奇怪的是+被编译成涉及StringBuilder相对低效的代码。

但是,你的身体问题指向其他问题。 用于为地图生成键的字符串连接是常见的“反成语”。 这是一个黑客,容易出错。 您确定生成的密钥是唯一的吗? 在为某些尚未知的要求维护代码后,它是否仍然是唯一的? 最好的方法是为密钥创建一个不可变的值类。 使用List和通用元组类是一个草率的黑客。

对我来说,concat3方法如下是在我的Windows和远程linux机器上进行基准测试后最快的方法: – 虽然我相信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; } } } 

我建议尝试ThorbjørnRavnAndersens的建议。

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

试试这个并告诉我们你的探查者有什么说法:

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

或许不是连接,你应该创建一个Pair类?

 public class Pair { private T1 first; private T2 second; public static  Pair create(U1 first, U2 second) { return new Pair(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使用HashMap,Whatever>

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

也许您可以通过单独计算两个字符串的哈希值来解决问题,然后将它们组合起来,或许可以使用对整数起作用的单独哈希函数?

就像是:

 int h1 = ccy1.hashCode(), h2 = ccy2.hashCode(), h = h1 ^ h2; 

这可能会更快,因为连接字符串只是为了计算串联的散列似乎是浪费。

请注意,上面将两个哈希与二进制XOR( ^运算符)组合在一起,这通常有效,但您可能希望进一步调查。

好的,你的问题是什么? 无所事事:如果你必须连接字符串就行了。 您可以对代码进行分析。 现在您可以看到字符串连接运算符+自动使用StringBuilder的append()方法,因此使用

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

不会给你带来很大的好处。

优化代码的唯一严肃方法可能是更改设计以省略连接。 但只有当你真的需要它时才这样做,即连接占用CPU时间的很大一部分。

@Duncan McGregor的答案为一个特定的例子(输入字符串的大小)和一个JVM版本提供了一些基准数字。 在这种情况下,看起来像String.concat()是一个重要因素的赢家。 该结果可能会或可能不会推广。

旁白:这让我感到惊讶! 我原以为编译器编写者会选择在可能更快的情况下使用String.concat。 解释是在对此错误报告的评估中…并且根植于字符串连接运算符的定义。

(如果+类型的字符串操作数为null ,则JLS声明字符串"null"用于它的位置。如果将生成的s + s2编码为s.concat(s2)ss2 ,则s.concat(s2)碰巧是null ;你会得到NPE。而s == null的情况意味着concat的替代版本无法解决NPE问题。)


但是,@ unwind的答案给了我一个替代解决方案的想法,避免了对字符串连接的需要。

如果ccy1ccy2的连接只是为了连接两个键,那么通过定义一个带有两个键而不是一个键的特殊哈希表类,你可能会获得更好的性能。 它将有如下操作:

  public Object get(String key1, String key2) ... public void put(String key1, String key2, Object value) ... 

效果将类似于Map, Object> (请参阅@ KitsuneYMG的答案),除了每次要执行getput都不需要创建Pair对象。 缺点是:

  • 你必须从头开始实现一个新的哈希表类,并且
  • 新类不符合Map接口。

通常,我不建议这样做。 但是,如果字符串连接和映射查找确实是一个关键瓶颈,则自定义多键哈希表可能会为您带来显着的加速。

这是一个带有双键,单值的线性探针映射的完整实现。 它也应该优于java.util.HashMap。

警告,它是从头开始写的,所以它可能包含错误。 请随时编辑它。

解决方案必须击败任何包装器,随时连接。 get / put上的no分配也使它成为快速通用映射。

希望这能解决问题。 (代码带有一些不需要的简单测试)


 package bestsss.util; @SuppressWarnings("unchecked") public class DoubleKeyMap { private static final int MAX_CAPACITY = 1<<29; private static final Object TOMBSTONE = new String("TOMBSTONE"); Object[] kvs; int[] hashes; int count = 0; final int rehashOnProbes; public DoubleKeyMap(){ this(8, 5); } public DoubleKeyMap(int capacity, int rehashOnProbes){ capacity = nextCapacity(Math.max(2, capacity-1)); if (rehashOnProbes>capacity){ throw new IllegalArgumentException("rehashOnProbes too high"); } hashes = new int[capacity]; kvs = new Object[kvsIndex(capacity)]; count = 0; this.rehashOnProbes = rehashOnProbes; } private static int nextCapacity(int c) { int n = Integer.highestOneBit(c)<<1; if (n<0 || n>MAX_CAPACITY){ throw new Error("map too large"); } return n; } //alternatively this method can become non-static, protected and overriden, the perfoamnce can drop a little //but if better spread of the lowest bit is possible, all good and proper private static int hash(K1 key1, K2 key2){ //spread more, if need be int h1 = key1.hashCode(); int h2 = key2.hashCode(); return h1+ (h2<<4) + h2; //h1+h2*17 } private static int kvsIndex(int baseIdx){ int idx = baseIdx; idx+=idx<<1;//idx*3 return idx; } private int baseIdx(int hash){ return hash & (hashes.length-1); } public V get(K1 key1, K2 key2){ final int hash = hash(key1, key2); final int[] hashes = this.hashes; final Object[] kvs = this.kvs; final int mask = hashes.length-1; for(int base = baseIdx(hash);;base=(base+1)&mask){ int k = kvsIndex(base); K1 k1 = (K1) kvs[k]; if (k1==null) return null;//null met; no such value Object value; if (hashes[base]!=hash || TOMBSTONE==(value=kvs[k+2])) continue;//next K2 k2 = (K2) kvs[k+1]; if ( (key1==k1 || key1.equals(k1)) && (key2==k2 || key2.equals(k2)) ){ return (V) value; } } } public boolean contains(K1 key1, K2 key2){ return get(key1, key2)!=null; } public boolean containsValue(final V value){ final Object[] kvs = this.kvs; if (value==null) return false; for(int i=0;i remove int probes = 0; final int[] hashes = this.hashes; final Object[] kvs = this.kvs; final int mask = hashes.length-1; //conservative resize: when too many probes and the count is greater than the half of the capacity for(int base = baseIdx(hash);probes>1);base=(base+1)&mask, probes++){ final int k = kvsIndex(base); K1 k1 = (K1) kvs[k]; K2 k2; //find a gap, or resize Object old = kvs[k+2]; final boolean emptySlot = k1==null || (value!=null && old==TOMBSTONE); if (emptySlot || ( hashes[base] == hash && (k1==key1 || k1.equals(key1)) && ((k2=(K2) kvs[k+1])==key2 || k2.equals(key2))) ){ if (value==null){//remove() if (emptySlot) return null;//not found, and no value ->nothing to do value = TOMBSTONE; count-=2;//offset the ++later } if (emptySlot){//new entry, update keys hashes[base] = hash; kvs[k] = key1; kvs[k+1] = key2; }//else -> keys and hash are equal if (old==TOMBSTONE) old=null; kvs[k+2] = value; count++; return (V) old; } } resize(); return doPut(key1, key2, value, hash);//hack w/ recursion, after the resize } //optimized version during resize, doesn't check equals which is the slowest part protected void doPutForResize(K1 key1, K2 key2, V value, final int hash){ final int[] hashes = this.hashes; final Object[] kvs = this.kvs; final int mask = hashes.length-1; //find the 1st gap and insert there for(int base = baseIdx(hash);;base=(base+1)&mask){//it's ensured, no equal keys exist, so skip equals part final int k = kvsIndex(base); K1 k1 = (K1) kvs[k]; if (k1!=null) continue; hashes[base] = hash; kvs[k] = key1; kvs[k+1] = key2; kvs[k+2] = value; return; } } //resizes the map by doubling the capacity, //the method uses altervative varian of put that doesn't check equality, or probes; just inserts at a gap protected void resize(){ final int[] hashes = this.hashes; final Object[] kvs = this.kvs; final int capacity = nextCapacity(hashes.length); this.hashes = new int[capacity]; this.kvs = new Object[kvsIndex(capacity)]; for (int i=0;i map = new DoubleKeyMap(4,2); map.put("eur/usd", "usd/jpy", 1); map.put("eur/usd", "usd/jpy", 2); map.put("eur/jpy", "usd/jpy", 3); System.out.println(map.get("eur/jpy", "usd/jpy")); System.out.println(map.get("eur/usd", "usd/jpy")); System.out.println("======"); map.remove("eur/usd", "usd/jpy"); System.out.println(map.get("eur/jpy", "usd/jpy")); System.out.println(map.get("eur/usd", "usd/jpy")); System.out.println("======"); testResize(); } static void testResize(){ DoubleKeyMap map = new DoubleKeyMap(18, 17); long s = 0; String pref="xxx"; for (int i=0;i<14000;i++){ map.put(pref+i, i, i); if ((i&1)==1) map.remove(pref+i, i); else s+=i; } System.out.println("sum: "+s); long sum = 0; for (int i=0;i<14000;i++){ Integer n = map.get(pref+i, i); if (n!=null && n!=i){ throw new AssertionError(); } if (n!=null){ System.out.println(n); sum+=n; } } System.out.println("1st sum: "+s); System.out.println("2nd sum: "+sum); } } 
 StringBuffer ccyPair = new StringBuffer(); ccyPair.append("ccy1").append("ccy2"); 

您是否尝试过使用字符串缓冲区,然后使用分析器来检查瓶颈在哪里。试一试,看看会发生什么。