Effective Java总结

一、创建和销毁对象

1.考虑用静态工厂方法代替构造器

  • 静态工厂方法有名称,可以指示产生对象的特点。
  • 静态工厂方法屏蔽了内部的实现。比如:对于不可变对象,可以返回相同的引用;将对象缓存起来,重复利用。
  • 静态工厂方法可以根据参数,返回原类型任意需要的子类型。
  • 服务提供框架
    • 服务接口:服务的通用接口。(Connection)
    • 服务提供者接口:可以生成服务对象(原型类或子类)的接口(所有要注册到框架的服务提供者都需要实现这个接口,这个接口必然有一个接口用来生成服务接口的具体实现类,这个具体实现类也归服务提供者提供,比如Driver)
    • 服务提供者管理类(持有所有具体的服务提供者,比如DriverManager)
      • 含有服务提供者注册接口:用来注册服务提供者(registerDriver)
      • 获取服务接口具体实现类的接口:整个服务提供框架的核心(getConnection)

2.遇到多个构造器参数时考虑用构建器

如果类的构造器或者静态工厂具有4个或以上多个参数时,为了防止构造器个数爆炸的情况,我们可以给这个类配置一个内部构建器类,通过内部构建类设置类的属性值,之后调用构建器的bulid方法将类实例化出来。

3.用私有构造器或者枚举类型强化singleton属性

构造器为私有的,共有的静态成员,并提供静态的方法返回该成员的引用。
优点:

  • 构造器是私有的,能确保外部只能通过API获取到该唯一的成员。
  • 静态static,能保证类的全局唯一。
  • 对外暴露统一的获取单例API,使得后面我们修改方法实现而不影响外部使用变得简单。(比如修改为不是单例的,或者每个线程获得一个唯一的实例等)
    注意:
  • 虽然构造器是私有的,但是外部依然可以通过反射改变访问权限来调用构造器。我们可以在构造器中,写代码让程序创建第二个实例时抛出异常。
  • 将通过api获取到的单例对象,序列化和反序列化,从而获得一个内容一样,但是全新的对象,从而打破单例。解决方法是,重写readResolve方法,在readResolve方法中,直接返回单例对象。

以上是通用的方法,其实从Java1.5以后,我们可以编写一个包含单个元素的枚举类型来实现单例,优点是更加简洁,并且天然提供了序列化机制防止多次实例化。

4.工具类等要使用私有构造器,强化不可实例化的能力

5. 避免创建不必要的对象

如何并且什么时候重用对象?

  • 对于不可变或者不会被修改的对象,尽量重用。例如:String s = new String("xxxx"); 虽然xxxx是个不可变的字符串,但是该语句依然会生成一个新的对象。而String s = "xxxx",在相同的虚拟机中时,对象xxxx会被重用。这段代码假如在一个频繁调用的方法或循环中时,性能将有很大的区别。
  • 特定对象的适配器(adapter,也称为视图),可以重用。比如Map接口的keySet方法返回了该Map对象的Set视图。对于一个特定的Map,它的key集合是固定不变的。而且当该Map中新增了新的key时,所有视图keySet也应该同步更新。所以,多份Set视图其实无意义,对于多次调用同一个Map的keySet方法,只需要返回相同的Set视图即可。
  • 基础类型和包装类型频繁的自动装箱和拆箱会引起性能的下降。当基本类型和包装类型运算时,会先将包装类型拆分为基本类型,运算后又自动装箱回来,当放在多次循环中时会影响性能。所以,要优先使用基本类型,当心无意识的自动装箱。
    注意:
  • 只有对象创建代价非常昂贵时(比如数据库连接池),我们才要避免尽可能避免创建对象。而对于一些小对象来说,由于它们的创建和回收非常廉价,通过创建附加的对象来提高程序的清晰和简洁性反而是好事。

