第3章:对于所有对象都通用的方法

前言

  1. Object的设计是为了扩展
  2. 它的所有非final方法(equals、hashCode、toString、clone、finalize),都有明确的通用约定
  3. 如果实现Object的类,重写这些方法,但未遵守约定,那么其他依赖于这些约定的类(例如HashMap),就无法结合该类一起正常工作

第10条:覆盖equals时请遵守通用约定

10.1 无需重写equals方法的情况
  1. 类的实例本质上唯一
例:Thread,新的线程就不应该和老的一样
  1. 类没有必要提供逻辑相等的测试功能
例:java.util.Pattern可以覆盖equals方法,以检查两个Pattern实例是否代表同一个正则表达式,但设计者不认为客户端需要这种功能
  1. 超类已覆盖equals,且超类的行为对这个类也合适
例:大多Set实现都从AbstractSet继承equals实现
  1. 类是私有的,或包级私有,确定它的equals永远不会调用
//为了防止equals方法被意外调用,甚至可以如下处理
@Override public boolean equals(Object o){
	throw new AssertionError();
}
  1. 实例受控的类,且每个值至多值存在一个对象的值类
例:枚举类,Objects的equals方法,等同于逻辑上的equals
10.2 需要重写equals的情况
  1. 类具有自己特有的逻辑相等概念,且超类没覆盖equals,这一般属于值类
    1. 程序员在利用equals方法时,希望了解它们在逻辑上是否相等,而不是想了解他们是否指向同一个对象
10.3 通用约定

以下描述的引用均非空(null)

  1. 自反性:x.equals(x)必须返回true。一般不会无意识的违反该条
  2. 对称性:y.equals(x)为true,x.equals(y)也必须为true
public final class CaseInsensitiveString {
    private final String s;

    public CaseInsensitiveString(String s) {
        if (s == null)
            throw new NullPointerException();
        this.s = s.toLowerCase();
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString)
            return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
        if (o instanceof String) 
            return s.equalsIgnoreCase((String) o);
        return false;
        //2. 修正后的equals方法,不允许CaseInsensitiveString对象与String对象比较,也就避免了违反对称性
        //return o instanceof CaseInsensitiveString&&((CaseInsensitiveString)o).s.equalsIgnoreCase(s);
    }
    public static void main(String[] args) {
    	CaseInsensitiveString a = new CaseInsensitiveString("wdf");
    	//1. 违反了对称性
    	//因为虽然CaseInsensitiveString的equals方法知道判断时,忽略大小写
    	//但String类型的"Wdf"并不知道与CaseInsensitiveString比较时,也应忽略
    	System.out.println(a.equals("Wdf"));
    	System.out.println("Wdf".equals(a));
    	
	}
}
  1. 传递性:x.equals(y)返回true,y.equals(z)返回true,x.equals(z)必须返回true。
    1. 我们无法在扩展可实例化的类的同时,既增加组件,又保留子类与父类的逻辑相等,又同时保留equals约定。
    2. 本例中的逻辑相等:父类对象.equals(子类对象)时,x、y与子类对象的x、y属性相同,就认为相同,且子类对象.equals(父类对象)时,由于增加color属性,不应返回相等
    3. Java类库中,有些类扩展了可实例化的类,并添加了新组件,例如java.sql.Timestamp对java.util.Date进行了扩展,增加nanoseconds域,Timestamp的equals方法,确实违反了对称性。即Timestamp与Date同处一个集合中,可能引起不正确行为
    4. 可以扩展不可实例化的类的同时增加组件,又保留equals约定,只要不可能直接创建超类实例,前面问题都不会发生
public class Point {
	private final int x;
	private final int y;
	public Point(int x,int y) {
		this.x = x;
		this.y = y;
	}
	@Override public boolean equals(Object o) {
		if(!(o instanceof Point)) {
			return false;
		}
		Point p = (Point)o;
		return p.x == x&&p.y== y ;
	}
}

