[javascript学习指南]Java字符串操作、基本运算方法优化策略 改善文档的理由、建议和技巧

更新时间:2019-12-25    来源:js教程    手机版     字体:

【www.bbyears.com--js教程】

Java 程序优化:字符串操作、基本运算方法等优化策略

字符串操作优化

字符串对象

字符串对象或者其等价对象 (如 char 数组),在内存中总是占据最大的空间块,因此如何高效地处理字符串,是提高系统整体性能的关键。

String 对象可以认为是 char 数组的延伸和进一步封装,它主要由 3 部分组成:char 数组、偏移量和 String 的长度。char 数组表示 String 的内容,它是 String 对象所表示字符串的超集。String 的真实内容还需要由偏移量和长度在这个 char 数组中进行定位和截取。

String 有 3 个基本特点:

1. 不变性;
2. 针对常量池的优化;
3. 类的 final 定义。

不变性指的是 String 对象一旦生成,则不能再对它进行改变。String 的这个特性可以泛化成不变 (immutable) 模式,即一个对象的状态在对象被创建之后就不再发生变化。不变模式的主要作用在于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅提高系统性能。

针对常量池的优化指的是当两个 String 对象拥有相同的值时,它们只引用常量池中的同一个拷贝,当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。

下面代码 str1、str2、str4 引用了相同的地址,但是 str3 却重新开辟了一块内存空间,虽然 str3 单独占用了堆空间,但是它所指向的实体和 str1 完全一样。代码如下清单 1 所示。

清单 1. 示例代码

public class StringDemo {
 public static void main(String[] args){
 String str1 = "abc";
 String str2 = "abc";
 String str3 = new String("abc");
 String str4 = str1;
 System.out.println("is str1 = str2?"+(str1==str2));
 System.out.println("is str1 = str3?"+(str1==str3));
 System.out.println("is str1 refer to str3?"+(str1.intern()==str3.intern()));
 System.out.println("is str1 = str4"+(str1==str4));
 System.out.println("is str2 = str4"+(str2==str4));
 System.out.println("is str4 refer to str3?"+(str4.intern()==str3.intern()));
 }
}


输出如清单 2 所示。

清单 2. 输出结果

is str1 = str2?true
is str1 = str3?false
is str1 refer to str3?true
is str1 = str4true
is str2 = str4true
is str4 refer to str3?true

SubString 使用技巧

String 的 substring 方法源码在最后一行新建了一个 String 对象,new String(offset+beginIndex,endIndex-beginIndex,value);该行代码的目的是为了能高效且快速地共享 String 内的 char 数组对象。但在这种通过偏移量来截取字符串的方法中,String 的原生内容 value 数组被复制到新的子字符串中。设想,如果原始字符串很大,截取的字符长度却很短,那么截取的子字符串中包含了原生字符串的所有内容,并占据了相应的内存空间,而仅仅通过偏移量和长度来决定自己的实际取值。这种算法提高了速度却浪费了空间。

下面代码演示了使用 substring 方法在一个很大的 string 独享里面截取一段很小的字符串,如果采用 string 的 substring 方法会造成内存溢出,如果采用反复创建新的 string 方法可以确保正常运行。

清单 3.substring 方法演示

import java.util.ArrayList;
import java.util.List;
 
public class StringDemo {
 public static void main(String[] args){
 List handler = new ArrayList();
 for(int i=0;i<1000;i++){
 HugeStr h = new HugeStr();
 ImprovedHugeStr h1 = new ImprovedHugeStr();
 handler.add(h.getSubString(1, 5));
 handler.add(h1.getSubString(1, 5));
 }
 }
 
 static class HugeStr{
 private String str = new String(new char[800000]);
 public String getSubString(int begin,int end){
 return str.substring(begin, end);
 }
 }
 
 static class ImprovedHugeStr{
 private String str = new String(new char[10000000]);
 public String getSubString(int begin,int end){
 return new String(str.substring(begin, end));
 }
 }
}


输出结果如清单 4 所示。

清单 4. 输出结果

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)
at java.lang.StringValue.from(Unknown Source)
at java.lang.String.(Unknown Source)
at StringDemo$ImprovedHugeStr.(StringDemo.java:23)
at StringDemo.main(StringDemo.java:9)

ImprovedHugeStr 可以工作是因为它使用没有内存泄漏的 String 构造函数重新生成了 String 对象,使得由 substring() 方法返回的、存在内存泄漏问题的 String 对象失去所有的强引用,从而被垃圾回收器识别为垃圾对象进行回收,保证了系统内存的稳定。

String 的 split 方法支持传入正则表达式帮助处理字符串,但是简单的字符串分割时性能较差。

对比 split 方法和 StringTokenizer 类的处理字符串性能,代码如清单 5 所示。

