我才是小灰灰
本帖最后由 我才是小灰灰 于 2015-11-8 12:39 编辑

      从我开始学java至今已经4年了,在此期间,我绝大部分时间来研究的是插件开发,虽然没写出几个插件也没深入研究API,但是学到了很多java底层的东西,对java语言有了深入了解,正好通过此贴向广大java编程者提出一些编程建议,无论是从优化或者代码的理解上,这些建议全部源自于java语言而不是API,因此这些建议同样适用于任何java编程。如若有更好的建议,可以在此贴分享。若我有什么错误或疏漏之处,请多多指教。



另:本书内容提取自《提高编程效率,改善java程序的151个编程建议》







对于新人的一些编程建议(以下建议针对初学java或者初学插件开发的新人,更侧重于编程思想):



  1.拥有一个清晰、明了的代码风格

    首先,我们写出来的代码是给人先读,再给机器看的。于是我们的代码风格直接决定着我们的编程效率。因之,有一个良好的代码风格是极其必要的。对于机器而言,不需要任何的分隔和换行,也能读懂,但是你可以试试读同样的代码,保证恶心至极。没有层次的代码不会有人看得懂,也不会有人浪费时间去看,而且很可能那个人就是未来的你。所以,我们应当有一个良好的代码风格,那么什么算是良好的代码风格呢?其实很简单,要用TAB就都用TAB,要用空格就都用空格,把各层次分清了,代码读起来简便、明了,而且注释一语中的(有关于注释的内容下一个建议详解),这样的代码风格才可以说是好,这样的代码质量才会高。



  2.要写注释,也要写好注释


    要想使自己的代码可读性高,注释必不可少,注释可以方便理解代码,一方面减少我们理解代码的时间,另一方面使一些难以理解的代码易于理解。但并非说所有注释都是好注释,良好的代码风格要求我们有良好的注释风格,那么我们应当摒弃以下几类注释:
     (1)废话式注释
     看这么一个例子:

  1. public class Test{
  2.     //此算法没有XX算法优秀,需要优化,时间紧,以后再说
  3.     public void doSomething(){
  4.     }
  5. }
复制代码
     此类注释没有必要存在,因为写了也没有什么卵用,如果是为了提醒自己的话,可以用TODO注释。

      (2)故事性注释
       我曾经见过一个奇异的算法,是解决一个数学问题的。尽管他将这个算法已经优化到了极致,但是他的注释风格不可取。他的注释非常详细,从问题的由来一直到之前的算法介绍都非常详细。但请知道,我们的算法不是给外行看的,没必要这么详细。


      (3)不必要的注释
       再看这么一个例子:
  1. public class Test{
  2.     //初始化,值为0
  3.     public int i;
  4.     //定义方法,无参,返回值空
  5.     public void doSomething(){
  6.         //自加
  7.         i++;
  8.     }
  9. }
复制代码
      此处的三个注释严重贬低了阅读者的智商,不要以为自己的代码是给外行看的,这些注释的内容不看也知道,因此这些注释不应当出现在我们的程序中。


      (4)过时的注释
       当程序早已经出了V7、V8了,我们的注释仍旧是V1,此时的注释已经形同虚设。


      (5)注释代码
       我们总会因各种原因去注释掉一些代码,但是当我们确定不需要这个代码的时候,请删除掉他,以免对他的理解性和代码连贯性产生影响。



       那么什么样的注释是好的注释呢?
      (1)解释意图的注释
       此类注释去解释代码为什么要这么做,而不是怎么去这么做,像是此代码解决了什么,或是为什么过时之类的,这种注释才能起到注释的真正作用。

      (2)警示性注释
       此类注释常常被我们忽略,但是他的用处非常重要,这些注释用来提示一些信息,比如代码在不同操作系统上的不同表现,或者警告后人不要修改之类的。

      (3)TODO注释
       对于一些没有完成的任务需要加上TODO注释,并且注释什么还没有做完,这样可以提醒自己及时修改。


       只有写好注释,才能使自己的代码更可读,更容易理解。




   3.java基础一定要牢固

     俗话说的好,工欲善其事,必先利其器。对我们javaer而言,java语言便是我们的“器”,所以要想写好插件,必须有牢固的java基础。学好java,不仅仅是语法上的理解,更要树立一种OOP(面向对象编程)的编程思想,否则,我们即使能保障程序的运行,也不能发挥程序的特色,发挥java的特色。也无法使代码更简洁,更高效,我们的程序依旧是死板的,模式化的。面向对象编程即是java的核心,所以想要学好java编程,必须先构建一个OOP的编程思想。


        以上3点即是我针对新人的一些编程建议,更侧重于思想,有什么错误或者更好的建议欢迎指教。

