读书笔记

Java核心技术卷1基础知识(第一部分)

第四部分:继承

类、超类和子类

定义子类
下面是由继承 Employee 类来定义 Manager 类的格式 ,关键字 extends 表示继承:

public class Manager extends Employee{
    ...//添加方法和属性
}

关键字 extends 表明正在构造的新类派生于一个已存在的类,已存在的类称为超类、基类或父类,新类称为子类、派生类或孩子类,超类和子类是 Java程序员最常用的两个术语。

在Manager 类中没有显式地定义 getName 和 getHireDay 等方法,但属于Manager 类的对象却可以使用它们, 这是因为 Manager 类自动地继承了超类 Employee 中的这些方法。同样 ,从超类中还继承了name 、salary和 hireDay 这3个域。

在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处 。因此在设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中。

覆盖方法
超类中的有些方法对子类 Manager 并不一定适用,为此 , 需要提供一个新的方法来覆盖超类中的这个方法。

注意:每个Manager 对象都拥有一个名为 salary 的域,但在 Manager 类的getSalary 方法中并不能够直接地访问 salary 域,只有 Employee 类的方法才能够访问私有部分。 如果Manager 类的方法一定要访问私有域, 就必须借助于公有的接口,Employee 类中的公有方法getSalary 正是这样一个接口。

public double getSalary(){
    double BaseSalary = getSalary();
    return BaseSalary + bonus;
}

上面这段代码仍然不能运行。 问题出现在调用getSalary 的语句上,这是因为 Manager 类也有一个 getSalary 方法( 就是正在实现的这个方法),所以这条语句将会导致无限次地调用自己,如果我们希望调用超类 Employee 中的 getSalary 方法 , 而不是当前类的这个方法,为此 ,可以使用特定的关键字 super 解决这个问题 :

Public double getSalary(){
    double BaseSalary = super.getSalary();
    return BaseSalary + bonus;
}

注意: 有些人认为 super 与 this 引用是类似的概念, 实际上, 这样比较并不太恰当。 这是因为 super 不是一个对象的引用 , 不能将 super 赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。

子类构造器

public Manager ( String name , double salary , int year , int month , int day )
{
super (name , salary , year , month , day);
bonus = 0;
}

这里的关键字 super 具有不同的含义,是 “ 调用超类 Employee 中含有n、s、yearmonth 和 day 参数的构造器”的简写形式,Manager 类的构造器不能访问 Employee 类的私有域, 所以必须利用 Employee 类的构造器对这部分私有域进行初始化,我们可以通过super 实现对超类构造器的调用 。 使用super 调用构造器的语句必须是子类构造器的第一条语句

注意:回忆一下,关键字 this 有两个用途 :一是引用隐式参数,二是调用该类其他的构造器,同样, super关键字也有两个用途 : 一是调用超类的方法, 二是调用超类的构造器 。在调用构造器的时候 , 这两个关键字的使用方式很相似 。 调用构造器的语句只能作为另一个构造器的第一条语句出现 。 构造参数既可以传递给本类 ( this ) 的其他构造器 , 也可以传递给超类 ( super ) 的构造器。

继承层次
继承并不仅限于一个层次 。 例如 , 可以由 Manager 类派生Executive 类 。 由一个公共超类派生出来的所有类的集合被称为继承层次,在继承层次中 , 从某个特定的类到其祖先的路径被称为该类的继承链

注意: Java 不支持多继承,Java 中多继承功能的实现方式使用接口。

多态
有一个用来判断是否应该设计为继承关系的简单规则 , 这就是 “ is - a ” 规则, 它表明子类的每个对象也是超类的对象。

例如 , 可以将一个子类的对象赋给超类变量

Employee e;
e = new Employee();
e = new Manager();

在 Java 程序设计语言中, 对象变量是多态的 。 一个Employee 变量既可以引用一个Employee 类对象,也可以引用一个 Employee 类的任何一个子类的对象。

然而, 不能将一个超类的引用赋给子类变量。