切分字符串方式讨论

String 的 split 方法支持传入正则表达式帮助处理字符串,操作较为简单,但是缺点是它所依赖的算法在对简单的字符串分割时性能较差。清单 5 所示代码对比了 String 的 split 方法和调用 StringTokenizer 类来处理字符串时性能的差距。

清单 5.String 的 split 方法演示

import java.util.StringTokenizer;
 
public class splitandstringtokenizer {
 public static void main(String[] args){
 String orgStr = null;
 StringBuffer sb = new StringBuffer();
 for(int i=0;i<100000;i++){
 sb.append(i);
 sb.append(",");
 }
 orgStr = sb.toString();
 long start = System.currentTimeMillis();
 for(int i=0;i<100000;i++){
 orgStr.split(",");
 }
 long end = System.currentTimeMillis();
 System.out.println(end-start);
 
 start = System.currentTimeMillis();
 String orgStr1 = sb.toString();
 StringTokenizer st = new StringTokenizer(orgStr1,",");
 for(int i=0;i<100000;i++){
 st.nextToken();
 }
 st = new StringTokenizer(orgStr1,",");
 end = System.currentTimeMillis();
 System.out.println(end-start);
 
 start = System.currentTimeMillis();
 String orgStr2 = sb.toString();
 String temp = orgStr2;
 while(true){
 String splitStr = null;
 int j=temp.indexOf(",");
 if(j<0)break;
 splitStr=temp.substring(0, j);
 temp = temp.substring(j+1);
 }
 temp=orgStr2;
 end = System.currentTimeMillis();
 System.out.println(end-start);
 }
}


输出如清单 6 所示:

清单 6. 运行输出结果

39015
16
15

当一个 StringTokenizer 对象生成后,通过它的 nextToken() 方法便可以得到下一个分割的字符串,通过 hasMoreToken 方法可以知道是否有更多的字符串需要处理。对比发现 split 的耗时非常的长,采用 StringTokenizer 对象处理速度很快。我们尝试自己实现字符串分割算法,使用 substring 方法和 indexOf 方法组合而成的字符串分割算法可以帮助很快切分字符串并替换内容。

由于 String 是不可变对象,因此,在需要对字符串进行修改操作时 (如字符串连接、替换),String 对象会生成新的对象,所以其性能相对较差。但是 JVM 会对代码进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。

以上实例运行结果差异较大的原因是 split 算法对每一个字符进行了对比,这样当字符串较大时,需要把整个字符串读入内存,逐一查找,找到符合条件的字符,这样做较为耗时。而 StringTokenizer 类允许一个应用程序进入一个令牌(tokens),StringTokenizer 类的对象在内部已经标识化的字符串中维持了当前位置。一些操作使得在现有位置上的字符串提前得到处理。 一个令牌的值是由获得其曾经创建 StringTokenizer 类对象的字串所返回的。

清单 7.split 类源代码

import java.util.ArrayList;
 
public class Split {
public String[] split(CharSequence input, int limit) { 
int index = 0; 
boolean matchLimited = limit > 0; 
ArrayList matchList = new ArrayList(); 
Matcher m = matcher(input); 
// Add segments before each match found 
while(m.find()) { 
if (!matchLimited || matchList.size() < limit - 1) { 
String match = input.subSequence(index, m.start()).toString(); 
matchList.add(match); 
index = m.end(); 
} else if (matchList.size() == limit - 1) { 
// last one 
String match = input.subSequence(index,input.length()).toString(); 
matchList.add(match); 
index = m.end(); 
} 
} 
// If no match was found, return this 
if (index == 0){ 
return new String[] {input.toString()}; 
}
// Add remaining segment 
if (!matchLimited || matchList.size() < limit){ 
matchList.add(input.subSequence(index, input.length()).toString()); 
}
// Construct result 
int resultSize = matchList.size(); 
if (limit == 0){ 
while (resultSize > 0 && matchList.get(resultSize-1).equals("")) 
resultSize--; 
 String[] result = new String[resultSize]; 
 return matchList.subList(0, resultSize).toArray(result); 
}
}
 
}


split 借助于数据对象及字符查找算法完成了数据分割,适用于数据量较少场景。

合并字符串

由于 String 是不可变对象,因此,在需要对字符串进行修改操作时 (如字符串连接、替换),String 对象会生成新的对象,所以其性能相对较差。但是 JVM 会对代码进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。针对超大的 String 对象,我们采用 String 对象连接、使用 concat 方法连接、使用 StringBuilder 类等多种方式,代码如清单 8 所示。

清单 8. 处理超大 String 对象的示例代码