下面是对于已经学完基本语法,略有实践的编程者的建议(偏向于语法)

  1.注意符号与数据一定要分清

   看一个简单的例子:

  1. public class Test{
  2.     public static void main(String args[]){
  3.         long l=1l;
  4.         //输出的值是多少呢?
  5.         System.out.println(l);
  6.     }
  7. }
复制代码
    输出的会是多少呢?22吗?并不是,是2。因为1后面的是l,是long型常量的标示符(Ps:打到论坛上一点混淆感觉都没有QAQ)。从这个例子我们应当明白,我们使用标识符的时候一定要区分开数据常量,以免降低代码的可读性,比如这里应该用大写L。当出现O的时候要大写而且要注释清楚。这样代码可读性更高,没有异义性。

  2.常量只能是常量,不做它用


   常量是值不可变的量,因此常量的值应当是已知且已确定的。所以在编程中,常量就应当是常量,不能以违法常量的定义存在,因此建议常量必须在编译时确定值。就如同下面这个代码是不应当出现的:
  1. public static final long l=new Random().nextLong();
复制代码
   常量只能是常量,必须在编译时确定其值,其他情况均定义为变量。


  3.警惕自加的陷阱

   看下面一串代码:
  1. public class Test{
  2.     public static void main(String[] args){
  3.         int c=0;
  4.         for(int i=0;i<10;i++){
  5.             //对变量c进行10次自加,输出结果是10吗?
  6.             c=c++;
  7.         }
  8.         System.out.println(c);
  9.     }
  10. }
复制代码
    从开始学习"++"运算时,我们就听过,"i++"是先自加再赋值,"++i"是先赋值再自加。但是这两句话什么意思呢?我们再想上面的例子,会输出什么呢?9?10?都不是。会输出0。
    为什么输出0呢?因为"i++"形式的运算等同于下面的代码:
  1. public int add(int c){
  2.     //创建一个拷贝
  3.     int temp=c;
  4.     c=c+1;
  5.     return temp;
  6. }
复制代码
    看到这里可能就恍然大悟了,原来在执行类似"i++"的运算时,系统会把原值的一个拷贝值作为返回值,于是返回值就变成了原值,而自加在赋值前就完成了。
    但遇到同样的问题,C语言和java的处理截然相反。
    在java编程中,要警惕这个陷阱。但不要拿相同的思维考虑别的语言。


  4.break万万不能忘


    switch是我们常用的条件语句,在某些情况下switch语句的效率比多重if高的多,而且可读性强。但使用switch一定要注意一个问题,case块默认是向下传递的。看这样一个算法:
  1. public void numToChinese(int i){
  2.     switch(i){
  3.         case 1:System.out.println("壹");
  4.         case 2:System.out.println("贰");
  5.         case 3:System.out.println("叁");
  6.         case 4:System.out.println("肆");
  7.         case 5:System.out.println("伍");
  8.         case 6:System.out.println("陆");
  9.         case 7:System.out.println("柒");
  10.         case 8:System.out.println("捌");
  11.         case 9:System.out.println("玖");
  12.     }
  13. }
复制代码
    假设我们调用这个方法,并输入参数1,打印的结果是"壹"吗?不是。那是多少呢?是"壹贰叁肆伍陆柒捌玖"为什么?因为switch的设定,当一个case块中没有跳转指令,将自动执行下一个case块。所以在这里当输入1时,块123456789都会被执行一次。所以这段代码每一个case块后面都要加一个break;跳出switch。谨记这一点,以防发生小细节的错误。


  5.避免带有变长参数的方法重载


    我们看一个计算折扣的代码示例:
  1. public class Client{
  2.     //简单折扣计算
  3.     public void calPrice(int price,int discount){
  4.         float kp=price*discount/100.0F;
  5.         System.out.println("简单折扣后:"+formateCurrency(kp));
  6.     }
  7.     //复杂多折扣计算
  8.     public void calPrice(int price,int...discounts){
  9.         float kp=price;
  10.         for(int discount:discounts){
  11.             kp=kp*discount/100;
  12.         }
  13.         System.out.println("复杂折扣后:"+formateCurrency(kp));
  14.     }
  15.     //格式化成本的货币形式
  16.     private String formateCurrency(float price){
  17.         return NumberFormat.getCurrencyInstance().format(price/100);
  18.     }
  19.     public static void main(String args[]){
  20.         Client client=new Client();
  21.         //499元的货物,打75折
  22.         client.calPrice(49900,75);
  23.     }
  24. }
