[Java学习笔记] Java核心技术 卷1 第五章 继承

拥有回忆 提交于 2020-02-01 17:11:46

第5章 继承

利用继承,可以基于已存在的类构造一个新类。继承已存在的类就是复用(继承)这些类的方法和域。还可以在此基础上添加新的方法和域。

反射。

 5.1 超类子类

使用extends构造一个派生类

class Manager extends Employee
{
    添加方法和域
    覆盖:重写一些基类中不适合派生类的方法
}

所有的继承都是公有继承。即所有的公有成员和保护成员都保持原来的状态,基类的私有成员仍然是私有的。不能被派生类的成员访问。

(C++中私有继承或保护继承会将 公有成员和保护成员都变成派生类的私有成员或保护成员)

基类、父类、超类:被继承的类

派生类、孩子累、子类:新的类

使用super调用基类被覆盖的方法。super与this不同,仅是用来指示编译器调用超类方法的特殊关键字。

使用super调用基类的构造器 super(参数列表..);由于派生类不能调用基类的私有域,所有需要调用构造器对私有域初始化。

如果派生类没有显示调用超类构造器,则自动调用基类默认的构造器。如果超类没有不带参数的构造器又没有显式的调用超类的其他构造器,则Java编译器将报错。

class Employee
{
   private double salary;
   public double getSalary(){...}
   public Employee(double x){...}
}
class Manager extends Employee
{
   private double bonus;
   public void setBonus(double b){...}
   public double getSallary()
   {
       //return salary+bonus;  不能运行,salary是基类的私有变量
       //return bonus + getSalary(); 不能运行,派生类覆盖了基类的getSalary方法
       return bonus +super.getSalary();//通过super调用基类的getSalary方法
   }
   public Manager(double b)
   {
       super(b);//调用基类构造器
   }
}

5.1.1 覆盖

子类中有与超类中相同签名的方法是为覆盖。

由于方法签名包括方法名和参数列表而不包括返回值,所有覆盖方法的返回类型可以是原类型的子类型。保证兼容。

在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。特别是如果超类的方法是public,子类方法一定要声明为public。

超类中的final方法不可被覆盖。

可以使用@Override来标记方法表示它需要覆盖超类,如果超类中没有合适的方法被覆盖就会报错。否则很可能程序员以为覆盖了超类的方法,但其实是写了一个仅属于子类的新方法。

@Override public boolean equals(Employee other)

5.1.2 继承层次

继承并不限于一个层次。但不支持多继承。

由一个公共超类派生出来的所有类的集合被称为继承层次。

从某个特定的类到其祖先的路径被称为该类的继承链。

5.1.3 多态

一个对象变量可以指示多种实际类型的现象称为多态。

祖先对象可以引用任何一个不同层次的后代对象,而不能将超类的引用赋给子类(需要强制类型转换)。

子类数组的引用可以转换成超类数组的引用而不需要采用强制类型转换。在这种情况下所有数组都要牢记创建它们的元素类型,并负责监督将类型兼容的引用存储到数组中。

is-a 规则 :4.1.3 。子类的每个对象也是超类的对象,反之出现超类的任何地方都可以用子类对象置换。

5.1.4 动态绑定

在运行时根据隐私参数的实际类型自动选择调用哪个方法的现象称为动态绑定。

在调用对象方法时:

1:编译器首先获得所有可能被调用的候选方法。通过查看对象获取其声明类型C和方法名f,可能会有多个相同名字但参数不同的f方法,编译器会一一列举C类型中的f方法以及C的超类中访问属性为public的f方法。

2:编译器查看调用方法时提供的参数列表。如果在所有名为f的方法中存在一个完全匹配的就选择这个方法,这个过程称为重载解析。

3:如果是private static final 方法或者构造器,编译器可以准确知道应该调用哪个方法,这种调用方式称为静态绑定。当调用的方法依赖于隐式参数的实际类型,并且在运行时绑定的称为动态绑定。