public class ColorPoint extends Point{
	private final Color color;
	public ColorPoint(int x,int y,Color color) {
		super(x,y);
		this.color = color;
	}
	//1. 如果不重写equals方法,直接从Point继承,equals比较时,Color信息被忽略,这不符合逻辑
//	@Override public boolean equals(Object o) {
//		if(!(o instanceof ColorPoint)) {
//			return false;
//		}
//		//2. 这样做时,对于Point对象p与ColorPoint对象cp,p.equals(cp)返回true,而cp.equals(p)返回false,违反之前的对称性
//		return super.equals(o)&&((ColorPoint)o).color==color;
//		
//		
//	}
	//3. 此时牺牲了传递性,p1.equals(p2)、p2.equals(p3)都返回true,但p1.equals(p3)为false
	//ColorPoint p1 = new ColorPoint(1,2,Color.RED);
	//Point p2 = new Point(1,2);
	//ColorPoint p3 = new ColorPoint(1,2,Color.BLUE);
	@Override public boolean equals(Object o) {
		if(!(o instanceof Point)) {
			return false;
		}
		if(!(o instanceof ColorPoint)) {
			//如果传入对象是Point实例,但不是ColorPoint实例,即如果传入的为父类对象,使用父类的equals方法进行比较
			return o.equals(this);
		}
		return super.equals(o)&&((ColorPoint)o).color==color;
		
		
	}
	
}
class Color{
	
}

//1. 如果有另一个类SmellPoint,也继承Point,与ColorPoint有同样逻辑的equals方法
public class SmellPoint extends Point{
	private final Color color;
	public SmellPoint(int x,int y,Color color) {
		super(x,y);
		this.color = color;
	}

	@Override public boolean equals(Object o) {
		if(!(o instanceof Point)) {
			return false;
		}
		if(!(o instanceof SmellPoint)) {
			//2. main方法中代码会进入这段,而由于o是ColorPoint的实例,又会调用ColorPoint中equals方法,而那个equals方法这个地方,又会调入SmellPoint额equals方法
			//会产生无限递归,并抛出java.lang.StackOverflowError异常
			return o.equals(this);
		}
		return super.equals(o)&&((SmellPoint)o).color==color;
		
		
	}
	public static void main(String[] args) {
		ColorPoint p = new ColorPoint(1, 2, new Color());
		SmellPoint p1 = new SmellPoint(1, 2, new Color());
		System.out.println(p1.equals(p));
	}
	
}

//本例演示了如何既获得可实例化的类的属性,又可以扩展新属性,又不违反逻辑,又不违反equals约定
public class ColorPoint {
	private final Point point;
	private final Color color;
	public ColorPoint(int x,int y,Color color) {
		point = new Point(x,y);
		this.color = color;
	}
	//2. asPoint可以称为一个视图方法,实际上这个point就是适配器模式中,适配器持有的其适配的对象,而这个视图方法,可以等价看作适配器的一个方法,这个方法会转交给被适配的对象处理
	public Point asPoint() {
		return point;
	}
	@Override public boolean equals(Object o) {
		if(!(o instanceof ColorPoint)) {
			return false;
		}
		ColorPoint cp = (ColorPoint)o;
		return cp.point.equals(point)&&cp.color.equals(color);
	}
	public static void main(String[] args) {
		//1. 当需要比较Point和ColorPoint时
		Point p = new Point(1,2);
		ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
		System.out.println(p.equals(p1.asPoint()));
	}
	
}
enum Color{
	RED,GREEN;
	
}

  1. 一致性:只要x、y中参与equals方法的成员变量值没被修改,x.equals(y)总返回true
    1. 对于可变的类,没什么特殊说法
    2. 但对于不可变类,那必须保证equals满足,相等的永远相等,不相等的永远不等
    3. java.net.URL的equals方法,依赖URL中主机IP地址比较,随时间推移,可能产生不同结果,违反equals约定
  2. x.equals(null)必须返回false