6.消除过期的对象引用

  • 当我们用数据结构(Stack类,数组,队列等)自己维护内存空间时,常常只是移动目前元素的位置来标记活动和非活动的元素,导致过期引用的产生,出现内存泄漏。(比如Stack,size表示当前栈的元素个数,出栈只是获取elements[--size],我们只是把index前移一位,但是出栈的元素依旧在数组elements中,引用依然存在,垃圾回收器是不会自动回收的 )。对于这种情况,我们只能是在移动index将元素置为失效元素的同时,手动将元素引用置为null,来释放对象。
  • 缓存是内存泄漏的又一个常见来源。对于缓存中的对象,常常在它失效之后,因为忘记清除后存在很长一段时间。
    • 假如当外部没有引用缓存对象时,你就希望该缓存对象从缓存中删除时,那么你可以用weakHashMap来实现缓存(WeakHashMap使用弱引用)
    • 假如缓存对象会随着时间偏移越来越没价值,那么可以设置个定时清理的后台线程进行清除工作。
  • 内存泄漏的第三个常见来源是监听器和其他回调。当客户端利用你提供的API进行注册回调或者监听,但是又不显示取消时,随着时间的推移就会积聚。对于这种情况,我们可以使用弱引用。

7.避免使用终结方法(finalizer)

  • 终结方法不会保证在对象不可达的情况下,马上执行。假如一个对象在终结方法中回收重要资源,由于不会马上执行,会导致众多重要资源不会及时被回收,当资源不足无法获取时,将会发生失败。

  • 终结方法不保证执行。当一个程序结束时,可能还有一些不可达对象的终结方法还没有执行就结束了。所以,“不应该依赖终结方法来更新重要的持久化状态”。例如,依赖终结方法来释放共享资源上(数据库)的永久锁,很容易让整个分布式系统垮掉。

  • 终结方法中发生未被捕获的异常,此时异常会被忽略,并且终结方法会停止执行,那么这个对象就是一个被回收一半的破坏对象,之后如果有线程调用它的话,会引起不确定的结果。

  • 使用终结方法会使得对象的垃圾回收时间变长。

  • 我们可以书写显示的终结方法回收资源,在对象不可用时显示调用。显示的终结方法的调用常常和try-catch一起使用来确保方法一定会被调用。

  • 终结方法的合法调用场景:

    • 作为安全网。在终结方法中调用显示的终结方法。可以确保在客户端忘记显示调用终结方法时,资源忘记回收(虽然会迟一点回收,但总比没有回收好)。而且,在这种情况发生时,我们可以记录log,提示bug。由于增加终结方法会有性能消耗,需要考虑增加这个安全网的效益。
  • 注意:

    • 终结方法链不会自动执行。父类实现了终结方法,子类重写了终结方法。那么在子类的终结方法中,需要显示地调用父类的终结方法,并且将这个显示调用放在finally中,从而确保子类终结方法有异常时,父类的终结方法依然能被执行。
    • 为了防止子类忘记调用父类的终结方法,可以给子类创建一个匿名的终结守卫者,用于帮助子类终结外围实例。外围类的每个实例都会持有一个终结守卫者的引用。

二、所有对象通用的方法

8、覆盖equals方法要满足对称性、传递性、一致性

  • 我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保持equals约定。
  • 覆盖equals方法的同时,必须同步覆盖hashCode。
  • 对于既不是float,也不是double的基本类型,可以直接==比较相等。对于对象引用域,可以递归调用equals方法。对于float,要使用Float.compare方法。对于double,要使用Dobule.compare.

9、覆盖equals方法,必须同步覆盖hashCode方法。

  • 两个通过equals方法判断一致的对象,如果不覆盖hashCode方法,可能会得到不一样的hashCode结果,从而导致在使用基于散列的集合时引起错误(HashMap,HashSet等)。
  • 一个好的hashCode实现,应该是让不等的对象获得不等的散列码。
  • 通过equals方法判断一致的对象,应该有相同的散列码。
  • 在散列码的计算过程中,可以把冗余域(可以通过其他域计算出来的域)排除在外。
  • 如果一个类是不可变的,并且计算散列码的开销很大,就应该考虑把散列码存在对象内部,并且延迟计算这个散列码,在第一次调hashCode时再计算。

10、始终要覆盖toString

  • 便于调试排查