理解方法调用

  1. 编译器査看对象的声明类型和方法名;
  2. 接下来,编译器将査看调用方法时提供的参数类型;
  3. 如果是 private 方法 、 static 方法 、 final 方法 或者构造器 , 那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定,与此对应的是, 调用的方法依赖于隐式参数的实际类型 , 并且在运行时实现动态绑定
  4. 当程序运行, 并且采用动态绑定调用方法时 ,虚拟机一定调用与 x 所引用对象的实际类型最合适的那个类的方法。

阻止继承:final 类和方法
不允许扩展的类被称为 final类,如果在定义类的时候使用了 final 修饰符就表明这个类是 final 类。

使用 final 修饰符声明,声明格式如下所示:

public final class Executive extends Manager{

}

类中的特定方法也可以被声明为 final 。 如果这样做, 子类就不能覆盖这个方法。

注意: 域也可以被声明为 final 。 对于 final 域来说 , 构造对象之后就不允许改变它们的值了 。 不过, 如果将一个类声明为final , 只有其中的方法自动地成为 final,而不包括域。

将方法或类声明为 final 主要目的是:确保它们不会在子类中改变语义, 例如 , Calendar类中的 getTime 和 setTime 方法都声明为 final,这表明 Calendar 类的设计者负责实现 Date 类
与日历状态之间的转换 , 而不允许子类处理这些问题。

如果一个方法没有被覆盖并且很短 , 编译器就能够对它进行优化处理,这个过程为称为内联, 例如 , 内联调用 e.getName () 将被替换为访问 e.name 域。

强制类型转换
将一个类型强制转换成另外一个类型的过程被称为强制类型转换

正像有时候需要将浮点型数值转换成整型数值一样,有时候也可能需要将某个类的对象引用转换成另外一个类的对象引用。 对象引用的转换语法与数值表达式的类型转换类似, 仅需要用一对圆括号将目标类名括起来, 并放置在需要转换的对象引用之前就可以了。

Manager m = (Manager)staff[0];

进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后 , 使用对象的全部功能,例如,在 managerTest 类中, 由于某些项是普通雇员 ,所以 staff 数组必须是 Employee 对象的数组。 我们需要将数组中引用经理的元素复原成 Manager 类 , 以便能够访问新增加的所有变量。

实际上,通过类型转换调整对象的类型并不是一种好的做法,大多数情况并不需要将 Employee 对象转换成 Manager 对象,只有在使用Manager 中特有的方法时才需要进行类型转换。

只要没有捕获ClassCastException 异常, 程序就会终止执行 在一般情况下 , 应该尽量少用类型转换和 instanceof 运算符。

抽象类
如果自下而上在类的继承层次结构中上移, 位于上层的类更具有通用性,甚至可能更加抽象。从某种角度看 ,祖先类更加通用, 人们只将它作为派生其他类的基类,而不作为想使用的特定的实例类。

为了提高程序的清晰度 , 包含一个或多个抽象方法的类本身必须被声明为抽象的

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

除了抽象方法之外, 抽象类还可以包含具体数据和具体方法,例如 , Person 类还保存着姓名和一个返回姓名的具体方法

public abstract class Person
{
    private String name;
    public Person (String name){
        this.name = name;
    }
    public abstract String getDescription();
    public String getName(){
        return name;
    }
}

注意:在抽象类中不能包含具体方法 。 建议尽量将通用的域和方法(不管是否是抽象的), 放在超类(不管是否是抽象类)中。

抽象方法充当着占位的角色,它们的具体实现在子类中,扩展抽象类可以有两种选择,一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;另一种是定义全部的抽象方法,这样一来 ,子类就不是抽象的了。

类即使不含抽象方法, 也可以将类声明为抽象类,抽象类不能被实例化,但可以创建一个具体子类的对象。

需要注意, 可以定义一个抽象类的对象变量 ,但是它只能引用非抽象子类的对象。

Person p = new Student ( "Vinee Vu", "Economics" );

这里的 p 是一个抽象类 Person 的变量, Person 引用了一个非抽象子类 Student的实例。

受保护访问
一般来说最好将类中的域标记为 private ,而方法标记为 public。 任何声明为 private的内容对其他类都是不可见的。

归纳一下 Java 用于控制可见性的 4 个访问修饰符:

  1. 仅对本类可见---private;
  2. 对所有类可见---public;
  3. 对本包和所有子类可见 protected;
  4. 对本包可见---默认,不需要修饰符。

Object类