复制代码
    这段代码完全可以编译,但是具有严重的二义性。我并不知道它调用的是哪个方法。实际上它调用的是简单折扣,因为java的编译器会从简单的开始猜想,把75先猜想成基础类型,再考虑是不是编入一个变长参数的数组中。尽管我们知道代码会调用哪个方法,但还是应当避免这种二义性特别高的代码出现。


  6.别让null和空值威胁到变长方法


    看下面的代码:
  1. public class Client{
  2.     public void method A(String str,Integer...is){
  3.     }
  4.     public void methodB(String str,String...args){
  5.     }
  6.     public static void main(String args[]){
  7.         Client client=new Client();
  8.         client.methodA("China",0);
  9.         client.methodA("China","People");
  10.         client.methodA("China");
  11.         client.methodA("China",null);
  12.     }
  13. }
复制代码

  此代码有2处无法编译,最后两个methodA的调用。方法模糊不清,编译器并不知道调用哪个。变长参数允许参数是0个,但方法规范却无法调用,因为设计者违反了KISS原则(Keep It Simple,Stupid,即懒人原则),此类代码的参数需要让调用者去猜测,这是万万不可的。
    最后一个调用不仅违反了懒人原则,而且有一个坏的代码风格,隐藏了参数的类型。方法中的null的类型被隐藏了。此处的代码应当改为下面的代码:

  1. String s=null//告诉编译器,这个null是String型的
  2. client.methodA("China",s);
复制代码



    在变长参数中需要注意,变长参数其实是一个数组参数,在编译生成的字节码里面有一个特殊标记。可以把空值,null,单个数据,数组“推测”为方法的实参,但当形参为数组的时候,编译器不会做此类“推测”


  7.覆写变长方法也循规蹈矩


    我们对覆写有着严格的要求:
        ①覆写方法不能缩小访问权限
        ②参数列表必须与被覆写方法相同
        ③返回类型必须与被覆写方法相同或者其子类
        ④覆写方法不能抛出新的异常,或者超出父类范围的异常,但可以抛出更少、更有限的异常
    满足以上条件的我们才能称之为覆写,那么看下面的代码:

  1. public class Client{
  2.     public static void main(String args[]){
  3.         //向上转型
  4.         Base base=new Sub();
  5.         base.fun(100,50);
  6.         //不转型
  7.         Sub sub=new Sub();
  8.         sub.fun(100,50);
  9.     }
  10. }
  11. //基类
  12. class Base{
  13.     void fun(int price,int...discounts){
  14.         System.out.println("Base......fun");
  15.     }
  16. }
  17. //子类
  18. class Sub extends Base{
  19.     @Override
  20.     void fun(int price,int[] discounts){
  21.         System.out.println("Sub......fun");
  22.     }
  23. }
复制代码

    此代码不可编译,原因不在覆写上,这个覆写是完全正确的,因为变长参数也是个数组。只是eclipse会报不是一个良好的代码风格。报错在sub.fun(100,50)上,报错的原因是找不到方法fun(int,int)。事实上,base对象把Sub类向上转型,形参列表是由父类决定的,所以50会被编译器“猜测”变成{50},再由子类执行。但是Sub类不进行向上转型的时候,由于形参为数组,所以调用时不进行猜测,java又要求严格的类型匹配,于是报错。


[size=14.44444465637207px]
  8.instanceof你会用了吗?



    看下面一个关于instanceof的代码示例:
  1. public class Client{
  2.     public static void main(String args[]){
  3.         boolean b1="abc" instanceof Object;
  4.         boolean b2=new String() instanceof String;
  5.         boolean b3=new String() instanceof Date;
  6.         boolean b4=null instanceof String;
  7.         boolean b5=(String)null instanceof String;
  8.     }
  9. }
复制代码

    请问这其中的5个代码,哪些能编译,哪些不能,哪些是true,哪些是false?我们来一个个分析:
    首先,"abc"是String型的常量,因此和Object构成实例关系,所以这个是true。b2中new String()是一个新的String类对象,因此和String绝对有实例关系,返回true。b3无法编译,因为String和Date类没有继承关系,instanceof不会允许没有直接继承关系的类别进行instanceof判断的。b4是false,在instanceof语法中,左操作数是null直接返回false。b5也是false,即使null是String型,它也是null,所以返回false。
    这么简单的instanceof,你答对几个?

