当前位置: 移动技术网 > IT编程>开发语言>Java > 深入理解 Java Object

深入理解 Java Object

2018年12月13日  | 移动技术网IT编程  | 我要评论

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     }
当两个对象存在父子关系,并且子类添加新的值字段,在equals方法中使用instanceof判断类型时容易破坏对称性或传递性,如timestamp;使用getclass判断类型又违法里氏替换原则,所以避免使用继承,尝试使用组合;但如果父类是抽象的,不能实例化,则不会出现上述问题。
 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中花了很长篇幅详细介绍了这几个方法,说明其重要性.实际开发中,编辑器、第三方库都能自动生成,但理解原理还是很重要的。

 

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网