Java继承---巩固强化

1.内容涉及

  1. 类、超类、子类
  2. Object:所有类的超类
  3. 对象包装器与自动装箱
  4. 继承的设计技巧

2.内容详解

(一)类、超类、子类

1.覆盖方法
超类中的有些方法对子类Manager并不一定适用。具体来说,Manager类中的getSalary方法应该返回薪水和奖金的总和。为此,需要提供一个新的方法来覆盖超类中的这个方法(这里可能会有些突兀,因为和上一篇博客有所关联,但影响不大,看完这一块内容就能明白笔者想表达的意思)。

public class Manager extends Employee
{
  ...
  public double getSalary()
  {
   ...
  }
  ...
}

应该如何实现这个方法呢?乍看起来似乎很简单,只要返回salary和bonus字段的总和就可以了:

public double getSalary()
{
  return salary+bonus;//won't work
}

不过,这样做是不行的。只有Employee方法能直接访问Employee类的私有字段。这意味着,Manager类的getSalaryy方法不能直接访问salary字段。如果Manager类的方法想要访问那些私有字段,就要像其他方法一样使用共有接口,在这里就要使用Employee类中的公共方法getSalary。
现在,再试一下。你需要调用getSalary方法而不是直接访问salary字段:

public double getSalary()
{
  double baseSalary=getSalary();//still won't work
  return baseSalary+bonus;
}

上面这段代码仍然有问题。问题出现在调用getSalary的语句上,它只是在调用自身,这是因为Manager类也有一个getSalary方法(就是我们正在实现的这个方法),所以这条语句将会导致无限次地调用自己,知道整个程序最终崩溃
这里需要指出:我们希望调用超类Employee中的getSalary方法,而不是当前类的这个方法。为此,可以使用特殊关键字super解决这个问题,下面是Manager类中getSalary方法的正确版本:

public double getSalary()
{
  double baseSalary=super.getSalary();
  return baseSalary+bonus;
}

正向前面看到的那样,在子类中可以增加字段、增加方法或覆盖超类的方法,不过,继承绝对不会删除任何字段或方法。
:有些人认为super和this引用是类似的概念,实际上,这样比较并不太恰当。这是因为super不是一个对象的引用,例如,不能将值super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。
2.组织继承:final类和方法
有时候,我们可能希望阻止 人们利用某个类定义子类。不允许扩展的类被称为final类。如果在定义类的时候使用了final修饰符就表明这个类是final类。例如,假设希望阻止人们派生Executive类的子类,就可以在声明这个类的时候使用final修饰符。声明格式如下:

public final class Executive extends Manager
{
  ...
}

类中的某个特定方法也可以被声明为final。如果这样做,子类就不能覆盖这个方法(final类中的所有方法自动地成为final方法)。例如

public class Employee
{
 ...
 public final String getName()
 {
   return name;
 }
}

:对于final字段来说,构造对象之后就不允许改变他们的值了。不过,如果将一个类声明为final,只有其中的方法自动成为final,而不包括字段。
3.类的强制类型转换
在进行强制类型转换之前,应该养成这样一个良好的习惯,先查看是否能够成功地转换。否则,Java运行时系统若注意到你的承诺不符,并产生一个ClassCastException异常。如果没有捕获这个异常,那么程序就会终止。为此只需要使用instanceof操作符就可以实现。例如:

if(staff[1] instanceof Manager)
{
  boss=(Manager) staff[1];
}

如果这个类型转换不可能成功,编译器就不会让你完成这个转换。例如,下面这个强制类型转换:

String c=(String) staff[1];

将会产生编译错误,这是因为String不是Employee的子类。
综上所述:
5. 只能在继承层次内进行强制类型转换。
6. 在将超类强制类型转换成子类之前,应该使用instanceof进行检查。
7. 一般情况下,最好尽量少用强制类型转换和instanceof运算符。

(二)Object:所有类的超类

1.equals方法
Object类中的equals方法用于检测一个对象是否等于另外一个对象。Object类中实现的equals方法将确定两个对象引用是否相等。这是一个合理的默认行为:如果两个对象引用相等,这两个对象肯定就相等。不过,经常需要基于状态检测对象的相等性,如果两个对象有相同的状态,才认为这两个对象时相等的。
例如,如果两个员工对象的姓名、薪水和雇佣日期都一样,就认为他们是相等的(在实际的员工数据库中,比较ID才更有意义。我们主要用下面这个实例展示equals方法的实现机制)。

public class Employee
{
  public boolean equals(Object otherObject)
  {
    if(this==otherObject)return true;
    if(otherObject==null)return false;
    if(getClass()!=other.getClass()) return false;
    Employee other=(Employee)otherObject;
    return name.equals(other.name)&&salary==other.salary&&hireDay.equals(other.hireDay);
  }
}