[size=14.44444465637207px]
  9.常量类在编译时不要只编译一个类

[size=14.44444465637207px]


    下面这条建议我们要抛开IDE,退回到记事本编程时代说起。
    我们用记事本写下面这个类:
  1. public class Constant{
  2. //定义人类寿命极限
  3. public static final int MAX_AGE=150;
  4. }
复制代码

    主类:
  1. public class Client{
  2.     public static void main(String args[]){
  3.        System.out.println(Constant.MAX_AGE);
  4.     }
  5. }
复制代码

    然后写一个批处理来编译:
  1. @echo off
  2. javac Constant.java
  3. javac Client.java
  4. java Client
复制代码

    这个批处理用来编译两个类,然后运行主类,输出是150无误。现在我想改一下常量,我们进入常量类,将人体最大寿命改为180。然后重写一个批处理,只编译常量类:
  1. @echo off
  2. javac Constant.java
复制代码

    这次只编译常量类,想知道结果是什么么?仍是150。为什么?因为对于常量,在编译时其值已经被写到字节码里了,因此会这样。这也是我们为什么不用IDE的原因,IDE默认全部重新编译。

[size=14.44444465637207px]
  10.用偶判断不用奇判断

[size=14.44444465637207px]

[size=14.44444465637207px]    下面我们说说取余运算,看一个代码:
[size=14.44444465637207px]
  1. public class Client{
  2.     public static void main(String args[]){
  3.         //接收键盘信息
  4.         Scanner input=new Scanner(System.in);
  5.         System.out.println("请输入多个数字判断奇偶");
  6.         while(input.hasNext()){
  7.             int i=input.nextInt();
  8.             String str=i+"->"+(i%2)==1?"奇数":"偶数";
  9.             System.out.println(str);
  10.         }
  11.     }
  12. }
复制代码

    这是一个简单的奇偶数判断,小学生都会,不详解释。
    现在我们输入几个数进行测试:
  1. 3,5,2,0,-1,-2
复制代码

    结果:
  1. 3->奇数
  2. 5->奇数
  3. 2->偶数
  4. 0->偶数
  5. -1->偶数
  6. -2->偶数
复制代码

    我们可以看出,正数和零没有一点问题,而负数便有问题了,-1怎么会是偶数呢?原来java的取余运算等同于如下代码:
  1. public static int remainder(int dividend,int divisor){
  2.     //dividend是被除数,divisor是除数
  3.     return dividend-dividend/divisor*divisor;
  4. }
复制代码

数学好的同学可能立马顿悟了,当-1被传递进去后-1除以2的余数是-1当然不是1了!所以上面的代码改为下面的更可靠:
  1. i%2==0?"偶数":"奇数";
复制代码











纯手打 求人气金粒 求精分A.A
(下次更新预计半个月之后,毕竟我是住校的学生党TAT)



RaycusMX
软文    

云闪
QAQ我还以为是针对BukkitAPI的建议

然而151个建议那本书我已经看完了,但是把它作为习惯应用到代码里还需要一定的时间23333

andylizi
关于注释的部分,我补充一下,看你认不认同。。。
有时候我会纠结某个复杂的数学算法到底怎么实现,尝试着写了一堆代码,但代码执行结果并不是我想象的那样,所以我得找出原因,但这时看着一堆复杂的玩意感觉很头疼,根本理不清哪个是哪个,这时我就会在后面加上详细的注释,写明这句代码是干嘛的,这个变量保存的值是什么,这个计算公式原理是什么,帮助我理清顺序,不然直接去看,怎么看看不出来究竟问题在哪。。

我才是小灰灰
andylizi 发表于 2015-10-23 13:13
关于注释的部分,我补充一下,看你认不认同。。。
有时候我会纠结某个复杂的数学算法到底怎么实现,尝试着 ...

特殊情况特殊对待A.A

andylizi

A.A可是这也是注释的用途!

494308843
= =为毛我输出是1
                        long l=1l;
                        //输出的值是多少呢?
                        System.out.println(l);

hahahahah
我基本上不打注释的。。。但是在尝试打注释

fyyo429
C党不服
  1. #include "stdio.h"
  2. void main(){
  3. int i=0;
  4. printf("%d %d %d %d",i++,i,++i,i);
  5. //0 1 2 2
  6. }
复制代码

在这点上java跟C系应该一样

是夜初哇丶
LZ辛苦!建议很有用!支持楼主!

第一页 上一页 下一页 最后一页