public class StringConcat {
 public static void main(String[] args){
 String str = null;
 String result = "";
 
 long start = System.currentTimeMillis();
 for(int i=0;i<10000;i++){
 str = str + i;
 }
 long end = System.currentTimeMillis();
 System.out.println(end-start);
 
 start = System.currentTimeMillis();
 for(int i=0;i<10000;i++){
 result = result.concat(String.valueOf(i));
 }
 end = System.currentTimeMillis();
 System.out.println(end-start);
 
 start = System.currentTimeMillis();
 StringBuilder sb = new StringBuilder();
 for(int i=0;i<10000;i++){
 sb.append(i);
 }
 end = System.currentTimeMillis();
 System.out.println(end-start);
 }
}


输出如清单 9 所示。

清单 9. 运行输出结果

375
187
0

虽然第一种方法编译器判断 String 的加法运行成 StringBuilder 实现,但是编译器没有做出足够聪明的判断,每次循环都生成了新的 StringBuilder 实例从而大大降低了系统性能。

StringBuffer 和 StringBuilder 都实现了 AbstractStringBuilder 抽象类,拥有几乎相同的对外借口,两者的最大不同在于 StringBuffer 对几乎所有的方法都做了同步,而 StringBuilder 并没有任何同步。由于方法同步需要消耗一定的系统资源,因此,StringBuilder 的效率也好于 StringBuffer。 但是,在多线程系统中,StringBuilder 无法保证线程安全,不能使用。代码如清单 10 所示。

清单 10.StringBuilderVSStringBuffer

public class StringBufferandBuilder {
public StringBuffer contents = new StringBuffer(); 
public StringBuilder sbu = new StringBuilder();
 
public void log(String message){ 
for(int i=0;i<10;i++){ 
/*
contents.append(i); 
contents.append(message); 
contents.append("\\n"); 
*/
contents.append(i);
contents.append("\\n");
sbu.append(i);
sbu.append("\\n");
} 
} 
public void getcontents(){ 
//System.out.println(contents); 
System.out.println("start print StringBuffer");
System.out.println(contents); 
System.out.println("end print StringBuffer");
}
public void getcontents1(){ 
//System.out.println(contents); 
System.out.println("start print StringBuilder");
System.out.println(sbu); 
System.out.println("end print StringBuilder");
}
 
