打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Java高效编程
  • 创建及销毁对象
    • - 考虑用静态工厂方法替代构造函数
      • 静态工厂方法的优势
        • 静态工厂方法的一个优势是,它们具有自己的名字。构造函数的参数自身无法描述被返回的对象,而选用名字合适的静态工厂方法可以使类的使用更加容易,产生的客户代码更容易阅读。
        • 静态工厂方法的第二个优势是它们不需要在每次调用时都去创建一个新的对象。这使得非可变类可以使用预先构造的实例,或者在构造阶段先缓存这些实例,然后重复使用它们,从而避免创建不必要的重复对象。
        • 静态工厂方法为重复调用而返回同一对象的能力,可以用来控制某一时刻实例的存在情况。
          • 有两个理由使静态方法可以做到这一点。
            • 首先它能够使类保证实例是singleton。
            • 其次,它能够使非可变类保证没有两个相等的实例同时存在。
        • 静态工厂的第三个优势是它们可以返回到返回类型的任何子类型(subtype)对象。这使用户在选择返回对象的类时具有很大的灵活性。
      • 静态工厂的缺陷
        • 静态工厂的主要缺陷是类在没有公共或受保护的构造函数时不能被子类化。例如,不可能子类化Collections Framework中的任何集合类。
        • 静态工厂方法的另一个缺陷是名字之间不容易区分。
      • 总地来说,如果你权衡过了这两种方法,同时没有其他因素影响你的选择取向,那么最好还是简单地使用构造函数,因为这符合规范。
    • - 使用私有构造函数强化singleton属性
      • singleton类就是一种只能被实例化一次的简单类。
      • - 实现singlton有两种途径,这两种途径都以保持构造函数私有及提供公共的静态成员允许客户访问它的唯一实例为基础。
        • 一种实现方法是,公共的静态成员是一个final域:
          //singleton with final field
          public class Elvis {
                    public static final Elvis INSTANCE = new Elvis();  
                    private Elvis() {
                               ...
                    }  
                    ...
          }
          私有构造函数只被调用一次,以初始化公共的静态final域Elvis.INSTANCE。公共的或受保护的构造函数的缺乏,保证了一个“唯一的elvis”的世界:一旦Elvis类被初始化,仅有一个Elvis实例存在。

          第一种方式的突出优点是类的成员的声明使类清楚地表明它是singleton:公共的静态域是final,所以这个域将总是包含着相同的对象引用。

          第二种方法中,提供了公共的静态工厂方法,取代了公共的静态final域:
          //singleton with static factory
          public class Elvis {
                    private static final Elvis INSTANCE = new Elvis();  
                    private Elvis() {
                               ...
                    }
                    public static Elvis getInstance() {
                               return INSTANCE;
                    }
          }
          对所有的静态方法Elvis.getInstance的调用,都返回同一个对象的引用,没有其他的Elvis实例被创建。

          第二种方式的突出优点在于它给使用者提供了灵活性,当你决定把这个类改变为非singleton的时候,无需修改API。

          总而言之,如果你确信该类将永远是singleton的,那么应该使用第一种方法。反之,第二种方式是更好的选择。

    • - 用私有构造函数强化不可实例化能力
      • 有些类不希望被实例化,对它们实例化也没有意义。

        试图通过将类抽象化来强化类的不可实例化能力是行不通的。这是因为类可以被子类化,而子类可以被实例化。这种做法还会误导用户,以为这种类的目的是为了实现继承的。

        有一种简单的方法可以解决这一问题。由于缺省的构造函数仅在类不包含显示的构造函数时才会生成,我们可以在类中包含显式的私有类型构造函数来实现类的不可实例化特性。

        因为声明的构造函数是私有的,所以它在类的外部不可访问。假设构造函数不会被类自身从内部调用,即能保证类永远不会被实例化。 例:
        class F {
               private F() {
                       ...
               }
               ...
        }

    • - 避免创建重复对象
      • 重用同一对象比每次都创建功能等同的新对象通常更适合。重用方式既快速也更加时尚。

        下面的语句是个极端的例子,千万不要这样做:
        String s = new String("Silly"); //never do this
        该语句每次执行时都创建一个新的String实例,然而这些对象的创建是不必要的。

        一个简单的版本如下: String s = "No longer silly";

        除了对非可变对象重用,我们也可以重用可变(mutable)对象,只要知道它不会发生变化。下面是一个例子,不要使用这种做法:
        public class Person {
               private final Date birthDate;
               public Person(Date birthDate) {
                       this.birthDate = birthDate;
               }

               //Don‘t do this!
               public boolean isBabyBoomer() {
                       Calendar gmtCal = Calender.getInstance(Timezone.getTimezone("GMT"));
                       gmtCal.set(1946, Calender.JANUARY, 1, 0, 0, 0);
                       Date boomStart = gmtCal.getTime();
                       gmtCal.set(1965, Calender.JANUARY, 1, 0, 0, 0);
                       Date boomEnd = gmtCal.getTime();
                       return birthDate.compareTo(boomStart) >= 0
                                   && birthDate.compareTo(boomEnd) <0;
               }
        }
        在每次被调用时,方法isBabyBoomer都不必要地创建了一个新的Calendar、TimeZone和两个Date实例。

        下面的版本通过使用静态方法的初始化方法避免了这样的低效做法。
        public class Person {
               private final Date birthDate;
               public Person(Date birthDate) {
                       this.birthDate = birthDate;
               }
               private static final Date BOOM_START;
               private static final Date BOOM_END;  

               static {
                       Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
                       gmtCal.set(1946, Calender.JANUARY, 1, 0, 0, 0);
                       Date BOOM_START = gmtCal.getTime();
                       gmtCal.set(1965, Calender.JANUARY, 1, 0, 0, 0);
                       Date BOOM_END = gmtCal.getTime();
               }

               public boolean isBabyBoomer() {
                       return birthDate.compareTo(boomStart) >= 0
                                       && birthDate.compareTo(boomEnd) <0;
               }
        }

    • - 消除对过期对象的引用
      • 例: //Can you spot the "memory leak"?
        public class Stack {
               private Object[] elements;
               private int size = 0;
               public Stack(int initialCapacity) {
                       this.elements = new Object[initialCapacity];
               }  

               public void push(Object e) {
                       ensureCapacity();
                       elements[size++] = e;
               }

               public Object pop() {
                       if(size =- 0)
                               throw new EmptyStackException();
                       return elements[--size];
               }  

               private void ensureCapacity() {
                       if(elements.length == size) {
                               Object[] OldElements = elements;                              elements = new Object[2*elements.lenght+1];                System.arraycopy(OldElements,0,elements,0,size);
                       }
               }
        }

        这个程序没有明显的错误,但是却隐藏着一个问题,不严格地讲,程序有一个“内存漏洞”。如果栈先增长再收缩,那么被栈弹出的对象即使在程序不再引用它们时,也不会被垃圾回收单元回收。这是由于栈维护着对这些对象的过期引用(obsolete reference)。所谓过期引用,是指永远不会被解除引用的引用。

        解决这种问题的方法很简单:一旦它们过期,清除掉对它们的引用。
        pop正确的版本是:
        public Object pop() {
                  if(size == 0)
                             throw new EmptyStackException();
                  Object result = elements[--size];
                  elements[size] = null;
                  return result;
        }

        清除掉过期引用的额外好处是,如果它们随后被错误地解除引用,程序将会由于NullPointerException异常退出,而不是继续错误地运行下去。

    • - 避免使用终结程序
      • 终结程序(finalizer)的行为是不可预测的,而且是危险的,通常也不必要。不要把终结程序作为C++析构函数(destructor)的类似物。

        终结程序无法保证能别及时的执行,这意味着不能用终结程序来处理时间关键(time-critical)性的操作。例如,依赖终结程序去关闭打开的文件是一个严重的错误,因为打开的文件描述符是一种有限资源,而JVM不会及时地安排终结程序执行,如果多个文件处于打开状态,程序有可能会因为无法再打开文件而执行失败。

        JLS不仅不保证终结程序的及时执行,它甚至不保证终结程序会获得执行。永远不要依赖终结程序去更新关键的持续状态(persistent state)。例如,依靠终结程序释放一个共享资源如数据库上的持续锁,将是导致所有分布系统跨掉的绝佳方法。

  • 类和接口
    • - 最小化类和成员的可访问能力
      • 区分设计良好与设计拙劣的模块的唯一也是最重要的元素是模块向外部模块隐藏内部数据和其他实现细节的程度。
      • 经验规则指出应该使每个类或成员尽可能地不被外部访问。换句话说,在设计程序时,应该根据软件功能,使用允许的最低的访问等级。
      • 有一条规则限制了我们给方法降低可访问性的能力:如果方法要重载超类中的方法,那么不允许子类中该方法的访问等级低于超类中的访问等级。
    • - 倾向于非可变性
      • 非可变类就是类的实例不能被修改的类。如String。

        非可变类有很多好的存在理由

        • 非可变类更容易设计、实现和使用
        • 不易犯错误
        • 更安全

        为了使类成为非可变的,要遵循下面5条原则

        • 不要提供任何修改对象的方法。
        • 保证没有可以被重载的方法。这防止了粗心的或恶意的子类危害对象的不可变行为。防止方法被重载的一般方式是是类成为final的。
        • 使所有的域成为final。
        • 使所有的域都是私有的。
        • 保证对任何可变组件互斥访问。

        下面是一个稍微复杂的例子:
        public final class Complex {
               private final float re;
               private final float im;  
               public Complex(float re, float im) {
                       this.re = re;
                       this.im = im;
               }
               public float realPart()        { return re; }
               public float imaginaryPart()        { return im; }  

               public Complex add(Complex c) {
                       return new Complex(re+c.re,im+c.im);
               }  
              
                ...  
               
               public boolean equals(Object o) {
                       if(o==this)
                               return true;
                       if(!(o instanceOf Complex))
                               return false;  
                       Complex c = (Complex) 0;
                       
                       return(Float.floatToIntBits(re) == Float.floatToIntBits(c.re) ) &&
                                       (Float.floatToIntBits(im) == Float.floatToIntBits(c.im));
               }  
               
               public int hashCode() {
                       int result = 17 + Float.floatToIntBits(re);
                       result = 37*result + Float.floatToIntBits(im);
                       return result;
               }  
               
               public String toString() {
                       return "("+re+" + "+im+"i)";
                }
        }
        这个类表示复数,注意到算术操作创建和返回一个新的复数实例,而不是修改了这个实例。

        非可变对象本质上是线程安全的,不需要同步机制。这是获得线程安全最简单的途径。线程不会看到其他线程对非可变对象施加的影响,因而,非可变对象可以被自由地共享。

        非可变对象可以提供一个静态工厂,将经常被请求的实例缓存起来,避免在被请求的实例存在时,重复地创建新的实例。BigInteger和Boolean类都具有这种静态工厂方法。

        不仅可以共享非可变对象,还可以共享它们的内部信息。

        非可变对象为其他对象——无论是可变还是非可变的,创建了大量的构造块。

        非可变对象的真正也是唯一的缺点是对每个不同的值要求一个单独的对象。

    • - 组合优于继承
      • 继承是实现代码重用的有力途径,但它不总是完成这项工作的最后的工具。与方法调用不同,继承打破了封装性。子类的特有的功能,依赖于它的超类的实现细节。超类的实现会随着版本而改变,如果出现这样的情况,即使不触动子类的代码,它也会被破坏。

        不去扩展现有的类,而是给类增加一个引用现有类实例的新的私有域,这种设计方法被成为复合(composition),因为现有的类成为了新的类的一部分。

        继承只有当子类确实是超类的“子类型”(subtype)时,才是适合的。换句话说,对两个类A和B,如果“B是A”的关系存在,那么B应该扩展A。在把B扩展A时,问这样一个问题:“任何的B都是A吗?”,如果答案是否定的,那么通常应该把A作为B的一个私有实例,然后暴露更小、更简单的API:A不是B的基本部分,只是它的实现细节。

    • - 设计和文档化继承
      • 类必须提供文档准确地描述重载任一方法的效果。类必须说明它的可重载方法的自用性(self-use):对每个公共的或受保护的方法或构造函数,它的文档信息都必须要表明它在调用哪一个可重载的方法、以什么顺序调用及每一个调用的结果如何影响后面的处理。

        为了允许程序员有效地进行子类化处理而不必承受不必要的痛苦,类必须用认真选择的受保护方法提供它内部实现的钩子(hook)。

        构造函数一定不能调用可重载的方法,无法直接地还是间接地。超类构造函数会在子类构造函数之前运行,所以子类中的重载方法会在子类构造函数运行之前被调用。如果重载方法依赖于由子类构造函数执行的初始化,那么该方法将不会按期望的方式执行。

        例:
        public class Super { //Broken - constructor invokes overridable method
               public Super(){
                       m();
               }  
               public void m() {     
               }
        }  
        下面的子类重载了m,而m被超类唯一的构造函数错误地调用了:
        final class sub extends Super {
               private final Date date;  
               //Blank final, set by constructor
               Sub() {
                       date = new Date();
               }  
               //Overrides Super.m, invoked by the constructor Super()
               public void m() {
                       System.out.println(date);
               }  
               public static void main(String[] args) {
                       Sub s = new Sub();
                       s.m();
               }  
        }  
        它第一次打印出的是null。这是因为方法m被构造函数Super()在构造函数Sub()初始化date域之前被调用。

        clone和readObject方法都不能调用可重载的方法,无论是直接的还是间接的。如果确定要用来继承的类实现Serializable,并且类中有一个readResolve或writeReplace方法,那么必须使readResolve或writeReplace方法成为受保护的而不是私有的。一旦这些方法是私有的,它们就将被子类悄然忽略。

        对那些不是专门设计用于安全地实现子类化并具有文档说明的类,禁止子类化。

        禁止子类化的方法有两种

        • 最容易的方法是将类声明为final的。
        • 另一种是使所有的构造函数私有或者成为包内私有,并用公共的静态工厂取代构造函数。
    • - 接口优于抽象类
      • Java语言为定义允许有多种实现的类型提供了两种机制:接口和抽象类。

        • 两种机制的最明显区别是抽象类允许包含某种方法的实现,而接口不允许。
        • 一个更重要的区别:为实现有抽象类定义的类型,实现的类必须是抽象类的子类。Java只允许单一继承,因此抽象类作为类型的定义受到了极大的限制。

        现有的类可以很容易的被更新以实现新的接口。所有需要做的工作是增加还不存在的方法并在类的声明中增加一个implement语句。

        接口是定义混合类型(mixins)的理想选择。mixin是这样的类型:除了它的“基本类型(primary type)”外,类还可以实现额外的类型,以表明它提供了某些可选的功能。

        接口允许非层次类型框架的构造。对于组织某些事物,类型层次是极好的选择,但有些事物不能清楚地组织成严格的层次。

        接口通过使用封装类方式,能够获得安全、强大的功能。如果使用抽象类定义类型,那么程序员在增加功能是,除了使用继承外别无选择,而且得到的类与封装类相比功能更差、更脆弱。

        尽管接口不允许方法实现,但使用接口定义类型并不妨碍给程序员提供实现上的帮助。可以通过抽象的构架实现类与希望输出的所有重要的接口配合,从而将接口与抽象类的优点组合在一起。

        使用抽象类定义具有多个实现的类型与使用接口相比有一个明显优势:演进抽象类比演进接口更容易。如果在以后的版本重,需要给抽象类增加新方法,那么总可以增加一个包含正确的缺省实现的具体方法。此时所有现存的该抽象类的实现都会具有这个新的方法。

    • - 静态成员优于非静态的
      • 嵌套类(nested class)是一种定义在其他类内部的类。嵌套类应该仅仅为包容它的类而存在。

        - 有四种类型嵌套类:

        • 静态成员类(static member class)
          • 静态成员类是最简单的嵌套类。它最好被看做是普通的类碰巧被声明在其他的类的内部。它对所有封闭类中的成员有访问权限,甚至那些私有成员。静态成员类的一种通常用法是作为公共的辅助类,仅当和它的外部类协作时才有意义。

            如果声明了一个不要求访问封闭实例的成员类,切记要在它的声明里使用static修饰符,把成员类变为静态的。 私有静态成员类的一般用法是用来表示它们的封闭类对象的组件。

        • 非静态成员类(nonstatic member classer)
          • 每一个非静态的成员类与它包含类的封闭实例(enclosing instance)隐式地关联。如果嵌套类的实例可以在它的封闭类实例之外单独存在,那么嵌套类不能成为非静态成员类:创建没有封闭实例的非静态成员类实例是不可能的。非静态成员实例与它的封闭实例之间的关联在前者被创建时即建立,在此之后它不能被修改。

        • 匿名类(anonymous class)
          • 匿名类的行为与静态的或非静态的成员类一样,依赖于它们出现的位置:如果它们出现在非静态的上下文中,则具有封闭实例。匿名类仅在代码中唯一的一点被实例化才能被使用,由于匿名类没有名字,因此仅在被实例化后不需要再被访问的情况下才适合使用。

            过长的匿名类会伤害程序的可读性。
            //Typical use of an anonymous class
            Arrays.sort(args, new Comparator() {
                          public int compare(Object o1,Object o2) {
                                         return ((String)o1).length() - ((String)o2).length();
                          }
            }

            匿名类另一种通常用法是创建过程对象(process object),例如Thread、Runnable或者TimerTask实例。

            第三种常用法是在静态工厂方法内使用。

            第四种用法用在复杂的类型安全枚举类型——要求给每个实例提供单独的子类——的公共静态的final域初始化程序中。

        • 局部类(local class)
          • 在局部变量可以声明的地方都可以声明局部类,它们同样遵守作用域规则,性质跟匿名类一样。
        • 除了第一种,其他的三种类都被称为内部类(inner class)

        四种嵌套类,每种都有自己的用处。如果嵌套类在单一的方法之外可见,或是太长而不合适使用在一个方法内,那么使用成员类。如果成员类实例需要它的封闭类的引用,那么使它成为非静态的;否则为静态的。如果类存在于方法内部,那么如果你仅需要在唯一一个位置创建实例,并且已存在刻化该类的类型,则使它成为匿名类;否则,用局部类。

  • 对象的通用方法
    • - 重载equals时要遵守通用约定
      • - 如果满足下列条件,就不要重载equals
        • 每个类实例本质上是唯一的。

          不关心类是否提供了“逻辑意义的等同”(logical equality)测试。例如java.util.Random本来可以重载equals方法,用以检查两个Random实例是否会产生相同的随机数序列,但设计者不认为客户会需要或想要这个功能。这种情况下,使用从Object继承的equals实现就够了。

          超类已经重载了equals,而从超类继承的行为适合该类。

          类是私有的或包内私有(package-private)的,而且可以确定它的equals方法永远不会被调用。

      • 当类有逻辑上的等同意义而不仅仅是对象意义上的等同,而且超类没有重载equals方法以实现期望的行为,这时才需要重载。
      • - equals方法实现了相等关系(equivalence relation)
        • 自反性(reflective):对于任意的引用值x,x.equals(x)总是返回true。
        • 对称性(symmetric):对于任意的引用值x、y,如果y.equals(x)返回true,x.equals(y)总返回true。
        • 传递性(transitive):对于任意的引用值x、y、z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)总是返回true.
        • 一致性(consistent):对于任意的引用值x、y,如果对象中用于equals比较的信息没有修改,那么对x.equals(y)的多个调用,要么一致为true,要么一致为false。
        • 对于任何非空引用值x,x.equals(null)总是返回false。
      • - 为实现高质量的equals方法,下面提供一些方法
        • 用"=="操作符检查是否参数是对该对象的引用。

          用instanceof操作符检查是否参数是正确的类型。
          public boolean equals(Object o) {
                  if(!(o instanceof SomeClass))
                           return false;
                  ...
          }

          把参数映射到正确的类型。

          对类中每一个“主要的”(significant)域,检查是否参数中的域与对象中的相应的域匹配。

          完成equals方法时,问自己3个问题:它是否是对称的、传递的、一致的。

      • - 实现equals方法应该注意的地方
        • 在重载equal方法时要重载hashCode方法。

          不要使自己聪明过头。把任何的同义形式考虑在比较的范围内一般是糟糕的想法,例如File类不应该与指向同一文件的符号链接进行比较,实际上File类也没有这样做。

          不要设计依赖于不可靠资源的equals方法。

          不要将equals声明中的Object替换为其他类型。程序员编写出形如下面所示的equals方法并不少见,它会让人摸不清头脑:所设计方法为什么不能正确工作:
          public boolean equals(Myclass o) {
                 ...
          }
          问题出在这个方法没有重载(override)参数为Object类型的Object.equals方法。而是过载(overload)了它。这在正常的equals方法中,又提供了一个“强类型”的equals方法。

    • - 重载equals时永远要重载hashCode
      • 一定要在每一个重载了equals的类中重载hashCode方法。不这样做会违背Object.hashCode的一般约定,并导致你的类与所有基于散列的集合一起作用时不能正常工作,这些集合包括HashMap、HashSet和Hashtable。

        不重载hashCode方法违背了java.lang.Object的规范:相等的对象必须有相等的散列码。两个截然不同的实例根据类的equals方法也许逻辑上是等同的,但对于Object类的hashCode方法,它们就是两个对象,仅此而已。因而对象的hashCode方法返回两个看上去是随机的数值,而不是约定中要求的相等的值。

        好的hash函数倾向于为不相等的对象生成不相等的hash码。理想的情况下,hash函数应该把所有不相等的实例的合理集合均一地分布到所有可能的hash值上去。达到理想状态很难,但是下面有一种相对合适的方法

        • 1.保存某个非0常数如17,到名为result的int类型变量中
        • 2.对对象中每个“主要域”f,(每个域由equals方法负责),做下面的工作
          • a.为域计算int型的hash码c
            • i.如果域是boolean型,计算(f?0:1)。

              ii.如果域是byte型、char型、short型或int型,计算(int)f。

              iii.如果域是long型,计算(int)(f^(f>>>32))。

              iv.如果域是float型,计算Float.floattoIntBits(f)。

              v.如果域是double型,计算Double.doubleToLongBits(f),然后如2.a.iii所示,对long型结果进一步处理。

              vi.如果域是对象引用,而且这个类的equals方法又递归地调用了equals方法对域进行比较,那么对这个域递归地调用hashCode方法。如果需要一种更复杂的比较方式,那么先为这个域计算出“范式表示”,然后在该“范式表示”上调用hashCode方法。如果域为null,则返回0。

              vii.如果域是数组,则把每个元素作为分离的域对待。即递归地使用这些规则,为每个“主要元素”计算hash码。然后用2.b所示方法复合这些值。

          • b.把步骤a中计算出的hash码c按如下方式与result复合: result = 37*result + c;
        • 3.返回result。
        • 4.完成hashCode方法后,测试是否相同的实例会有相同的hash码,如果不是,找到原因,修正问题。
    • - 永远要重载toString
      • 为类提供一个好的toString实现可以使类使用起来更加赏心悦目。
      • 实际使用中,toString方法应该返回包含在对象中的所有令人感兴趣的信息。
      • 无论是否指明格式,都要把你的意图清楚地文档化出来。
    • - 谨慎的重载clone
      • 为了实现Cloneable接口,会产生一种古怪的机制:不通过调用构造函数却创建了一个对象。
      • 实现对像拷贝的精巧的方法是提供一个拷贝构造函数(copy constructor)。拷贝构造函数及它的静态工厂变形与Cloneable/clone方法相比有很多好处
        • 它们不依赖于那种有风险的蹩脚的对象创建机制;
        • 不需要遵守由糟糕的文档规范的规约;
        • 不会与final域的正常使用产生冲突;
        • 不要求客户不必要地捕获被检查的异常;
        • 给客户提供了一种类型化的对象。
  • 方法
    • - 检查参数的有效性
      • 如果方法没有对参数做检查,会出现几种情形。

        • 方法可能在执行中间失败退出并给出含糊的异常。
        • 更差的是,方法能正常返回,并计算了错误结果。

        对那些不被方法使用但会被保存以供使用的参数,检查有效性尤为重要。

        一种重要的例外是有效性检查开销高,或者不切实际,而且这种有效性检查在计算的过程中会被隐式地处理的情形。

        总的来说,每次在设计方法或设计构造函数时,要考虑它们的参数有什么限制。要在文档中注释出这些限制,并在方法体的开头通过显示的检查,对它们进行强化。养成这样的习惯是重要的,有效性检查所需要的不多的工作会从它的好处中得到补偿。

    • - 使用保护性拷贝
      • 必须在客户会使用一切手段破坏类的约束的前提下,保护性地设计程序。

        下面的类的目的是表示非可变的时间周期:
        //Broken "immutable" time period class
        public final class Period {
                 private final Date start;
                 private final Date end;

                 public Period(Date start, Date end) {
                           if(start.compareTo(end) >0 )
                                     throw new IllegalArgumentException(start+" after "+end);
                           this.start = start;
                           this.end = end;
                 }

                 public Date start() {
                           return start;
                 }
                 public Date end() {
                           return end;
                 }

                 ...
        }
        乍看上去,这个类是非可变的,并通过执行周期的起始点不会落在周期终止点之后的判断,增强了类的约束。然而,如果Date是可变的,这种约束很容易被违背:
        // Attack the internals of a Period instance
        Date start = new Date();
        Date end = new Date();
        Period p = new Period(start,end);
        end.setYear(78);  //Modifies internals of p!

        为了保护Period实例的内部细节免于这种攻击,对构造函数的每一个可变参数使用保护性拷贝是必要的。

        使用副本代替初始值作为Period实例的组件:
        //Repaired constructor - make defensice copies of parameters
        public Period(Date start, Date end){
                 this.start = new Date(start.getTime());
                 this.end = new Date(end.getTime());
                 if(this.start.compareTo(this.end)>0)
                           throw new IllegalArgumentException(start+" after "+end):
        }

        保护性拷贝要在参数的有效性检查的前面,并且有效性检测要在副本而不是初值上执行。

        尽管替代构造函数成功防止了前面的攻击,但改变一个Period实例仍然是可能的,因为访问器对它的可变的内部细节提供了访问能力。
        //Second attack on the internals of a Period instance
        Date start = new Date();
        Date end = new Date();
        Period p = new Period(start,end);
        p.end().setYear(78);          //Modifies internals of p!

        为了防止第二种攻击,简单地修改访问器,返回可变内部域的保护性拷贝即可:
        //Repair accessors - make defensive copies of internal fields
        public Date start() {
                 return (Date) start.clone();
        }
        public Date end() {
                 return (Date) end.clone();
        }

        只要可能,就应该使用非可变对象作为对象的组件,以便不再关心保护性拷贝问题。

    • - 认真设计方法签名
      • 认真地给方法选名字。名字要永远遵守标准的命名惯例。
      • 不要过于追求提供便利的方法。对接口这一点是千真万确的,接口中方法太多会使实现程序和用户使用时变得复杂。
      • 避免长参数列表。三个参数的使用就该作为最大值。类型相同的长参数序列尤其有害。
        • 有两种技术可以大幅缩短常参数列表。一种技术是将一个方法分成多个方法实现,其中每个方法仅需要参数类表的一个子集。
        • 第二种技术是创建助手类,用来保持参数集合。
      • 在参数类型的使用上,接口优于类。
      • 谨慎地使用函数对象。
    • - 谨慎地使用过载
      • 下面是一个意图良好的尝试,按照是否是集、列表还是其他种类的集合把集合分类:
        //Broken - incorrect user of overloading!
        public class CollectionClassifier {
                 public static String classify(Set s) {
                           return "Set";
                 }
                 public static String classify(List l) {
                           return "List";
                 }
                 public static String classify(Collection c) {
                           return "Unkown Collection";
                 }
                 public static void main(String[] args) {
                           Collection[] tests = new Collection[] {
                                     new HashSet(),
                                     new ArrayList(),
                                     new HaspMap().values()
                           };

                           for(int i=0;i<tests.length;i++)
                                     System.out.println(classify(tests[i]));
                 }
        }
        也许认为这个程序会依次打出Set、List和Unkown Collection,但实际是打印3次Unkown Collection。这是因为classify方法被过载了,选择使用哪个调用的决定是在编译期做出的。

        改正的方法是用一个显式instanceof测试实例的方法代替classify中的三个过载方法:
        public static String classify(Collection c) {
                 return (c instanceof Set? "Set" :(c instanceof List ? "List" : "Unkown Collection"));
        }

        一种安全、保守的策略是永远不要导出两个具有相同数目参数的过载方法。

    • - 返回0长度的数组而不是null
      • 下面的方法比较常见:
        private List CheesesInStock = ...;

        public Cheese[] getCheeses() {
                 if(cheesesInStock.size() == 0)
                           return null;
                 ...
        }
        没有理由需要对无奶酪可买这种情况做特殊的处理。着需要客户方提供额外代码处理null的返回值,例如:
        Cheese[] cheeses = shop.getCheeses();
        if (cheeses != null && Array.asList(shop.getCheeses()).contains(Cheese.STILTON))
                 System.out.println("Jolly good, just the thing.");
        而不是:
        if (Array.asList(shop.getCheeses()).contains(Cheese.STILTON))
                 System.out.println("Jolly good, just the thing.");

        几乎每次返回null而不是返回0长度数组的方法时,都需要这种多余地处理。返回null是易出错的,因为写代码的程序员可能会忘记设计处理返回null的特殊情形的代码。

        正确的做法是:
        private List cheeseInStock = ...;
        private final static Cheese[] NULL_CHEESE_ARRAY = new Cheese[0];

        public Cheese[] getCheeses() {
               return (Cheese[]) cheesesInStock.toArray(NULL_CHEESE_ARRAY);
        }

  • - 通用编程
    • - 最小化局部变量作用域
      • 通过最小化局部变量的作用域,可以增加代码的可读性和可维护性,减少出错可能。
      • 最小化局部变量的最有效的方式是在它第一次被使用时声明。
      • 几乎每一个局部变量声明都应该包含一个初始化器(initializer)。
      • 最小化局部变量的最后一个技术是使方法小而集中。
    • 需要确切答案时,不要使用float或double类型
      • float和double类型特别不适合货币计算。
      • 假设手中有$1.03,花掉0.42后还剩多少钱呢?
        System.out.println(1.03 - .42);
        不幸的是,它会打印0.6100000000000001。
      • 解决这个问题的正确方法是使用BigDecimal、int和long类型进行货币计算。
      • 使用BigDecimal有两个缺点。
        • 它不如使用算术类型方便。
        • 速度慢。
    • - 尽量避免使用串
      • 串是值类型的糟糕的替代物。
      • 串是可枚举类型的糟糕的替代物。
      • 串是集合类型的糟糕的替代物
      • 串是capabilities(能力表)的糟糕的替代物
    • - 了解串并置的性能
      • 为连接n个串重复地使用串合并操作符需要n的二次方时间。
      • 为了获得可接受的性能,可以使用StringBuffer代替String。
    • 通过接口访问对象
      • 如果存在合适的接口类型,那么参数、返回值、变量和域应该用接口类型声明。

        应该养成下面这样的程序习惯:
        //Good - uses interface as type
        List subscribers = new Vector();

        而不要这样做:
        //Bad - uses class as type!
        Vector subscribers = new Vector();

        如果养成了使用接口作为类型的习惯,程序就会有更好的扩展性。当希望转换实现时,需要做的全部工作就是改变构造函数中类的名字。

    • 谨慎地做优化
      • 人们通常都把计算机的罪归咎于效率问题(甚至是不必要的获得的效率),而不去怀疑任何其他的原因——甚至包括盲目地做傻事。——William A.Wulf
      • 不要计较微小效率的得失,在97%的情况下,不成熟的优化是一切罪恶的根源。 ——Donald E.Knuth
      • 做优化时,要遵循两条原则:
        原则1  不要做优化
        原则2(仅对专家)  还是不要做优化——也可以这么说:在绝对清楚的、未经优化的方案之前,不要做优化。 
        ——M.A.Jackson
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Java知识体系详解
java 编程思想 笔记
C#构造函数
静态方法跟实例方法有什么区别
多用静态工厂代替构造器
【一分钟知识】静态变量和实例变量、构造器
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服