Object 类是 Java 中所有类的始祖 , 在 Java 中每个类都是由它扩展而来的。

可以使用 Object 类型的变量引用任何类型的对象:

Object obj = new Employee("Harry Hacker",35000);

当然 , Object 类型的变量只能用于作为各种值的通用持有者 。 要想对其中的内容进行具体的操作 , 还需要清楚对象的原始类型 , 并进行相应的类型转换:

Employee e = (Employee)obj;

在 Java 中, 只有基本类型不是对象,所有的数组类型,不管是对象数组还是基本类型的数组都扩展了 Object 类。

equals 方法
Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这个方法将判断两个对象是否具有相同的引用, 如果两个对象具有相同的引用 , 它们一定是相等的。

public class Employee{
    public boolean equals(Object obj){
        if(this == obj){
            return true;
        }
        if(this == null){
            return false;
        }
        if(this.getClass()!=obj.getClass()){
            return false;
        }
        Employee other = (Employee)obj;

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

    }

}

相等测试与继承

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)返true,y.equals(z)返回true ,x.equals(z)也应该返回 true;
  4. 一致性 :如果 x 和 y 引用的对象没有发生变化,反复调用 x.equals(y) 应该返回同样的结果;
  5. 对于任意非空引用 x,x.equals(null) 应该返回 false。

可以从两个截然不同的情况看一下这个问题:

  • 如果子类能够拥有自己的相等概念 ,则对称性需求将强制采用 getClass 进行检测;
  • 如果由超类决定相等的概念,那么就可以使用inatanceof 进行检测 ,这样可以在不同子类的对象之间进行相等的比较。

hashCode 方法
散列码 ( hash code ) 是由对象导出的一个整型值。散列码是没有规律的,如果x 和 y 是两个不同的对象,x.hashCode()与 y.hashCode() 基本上不会相同。

String类计算散列码:

int hash = 0;
for(int i=0;i<length();i++){
    hash = 31*hash+charAt(i);
}

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

String s = 'ok';
StringBuilder sb = new StringBuilder(s);
System.out.println(s.hashcode()+" "+sb.hashcode());
String t = new String("ok");
StringBuilder tb = new StringBuiler(t);
System.out.println(t.hashcode()+" "+tb.hashcode());
对象 散列码
s 2556
sb 20526976
t 2556
tb 20527144

字符串 s 与 t 拥有相同的散列码, 这是因为字符串的散列码是由内容导出的, 而字符串缓冲 sb 与tb 却有着不同的散列码,这是因为在StringBuffer 类中没有定义hashCode 方法,它的散列码是由Object 类的默认 hashCode 方法导出的对象存储地址。

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

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

toString 方法

在 Object 中还有一个重要的方法 , 就是 toString 方法 , 它用于返回表示对象值的字符串。

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

public String toString(){
    return "Employee[name="+name+",salary="
        +salary+",hireday="+hireday+"]"
}

最好通过调用 getClaSS().getName() 获得类名的字符串,而不要将类名硬加到 toString 方法中:

publc String toString(){
    return getClass().getName()
    +"name="+name+",salary="+salary
    +",hireday="+hireday+"]";
}

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

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

提示:强烈建议为自定义的每一个类增加 toString 方法 。 这样做不仅自己受益 , 而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅。

  • Class getClass ( )
    返回包含对象信息的类对象 。 稍后会看到 Java 提供了类运行时的描述 , 它的内容被封装在 Class 类中。
  • boolean equals ( Object otherObject )
    比较两个对象是否相等 , 如果两个对象指向同一块存储区域 ,方法返回 true ; 否则方法返回 false。 在自定义的类中 ,应该覆盖这个方法。
  • String toString ( )
    返冋描述该对象值的字符串。在自定义的类中,应该覆盖这个方法。
  • String getName ( )
    返回这个类的名字。
  • Class getSuperclass ( )
    以 Class 对象的形式返回这个类的超类信息。

泛型数组列表

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

ArrayList 是一个采用类型参数的泛型类, 为了指定数组列表保存的元素对象类型, 需要用一对尖括号将类名括起来加在后面,例如 , ArrayList< Employee >。

下面声明和构造一个保存 Employee 对象的数组列表:

ArrayList<Employee> staff = new ArrayList<>();

这被称为 “ 菱形 ” 语法, 因为空尖括号 o 就像是一个菱形 。 可以结合new 操作符使用菱形,语法。 编译器会检查新值是什么 。如果赋值给一个变量 , 或传递到某个方法 , 或者从某个方法返回 , 编译器会检査这个变量 、 参数或方法的泛型类型 , 然后将这个类型放在 o 中 。 在这个例子中 , new ArrayListo ( ) 将赋至一个类型为 ArrayList < Employee > 的变量 , 所以泛型类型为 Employee。

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

staff.add(new Employee("zs"...));

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

访问数组列表元素
使用 get 和 set 方法实现访问或改变数组元素的操作。

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

ArrayList<X> list = new ArrayList<>();
while (...)
{
x =...;
list.add (x);
}

执行完上述操作后, 使用 toArray 方法将数组元素拷贝到一个数组中 。

X[] a = new X[list.size()];
list.toArray(a);

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

for ( Employee e : staff )
    System.out.println("姓名:"+e.getName());

将Employee[]数组替换成了 ArrayList < Employee >,其中变化:

  • 不必指出数组的大小;
  • 使用 add 将任意多的元素添加到数组中;
  • 使用 size () 替代 length 计算元素的数目;
  • 使用 a.get(i)替代a[i] 访问元素。

类型化与原始数组列表的兼容性
假设有下面这个遗留下来的类

public class EmployeeDB
{
    public void update (ArrayList list) { ... }
    public ArrayList find (String query) { ... }
}

可以将一个类型化的数组列表传递给 update 方法 , 而并不需要进行任何类型转换。

ArrayList <Employee> staff = . . .;
employeeDB.update(staff);

也可以将 staff 对象传递给 update 方法。

对象包装器和自动装箱

对象包装器类:Integer 、 Long 、 Float 、 Double 、 Short 、 Byte 、 Character 、 Void 和 Boolean。

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

假设想定义一个整型数组列表。而尖括号中的类型参数不允许是基本类型,也就是说不允许写成 ArrayList < int >。 这里就用到了 Integer 对象包装器类 。 我们可以声明一个Integer对象的数组列表。

ArrayList <Integer> list = new ArrayList<>( );

注意:由于每个值分别包装在对象中,所以 ArrayList < lnteger > 的效率远远低于 int[] 数组。因此,应该用它构造小型集合,其原因是此时程序员操作的方便性要比执行效率更加重要。

自动装箱:

list.add (3);

将自动地变换成

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

将一个 Integer 对象赋给一个 int 值时, 将会自动地拆箱

int i = list.get(i);

翻译为:

int i = list.get(i).ValueOf();

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

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

Integer n = 1;
double x = 2.0;
System.out.println(n*x);

参数数量可变的方法

在 Java SE 5.0 以前的版本中, 每个 Java 方法都有固定数量的参数,现在的版本提供了可以用可变的参数数量调用的方法。

可以定义可变参数的方法 , 并将参数指定为任意类型 , 甚至是基本类型,例如计算若干个数值的最大值:

public static double max(double...values){
    double largest = Double.NEGATIVE.INFINFITY;
    for(double v:values) if(v>largest) largest = v;
    return largest;
}

double m = max(3.1,40.4,-5);

编译器将 new double [ ] { 3.1 , 40.4 , - 5 } 传递给 max 方法。

枚举类

常见枚举类:

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

可以在枚举类型中添加一些构造器 、 方法和域:

public enum Size{
    SMALL("S"),MEDIUM("M"),LARGE("L"),EXTRA_LARGE("XL");

    private String abbreviation;

    private Size(String abbreviation) {this.abbreviation=abbreviation;}
    public String getAbbreviation(){return abbreviation;}
}

所有的枚举类型都是 Enum 类的子类,它们继承了这个类的许多方法。其中最有用的一个是 toString, 这个方法能够返回枚举常量名,例如,Size.SMALL.toString()将返回字符串“SMALL”。

每个枚举类型都有一个静态的 values 方法 , 它将返回一个包含全部枚举值的数组,例如:

Size[] values = Size.values();

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

页码: 1 2 3 4

Java核心技术卷1基础知识(第一部分)已关闭评论