1-05-1 继承

1-05-1 继承

  • 利用继承,人们可以基于已存在的类构造一个新类。
  • 继承已存在的类就是复用(继承)这些类的方法和域。
  • 在此基础上,还可以添加一些新的方法和域,以满足新的需求。
  • 反射(reflection)是指在程序运行期间发现更多的类及其属性的能力

5.1 类、超类和子类

5.1.1 定义子类
  • 关键字extends表示继承。关键字extends表明正在构造的新类派生于一个已存在的类。
  • 已存在的类称为超类(super class)、基类(base class)或父类( parent class);
  • 新类称为子类(subclass)、派生类(derived class)或孩子类(child class)。
  • 子类比超类拥有的功能更加丰富。
5.1.2 覆盖方法
  • 可以在子类定义一个新的方法来覆盖(override)超类中的这个方法
  • 子类中的方法不能够直接访问超类中的私有域,如果需要访问,需要借助超类中的公有方法。
  • super关键字:代表超类对象。super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。
5.1.3 子类构造器
  • public Manager(String name, double salary,int year, int month, int day){
    	super(name,salary,year,month,day);//调用超类中含有name,salary,year,month和day参数得构造器
    	bonus = 0;
    }
    
  • super(xxx);表示调用超类中含有xxx参数的构造器

  • 由于子类得构造器不能访问超类的私有域,所以必须利用超类的构造器对这部分私有域进行初始化,可以通过super实现对超类构造器的调用。使用super调用构造器的语句必须是子类构造器的第一条语句。如果子类的构造器没有显示地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。

  • this:(1)引用隐式参数;(2)调用该类其他的构造器

  • super:(1)调用超类的方法;(2)调用超类的构造器

  • 一个对象变量可以指示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。

  • 在Java中,动态绑定是默认的处理方式。

5.1.4 继承层次
  • 由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy)。
  • 在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链( inheritance chain)。
  • Java不支持多继承
5.1.5 多态
  • “is-a”规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以用子类对象置换。

    //例如,可以将一个子类的对象赋给超类变量。
    Employee e;
    e = new Employee(. . .);//Employee object expected
    e = new Manager(. . .);// OK,Manager can be used as well
    
  • 在Java程序设计语言中,对象变量是多态的

    //从程序清单5-1中,已经看到了置换法则的优点:
    Manager boss = new Manager(. . .);
    Employee[] staff = new Employee[3];
    staff[O]= boss;
    //在这个例子中,变量staff[O]与boss引用同一个对象。但编译器将staff[0]看成Employee对象。
    //这意味着,可以这样调用
    boss.setBonus(5000);// OK
    //但不能这样调用
    staff[0].setBonus(5000);// Error
    //这是因为staff[0]声明的类型是Employee,而setBonus不是Employee类的方法。
    
  • 不能将一个超类的引用赋给子类变量。

    Manager m = staff[i]; //Error,不是所有的雇员都是经理
    
  • 在Java中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型转换。

5.1.6 理解方法调用
  • 方法调用的过程

    • 编译器查找对象的声明类型和方法名(超类中的私有方法不可访问);
    • 编译器查看调用方法时提供的参数类型。通过重载解析(overloading resolution)找到上一步找到的方法中与通过的参数类型完全匹配(考虑类型转换intdouble...)的方法。如果没找到,将报错。
    • 如果是private方法、static方法、final方法或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定(static binding)。 与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定
    • 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实
      际类型最合适的那个类的方法。假设x的实际类型是D,它是C类的子类。如果D类定义了方法f(String),就直接调用它;否则,将在D类的超类中寻找f(String),以此类推。
    • 每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表( method table),其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。

    如果在子类中定义了一个与超类签名相同的方法,那么子类中的这个方法就覆盖了超类中的这个相同签名的方法。

    允许子类将覆盖方法的返回类型定义为原返回类型的子类型

    //假设Employee类有
    public Employee getBuddy(){...}
    //经理不会想找这种地位低下的员工。为了反映这一点,在后面的子类Manager中,
    //可以按照如下所示的方式覆盖这个方法
    public Manager getBuddy() {...}// OK to change return type
    
  • 动态绑定有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展。

    • 假设增加一个新类Executive,并且变量e有可能引用这个类的对象,我们不需要对包含调用e.getSalary()的代码进行重新编译。如果e恰好引用一个Executive类的对象,就会自动地调用Executive.getSalary()方法。
  • 在覆盖一个方法的时候,子类方法不能低于超类方法的可见性。

    • 特别的,如果超类方法时public,子类方法一定要声明为public。
