java经典问题总结(4)

简介: java经典问题总结(4)

三十二、用EnumSet代替位域:


     下面的代码给出了位域的实现方式:


public class Text {

        public static final int STYLE_BOLD = 1 << 0;


        public static final int STYLE_ITALIC = 1 << 1;


        public static final int STYLE_UNDERLINE = 1 << 2;


        public static final int STYLE_STRIKETHROUGH = 1 << 3;


        public void applyStyles(int styles) { ... }


    }


这种表示法让你用OR位运算将几个常量合并到一个集合中,使用方式如下:


     text.applyStyles(Text.STYLE_BOLD | Text.STYLE_ITALIC);


     Java中提供了EnumSet类,该类继承自Set接口,同时也提供了丰富的功能,类型安全性,以及可以从任何其他Set实现中得到的互用性。但是在内部具体实现上,没有EnumSet内容都表示为位矢量。如果底层的枚举类型有64个或者更少的元素,整个EnumSet就用单个long来表示,因此他的性能也是可以比肩位域的。与此同时,他提供了大量的操作方法,其实现也是基于位操作的,但是相比于手工位操作,由于EnumSet替我们承担了这部分的开发,从而也避免了一些容易出现的低级错误,代码的美观程度也会有所提升,见如下修改的代码:


public class Text {

        public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }


        public void applyStyles(Set<Style> styles) { ... }


    }


 新的使用方式如下:


     text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC));


     需要说明的是,EnumSet提供了丰富的静态工厂来轻松创建集合。



三十三、用EnumMap代替序数索引:


     前面的条目已经给出了尽量不要直接使用枚举的ordinal()方法的原因,这里就不在做过多的赘述了。在这个条目中,只是再一次给出了ordinal()的典型用法,与此同时也再一次提供了一个更为合理的解决方案用于替换ordinal()方法,从而进一步证明我们在编码过程中应该尽可能减少对枚举中ordinal()函数的依赖。见如下代码:


public class Herb {

        public enum Type { ANNUAL, PERENNIAL, BIENNIAL }


        private final String name;


        private final Type type;


        Herb(String name, Type type) {

            this.name = name;


            this.type = type;


        }


        @Override public String toString() {

            return name;


        }


    }


    public static void main(String[] args) {

        Herb[] garden = getAllHerbsFromGarden();


        Set<Herb> herbsByType = (Set<Herb>[])new Set[Herb.Type.values().length];


        for (int i = 0; i < herbsByType.length; ++i) {

            herbsByType[i] = new HashSet<Herb>();


        }


        for (Herb h : garden) {

            herbsByType[h.type.ordinal()].add(h);


        }


        for (int i = 0; i < herbsByType.length; ++i) {

            System.out.printf("%s: %s%n",Herb.Type.values()[i],herbByType[i]);


        }


    }


这里我需要简单描述一下上面代码的应用场景:在一个花园里面有很多的植物,它们被分成3类,分别是一年生(ANNUAL)、多年生(PERENNIAL)和两年生(BIENNIAL),正好对应着Herb.Type中的枚举值。现在我们需要做的是遍历花园中的每一个植物,并将这些植物分为3类,最后再将分类后的植物分类打印出来。下面将提供另外一种方法,即通过EnumMap来实现和上面代码相同的逻辑:


public static void main(String[] args) {

        Herb[] garden = getAllHerbsFromGarden();


        Map<Herb.Type,Set<Herb>> herbsByType =


            new EnumMap<Herb.Type,Set<Herb>>(Herb.Type.class);


        for (Herb.Type t : Herb.Type.values()) {

            herbssByType.put(t,new HashSet<Herb>());


        }


        for (Herb h : garden) {

            herbsByType.get(h.type).add(h);


        }


        System.out.println(herbsByType);


    }


和之前的代码相比,这段代码更加清晰,也更加安全,运行效率方面也是可以与使用ordinal()的方式想媲美的。