11、谨慎地覆盖clone方法

  • 假如类中有自定义类型域,要实现深复制,要求自定义类型也继承cloneable接口,并实现clone()方法。日常开放中,很少会实现cloneable接口,所以常常是需要侵入的修改代码。为了减少对代码的侵入,我们可以先将类序列化,再反序列化,实现深复制的目的。
  • Object.clone()方法返回的是Object,但是我们重写的时候,返回的是具体的类型,而不是Object。这是合法的,也是我们期望的。因为从java1.5以后,出现了“协变返回类型”,也就是覆盖的方法的返回类型可以是被覆盖方法的返回类型的子类了。这样客户端使用时就不需要转化了。这里也体现了一个通则“永远不要让客户端去做任何类库能够替客户完成的事情”。
  • 如果为了继承而专门设计的类,覆盖了clone方法,那么为了它的子类可以灵活的选择是否要实现Cloneable接口,我们需要把这个覆盖的clone方法声明为protected,并且抛出CloneNotSupportedException异常,并且该类不应该实现Cloneable接口,这样对于clone方法,子类仿佛是直接从Object中扩展过来的一样。
  • 如果决定用线程安全的类实现Cloneable接口,那么clone方法必须得到很好的同步,Object的clone方法没有同步。
  • 对于不可变类,支持对象拷贝并没有意义,因为被拷贝的对象与原始对象并没有实质的不同。
  • 不得不说,Cloneable具有太多的问题,有那么多隐含的规定。对象拷贝更好的方法,是提供一个拷贝构造器或拷贝工厂。
    拷贝构造器:public Person(Person person);
    拷贝工厂: public static Person newInstance(Person person);
    这种方式的优点是:
    • 不会要求我们遵守尚未制定好的文档规范。
    • 不会抛出不必要的受检异常,要求客户端处理。
    • 不需要进行类型转换。
    • 利用这种方式,有时我们通过拷贝,还能将类型转为其他类型。比如Collection和Map就提供了转为其他类型的拷贝构造器和拷贝工厂。比如:你想将HashSet s,拷贝为TreeSet。clone方法无法实现,但是拷贝构造器却很简单:new TreeSet(s);

12、考虑实现Compareable接口

  • 实现Compareable接口的compareTo方法,必须满足自反性,对称性,传递性。
  • 如果正在编写一个值类,有很明显的内在排序关系,那么应该坚决实现Compareable接口,因为可以和许多泛型算法以及依赖于该接口的泛型集合协作。
  • 无法在用新的值组件扩展可实例化的类时,同时保证compareTo的约定(除非compareTo方法的实现愿意放弃新增加的值组件)。对此的应对策略是:用组合不用继承即编写一个新类,其中包含第一个类的一个实例,并且提供一个视图方法返回这个实例。这样我们既能自由地在第二个类上实现compareTo方法,也能允许客户端在只想使用第一个类实例的时候,把第二个类实例当成第一个类实例使用。
  • BigDecimal类,它的compareTo方法与equals不一致,即new BigDecimal("1.0")和new BigDecimal("1.00"),调用compareTo方法返回0表示相等。但是调用equals却等于false。当将这两个实例放入HashSet时,会存在两个(因为是根据equals判断)。而放入treeSet时,却只有一个实例(因为是根据compareTo判断)。
  • 对于某些旧的类,没有实现Compareable接口时,你可以编写一个Comparator来代替。

类和接口

13、使类和成员的可访问性最小

  • 尽可能地使每个类或者成员不被外界访问。(包级私有的类,是类实现的一部分。那么在以后的发布版本中,你可以根据需要进行修改删除等,而不用担心影响到调用的客户端。而对于开放出去的共有的类或接口,我们则必须保证它们的兼容性。)
  • 如果一个类或接口只是给某个类的内部调用,那么就应该考虑把它做成使用类的私有嵌套类。
  • 导出类的protected成员的兼容性同样需要获得支持。protected的成员应该尽量少用。
  • 子类中覆盖的方法的访问权限不允许低于超类中被覆盖方法的访问权限。这是为了保证任何可以使用超类方法的地方,也能使用子类该方法。但是这条规定限制了我们降低方法访问性的能力。
  • 不能为了测试而把类或接口的可访问性提高。我们可以通过让测试部分成为类实现的一部分,来达到相同的目的。
  • 让final的引用指向一个可变的类实例是错误的。同样的,类具有共有的静态final数组域,或者返回这种域的方法,几乎总是错误的。这样获取这个引用的客户端就能随意修改,这常常是导致安全漏洞的一个根源。
  • 当类中需要返回一个指向私有数组域的引用时,有两种方法:
    • 将公有数组变成私有的,并增加一个新的公有的final的数组引用指向不可变列表.
