effective java

总结一下 effective Java这本书里面的东西!


6.方法
检查参数有效性
必要时保护性拷贝
谨慎设计方法签名
慎用重载
慎用可变参数
返回0长度的数组或集合(而不是null)
为所有导出的api元素编写文档注释
7.通用程序设计
局部变量作用域最小化
for-each优于传统for
了解和使用库类
精确答案避免使用float和double
基本类型优于装箱基本类型
其他类适合情况下少使用字符串
字符串拼接性能
接口引用对象
接口优于反射机制
慎用本地方法
谨慎优化
命名惯例
8.异常
仅针对异常情况才使用异常
可恢复情况使用编译时异常,编程错误使用运行时异常
避免使用不必要的编译时异常
优先使用标准的异常
抛出与抽象相对应的异常
方法抛出异常需编写文档
细节消息中能包含捕获失败的信息
使失败保持原子性
不要忽略异常
9.并发
同步访问共享的可变参数
避免过度同步
executor、task优于线程
并发工具优于wait、notify
线程安全性文档化
慎用延迟初始化
不要依赖于线程调度器
避免使用线程组
10.序列化
慎用Serializable接口
考虑自定义序列化方式
保护性编写readObject()
实例控制时,枚举优于readResolve
序列化代理代替序列化实例

<!----分割线---->


1.创建和销毁对象

(1)静态工厂方法代替构造器

类可以提供一个静态的工厂方法,专门为客户端提供自身的实例。

利用构造器重载通过参数判断调用哪一个可读性不强,利用静态工厂方法可以指定名称表明创建的是本类的哪一种实例。且不必每次都用构造器创建新实例,可以返回同一个实例,并且最重要一点静态工厂方法可以返回本类的子类实例。

缺点在于,本类如果没有公有或者受保护的构造器那么本来不能被继承。

//Android源码Watchdog.java  
public static Watchdog getInstance() {  
        if (sWatchdog == null) {  
            sWatchdog = new Watchdog();  
        }  
        return sWatchdog;  
    } 

(2)多构造参数用构建器

静态工厂方法和构造器都不能扩展到大量的可选参数。例如多个参数都是可选的情况下,需要为每种参数组合创建一个构造器,否则在利用Person(A,B,C,D,E)创建对象时强行要求传入一些不必要的参数。当然也可以使用Setter方法为无参构造器创建的对象赋值来解决。为了保证重叠构造器的安全性、set方法的可读性,可以使用构建器Builder模式,将每个属性的赋值方法返回this对象。构建器是一个静态内部类用于外部类参数赋值

public class Person {
    private int a;
    private int b;
    private int c;
    public static class Builder {
        private int a;
        private int b;
        private int c;
        public Builder(){}
        public Builder A(int a) { this.a = a; return this} 

        public Builder B(int b) { this.b = b; return this}

        public Builder C{ this.c = c; return this}

        public A build() { return new A(this)}

    }

//调用方式如下:
new Person().Builder().A(a).B(b).C(c).D(d)

可以将他抽象为一个接口,利用泛型实现工厂化

public interface Builder <T> {
	public T biuld();
}

(3)强化单例模式

强化单例模式从构造函数私有化、枚举类型方面。

//构造函数私有化
public class Person{
	//final保证应用不能更改
	public static final Person person= new Person();
	//构造函数私有化(包、子类等不可见)
	private void Preson(){};
	public Person getInstance(){
		return person;
	}
}
//静态工厂方法
public class Person{
	//final保证应用不能更改
	public static final Person person= new Person();
	//构造函数私有化(包、子类等不可见)
	private void Preson(){};
	//静态工厂方法所有调用均返回同一个引用
	public static Person getInstance(){
		return person;
	}
}

上述方法可以利用反射获取class对象然后设置setAccessible()反射调用其构造方法。反射会破坏原有封装性。目前单元素的枚举类是实现单例的最佳方式,同时实现了线程安全、单例、可序列化。

//利用枚举实现
public class Person{
	//私有构造函数
    private Person(){}
    
    //枚举内部类
    private static enum Singleton{
    	
        INSTANCE;
        private Person singleton;
        //枚举类的构造器,JVM会保证此方法绝对只调用一次
        private Singleton(){
            singleton = new Person();
        }
        public Person getInstance(){
            return singleton;
        }
    }
    
    //利用枚举返回实例
    public static Person getInstance(){
        return Singleton.INSTANCE.getInstance();
    }
    
}

特点:eunm是由class实现的,可以包含成员和成员函数,是final类不能被继承,仅包含private构造器,且继承自Enum所以不能再继承其他父类。如下:

public final class T extends Enum
Java在序列化和反序列化枚举时做了特殊的规定,枚举的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法是被编译器禁用的,因此也不存在实现序列化接口后调用readObject会破坏单例的问题。

(4)避免创建无用对象

不可变对象始终可以重用。

//每次调用都会创建一个新String对象(都指向堆)
String StrA = new String("test");
//每次调用都返回同一个引用(都指向方法区常量池)
String StrB = "test";

可以使用构造函数私有化并提供静态工厂方法来代替构造器创建类实例以避免创建无用对象。

查看Boolean源码,可以看到静态工厂方法对比构造方法的优先性:

//Boolean静态工厂方法
public static Boolean valueof(boolean b){
	return b?Boolean.TRUE:Boolean.FALSE;
}
//利用构造器每次都返回一个新的Boolean对象
boolean isA = new Boolean("false");
//利用静态工厂方法返回同一个实例
boolean isB = Boolean.valueOf("false");

除了重用不可变对象之外,也可以重用那些已知不会再改变的对象。对于某个类的某些成员在类初始化后这些成员的引用不会再改变,可以将这些成员以final修饰,并在static块中将这些成员初始化(利用static快只会执行一次):

public class Person{
	//在类加载后不会改变的引用
	private static final String kind;
	//利用静态块初始化
	static{
		//指明这一类的所有实例都是老师
		kind = new String("Teacher");
	}
}

基本类型装箱会创建对象,如果for循环中使用装箱会造成大量对象的创建如下:

//Long sum = 0L;//需要5秒
long sum = 0;//需要1秒
for(int i = 0;i<Integer.MAX_VALUE;i++ ){
	sum+=i;
}
(5)清除过期的对象引用

清除过期对象引用是为了防止内存泄露。某些对象被程序无意识的保留其引用,那么GC器将不再处理这些内存。修复方式很简单,当对象不再使用时将其引用清空(置为null)

但是清空对象的引用只是一种例外而不是规范做法,并不用每次不再使用对象时就清空,而让包含该引用的变量结束自己生命周期是最好的方式。

内存泄露一般来源于缓存(放入缓存后忘记清除引用,会造成长时间留存)、监听器和回调(客户端注册回调后忘记取消注册造成的堆积)。通常利用java.lang.ref包下面的Reference的子类将对象包装到SoftReference(弱引用)中,当没有强引用指向它时,会在内存中停留一段的时间,垃圾回收器会根据 JVM 内存的使用情况及 SoftReference 的 get() 方法的调用情况来决定是否对其进行回收。

//将Person包装到弱引用类中
SoftReference<Person> bean = new SoftReference<Person>(new Person("name", 10)); 
//调用其get()方法返回Person对象的强引用
System.out.println(bean.get());
(6)避免使用终结方法

finalize()方法由jvm判断对象是否有必要执行(还未执行或者经过两次标记),这个方法的执行时间不确定,并不能保证能够及时地执行,并且在终结方法中抛出异常不会使线程停止(正常情况下未捕获的异常抛出来会造成线程停止)即异常被忽略掉,这个对象被其他线程使用会造成不确定的后果。

代替的方法为使用一个显示的终结方法,要求客户端在不再使用这个对象时都必须调用这个显示的终结方法,随后用一个标识符记录这个对象不再使用,其他线程或方法访问这个对象时先访问他标识符,如果表明是终结的对象则不能访问。

一点需要注意,父类A、子类B、子类的子类C,如果子类C覆盖了超类的终结方法,但是忘了手动的调用终结方法,那么超类的终结方法将永远也不会被调用到。只要有finalize()函数的类里面存在着其他类的引用,例如上面的C类里面存放着一个A类的实例引用和一个B类的实例引用,则它们都会被调用到finalize()方法,唯独被覆盖了的父类不会自动调用finalize(),需要我们手动super.finalize()。

除非是作为资源回收处理的第二道防线(安全网)或者是为了终结非关键的资源,否则请不要使用终结方法。如果没办法真的使用了finalize,别忘记了调用super.finalize()。

2.对象通用方法

(1)equals()

jse1.6规范Object的equals()方法需要保证自反(自己与自己equals)、对称(相互equals)、传递(多对象连续equals)、一致(多次equals)、非null的引用与nullequals必须返回false四个特性。

实现高质量equals方法从下面几点入手:

使用==操作符检查“参数是否为这个对象的引用”
使用instanceof操作符检查“参数是否为正确的类型”
把参数转换成正确的类型
对于该类中的每个”关键”域,检查参数中的域是否与该对象中对应的域相匹配
当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的
覆盖equals时总要覆盖hashCode()
不要企图让equals方法过于智能
不要将equals声明中的Object对象替换为其他的类型。

//Object的equals方法  
public boolean equals(Object obj) {    
      return (this == obj);    
}   

给出相对规范的equals写法,如String的equals():

//String的equals方法  
public boolean equals(Object anObject) { 
	
// 参数类型不能换成其他特定强类型,那样会将equals的覆盖变成重载
	
//==排除参数为当前对象的引用
if (this == anObject) {    
    return true;    
}    
//instanceof检查参数类型
   if (anObject instanceof String) { 
      //参数类型转换
      String anotherString = (String) anObject; 
      //String通过判断字符数组是否相同
        int n = value.length;    
        if (n == anotherString.value.length) {    
          char v1[] = value;    
          char v2[] = anotherString.value;    
          int i = 0;    
          while (n-- != 0) {    
              if (v1[i] != v2[i])    
                  return false;    
                    i++;    
                }    
              return true;    
            }    
        }    
        return false;    
} 
(2)hsahcode()