//一般无需显式检查,因为如果equals类中一般都有instanceof方法,null instanceof任何类,都是false,这就同时检查了是否为空
if(!(o instanceof MyType))
	return false;
10.4 最佳实践
public class PhoneNumber {
	// 连续定义三个short类型变量,可以写在一起
	private final short areaCode, prefix, lineNum;

	public PhoneNumber(int areaCode, int prefix, int lineNum) {
		this.areaCode = rangeCheck(areaCode, 999, "area code");
		this.prefix = rangeCheck(prefix, 999, "prefix");
		this.lineNum = rangeCheck(lineNum, 9999, "line Num");
	}

	private static short rangeCheck(int val, int max, String arg) {
		if (val < 0 || val > max)
			throw new IllegalArgumentException(val + ":" + val);
		return (short) val;
	}

	@Override
	public boolean equals(Object o) {
		//1. 使用==操作符检查"参数是否为这个对象的引用",如果是返回"true"
		if (o == this)
			return true;
		//2. 使用instanceof操作符检查"参数是否为正确类型",如果不是返回false
		//"正确类型"指equals方法所在类,或该类实现的某个接口,如果是接口,那表明想对实现该接口的类进行比较
		if (!(o instanceof PhoneNumber))
			return false;
		//3. 把参数转成正确类型,因为之前使用过instanceof,因此一定会成功
		PhoneNumber pn = (PhoneNumber) o;
		//4. 对该类中每个关键域,检查参数中域是否与该对象中对应的域相匹配
		//如果PhoneNumber为接口,那就需要利用接口中的get方法获得域的值,如果为类,就可以直接访问传入的参数o的域
		//对于不同类型的域,一般采取不同的比较方式
		//a. 非double和float的基本类型:==
		//b. double、float:Float.compare(float,float)、Double.compare(double,double),不使用Double.equals和Float.equals,因为会导致自动装箱,效率低
		//c. 引用类型:递归调用其equals方法。如果该域允许为null,使用Objects.equals(Object,Object)检查同等性
		//d. 对于数组:Arrays.equals比较每个元素的值
		//5. 域的比较顺序:最先比较最可能不一致、比较起来开销最低的域。不需比较衍生域,因为衍生域是由关键域计算获得。但有可能某情况下比较它效率高
		//6. 编写后问自己是否满足对称、传递、一致,并编写单元测试。其他特性一般会自动满足
		//7. 可以使用AutoValue或IDE自动生成equals方法,这样就不用自己测试检查了
		return pn.lineNum == lineNum && pn.prefix == prefix && pn.areaCode == areaCode;

	}
	
}

10.5 一些告诫
  1. 覆盖equals时,总要覆盖hashCode
  2. 不要企图让equals方法过于智能:过于智能就很难做到满足equals的通用约定
  3. 不要讲equals方法签名中Object o改成其他类型,因为这样就不是重写了,因为形参列表不同,只是重载
10.6 概念补充
  1. 值类(value class):仅仅表示值的一个类,比如Integer、String
  2. 里氏替换原则:基本可以理解为,用父类型的地方,都可以用它的子类型代替
//本例中指的是这个方法中传入参数为Point类型,那么传入Point的子类型进来,该方法仍然应该生效
public static boolean onUnitCircle(Point p){
	return unitCircle.contains(p);
}
  1. AtomicInteger:适用于记录线程共执行多少次,传统static变量,无法正确记录
public class AtomicIntegerTest {
 
    private static final int THREADS_CONUT = 20;
    //public static AtomicInteger count = new AtomicInteger(0);

    public static int count = 0;
 
    public static void increase() {
    	//count.incrementAndGet();
        count++;
    }
 
    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_CONUT];
        for (int i = 0; i < THREADS_CONUT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
 
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println(count);
    }
}
  1. 范式
