Effective Java学习笔记二

来源:互联网 发布:java socket包 编辑:程序博客网 时间:2024/05/06 01:11
二十六.优先考虑泛型
由于E是不可具体化的,所以下面两种写法均是错的
elements = new E[DEFAULT_INITIAL_CAPACITY];
elements = (E[])new Object(DEFAULT_INITIAL_CAPACITY];
java并不是生来就支持列表的,因此有泛型为ArrayList,则必须在数组上实现
在容器中使用Stack<int>或者Stack<double>会产生编译错误,因为里面用的是Object数组,这也是java泛型系统根本的局限性
E extends Object进行限制,表示E是Object的子类型(Object本身也是自身的子类型)

二十七.优先考虑泛型方法
   public static <E> Set<E> union(Set<E> s1, Set<E> s2)
前面的<E>修饰声明一个类型参数
提醒:在调用泛型构造器的时候,要明确传递类型参数的值可能有点麻烦
一般将工具类的方法声明为泛型的,便于广泛的使用
public static <T extends Comparable<T>> T max(List<T> list) {...}
先声明类型限制<T extends Comparable<T>>, 可以让T中每个元素用使用comparaTo方法

二十八.利用有限制通配符来提升API的灵活度
eg:public void push(E e)
  public void pushAll(Iterable<E> src)
  我们可以使用push方法将Integer对象添加到Stack<Number>中,但是如果使用pushAll添加Stack<Integer>到Stack<Number>中则会报错
导致上面的原因是因为参数类型是不可协变的。
解决上面问题的一种方法就是使用有限制的通配符类型
上面pushAll的输入参数不应该是Iterable<E>类型,而应该是Iterable<? extends E>类型
与pushAll对应的应有一个popAll方法,将集合中的元素都弹出,然后添加到指定的集合中去
public void popAll(Collection<E> dst>)
这里也会出现将Stack<Number>中的元素添加到Stack<Object>集合中的行为,这本应是合法的,但是在这里会报错
这里应该修改popAll的参数类型,不应是E的集合,而应该是E的某种超类的集合
为了获得最大限度的灵活度,要在表示生产者或者消费者的输入参数上使用通配符类型
如果参数类型表示一个T生产者,就是用<? extends T>,如果它表示一个T消费者,就使用<? super T>
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2);
如果你使用Set<Number> numbers = union(Set<Integer>, Set<Double>)
上面的代码会报错,如果编译器不能推断你希望它拥有的类型,可能通过一个显示的类型参数来告诉它要使用哪种类型
Set<Numbe> numbers = Union.<Number>union(Set<Integer>, Set<Double>); //正确方式
记住所有的comparable和comparator都是消费者

二十九.优先考虑类型安全的异构容器
从java1.5开始,Class 已经被泛型化了,类的泛型从字面上来看不再只是简单的Class,而是Class<T>

三十.用enum代替int常量
int枚举模式的缺点:
如果传错值的话,编译器不会出现任何的警告
int枚举是编译时常量,被编译到使用它们的客户端中,如果与枚举常量关联的int发生了变化,客户端就必须重新编译
int枚举常量翻译成可打印的字符串,并没有很遍历的方法
String枚举模式的缺点:
可能会导致性能问题,因为它依赖于字符串的比较操作
会将字符串常量硬编码到客户端代码中,可能会存在书写错误。
枚举提供了编译时的类型安全,如果声明了一个参数的类型为Apple,就可以保证,被传到该参数上的任何非null都是Apple类型
在枚举中因为是相同的对象始终只会创建一个,所以可以使用==来进行不同枚举类型的比较
同样,枚举类型都有自己的命名空间,你可以增加或者重新排列枚举类型中的常量,而无需重新编译它的客户端代码。
还可以通过toString方法将枚举对象转成可打印的字符串
为了将数据与枚举常量关联起来,得声明实例域,并编写一个带有数据并将数据保存在域中的构造器
有时,你需要将本质上不同的行为与每个常量关联起来,有个很好的解决方法,在枚举类型中声明一个抽象的apply方法,
并在特定于常量的类主体中,用具体的方法覆盖每个常量的抽象apply方法
枚举类有一个自动产生的valueOf方法,它将常量的名字转变成常量本身。如果在枚举类型中覆盖toString,要考虑编写一个fromString方法
将定制的字符串表示法变回相应的枚举
特定于常量的方法实现有一个美中不足的地方,它们使得在枚举常量中共享代码变得更加困难
使用枚举的时机,每当需要一组固定常量的时候,eg:行星,一周的天数以及棋子的数目等等