 众所周知集合set元素不能重复,通常会调用equals逐一对比,效率低下,所以发明了利用散列函数计算一个对象的的哈希值,这个值通常有不同的分组,根据得到的哈希值可以快速定位对象的物理存储位置。之后再调用equals进行对比。

hash值工作方式:先计算一个对象的哈希值找到他应该存储的物理位置,如果这个位置上面没有对象说明没有与他重复的对象,可以加入set集合。如果位置上面有某些对象,再调用equals进行判断,返回相同则不能加入集合,返回不同则可以映射其他散列地址。

约定:hashcode()相等,equals()不一定。equals()相等,hashcode()一定相等。

例如hashset的判断重复流程:hashcode()、equals()均返回false才能插入。只要第一步hashcode()返回true就不能插入。

正由于基于散列工作的集合(hashset,hashmap,hashtable)利用上面的特性,所有每个覆盖了equals方法的类中,也必须覆盖hashCode方法。

下面给出String的hashcode()方法:

//String的hashcode()方法
public int hashCode() {
     int h = hash;
     if (h == 0 && value.length > 0) {
         char val[] = value;
 
         for (int i = 0; i < value.length; i++) {
             h = 31 * h + val[i];
         }
         hash = h;
     }
     return h;
}   

java库中String、Integer、Date等类的hashcode()方法,可以直接并返回一个值作为你自己类的hashcode()返回值。当然并不算好,因为限制了这个类在将来版本中改进散列函数的能力。

Object的hashCode()源码:

//Object的hashCode()方法
public int hashCode() {  
        int lockWord = shadow$_monitor_;  
        final int lockWordStateMask = 0xC0000000;  // Top 2 bits.  
        final int lockWordStateHash = 0x80000000;  // Top 2 bits are value 2 (kStateHash).  
        final int lockWordHashMask = 0x0FFFFFFF;  // Low 28 bits.  
        if ((lockWord & lockWordStateMask) == lockWordStateHash) {  
            return lockWord & lockWordHashMask;  
        }  
        return System.identityHashCode(this);  
    }  

利用Date()类的hashcode如下:

@Override
public int hashCode() {
    return new Date().hashCode();
}
(3)toString()

一般建议重写toString()方法以返回某些特定格式的对象有用信息,并添加文档注释。

Object的toString()返回字符串(类名@对象哈希码的无符号十六进制表示)源码如下:

//Object的toString()方法	
public String toString() {   
		//类名@对象哈希码的无符号十六进制表示
	    return getClass().getName() + "@" + Integer.toHexString(hashCode());   
	    }   

数组对象的toString()方法并没有被重写,依然是返回类@hash值,源码如下:

数组工具类Array也有很多toString(a),但是它没有重写而是重载,传入一个数组并将它打印,源码如下:

//Array工具类的toString()方法重载
public static String toString(Object[] a) {
if (a == null)
return "null";
if (a.length == 0)
return "[]";
StringBuilder buf = new StringBuilder();
buf.append('[');

for (int i = 1; i < a.length; i++) {
buf.append(", ");
buf.append(a[i]);
}

buf.append("]");
return buf.toString();
}
(4)clone()

查看Object的clone()方法,源码如下:

protected native Object clone() throws CloneNotSupportedException;

可以看到是个native方法,具体实现部分源码如下:

if (obj->is_array()) {
    guarantee(klass->is_cloneable(), "all arrays are cloneable");
  } else {
    guarantee(obj->is_instance(), "should be instanceOop");
    bool cloneable = klass->is_subtype_of(SystemDictionary::Cloneable_klass());
    guarantee(cloneable == klass->is_cloneable(), "incorrect cloneable flag");
  }
if (!klass->is_cloneable()) {
    ResourceMark rm(THREAD);
    THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
  }

大意是说在进行具体的克隆时会检查对象是否有cloneable这个 标志(即检查是否实现这个Cloneable接口)。只有实现了Cloneable接口才能进行对象克隆。另一方面Cloneable这个接口只是个标识,并没有申明clone()这个方法。用户想要实现克隆必须先实现Cloneable接口,然后在重写Object的clone()方法。如果没有继承接口而调用会抛出上述异常CloneNotSupportedException。

为什么要这样设计:
因为当时需要对象支持克隆功能,但是不能全部都支持,需要用户自己设置标志。早期想要标志一个类是否支持克隆可以用继承基类、实现接口、修改class文件、注解,但是java单继承不能将唯一的继承机会用在克隆功能上,而且java5之前没有注解,也不至于为了克隆功能专门建立修饰符,所有最终使用接口。

为什要以protected修饰:
因为克隆这个需求有浅拷贝、深拷贝之分。浅拷贝就是当前对象需要拷贝的成员没有引用类型,利用Object的clone()就可以实现。深拷贝指的是需要拷贝的对象包含可变引用类型,如果仍然使用Object提供的clone()只能将被拷贝的对象进行内存复制,而他的引用类型还是指向原来那块内存,即引用类型的成员并没有被内存复制而仅仅是一个引用的副本,这样会带来巨大的危害(A浅拷贝得到B,A和B的引用类型的成员均指向同一块内存,A改变则B也受到了改变)。
所以使用protected修饰,强制用户实现接口,然后重写clone()方法,并将修饰符改为public,然后用户重写的clone()方法中除了使用super.clone()之外进行额外操作实现深拷贝。


基本的使用方式:

//实现接口后重写clone()方法
//Stack是测试用的栈模型对象
   @Override
   public Stack clone() {
	    try {
	    	//先调用Object的clone()实现浅克隆
	        Stack result = (Stack) super.clone();
	        //在将Stack中包含的引用类型全部克隆一遍(栈递归、数组遍历,具体看引用类型的模型)
	        //这里elements不能使final,因为final意味着引用不可变
	        result.elements = elements.clone();
	        //最终返回的克隆对象就是深克隆
	        return result;
	    } catch (CloneNotSupportedException e) {
	        throw new AssertionError();
	}
}

慎用clone()方法:

Object的clone()包含约定,(1)x.clone()!=x为true,x.clone().getClass() == x.getClass()为true。(2)且拷贝过程不调用构造器。这两点存在问题,首先良好的clone实现可以调用构造器创建空白对象然后复制内部数据,对于约定(2)要求继承链均使用super.clone()最终执行了Object的clone()。对于一个专门为继承而设计的类,如果未能提供行为良好的受保护clone方法,它的子类就不可能实现Cloneable接口。


拷贝构造函数就是用一个已有的对象new另一个对象

拷贝构造函数代替clone:

public class Date {
	    private int hour;
	    private int minute;
	    private int second;
	    // 构造方法
	    public Date() {
	 
	    }
	    // 拷贝构造方法
	    public Date(Date t) {
	        super();
	        this.hour = t.hour;
	        this.minute = t.minute;
	        this.second = t.second;
	    } 
	}

(5)comparable接口

接口提供比较方法,如果一个类对象存在内在大小关系可以实现这个接口:

public interface Comparable<T> {
    int compareTo(T t);
}

数组工具类Arrays的sort()源码如下:

//调用了DualPivotQuicksort排序法
public static void sort(int[] a) {
        DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
    }
//DualPivotQuicksort的sort()方法根据数组长度选择合适的排序算法
 static void sort(int[] a, int left, int right,int[] work, int workBase, int workLen) {
//短数组使用快排序
//省略
}

Arrayys的sort()调用方式:

Arrays.sort(a);

集合工具类Collections的sort()本质上也是调用Arrays的sort():

//Collections的sort()方法
public static <T> void sort(List<T> list, Comparator<? super T> c) {  
    Object[] a = list.toArray();  
    Arrays.sort(a, (Comparator)c);  
    ListIterator i = list.listIterator();  
    for (int j=0; j<a.length; j++) {  
        i.next();  
        i.set(a[j]);  
    }  
}  

接口方式:对于存储在集合中的Comparable对象进行搜索、计算极限值以及自动维护也是同样的简单。

3.类和接口

(1)使类、成员可访问性最小

良好的设计模块需要尽可能将信息封装到需求之内的最小可访问权限,使得模块之间高度松耦合。对于顶层类、接口仅两种访问权限,包级私有和公有。如果你做成了包级私有的,那么仅属于包内,不在包提供的外部api之中,在后续修改和升级时不用考虑外部客户端是否使用了这个类。如果你做成公有的,那么外部可以使用api访问,后续对该类的维护和升级必须不得不考虑永久支持外部客户端。

在设计类和接口时应该尽量使所有成员(5类)都是private的仅本类可以访问,当某成员一定需要在包内其他类使用时才使用默认缺省的访问权限。另一方面如果实例域(非静态的)不是final或者是可变final引用类型,如果这些域变为公有,那么外部的访问可能会破坏原有存储的值,将造成严重后果。如果一个类仅在另外一个类中用得到,那么应该考虑设计为私有嵌套类。

对于import导入:仅public类、接口才能导入。类成员的原有访问权限不变,即虽然导入了一个public类,但是这个类不属于本包,在本包中只能访问这个类的public域。

对于成员(五类:域,初始化块,构造器,方法,内部类、内部接口)包含四种访问权限:

私有的              private          (该类内部可访问)
包级私有的      缺省              (该类内部、包内可访问)
受保护的          protected     (该类内部、包内、该类子类可访问)
公有的              public           (均可访问)

(2)公有类中用方法访问(而非公有域)

公有域访问:public类中,public域可以被外部直接访问。

方法访问:public中,private域外部不能直接访问,但是提供public的get、set方法给外部使用。
前者坏处在于,如果 前期使用第一种方式设计类,那么众多客户端都通过直接的方式访问其公有域,假如后期想要修改这些域的表示方法或加上约束条件时需要一一修改所有客户端。
后者好处在于,提前保留了域的表示法后期修改的灵活性。
当然如果类是包级私有的、或者是私有嵌套类,直接public暴露其域没有什么问题。另一方面域是public  final的值、或者public  final引用的对象不可变,上述危害性就会小一点。

public class Person{
	//私有域
	private String name;
	//公有访问方法
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	} 
 }