三十四、用接口模拟可伸缩的枚举:


     枚举是无法被扩展(extends)的,这是一个无法回避的事实。如果我们的操作中存在一些基础操作,如计算器中的基本运算类型(加减乘除)。然而对于有些用户来讲,他们也可以使用更高级的操作,如求幂和求余等。针对这样的需求,该条目提出了一种非常巧妙的设计方案,即利用枚举可以实现接口这一事实,我们将API的参数定义为该接口,而不是具体的枚举类型,见如下代码:


public interface Operation {

        double apply(double x,double y);


    }


    public enum BasicOperation implements Operation {

        PLUS("+") {

            public double apply(double x,double y) { return x + y; }


        },


        MINUS("-") {

            public double apply(double x,double y) { return x - y; }


        },


        TIMES("*") {

            public double apply(double x,double y) { return x * y; }


        },


        DIVIDE("/") {

            public double apply(double x,double y) { return x / y; }


        };


        private final String symbol;


        BasicOperation(String symbol) {

            this.symbol = symbol;


        }


        @Override public String toString() {

            return symbol;


        }


    }


    public enum ExtendedOperation implements Operation {

        EXP("^") {

            public double apply(double x,double y) {

                return Math.pow(x,y);


            }


        },


        REMAINDER("%") {

            public double apply(double x,double y) {

                return x % y;


            }


        };


        private final String symbol;


        ExtendedOperation(String symbol) {

            this.symbol = symbol;


        }


        @Override public String toString() {

            return symbol;


        }


    }


 通过以上的代码可以看出,在任何可以使用BasicOperation的地方,我们也同样可以使用ExtendedOperation,只要我们的API是基于Operation接口的,而非BasicOperation或ExtendedOperation。下面为以上代码的应用示例:


public static void main(String[] args) {

        double x = Double.parseDouble(args[0]);


        double y = Double.parseDouble(args[1]);


        test(ExtendedOperation.class,x,y);


    }


    private static <T extends Enum<T> & Operation> void test(


        Class<T> opSet,double x,double y) {

        for (Operation op : opSet.getEnumConstants()) {

            System.out.printf("%f %s %f = %f%n",x,op,y,op.apply(x,y));


        }


    }


  注意,参数Class<T> opSet将推演出类型参数的实际类型,即上例中的ExtendedOperation。与此同时,test函数的参数类型限定确保了类型参数既是枚举类型又是Operation的实现类,这正是遍历元素和执行每个元素相关联的操作所必须的。


三十五、注解优先于命名模式

目前使用很少,没有什么深刻体会,理解不透



三十六、坚持使用Override注解

在你想要覆盖超类声明的每个方法中声明使用Override注解,这样编译器就会帮助你发现是否正确覆盖了一个方法。


例外:在具体的类中,不必标注你确信覆盖了的抽象的方法;实现接口的类中,也不必标注出你想要哪些方法来覆盖接口方法,这两种情况编译器都会帮助你提醒,没有覆盖,抛出错误。当然标注了也没有什么坏处。



但是在抽象类和接口覆盖超类或者超接口的时候,坚持使用Override注解



三十七、记接口定义类型

不知所云


三十八、检查参数的有效性:


     绝大多数方法和构造器对于传递给它们的参数值都会有些限制。比如,索引值必须大于等于0,且不能超过其最大值,对象不能为null等。这样就可以在导致错误的源头将错误捕获,从而避免了该错误被延续到今后的某一时刻再被引发,这样就是加大了错误追查的难度。就如同编译期能够报出的错误总比在运行时才发现要更好一些。事实上,我们不仅仅需要在函数的内部开始出进行这些通用的参数有效性检查,还需要在函数的文档中给予明确的说明,如在参数非法的情况下,会抛出那些异常,或导致函数返回哪些错误值等,见如下代码示例:


/**


     * Returns a BigInteger whose value is(this mod m). This method


     * differs from the remainder method in that it always returns a


     * non-negative BigInteger.


     * @param m the modulus, which must be positive.


     * @return this mod m.


     * @throws ArithmeticException if m is less than or equal to 0.


*/


     public BigInteger mod(BigInteger m) {

         if (m.signum() <= 0)


             throw new ArithmeticException("Modulus <= 0: " + m);


         ... //Do the computation.


     }


  是不是我们为所有的方法均需要做出这样的有效性检查呢?对于未被导出的方法,如包方法等,你可以控制这个方法将在哪些情况下被调用,因此这时可以使用断言来帮助进行参数的有效性检查,如:


private static void sort(long a[],int offset,int length) {

         assert(a != null);


         assert(offset >= 0 && offset <= a.length);


         assert(length >= 0 && length <= a.length - offset);


         ... //Do the computation


     }


 和通用的检查方式不同,断言在其条件为真时,无论外部包得客户端如何使用它。断言都将抛出AssertionError。它们之间的另一个差异在于如果断言没有起到作用,即-ea命令行参数没有传递给java解释器,断言将不会有任何开销,这样我们就可以在调试期间加入该命令行参数,在发布时去掉该命令行选项,而我们的代码则不需要任何改动。


      需要强调的是,对于有些函数的参数,其在当前函数内并不使用,而是留给该类其他函数内部使用的,比较明显的就是类的构造函数,构造函数中的很多参数都不一样用于构造器内,只是在构造的时候进行有些赋值操作,而这些参数的真正使用者是该类的其他函数,对于这种情况,我们就更需要在构造的时候进行参数的有效性检查,否则一旦将该问题释放到域函数的时候,再追查该问题的根源,将不得不付出更大的代价和更多的调试时间。


      对该条目的说法确实存在着一种例外情况,在有些情况下有效性检查工作的开销是非常大的,或者根本不切实际,因为这些检查已经隐含在计算过程中完成了,如Collections.sort(List),容器中对象的所有比较操作均在该函数执行时完成,一旦比较操作失败将会抛出ClassCastException异常。因此对于sort来讲,如果我们提前做出有效性检查将是毫无意义的。


 


三十九、必要时进行保护性拷贝:


     如果你的对象没有做很好的隔离,那么对于调用者而言,则有机会破坏该对象的内部约束条件,因此我们需要保护性的设计程序。该破坏行为一般由两种情况引起,首先就是恶心的破坏,再有就是调用者无意识的误用,这两种条件下均有可能给你的类带来一定的破坏性,见如下代码:


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类本身是可变了,因此很容易违反这个约束,见如下代码:


public void testPeriod() {

        Date start = new Date();


        Date end = new Date();


        Period p = new Period(start,end);


        end.setYear(78);  //该修改将直接影响Period内部的end对象。


    }


为了避免这样的攻击,我们需要对Period的构造函数进行相应的修改,即对每个可变参数进行保护性拷贝。


