深入理解 Java Object
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解 Java Object相关的知识,希望对你有一定的参考价值。
Java中的Object对象为所有对象的直接或间接父对象,里面定义的几个方法容易被忽略却非常重要。以下来自Effective Java 对Object中几个关键方法的应用说明。
1 public class PhoneNumber implements Cloneable, Comparable<PhoneNumber> { 2 3 private final short linNum, prefix, areaCode; 4 5 public PhoneNumber(int number, int prefix, int areaCode) { 6 7 this.linNum = rangeCheck(number, 999, "linNUm"); 8 9 this.prefix = rangeCheck(prefix, 999, "prefix"); 10 11 this.areaCode = rangeCheck(areaCode, 999, "areaCode"); 12 13 } 14 15 private static short rangeCheck(int val, int max, String arg) { 16 17 if (val < 0 || val > max) { 18 19 throw new IllegalArgumentException(arg + ":" + val); 20 } 21 22 return (short) val; 23 } 24 }
equals(Object o)
Object中equals方法的实现仅仅是比较了两个对象的地址,对于某些类来说正是所需用的、毋需复写的
-
Thread,由于每个线程对象天生就是独一无二的,重点表达是实体而不是值,不需要比较
-
java.util.regex.Pattern,正则表达式的类型也没有比较实例是否相同的必要
-
父类复写了equals方法,并且是子类所需要的,如AbstractSet,AbstractList,AbstractMap,其子类毋需复写。
-
private或package private修饰的类,其方法不会被调用
什么时候需要对类的equals方法复写?
当一个类表示一个值,如String、Integer;它的不同实例需要逻辑上判断是否相同,而不仅仅是地址是否相同,此时需要复写来自定义相等的条件。由于Map的键和Set的元素都是唯一的,如何判断元素相同是使用此类集合的基础。
equals方法的复写需要满足以下通用约定
-
自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true,就是自己和自己比较必须相等。
-
对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true,就是x若等于y,那么y也应该等于x。
-
传递递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
-
一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。
-
非空性:对于任何非空引用值 x,x.equals(null) 都应返回 false。
如无必要不要复写equals 方法,如果复写了此方法一定要记得复写hashCode方法,因为两个对象相等,它们的hashCode也要相等,下面是equals方法的常用步鄹
1 @Override 2 public boolean equals(Object o) { 3 4 //判断引用是否相等 5 if (o == this) { 6 return true; 7 } 8 9 //判断参数类型是否正确 如果o为null也会返回false 10 11 //这里判断的是class类型,也有可能是接口类型,这样就允许实现这个接口的类之间进行比较 12 13 //AbstractSet,AbstractList,AbstracMap的equals方法这一步都是比较的接口 14 15 if (!(o instanceof PhoneNumber)) { 16 return false; 17 } 18 //类型转换 19 // AbstractSet的类型转换 Collection<?> c = (Collection<?>) o; 20 21 PhoneNumber pNum = (PhoneNumber) o; 22 23 // 判断重要字段的相等,如果使用的是接口,调用接口的方法获取字段 24 25 // 对于基本类型 如果不是float或double 直接使用==比较 26 27 // float使用Float.compare(float, float), 原因参考testFloat方法 28 29 //double使用Double.compare(double, double) 同上 30 31 //Float.equals和Double的equals都设计autobox,影响性能 32 33 //引用类型继续调用其equals方法 34 35 // 上述方法也同样适用于数组元素,如果要比较整个数值,使用Arrays.equals对应的方法 36 37 //对象的某些字段能为Null,为了避免NPE,使用Objects.equals(Object, Object) 38 39 return this.linNum == pNum.linNum && 40 41 this.areaCode == pNum.areaCode && 42 43 this.prefix == pNum.prefix; 44 }
1 //使用组合方式替代继承Point 2 class ColorPoint { 3 4 private final Point point; 5 6 private final Color color; 7 8 public ColorPoint(int x, int y, Color color) { 9 point = new Point(x, y); 10 this.color = Objects.requireNonNull(color); 11 } 12 13 public Point asPoint() { 14 return point; 15 } 16 17 @Override 18 public boolean equals(Object o) { 19 if (o == this) { 20 return true; 21 } 22 23 if (!(o instanceof ColorPoint)) 24 return false; 25 26 ColorPoint cp = (ColorPoint) o; 27 28 return cp.point.equals(point) && cp.color.equals(color); 29 30 } 31 32 }
为什么不使用==比较浮点值,因为有两个例外使比较不一致
-
如果 f1 和 f2 都表示 Float.NaN,那么即使 Float.NaN == Float.NaN 的值为 false,equals 方法也将返回 true。
-
如果 f1 表示 +0.0f,而 f2 表示 -0.0f,那么即使 0.0f==-0.0f 的值为 true,equal 测试也将返回 false。
1 private void testFloat() { 2 3 Float f1 = Float.NaN; 4 5 Float f2 = Float.NaN; 6 7 System.out.println(f1.floatValue() == f2.floatValue());//false 8 9 System.out.println(f2.equals(f1));//true 10 11 f1 = 0.0f; 12 13 f2 = -0.0f; 14 15 System.out.println(f1.floatValue() == f2.floatValue());//true 16 17 System.out.println(f2.equals(f1));//false 18 }
hashCode()
上文说到如果复写equals方法一定要复写hashCode方法。下面说说hash值的计算
-
确保与equals中使用的字段一致
-
如果字段是基本类型,使用包装类计算hash值如Float.hashCode(f)
-
如果字段是引用类型,并且在equals方法中递归调用去equals方法,那么这里也递归调用其hashCode方法
-
如果字段是数组类型,对其中重要元素的hash计算上述方法同样使用,如果要计算整个数组的hash值,使用Arrays.hashCode(array)
-
质素31的选取是个传统,能尽量让不同对象拥有不同hash值,即分布均匀,
-
Objects.hash(linNum,prefix,areaCode)方法简便,但涉及可变数组的创建和拆装箱操作,性能敏感
-
此方法返回的值不应该有详细规范,如String的hashCode方法返回精确值就是一个失误
如果没有明确规范,发现更好的hash方法可以在以后版本修改
1 @Override 2 3 public int hashCode() { 4 5 int result = 0; 6 7 result = Short.hashCode(linNum); 8 9 result = 31 * result + Short.hashCode(prefix); 10 11 result = 31 * result + Short.hashCode(areaCode); 12 13 return result; 14 15 }
那些不可变对象如果hash值计算量大,需要使用缓存防止重复计算影响性能,这里线程不安全
1 private int hashCode = 0; 2 3 public int hashCode() { 4 5 int result = hashCode; 6 7 if (result == 0) { 8 9 result = Short.hashCode(linNum); 10 11 result = 31 * result + Short.hashCode(prefix); 12 13 result = 31 * result + Short.hashCode(areaCode); 14 15 } 16 17 return result; 18 19 }
toString()
尽量复写toString方法,虽然不及equals和hashCode方法必要,但良好的类描述将能提供充分和友好的信息,AbstractCollection的toString为其子类统一提供集合信息的描述
如果要指定返回值的格式 可做如下说明 这样用户知道如何对其解析 但缺点是如果变更将导致以前的解析方式失败
1 /** 2 3 * 返回格式化的电话号码"XXX-YYY-ZZZZ" 4 5 * 每个大写字母表示一个数字 6 7 * XXX表示区号,YYY表示前缀,ZZZZ是号码 8 9 * 位数不够的用0填充,如最后一个是123将表示为0123 10 11 */ 12 13 @Override 14 15 public String toString() { 16 17 return String.format(Locale.CHINA, "%03d-%03d-%04d", areaCode, prefix, linNum); 18 19 }
clone()
如果一个class 实现了Cloneable接口 那么它应该 提供一个public clone方法
-
这是一个毋需构造器就能创建对象的方法
-
注意:这种方式复制对象容易出错而且复杂,难以维护 仅仅在对基本类型数组的复制是可取的
-
这个方法是个浅拷贝,也就是字段到字段的复制,如果都是基本类型,那将是一步到位的,
-
但如果还有引用类型,它们指向的对象不会被拷贝,而仅仅拷贝了引用,这就会导致拷贝后的对象和被拷贝的对象不是相互独立的,这些引用指向了相同的对象,也就是任何一方的修改都在另一方得到体现
-
如果要深度拷贝,可以每个引用类型都需要实现cloneable接口和clone方法,
或者使用序列化的方式将对象写到磁盘中,再通过反序列化实现克隆对象,如Apache Commons3工具类,transient修饰的字段不会被序列化。
-
我们这个类的字段都是基础类型,clone方法比较简单,由于字段都是final,这个一个immutable(不可更改的)类,提供拷贝方法就是多余的,这里仅做演示
-
一定要先实现Cloneable接口,尽管里面什么都没有“
1 @Override 2 3 public PhoneNumber clone() { 4 5 try { 6 7 return (PhoneNumber) super.clone(); 8 9 } catch (CloneNotSupportedException e) { 10 11 //实现Cloneable接口就不会跑出此异常 12 13 throw new AssertionError(); 14 15 } 16 17 }
在实际中要实现对象拷贝,并不建议使用clone方法,而建议采用静态工厂或构造器方式提供复制操作
相比clone的优点:
-
不依赖容易出错的对象创建机制;
-
不会与final字段的正确使用冲突
-
不会抛出 checked exceptions;
-
不要求类型转换
比如某些集合类,以接口为参数的复制构造函数,还能实现转换复制
1 将其他集合复制成TreeSet 2 3 public TreeSet(Collection<? extends E> c) { 4 5 this(); 6 7 addAll(c); 8 9 } 10 11 复制转化成TreeMap 12 13 public TreeMap(Map<? extends K, ? extends V> m) { 14 15 comparator = null; 16 17 putAll(m); 18 19 }
Comparable
compareTo是个很重要的方法,虽然不是Object中的,因为和其他几个方法一样广泛应用,所以放在这里解释,实现Comparabe接口,复写compareTo方法后一个对象就有了可比较性。
-
如果此方法返回0那么equals应该返回true,如果不是一定要说明不一致性
-
HashSet依赖equals比较元素是否重复,TreeSet依赖compareTo给元素排序
-
BigDecimal这两个方法就是不一致的,BigDecimal(1.0)and BigDecimal(1.00)equals返回false,因此加入HashSet是不相同的元素
但compareTo返回0,也就是大小相等,加入TreeSet就只有一个元素
注意:不要使用< >来比较大小,对浮点有例外,也不要使用减号,会有溢出
建议如上使用基本数据类型包装类的静态比较方法compare
也可以使用Comparator接口里面的方法,在Java8中,可以如下生成按某种顺序比较的复合比较器。内部实现是从最后一个比较方法进入向前调用的
-
优点:在lambda表达式的帮助下逻辑清晰,表达简便
-
缺点:效率比传统的低,每层比较都创建新对象在Effect Java中花了很长篇幅详细介绍了这几个方法,说明其重要性.实际开发中,编辑器、第三方库都能自动生成,但理解原理还是很重要的。
以上是关于深入理解 Java Object的主要内容,如果未能解决你的问题,请参考以下文章