(3)使可变性最小化

不可变类:类实例所包含的信息在创建对象时提供,整个生命周期内不可改变。

优缺点:可变性越小,越易于设计、实现和使用,不容易出错且更加安全。不可变对象本质上线程安全,不需要同步。但是对于大量类对象,大部分成员相同,小部分成员值不同就需要创建同等数量的对象,因为对象不能重用。

不可变类实现原则:

不提供可改变对象状态的方法
类不能被扩展
所有关键域为final、private
确保可变组件的互斥访问(指向可变域的引用不被客户端拿到,且不由客户端提供,防止被外部串改)

例如String类:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    //省略构造函数、其他函数
}

例如基本类型的包装类:

例如BigInteger类:

(4)复合优先于继承

继承使用覆盖来扩展子类功能,但是基类的后期改变会造成子类的破坏:

hashSet继承AbstractSet继承AbstractCollection。分别查看add、addAll方法源码。依次分析。

1.AbstractCollection把add()交给子类(hashSet实现),addAll()调用add()实现

//AbstractCollection类
public abstract class AbstractCollection<E> implements Collection<E> {
	 
	//add由HashSet或者其他集合自己实现
    public boolean add(E e) {
       throw new UnsupportedOperationException();
   }
   
   //公共的addAll有本基类调用add实现
   public boolean addAll(Collection<? extends E> c) {
       boolean modified = false;
       for (E e : c)
           if (add(e))
               modified = true;
       return modified;
   }

}

2.haseSet自己实现了add(),从父类继承了addAll()

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable
{
	//add方法
	public boolean add(E e) {
	    return map.put(e, PRESENT)==null;
}
}

3.可以看出上述方法由自用性(self-use),即顶层父类决定addAll()方法基于add()实现,其子类(hashSet)如果不重写addAll()就必须按照父类的规定用add()实现。假如我们在不知道这个性质的情况下继承HashSet,例如下面扩展了一个addCount参数在每次添加时记录次数。

public class InstrumentedHashSet<String> extends HashSet<String> {  

	private static final long serialVersionUID = 1L;
	//计数器
    private int addCount = 0; 

	//重写add(每次加就计数)
	public boolean add(String e) {
		addCount ++;
		return super.add(e);
	}
	//重写addAll(每次批量加就计数)
	public boolean addAll(Collection<? extends String> c) {
		addCount += c.size();
		return super.addAll(c);
	}
    
    public int getAddCount() {  
        return addCount;  
    }  

} 

可以看到,我们即想要在add()时也计数,又想要在addAll()时也计数,可是由于使用继承来扩展子类,本类的add()时可以正常计数,但是addAll()在计数之后又多次调用add()进行计数一共计数两次。这就可以看出继承的不足

复合与转发:如果本类想要扩展目标类功能,可以不直接继承目标类,而是继承转发类(转发类持有目标类的私有引用)。本类所有操作互不影响,而是全部转发给目标类的私有引用来处理。不受影响的原因在于这里由于没有使用继承,不需要强行满足父类定下的规则(自用性规则)。

1.创建转发类,其持有目标类set的私有引用

class ForwardingSet<E> implements Set<E> {
    //持有Set的引用
    private final Set<E> s;
    //转发类构造函数
    public ForwardingSet(Set<E> s) {this.s = s;}
    //转发类将所有实现都转发给私有域,通过目标类的私有引用来调用
    @Override
    public int size() {return s.size();}

    @Override
    public boolean isEmpty() {return s.isEmpty();}

    @Override
    public boolean contains(Object o) {return s.contains(o);}

    @Override
    public Iterator<E> iterator() {return s.iterator();}

    @Override
    public Object[] toArray() {return s.toArray();}

    @Override
    public <T> T[] toArray(T[] a) {return s.toArray(a);}

    @Override
    public boolean add(E e) {return s.add(e);}

    @Override
    public boolean remove(Object o) {return s.remove(o);}

    @Override
    public boolean containsAll(Collection<?> c) {return s.containsAll(c);}

    @Override
    public boolean addAll(Collection<? extends E> c) {return s.addAll(c);}