//可以改写CaseInsensitiveString类,原equals方法中使用的equalsIgnoreCase方法,效率较低,为了提高效率,可以考虑将传入的字符串,先小写,并保存
public final class CaseInsensitiveString {
    private final String s;
    public CaseInsensitiveString(String s) {
        if (s == null)
            throw new NullPointerException();
        //1. 可以理解为小写后的s,就是传入的s的范式。如果对象变化,其范式也应该变化
        this.s = s.toLowerCase();
    }
    @Override
    public boolean equals(Object o) {
    	//2. 下面不再使用低效的equalsIgnoreCase判断相等,而是可以使用高效的equals方法
        if (o instanceof CaseInsensitiveString)
            return s.equals(((CaseInsensitiveString) o).s);
        if (o instanceof String) // One-way interoperability!
            return s.equals(((String) o).toLowerCase());
        return false;
    }
    public static void main(String[] args) {
    	CaseInsensitiveString a = new CaseInsensitiveString("wdf");
    	System.out.println(a.equals("Wdf"));
	}
}

第11条:覆盖equals时总要覆盖hashCode

11.1 通用约定
  1. 只要对象的equals方法的比较操作所用到的信息没有被修改,那么同一个对象多次调用hashCode方法都必须返回同一个值。在不同两个应用程序中hashCode值可以不同
  2. equals返回true,hashCode值也必须相同
Map<PhoneNumber,String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867,5309),"Jenny");
//该值为空,因为两个key对象的hashcode值不同,而HashMap获取元素时,必须equals和hashCode都相等。这样就造成明明相等的元素(equals等),无法被正确获取
m.get(new PhoneNumber(707, 867,5309));
  1. equals返回false,hashCode最好不同,否则可能影响性能
//这样做会使散列表退化成链表,效率降低
@Override public int hashCode(){return 42}
11.2 最佳实践
  1. 最佳实践原则
//如果一个类不可变(散列码一般不会变动,因为属性值不会变),且计算散列码开销较大,可以把散列码缓存在对象内部,而不是每次请求都计算散列码
//如果觉得大多数该类对象,会取其hashCode值,那么可以在创建对象时,计算其hashCode,不然可以延时初始化,例如下面,创建对象时为0,调用hashCode方法后,计算hashCode值,并缓存在内部
private int hashCode; 

//重写PhoneNumber类hashCode方法
@Override
	public int hashCode() {
		//1. 定义int型result变量,初始值为第一个关键域(参与equals方法的域)的hashCode值。
		//基本类型:Type.hashCode(f)
		//引用类型:如果equals方法通过递归的调用equals方式比较这个域,那同样为这个域递归地调用hashCode。如果值为null,返回0
		//数组:如果数组中元素都重要,Arrays.hashCode,如果部分重要,那么按2的方式,将这些值组合起来
		//2. 将每一个关键域得到的hashCode值进行合并,合并方式为result = 31*result+c,c表示上一个关键域得到的hashCode
		int result = hashCode;
		if (result == 0) {
			result = Short.hashCode(areaCode);
			result = 31 * result + Short.hashCode(lineNum);
			result = 31 * result + Short.hashCode(prefix);
		}
		return result;
	}
  1. 散列码计算时,可以排除衍生域
  2. 必须排除非关键域
  3. 选用31,是因为它是一个奇素数
  4. Objects.hash(Object… values):可以自动根据提供的关键域生成hashCode值,但性能较低
  5. 如果类不可变,且计算散列码开销大,可以考虑将散列吗缓存在对象内部,这样无需每次都计算散列码
  6. 不要试图从散列码计算中排除掉一个对象的关键域来提升性能
    1. 例如Java2之前,String对象如果超过16位,计算hashCode时,在整个字符串中,间隔、均匀选取样本进行计算。对于URL这种对象,有可能大量不同对象hashCode值相同,影响性能
  7. 不要对hashCode方法返回值做具体规定。否则如果有客户端依赖这个值进行操作,以后就没法提供更好的算法来重写hashCode
  8. 可以使用IDE或AutoValue自动生成hashCode方法,无需测试,一定满足通用约定