private static final Thing[] PRIVATE_VALUES={...};
public static final List<Thing> VALUES = 
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

 

* 或者提供一个共有的返回数组域的方法,该方法返回该数组域的clone对象。
private static final Thing[] PRIVATE_VALUES={...};
public static final Thing[] values(){
    return PRIVATE_VALUES.clone();
}

 

14、在共有类中使用访问方法而非公有域

  • 有些类中的值域提供了get和set方法(可以被访问,也可以被修改),而不采用将值域改为public的方法,原因是通过这种方式,以后我们要修改类内部的表示法时就会很简单,而不用考虑兼容性。
  • 如果类是包级私有的或私有嵌套类,那么直接暴露值域,是没有错的。

15、使可变性最小化

  • 不可变类指的是实例的域在创建的时候就确定了,并在整个对象的生命周期保持不变。java平台库不可变类有String,基本类型的包装类型,BigInteger和BigDecimal等。

  • 使类成为不可变类需要遵循以下原则:

    • 不提供任何能修改对象状态的方法。(比如set方法)
    • 类无法子类化(使类变成final类或者仅提供私有的构造器使类无法扩展)
    • 类的所有对象域为final,无法修改的。
    • 如果类的值域指向可变对象,那就不能让客户端获得类的该值域。
  • 不可变对象因为它的不变性,不用担心被线程破坏了内部状态,因此可以被线程自由共享,是线程安全的。

  • 不可变对象可以被不断重用,因为创建多个完全一样的实例是没有意义的。为了提高不可变对象的重用性,有两种方法:

    • 我们可以为频繁调用的不可变对象提供公有的静态final常量。比如public static final ConstantClass ZERO = New ConstantClass(xxxx, xxxx, ....);
    • 提供静态的工厂方法。在静态的工厂方法里,将频繁使用的不可变类缓存起来。
  • 不可变类的唯一缺点:对于每个不同的值,我们都需要提供一个单独的对象,这常常会导致性能问题。比如:

// 假如有List<String> values,是一个拥有一万多的字符串列表。
String result = "";
for(String s : values){
    result = result + s;
}

 

由于String是不可变类,每一步result + s都会产生一个新的String的不可变实例。在这个过程当中,产生的所有中间实例都会被丢弃,这就导致了性能问题。解决这种缺点的方法有两种:
* 预测会有多步骤操作的不可变类,使用它们的基本类型进行操作,这样就可以不必在每个步骤中都产生一个不可变类实例了。比如:在循环进行加减操作时,对于Long和long,我们应该使用基本类型long进行操作。
* 为不可变类提供可变的配套类。比如为不可变类String提供可变的StringBulider这个配套类。在多步骤循环时,使用可变的StringBuilder,后面再转为不可变的String类。

  • 对于不可变类,有时为了提高它的使用性能,我们可以在不可变类中提供一个或者多个非final域,在对象第一次被使用时,将一些昂贵的计算结果缓存在这些域中。比如计算一个不可变类的hashCode值,我们就可以将计算结果缓存在对象中,也就是延迟初始化。
  • 构造器应该完全初始化对象,并建立起约束关系。不要在构造器或者静态工厂方法之外再提供公共的初始化方法。同样,也不应该提供“重新初始化”方法。

16、复合优先于继承

  • 继承打破了封装性,使得子类依赖于其超类中特定功能的实现细节。在后续的发布版本中,超类在更新实现细节时,就有可能导致代码完全没动的子类功能发生问题。
    • 假如超类是一个集合,子类继承了超类,并且重写了超类中关于元素添加的方法,并在方法调用前加了一些先决条件保证合法性。之后超类发行了一个版本,增加了一个新的添加元素的方法,而子类没做任何变化。那么就可能导致有非法的元素添加进来。
    • 假如超类在一个新的版本发行一个新的方法,而不幸的子类也有一个函数名相同的扩展方法,而返回类型不一样。那么会导致子类编译不通过。
    • ....
  • 不扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的实例。这种方法称为“复合”。因为现有类成为新类的一个组件,新类中的方法通过调用现有类中对应的方法,将结果返回,这被称为“转发”。这种把现有类包装起来,增加新功能产生的新类,称为包装类,这也正是Decorator模式。
  • 包装类不适合用在回调框架中。
  • 只有当子类确实是超类的子模型,即两者是“is-a”关系时,才适合用继承。