4:当程序运行,并且采用动态绑定方法时,虚拟机一定调用与其所引用对象的实际类型最合适的那个类的方法。

5.1.5 final类和方法

阻止继承,使用final修饰符。

使用final修饰类则这个类被称为final类,不可被继承;使用final修饰方法则这个方法不可被覆盖。

如果将一个类声明为final时,只有其中的方法自动称为final,不包括域。

域也可以被声明为final,对于final域来说,构造对象之后就不允许改变值了。

将方法或类声明为final的主要目的是确保它们不会在子类中改变语义。

5.1.6 强制类型转换

Manager boss = (Manager)employee;

进行类型转换的唯一原因是:在暂时忽略对象的实际类型之后,使用对象的全部功能。

将超类的引用赋给子类变量时必须进行类型转换。

转换失败时会抛出异常,而不会像C++那样生成一个null对象。

使用instanceof运算符判断是否能够成功转换:

if(employee instanceof Manager)
{
   boss=(Manager)employee;
   ...
}

5.1.7 抽象类

abstract class Person
{
   ....
   public abstract String getDescription();
}

某个方法在派生类中有不同的覆盖实现,如果祖先类不作为特定类使用时,可以将这个方法声明为抽象方法,从而忽略在祖先类中的具体实现。

抽象方法充当着占位的角色,它们的具体实现在子类中。

扩展抽象类可以由两种选择,一是在子类中定义部分抽象方法或抽象方法也不定义,这样子类必须被标记为抽象类;二是定义全部的抽象方法,这样子类就不是抽象的了。

当一个类中包含一个或多个抽象方法时类本身必须被声明为抽象的。类即使不含抽象方法也可以将类声明为抽象类。

抽象类不能被实例化,不能创建抽象类本身的对象,但可以引用非抽象的子类。

除了抽象方法以外,抽象类还可以包含具体数据和具体方法。建议尽量将通用的域和方法(不管是不是抽象的)放在超类(不管是否抽象类)中。

在Java程序设计语言中,抽象方法是一个重要的概念,在接口中将会看到更多的抽象方法。

5.1.8 受保护访问

超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域,则可以将这些方法或域声明为protected。

子类只能访问其对象中的受保护域,而不腻访问其他超类对象中的这个域。

谨慎使用protected属性,由于派生出的类中可以访问受保护域,如果需要对类的实现进行修改,就必须通知所有使用了这个类的程序员,违背了OOP提倡的数据封装原则。

不过protected方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。

5.1.9 访问修饰符

private       仅对本类可见

public        所有类可见

protected     本包和所有子类可见

无修饰         对本包可见,默认

5.2 Object

Object类是Java所有类的始祖,每个类都由它扩展而来。如果没有明确指出超类,Object就被认为是这个类的超类。

只有基本类型不是对象.

//可以使用Object类型的变量引用任何类型的对象:
Object obj = new Employee();
//但Object类型的变量只能作为各种值的通用持有者,要写对其进行具体的操作,还需要转换成原始类型:
Employee em = (Employee)obj;

5.2.1 equals方法

检测一个对象是否等同于另一个对象。 在Object中这个方法判断两个对象是否具有相同的引用。

如果重新定义equals方法,则必须重新定义hashCode方法,以便用户可以将对象插入到散列表中。

对于数组类型的域,可以使用静态的Arrays.equals方法检测相应的数组元素是否相等。

Java语言要求equals方法具有下面的特性:

1:自反性,对任何非空引用x,x.equals(x)返回true

2:对称性,对任何引用x和y,当且仅当y.equals(x)返回true,x.equals(y)也应该返回true

3:传递性,对任何引用x y z,如果x.equals(y) y.equals(z)返回true,那么x.equals(z)也应该返回true

4:一致性,如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。

5:对任意非空引用x,x.equals(null)应该返回false

 