11.3 概念补充
  1. 散列表:也叫哈希表,给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
  2. 链表:第一个为值,第二个是链表中下一个元素的位置
  3. 对于Hashtable,首先根据key的hashCode找到这组key/value对在散列表中的位置,在这个位置上实际上又是一个链表,该链表上是一个个key/value对,如果不同对象hashCode值相同,那么他们全在散列表中同一个位置上的链表中,那实际上散列表的作用就消失了,这就是所谓的散列表退化成链表
  4. 散列码:hashcode值
  5. 关键域:影响equals比较的域

第12条:始终要覆盖toString

Object的toString方法:类名@该对象散列码的无符号16位表达式

12.1 通用约定
  1. 被返回字符串应该是简洁、信息丰富、易于阅读的表达式
  2. 建议所有子类都覆盖该方法
12.2 注意事项
  1. 提供好的toString可以使类用起来更舒适,更容易调试
//如果没重写,无法直接打印出未连接的电话号号码
System.out.println("Fiald to connect to "+phoneNumber);
  1. 应返回对象中包含的所有值得关注的信息,如果对象太大,可以返回一个摘要
  2. 实现toString时,可以指定其格式,并提供一个相匹配的静态工厂或构造器,用于对象以及其toString的字符串之间转换。BigInteger、BigDecimal、大部分基本类型包装类,都是这样做的
BigDecimal a = new BigDecimal("23");
  1. 一旦指定toString格式,那么就必须始终坚持,不能再修改,防止依赖该格式编写的客户端代码出错
  2. 无论是否指定toString格式,都应该在文档(注释可以自动生成文档,因此在注释中写明)中明确地表明意图
  3. 需要为toString返回值包含的所有信息提供一种可以通过编程访问之的途径
//如果方法这样编写,那么应提供getAreaCode、getPrefix、getLineNum方法, 防止程序自己解析,容易解析错
@Override
public String toString() {
	return  areaCode +"-"+ prefix + "-" + lineNum ;
}
  1. 静态工具类中编写toString没意义
  2. 枚举类中也不要编写,因为Java已提供了完美的方法
  3. 在所有子类共享通用toString的抽象类中,一定要编写该方法
  4. AutoValue和IDE可以自动生成toString,但不一定能按你希望的逻辑:对于PhoneNumber,电话号一般都是xxx-xxx-xxxx格式,如果自动实现,就无法做到这样

第13条:谨慎地覆盖clone

  1. Cloneable接口中并没有clone方法,但必须实现该接口,才能使用Object的clone方法,否则抛出CloneNotSupportedException。
  2. Object的clone方法,是一个native方法,是操作系统实现的,而不是传统的由构造器实现
约定:都不是绝对的
  1. x.clone()!=x
  2. x.clone().getClass() == x.getClass()
  3. x.clone().equals(x)返回true
  4. clone方法返回的对象应该由super.clone获得。如果类及超类遵守该约定,那么x.clone().getClass == x.getClass()。正常理解super.clone应该会返回super类型,但如果遵守该约定,就会直接返回其子类型
  5. 返回的对象不应依赖于被克隆的对象,为实现这种独立性,可能需要在super.clone返回对象前,修改对象的一个或更多域
Stack result = (Stack)super.clone();
resultelements = elements.clone();
return result;
  1. 如果类的clone方法返回的实例,不是super.clone获得,而是调用构造器获得,由于不调用super.clone,所以不用Object的native方法,也就没必要实现Cloneable,也不会抛异常。但该类的子类调用super.clone,得到的对象类型就不对(因为只有super.clone会自动转运行时类型),也阻止了clone方法的子类正常工作。因此只有final类(无子类),可以不使用super.clone获取对象,也无需实现Cloneable
  2. 不可变类永远都不应该提供clone方法,它只会激发不必要的克隆,也就是说没有场景需要这样做
案例
  1. 简单实现