为了防备name或hireDay可能为null的情况,需要是引用Objects.equals方法。如果两个参数都为null,Objects.equals(a,b)调用将返回true;如果其中一个参数为null,则返回false;否则,如果两个参数都不为null,则调用a.equals(b)。利用这个方法,Employee.equals方法的最后一条语句要改写为:

return Objects.equals(name,other.name)&&salary==other.salary&&Objects.equals(hireDay,other.hireDay);

下面给出编写一个完美的equals方法的建议:

  1. 显式参数命名为otherObject,稍后需要将它强制转换成另一个名为other的变量。
  2. 检测this与otherObject是否相等:
if(this==otherObject)return true;
  1. 这条语句只是个优化。实际上,这是一种经常采用的形式。因为检查身份要比逐个比较字段开销小。
  2. 检测otherObject是否为null,如果为null,返回false。这项检测是很必要的。
if(otherObject==null)return false;
  1. 比较this与otherObject的类。如果equals的语义可以在子类中改变,就使用getClass检测:
if(getClass()!=otherObject.getClass()) return false;
  1. 将otherObject强制转换为相应类类型的变量:
ClassName other=(ClassName)otherObject;
  1. 现在根据相等性概念的要求来比较字段。使用==比较基本类型字段,使用Objects,equals比较对象字段。如果所有的字段都匹配,就返回true;否则返回false。
return field1=other.field1&&Objects.equals(field2,other.field2)...;

你以为写好了equals方法,就真的可以equal了嘛,不!!!(已泪奔)
如果重新定义了equals方法,就必须为用户可能插入散列表的对象重新定义hashCode方法。为什么呢?注意到上述return代码里的Objects.equals了嘛,它比较的就是传入参数的散列码,为了让它更加准确,我们不用默认的hashCode,而自己重新定义一个新的hashCode方法。hashCode方法应该返回一个整数(也可以是负数)。要合理地组合实例字段的散列码,以便能够让对象产生的散列码分布更加均匀。可以简单简写为如下:

public int hashCode()
{
  return Objects.hash(name,salary,hireDay);
}

:equals与hashCode必须相容:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()返回相同的值。例如,如果定义Employee.equals比较员工的ID,那么hashCode方法就需要散列ID,而不是员工的姓名或存储地址。

(三)对象包装器与自动装箱

弱弱的问一句,你知道Integer和int有啥区别嘛?如果清楚的话,再见!,如果不太明白,下面的内容能帮你解决这个问题。
有时,需要将int这样的基本类型转换为对象。所有的基本类型都有一个与之对应的类。例如,Integer类对应基本类型int。通常,这些类称为包装器。这些包装器类有显而易见的名字:Integer,Long,Float,Double,Short,Byte,Character和Boolean(前6个类派生与公共的超类Number)。包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。
假定想要定义一个整型数组列表。遗憾的是,尖括号中的类型参数不允许是基本类型,也就是说,不允许写成ArrayList。这里就可以用到Integer包装器类。我们可以声明一个Integer对象的数组列表。

ArrayList list=new ArrayList<Integer>();

:由于每个值分别包装在对象中,所以ArrayList的效率远远低于int[]数组。因此,只有当程序员操作的方便性比执行效率更重要的时候,才会考虑对较小的集合使用这种构造。
幸运的是,有一个很有用的特性,从而可以很容易地向ArrayList添加int类型的元素。下面这个调用

list.add(3);

将自动地变换成

list.add(Integer.valueOf(3));

这种变换称为自动装箱。
相反的,当将一个Integer对象赋给一个int值时,将会自动地拆箱。也就是说,编译器将以下语句:

int n=list.get(i);

转换成

list.add(Integer.valueOf(3));

大多数情况下容易有一种假象,认为基本类型与它们的对象包装器是一样的。但它们有一点有很大不同:同一性。大家知道,==运算符可以应用于包装器对象,不过检测的是对象是否有相同的内存位置因此,下面的比较通常会失败:

Integer a=100;
Integer b=100;
if(a==b)...

这里给大家实例运行一下:
我承认我打自己脸了…
在这里插入图片描述幸好还能挽回一些…
在这里插入图片描述
这种不确定的结果并不是我们所希望的。这里强调一下,装箱和拆箱是编译器要做的工作,而不是虚拟机。编译器在生成类的字节码时会插入必要的方法调用。虚拟机只是执行这些代码。

(四)继承的设计技巧

  1. 将公共操作和字段放在超类中
  2. 不要使用受保护的字段
  3. 使用继承实现“is-a”关系
  4. 除非所有继承的方法都有意义,否则不要使用继承
  5. 在覆盖方法时,不要改变预期的行为
  6. 使用多态,而不要使用类型信息

猜你喜欢

转载自blog.csdn.net/Achenming1314/article/details/105744643
今日推荐