Java基础知识 二 继承

来源:互联网 发布:mysql password 编辑:程序博客网 时间:2024/05/29 11:37

继承已存在的类就是复用(继承)这些类的方法和域。在此基础上,还可以添加一些新的方法和域,以满足新的需求。
(1)类、父类和子类
覆盖(override):重写父类中的方法。
super关键字的两个作用:

  • 一是,调用父类的方法;
  • 二是,调用父类的构造器。

this关键字的两个作用:

  • 一是,引用隐式参数;
  • 二是,编写构造器时,调用同一个类中的其他构造器。

多态:一个对象可以指示多种实际类型的现象。
动态绑定:在运行时能够自动地选择调用哪个方法的现象。

①继承层次
继承层次、继承链
②多态

Manager boss = new Manager(...);boss.setBonus(5000); //okEmployee[] staff = new Employee[3];staff[0] = bossstaff[0].setBonus(5000); //ERROR

在上例中,变量staff[0] 与 boss 引用同一个对象。但编译器将staff[0] 看成 Employee 对象。
③动态绑定
调用对象方法的执行过程、重载解析、静态绑定、动态绑定、方法表
④阻止继承:final类和方法
final类:不允许扩展(继承)的类。
final方法:子类不能覆盖这个方法(final类中的所有方法自动成为final方法,但域不是final的)。
final域:构造对象后,不允许改变它们的值。
类设计时的建议:仔细思考应该将哪些方法和类声明为final的。
内联(inlining):如果一个方法没有被覆盖且很短,编译器就能够对它进行优化处理,这个过程称为内联。
⑤强制类型转换
类型转换:将一个类型强制转换成另一个类型的过程。
进行类型转换的唯一原因:在暂时忽视对象的实际类型之后,又需要使用对象的全部功能。
注意:在进行类型转换之前,先查看一下是否能够成功的转换(可使用 instanceof 运算符检测)。
综上所述:

  • 只能在类继承层次内进行类型转换。
  • 在将父类转换成子类之前,应该使用 instanceof 进行检查。

⑥抽象类
这里写图片描述
为何要进行高层次的抽象?

  • 在类的继承层次中,位于上层的祖先类更具有通用性,所以可以用作基类来定义子类中的公共部分, 但同时不希望这个类能够被实例化(被直接使用);
  • 基类中的需要定义某些方法,但不想实现这些方法,所以需要将方法声明为abstract的(当然,也可以让它返回空字符串),在Java中为了程序的清晰度,包含一个或多个抽象方法的类必须声明为抽象类;

抽象类的特性?

  • 抽象类不能被实例化。
  • 除了抽象方法以外,抽象类还可以包含具体数据和具体方法。
  • 抽象方法充当着占位的角色,它们的具体实现在子类中。扩展抽象类的两种选择:
    • 一种是在扩展类中定义部分原抽象类的方法或不定义原抽象类的方法,这样扩展类也必须标记为抽象类;
    • 另一种是在扩展类中定义原抽象类全部的抽象方法,这样一来,子类就不是抽象的了。
  • 类不含抽象方法,也可以声明类为抽象的。
  • 可以定义一个抽象类的对象变量,但是它只能引用抽象类的非抽象子类的对象。例如,

    Person p = new Student("Maria Morris", "computer science");

    注意:Person 是Student类的父类,且是抽象的;Student类是非抽象的,它定义了Person类的全部抽象方法。

  • 对于如下调用的解释:

    for (Person p : people)    System.out.println(p.getName() + ", " + p.getDescription());
    • 这个调用,调用了一个没有定义的方法吗?
      注意,由于不能构造抽象类 Person 的对象,所以变量 p 永远不会引用Person对象,它引用的是诸如 Employee 或 Student 这样的具体子类对象,而这些对象中都定义了 getDescription 方法。
    • 是否可以省略 Person 父类中的抽象方法getDescription 呢?
      如果这样,就不能通过变量 p 调用 getDescription 方法了。编译器只允许调用在类中声明的方法。

⑦受保护访问
Java中受保护的部分,对于所有的子类及同一个包中的所有其他类都可见。