//2.clone方法会抛出CloneNotSupportedException,是checked exception,必须被catch,或throws
@Override public Test clone(){
	try {
		//1. 方便客户端使用,所以转成Test类型
		//2. 对于public的clone方法,最好使用try、catch来处理异常,不要使用throws,不然客户端使用起来太费劲
		return (Test) super.clone();
	} catch (CloneNotSupportedException e) {
		throw new AssertionError();
	}
	
}
  1. 对象的域为可变的引用类型的克隆
//Stack类的代码见第7条
//1. clone得到的结果,int类型的成员变量size,值还是原对象的值。
@Override public Stack clone() {
	try {
		Stack result = (Stack)super.clone();
		//2. elements类型为数组。如果不加这句话,那么,克隆出的对象b的elements与原对象a的elements指向同一块内存,即a对数组进行修改,b的也变化
		//3. 但由于数组中存放的是引用类型Test,虽然调用了数组的clone方法,a和b的elements并不指向同一内存,但其内的元素,还是指向同一内存,即a中elements中的Test元素的int类型的成员变量修改,b中的也变
		//4. 数组的clone方法返回的数组,编译时类型与被克隆数组类型(Object[])相同,而不是Object,因此无需类型转换
		//5. 如果elements为final修饰,这个方案无法正常工作,因为无法被重新赋值。即Cloneable架构与引用可变对象的final域的正常用法不兼容,有时可能需要去掉某些域的final修饰
		result.elements = elements.clone();
		return result;
	}catch(CloneNotSupportedException e) {
		throw new AssertionError();
	}
	
}

  1. 实现深度克隆
@Override
	public HashTable clone() throws CloneNotSupportedException {
		// 模拟了HashTable的实现,实际上在HashTable的get方法时,会根据传入的key,通过一系列哈希计算,得到一个数组下标,这个下标对应的Entry类型的数组元素,其内维护一个next值,表示链表上下一个元素
		// 只要找到下一个元素,就判断该元素key与传入的对象的key是否equals和hashCode是否相同,相同返回value,不相同一直循环到next为null
		// 实际上在HashTable存储数据时,当key的hashCode不同时,会存储在数组中,当hashCode相同时,不会放入数组,只会在同一个hashCode的数组元素的next属性中,记录这个新key值,以供后续查找
		// 所以说这里Entry数组就是散列桶,而由于其内元素维护了一个next,可以找到其下一个对象,属于一个链表
		HashTable result = (HashTable) super.clone();
		// 这样克隆得到的链表,和原链表是同一个,因为数组的clone操作只是不同的引用指向了不同的数组,但数组中元素还是同一个
		result.buckets = buckets.clone();
		return result;

	}
//1. 使用递归,多次调用deepCopy1方法,而每次调用一个方法, 都会在栈中创建一个栈帧,从而消耗一段栈空间,当链表较长,很容易导致栈溢出
		Entry deepCopy1() {
			return new Entry(key,value,next== null?null:next.deepCopy1());
		}
		//2. 使用迭代,每次迭代后,局部变量就释放了,不会将栈顶满
		Entry deepCopy() {
			Entry result = new Entry(key,value,next);
			for (Entry e = result ; e != null ; e = e.next) {
	            e.next = new Entry(e.next.key,e.next.value,e.next);
	        }
			return result;
			
		}
		//3. 可以直接将所有HashTable中元素取出,调用其原来的put方法, 将一个个元素放到新的HashTable对象中,也可以实现深度拷贝
@Override
	public HashTable clone() throws CloneNotSupportedException {
		HashTable result = (HashTable)super.clone();
		result.buckets = new Entry[buckets.length];
		for(int i = 0 ;i<buckets.length;i++) {
			if(buckets[i]!=null) {
				result.buckets[i] = buckets[i].deepCopy();
			}
		}
		return result;

	}
  1. 为了被继承而设计的类,不应该实现Cloneable接口,应该让子类有实现或不实现Cloneable的自由
  2. 有时候,不想子类实现clone方法,自己也不想使用clone方法
