面试系列-创建对象别忘了这些操作

·  阅读 463

详情请戳www.codercc.com

1. 覆盖equals方法

  • 问题

    在面对equals方法时,会有这样的疑问,什么时候该覆盖equals方法,什么时候不应该覆盖,也就是说覆盖equals方法的时机是什么?如果覆盖equals方法,那么应该写?

  • 解决

    1. 覆盖equals方法的时机

      覆盖equals方法看起来似乎很简单,但是有许多覆盖方式会导致错误,并且后果非常严重,最容易避免这类问题的办法就是**不覆盖equals方法,在这种情况下,类的每个实例都只与它自身相等。**下面这几种情况就不需要覆盖equals()方法:

      • **类的每个实例本质上都是唯一的。**对于代表实体而不是值(value)的类来说确实如此,例如Thread。Object提供的equals实现对于这些类来说是正确的行为;
      • **不关心类是否提供了“逻辑相等(logical equality)”的测试功能。**如java.util.Random覆盖了equals,以检查两个Random实例是否产生相同的随机序列,但是调用者并不期望这样的功能。在这样的情况下,从Object继承得到的equals实现已经足够了;
      • 父类已经覆盖了equals,从父类继承过来的行为对于子类来说也是合适的。例如大多数Set实现都从AbstractSet继承equals实现,类似的有List和Map等;
      • 类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用。
    2. 覆盖equals方法的规范写法

      在覆盖equals方法时,需要遵守的约定有:

      • 自反性:对于任何非null的引用值x,x.equals(x)必须返回true;
      • 对称性:对于任何非null的引用值x,y,当且仅当y.equals(x)返回true时,x.equals(y)也应该返回true;
      • 传递性:对于任何非null得引用值x、y和z,如果x.equals(y)返回true时,并且y.equals(z)也返回true,那么x.equals(z)也返回true;
      • 一致性:对于任何非null得引用值x和y,只要equals的比较操作在对象中所用的信息没有被修改,那么多次调用x.equals(y)就会一致的返回true,或者一致的返回false;
      • 非空性:对于任何非null的引用值x,x.equals(null)必须返回false;

      编写的技巧有:

      • 使用==操作符检查“参数是否为这个对象的引用”;
      • 使用instanceof操作符检查“参数是否为正确的类型”;
      • 经过instanceof类型检查之后把参数转换成正确的类型;
      • 对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配。对于不是double和float的基本类型,可以使用==进行比较,对于引用类型,可以递归调用equals方法,对于float域,可以使用Float.compare方法,对于double域,可以使用Double.compare方法;
      • 当编写完equals方法时,应该问自己三个问题:它是否满足对称性、传递性、以及一致性;
      • 覆盖equals方法总要覆盖hashCode()方法;
      • 判断各个域值是否相等的逻辑不要过于复杂;
      • 不要将所覆盖的equals方法中的入参Object对象替换成其他对象,应该使用@Override。
  • 结论

    当面对equals方法时,应该根据覆盖equals方法的时机去判断是否需要覆盖equals方法,如果需要覆盖equals方法时,要严格遵守equals方法的规范。

2. 覆盖equals方法同时覆盖hashCode方法

  • 问题

    在每个覆盖了equals方法的类中,也必须覆盖hashCode方法,如果不这样的话,就会违反了Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常动作,比如说HashMap,HashSet,Hashtable。那么,Object.hashCode规范是什么?以及一个性能良好的hashCode应该怎样写?

  • 解决

    1. Object.hashCode规范

      • 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法必须都始终如一地返回同一个整数
      • 如果两个对象根据equals(Object)方法比较是相等的。那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
      • 如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中的任意一个对象的hashCode方法,则不一定要产生不同的整数结果。
    2. hashCode的写法

      一个好的散列函数通常倾向于“为不相等的对象产生不相等的hashCode”,编写好的hashCode也如下这种简单的方式:

      1. 把某个非零的常数值,比如说17保存在一个名为result的int类型的变量中。
      2. 对于对象中的每个关键域f(指equals方法中涉及的每个域),完成以下步骤:

    a. 为该域计算int类型的散列码c:

    1. 如果该域是boolean类型,则计算(f ? 1 : 0)

    2). 如果该域是byte、char、short或者int类型,则计算(int)f

    1. 如果该域是long类型,则计算(int)(f^(f>>>32))。
    2. 如果该域是float类型,则计算Float.floatToIntBits(f)。
    3. 如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步 骤2.a.3),为得到的long类型值计算散列值。
    4. 如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals方式来比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个范式,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0(不绝对,但通常是0)。
    5. 如果该域是一个数组,则要把每个元素当做单独的域来处理。也就是说,递归地应用上面的规则,对每个重要的元素计算一个散列码。然后再用2中的方法组合起来。如果数组中的每个元素都很重要,则可以用Arrays.hashCode方法。

    b. 按照下面的公式,把步骤2.a计算得到的散列码c合并到result中。

    result = 31 * result + c;

    1. 返回result。

    2. 示例

      public final class PhoneNumber {
          private final short areaCode;
          private final short prefix;
          private final short lineNumber;
      
          @Override
          public int hashCode() {
              int result = 17;
              result = 31 * result + areaCode;
              result = 31 * result + prefix;
              result = 31 * result + lineNumber;
              return result;
          }
      }
      复制代码
  • 结论

    1. 如果覆盖了equals方法一定要覆盖hashCode方法,否则会造成基于散列值得集合使用出现问题,如HashMap或者HashSet等;
    2. **不要试图从散列码计算中排除一个对象的关键部分来提高性能。**虽然这样可能使计算的速度得到提升,但是效果并不见得会好,可以会导致散列表慢到根本无法使用,如果因此大量的实例映射到极少的散列码上,那基于散列的集合将会显示出平方级的性能。Java平台类库中的许多类如 String、Integer、Date,都可以把它们的hashCode方法返回确切值规定为该实例的一个函数,一般来说,这并不是一个好主意,因为这样做严格地限制了在将来的版本中改进散列函数的能力。