    @Override
    public boolean retainAll(Collection<?> c) {return s.retainAll(c);}

    @Override
    public boolean removeAll(Collection<?> c) {return s.retainAll(c);}

    @Override
    public void clear() {s.clear();}
    

2.创建扩展类,继承上面的转发类

public class InstrumentedSet<E> extends ForwardingSet<E>{

    private int addCount = 0;
    
    public InstrumentedSet(Set<E> s) {
        super(s);
    }
 
    @Override
    public boolean add(E e) {
        addCount ++;
        return super.add(e);
    } 
    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }
   
    public int getAddCount() {
        return addCount;
    }
}

3.可以看到,本类再次使用addAll()方法在批量加入的同时计数,这时addAll()被转发给Set的私有引用来处理了,不再受本类的add()方法的影响。不受影响的原因在于这里由于没有使用继承,不需要强行满足父类定下的规则(自用性规则)。

(5)为继承设计,否则禁止继承

对于专门为了继承而设计并且具有良好文档说明的类而言,该类的文档必须精确地描述覆盖每个方法所带来的影响。该类必须有文档说明它可覆盖的方法的自用性。对于每个公有的或受保护的方法或者构造器,它的文档必须指明该方法或者构造器调用了哪些可覆盖的方法,是以什么顺序调用的,每个调用的结果又是如何影响后续的处理过程的。更一般的,类必须在文档中说明,在哪些情况下它会调用可覆盖的方法。避免因父类方法的自用性等不安全元素为子类带来困扰。

某些类就是专门为了继承而设计的类,这种类必须以文档方式精确描述每个可覆盖方法在被子类覆盖时会带来的影响。这里可覆盖的方法指子类能够重写且可以访问的方法即final、public或protected的。

例如查看AbstractCollection的remove()方法的文档:

/*
Note that this implementation throws an UnsupportedOperationException if the iterator returned by this collection's iterator method does not implement the remove method and this collection contains the specified object.
*/
/*
如果集合的iterator()方法返回的迭代器没有实现remove()方法就会抛出UnsupportedOperationException 异常
*/
boolean java.util.AbstractCollection.remove(Object o)

父类可以提供某种钩子(特意让子类重写的方法),让子类进入并修改原始类,当子类调用A时会使用建议的钩子B。下面以父类AbstractList和子类LinkedList依次分析。

1.AbstractList类

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
	
    //1.clear()方法
    public void clear() {
        removeRange(0, size());
    }
    
	//2.removeRange()方法:for循环调用迭代器的remove()
    protected void removeRange(int fromIndex, int toIndex) {
        //使用4提供的迭代器内部类实现,获得一个处于fromIndex之前列表的迭代器,然后重复调用迭代器的remove()方法
        ListIterator<E> it = listIterator(fromIndex);
        for (int i=0, n=toIndex-fromIndex; i<n; i++) {
            it.next();
            //调用的是迭代器的remove()方法
            it.remove();
        }
    }
    
    //3.remove()是list接口继承自Iterable接口的方法,只是抛出异常,具体交给子类实现
    public E remove(int index) {
        throw new UnsupportedOperationException();
    }
    
    //4.返回一个迭代器对象的内部类实现
    public ListIterator<E> listIterator(final int index) {
        checkForComodification();
        rangeCheckForAdd(index);

        return new ListIterator<E>() {
            private final ListIterator<E> i = l.listIterator(index+offset);
            //迭代器提供了remove()方法
            public void remove() {
                i.remove();
                SubList.this.modCount = l.modCount;
                size--;
            }

        };
}

父类clear()方法调用的是removeRange()方法,removeRange()中调用的是迭代器的remove()方法。removeRange()用于删除子列表(sublist),在list的子list调用clear()时如果不重写remove()就不得不使用上面迭代器给的低效率方法,
2.LinkedList类

//子类LinkedList
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    //2.remove()方法:子类自己实现了更高效率的子列表删除
    public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    } 
}

父类如果是为继承而设计的类,那么构造器决不能调用能被覆盖的类。因为超类的构造器会最先执行如果超类调用了可被覆盖的方法,那么子类覆盖版本的方法将会在子类构造器之前调用。测试代码如下:

//超类
public class Super() {  

	//超类构造器调用可被覆盖的方法
    public Super() {  
        overrideMe();  
    }  
    public void overrideMe() {}  
}  
  
public final class Sub extends Super {  
    private final Date date;  
    Sub() {  
        date = new Date();  
    }  
      
    //超类的覆盖版本
    @Override  
    public void overrideMe() {  
        System.out.println(date);  
    }  
      
    public static void main(String[] args) {  
        //超类构造方法调用时,会调用子类覆盖版本的overrideMe()即打印Date,由于此时子类构造器还未将Date初始化会打印null
        Sub sub = new Sub(); 
        //子类构造方法调用后,Date才被初始化
        sub.overrideMe(); 

    }  
}  
(6)接口优于抽象类

想要实现由抽象类定义的类型,就必须继承这个抽象类。对于多层次结构的继承关系,从最顶层父类到下面各个子类,在某些情况下扩展困难:C1继承B1继承A,C2继承B2继承A,现在引入一个新扩展方法需要B1和B2来扩展,那么只能将他们的公有祖先A去继承一个新的抽象类,这样伤害了类层次,迫使A所有后代(B1、B2、B3、B4等)都扩展新抽象类虽然他们并不需要。

使用接口就可以在继承关系的任意层次进行方法插入。

接口让我们可以定义水平结构的而非层次接口的类型框架:

//接口A
public interface A{
	public void A();
}
//接口B
public interface B{
	public void B();
}

//对于已有的某些接口,组织到同一层
public interface AB extends A,B{
	public void A();
	public void B();
}

//将不同的扩展方法组织到同一层,在不能严格利用继承链的层次关系来组织的情况下很有用
public class Person implements AB{
	public void A(){
		
	}
	public void B(){
		
	}
}

骨架实现(抽象类可以实现方法但是依赖于继承,接口不能实现方法但是比继承更灵活。两者优点结合,可以设计一个专门用于继承的骨架实现类):

1.Map.Entry接口:

//Map接口
public interface Map<K,V> {
	  //内部接口Entry
	  interface Entry<K,V> {
	        K getKey();
	        V getValue();
	        V setValue(V value);
	        boolean equals(Object o);
	        int hashCode();
	    }
}

2.Map.Entry的骨架实现类

//Map.Entry<K, V>的骨架实现类
public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V>{
	 
    //将一部分接口方法抽象
    public abstract K getKey();
    public abstract V getValue();
     
    //将其余的接口方法给出实现(依赖于未实现的抽象方法)
    public V setValue(V value) {
    	//省略
    }
    public boolean equals(Object obj) {
    	if (obj == this) {
            return true;
        }
        if (!(obj instanceof Map.Entry)) {
            return false;
        }
        Map.Entry<?, ?> arg = (Map.Entry) obj;
        //依赖于未实现的抽象getKey()、getValue()方法
        return equals(getKey(), arg.getKey()) && equals(getValue(), arg.getValue());
    }    
    private static boolean equals(Object obj1, Object obj2) {
    	//省略
    }
    public int hashCode() {
    	//省略
    }
    private static int hashCode(Object obj) {
        //省略
    }
}

3.可以看到,骨架实现类就是实现目标接口,接口重要方法(最基本的、其他方法依赖于这些方法的具体实现)抽象化并交给子类实现,接口其他非基础方法给出具体实现。且设计为继承良好的类专门用于子类继承。其优点在于,已经给出具体实现的方法子类可以原封不动的使用,也可以看情况覆盖。对于没有给出具体实现的方法,由子类自己实现。另一方面,由于专门为继承而设计,有很多子类,且后期出现新的方法需要子类扩展时,直接在抽象类中扩展就可以了子类可以选择重写或直接使用抽象类默认实现。因为在公有接口中添加新方法,所有实现类想要通过编译关需要一一增加接口方法。

(7)接口只定义类型

接口可以引用其接口实现类充当type的角色,为了任何其他目的而定义接口是不恰当的。