三十一.用实例域代替序数
永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中


三十二.用EnumSet代替域
位域表示法也允许利用位操作,有效地执行像union和intersection这样的集合操作
EnumSet类可以有效地表示从单个枚举类型中提取的多个值的多个集合,当元素个数小于等于64个的时候,EnumSet就是用单个long来进行表示的

三十三.用EnumMap代替序数索引

三十四.用接口模拟可伸缩的枚举
用opcode操作码的例子来说明可伸缩的枚举类型
利用枚举类型来实现这种效果,由于枚举类型可以通过给操作码类型和枚举定义接口,来实现任意接口
eg:
public interface Operation {
double apply(double x, double y);
}
public enum ExtendedOperation implements Operation
然后根据枚举值的不同进行相应的实现
虽然无法编写可扩展的枚举类型,却可以通过编写接口以及实现该接口的基础枚举类型,对它进行模拟

三十五.注解优于命名模式
这点体现的最为明显的便是JUNIT
在以前的版本中,junit测试框架要求用户一定要用test作为测试方法名称的开头.
缺点:
①首先文字拼写错误会导致失败,而没有任何提示
②无法确保它们只用于相应的程序元素上
③它们没有提供将参数值与程序元素关联起来的好方法
注解的出现很好的解决了上面出现的问题
一般地讲,注解永远不会改变被注解代码的语义,但是使它可以通过工具进行特殊的处理
并且注解因为有额外信息,并且可以通过反射进行处理,可以带来更为强大的功能。

三十六.坚持使用Override注解
在平时编码过程中,你可能忘记忘记覆盖一些方法,如果使用Override注解的话,起到了提示的作用
有可能我们在平时覆盖方法时,将参数定义的不一样,以为我们已经覆盖掉了父类的方法,但是使用的时候内部仍然调用的是父类的方法,
如果我们在代码中加上Override的话,编译器会自动帮我们检查是否进行了覆盖操作
平时我们在编码的过程中,应该养成在覆盖方法的时候都加上Override注解
例外情况,如果我们在抽象类中覆盖了类,这是不必将Override注解方在该方法上,因为编译器不会进行检查
坚持使用Override的另一种理由是,现在的IDE有自动检查的功能(代码检查),IDE和编译器可以确保你覆盖任何你想要覆盖的方法


三十七.用标记接口定义类型
标记接口指的是没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口
标记接口定义的类型是由被标记类的实例实现的,标记注解则没有定义这样的类型
标记接口胜过标记注解,他们可以被更加精确地进行锁定
如果标记是应该用到任何程序元素而不是类或者接口,就必须使用注解,因为只有类和接口用来实现或者扩展接口
如果要编写一个或者多个只接受有这种标记的方法,那么就应该使用标记接口

三十八.检查参数的有效性
绝大多数方法和构造器对于传递给它们的参数都会有某些限制。你应该在文档中指明这些限制,并且在方法体的开头处检查参数,以强制施加这些限制
对于公有的方法,要用javadoc的@throws标签在文档中说明违反参数值限制时会抛出的异常
在非公有的方法中,通常应该使用断言来检查它们的参数,断言如果出现失败,将会抛出AssertionError,不同于一般的有效性检查,如果它们没有起到作用
实质上也不会有成本开销,除非你加上-ea(enableAssertions)给java解释器
在方法执行它的计算任务之前,
在执行操作的时候应该执行必要的有效性检查,大多时候,有效性检查隐藏在计算过程中(比如强制类型转换)