 public static void main(String[] args) throws InterruptedException { 
StringBufferandBuilder ss = new StringBufferandBuilder(); 
runthread t1 = new runthread(ss,"love");
runthread t2 = new runthread(ss,"apple");
runthread t3 = new runthread(ss,"egg");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
 
}
 
class runthread extends Thread{ 
String message; 
StringBufferandBuilder buffer; 
public runthread(StringBufferandBuilder buffer,String message){ 
this.buffer = buffer;
this.message = message; 
} 
public void run(){ 
while(true){ 
buffer.log(message); 
//buffer.getcontents();
buffer.getcontents1();
try {
sleep(5000000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} 
} 
 
}


输出结果如清单 11 所示。

清单 11. 运行结果

start print StringBuffer
0123456789
end print StringBuffer
start print StringBuffer
start print StringBuilder
01234567890123456789
end print StringBuffer
start print StringBuilder
01234567890123456789
01234567890123456789
end print StringBuilder
end print StringBuilder
start print StringBuffer
012345678901234567890123456789
end print StringBuffer
start print StringBuilder
012345678901234567890123456789
end print StringBuilder

StringBuilder 数据并没有按照预想的方式进行操作。StringBuilder 和 StringBuffer 的扩充策略是将原有的容量大小翻倍,以新的容量申请内存空间,建立新的 char 数组,然后将原数组中的内容复制到这个新的数组中。因此,对于大对象的扩容会涉及大量的内存复制操作。如果能够预先评估大小,会提高性能。

 
数据定义、运算逻辑优化


使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈 (Stack) 里面,读写速度较快。其他变量,如静态变量、实例变量等,都在堆 (heap) 中创建,读写速度较慢。清单 12 所示代码演示了使用局部变量和静态变量的操作时间对比。

清单 12. 局部变量 VS 静态变量

public class variableCompare {
public static int b = 0;
 public static void main(String[] args){
 int a = 0;
 long starttime = System.currentTimeMillis();
 for(int i=0;i<1000000;i++){
 a++;//在函数体内定义局部变量
 }
 System.out.println(System.currentTimeMillis() - starttime);
 
 starttime = System.currentTimeMillis();
 for(int i=0;i<1000000;i++){
 b++;//在函数体内定义局部变量
 }
 System.out.println(System.currentTimeMillis() - starttime);
 }
}


运行后输出如清单 13 所示。

清单 13. 运行结果

0
15

以上两段代码的运行时间分别为 0ms 和 15ms。由此可见,局部变量的访问速度远远高于类的成员变量。


位运算代替乘除法

位运算是所有的运算中最为高效的。因此,可以尝试使用位运算代替部分算数运算,来提高系统的运行速度。最典型的就是对于整数的乘除运算优化。清单 14 所示代码是一段使用算数运算的实现。

清单 14. 算数运算

public class yunsuan {
 public static void main(String args[]){
 long start = System.currentTimeMillis();
 long a=1000;
 for(int i=0;i<10000000;i++){
 a*=2;
 a/=2;
 }
 System.out.println(a);
 System.out.println(System.currentTimeMillis() - start);
 start = System.currentTimeMillis();
 for(int i=0;i<10000000;i++){
 a<<=1;
 a>>=1;
 }
 System.out.println(a);
 System.out.println(System.currentTimeMillis() - start);
 }
}


运行输出如清单 15 所示。

清单 15. 运行结果

1000
546
1000
63

两段代码执行了完全相同的功能,在每次循环中,整数 1000 乘以 2,然后除以 2。第一个循环耗时 546ms,第二个循环耗时 63ms。


替换 switch

关键字 switch 语句用于多条件判断,switch 语句的功能类似于 if-else 语句,两者的性能差不多。但是 switch 语句有性能提升空间。清单 16 所示代码演示了 Switch 与 if-else 之间的对比。

清单 16.Switch 示例

public class switchCompareIf {
 
public static int switchTest(int value){
int i = value%10+1;
switch(i){
case 1:return 10;
case 2:return 11;
case 3:return 12;
case 4:return 13;
case 5:return 14;
case 6:return 15;
case 7:return 16;
case 8:return 17;
case 9:return 18;
default:return -1;
}
}
 
public static int arrayTest(int[] value,int key){
int i = key%10+1;
if(i>9 || i<1){
return -1;
}else{
return value[i];
}
}
 
 public static void main(String[] args){
 int chk = 0;
 long start=System.currentTimeMillis();
 for(int i=0;i<10000000;i++){
 chk = switchTest(i);
 }
 System.out.println(System.currentTimeMillis()-start);
 chk = 0;
 start=System.currentTimeMillis();
 int[] value=new int[]{0,10,11,12,13,14,15,16,17,18};
 for(int i=0;i<10000000;i++){
 chk = arrayTest(value,i);
 }
 System.out.println(System.currentTimeMillis()-start);
 }
}


运行输出如清单 17 所示。

清单 17. 运行结果

172
93

使用一个连续的数组代替 switch 语句,由于对数据的随机访问非常快,至少好于 switch 的分支判断,从上面例子可以看到比较的效率差距近乎 1 倍,switch 方法耗时 172ms,if-else 方法耗时 93ms。


一维数组代替二维数组

JDK 很多类库是采用数组方式实现的数据存储,比如 ArrayList、Vector 等,数组的优点是随机访问性能非常好。一维数组和二维数组的访问速度不一样,一维数组的访问速度要优于二维数组。在性能敏感的系统中要使用二维数组,尽量将二维数组转化为一维数组再进行处理,以提高系统的响应速度。

清单 18. 数组方式对比

public class arrayTest {
 public static void main(String[] args){
 long start = System.currentTimeMillis();
 int[] arraySingle = new int[1000000];
 int chk = 0;
 for(int i=0;i<100;i++){
 for(int j=0;j


运行输出如清单 19 所示。

清单 19. 运行结果

343
624
287
390

第一段代码操作的是一维数组的赋值、取值过程,第二段代码操作的是二维数组的赋值、取值过程。可以看到一维数组方式比二维数组方式快接近一半时间。而对于数组内如果可以减少赋值运算,则可以进一步减少运算耗时,加快程序运行速度。


提取表达式

大部分情况下,代码的重复劳动由于计算机的高速运行,并不会对性能构成太大的威胁,但若希望将系统性能发挥到极致,还是有很多地方可以优化的。

清单 20. 提取表达式

public class duplicatedCode {
 public static void beforeTuning(){
 long start = System.currentTimeMillis();
 double a1 = Math.random();
 double a2 = Math.random();
 double a3 = Math.random();
 double a4 = Math.random();
 double b1,b2;
 for(int i=0;i<10000000;i++){
 b1 = a1*a2*a4/3*4*a3*a4;
 b2 = a1*a2*a3/3*4*a3*a4;
 }
 System.out.println(System.currentTimeMillis() - start);
 }
 
 public static void afterTuning(){
 long start = System.currentTimeMillis();
 double a1 = Math.random();
 double a2 = Math.random();
 double a3 = Math.random();
 double a4 = Math.random();
 double combine,b1,b2;
 for(int i=0;i<10000000;i++){
 combine = a1*a2/3*4*a3*a4;
 b1 = combine*a4;
 b2 = combine*a3;
 }
 System.out.println(System.currentTimeMillis() - start);
 }
 
 public static void main(String[] args){
 duplicatedCode.beforeTuning();
 duplicatedCode.afterTuning();
 }
}


运行输出如清单 21 所示。

清单 21. 运行结果
    
202
110

两段代码的差别是提取了重复的公式,使得这个公式的每次循环计算只执行一次。分别耗时 202ms 和 110ms,可见,提取复杂的重复操作是相当具有意义的。这个例子告诉我们,在循环体内,如果能够提取到循环体外的计算公式,最好提取出来,尽可能让程序少做重复的计算。


优化循环

当性能问题成为系统的主要矛盾时,可以尝试优化循环,例如减少循环次数,这样也许可以加快程序运行速度。

清单 22. 减少循环次数

public class reduceLoop {
public static void beforeTuning(){
 long start = System.currentTimeMillis();
 int[] array = new int[9999999];
 for(int i=0;i<9999999;i++){
 array[i] = i;
 }
 System.out.println(System.currentTimeMillis() - start);
}
 
public static void afterTuning(){
 long start = System.currentTimeMillis();
 int[] array = new int[9999999];
 for(int i=0;i<9999999;i+=3){
 array[i] = i;
 array[i+1] = i+1;
 array[i+2] = i+2;
 }
 System.out.println(System.currentTimeMillis() - start);
}
 
public static void main(String[] args){
reduceLoop.beforeTuning();
reduceLoop.afterTuning();
}
}


运行输出如清单 23 所示。

清单 23. 运行结果

265
31

这个例子可以看出,通过减少循环次数,耗时缩短为原来的 1/8。


布尔运算代替位运算

虽然位运算的速度远远高于算术运算,但是在条件判断时,使用位运算替代布尔运算确实是非常错误的选择。在条件判断时,Java 会对布尔运算做相当充分的优化。假设有表达式 a、b、c 进行布尔运算“a&&b&&c”,根据逻辑与的特点,只要在整个布尔表达式中有一项返回 false,整个表达式就返回 false,因此,当表达式 a 为 false 时,该表达式将立即返回 false,而不会再去计算表达式 b 和 c。若此时,表达式 a、b、c 需要消耗大量的系统资源,这种处理方式可以节省这些计算资源。同理,当计算表达式“a||b||c”时,只要 a、b 或 c,3 个表达式其中任意一个计算结果为 true 时,整体表达式立即返回 true,而不去计算剩余表达式。简单地说,在布尔表达式的计算中,只要表达式的值可以确定,就会立即返回,而跳过剩余子表达式的计算。若使用位运算 (按位与、按位或) 代替逻辑与和逻辑或,虽然位运算本身没有性能问题,但是位运算总是要将所有的子表达式全部计算完成后,再给出最终结果。因此,从这个角度看,使用位运算替代布尔运算会使系统进行很多无效计算。

清单 24. 运算方式对比

public class OperationCompare {
 public static void booleanOperate(){
 long start = System.currentTimeMillis();
 boolean a = false;
 boolean b = true;
 int c = 0;
 //下面循环开始进行位运算,表达式里面的所有计算因子都会被用来计算
 for(int i=0;i<1000000;i++){
 if(a&b&"Test_123".contains("123")){
 c = 1;
 }
 }
 System.out.println(System.currentTimeMillis() - start);
 }
 
 public static void bitOperate(){
 long start = System.currentTimeMillis();
 boolean a = false;
 boolean b = true;
 int c = 0;
 //下面循环开始进行布尔运算,只计算表达式 a 即可满足条件
 for(int i=0;i<1000000;i++){
 if(a&&b&&"Test_123".contains("123")){
 c = 1;
 }
 }
 System.out.println(System.currentTimeMillis() - start);
 }
 
 public static void main(String[] args){
 OperationCompare.booleanOperate();
 OperationCompare.bitOperate();
 }
}


运行输出如清单 25 所示。

清单 25. 运行结果

63
0

实例显示布尔计算大大优于位运算,但是,这个结果不能说明位运算比逻辑运算慢,因为在所有的逻辑与运算中,都省略了表达式“”Test_123″.contains(“123″)”的计算,而所有的位运算都没能省略这部分系统开销。


使用 arrayCopy()

数据复制是一项使用频率很高的功能,JDK 中提供了一个高效的 API 来实现它。System.arraycopy() 函数是 native 函数,通常 native 函数的性能要优于普通的函数,所以,仅处于性能考虑,在软件开发中,应尽可能调用 native 函数。ArrayList 和 Vector 大量使用了 System.arraycopy 来操作数据,特别是同一数组内元素的移动及不同数组之间元素的复制。arraycopy 的本质是让处理器利用一条指令处理一个数组中的多条记录,有点像汇编语言里面的串操作指令 (LODSB、LODSW、LODSB、STOSB、STOSW、STOSB),只需指定头指针,然后开始循环即可,即执行一次指令,指针就后移一个位置,操作多少数据就循环多少次。如果在应用程序中需要进行数组复制,应该使用这个函数,而不是自己实现。具体应用如清单 26 所示。

清单 26. 复制数据例子

public class arrayCopyTest {
public static void arrayCopy(){
int size = 10000000;
 int[] array = new int[size];
 int[] arraydestination = new int[size];
 for(int i=0;i1000;j++){
 System.arraycopy(array, 0, arraydestination, 0, size);//使用 System 级别的本地 arraycopy 方式
 }
 System.out.println(System.currentTimeMillis() - start);
}
 
public static void arrayCopySelf(){
int size = 10000000;
 int[] array = new int[size];
 int[] arraydestination = new int[size];
 for(int i=0;i


输出如清单 27 所示

清单 27. 运行结果

0
23166

上面的例子显示采用 arraycopy 方法执行复制会非常的快。原因就在于 arraycopy 属于本地方法,源代码如清单 28 所示。

清单 28.arraycopy 方法

public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);

src ? 源数组;srcPos ? 源数组中的起始位置; dest ? 目标数组;destPos ? 目标数据中的起始位置;length ? 要复制的数组元素的数量。清单 28 所示方法使用了 native 关键字,调用的为 C++编写的底层函数,可见其为 JDK 中的底层函数。

 
结束语

Java 程序设计优化有很多方面可以入手,作者将以系列的方式逐步介绍覆盖所有领域。本文是该系列的第一篇文章,主要介绍了字符串对象操作相关、数据定义方面的优化方案、运算逻辑优化及建议,从实际代码演示入手,对优化建议及方案进行了验证。作者始终坚信,没有什么优化方案是百分百有效的,需要读者根据实际情况进行选择、实践。



改善Java文档的理由、建议和技巧


我非常确定,作为开发人员我们都喜爱技术文档。我们喜欢阅读文档、写文档,更不用说维护文档了,我简直爱死它了!

我也知道,每次你创建一个类或者一个方法,你都会想到要为此写文档。我也很确定你很享受于写文档,就像你喜欢偶尔美味的汉堡一样。但是有时候,只是有时候,你会想要松懈一下,也许这次就跳过文档部分。不幸的是,这种行为会很快地失控。

所以在这篇文章中,我想聊聊这个开发者的生活中关键但是通常被忽视并遗忘的部分。希望你会从此爱上文档,明白你的代码为什么能工作,能帮助你、你的团队和使用你的软件的数不尽的用户。


为什么文档很重要

通常,开发者都不会忘记他们两个星期前写的代码。两个月以后甚至更长时间以后他们都会记得。即使我们保证我们从来不忘记我们写过的任何代码,写文档却有另一个理由并且更加重要。


在写代码前理清思路

我会举一个自己的例子:我有一个开发SlideshowFX里一个全新特性的想法,这时我就想直接开始写代码并实现它。但我知道我不是做这项工程的唯一一个有激情的开发者。所以我的典型行为是这样的:

1. 写出以下类主体
public class BurgersManager {
}
2. 思考:“那么,我应该在BurgersManager类中有些CRUD操作”
3. 写下:
public…
4. 思考:“我应该返回什么值?目前来说void就可以”
5. public void addBurger(Burger burger) {
// TODO implement that later
}
public …
6. 思考:“我应该返回被吃掉的汉堡的实例吗?还是void就可以?就像第4步那样。。。”
7. public void eat(Burger burger, boolean fast) {
// TODO …
8. 告诉自己:“糟糕,咖啡时间了,我的咖啡呢。。。”
9. 搜索,喝咖啡,和同事交谈
10. 然后告诉自己:“回去工作吧,我刚才在做什么来着?”

我知道,你在这个例子中看到了自己,对吧?在创造性工作刚开始的时候,我们的思路有些混乱,所以当你直接开始写代码,那么代码也会很混乱。在写代码之前就考虑文档能够帮你理清思路并清除列出你要用代码实现的事。所以第一步应该是写出以下代码:

/**
* 此类通过提供CRUD操作来管理汉堡
* 采用单件模式。可以使用{@link #getInstance()}来获得这个管理器的实例。
* 之后可以用以下方法来调用CRUD操作:
*/
 
{@link #addBurger(Burger)} 用来增加汉堡,并受管理于
* 单件实例 ;
* @作者 Thierry Wasylczenko
* @版本 0.1
* @since BurgerQueen 1.0
*/
public class BurgersManager {
 
}


这就是一个简短的例子,这个例子能够:

    强迫你思考你创建的类的目的是什么
    帮你确定你的需要
    即使是在你休息之后也能帮你想起来你在做什么
    帮助你预估还有什么是需要做的

伙计,你是在团队中开发

你也许不是在单独工 作,你可能有尊敬的同事,你想和那些同事一起喝咖啡聊天。重点是,因为你喜欢他们,所以你想要帮助他们参与到你那令人兴奋的汉堡王的实现中去。为此,最好的做法就是确定他们在读你的代码时,有完美的文档参考。即使他们在你写代码之后的两个星期问你问题,你也能毫无犹豫地回答他们。

这就是另一个为什么文档很重要的理由:它能避免人们多次跑来问你你这复杂的算法是怎样运作的,或者为什么管理器中增加的汉堡没有同样被加到职工管理器的统计中去。在一个团队中,文档可以避免以下问题:

    在工作的时候被打断,之后难以返回继续工作;
    寻找可以回答问题的人,因为让其他成员知道了解自己是否能够回答问题;
    等待某个队员有时间回答他们的问题。
    所以写文档可以帮助团队提高生产力并专注于开发。

让成功更进一步

这一点更加主观些。写Javadoc让我非常有成就感,因为当我再次使用我的API的时候,我写代码有文档参考,这帮我确保我没有忘记任何小细节。尽管我通常不会忘记,知道有文档在支撑我的记忆力也是件很棒的事。

看到IntelliJ IDEA展示我的文档让我有“嘿,看,我就像是专业的,我做的东西太棒了,我甚至有文档噢”的感觉。在某些程度上的确是这样,不是吗?因为当你在使用一个 lib,其中的 log(String s, int i) 没有任何命名良好的参数描述,你一定像我一样在想“这个究竟是什么玩意儿?”。

不知道你怎样想的,我反正是觉得新的Javadoc设计特别赞。我认为让自己的文档整洁是非常棒的事。但是正如我说的,这只是我个人的感受。


写Javadoc的小技巧

在Javadoc中你有一下很好的标签可以使用:

    @author
    @version
    @param
    @return
    @exception/@throws
    @see
    @since
    @serial/@serialField/@serialData
    @deprecated

但是这篇文章的目的并不是详细解释所有标签,而是作为文档作者和开发人员,我想分享我在写我的Javadoc时使用的技巧。


使用@link和@linkplain来指向某些代码

在我的Javadoc中,如果有依赖关系或者对文档有用,我会提及其它类和方法。为了使方法和类的浏览更简便,你可以使用@link。它是这样工作的:

    {@link BurgersManager} 指向一个类
    {@link BurgersManager burgers manager} 指向带有标签的类
    {@link #eat(Burger, boolean)} 指向此类中的某个方法
    {@link #eat(Burger, boolean) eat} 指向此类中带有标签的某个方法
    {@link BurgersManagers#eat(Burger, boolean)} 指向其他类中的某个方法
    {@link BurgersManagers#eat(Burger, boolean) burgers manager eat} 指向其他带有标签的类的某个方法

@link 和 @linkplain 的区别是后者不会生成等宽字体的代码。

使用@code来表明代码段

通常你会在Javadoc中发现一段代码,用来说明怎样使用方法和类,或者提供其它例子。为了正确显示代码,并防止一些像这样的标记被打断,你可以使用@code。

{@code
List<Burger> burgers = new ArrayList<>();
  for(int index = 0; index < 10; index++) {
    burgers.add(new Burger(“Burger #” + index));
  }
}

@code会为你生成标记。

使用@value来在文档中插入字段值

当你有一个常量,我可能想要它的值在文档中显示出来。有两个选择:

    自己插入这个值。但是如果这个值改变了,你必须更新你的文档,如果你绝对不会忘记这点,那你可以放心选择这个做法;
    使用@value来为你插入值,这样你就不用手动更新你的文档。

对我来说第二个选择是利用Javadoc工具的最佳方法,我会讨论这个方法。实际上,使用单一属性特别有用:

/**
* The default value for this field is {@value}.
* 这个域的默认值是{@value}.
*/
public static final String BURGER_SHOP_NAME = "Thierry's shop";


但你也可以指向其它常量,比如:

/**
* The default value for this field is {@value} when the value
* of {@link #OWNER} is {@value #OWNER}.
 
* 这个域的默认值是{@value} 当
* {@link #OWNER}的值为{@value #OWNER}.
*/
public static final String BURGER_SHOP_NAME = "Thierry's shop";
 
/**
* The default owner of this awesome burger shop.
 
* 这家很棒的汉堡店的默认店主.
*/
public static final String OWNER = " Thierry";


用@since来表明此特性的生效时间

通常,在你的代码中表明类或者方法何时开始生效非常有用。为此使用@since标签并在其后注明该特性执行的版本/年份:

/**
* This awesome class is for doing awesome things
* 这个棒呆了的类是用来做些棒呆了的事
* @since burger-core-0.1
* @version 0.2
*/
public class BurgersManager {
 
/**
* Allows to eat burgers
* 可以吃汉堡
* @since burger-core-0.2
*/
public void eat(Burger burger, boolean fast) {
// TODO
}
}


你可以看到,我把它用在了方法和类上,并且不止包含了版本号。事实上,现在我们的应用有很多不同的模块,这些模块可以有不同生命周期,即版本。说某个方法或者类从0.2版本开始生效并没有特别的意思。那么究竟是什么的0.2版本?这就是为什么我总是用一个相关的@since 来帮助我的同事第一眼就明白这些是什么时候开始生效的。

不止如此,这个标签的一个好处就是它可以帮你创建发布说明。等会儿,啥?不,并不是使用你最喜欢的IDE,比如IntelliJ IDEA,然后查找包含“@since burger-core-0.2″的文件。然后瞧,你可以找到自那个版本之后添加的所有方法和类。当然,这无法告诉你被更新的方法和类,而只会告诉你新添加的东西。但是你应该看到,这么简单的窍门多有用。


不要匿名,使用 @author

我非常讨厌的一件事:开发人员不承认自己的代码,并且不表明是他们为了一个糟糕的原因写了这糟糕的代码。如果你写了一段代码,要么承认它,要么去当经理。你可以用 @author 来表明你是这个类或者方法的作者。我认为把这标签既放在类上也放在方法上比较好,因为一个类的方法可能不是都是类的作者写的。

另一个好习惯就是,把一个方法或类的所有作者都加上。 试想一下,你和你的同事写了一个很棒的方法,而标签表明你是这个方法的唯一作者。有一天你去度假了,有人在读你的方法,但不是很明白并且想要一些细节。而是因为你被标为唯一的作者,他们不知道这个信息可以从和你一起写代码的同事那里很容易就获得。你知道我要说什么了,对吧?要记得给代码加@author来表 明作者。

对非void方法要使用@return

我要说这一点对我来说非常有意义。有时候我看到类似以下例子中的代码就要跪了。

/** Get the address.
 * @return
 */
public String getAddress() { /* … */ }

为什么!?说真的,为什么你不填好@return?“因为只是一行而已,就是获得地址”。

不不不,请不要这样。如果你那样回答,是因为你的文档。怎么说呢,因为你的文档欠佳。是的,因为你可以很简单地写出一个更好的版本,而不是像以上你见到的糟糕的文档, 看:

/**
* Get the address of this burger shop. The address is of the following format:
* {@code address line 1
* address line 2
* zipcode city}
* @return the address of this burger shop or {@code null} if not filled.
*/
 
/**
*获取汉堡店的地址。地址格式:
* {@code 地址行1
* 地址行2
* 邮编 城市}
* @return 汉堡店的地址,如果没有填地址返回 {@code null}.
*/


好太多了,对吧?这样你的文档就有用了。我一直试着寻找给代码写文档的合适方法,因为有时候读者只读 @return 的内容,有时候也会读 @return 上面的内容,你添加一些说明就可以简单地避免疑惑。


用@param说明参数的含义

有什么比看到方法使用一个像 i 这样的意义不明的参数而不加任何文档更加沮丧呢?有时候你可以通过方法的名字来猜到这个参数的目的,可是有时候就不行。所以在你的文档里,你应该使用@param来表明这个参数的含义,并说明可能的有效值。在我们的例子中,i可以是日志的级别:INFO, DEBUG或者TRACE。这个标签另一个很有用的例子就是当这个值对应的是一个索引。有些情况下索引从0开始,有些情况下从1开始。@param就是用来描述这一区别的标签。


生成文档

在代码中有文档是非常好的,但是现在你必须生成文档。所以你可以使用JDK提供的Java文档工具来生成它。

通过执行类似这样的命令:

javadoc {packages|source-files} [options]

你可以指定想要生成文档的包名或文件名,多个名字用空格分隔。

以下简要描述了一些jJavadoc工具能够接受的选项:

    -author: 在生成的文档中生成@author用
    -d: 要在当前目录之外生成文档的目录
    -nodeprecated: 不为被标为@deprecated的代码生成文档
    -protected: 包含protected和public类和类成员
    -private: 包含private类和类成员
    -public: 只包含public类和类成员

像IDE之类的工具也可以生成你的文档,但是如果它很好地格式化并且可以提供预览。

一些像Maven和Gradle这样的依赖管理工具也带有生成文档的阶段或任务。这很棒,因为你的文档可以一直紧随代码的发布来生成,这样它就一直是最新的。

总结

文档对于你的整个团队非常重要。它能帮你理清你在写什么代码,更重要的是,你为什么这样实现它。

本文来源:http://www.bbyears.com/wangyezhizuo/83683.html

热门标签

更多>>

本类排行