常量接口就违背了上述原则,这种接口不包含抽象方法,仅仅包含静态的final常量,类继承这种接口可以避免用类名修饰常量名:

//ObjectStreamConstants定义的常量接口
public interface ObjectStreamConstants {
    final static short STREAM_MAGIC = (short)0xaced;
    final static short STREAM_VERSION = 5;
    final static byte TC_BASE = 0x70;
    final static byte TC_NULL = (byte)0x70;
    //其他省略
}

//可以直接访问常量,而不以类名访问
public class Person implements ObjectStreamConstants{
	
	public void test(){
		System.out.println(STREAM_MAGIC);
	}

}

使用这种常量接口缺点在于:类使用的这些常量的实现细节会泄露到类导出api中,且其子类也会受到这种影响。正确的导出常亮的方式在于,将某个类紧密使用的常量添加到这个类中。接口应该仅用来表示实现类type,而非导出常量。

(8)类层次优于标签类

标签类就是指明标签属性,然后将不同标签值的实现挤在一个类中:

public class Figure1{
	//指明标签有哪些类型
    enum Shape {
        RECTANGLE,
        CIRCLE
    }
    //标签
    final Shape shape;
    //类型1的属性
    double length;
    double width;
    //类型2的属性
    double radius;
    //类型1的实现
    public Figure1(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }
    //类型2的实现
    public Figure1(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }
     
    double area() {
        switch (shape) {
        case RECTANGLE:
            return length * width;
        case CIRCLE:
            return Math.PI * (radius * radius);
        default:
            throw new AssertionError();
        }
    }
}

可以看到标签类的缺点:创建不同标签类型的对象时(比如这里创建圆、方两个对象),他们相互包含了对方的属性和实现虽然这些东西自己并不需要。且类冗长。

使用继承代替标签类,不同子类属性还可以final修饰:

abstract class Figure2 {
    abstract double area();
}
 
class Circle extends Figure2 {
    final double radius;
     
    Circle(double radius) {
        this.radius = radius;
    }
     
    double area() {
        return Math.PI * (radius * radius);
    }
}
 
class Rectangle extends Figure2 {
    final double length;
    final double width;
     
    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }
    double area() {
        return length * width;
    }
}

(9)用函数对象表示策略

策略模式:方法调用者传入第二个函数,来指定当前调用函数的策略。这里分析数组工具类Arrays的sort(a,c)方法来分析策略模式。

1.Arrays工具类的sort(T,Comparator)方法底层使用归并排序时,利用传入的Comparator接口方法compare(T,T)来作为大小关系的比较依据。源码如下:

//数组工具类Arrays
public class Arrays {
	//传入待排排序数组、Comparator接口(即排序规则)
    public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a);
        } else {
        	//利用java.security.AccessController.doPrivileged产生true值
            if (LegacyMergeSort.userRequested)
            	//调用legacyMergeSort
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }
	
    //legacyMergeSort
    private static <T> void legacyMergeSort(T[] a, Comparator<? super T> c) {
        T[] aux = a.clone();
        if (c==null)
            mergeSort(aux, a, 0, a.length, 0);
        else
        	//调用归并排序
            mergeSort(aux, a, 0, a.length, 0, c);
    }
    
    //归并排序
    private static void mergeSort(Object[] src,
                                  Object[] dest,
                                  int low, int high, int off,
                                  Comparator c) {
        int length = high - low;
        //短数组用插入排序
        if (length < INSERTIONSORT_THRESHOLD) {
            for (int i=low; i<high; i++)
            	//利用Comparator接口的排序规则判断数组元素大小关系
                for (int j=i; j>low && c.compare(dest[j-1], dest[j])>0; j--)
                    swap(dest, j, j-1);
            return;
        }
        int destLow  = low;
        int destHigh = high;
        low  += off;
        high += off;
        int mid = (low + high) >>> 1;
        mergeSort(dest, src, low, mid, -off, c);
        mergeSort(dest, src, mid, high, -off, c);

        //同样利用了Comparator接口的排序规则
        if (c.compare(src[mid-1], src[mid]) <= 0) {
           System.arraycopy(src, low, dest, destLow, length);
           return;
        }

        for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
            if (q >= high || p < mid && c.compare(src[p], src[q]) <= 0)
                dest[i] = src[p++];
            else
                dest[i] = src[q++];
        }
    }
}

2.由于策略用接口方法来表达,具体的策略由接口实现类来表达,通常用接口创建匿名实现作为策略传入方法中。调用方式如下:

//使用接口创建匿名内作为排序规则.这里指定数组元素长度为排序依据
Arrays.sort(new String[]{"1","2","ssss"}, new Comparator<String>() {  
    public int compare(String o1, String o2) {  
         return o1.length()-o2.length();  
    }  
});  

3.上述使用接口匿名实现来作为策略在这个策略仅仅被使用一两次时才这么做。如果一个策略被大量重复使用,例如上面的示例,这个策略应该以final私有内部类存在于Arrays中,比如:

final private Tclass implement Comparator<String>(){
	public int compare(String o1, String o2) { 
	return o1.length()-o2.length(); 
	} 
}
并且Arrays提供一个公有的静态final域将其引用导出:

public static final Tclass = new Tclass();
(10)优先考虑静态成员类

一共有四种嵌套类(定义在类内部):

静态成员类:是外部类的静态成员,与类其他静态成员一样可以访问包括私有的所有成员。如果是私有静态成员类,那么仅外部类的内部才能访问。

非静态成员类:是外部类的非静态成员,非静态成员类实例都与外部类实例相关联,所以需要先创建外部类对象才能创建内部类对象。非静态成员类的方法内部可以调用外部类的实例方法。

匿名类:在使用时同时被申明实现、实例化

局部类:

如果申明成员类不要求访问外部实例,那么可以将成员类申明为静态的。如果省略了static关键字,非静态成员类的每个实例都包含一个外部类对象的引用,很多时候使得本该被GC的外部类实例不能被回收,有时候仅仅想使用内部类实例却不得不先创外部类实例。因此应该优先考虑静态成员类。

1.例如HashMap的静态成员内部类Node。源码如下:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
	
	//Node代表每一个键值对
	static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        //每个Node包含下一个Node的引用
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        //下列均是Entry的接口方法,由于这些方法仅仅访问Node本身的成员,不需要访问外部HashMap的实例,所有Node被申明为静态成员内部类
        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
}

可以看到HashMap的多个键值对,对应于多个Node实例,由于不要求Node实例的接口方法访问HashMap实例,所以Node被申明为静态成员内部类。

2.例如HashMap的非静态成员内部类HashIterator。源码如下:

//HashMap
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
	
	//这个成员记录HashMap结构被改变的次数
	transient int modCount;
	
	//抽象的非静态成员内部类
    abstract class HashIterator {
    	
        Node<K,V> next;        
        Node<K,V> current;     
        int expectedModCount;  
        int index;            

        //构造函数
        HashIterator() {
        	//比如这里需要访问HashMap的实例成员,所有HashIterator被申明为非静态
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        //省略 public final boolean hasNext() {..}
        //省略 final Node<K,V> nextNode() {..}
        //省略 public final void remove() {..}
    }

    final class KeyIterator extends HashIterator
        implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }
}

可以看到,由于HashIterator的接口方法需要访问HashMap的实例成员,只能申明为非静态成员内部类。

4.泛型

一般集合类设计时都要求能够操作任意类型的对象,但是具体保存什么类型由用户指定,所以以前使用Object申明集合类可以存储任意对象,在取出时强制类型转换一下,这样的坏处在于类型不安全(不能保证强制类型转换可以成功,编译器也不知道是否运行时会转换报错)。后来才出现了泛型,使得可以在编译期保证存入集合的对象复合目标类型,这样在后期就不会强转报错。

(1)新代码避免使用原生态类型

首先在泛型出现以前,集合接口、实现类等都是原生态类型(不指定参数类型比如List),使得添加不同的类型对象到List中可以通过编译,但是在后期操作比如迭代中进行类型转换时才报错。而泛型出现使得类型检查由编译器完成,将可能出现的类型转换异常提前到编译期。运行期间泛型信息会被擦除,因为能够通过编译那么说明参数类型已经被确定了。