完美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() != otherObject.getClass()) return false;

  如果所有的子类拥有统一的语义,使用Instanceof检测:

  if(!(otherObject instanceof ClassName)) returen false;

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

   ClassName other = (ClassName) otherObject;

6:对需要比较的域进行比较。使用==比较基本类型域,使用equals比较对象域。所有域都匹配则返回true,否则false

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

5.2.2 hashCode方法

散列码是由对象导出的一个整形值。没有规律。

hashCode方法定义在Object类中,每个对象都有一个默认的散列码,其值为对象的存储地址。

String的散列码是由内容导出的。

如果重新定义equals方法,则必须重新定义hashCode方法,以便用户可以将对象插入到散列表中。

最好使用null安全的方法 Objects.hashCode,如果其参数为null则返回0

equals 与 hashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode()就必须与y.hashCode()相同。

数组类型的域可以使用Arrays.hashCode计算一个散列码,这个散列码由数组元素的散列码组成。

5.2.3 toString 方法

大多数的toString方法都应该遵循这样的格式:类名[域=域值,...]

数组的默认toString是按照旧格式打印,[I@...] 前缀[I表明是一个整形数组,内容不是元素值罗列的字符串。可以使用Arrays.toString代替,将生成元素的字符串[2,3...]。

多维数组使用Arrays.deepToString

5.3 泛型数组列表

5.3.1 数组列表

一旦确定了数组大小,改变它就不容易了。可以使用ArrayList。

ArrayList是一个采用类型参数的泛型类。

<>内的类型参数不允许是基本类型。

数组列表的容量与数组的大小有个非常重要的区别,如果为数组分配100个元素的存储空间,数组就有100个空位置可以使用。而容量为100个元素的数组列表只是拥有保存100个元素的潜力(实际上重新分配空间的话将会超过100个),但是在最初,甚至完成初始化构造之后,数组列表根本就不包含任何元素。

对数组的插入和删除效率较低,大型的应使用链表。

//构造一个空数组列表
ArrayList<Employee> staff = new ArrayList<>();

//添加 调用add且内部数组满了,将自动创建一个更大的数组,并将所有对象拷贝过去
staff.add(new Employee());

//确保数组列表在不重新分配存储的情况下就能够保存给定数量的元素
staff.ensureCapacity(100);

//返回实际的元素数目,等价于数组a的a.length
staff.size();

//用指定容量构造一个空数组列表
ArrayList<Employee> staff = new ArrayList<>(100);

//将数组列表的存储容量削减到当前尺寸
staff.trimToSize();

//创建列表并拷贝到数组中
ArrayList<x> list = new ArrayList<>();
while(...){ x = ...; list.add(x); }
X[] a = new  x[list.size()];
list.toArray(a);

//在第n个位置插入,n之后的后移
staff.add(n,e);

//使用get和set方法实现访问或改变数组元素的操作,而非[]方式。
//替换已存在的i元素的内容
staff.set(i,harry);
// 获取第i个元素
Employee e = staff.get(i);

//移除第n个元素
Employee e = staff.remove(n);

5.3.2 类型化与原始数组列表的兼容性

5.4 对象包装器

将基本类型转换为对象。所有的基本类型都有一个与之对应的类,这些类被称为包装器。

Integer  Long  Float  Double  Short  Byte  Character  Void  Boolean

对象包装器的类是不可变的,一旦构造了包装器,就不允许更改包装在其中的值。同时对象包装器的类还是final,因此不能定义它们的子类。

基本类型的数组列表,如整型,不能写成ArrayList<int> 可以写成ArrayList<Integer> list= ...;效率比数组低的多。

list.add(3);将自动变成list.add(Integer.valueOf(3));这种变化成为自动装箱。

相反将Integer对象赋给一个int值时将会自动拆箱。

int n = list.get(i); 将被翻译成 int n = list.get(i).intValue();

装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。

//将字符串转换成整型
int x = Integer.parseInt(s);

//修改数字参数值  使用持有者类型 每个持有者类型都包含一个公有域值
public static void triple(IntHolder x)
{  x.value = 3 * x.value; }

5.5 参数数量可变的方法

public PrintStream printf(String fmt ,Object... args)
{
   return format(fmt,args);
}

省略号...是代码的一部分,表明这个方法可以接收任意数量的对象

允许将一个数组传递给可变参数方法的最后一个参数

System.out.printf("%d %s",new Object[]{new Integer(1),"widgets" });

因此可以将已存在且最后一个参数是数组的方法重新定义为可变参数的方法而不会破坏任何已存在的代码。

public static void main(String... args)

 5.6 枚举类

public enum Size {SMALL , MEDIUM , LARGE , EXTRA_LARGE };

实际上这个什么定义的类型是一个类,刚好有4个实例,在此尽量不要构造新对象。

在比较两个枚举类型的值时,永远不需要调用equals,直接使用 ==

可以在枚举类型中添加一些构造器、方法和域。构造器只在构造美剧常量的时候被调用。

所有枚举类型都是Enum类的子类,

//toString返回枚举常量名
Size.SMALL.toString() 返回字符串 "SMALL"

//valueOf toString的逆方法
Size s = Enum.valueOf(Size.class,"SMALL"); 将s设置成Size.SMALL

//values 返回包含全部枚举值的数组
Size[] values = Size.values();

//ordinal返回enum声明中枚举常量的位置,从0开始
int i = Size.MEDIUM.ordinal() ;//i=1

//compareTo(E e) 返回相对顺序 在e前返回负值;this==e返回0;在后面返回正值

5.7 反射

能够分析类能力的程序称为反射。

5.7.1 Class类

在程序运行期间,Java运行时系统时钟为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。保存这些信息的类称为Class。

一个Class对象实际上表示的是一个类型,这个类型未必一定是一种类。如int不是类,但int.class是一个Class类型的对象。

Employee e ;...

//getClass返回一个Class类的实例。
Class c = e.getClass();

//返回类名字,包含包名
c.getName();

//Class.forName()获得类名对应的Class对象
//这个方法只有在className是类名或接口名才能够执行。否则抛出checkedexception 已检查异常。无论何时使用这个方法都应该提供一个异常处理器

String className="java.util.Date";
Class c1 = Class.forName(className);

//T.class代表匹配的T类对象
Class cl1 = Date.class;
Class cl2 = int.class;

//虚拟机为每个类型管理一个Class对象,使用==实现两个类对象比较
if(e.getClass() == Employee.class )...

//newInstance() 创建一个类的实例 如果类没有默认构造器会抛出异常
e.getClass().newInstance();

//根据类名创建一个对象
Object o = Class.forName("java.util.Date").newInstance();

//按名称创建构造器带参数的对象
Constructor.newInstance( ... );

5.7.2 捕获异常

异常有两种类型:未检查异常和已检查异常。已检查异常编译器会检查是否提供了处理器,未..不会。

try{
   statements that maight throw exceptions
}
catch(Exception e){
   handler action
}

5.8 继承设计的技巧

1:将公共操作和域放在超类。

2:不要使用受保护的域。

   子类访问超类的域,破坏封装。

3:使用继承实现 is-a 关系

   例如职工超类包含工资,钟点工不包含工资是按时间收费。若按继承实现钟点工,则需要给其增加一个计时工资域,但还会继承职工类中的工资域,与实际的钟点工含义不同,不符合 is-a关系

4:除非所有继承的方法都有意义,否则不要使用继承。

5:覆盖方法时不要改变预期的行为,不应该毫无缘由的改变行为的内涵。

   覆盖子类中的方法时,不要偏离最初的设计想法。

6:使用多态,而非类型信息

   对以下情况应该考虑使用多态性,如果action1和action2是相同的概念,应该为这个概念定义一个方法并将其放置在两个类的超类或接口中。然后调用x.action();动态绑定

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

7:不要过多使用反射

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!