Java中用于控制可见性的4个访问修饰符:

  • 仅对本类可见——private。
  • 对所有类可见——public。
  • 对本包和所有子类可见——protected。
  • 对本包可见——默认,不需要修饰符。

(2)Object:所有类的超类
Oject 类是Java 中所有类的始祖,在Java中每个类都是由它扩展而来的。如果没有明确指出父类,Object类就被认为是这个类的父类。因此,熟悉这个类所提供的所有服务非常重要。
在Java中,只有基本类型不是对象,例如,数值、字符和布尔类型的值。所有的数组类型,不管是对象数组还是基本数组都扩展于Object。
①equals方法

  • Object的equals方法的代码实现:

    public boolean equals(Object obj) {   return (this == obj);}

    即,equals 要求两个对象具有相同的引用时,返回true;否则,返回false。

  • 通常,检测两个对象的状态是否完全相同,来检测两个对象是否相等(即equals 返回值为true;但注意与== 返回true的区分:当且仅当同一个对象。)。见P170页。

②相等测试与继承
编写一个完美的 equals 方法的建议:
1)显式参数命名为 otherObject,稍后需要将它转换成另一个叫做 other 的变量。
2)检测 this 与 otherObject 是否引用同一个对象:

if(this == otherObject) return true;

这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个的比较类中的域所付出的代价小得多。
3)检测 otherObject 是否为 null,如果为 null,返回 false。这项检测是很有必要的。

if(otherObject == null) return false;

4)比较 this 与 otherObject 是否属于同一个类。如果 equals 的语义在每个子类中有所改变,就用 getClass 检测:

if(getClass() == null) return false;

如果所有的子类都拥有统一的语义,就是用 instanceof 检测:

if(!otherObject instanceof ClassName) return false;

5)将 otherObject 转换为相应的类类型变量:

className other = (ClassName) otherObject;

6)现在开始对所有需要比较的域进行比较。使用 == 比较基本类型域,使用 equals 比较对象域。如果所有的域都匹配,就返回 true;否则返回 false。

return field1 == other.field1 && Objects.equals(field2, other.field2) && ...;

如果在子类中重新定义 equals,就要在其中包含调用 super.equals(other)。

注意:下面是实现equals 方法常见的错误。可以找到其中的问题吗?

public boolean equals(Employee other){      return Objects.equals(name, other.name)           && salary == other.salary           && Objects.equals(hireDay, other.hireDay);    ...   }

这个方法声明的显式参数类型是 Employee。其结果并没有覆盖 Object 类的 equals 方法,而是定义了一个完全无关的方法。
为了避免发生类型错误,可以使用 @Override 对覆盖父类的方法进行标记;

@Overridepublic boolean equals(Object otherObject)

如果出现了错误,并且正在定义一个新方法,编译器就会给出错误报告(提示,该方法没有覆盖父类 Object 中的任何方法)。

③hashCode 方法

  • 散列码(hash code)是由对象导出的一个整型值。散列码是没有规律的。如果x 和 y 是两个不同的对象,x.hashCode() 与 y.hashCode() 基本上不会相同。
  • 如果重新定义 equals 方法,就必须重新定义 hashCode 方法,以便用户可以将对象插入到散列表中。(??)
    具体解释参见,为什么在重写equals时必须重写hashCode方法,两个方法的解析

  • hashCode方法应该返回一个整形数值(也可以是负数),并合理地组合实例域的散列码,以便能够让各个不同的对象产生的散列码更加均匀。

  • 在Java 7 中对获取散列码的方法做出了改进:

    • 首先,最好使用 null 安全的方法 Objects.hashCode 。如果其参数为 null,这个方法返回0,否则返回对参数调用 hashCode 的结果。

      public int hashCode(){    return 7 * name.hashCode()        + 11 * new Double(salary).hashCode()        + 13 * hireDay.hashCode(); }
    • 还有更好的做法,需要组合多个散列值时,可以调用 Objects.hash 并提供多个参数。这个方法会对各个参数调用 Objects.hasCode,并组合这些散列值。这样 Employee.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,而不是雇员的姓名或存储地址。
    ④toString 方法

  • toString方法是Object类的一个重要方法,它用于返回表示对象值的字符串。

  • 绝大多数(但不是全部)的 toString 方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。例如,下面是 Employee 类的 toString 方法的实现:

    public String toString()   {      return getClass().getName() + "[name=" + name             + ",salary=" + salary + ",hireDay=" + hireDay            + "]";   }

    当然,设计子类的程序员也应该定义自己的 toString 方法,并将子类域的描述添加进去。如果使用了 getClass().getName(),那么子类只要调用 super.toString() 就可以了。例如,下面是 Manager类的 toString 方法的实现:

     public String toString()   {      return super.toString() + "[bonus=" + bonus + "]";   }
  • Object 类定义了 toString 方法,用来打印输出对象所属的类和散列码。

  • 注意,数组继承了 Object类的 toString 方法,数组类型将按照旧的格式(Object.toString())打印。修正的方法是调用静态方法 Arrays.toString (一维数组打印)或 Arrays.deepToString (二维数组打印)。
  • 提示:强烈建议为自定义的每一个类增加 toString 方法。