17、要么为继承而设计并提供文档说明,要么就禁止继承

  • 为继承而设计的类,提供的文档,必须精确地描述覆盖每个方法所带来的影响。
  • 构造器决不能调用可以覆盖的方法。因为子类实例化时,会先调用父类构造器,而父类构造器
    调用的可覆盖方法是子类的,子类还没开始实例化,那么就会导致不确定的后果。
  • 继承的父类中实现clone()和readResolve()方法时,也不能调用被覆盖的方法。因为clone()在拷贝时,会先调父类的clone()方法,readResolve()反序列化时,和构造器类似。
  • 对于普通的具体类,我们既没有将类声明为final,也没有将构造器设置为private或包级私有来防止它被子类化,所以还是挺危险的。解决方法是:
    • 确保类内部实现不会调用可覆盖的方法,并在文档中说明这一点,也就是消除可覆盖方法的自用性(覆盖方法给自己用的意思)。
    • 或者,将可覆盖方法中的方法体移到另一个辅助方法中,可覆盖方法调用这个辅助方法来实现功能。类内部需要使用覆盖方法的地方就使用这个辅助方法。这样就不怕子类覆盖引起父类功能的破坏。

18、接口优于抽象类

  • 接口和抽象类最明显的区别在于,抽象类允许包含某些方法的默认实现,但是接口则不允许。但是因为java只支持单继承,导致抽象类作为类型定义受到了极大的限制。

  • 接口比抽象类好用的优点:

    • 现有的类很容易更新,以实现新的接口。由于单继承的原因,假如要为现有的类扩展新的抽象类,你无法直接在现有的类上继承该新的抽象类(假如现有的类已继承了其他类), 你只能让现有的类的某个祖先成为新的抽象类的子类,这就导致祖先下面不需要继承该抽象类的类被迫继承,从而打破了继承的层次结构。而接口不同,你可以在现有类的声明中声明implements该接口,就可以增加新的方法。
    • 接口是定义混合类型的理想选择。混合类型是指类除了实现它的基本功能之外(比如提供计算),它还可以通过实现Compareable等接口,表达自己是可比较的类型等。而抽象类由于单继承,只能通过多次继承的臃肿的层次结构实现。
    • 我们可以通过接口构造非层次的类型框架。比如现在有唱歌的接口,作曲的接口,我们还能新增一个唱歌作曲的接口(通过同时继承唱歌和作曲接口实现),并提供新的方法。
  • 为了获得抽象类能提供方法的默认实现的优点,我们可以为每个重要的接口提供一个抽象的骨架实现,让接口负责定义类型,而骨架实现类负责接口方法的默认实现。首先我们需要研究接口中的多个方法,确定哪些方法是最基本的,作为抽象方法,供子类自己实现。然后我们为其他方法提供默认实现(利用抽象方法的功能)。

  • 抽象类比接口好的一个地方是:当抽象类和接口被公开出去之后,抽象类依然可以增加新的具体方法(必须提供默认实现),这样继承抽象类的子类都拥有了这个方法。而开放出去的接口增加新的方法是不可能的,因为这会导致实现该接口的客户端直接编译报错。

  • 总之,接口是定义允许多个实现的类型的最佳途径。在你导出一个重要的接口之后,应该坚决为它提供骨架实现类。最后,应该谨慎地设计所有公有的接口,并通过多个实现类来测试,因为这常常代表着永久维护的承诺。

19、接口只用于定义类型

  • 在Java中,有一种不良的使用接口的行为:在接口中定义常量,这种接口被称为常量接口。这是不对的,常量是类内部的实现细节,但是通过接口暴露出来,导致要永久维护,即使在以后的新的发行版本中,我们发现常量已经没用了,或者值需要更新,我们都不能修改这些常量。

  • 常量维护可以在以下几个地方:

    • 如果常量和类和接口紧密结合,那么我们应该将这些常量添加到接口和类中。
    • 如果常量被看做枚举类型的成员,就应该用枚举类型。
    • 也可以使用工具类保存这些常量。如果大量利用工具类导出的常量,可以通过利用静态导入机制,避免用类名来修饰常量名。
  • 简而言之,接口应该只用于定义类型,而不能用于常量导出。