5.1.7 阻止继承:final类和方法
  • 有时可能希望阻止人们利用某个类定义子类。不允许扩展的类被称为final类
  • 类中的特定方法也可以被声明为final。如果这样做,子类就不能覆盖这个方法( final类中的所有方法自动地成为final方法)。
  • 如果将一个类声明为final,只有其中的方法自动地成为final,而不包括域。
  • 将方法或类声明为final的主要目的是:确保他们不会在子类中改变语义
  • 如果一个方法没有被覆盖并且很短,编译器就能够对它进行优化处理,这个过程为称为内联(inlining)
    • 例如,内联调用e.getName()将被替换为访问e.name域
5.1.8 强制类型转换
  • 正像有时候需要将浮点型数值转换成整型数值一样,有时候也可能需要将某个类的对象引用转换成另外一个类的对象引用。

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

  • 将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量,必须进行类型转换

  • instanceof 操作符:

    只能在继承层次内进行类型转换。

    • 在将超类转换成子类之前,应该使用instanceof进行检查。
  • 在一般情况下,应该尽量少用类型转换和instanceof运算符。

5.1.9 抽象类
  • 位于上层的类更具有通用性,甚至可能更加抽象。
  • abstact关键字:包含一个或多个抽象方法的类本身必须被声明为抽象的。除了抽象方法之外,抽象类还可以包含具体数据和具体方法。
  • 抽象方法充当着占位的角色,它们的具体实现在子类中。扩展抽象类可以有两种选择。
    • 一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;
    • 另一种是定义全部的抽象方法,这样一来,子类就不是抽象的了。
  • 类即使不含抽象方法,也可以将类声明为抽象类。
    • 抽象类不能被实例化。
    • 可以定义一个抽象类的对象变量,但是它只能引用非抽象子类的对象。
      例如:
Person p = new Student("Jacky", "CS");
  • 编译器只允许调用在类中声明的方法。
5.1.10 受保护访问
  • 最好将类中的域标记为private,而方法标记为public。
  • 任何声明为private的内容对其他类都是不可见的。
  • 前面已经看到,这对于子类来说也完全适用,即子类也不能访问超类的私有域。
  • 然而,在有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为protected
  • 事实上, Java中的受保护部分对所有子类及同一个包中的所有其他类都可见。
  • 下面归纳一下Java用于控制可见性的4个访问修饰符:
    • 仅对本类可见一private。
    • 对所有类可见一public。
    • 对本包和所有子类可见——protected。
    • 对本包可见一默认 (很遗憾),不需要修饰符。

5.2 Object:所有类的超类

  • 如果没有明确地指出超类,Object就被认为是这个类的超类。
  • 可以使用Object类型的变量引用任何类型的对象。但Object类型的变量只能用于作为各种值的通用持有者。
  • 在Java中,只有基本类型( primitive types)不是对象。
    • 所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。
5.2.1 equals方法
  • Object类中的equals方法用于检测一个对象是否等于另外一个对象。
    • 在Object类中,这个方法将判断两个对象是否具有相同的引用。
  • getClass()方法将返回一个对象所属的类。
    • 在检测中,只有在两个对象属于同一个类时,才有可能相等。
  • 在子类中定义equals方法时,首先调用超类的equals。
    • 如果检测失败,对象就不可能相等。
    • 如果超类中的域都相等,就需要比较子类中的实例域。
5.2.2 相等测试与继承
  • Java语言规范要求equals方法具有下面的特性:

    • 自反性
    • 对称性
    • 传递性
    • 一致性
    • 对于任何非空引用x,x.equals(null)应该返回false
  • 如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检测。

  • 如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同子类的对象之间进行相等的比较。

    在标准Java库中包含150多个equals方法的实现,包括使用instanceof检测、调用getClass检测、捕获ClassCastException或者什么也不做。

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

    • 显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量。
    • 检测this与otherObject是否引用同一个对象:
    if (this == other0bject) return true;
    

    这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个地比较类中的域所付出的代价小得多。

    • 检测otherObject是否为null,如果为null,返回false。这项检测是很必要的。
    if (other0bject = null) return false;
    
    • 比较this与otherObject是否属于同一个类。如果equals的语义在每个子类中有所改变,就使用getClass检测:
    if (getClass() != other0bject.getClass0) return false;
    

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

    if (! (other0bject instanceof ClassName)) return false;
    
    • 将otherObject转换为相应的类类型变量:
    ClassName other = (className) other0bject;
    
    • 现在开始对所有需要比较的域进行比较了。使用==比较基本类型域,使用equals比较对象域。如果所有的域都匹配,就返回true;否则返回false。
        return field1 = other. field1
        && 0bjects. equals(field2, other.field2)
        && ...;
    

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

  • 对于数组类型的域,可以使用静态的Arrays.equals方法检测相应的数组元素是否相等。如果两个数组长度相同,并且在对应的位置上数据元素也均相同,将返回true。

  • 为了避免发生类型错误,可以使用@Override对覆盖超类的方法进行标记:

@Override public boolean equals(0bject other)
5.2.3 hashCode方法
  • 散列码(hashcode)是由对象导出的一个整型值。

    • 由于hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址。
    • 字符串的散列码是由内容导出的
    • 字符串缓存却有着不同的散列码,这是因为在StringBuffer类中没有定义hashCode方法,它的散列码是由Object类默认hashCode方法导出的对象存储地址
  • 如果重新定义equals方法,就必须重新定义hashCode方法,以便用户可以将对象插入到散列表中(有关散列表的内容将在第9章中讨论)。

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

    • //例如,下面是Employee类的hashCode方法。
      public class Employee
      {
          public int hashCod(){
               return 7 * name.hashCode()
              +11 * new Double(salary).hashCode()
              +13* hireDay.hashCode();
          }
      }
      
    • 不过,还可以做得更好。首先,最好使用null安全的方法Objects.hashCode。如果其参数为null,这个方法会返回0,否则返回对参数调用hashCode的结果。

    • 另外,使用静态方法Double.hashCode来避免创建Double对象。

    • public int hashCode(){
          return 7 *0bjects.hashCode(name)
          +11* Doub1e.hashCode(salary)
          +13 * 0bjects.hashCode(hireDay);
      }
      
    • 还有更好的做法,需要组合多个散列值时,可以调用Objects.hash并提供多个参数。

      public int hashCode(){
      	return 0bjects.hash(name,salary,hireDay);
      }
      
  • Equals 与hashCode的定义必须一致:如果x.equals(y)返回true,那么x.hashCode( )就必须与y.hashCode()具有相同的值。

  • 如果存在数组类型的域,那么可以使用静态的Arrays.hashCode方法计算一个散列码,这个散列码由数组元素的散列码组成。