原生态类型:就是不提供任何类型参数,编译期不进行泛型检查。比如:List

参数化类型:就是提供具体的类型参数,编译期进行泛型检查。比如:List<Sring>

a.不要使用原生态类型作为方法参数

示例代码如下:

public static void main(String[] args) { 
	
	//指定参数类型为String
    List<String> lists = new ArrayList<String>(); 
    //插入一个Intger对象:由于方法使用了原生态类型使得插入时逃过了编译期的类型检查
    unsafeAdd(lists, new Integer(42)); 
    //get时由编译器将得到的对象自动转为String类型,报类型转换异常
    String s = lists.get(0);  
}  
  
//方法参数为原生态类型,不进行编译时的类型检查
private static void unsafeAdd(List list, Object o) {  
    list.add(o);  
}  

在这里,main函数中使用泛型的初衷是好的,但是在调用unsafeAdd的时候却使用了原生态类型,这样就失去了泛型的类型检查机制,将非String类型的元素添加到了集合里,所以,将使用get方法取出这个错误元素将将其赋给String类型的时候编译出错了。

b.不确定集合元素类型时,用不限制通配符类型代替原生态类型

		//不限制通配符类型
		//由于编译器不知道具体类型,所以只能操作null
	    List<?> lists = new ArrayList<>();
	    lists.add(null);

使用?来表明不知道具体操作类型,但是必须提供编译期类型检查机制。防止破坏就集合的类型安全机制。

c.泛型类型擦除,使得instanceof不能一起使用

首先我们知道类型擦除使得java的泛型只是“伪泛型”,即类型只在编译器存在,编译器之后的运行期jvm并不知道泛型的存在。比如下面的代码:

        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        //这里返回true
        System.out.println(c1 == c2);

类似下面代码编译不能通过:

	Person<Integer> p = new Person<Integer>();
	if(p instanceof Person<Integer>){ }

但是改为不限定通配类型则能够通过编译:

	Person<Integer> p = new Person<Integer>();
	if(p instanceof Person<?>){  }
(2)消除运行时异常警告

消除所有的非受检异常可以确保类型安全,意味着不会在运行时出现类型转换异常。在报警告的地方如果能够确保代码类型安全,那么可以使用注解(unchecked)忽略警告,且尽量将注解范围缩到最小。

(3)列表优于数组

数组和列表使用类似的方式保存数据。

a.数组是协变的,泛型是不可变的

数组是协变的(若sub是super的子类,sub[ ] 是 super[ ]的子类),而使用泛型的列表没有这种特性(List<Type1>与List<Type2>没有子类或超类的关系)。所以后者可以使得类型转换异常提前到编译器,代码如下:

	//编译通过
	Object[] objectArray = new Long[1];
	//运行时抛出ArrayStoreException
	objectArray[0] = "test";

	//编译不通过
	List<Object> list = new LinkedList<Long>();
	list.add("test");

b.除非使用通配符,否则数组的元素不能是泛型的。为了类型安全性,泛型数组不能通过编译

从上面代码可以发现,泛型的出现可以保证类型安全将类型转化异常提前到编译器,但是数组由于协变使得类型转化异常在运行时才被抛出。而泛型数组虽然使用了泛型,但是使用数组导致仍然不保证类型安全,所以jvm编译期对泛型数组的申明不能通过。例如下面代码为了防止类型转化异常推迟到运行时才抛出,直接在申明泛型数组时编译不通过,代码如下:

	//申明泛型数组,编译不通过
	List<String>[] lsa = new List<String>[10];
	Object o = lsa;
	Object[] oa = (Object[]) o;
	List<Integer> li = new ArrayList<Integer>();
	li.add(new Integer(3));
	//添加不符合类型的元素到数字中,在能够申明泛型数组的情况下这里不会报错,因为运行时擦除了类型信息
	oa[1] = li;
	//为了避免将类型转化异常推迟到此处才抛出,所以jvm让泛型数组申明在编译器失败
	String s = lsa[1].get(0); 

如果非要申明List<String>[ ],那么可以使用List<?>[ ]使得编译通过,但是这样就把类型转换异常推迟到了运行时,丢失了泛型的类型安全特性。代码如下:

	//将类型参数改为不限定通配符类型,编译器会放行
	List<?>[] lsa = new List<?>[10];
	Object o = lsa;
	Object[] oa = (Object[]) o;
	List<Integer> li = new ArrayList<Integer>();
	li.add(new Integer(3));
	//虽然你只想数组存放List<String>,但是能够存放List<Integer>进去,因为运行时擦除了类型信息
	oa[1] = li;
	//这里使用强制类型转换才能绕过编译器,尽管在运行时还是会抛出类型转换异常
	String s =  (String)lsa[1].get(0);     
(4)优先考虑泛型、泛型方法

泛型是一种类型安全机制:让用户指定操作类型,在用户操作其他类型时以编译失败的方式告诉用户所操作的对象不符合前面的类型规定,将后期可能出现的类型转换异常让用户提前知晓。

a.优先考虑泛型类

现在用泛型数组实现一个栈,由于引用泛型数组会编译失败,现在用两种方式实现泛型数组将栈类泛型化。

方法1,代码如下:

//解决方式1
public class STACK<E> {
	 //创建泛型数组
	 private E[] elements;
	 private int size = 0;
	  
	 public STACK() {
		 //Object[]强转为目标类型  E[]
		 //编译时E类型确定了,所以上述转换可以通过编译
		 //一条警告:类型不安全
		 elements = (E[]) new Object[16];  	
	}
	 //入栈
	 public void push(E e) {
	     elements[size++] = e;
	  }
	 //出栈
	 public E pop() {
	      E result = elements[--size];
	      elements[size] = null;
	      return result;
	  }
	 
	  public static void main(){
			//编译失败
			new STACK<String>().push(new Date());
	}

}

方法2,代码如下:

//解决方式2
public class STACK2<E> {
	//Object数组
	private Object[] elements;
	private int size = 0;

	//泛型数组的创建
	public STACK2() {
		elements = new Object[16];
	
	}
	//入栈
	public void push(E e) {
	    elements[size++] = e;
	}
	//出栈
	public E pop() {
		    //一条警告:类型不安全
		    E result = (E) elements[--size];
		    elements[size] = null;
		    return result;
		}
	
	public static void main(){
		//编译失败
		new STACK2<String>().push(new Date());
	}

}

b.优先考虑泛型方法

方法参数不应该使用原生态类型,改为使用泛型可以消除警告,代码如下:

	//3条警告:类型不安全
	public static Set union(Set s1, Set s2) { 
	    Set result = new HashSet(s1);  
	    result.addAll(s2);  
	    return result;  
	}  
	
	//可以消除警告
	public static <E> Set<E> union(Set<E> s1, Set<E> s2) {  
	    Set<E> result = new HashSet<E>(s1);  
	    result.addAll(s2);  
	    return result;  
	}  

静态泛型方法可以使泛型集合申明变得简洁,代码如下:

	//类似的申明两边泛型信息冗余
	Map<String, List<String>> map =   new HashMap<String, List<String>>(); 
	
	//提供静态的方法,省略右侧的泛型信息
	public static <K, V> HashMap<K, V> newHashMap() {  
	    return new HashMap<K, V>();  
	}  
	Map<String, List<String>> map =  newHashMap();

参数类型限制,可以更加详细的指定类型的特点。比如可以指定参数必须是一种实现了Comparable接口的类型,代码如下:

public static <T extends Comparable<T>> T max(List<T> list) {  
    ...  
}  
(5)利用有限通配符提升API灵活性

虽然Integer 继承自Number,但在逻辑上Math<Number>不能视为Math<Integer>的父类。通配符用于在逻辑上表示子类、父类关系。即Math<?>在逻辑上是Math<Integer>、Math<Number>...等所有Math<具体>的父类。可以放宽编译时类型检查的范围,如下:

上限:<? extends T> ?是T和T的子类
下限:<? super T> ?是T和T的父类
无限:< ? > 不限制?具体类型