20、类层次优于标签类

  • 标签类是指类中有一个域,用于标识该类的类型,并且该类中包含每种类型特有的域。标签类中的方法,常常会有很多判断语句,根据不同的类型,进行不同的操作。这种标签类有很多的缺点,类中充斥着多种类型的样板代码,由于多种类型的实现都挤在一个单个类中,破坏了可读性。同时,引起内存占用变大,因为多了很多不同的域。
  • 标签类往往能用类层次结构来改造。将共有的域和方法放到一个抽象类中,而具体的类型都继承该抽象类。
  • 简而言之,标签类很少用到。当你发现代码中有标签类时,应该考虑一下这个标签类是否可以被取消,用类层次来重构。

21、用函数对象表示策略

  • 有些对象允许函数调用者传入第二个函数来指定自己的行为,这样的函数称为函数指针(例如C)。但是java中并不支持,但是我们可以传递对象的引用,调用对象上的方法来达到相同的目的。如果一个类仅仅只是包含这样一个方法,那么这样的实例称为函数对象。
  • 函数对象一般用于策略模式,函数对象表示某种操作的具体策略,通过传入不同的函数对象(函数对象类一般都有一个支持泛型的策略接口),执行不同的策略。例如:排序相关的函数对象.
    排序的策略接口
public interface Comparator<T>{
    public int compare(T t1, T t2);
}

 

具体的函数对象类,这个具体的策略是根据字符串的长度判断大小

class StringLengthComparator implements Comparator<String>{
    
}

 

Arrays.sort(stringArray, StringLengthComparator});
大部分函数对象只会使用一次,这时我们可以使用匿名类

Arrays.sort(stringArray, new Comparator<String>(){
    public int compare(String s1, String s2){
        return s1.length() - s2.lengthh();
    }
});

 

匿名类这种方式,每次使用都会创建一个新的实例对象。如果会被重复调用,可以将函数对象存储到一个私有的静态final域中,并给它起一个有意义的名字。

  • 总之,用函数对象表示策略,我们需要声明一个接口表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被使用一次,通常使用匿名类来声明和实例化这个具体的策略类。当一个具体策略是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的final域导出,其类型为策略接口。

22、优先考虑静态成员类

嵌套类是指被定义在另一个类内部的类。嵌套类分为:静态成员类、非静态成员类、匿名类和局部类。

  • 静态成员类是最简单的一种嵌套类,它跟普通的类除了声明位置不同(在另一个类内部声明)之外,其他基本都相同。静态成员类一种常见用法是作为公有的辅助类,仅当与它的外部类一起使用时才有效果。比如一个表示计算的外部类Calcuator,有表示具体操作的内部类枚举Operation.
  • 非静态成员类与静态成员类相比是少了修饰符static。
    • 非静态成员类的实例拥有外围类实例的引用,不能独立于外围实例存在。
    • 通常情况,我们会在外围类某个实例方法内部调用非静态成员类的构造器来实例化非静态成员类
    • 外围类实例.new MemberClass(args)也可以用这个表达式在外部实例化非静态成员类。
    • 由于非静态成员类需要记录外围类的实例引用,所以增加了空间开销,并且增加了构造的时间开销。
    • 非静态成员类一般用来定义一个Adapter。例如Map接口的实现往往使用非静态成员类来实现它们的集合视图,这些集合视图由keySet,entrySet和Values方法来返回。同样地,Set和List集合接口也使用非静态成员类来实现它们的迭代器。
  • 匿名类指类声明和实例在同一个地方,没有名字。
    • 匿名类可以出现在代码中任何允许表达式的地方。
    • 匿名类的客户端无法调用任何成员,除了从它的超类型中继承得到的。
    • 由于匿名类出现在表达式中,因此它必须保持简短。
    • 匿名类的一种常见用法就是动态地创建函数对象。比如利用匿名的comparator实现排序。或者,用户创建过程对象,比如Runable、Thread或者TimerTask实例。
    • 匿名类不能含有静态成员。
  • 局部类在任何可以声明局部变量的地方都可以声明。
    • 局部类有名字,可以重复使用
    • 只有在非静态环境,才能拥有外围类实例。
    • 局部类不能包含静态成员。