// clone method for extendable class not supporting Cloneable
@Override
protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}
  1. 线程安全的类想实现Cloneable接口时,它的clone方法一定要得到严格的同步,而Oracle自带的clone方法,没有同步,所以需要自己编写同步的clone
对象拷贝的其他办法
  1. 可以使用拷贝构造器、拷贝工厂,来替代实现Cloneable接口获取clone方法的
// Copy constructor
public Yum(Yum yum) { ... };
// Copy factory
public static Yum newInstance(Yum yum) { ... };
  1. 优势
    1. 不依赖于有风险的、语言之外的对象创建机制
    2. 不会与final发生冲突
    3. 不会抛出不必要的checked异常
    4. 不需要类型转换
  2. 转换构造方法和转换工厂,支持将同一接口下的类型A拷贝成类型B
//接受类型为该类实现的接口的参数,例如Collection
public TreeSet(Collection<? extends E> c) {
    this();
    addAll(c);
}
...
//将HashSet类型的a复制成TreeSet类型的b
HashSet<String> a = new HashSet<>();
TreeSet<String> b = new TreeSet<>(a); 

结论

  1. 对于Cloneable接口,新的接口不应该继承它,新的可扩展类不应该实现它
  2. 虽然实现Cloneable接口对于final类没有什么危害,但考虑到性能,极少数情况下才合理
  3. 复制功能最好由构造方法或工厂提供,数组例外,数组最好用clone方法复制
补充
  1. 协变返回类型:重写方法时,返回值可以与原方法不同,只要不大于原方法就行

第14条:考虑实现Comparable接口

  1. 类实现Comparable接口,就代表该类的实例,具有内在的排序关系
  2. 对存储在数组中的Comparable对象操作
//排序
Arrays.sort(a);
  1. 对存储在集合中的Comparable对象操作
//String实现了Comparable接口
//将args的字符串,按字母顺序打印
public class WordList {
	public static void main(String[] args) {
	    Set<String> s = new TreeSet<>();
	    Collections.addAll(s, args);
	    System.out.println(s);
	}
}
  1. Java所有值类、枚举类,都实现了Comparable。自己编写值类时,也应该这样做
14.1 通用约定

sgn(expression),表示如果expression>0,返回1,=0返回0,<0返回-1

  1. sgn(x.compareTo(y)) == -sgn(y. compareTo(x))。(这意味着当且仅当y.compareTo(x)抛出异常时,x.compareTo(y)必须抛出异常。)
  2. 可传递:(x. compareTo(y) > 0 && y.compareTo(z) > 0)意味着x.compareTo(z) > 0
  3. [x.compareTo(y) == 0意味着sgn(x.compareTo(z)) == sgn(y.compareTo(z))
  4. 强烈推荐x.compareTo(y) == 0) == (x.equals(y)),但不是必需的。如果不一致,应写明"这个类有一个自然顺序,与equals不一致"
14.2
  1. compareTo没有equals实现起来复杂,因为它不必跨越不同类型的对象:当遇到不同类型的对象时,compareTo被允许抛出ClassCastException异常。就算要不同类型间比较,这种比较也仅限于同一接口下对象,且compare方法会在该接口中定义。
  2. 同hashCode约定的类可能会破坏依赖于哈希的其他类一样,违反compareTo约定的类可能会破坏依赖于比较的其他类
    1. TreeSet和TreeMap类
    2. 包含搜索和排序算法的实用程序类Collections和Arrays。
  3. 由于第三条规定,那么其和equals一样,除非你愿意放弃面向对象抽象(条目 10)的好处,否则无法在保留compareTo约定的情况下使用新的值组件继承可实例化的类。同样的解决方法也适用。 如果要将值组件添加到实现Comparable的类中,请不要继承它;编写一个包含第一个类实例的不相关的类。 然后提供一个返回包含实例的“视图”方法。
  4. 例如TreeSet,判断相等的标准是compareTo方法返回0,那么两个equals不等,但compareTo相等的元素,无法同时放入TreeSet,因为Set不允许重复,这可能导致和想要的逻辑不同