5.2.4 toString方法
  • 返回表示对象值的字符串。

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

    • 实际上,还可以设计得更好一些。最好通过调用getClass().getName( )获得类名的字符串,而不要将类名硬加到toString方法中。

    • public String toString(){
          return getC1ass().getName()
          +"[name=" +name
          + ",salary=" + salary
          + ",hireDay=" + hireDay
          +"]";
      }
      
    • toString方法也可以供子类调用

    • 如果超类使用了getClass( ).getName( ),那么子类只要调用super.toString( )就可以了。

  • 随处可见toString方法的主要原因是:只要对象与一个字符串通过操作符“+”连接起来,Java编译就会自动地调用toString方法,以便获得这个对象的字符串描述。

    • 在调用x.toString( )的地方可以用""+x 替代。

    • Object 类定义了toString方法,用来打印输出对象所属的类名和散列码

    • System.out.println(System.out);
      //将输出下列内容(PrintStream类的设计者没有覆盖toString方法)
      java.io.PrintStream@2f6684
      
  • 数组继承了object类的toString方法,数组类型将按照旧的格式打印

    • 生成字符串“[I@1a46e30”(前缀[I表明是一个整型数组)。
    • 修正的方式是调用静态方法Arrays.toString。代码:
    String s = Arrays.toString(luckyNumbers);
    //将生成字符串“[2,3,5,7,11,13]”。
    
    • 打印多维数组(即,数组的数组)则需要调用Arrays.deepToString方法。
  • 强烈建议为自定义的每一个类增加toString方法。这样做不仅自已受益,而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅。

  • java.lang.Class.getName()返回这个类的名字

  • java.lang.Class.getSuperClass()以Class对象的形式返回这个类的超类信息。

5.3 泛型数组列表(ArrayList)

  • ArrayList,它使用起来有点像数组,但在添加或删除元素时,具有自动调节数组容量的功能,而不需要为此编写任何代码。

  • ArrayList 是一个采用类型参数(type parameter)的泛型类(generic class)。为了指定数组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面.

    • ArrayList<Employee> staff = new ArrayList<>();这被称为“菱“形语法。

    • 尖括号中的类型参数不允许是基本类型

    • 在Java SE5.0以后的版本中,没有后缀<.. .>仍然可以使用ArrayList,它将被认为是一个删去了类型参数的“原始”类型。

    • 使用add方法可以将元素添加到数组列表中。

    • 数组列表管理着对象引用的一个内部数组。最终,数组的全部空间有可能被用尽。这就显现出数组列表的操作魅力:如果调用add且内部数组已经满了,数组列表就将自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。

    • 如果已经清楚或能够估计出数组可能存储的元素数量,就可以在填充数组之前调用ensureCapacity方法:

      staff. ensureCapacity(100);
      
    • 另外,还可以把初始容量传递给ArrayList 构造器:

      ArrayList<Employee> staff = new ArrayList<>(100);
      
  • 数组列表的容量与数组的大小有一个非常重要的区别。

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

  • 一旦能够确认数组列表的大小不再发生变化,就可以调用trimToSize方法

    • 这个方法将存储区域的大小调整为当前元素数量所需要的存储空间数目。垃圾回收器将回收多余的存储空间。
    • 一旦整理了数组列表的大小,添加新元素就需要花时间再次移动存储块, 所以应该在确认不会添加任何元素时,再调用trimToSize。
5.3.1 访问数组列表元素
  • 数组列表自动扩展容量的便利增加了访问元素语法的复杂程度。

    • 其原因是ArrayList类并不是Java程序设计语言的一部分。它只是一个由某些人编写且被放在标准库中的一个实用类。
    • 使用get和set方法实现访问或改变数组元素的操作。
    • 只有i小于或等于数组列表的大小时,才能够调用list.set(i,x)。
    • 使用add方法为数组添加新元素,而不要使用set方法,它只能替换数组中已经存在的元素内容。
  • 原始的ArrayList存在一定的危险性。它的add和set方法允许接受任意类型的对象(即返回Object对象,需要进行类型转换)。

  • 下面这个技巧可以一举两得,既可以灵活地扩展数组,又可以方便地访问数组元素

    • 首先,创建一个数组,并添加所有的元素。
    Arraylist<X> list = new ArrayList<>0;
    while (...)
    {
        x=.. .;
        list.add(x);
    }
    
    • 执行完上述操作后,使用toArray方法将数组元素拷贝到一个数组中。
    X[] a= new X[list.size()];
    list.toArray(a);
    
  • 除了在数组列表的尾部追加元素之外,还可以在数组列表的中间插入元素,使用带索引参数的add方法

    • 同样地,可以从数组列表中间删除一个元素。
    Employee e = staff. remove(n);
    
  • 如果数组存储的元素数比较多,又经常需要在中间位置插入、删除元素,就应该考虑使用链表

  • 可以使用”for each“循环遍历数组列表。

5.3.2 类型化与原始数组列表的兼容性
  • 鉴于兼容性的考虑,编译器在对类型转换进行检查之后,如果没有发现违反规则的现象,就将所有的类型化数组列表转换成原始ArrayList对象
    • 在程序运行时,所有的数组列表都是一样的,即没有虚拟机中的类型参数。因此,类型转换( ArrayList)和( ArrayList)将执行相同的运行时
      检查。
    • 在这种情形下,不必做什么。只要在与遗留的代码进行交叉操作时,研究一下编译器的警告性提示,并确保这些警告不会造成太严重的后果就行了。
      (一旦能确保不会造成严重的后果,可以用@Suppress Warnings( "unchecked")标注来标记这个变量能够接受类型转换,如下所示:
@suppressWarnings("unchecked") Arraylist<Employee> result =
(Arraylist<Employee>) employeeDB. find(query); // yields another warning

5.4 对象包装器与自动装箱

  • 包装器(wrapper):需要将int这样的基本类型转换为对象。

    • 所有的基本类型都有一个与之对应的类。
    • 这些对象包装器类拥有很明显的名字: Integer、Long、Float、Double、Short、Byte、Character 、Void和Boolean (前6个类派生于公共的超类Number)。
    • 对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是final,因此不能定义它们的子类
  • 由于每个值分别包装在对象中,所以ArrayList的效率远远低于int[]数组。

    • 因此,应该用它构造小型集合,其原因是此时程序员操作的方便性要比执行效率更加重要。
  • 自动装箱(autoboxing)

    • 由于包装器类引用可以为null,所以自动装箱有可能会抛出一个NullPointerException异常。

    • 另外,如果在一个条件表达式中混合使用Integer和Double类型,Integer值就会拆箱,提升为double,再装箱为Double。

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

    • 两个包装器对象比较时调用equals方法。

    • 自动装箱规范要求boolean、byte、char≤127,介于-128~127之间的short和int被包装到固定的对象中。

    • 可以将某些基本方法放置在包装器中。如Integer.parseInt()

  • Integer 对象是不可变的:包含在包装器中的内容不会改变。不能使用这些包装器类创建修改数值参数的方法。

    • 如果想编写一个修改数值参数值的方法,就需要使用在org.omg.CORBA包中定义的持有者( holder)类型,包括IntHolder、BooleanHolder等。每个持有者类型都包含一个公有(!)域值,通过它可以访问存储在其中的值。

5.5 参数数量可变的方法

  • printf方法是这样定义的:

    public class PrintStream
    public PrintStream printf(String fmt, 0bject... args)
    { return format(fmt, args); }
    
    • 这里的省略号...是Java代码的一部分,它表明这个方法可以接收任意数量的对象(除fmt参数之外)。
    • 实际上,printf方法接收两个参数,一个是格式字符串,另一个是Object[]数组,其中保存着所有的参数。
    • 换句话说,对于printf的实现者来说,Object... 参数类型与Object[ ]完全一样。
  • 用户自己也可以定义可变参数的方法,并将参数指定为任意类型,甚至是基本类型。

  • 允许将一个数组传递给可变参数方法的最后一个参数。(!!!)

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

5.6 枚举类

  • public enum Size { SMALL, MEDIUM, LARCE, EXTRA_ LARGE };

    • 这个声明定义的类型是一个类。它刚好有四个实例,在此尽量不要构造新对象。
    • 因此,在比较两个枚举类型的值时,永远不需要调用equals,而直接使用“==”就可以了。
    • 如果需要的话,可以在枚举类型中添加一些构造器、方法和域。当然,构造器只是在构造枚举常量的时候被调用。
  • 所有的枚举类型都是Enum类的子类。它们继承了这个类的许多方法。

    • toString()方法:返回枚举常量名。

      Size.SMALL.toString(); //返回字符串”SMALL“
      
    • 逆方法是静态方法valueOf()

      Size s = Enum.valueOf(Size.class, "SMALL"); //将s设置成Size.SMALL
      
    • 每个枚举类型都有一个静态的values方法,它将返回一个包含全部枚举值的数组。

    • ordinal方法返回enum声明中枚举常量的位置,位置从0开始计数。例如: Size. MEDIUM. ordinal()返回1。

    • compareTo(E other)
      如果枚举常量出现在other之前,则返回一个负值;如果this==other,则返回0;否则,返回正值。枚举常量的出现次序在enum声明中给出。

  • public class EnumTest
    {
    	public static void main(String[]args){
    		Scanner in = new Scanner(System.in);
    		System.out.print("Enter a size: (SMALL,MEDIUM,LARCE,				EXTRA_LARCE)");
    		String input = in.next().toUpperCase();
    		Size size = Enum.valueOf(Size.class,input);
    		System.out.print1n("size=" + size);
    		System.out.print1n("abbreviation=" + size.getAbbreviation());
    		if (size == Size.EXTRA_LARGE)
    		System.out.println("Good job--you paid attention to the _.");
    	}
    
    enum Size
    {
        SMALL(""S"),MEDIUM("M""),LARGE("L"),EXTRA_LARGE("XL");
        private Size(String abbreviation){ 
        	this.abbreviation = abbreviation; }
        public String getAbbreviation() { return abbreviation; }
        private String abbreviation;
    }
    

5.7 反射

5.8 继承的设计技巧

  • 将公共操作和域放在超类
  • 不要使用受保护的域
  • 使用继承实现“is-a”关系
  • 除非所有继承的方法都有意义,否则不要使用继承
  • 在覆盖方法时, 不要改变预期的行为
  • 使用多态,而非类型信息
  • 不要过多地使用反射
原文地址:https://www.cnblogs.com/nojacky/p/13906501.html