三十九.必要时进行保护性拷贝
因为引用指向的都是同一内存空间,final也不是真正的不可变的,所以在必要的时候,我们应该进行相应的保护性拷贝
要防止clone方法带来的问题
要将内部域的保护性拷被即可
平时在编写对象的时候,需要考虑对象是否是可变的,如果是可变的话,要考虑你的类是否能够容忍对象进入数据结构之后发生变化
在一个指向内部可变组件的引用返回给客户端之前,也应该加倍认真地考虑,解决方案就是返回保护性拷贝

四十.谨慎设计方法签名
谨慎选择方法的名称:方法的名称应该始终遵循标准的命名规范,驼峰法
不要过于追求提供便利的方法:只有当一项操作被经常用到的时候,才考虑为它提供快捷方式
避免过长的参数列表:目标是四个参数或者更少,参数太多会导致程序员无法记忆。。
缩短参数列表的方法:
①把方法分解成多个方法,每个方法包含这些参数的一个子集
②用来创建辅助类,用来保存参数的分组,这些辅助类一般为静态成员类
③从对象创建到方法调用都采用Builder模式
对于参数类型,要优先使用接口而不是类
对于boolean参数,要优先使用两个元素的枚举类型。

四十一.慎用重载

重载时调用哪个方式是在编译期间确定的,覆盖时方法的调用时运行期间确定的
对于重载方法的选择是静态的,而对于被覆盖的方法的选择则是动态的
安全而保守的策略是,永远不要到处两个具有相同参数数目的重载方法,如果方法是用可变参数,保守的策略是根本不要重载它。
对于构造器,你没有选择使用不同名称的机会,一个类的多个构造器总是重载的,此时就可以采用静态工厂。
能够重载的方法并不意味着就应该重载方法,一般情况下,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法

四十二.慎用可变参数
eg:static int sum(int... args) {
int sum = 0;
for(int arg : args) {
sum += arg;
}
return sum;
  }
如果调用这个方法是没有传入参数,在编译的时候不会出现错误,但是运行的时候会报错。
在重视性能的情况下应该谨慎使用可变参数机制

四十三.返回零长度的数组或者集合, 而不是null
对于返回null的情况,程序中需要进行判断然后再进行处理,不然很可能出现异常。
返回类型为数组或集合的方法没理由返回null,而不是返回一个零长度的数组或者集合。

四十四.为所有导出的API元素编写文档注释
要想让API真正可用的话,就必须为其编写文档
为了正确编写API,必须在每个被导出的类,接口,构造器,方法和域声明之前增加一个文档注释
文档注释应该列举出这个方法的所有前提条件和后置条件@param
{@code}标签更好,因为它避免了转义HTML元字符,并且以代码的字体进行显示
如果在注释中使用了特殊的字符,应该使用@literal标签将他们包围起来
Javadoc具有继承方法注释的能力,如果API元素没有文档注释,javadoc将会搜索最为适用的文档注释(接口的文档注释优先于超类的文档注释)
虽然为所有导出的API元素提供文档注释是必要的,但是这样做并非永远就足够了,应该用外部文档来描述该API的总体结构

四十五.将局部变量的作用于最小化
要使局部变量的作用于最小化,最有力的方法就是在第一次使用它的地方声明
循环中提供了特殊的机会来将变量的作用域最小化
eg: for(Element e : c) {
doSomething(e);
}

四十六.for-each循环优先于传统的for循环
for-each循环,通过完全隐藏迭代器或者索引变量,避免了混乱和出错的可能
当见到:,可以把它读作“在……里面”,
for(Iterator<Face> i = faces.iterator(); i.hasNext(); )
for(Iterator<Face> j = faces.iterator(); j.hasNext();)
System.out.println(i.next() + " " + j.next())
总之,for-each循环在简洁性和预防Bug方面有着for循环无法比拟的优势,并且没有性能损失
三种常见的情况无法使用for-each循环:
1.过滤 2.转换 3.平行迭代
0 0
原创粉丝点击