三、泛型

23、不要在新代码中使用原生态类型

  • 每个泛型都定义一个原生态类型,即不带任何实例类型参数的泛型名称。例如List<E>对应的原生态类型是List。
  • 原生态类型的集合,你可以往里面放入任何类型的对象而不报错。但是在运行取出使用时就可能发生类型转化失败的错误。泛型可以指定集合中的类型,并在编译期就发现集合中放入了不正确的类型。
  • 从泛型集合中取出实例时,不再需要进行类型转化。
  • 有时我们需要往集合中放入任何类型的实例时,我们可以声明List<Object>。List<Object>和原生态类型List的区别在于,List逃避了类型检查,而List<Object>显示地声明了要放入任意类型。而且List<String> 可以赋给List,但是却不能赋给List<Object>,编译器认为List<String>是List的子类型,却不是List<Object>的子类性。
  • 如果要使用泛型,但是不确定或者不关心实际的类型参数,就可以使用一个问号代替。例如泛型Set<E>的无限通配符类型就是Set<?>.
  • 不要在新代码中使用原生态类型,有两个例外
    • 在类文字中必须使用原生态类型。比如List.class是合法的,但是List<String>.class则是不合法的。
    • 泛型信息在运行时会被擦除,因此在参数化类型而非无限制通配符类型上使用instanceof操作符是非法的。

24、消除非受检警告

  • 用泛型编程时,会遇到许多编译器警告:非受检强制转化警告、非受检方法调用警告、非受检普通数组创建警告、以及非受检转换警告。
  • 要尽可能地消除每一个非受检警告。如果消除了所有的警告,就可以确保代码是类型安全的。
  • 如果无法消除警告,同时可以证明引起警告的代码是类型安全的,可以利用一个@SuppressWarnings("unchecked")注解来禁止这条警告。
  • 但是如果忽略(而不是禁止)明知道是安全的非受检警告时,当出现一条真正有问题的警告时,你也不会注意到。
  • SuppressWarnings注解可以用在任何粒度的级别中,从单独的局部变量声明到整个类都可以。应该始终尽可能小的范围中使用SuppressWarnings注解。永远不要再整个类上使用SuppressWarnings注解。
  • 每当使用SuppressWarnings注解时,都要添加一条注释,说明为什么这么做是安全的。

25、列表优先于数组

  • 数组是协变的,即如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型。这种协变容易引起异常:
    // 编译通过,运行时异常,抛出ArrayStoreException异常
    Object[] objArray = new Long[1];
    objArray[0] = "i am here";
    
    //编译直接报错
    List<Object> objList = new ArrayList<String>(); 

     

  • 数组是在运行时检查类型,而泛型是在运行时擦除类型,而在编译时强化类型检查。因此数组和泛型不能混用。比如创建泛型列表的数组(List<String>[])是不合法的。
  • 创建泛型数组也是不合法的(new E[])。
  • 像E, List<E>和List<String>这样的类型称为不可具体化的类型。不可具体化类型是指运行时拥有的信息比编译时拥有的信息少。值得注意的是:无限通配符是具体化类型,所以可以声明为数组。
  • 由于泛型是不可具体化类型,这就意味着在混合使用可变参数类型和泛型时,会出现警告。因为调用可变参数类型方法时,会创建一个数组来存放可变参数,而程序会发现类型是不可具体化类型,从而发出警告。对于这种警告,我们要么不结合使用,要么就禁止警告。
  • 数组是斜变的,是具体化类型;泛型是不可具体化类型。一般来说,数组和泛型不能很好地混合使用。如果你发现自己将它们混合使用,并且得到了编译时错误或者警告,你的第一反应就应该是用列表代替数组。

26、优先考虑泛型

使用泛型比使用需要在客户端代码中进行类型转换要更加安全,也更加容易。在设计新类型的时候,要确保它们不需要这种转换就可以使用。只要时间允许,就把所有现有的类型都泛型化。

27、优先考虑泛型方法

静态工具方法尤其适合于泛型化。

转载于:https://www.cnblogs.com/kejicjk/p/7142180.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值