3. 覆盖toString方法

  • 问题

    Object中默认的toString方法,它返回的字符串只类类名加上一个“@符号”,后面是十六进制形式的hashCode,这些信息对我们来说用处不大,所以为了提供更好的关于类和对象的说明,我们应该总是覆盖toString()方法来提供更加清晰的说明,覆盖toString方法的好处以及覆盖toString的注意事项?

  • 解决

    1. 覆盖toString方法的好处

      toString方法虽然不会像equals这样的方法对类造成那么大的影响,但是一个好的toString可以使类用起来更加的舒服。当对象被传给println、printf、字符串联操作符(+)以及assert或者被调试器打印出来时,toString方法会被自动调用。这是一种重要的调用手段,如果不重写toString提供更明确的信息,这将很难让人理解。toString的输出,也可以方便我们debug

    2. 覆盖toString的注意事项

      • 在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息,如果对象太大或者对象中包含的状态信息难以用字符来表达,这样做就有点不切实际了,在这种情况下toString方法应该返回类的关键域信息;
      • 在覆盖toString时可以指定输出格式,这样就可以编写相应的代码来解析这种字符串表示法,产生字符串表示法,以及把字符串表示嵌入到持久的数据中。但是,将来一旦输出格式变化了,会造成更大的问题。是否指定输出格式应该权衡。
  • 总结

    在实际开发过程中最好要覆盖toString方法,将类的有用信息使用toString方法进行输出,这样就可以方便调试或者打印的时候输出

4.实现comparable

  • 问题

    compareTo方法是Comparable接口中唯一的方法,不但允许进行简单的等同性比较,而且允许执行顺序比较。一旦实现了Comparable接口,就可以跟许多泛型方法以及依赖于该接口的集合实现类进行协作。实现CompareTo方法有哪些规范?

  • 解决

    1. 使用compareTo方法有一个重要的约定,就是通常情况下compareTo方法施加的等同性测试和equals方法一致。如果不一致的话,集合接口一般是使用equals方法来进行等同性测试,而有序集合是采用compareTo方法进行等同性测试,如果两者不一致的话,容易造成灾难性的后果;

    2. 将对象与指定的对象进行比较。当该对象小于、等于或者大于指定对象的时候,分别返回一个负整数,零或者正整数,如果由于指定对象的类型而无法与该对象进行比较,则抛出ClassCastException。在下面的说明中,符号sgn(表达式)表示数学中的signum函数,它根据表达式(expression)的值为负值、零和正值,分别返回-1、0、1。

      • 必须确保所有的x和y都满足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))。这也暗示着当且仅当y.compareTo(x)抛出异常时,x.compareTo(y)才抛出异常。
      • 必须确保这个比较关系是可传递的:(x.compareTo(y) > 0 && y.compareTo(z) > 0)暗示着x.compareTo(z) > 0也成立。对应着equals使用规范里面的传递性
      • 必须确保x.compareTo(y) == 0暗示着所有的z都满足sgn(x.compareTo(z)) == sgn(y.compareTo(z))。
      • 强烈建议(x.compareTo(y) == 0) == (x.equals(y)),但是这个并非绝对必要。一般来说,任何实现了Comparable接口的类,若违反了这个条件,都应该明确予以说明。推荐使用这样的说法:“注意,该类具有内在的排序功能,但是与equals不一致”。
    3. 示例

      如果一个类有多个关键域,那么比较这些关键域的顺序非常关键。必须从最关键的域开始,逐步进行到所有的重要域。如果某个域的比较产生了非零的结果(0代表着相等),则整个比较操作结束,并返回该结果。如果最关键的域是相等的,则再比较下一个关键域,以此类推,如果所有域都是相等的,那么才返回0。例如下面的例子:

      public final class PhoneNumber implements Comparable {
      
          private final short areaCode;
          private final short prefix;
          private final short lineNumber;
      
          public PhoneNumber(int areaCode, int prefix,
                              int lineNumber) {
              this.areaCode = (short) areaCode;
              this.prefix = (short) prefix;
              this.lineNumber = (short) lineNumber;
          }
      
          @Override
          public int compareTo(PhoneNumber pn) {
              if (areaCode < pn.areaCode) 
                  return -1;
              if(areaCode > pn.areaCode)
                  return 1;
      
              if (prefix < pn.prefix)
                  return -1;
              if (prefix > pn.prefix)
                  return 1;
      
              if (lineNumber < pn.lineNumber)
                  return -1;
              if (lineNumber > pn.lineNumber)
                  return 1;
      
              return 0;
          }
      }
      复制代码

      可以改进如下:

      public int compareTo(PhoneNumber pn) {
          int areaCodeDiff = areaCode - pn.areaCode;
          if (areaCodeDiff != 0)
              return areaCodeDiff;
      
          int prefixDiff = prefix - pn.prefix;
          if (0 != prefixDiff)
              return prefixDiff;
      
          return lineNumber - pn.lineNumber;
      }
      复制代码

      使用这种方法的时候需要注意,有符号的32位整数还不足以大到能够表达任意两个32位整数的差值,如果i是一个很大的正整数,j是一个很小的负整数,i-j有可能会溢出,并且返回一个负值。

  • 结论

    在实现Comparable接口时,应该遵守这些规范,特别是在做等同性测试的时候,要和equals等同性测试结果保持一致。

分类:
后端