14.3 compareTo和equals方法区别
  1. equals方法传入的是Object,而compareTo传入的是T o,因此不需要输入检查或者转换它的参数。 如果参数是错误的类型,那么调用将不会编译。如果参数为null,一旦该方法尝试访问其成员,它就会立即抛出NullPointerException
  2. 要比较对象引用属性,请递归调用compareTo。如果这个引用的类型没有实现Comparable,或者你需要一个非标准的顺序(只要有顺序就行,不在乎是什么顺序),可以使用Comparator接口中一些现成的静态方法
//自定义compareTo方法
//CaseInsensitiveString类实现了Comparable <CaseInsensitiveString>接口。 这意味着CaseInsensitiveString引用只能与另一个CaseInsensitiveString引用进行比较
public final class CaseInsensitiveString
        implements Comparable<CaseInsensitiveString> {
    public int compareTo(CaseInsensitiveString cis) {
        return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
    }
    ... // Remainder omitted
}
  1. 要比较基本类型,使用Xxx.compare静态方法,不要使用>和<
  2. 如果一个类有多个重要的属性,从最重要的开始,逐步比较所有重要属性
// PhoneNumber 的compareTo方法
public int compareTo(PhoneNumber pn) {
    int result = Short.compare(areaCode, pn.areaCode);
    if (result == 0)  {
        result = Short.compare(prefix, pn.prefix);
        if (result == 0)
            result = Short.compare(lineNum, pn.lineNum);
    }
    return result;
}
14.4 通过Comparator的静态比较器(方法)实现compareTo方法
//1. comparingInt中的泛型是泛型方法,需要指定传入的ToIntFunction的类型,来推测泛型方法的类型,不然就认为泛型方法的T是Object
//2. 但由于传入的是Lambda表达式,如果省略了类型定义,那么ToIntFunction的泛型类的泛型没法确定,也就导致外层的泛型无法确定,最后只能被当做Object,所以需要指定
//3. comparingInt方法是一个静态方法,该包中可以直接使用是因为使用了静态导入,该方法需要一个ToIntFunction类型的参数,它叫做键提取器函数式接口,该方法可以将对象引用映射为int类型的键,并返回一个根据该键排序的实例的比较器
//4. thenComparingInt是Compartor上的一个实例方法,当areaCode相同,使用其继续比较下一个参数prefix
//5. 对于long和double基本类型,也有对应的类似于comparingInt和thenComparingInt的方法,int版本的方法也可以应用于取值范围小于 int的类型上,double版本的方法也可以用在float类型上。这提供了所有Java的基本数字类型的覆盖。
//6. 也有对象引用类型的比较器构建方法comparing和thenComparing
private static final Comparator COMPARATOR =
        comparingInt((PhoneNumber pn) -> pn.areaCode)
          .thenComparingInt(pn -> pn.prefix)
          .thenComparingInt(pn -> pn.lineNum);
//3. 重写compareTo方法
public int compareTo(PhoneNumber pn) {
    return COMPARATOR.compare(this,pn);
}
14.5 自定义Comparator对象,并通过其实现compareTo方法
// 不要这样做,可能会导致整数最大长度溢出和IEEE 754浮点运算失真的危险
static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return o1.hashCode() - o2.hashCode();
    }
};
//使用Integer.compare静态方法
static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return Integer.compare(o1.hashCode(), o2.hashCode());
    }
};
//使用Comparator的静态构建方法
static Comparator<Object> hashCodeOrder =
        Comparator.comparingInt(o -> o.hashCode());
发布了32 篇原创文章 · 获赞 0 · 访问量 938

猜你喜欢

转载自blog.csdn.net/hanzong110/article/details/102695556