1. Introduction
1.介绍
In this tutorial, we’re going to focus on the performance aspect of the Java String API.
在本教程中,我们将重点讨论Java字符串API的性能问题。
We’ll dig into String creation, conversion and modification operations to analyze the available options and compare their efficiency.
我们将深入研究String的创建、转换和修改操作,分析可用的选项并比较其效率。
The suggestions we’re going to make won’t be necessarily the right fit for every application. But certainly, we’re going to show how to win on performance when the application running time is critical.
我们将提出的建议不一定适合每个应用程序。但可以肯定的是,当应用程序的运行时间很关键时,我们将展示如何在性能上获胜。
2. Constructing a New String
2.构造一个新的字符串
As you know, in Java, Strings are immutable. So every time we construct or concatenate a String object, Java creates a new String – this might be especially costly if done in a loop.
正如你所知,在Java中,字符串是不可变的。因此,每当我们构造或连接一个String对象时,Java就会创建一个新的String – 如果在一个循环中进行,这可能会特别昂贵。
2.1. Using Constructor
2.1.使用构造函数
In most cases, we should avoid creating Strings using the constructor unless we know what are we doing.
在大多数情况下,我们应该避免使用构造函数创建字符串,除非我们知道我们在做什么。
Let’s create a newString object inside of the loop first, using the new String() constructor, then the = operator.
让我们先在循环中创建一个newString对象,使用new String()构造函数,然后使用=操作符。
To write our benchmark, we’ll use the JMH (Java Microbenchmark Harness) tool.
为了编写我们的基准,我们将使用JMH(Java Microbenchmark Harness)工具。
Our configuration:
我们的配置。
@BenchmarkMode(Mode.SingleShotTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Measurement(batchSize = 10000, iterations = 10)
@Warmup(batchSize = 10000, iterations = 10)
public class StringPerformance {
}
Here, we’re using the SingeShotTime mode, which runs the method only once. As we want to measure the performance of String operations inside of the loop, there’s a @Measurement annotation available for that.
这里,我们使用的是SingeShotTime模式,该模式只运行一次方法。由于我们想测量循环内String操作的性能,因此有一个@Measurement注解可用于此。
Important to know, that benchmarking loops directly in our tests may skew the results because of various optimizations applied by JVM.
重要的是要知道,在我们的测试中直接对循环进行基准测试可能会歪曲结果,因为JVM应用了各种优化措施。
So we calculate only the single operation and let JMH take care for the looping. Briefly speaking, JMH performs the iterations by using the batchSize parameter.
所以我们只计算单次操作,让JMH来处理循环操作。简而言之,JMH通过使用batchSize参数来执行迭代。
Now, let’s add the first micro-benchmark:
现在,让我们添加第一个微观基准。
@Benchmark
public String benchmarkStringConstructor() {
return new String("baeldung");
}
@Benchmark
public String benchmarkStringLiteral() {
return "baeldung";
}
In the first test, a new object is created in every iteration. In the second test, the object is created only once. For remaining iterations, the same object is returned from the String’s constant pool.
在第一个测试中,每次迭代都会创建一个新的对象。在第二个测试中,对象只被创建一次。在其余的迭代中,同一个对象从String的常量池中返回。
Let’s run the tests with the looping iterations count = 1,000,000 and see the results:
让我们用循环迭代计数=1,000,000运行测试,看看结果。
Benchmark Mode Cnt Score Error Units
benchmarkStringConstructor ss 10 16.089 ± 3.355 ms/op
benchmarkStringLiteral ss 10 9.523 ± 3.331 ms/op
From the Score values, we can clearly see that the difference is significant.
从Score值,我们可以清楚地看到,差异是显著的。
2.2. + Operator
2.2.+ 操作员
Let’s have a look at dynamic String concatenation example:
让我们看一下动态String连接的例子。
@State(Scope.Thread)
public static class StringPerformanceHints {
String result = "";
String baeldung = "baeldung";
}
@Benchmark
public String benchmarkStringDynamicConcat() {
return result + baeldung;
}
In our results, we want to see the average execution time. The output number format is set to milliseconds:
在我们的结果中,我们想看到的是平均执行时间。输出的数字格式被设置为毫秒。
Benchmark 1000 10,000
benchmarkStringDynamicConcat 47.331 4370.411
Now, let’s analyze the results. As we see, adding 1000 items to state.result takes 47.331 milliseconds. Consequently, increasing the number of iterations in 10 times, the running time grows to 4370.441 milliseconds.
现在,让我们分析一下结果。正如我们所看到的,向state.result添加1000项需要47.331毫秒。因此,增加10次迭代的数量,运行时间增长到4370.441毫秒。
In summary, the time of execution grows quadratically. Therefore, the complexity of dynamic concatenation in a loop of n iterations is O(n^2).
综上所述,执行的时间呈二次增长。因此,在一个有n次迭代的循环中,动态连接的复杂性是O(n^2)。
2.3. String.concat()
2.3.String.concat()
One more way to concatenate Strings is by using the concat() method:
还有一种连接字符串的方式是使用concat()方法。
@Benchmark
public String benchmarkStringConcat() {
return result.concat(baeldung);
}
Output time unit is a millisecond, iterations count is 100,000. The result table looks like:
输出时间单位为毫秒,迭代次数为100,000次。结果表看起来像。
Benchmark Mode Cnt Score Error Units
benchmarkStringConcat ss 10 3403.146 ± 852.520 ms/op
2.4. String.format()
2.4.String.format()
Another way to create strings is by using String.format() method. Under the hood, it uses regular expressions to parse the input.
另一种创建字符串的方法是使用String.format()方法。在引擎盖下,它使用正则表达式来解析输入。
Let’s write the JMH test case:
我们来写一下JMH的测试案例。
String formatString = "hello %s, nice to meet you";
@Benchmark
public String benchmarkStringFormat_s() {
return String.format(formatString, baeldung);
}
After, we run it and see the results:
之后,我们运行它并看到结果。
Number of Iterations 10,000 100,000 1,000,000
benchmarkStringFormat_s 17.181 140.456 1636.279 ms/op
Although the code with String.format() looks more clean and readable, we don’t win here in term of performance.
尽管使用String.format()的代码看起来更干净、更易读,但在性能方面我们并没有获胜。
2.5. StringBuilder and StringBuffer
2.5.StringBuilder和StringBuffer
We already have a write-up explaining StringBuffer and StringBuilder. So here, we’ll show only extra information about their performance. StringBuilder uses a resizable array and an index that indicates the position of the last cell used in the array. When the array is full, it expands double of its size and copies all the characters into the new array.
我们已经有一篇写作解释了StringBuffer和StringBuilder。所以在这里,我们只展示关于它们性能的额外信息。StringBuilder使用一个可调整大小的数组和一个索引,该索引指示数组中最后使用的单元的位置。当数组满了之后,它将其大小扩大一倍,并将所有的字符复制到新的数组中。
Taking into account that resizing doesn’t occur very often, we can consider each append() operation as O(1) constant time. Taking this into account, the whole process has O(n) complexity.
考虑到调整大小并不经常发生,我们可以将每个append()操作视为O(1)恒定时间。考虑到这一点,整个过程具有O(n)的复杂性。
After modifying and running the dynamic concatenation test for StringBuffer and StringBuilder, we get:
修改并运行StringBuffer和StringBuilder的动态连接测试后,我们得到。
Benchmark Mode Cnt Score Error Units
benchmarkStringBuffer ss 10 1.409 ± 1.665 ms/op
benchmarkStringBuilder ss 10 1.200 ± 0.648 ms/op
Although the score difference isn’t much, we can notice that StringBuilder works faster.
虽然分数差距不大,但我们可以注意到StringBuilder工作得更快。
Fortunately, in simple cases, we don’t need StringBuilder to put one String with another. Sometimes, static concatenation with + can actually replace StringBuilder. Under the hood, the latest Java compilers will call the StringBuilder.append() to concatenate strings.
幸运的是,在简单的情况下,我们不需要StringBuilder来把一个String和另一个String放在一起。有时,用+进行静态连接实际上可以取代StringBuilder。在引擎盖下,最新的Java编译器会调用StringBuilder.append()来连接字符串。
This means winning in performance significantly.
这意味着在性能上明显获胜。
3. Utility Operations
3.公用事业运营
3.1. StringUtils.replace() vs String.replace()
3.1.StringUtils.replace()与String.replace()
Interesting to know, that Apache Commons version for replacing the String does way better than the String’s own replace() method. The answer to this difference lays under their implementation. String.replace() uses a regex pattern to match the String.
有趣的是,Apache Commons版本用于替换String,比String自己的replace()方法要好得多。这种差异的答案就在他们的实现之下。String.replace()使用一个regex模式来匹配String.。
In contrast, StringUtils.replace() is widely using indexOf(), which is faster.
相比之下,StringUtils.replace()广泛使用indexOf(),这更快。
Now, it’s time for the benchmark tests:
现在,是进行基准测试的时候了。
@Benchmark
public String benchmarkStringReplace() {
return longString.replace("average", " average !!!");
}
@Benchmark
public String benchmarkStringUtilsReplace() {
return StringUtils.replace(longString, "average", " average !!!");
}
Setting the batchSize to 100,000, we present the results:
将batchSize设置为100,000,我们提出了结果。
Benchmark Mode Cnt Score Error Units
benchmarkStringReplace ss 10 6.233 ± 2.922 ms/op
benchmarkStringUtilsReplace ss 10 5.355 ± 2.497 ms/op
Although the difference between the numbers isn’t too big, the StringUtils.replace() has a better score. Of course, the numbers and the gap between them may vary depending on parameters like iterations count, string length and even JDK version.
虽然数字之间的差距不是很大,但StringUtils.replace()的得分更高。当然,这些数字和它们之间的差距可能会因迭代次数、字符串长度甚至JDK版本等参数而有所不同。
With the latest JDK 9+ (our tests are running on JDK 10) versions both implementations have fairly equal results. Now, let’s downgrade the JDK version to 8 and the tests again:
在最新的JDK 9+(我们的测试是在JDK 10上运行的)版本下,两种实现的结果相当。现在,让我们把JDK版本降级到8,再进行测试。
Benchmark Mode Cnt Score Error Units
benchmarkStringReplace ss 10 48.061 ± 17.157 ms/op
benchmarkStringUtilsReplace ss 10 14.478 ± 5.752 ms/op
The performance difference is huge now and confirms the theory which we discussed in the beginning.
现在的性能差异是巨大的,证实了我们在开始时讨论的理论。
3.2. split()
3.2.split()
Before we start, it’ll be useful to check out string splitting methods available in Java.
在我们开始之前,检查一下Java中可用的字符串拆分方法会很有用。
When there is a need to split a string with the delimiter, the first function that comes to our mind usually is String.split(regex). However, it brings some serious performance issues, as it accepts a regex argument. Alternatively, we can use the StringTokenizer class to break the string into tokens.
当需要用分隔符来分割一个字符串时,我们想到的第一个函数通常是String.split(regex)。然而,它带来了一些严重的性能问题,因为它接受了一个regex参数。另外,我们可以使用StringTokenizer类来将字符串分解为标记。
Another option is Guava’s Splitter API. Finally, the good old indexOf() is also available to boost our application’s performance if we don’t need the functionality of regular expressions.
另一个选择是Guava的Splitter API。最后,如果我们不需要正则表达式的功能,也可以使用古老的indexOf()来提高我们应用程序的性能。
Now, it’s time to write the benchmark tests for String.split() option:
现在,是时候为String.split()选项编写基准测试了。
String emptyString = " ";
@Benchmark
public String [] benchmarkStringSplit() {
return longString.split(emptyString);
}
Pattern.split() :
Pattern.split() :
@Benchmark
public String [] benchmarkStringSplitPattern() {
return spacePattern.split(longString, 0);
}
StringTokenizer :
StringTokenizer 。
List stringTokenizer = new ArrayList<>();
@Benchmark
public List benchmarkStringTokenizer() {
StringTokenizer st = new StringTokenizer(longString);
while (st.hasMoreTokens()) {
stringTokenizer.add(st.nextToken());
}
return stringTokenizer;
}
String.indexOf() :
String.indexOf() :
List stringSplit = new ArrayList<>();
@Benchmark
public List benchmarkStringIndexOf() {
int pos = 0, end;
while ((end = longString.indexOf(' ', pos)) >= 0) {
stringSplit.add(longString.substring(pos, end));
pos = end + 1;
}
stringSplit.add(longString.substring(pos));
return stringSplit;
}
Guava’s Splitter :
Guava的Splitter :
@Benchmark
public List<String> benchmarkGuavaSplitter() {
return Splitter.on(" ").trimResults()
.omitEmptyStrings()
.splitToList(longString);
}
Finally, we run and compare results for batchSize = 100,000:
最后,我们运行并比较batchSize = 100,000的结果。
Benchmark Mode Cnt Score Error Units
benchmarkGuavaSplitter ss 10 4.008 ± 1.836 ms/op
benchmarkStringIndexOf ss 10 1.144 ± 0.322 ms/op
benchmarkStringSplit ss 10 1.983 ± 1.075 ms/op
benchmarkStringSplitPattern ss 10 14.891 ± 5.678 ms/op
benchmarkStringTokenizer ss 10 2.277 ± 0.448 ms/op
As we see, the worst performance has the benchmarkStringSplitPattern method, where we use the Pattern class. As a result, we can learn that using a regex class with the split() method may cause performance loss in multiple times.
正如我们所看到的,性能最差的是benchmarkStringSplitPattern方法,其中我们使用Pattern类。因此,我们可以了解到,使用带有split()方法的regex类可能会导致多次的性能损失。
Likewise, we notice that the fastest results are providing examples with the use of indexOf() and split().
同样,我们注意到,最快的结果是提供使用indexOf()和split()的例子。。
3.3. Converting to String
3.3.转换为字符串
In this section, we’re going to measure the runtime scores of string conversion. To be more specific, we’ll examine Integer.toString() concatenation method:
在本节中,我们将测量字符串转换的运行时间分数。更具体地说,我们将考察Integer.toString()连接方法。
int sampleNumber = 100;
@Benchmark
public String benchmarkIntegerToString() {
return Integer.toString(sampleNumber);
}
String.valueOf() :
String.valueOf() :
@Benchmark
public String benchmarkStringValueOf() {
return String.valueOf(sampleNumber);
}
[some integer value] + “” :
[一些整数值] + “” 。
@Benchmark
public String benchmarkStringConvertPlus() {
return sampleNumber + "";
}
String.format() :
String.format() :
String formatDigit = "%d";
@Benchmark
public String benchmarkStringFormat_d() {
return String.format(formatDigit, sampleNumber);
}
After running the tests, we’ll see the output for batchSize = 10,000:
运行测试后,我们会看到batchSize = 10,000的输出。
Benchmark Mode Cnt Score Error Units
benchmarkIntegerToString ss 10 0.953 ± 0.707 ms/op
benchmarkStringConvertPlus ss 10 1.464 ± 1.670 ms/op
benchmarkStringFormat_d ss 10 15.656 ± 8.896 ms/op
benchmarkStringValueOf ss 10 2.847 ± 11.153 ms/op
After analyzing the results, we see that the test for Integer.toString() has the best score of 0.953 milliseconds. In contrast, a conversion which involves String.format(“%d”) has the worst performance.
分析结果后,我们看到对Integer.toString()的测试有最好的分数0.953毫秒。相比之下,涉及String.format(“%d”)的转换性能最差。
That’s logical because parsing the format String is an expensive operation.
这是合乎逻辑的,因为解析格式String是一个昂贵的操作。
3.4. Comparing Strings
3.4.比较字符串
Let’s evaluate different ways of comparing Strings. The iterations count is 100,000.
让我们评估一下比较字符串的不同方法。 迭代次数为100,000。
Here are our benchmark tests for the String.equals() operation:
下面是我们对String.equals()操作的基准测试。
@Benchmark
public boolean benchmarkStringEquals() {
return longString.equals(baeldung);
}
String.equalsIgnoreCase() :
String.equalsIgnoreCase() :
@Benchmark
public boolean benchmarkStringEqualsIgnoreCase() {
return longString.equalsIgnoreCase(baeldung);
}
String.matches() :
String.matches() :
@Benchmark
public boolean benchmarkStringMatches() {
return longString.matches(baeldung);
}
String.compareTo() :
String.compareTo() :
@Benchmark
public int benchmarkStringCompareTo() {
return longString.compareTo(baeldung);
}
After, we run the tests and display the results:
之后,我们运行测试并显示结果。
Benchmark Mode Cnt Score Error Units
benchmarkStringCompareTo ss 10 2.561 ± 0.899 ms/op
benchmarkStringEquals ss 10 1.712 ± 0.839 ms/op
benchmarkStringEqualsIgnoreCase ss 10 2.081 ± 1.221 ms/op
benchmarkStringMatches ss 10 118.364 ± 43.203 ms/op
As always, the numbers speak for themselves. The matches() takes the longest time as it uses the regex to compare the equality.
像往常一样,数字本身就说明了问题。matches()花费的时间最长,因为它使用了regex来比较平等。
In contrast, the equals() and equalsIgnoreCase() are the best choices.
相比之下,equals()和equalsIgnoreCase()是最佳选择。
3.5. String.matches() vs Precompiled Pattern
3.5.String.matches() vs Precompiled Pattern
Now, let’s have a separate look at String.matches() and Matcher.matches() patterns. The first one takes a regexp as an argument and compiles it before executing.
现在,让我们分别看看String.matches()和Matcher.matches()模式。第一个模式需要一个regexp作为参数,并在执行前对其进行编译。
So every time we call String.matches(), it compiles the Pattern:
因此,每次我们调用String.matches()时,都会编译出Pattern:。
@Benchmark
public boolean benchmarkStringMatches() {
return longString.matches(baeldung);
}
The second method reuses the Pattern object:
第二个方法是重复使用Pattern对象。
Pattern longPattern = Pattern.compile(longString);
@Benchmark
public boolean benchmarkPrecompiledMatches() {
return longPattern.matcher(baeldung).matches();
}
And now the results:
现在,结果出来了。
Benchmark Mode Cnt Score Error Units
benchmarkPrecompiledMatches ss 10 29.594 ± 12.784 ms/op
benchmarkStringMatches ss 10 106.821 ± 46.963 ms/op
As we see, matching with precompiled regexp works about three times faster.
正如我们所看到的,用预编译的regexp进行匹配的速度大约是三倍。
3.6. Checking the Length
3.6.检查长度
Finally, let’s compare the String.isEmpty() method:
最后,让我们比较一下String.isEmpty()方法。
@Benchmark
public boolean benchmarkStringIsEmpty() {
return longString.isEmpty();
}
and the String.length() method:
和String.length()方法。
@Benchmark
public boolean benchmarkStringLengthZero() {
return emptyString.length() == 0;
}
First, we call them over the longString = “Hello baeldung, I am a bit longer than other Strings in average” String. The batchSize is 10,000:
首先,我们在longString = “Hello baeldung, I am a bit longer than other Strings in average” String. 上调用它们,batchSize是10,000。
Benchmark Mode Cnt Score Error Units
benchmarkStringIsEmpty ss 10 0.295 ± 0.277 ms/op
benchmarkStringLengthZero ss 10 0.472 ± 0.840 ms/op
After, let’s set the longString = “” empty string and run the tests again:
之后,让我们设置longString = “”空字符串并再次运行测试。
Benchmark Mode Cnt Score Error Units
benchmarkStringIsEmpty ss 10 0.245 ± 0.362 ms/op
benchmarkStringLengthZero ss 10 0.351 ± 0.473 ms/op
As we notice, benchmarkStringLengthZero() and benchmarkStringIsEmpty() methods in both cases have approximately the same score. However, calling isEmpty() works faster than checking if the string’s length is zero.
我们注意到,benchmarkStringLengthZero()和benchmarkStringIsEmpty()方法在两种情况下的得分大致相同。然而,调用isEmpty()比检查字符串的长度是否为零效果更快。
4. String Deduplication
4.字符串重复数据删除
Since JDK 8, string deduplication feature is available to eliminate memory consumption. Simply put, this tool is looking for the strings with the same or duplicate contents to store one copy of each distinct string value into the String pool.
从JDK 8开始,可以使用重复数据删除功能来消除内存消耗。简单地说,该工具正在寻找内容相同或重复的字符串,将每个不同的字符串值的一个副本存储到字符串池中。
Currently, there are two ways to handle String duplicates:
目前,有两种方法来处理String重复。
- using the String.intern() manually
- enabling string deduplication
Let’s have a closer look at each option.
让我们仔细看看每个选项。
4.1. String.intern()
4.1.String.intern()
Before jumping ahead, it will be useful to read about manual interning in our write-up. With String.intern() we can manually set the reference of the String object inside of the global String pool.
在跳转之前,在我们的写作中阅读一下手动插值的内容将是非常有用的。通过String.intern()我们可以手动设置全局String池内的String对象的引用。
Then, JVM can use return the reference when needed. From the point of view of performance, our application can hugely benefit by reusing the string references from the constant pool.
然后,JVM可以在需要时使用返回引用。从性能的角度来看,我们的应用程序可以通过重复使用常量池中的字符串引用而大大受益。
Important to know, that JVM String pool isn’t local for the thread. Each String that we add to the pool, is available to other threads as well.
重要的是要知道,JVM的String池并不是线程的本地。我们添加到池中的每个String,对其他线程也是可用的。
However, there are serious disadvantages as well:
然而,也有严重的弊端。
- to maintain our application properly, we may need to set a -XX:StringTableSize JVM parameter to increase the pool size. JVM needs a restart to expand the pool size
- calling String.intern() manually is time-consuming. It grows in a linear time algorithm with O(n) complexity
- additionally, frequent calls on long String objects may cause memory problems
To have some proven numbers, let’s run a benchmark test:
为了有一些经过验证的数字,让我们进行一次基准测试。
@Benchmark
public String benchmarkStringIntern() {
return baeldung.intern();
}
Additionally, the output scores are in milliseconds:
此外,输出分数的单位是毫秒。
Benchmark 1000 10,000 100,000 1,000,000
benchmarkStringIntern 0.433 2.243 19.996 204.373
The column headers here represent a different iterations counts from 1000 to 1,000,000. For each iteration number, we have the test performance score. As we notice, the score increases dramatically in addition to the number of iterations.
这里的列头代表不同的迭代数,从1000到1,000,000。对于每个迭代数,我们有测试性能得分。正如我们注意到的,分数随着迭代次数的增加而急剧增加。
4.2. Enable Deduplication Automatically
4.2.自动启用重复数据删除功能
First of all, this option is a part of the G1 garbage collector. By default, this feature is disabled. So we need to enable it with the following command:
首先,这个选项是G1垃圾收集器的一部分。默认情况下,这个功能是禁用的。所以我们需要用以下命令来启用它。
-XX:+UseG1GC -XX:+UseStringDeduplication
Important to note, that enabling this option doesn’t guarantee that String deduplication will happen. Also, it doesn’t process young Strings. In order to manage the minimal age of processing Strings, XX:StringDeduplicationAgeThreshold=3 JVM option is available. Here, 3 is the default parameter.
需要注意的是,启用这个选项并不能保证字符串重复数据删除会发生。而且,它不会处理年轻的字符串。为了管理处理字符串的最小年龄,XX:StringDeduplicationAgeThreshold=3 JVM选项可用。这里,3是默认参数。
5. Summary
5.总结
In this tutorial, we’re trying to give some hints to use strings more efficiently in our daily coding life.
在本教程中,我们试图给出一些提示,以便在我们的日常编码生活中更有效地使用字符串。
As a result, we can highlight some suggestions in order to boost our application performance:
因此,我们可以强调一些建议,以提高我们的应用性能。
- when concatenating strings, the StringBuilder is the most convenient option that comes to mind. However, with the small strings, the + operation has almost the same performance. Under the hood, the Java compiler may use the StringBuilder class to reduce the number of string objects
- to convert the value into the string, the [some type].toString() (Integer.toString() for example) works faster then String.valueOf(). Because that difference isn’t significant, we can freely use String.valueOf() to not have a dependency on the input value type
- when it comes to string comparison, nothing beats the String.equals() so far
- String deduplication improves performance in large, multi-threaded applications. But overusing String.intern() may cause serious memory leaks, slowing down the application
- for splitting the strings we should use indexOf() to win in performance. However, in some noncritical cases String.split() function might be a good fit
- Using Pattern.match() the string improves performance significantly
- String.isEmpty() is faster than String.length() ==0
Also, keep in mind that the numbers we present here are just JMH benchmark results – so you should always test in the scope of your own system and runtime to determine the impact of these kinds of optimizations.
此外,请记住,我们在这里提出的数字只是JMH的基准结果 – 所以你应该始终在自己的系统和运行时间范围内进行测试,以确定这些类型的优化的影响。
Finally, as always, the code used during the discussion can be found over on GitHub.
最后,像往常一样,讨论中使用的代码可以在GitHub上找到。