注意这个与申明泛型类、泛型方法时限定泛型特点有区别,比如下面泛型类,申明类操作的类型必须实现了Comparable接口,代码如下:

//限定泛型类操作的T类型必须实现Comparable接口
public class Math<T extends Comparable<T>>{

}

a.不能用原始类型作为方法形参,应该用泛型,而且可以进一步使用有限制通配符扩展泛型范围

泛型类:

public class Math<T>{
	//数据
	private T data;
	//数据初始化
	Math(T data){
		this.data = data;
	}
	//获取数据
	public T getDate(){
		return data;
	}
}

测试方法:

public class TestMath {
	
	
	//测试
	public static void main(String args[]){
		
		Math<String> stringData= new Math<>("data");
		Math<Integer> integerData= new Math<>(10);
		Math<Number> numberData= new Math<>(20);
		
		//无限制通配符:?
		get_All_Date(stringData);
		get_All_Date(integerData);
		get_All_Date(numberData);
		
		//上线通配符:? extends Number
		get_Number_Date(stringData);//这一条编译错误
		get_Number_Date(integerData);
		get_Number_Date(numberData);
		
		//下线通配符:? super Integer
		get_Integer_Date(stringData);//这一条编译错误
		get_Integer_Date(integerData);
		get_Integer_Date(numberData);
	}

	//无限制通配符:?
	//可以接收Math<String>、Math<Integer>、Math<Number>、等任意类型。
	public static void get_All_Date(Math<?> math){
		System.out.println("data :" + math.getDate());
	}
	
	//上线通配符:? extends Number
	//可以接收Math<Number>的子类Math<Integer>
	public static void get_Number_Date(Math<? extends Number> math){
		System.out.println("data :" + math.getDate());
	}
	
	//下线通配符:? super Integer
	//可以接收Math<Integer>的父类Math<Number>
	public static void get_Integer_Date(Math<? super Integer> math){
		System.out.println("data :" + math.getDate());
	}
}
(6)优先考虑类型安全的异构容器

java的集合仅提供了参数化的容器,即仅能够映射同类型的对象。通常想要使用异构容器(例如Map用键绑定任意类型的值)必须考虑他的类型安全问题。

下面给出一个简单的、非类型安全的异构容器实现。代码如下:

//上下文类
public class Context {
	 
	  //想要用String类型的键映射不同类型的值
	  private final Map<String,Object> values = new HashMap<>();
	 
	  public void put( String key, Object value ) {
	    values.put( key, value );
	  }
	 
	  public Object get( String key ) {
	    return values.get( key );
	  }
	  
	  
	  //测试
	  public static void main(){
		  //上下文类
		  Context context = new Context();
		  
		  //Runnable类型
		  Runnable runnable = new Runnable() { 
			  //略
		  };
		  //put
		  context.put( "key", runnable );
		  //get
		  Runnable value = ( Runnable )context.get( "key" );//缺点:这里需要强转
		  
		  //Executor类型
		  Executor executor = new Executor() {
			//略
		  };	  
		  //替换value(操作本不应该被允许,但是这里编译不提供检查所以察觉不到这种误操作,类型不安全)
		  context.put( "key", executor );
		  //get
		  Runnable value = ( Runnable )context.get( "key" );//类型不安全:强转报错
	  }	  
}

可以看到,上面实现不保证类型安全,一个键映射的值类型A被某种操作更改B,后期取值时用A去强转会抛出异常造成类型不安全。

考虑到类型安全问题,提供另一种思路:jdk1.5后Class类被泛型化为Class<T>,即String.class对象属于Class<Sring>类型,Integer.class对象属于Class<Integer>类型。所以我们把valueObject.class对象作为key,使用Class<T>作为key的类型利用其泛型的类型检查机制实现类型安全。

//上下文类
public class Context {
	 
	  //以Class对象映射不同类型的值
	  private final Map<Class<?>, Object> values = new HashMap<>();

	  public <T> void put( Class<T> key, T value ) {
	    values.put( key, value );
	  }	 
	  public <T> T get( Class<T> key ) {
		//valueObject.getClass().cast(valueObject)==valueObject
	    return key.cast( values.get( key ) );
	  }   
	  
	  //测试
	  public static void main(String args[]){
		  //上下文类
		  Context context = new Context();
		  
		  //Runnable类型
		  Runnable runnable = new Runnable() {
			//略
		};
		  //put
		  context.put( Runnable.class, runnable );
		   
		  //Executor类型 
		  Executor executor = new Executor() {
			//略
		};
		  //put 
		  context.put(Executor.class, executor ); 
		  
		  //类型安全:通过Runnable.class的键来保存Executor类型的值会编译失败
		  //get
		  Runnable value = context.get( Runnable.class );//类型安全,不可能出现类型转换错误
		  //put 
		  context.put(Runnable.class, executor );//本条编译失败
	  }
}

但是上面实现并不是实用的,因为以valueObject的Class对象作为key,使得一种valueObject的Class对象只能映射一个valueObject,比如整个集合只能保存一个Runnable类型的值,也只能保存一个Executer类型的值。现在将key封装到一个类中,再添加一个属性来区分同一种类型的不同value。代码如下:

//key类
public class Key<T> {
	 
	  //利用Class<T>保证类型安全
	  final Class<T> type;
	  //再添加一个属性,找到T类型下不同的value
	  final String identifier;
	  
	  public Key( String identifier, Class<T> type ) {
	    this.identifier = identifier;
	    this.type = type;
	  }
}
//上下文类
public class Context {
	
	  private final Map<Key<?>, Object> values = new HashMap<>();
	  //put
	  public <T> void put( Key<T> key, T value ) {
	    values.put( key, value );
	  }
	  //get
	  public <T> T get( Key<T> key ) {
	    return key.type.cast( values.get( key ) );
	  }
	  
	  //测试
	  public static void main(){
		  //上下文
		  Context context = new Context();
		  
		  //Runnable类型
		  Runnable runnable1 = ...
		  Key<Runnable> key1 = new Key<>( "id1", Runnable.class );
		  context.put( key1, runnable1 );
		   
		  //Runnable类型
		  Runnable runnable2 = ...
		  Key<Runnable> key2 = new Key<>( "id2", Runnable.class );
		  context.put( key2, runnable2 );  

	  }
}

5.枚举和注解

(1) enum代替int常量

在java1.5之前,表示枚举类型的常用模式是声明一组具名的int常量,这里申明两类int常量:

public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;

这种方式包含很多缺点:

将apple传到想要orange的方法中,编译器不能提示类型错误
用==操作符将apple与orange比较,在int值相等情况下返回了true,即类型概念比较模糊
遍历一个组中所有的int枚举常量,获得int枚举组的大小,没有可靠的方法
其他

a.使用枚举代替int常量

public enum Apple {FUJI, PIPPIN, GRANNY_SMITH}
public enum Orange {NAVEL, TEMPLE, BLOOD}

优点:

枚举是功能齐全的类,通过公有的静态final域为每个枚举常量导出实例的域。因为没有可以访问的构造器,客户端不能创建枚举类型的实例,也不能扩展它。
枚举提供编译时的类型安全,如果一个参数的类型是Apple,就可以保证,被传入到该参数上的任何非null对象引用一定是FUJI,PIPPIN,GRANNY_SMITH三个之一。
包含同名常量的多个枚举类型可以共存,因为每个类型有自己的命名空间,增加或重新排列枚举类型的常量,无需重新编译客户端代码。

b.每个常量可以与特定的行为相关联

很多时候,不同的常量对同一个行为有不同的实现,正好枚举类型支持方法和域,于是抽象一个静态方法对不同的枚举类型给出不同的实现。比如下面的计算器,代码如下:

public enum Operation {
    PLUS("+") {
        double apply(double x, double y) {return x + y;}
    },
    MINUS("-") {
        double apply(double x, double y) {return x - y;}
    },
    TIMES("*") {
        double apply(double x, double y) {return x - y;}
    },
    DIVIDE("/") {
        double apply(double x, double y) {return x - y;}
    };
    private final String symbol;
    Operation(String symbol) {
        this.symbol = symbol;
    }
    public String toString() {
        return symbol;
    }
    abstract double apply(double x, double y);
}