public Period(Date start,Date end) {

        this.start = new Date(start.getTime());


        this.end = new Date(end.getTime());


        if (start.compareTo(end) > 0) {

            throw new IllegalArgumentException(start + "After " + end);


    }


需要说明的是,保护性拷贝是在坚持参数有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始对象的。这主要是为了避免在this.start = new Date(start.getTime())到if (start.compareTo(end) > 0)这个时间窗口内,参数start和end可能会被其他线程修改。


     现在构造函数已经安全了,后面我们需要用同样的方式继续修改另外两个对象访问函数。


public Date start() {

        return new Date(start.getTime());


    }


    public Date end() {

        return new Date(end.getTime());


    }


经过这一番修改之后,Period成为了不可变类,其内部的“周期的起始时间不能落后于结束时间”约束条件也不会再被破坏。


     参数的保护性拷贝并不仅仅针对不可变类。每当编写方法或者构造器时,如果它要允许客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象是否有可能是可变的。如果是,就要考虑你的类是否能够容忍对象进入数据结构之后发生变化。如果答案是否定的,就必须对该对象进行保护性拷贝,并且让拷贝之后的对象而不是原始对象进入到数据结构中。例如,如果你正在考虑使用有客户提供的对象引用作为内部Set实例的元素,或者作为内部Map实例的键(Key),就应该意识到,如果这个对象在插入之后再被修改,Set或者Map的约束条件就会遭到破坏。


 


四十一、谨慎重载:


     见下面一个函数重载的例子:


public class CollectionClassfier {

        public static String classify(Set<?> s) {

            return "Set";


        }


        public static String classify(List<?> l) {

            return "List";


        }


        public static String classify(Collection<?> c) {

            return "Unknown collection";


        }


        public static void main(String[] args) {

            Collection<?>[] collections = {

                new HashSet<String>(),


                new ArrayList<BigInteger>(),


                new HashMap<String,String>().values()


            };


            for (Collection<?> c : collections)


                System.out.println(classify(c));


        }


    }


这里你可能会期望程序打印出


     //Set


     //List


     //Unknown Collection


     然而实际上却不是这样,输出的结果是3个"Unknown Collection"。为什么会是这样呢?因为函数重载后,需要调用哪个函数是在编译期决定的,这不同于多态的运行时动态绑定。针对此种情形,该条目给出了一个修正的方法,如下:


public static String classify(Collection<?> c) {

        return c instanceof Set ? "Set" : c instanceof List


            ? "List" : "Unknown Collection";


    }


和override不同,重载机制不会像override那样规范,并且每次都能得到期望的结果。因此在使用时需要非常谨慎,否则一旦出了问题,就会需要更多的时间去调试。该条目给出以下几种尽量不要使用重载的情形:


     1.    函数的参数中包含可变参数;


     2.    当函数参数数目相同时,你无法准确的确定哪一个方法该被调用时;


     3.    在Java 1.5 之后,需要对自动装箱机制保持警惕。


     我们先简单说一下第二种情形。比如两个重载函数均有一个参数,其中一个是整型,另一个是Collection<?>,对于这种情况,int和Collection<?>之间没有任何关联,也无法在两者之间做任何的类型转换,否则将会抛出ClassCastException的异常,因此对于这种函数重载,我们是可以准确确定的。反之,如果两个参数分别是int和short,他们之间的差异就不是这么明显。


     对于第三种情形,该条目给出了一个非常典型的用例代码,如下:


public class SetList {

        public static void main(String[] args) {

            Set<Integer> s = new TreeSet<Integer>();


            List<Integer> l = new ArrayList<Integer>();


            for (int i = -3; i < 3; ++i) {

                s.add(i);


                l.add(i);


            }


            for (int i = 0; i < 3; ++i) {

                s.remove(i);


                l.remove(i);


            }


            System.out.println(s + " " + l);


        }


    }


 在执行该段代码前,我们期望的结果是Set和List集合中大于等于的元素均被移除出容器,然而在执行后却发现事实并非如此,其结果为:


     [-3,-2,-1] [-2,0,2]


     这个结果和我们的期望还是有很大差异的,为什么Set中的元素是正确的,而List则不是,是什么导致了这一结果的发生呢?下面给出具体的解释:


     1. s.remove(i)调用的是Set中的remove(E),这里的E表示Integer,Java的编译器会将i自动装箱到Integer中,因此我们得到了想要的结果。


     2. l.remove(i)实际调用的是List中的remove(int index)重载方法,而该方法的行为是删除集合中指定索引的元素。这里分别对应第0个,第1个和第2个。


     为了解决这个问题,我们需要让List明确的知道,我们需要调用的是remove(E)重载函数,而不是其他的,这样我们就需要对原有代码进行如下的修改:


public class SetList {

        public static void main(String[] args) {

            Set<Integer> s = new TreeSet<Integer>();


            List<Integer> l = new ArrayList<Integer>();


            for (int i = -3; i < 3; ++i) {

                s.add(i);


                l.add(i);


            }


            for (int i = 0; i < 3; ++i) {

                s.remove(i);


                l.remove((Integer)i); //or remove(Integer.valueOf(i));


            }


            System.out.println(s + " " + l);


        }


    }


该条目还介绍了一种实现函数重载,同时又尽可能避免上述错误发生的方式。即其中的一个重载函数,在其内部通过一定的转换逻辑转换之后,再通过转换后的参数类型调用其他的重载函数,从而确保即便使用者在使用过程中出现重载误用的情况,也因两者可以得到相同的结果而规避了潜在错误的发生。



四十二、慎用可变参数:


     可变参数方法接受0个或者多个指定类型的参数。可变参数机制通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法,如:


static int sum(int...args) {

        int sum = 0;


        for (int arg : args)


            sum += arg;


        retrun sum;


    }


上面的方法可以正常的工作,但是在有的时候,我们可能需要至少一个或者多个某种类型参数的方法,如:


static int min(int...args) {

        if (args.length == 0)


            throw new IllegalArgumentException("Too few arguments.");


        int min = args[0];


        for (int i = 0; i < args.length; ++i) {

            if (args[i] < min)


                min = args[i];


        }


        return min;


    }


 对于上面的代码主要存在两个问题,一是如果调用者没有传递参数是,该函数将会在运行时抛出异常,而不是在编译期报错。另一个问题是这样的写法也是非常不美观的,函数内部必须做参数的数量验证,不仅如此,这也影响了效率。将编译期可以完成的事情推到了运行期。下面提供了一种较好的修改方式,如下:


static int min(int firstArg,int...remainingArgs) {

        int min = firstArgs;


        for (int arg : remainingArgs) {

            if (arg < min)


                min = arg;


        }


        return min;


    }


  由此可见,当你真正需要让一个方法带有不定数量的参数时,可变参数就非常有效。


     有的时候在重视性能的情况下,使用可变参数机制要特别小心。可变参数方法的每次调用都会导致进行一次数组分配和初始化。如果确定确实无法承受这一成本,但又需要可变参数的灵活性,还有一种模式可以弥补这一不足。假设确定对某个方法95%的调用会有3个或者更少的参数,就声明该方法的5个重载,每个重载方法带有0个至3个普通参数,当参数的数目超过3个时,就使用一个可变参数方法:


public void foo() {}


    public void foo(int a1) {}


    public void foo(int a1,int a2) {}


    public void foo(int a1,int a2,int a3) {}


    public void foo(int a1,int a2,int a3,int...rest) {}


 所有调用中只有5%参数数量超过3个的调用需要创建数组。就像大多数的性能优化一样,这种方法通常不恰当,但是一旦真正需要它时,还是非常有用处的。


 


四十三、返回零长度的数组或者集合,而不是null:

     见如下代码:


public class CheesesShop {

        private final List<Cheese> cheesesInStock = new List<Cheese>();


        public Cheese[] getCheeses() {

            if (cheesesInStock.size() == 0)


                return null;


            return cheeseInStock.toArray(null);


        }


    }


    从以上代码可以看出,当没有Cheese的时候,getCheeses()函数返回一种特例情况null。这样做的结果会使所有的调用代码在使用前均需对返回值数组做null的判断,如下:


public void testGetCheeses(CheesesShop shop) {

        Cheese[] cheeses = shop.getCheeses();


        if (cheese != null && Array.asList(cheeses).contains(Cheese.STILTON))


            System.out.println("Jolly good, just the thing.");


    }


对于一个返回null而不是零长度数组或者集合的方法,几乎每次用到该方法时都需要这种曲折的处理方式。很显然,这样是比较容易出错的。如果我们使getCheeses()函数在没有Cheese的时候不再返回null,而是返回一个零长度的数组,那么我的调用代码将会变得更加简洁,如下:


public void testGetCheeses2(CheesesShop shop) {

        if (Array.asList(shop.getCheeses()).contains(Cheese.STILTON))


            System.out.println("Jolly good, just the thing.");


    }


   相比于数组,集合亦是如此。


四十四、为所有到处的API元素编写文档注释

如果想要一个API真正可用,就必须为其编写文档。javadoc利用特殊格式的稳定注释(documentation comment),根据源代码自动产生API文档。


详细的规范可以参考:Sun的 How to Write Doc Comments



为了正确地编写API文档,必须在每个被到处的类,接口,构造器,方法和域声明之前增加一个文档注释。如果类是序列化的,也应该对它的序列化形式编写文档。



注意事项:


1. @param、@return、@throws标签后面的短语或者子句都不用句点来结束。


2. 使用html标签会被转换成HTML


3. 使用代码片段放在{@code}中


4. 特殊字符文档,比如小于号,放在{@literal}中


5. 文档第一句话成注释所属元素的概要描述,因此要注意句点的使用


6. 方法和构造器,概要最好是完整的动词短语,而类,接口和域,应该是名词短语


7. 关于泛型,枚举和注解(后两者体验不深 )


1. 为泛型或者方法编写文档,确保说明所以的类型参数


2. 枚举,说明常量


3. 注解,确保说明所有成员已经类型本身


简而言之:要为API编写文档,文档注释是最好的最有效的途径。


相关文章
|
存储 消息中间件 缓存
四万字爆肝总结java多线程所有知识点(史上最全总结)
全文从多线程的实现方式、线程的状态、线程的方法、线程的同步、线程的通讯、等角度对多线程的基础知识进行总结
926 1
四万字爆肝总结java多线程所有知识点(史上最全总结)
|
存储 缓存 NoSQL
新鲜出炉java后端高频面经总结-持续更新中(万字长文,助君青云)(下)
新鲜出炉java后端高频面经总结-持续更新中(万字长文,助君青云)(下)
436 0
新鲜出炉java后端高频面经总结-持续更新中(万字长文,助君青云)(下)
|
Java API
Java中常用API总结(5)—— Object类中的深克隆和浅克隆
不管对象内部的属性是基本数据类型还是引用数据类型,都完全拷贝过来;基本数据类型拷贝过来的是具体的数据,引用数据类型拷贝过来的是地址值。在Object类中默认的克隆方式就是浅克隆
308 0
Java中常用API总结(5)—— Object类中的深克隆和浅克隆
|
Java API
Java中常用API总结(4)—— Object类(含实例解读和源码阅读)
Object类所在包是java.lang包,Object 是类层次结构的根,每个类都可以将 Object 作为超类。所有类都直接或者间接的继承自该类;换句话说,该类所具备的方法,其他所有类都继承了。
309 0
Java中常用API总结(4)—— Object类(含实例解读和源码阅读)
|
Java API
Java中常用API总结(3)—— Runtime类(含实例解读)
Runtime类所在包为java.lang包,因此在使用的时候不需要进行导包;并且Runtime类被public修饰了,因此该类是可以被继承的
346 0
Java中常用API总结(3)—— Runtime类(含实例解读)
|
Java API
Java中常用API总结(2)—— System类(含实例解读)
System类所在包为java.lang包,因此在使用的时候不需要进行导包;并且System类被final修饰了,因此该类是不能被继承的
444 0
Java中常用API总结(2)—— System类(含实例解读)
|
Java API
Java中常用API总结(1)—— Math类(含底层源码阅读)
Java中常用API总结(1)—— Math类(含底层源码阅读)
243 0
Java中常用API总结(1)—— Math类(含底层源码阅读)
|
Java
Java面向对象11——匿名内部类知识点总结
Java面向对象11——匿名内部类知识点总结
291 0
Java面向对象11——匿名内部类知识点总结
|
Java
Java面向对象10——内部类知识点总结
Java面向对象10——内部类知识点总结
221 0
Java面向对象10——内部类知识点总结