(3)泛型数组列表
在C++中,必须在编译时就确定整个数组的大小。这会造成空间的严重浪费,或是产生复杂的操作。

  • 在Java中,解决这个问题最简单的办法是使用ArrayList类。它使用起来像数组,但是在添加或删除元素时,具有自动调节容量的功能,而不需要为此编写任何代码。
  • ArrayList 是一个采用类型参数的泛型类。语法为:

    ArrayList<E> list = new ArrayList<>();  //菱形语法
  • 数组列表管理着对象引用的一个内部数组。当数组全部空间用完时,如果再调用 add 方法,数组列表将自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。
  • 如果已经确定或能够估计数组可能存储的元素数量,就可以在填充数组之前调用 ensureCapcity 方法。
  • 特别注意:

    • 分配数组列表,如下所示:
    new ArrayList<>(100) //capacity is 100

    与为新数组分配空间有所不同:

    new Emplyee[100]    //size is 100

    第一种分配方法,知识拥有保存100个元素的潜力,但是在最初,甚至完成初始化构造之后,数组列表根本就不含任何元素(size 方法的返回值为0,因为它返回的是数组列表中当前元素的个数);第二种,为数组分配了100个存储空间的空位置(a.length 的值为100)。

  • 一旦能够确定数组列表的大小不再发生变化时,可调用 trimToSize 方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。

① 访问数组列表元素
add(添加、插入新元素)、get(读取)、set(只能替换已存在的元素)、remove(删除)
②类型化与原始数组列表的兼容性
给出警告标记即可。

(4)对象包装器与自动装箱(重点补充)
包装器(wrapper)、自动装箱、自动拆箱

(5)参数数量可变的方法
(6)枚举类

  • 比较两个枚举类型的值时可以直接使用“==”比较。
  • 所有的枚举类型都是Enum类的子类。它们继承了这个类的很多方法。
  • toString方法、valueOf方法、values方法、ordinal方法

(7)反射
待补充。
(8)继承设计的技巧
1)将公共操作和域放在父类中。
2)不要使用受保护的域。
3)使用继承实现“is-a”关系(注意不要滥用继承)。
4)除非所有继承的方法都有意义,否则不要使用继承。
5)在覆盖方法时,不要改变预期的行为(不要改变方法的含义)。
6)使用多态,而非类型信息。
无论何时,对于下面这种形式的代码都应该考虑使用多态性:

if(x is of type1)    action1(x);else if(x is of type2)    action2(x);

如果action1 和 action2表示的是相同概念,就应该考虑将其放在两个类的父类或接口中,然后就可以调用
x.action();
以便使用多态提供的动态分派机制执行相应的动作。
使用多态或接口编写的代码比使用对多种类型进行检测的代码更加易于维护和扩展。
7)不要过多的使用反射。
反射对于编写系统程序来说极其有用,但通常不适用于编写应用程序。反射很脆弱,即编译器很难帮助人们发现程序中的错误,因此只有在运行时才发现错误并导致异常。

原创粉丝点击