//测试
	public static void main(String[] args) {
	    double x = 2.0;
	    double y = 4.0;
	    //加
	    Operation.PLUS.apply(x, y);
	    //减
	    Operation.MINUS.apply(x, y);
	    //乘
	    Operation.TIMES.apply(x, y);
	    //除
	    Operation.DIVIDE.apply(x, y);
	}

优点:

在添加枚举常量时,不会忘记添加他的行为实现,编译器会提醒你重写他的抽象方法。

(2) 实例域代替序数

枚举类都有一个ordinal()方法用于返回一个int值,这个值是按照申明顺序排列,代码如下:

public enum Ensemble {
  SOLO, DUET, TRIO, QUINTET, SEXTET, SEPTET ,OCTET, NONET, DECTET;
  public int numberOfMusicians(){
    return ordinal() + 1;
  }
}

虽然提供了这个方法,但是我们不应该将序数类的需求依赖于ordinal()方法,如果常量进行重新编译,numberOfMusicians方法就会遭到破坏。永远不要根据枚举的序号导出与它有关的值,而是要将它保存到一个实例中,代码如下:

public enum Ensemble{
	  //构造方法中传入序数
	  SOLO(1), DUET(2), TRIO(3), QUINTET(4), SEXTET(5), 
	  SEPTET(7), OCTET(8), DOUBLE_QUARTET(8), NONET(9),
	  DECTET(10), TRIPLE_QUARTRT(12);
	  //实例域
	  private final int numberOfMusicians;
	  //用实例域来接收序数
	  Ensemble(int size){
	    this.numberOfMusicians = size;
	  }
	  //返回序数
	  public int numberofMusicians(){
	    return numberOfMusicians;
	  }
	}
(3) EnumSet代替位域

位域:

0和1 两种状态, 用一位二进位即可。
为了节省存储空间,并使处理简便,提供了一种数据结构,称为“位域”或“位段”。
所谓“位域”是把一个字节中的二进位划分为几 个不同的区域,并说明每个区域的位数。
每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

下面使用int常量的方式来表示位域,代码如下:

//Bit field enumeration constant - OBSOLETE
public class Test {
    //位域名:位域长度
    public static final byte STYLE_BOLD          = 1<<0; // 1
    public static final byte STYLE_ITALIC        = 1<<1; // 2
    public static final byte STYLE_UNDERLINE     = 1<<2; // 4
    public static final byte STYLE_STRIKETHROUGH = 1<<3; // 6

    //Parameter is bitwise OR of zero or more STYLE_ constants
    public void applyStyles(int styles) { ... }
}

使用EnumSet代替,代码如下:

//EnumSet
public class Text {
  public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH };

  //使用Set存放枚举值
  public void applyStyles(Set<Style> styles) { 
      System.out.println(styles);
  }

  public void test() {
      applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
  }
}

//执行 test(),输出 [BOLD, ITALIC]。
(4) EnumMap代替序数索引

经常会碰到使用Enum来罗列类成员的有限个取值,代码如下:

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;
    }
}

上述类有type和name两个属性,其中type为有限个已知的取值。现在给你一个大集合(包含很多个Herb实例),想要你将这个Herb实例以type分组,java.util.EnumMap是一种非常快速的Map实现专门用于枚举的键。代码如下:

//假如每种type有很多种不同name,想要以type划分并罗列出来
//形如:
//{type1:Herb1,Herb2,Herb3...}
//{type2:Herb4,Herb5...}
//{type3:Herb6,Herb7,Herb8...}
Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<Herb.Type, Set<Herb>>(Herb.Type.class);

//遍历枚举类的所有取值
for(Herb.Type t : Herb.Type.values)
  //以type枚举值为键,以新建的Set集合为值
  herbsByType.put(t, new HashSet<Herb>());

//遍历已知的Herb大集合(garden)
for(Herb h : garden){
	//按照枚举值(键)拿到对应的集合,将它放入Set中
	herbsByType.get(h.type).add(h);
}
(5) 接口模拟可伸缩枚举

在java里枚举无法通过继承来实现扩展,但可以用接口来模拟。

基础接口,代码如下:

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;  
    }  
    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;  
    private ExtendedOperation(String symbol) {  
        this.symbol = symbol;  
    }  
    public String toString() {  
        return symbol;  
    }  
}  

客户端代码:

public class Main {  
  
    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));  
    }  
      
    public static void main(String[] args) {  
        double x = 2.0;  
        double y = 4.0;  
        test(ExtendedOperation.class, x, y);  
    }  
}  

在可以使用枚举基类的任何地方 , 也都可以使用这些枚举基类的扩展类。实际上是用接口实现了枚举类型的可伸缩性。

(6) 注解优于命名模式

Java注解能够提供代码的相关信息给jvm,同时对于所注解的代码结构又没有直接影响。注解出现之后表明了一种观点:既然有了注解,就没必要使用命名模式了。例如junit测试框架原本要求他的用户一定要用test作为测试方法的命名开头,假如测试方法写错不合要求,那么junit不会走这个测试方法,程序员会以为测试成功。

现使用注解写一个简单的测试框架,被注解标注的方法表明是测试方法,测试结果为成功(代码段不报错)或者失败(代码段抛出异常)。注解申明如下:

//运行时保留
@Retention(RetentionPolicy.RUNTIME)
//作用范围方法级别
@Target(ElementType.METHOD)
public @interface Test {
}

测试类,部分方法使用@Test标注,表明是测试方法:

public class Sample {
    @Test 
    public static void m1() {
    	//测试通过
    }
    public static void m2() {
        //不进行测试
    }
    @Test 
    public static void m3() {
    	//测试失败
    	//失败原因
        throw new RuntimeException("异常抛出1");
    }
    @Test 
    public static void m4() {
    	//测试失败
    	//失败原因
        throw new RuntimeException("异常抛出2");
    }  
}

反射调用测试方法,并拿到测试结果成功与否,失败原因。代码如下:

public class RunTests {

    public static void main(String[] args) throws ClassNotFoundException {
        int tests = 0;
        int passed = 0;
        //拿到测试类
        Class testClass = Class.forName("xxxx.Sample");
        //反射调用其方法
        for(Method m : testClass.getDeclaredMethods()) {
        	//仅仅执行测试方法(被标注的方法)
            if(m.isAnnotationPresent(Test.class)) {
                tests++;
                try {
                    m.invoke(null);
                    passed++;
                } 
                //拿到失败原因
                catch(InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    System.out.println(m + " failed: " + exc);
                } catch(Exception e) {
                    System.out.println("INVALID @Test: " + m);
                }
                
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }
}
测试结果:1Passed,2 faild

上面实现了一个简单的测试框架,可以看到注解其实就是一种描述方法或者类的元数据,代表一种信息,jvm会解读这些信息对程序员编程起到一种约束的作用,这种约束比命名模式这种“口头约束”更强。例如@Override注解表明覆盖父类方法,那么编译器就能帮你检查这个方法在父类到底有没有。

(7) 坚持使用Overried注解

重载:看参数列表不同,不看返回类型。

重写:重写父类方法,要求有相同的参数列表、返回类型、方法名。

坚持使用Overried注解,可以防止一大类非法错误,可以在编译器检查出编写错误。代码如下:

//本想重写,但是参数列表非法,变成了重载
public boolean equals(Bigram b) {
            return b.first == first && b.second == second;
        }

(8) 标记接口定义类型

“接口只定义类型,如果不定义类型就不要使用接口,如果要定义类型就一定使用接口”。前面分析过常量接口用来定义常量,这种接口使用方法是错误的,而接口应该用来定义类型。

标记接口:没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口。例如,Serializable接口、Cloneable接口。

标记注解:特殊类型的注解,其中不包含成员。标记注解的唯一目的就是标记声明。例如,@Override。

相比优缺点:


猜你喜欢

转载自blog.csdn.net/qq_34448345/article/details/79768656