《Java核心技术》Chap5笔记

继承

类、超类、子类

通过extends关键字来实现类的继承

class Manager extends Employee
{
    ...//Add some new methods and new fields
}

此时,ManagerEmployee子类EmployeeManager超类
Manager会继承Employee的所有方法和域

重写类方法、访问超类的私有域

重新定义一个方法,如果与超类的某个方法重名,那么该方法会覆盖掉超类的方法
但是,子类并不能访问超类的私有域
解决方法:
通过超类的方法来访问超类的私有域,
如果子类重写了超类的方法,那么可以通过关键字super来访问超类的方法
如:

class Manager extends Employee
{
    public double getSalary()   //超类中也有一个getSalary方法
    {
        double baseSalary = super.getSalary();  //通过super关键字来访问超类的方法
        return baseSalary + bonus;
    }
}

C++中,访问超类是用::操作符的形式

子类的构造器

子类的构造器第一行必须调用超类的构造器对对象进行构造
如:

public Manager(String n, double s, int year, int month, int day)
{
    super(n, s, year, month, day);   //借助super关键字
    bonus = 0;  //而外的域初始化
}

如果没有显式地调用超类的构造器,那么将自动调用超类默认的(没有参数)的构造器;
如果超类没有默认的构造器,也没有显示地调用超类的构造器,那么java编译器将会报错

在C++的构造函数中,使用初始化列表语法来调用超类的构造函数

对象的引用变量

如果将对象的引用变量声明为某个类的引用变量,那么它除了可以引用这个类的对象之外,还可以引用它的子类的对象
如:
已知ManagerEmployee的子类,
Employee e;既可以引用Employee的对象,也可以引用Manager类的对象

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

继承层次

由一个类派生出来的所有类的集合称为继承层次(inheritance hierarchy)
从某个特定的类到其祖先的路径称为继承链(inheritance chain)

Java不支持多继承

多态

如前所述,某个类的引用变量可以引用该类的子类的实例,称对象变量是多态的
但是,不能通过这个引用变量访问子类的方法和域
如:

Manager boss = new Manager(...);
Employee e = boss;    //Manager是Employee的子类
boss.setBonus(5000);  //setBonus()是Manager类特有的方法,Employee没有该方法    
e.setBonus(5000);     //ERROR!!!! Employee没有这个方法,不能调用;此时可以利用强制类型转换解决  
e.getBonus();         //OK,Employee有这个方法,但是Manager重写了这个方法,由于动态绑定,此处会自动调用Manager的方法   

注意:不能将一个超类的引用赋给子类变量(如:Manager boss = e;

动态绑定、对象方法调用的过程

  1. 编译器查看对象的声明类型和方法名
    编译器会一一列出对象所在类的所有该方法名(包括超类的public方法)的方法(此处还没有检查参数类型)
  2. 编译器查看调用方法时提供的参数类型
    编译器会挑出类型匹配的所有方法(不一定要完全匹配,兼容即可)
    • 方法名和参数类型作为一个方法的签名(唯一证明),而返回类型不是
      因此覆盖方法时应保证返回类型是兼容的
  3. 如果是privatestaticfinal方法
    编译器则可以准确地知道调用哪一个方法,这种调用方式称为静态绑定
  4. 程序运行时采用动态绑定调用方法,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法

如果每次调用方法都进行一次搜索,那么时间开销太大了
因此,虚拟机会预先为每个类创建一个方法表(method table),调用方法时虚拟机直接在该表中查找对应的方法

  • 动态绑定的特性:无需对现存代码进行修改,就可以对程序进行扩展。
    假如为一个类创建子类,那么子类的实例无需对该类进行重新编译即可调用该类的方法

注意:覆盖一个方法时,子类方法的可见性不得低于超类方法的可见性;特别是当超类方法是public时,子类方法必须是public

阻止继承:final类和方法

  • final类:不允许扩展(即不允许利用该类定义子类)的类
    类中的方法会自动设置为final,而域不会
  • final方法:不允许覆盖(重写)的方法
  • 如果类中某个方法很简短、被频繁调用且没有真正进行覆盖
    那么即时编译器会将其进行内联处理以提高程序效率
    如果虚拟机又加载了一个子类,子类对这个方法进行了覆盖,那么编译器会自动取消内联

强制类型转换

  • 与内置类型的强制转换形式一致
  • 只能在继承称此内进行类型转换
  • 子类转换成超类是允许的
    正如超类的引用变量能直接引用子类
  • 超类转换成子类不一定允许
    如果出错,会引发一个异常,如果没有被捕获,那么出现编译错误
    通常需要借助instranceof运算符来判断该对象是否能成为目标子类的实例
    如:(ManagerEmployee的子类)

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

注意:尽可能避免强制类型转换和instanceof的使用
在C++中,如果强制转换失败将得到的是null对象,而不会产生异常或是出现编译错误

抽象类

abstract修饰符定义抽象类和抽象方法

  • 抽象方法相当于“占位”的作用,表明这个类存在这个方法,但类的定义中不给出该方法的具体定义,而在其子类中进行具体的定义
  • 包含抽象方法的类必须定义为抽象类,不包含抽象方法的类也可以定义为抽象类
    • 抽象类不允许直接产生实例,但其具体子类可以
    • 可以定义一个抽象类的引用变量,用它来引用其子类的实例

如:

abstract class Person   //包含抽象方法,必须定义为抽象类
{
    private String name;
    public Person(String n)
    {
        name = n;
    }
    public abstract String getDescription();  //抽象方法
    public String getName()
    {
        return name;
    }
}

C++中,在方法定义的尾部加上=0来表示一个纯虚函数(抽象方法);包含抽象方法的类自动作为抽象类

抽象类的作用:
假设StudentEmployeePerson的子类
StudentEmloyee中都定义了getDescription方法
变量Student s = new StudentEmployee e = new Employee都可以调用这个方法
但是Person p = sPerson p = e则不行(因为Person类不具有这个方法)
如果给Person类添加一个getDescription抽象方法,那么变量p就能调用这个方法(即时编译器会自动确定调用哪一个类的方法)

受保护访问

使用protected的实例域对所有子类及同一个包的所有其他类都可见
该实例域可以被子类的方法访问
假设p域为A类的protected域,B类和C类为A类的子类
但B类中的方法只能访问B对象的p域,而不能访问C类的p域
注意:该修饰符应当谨慎使用;一定程度上破坏了oop的数据封装原则
C++中,保护域仅对子类可见,这一点上Java的protected安全性比较低

控制可见性的访问修饰符归纳

  • private:仅对本类可见(常用于实例域)
  • public:对所有类可见(常用于方法)
  • protected:对本包和所有子类可见(常用于特殊的实例域)
  • 默认:本包可见(尽量避免)

Object:所有类的超类

每个类均由Object类扩展而来,但无需显式的书写extends Object
可以创建一个Object类的引用变量来引用任何类型的对象
所有类都拥有一些通用方法,这些方法定义在Object类中
C++中没有所有类的根类,但每个指针都可以转换成`void `指针*

equals方法

该方法不是标准库提供的方法,需要自己编写
检查两个对象是否相等(状态相同)
形式:objectA.equals(objectB),返回布尔值

一般实现机制:

  1. 检查两个对象是否为同一个引用
  2. 检查参数里的对象是否为null
  3. 检查两个对象是否为同一个类
  4. 检查两个对象的实例域是否完全相等

如果在子类中定义equals方法,那么它必须显调用超类的equals方法

class Manager extends Employee
{
    ...
    public boolean equals(Object otherObject)
    {
        //检查是否属于同一个类     
        if( !super.equals(otherObject) ) 
            return false;   
        Manager other = (Manager) otherObject;
        //检查实例域是否完全相等
        return bonus == other.bonus;
    }
}

相等测试与继承

  • Java语言规范要求equals方法具有以下特性
    • 自反性:x.equals(x)应该返回true
    • 对称性:若x.equals(y)为true,则y.equals(x)也应该为true
    • 传递性:若x.equals(y)y.equals(z)为true,则x.equals(z)也应该为true
    • 一致性:若x和y引用的对象不变,那么反复调用x.equals(y)应该返回相同的结果
    • 对于任意非空引用x,x.equals(null)应该返回false
  • 编写equals方法的建议
    • 显式参数命名为otherObject,类型为Object(否则无法覆盖Object.equals()),之后将其转换为一个叫做other的变量
    • 检测thisotherObject是否引用同一个对象
      if(this == otherObject) return true;
    • 检测otherObject是否为null
      if(otherObject == null) return false;
    • 检测thisotherObject是否属于同一个类
      • 如果equals在每个子类中语义不同:
        if(getClass() != otherObject.getClass()) return false;
      • 如果语义统一
        if(!(otherObject instanceof ClassName)) return false;
    • otherObject转换成相应的类的类型变量
      ClassName other = (ClassName) otherObject;
    • ==比较所需的基本类型域,用equals比较所需的对象域
      如果都匹配,返回true,否则返回false
    • 如果在子类中重定义equals,则必须调用suer.equals(other)
  • 对于数组类型的域,可以使用静态的Arrays.equals方法检测两个数组是否相等
    甚至可以用于对象数组,它将自动调用对应类的equals方法
  • 可以使用@Override对覆盖超类的方法进行标记
    当尝试定义一个新的方法(即不小心出现错误)的时候,编译器会报错
    如:@Override public boolean equals(Object otherObject)

hashCode方法

散列码是由对象导出的一个整型值,是没有规律的
一般两个不同的对象的散列码不同

  • Object.hashCode()导出的是对象的存储地址
  • 自定义类可以自己重写hashCode方法
  • String.hashCode()的散列码是由字符串内容计算得到
  • hashCode()equals()的结果应统一
    即当x.equals(y)返回true时,x.hashCode()y.hashCode()的结果应该一致
  • Objects.hashCode(Object a)(注意是Objects不是Object)
    当a为null时返回0,否则返回a.hashCode()
  • 散列码的计算应合理组合实例域的散列码,以便能够均匀地产生散列码
    也可以直接借助Objects.hash来生成散列码
    如利用实例域xyz组合生成散列码:

    public int hashCode()
    {
        return Objects.hash(x, y, z);
    }
    

toString方法

用于返回表示对象值的字符串
Point.toString()返回java.awt.Point[x=10,y=20]
通常toString方法都以类名+域值的形式返回

  • 标准库中大多数类都提供了toString方法
  • 自定义类最好也能提供这个方法,以便后期调试
  • 当一个对象与字符串相加时,系统会自动调用toString方法
    "abcd" + x相当于"abcd" + x.toString()
  • 当用System.out.println(x)打印对象x时,也会自动调用toString方法
    System.out.println(x.toString())
  • Object.toString()返回对象的类名与散列码
    System.out.println(System.out)返回java.io.PrintStream@2f6684
  • 但是,数组直接继承了object类的toString方法
    也就是说
    int[] luckNumbers = {2, 3, 5};
    String s = "" + luckNumbers;
    得到的s将会是I@1a46e30(I表示整型数组)
    • 此时我们可以调用静态方法Arrays.toString()来打印
      String s = "" + Arrays.toString(luckNumbers);
    • 如果要打印多维数组,那么应调用Arrays.deepToString方法
  • 一般的toString方法的定义方式

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

泛型数组列表

Java允许定义数组时用变量来确定数组大小
ArrayList类又提供了一个动态数组,称为数组列表
ArrayList类是一个采用类型参数(type parameter)的泛型类(generic class)

  • 数组列表管理着对象引用的一个内部数组
  • 当内部数组占满后如果再往其中添加内容,数组列表会自动创建一个更大的数组,并把数据拷贝到新的数组上

    //创建数组列表时需要指明对象的类
    ArrayList<Employee> staff = new ArrayList<Employee>();    
    //等号右边可以不指明对象类型
    ArrayList<Employee> staff = new ArrayList<>();  
    //可以对数组列表调用“add”方法添加对象
    staff.add(new Employee(...));
    //如果能提前估计对象的熟练,可以在填充数组前,调用“ensureCapacity”方法指定内部数组的大小,当超过这个大小时会自动创建一个更大的数组(这是与普通数组的区别)  
    staff.ensureCapacity(100);   
    //也可以在初始化时直接指定内部数组的大小   
    ArrayList<Employee> staff = new ArrayList<>(100);
    //调用“size”方法返回内部数组的实际大小  
    //与普通数组的“length”方法类似 
    staff.size();
    //当确认数组大小不再发生变化时,可以调用“trimToSize”回收多余空间
    //如果大小固定后再次添加元素,那么将会移动存储块
    staff.trimToSize();
    

在老版本的Java中,程序员采用Vector类实现动态数组,但ArrayList类更加有效
在C++中,利用vector模板来实现动态数组。C++中重载了[]运算符,Java中必须显式调用;C++中向量是值传递的,即a=b会拷贝一个新的给a,Java中ArrayList类是引用的传递,即a和b引用同一个数组列表

访问数组列表元素

staff.set(i, harry);          //类似于数组的staff[i] = harry;
Employee e = staff.get(i);    //类似于数组的e = staff[i];
staff.add(i, e);              //把e插入到i位置,i~(size()-1)的元素后移
Employee e = staff.remove(i); //移除i位置的元素并返回,类似python的pop
staff.toArray(a);             //把数组列表staff的内容填入普通数组a    
for(Employee e : staff)       //可以利用for each遍历数组列表
{
    ...
}

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

P186~187,此处并没有看懂!

对象包装器与自动装箱

有时候需要将基本类型转换成对象使用
这些类称为包装器(wrapper)

  • 常见对象包装器类
    • Interger
    • Long
    • Float
    • Double
    • Short
    • Byte
    • …以上六个对象包装器类又派生于Number类
    • Charactrer
    • Void
    • Boolean
  • 对象包装器类是不可变的
  • 对象包装器类是不可扩展的(final类)
  • 常用于构造数组列表
    数组列表是不支持基本类型数据的
    此时可以借助对象包装器类实现
    ArrayList<Interger> list = new ArrayList<>();
  • 为数组列表添加修改元素时会自动装箱(autoboxing)
    如:list.add(3)会自动替换成list.add(Interger.valueOf(3))
  • 表达式中绝大多数的对象包装器能够自动装箱和拆箱
    如:
    int n = list.get(i);等价于int n = list.get(i).intValue();
    Interger n = 3;n++等也会自动装箱和拆箱
  • 但是对于==运算符,对象包装器不会自动拆箱
    如:a、b为某对象包装器的实例
    if(a==b) ...判断的是a和b是否为同一个引用
    if(a.equals(b)) ...才能实现两个值的比较
  • 对象包装器类不能实现方法的传递的参数可变
    因为对象包装器类是不可变的
    可以借助org.omg.CORBA包中定义的持有者类型(holder)
    如:IntHolder、BooleanHolder等
    持有者类型都包含一个公有值域value!!!可以直接访问这个域

常见方法——

//java.lang.Interger,其他对象包装类的方法类似
int intValue()      //以int的形式返回对象的值   
static String toString(int i)   //以String对象的形式返回i的十进制表示    
static String toString(int i, int radix)   //同上,但以radix进制表示 
static int parseInt(String s)   //以十进制将s转换为整型   
static int parseInt(String s, int radix)  //同上,以radix进制
static Interger valueOf(String s)  //以十进制将s转换成Interger类的实例   
static Interger valueOf(String s, int radix)    //同上,以radix进制    
//java.text.NumberFormat    
Number parse(String s)     //将s转换成数值并返回    

参数数量可变的方法

在参数类型之后加上三个句点.即表示可接受任意数量的该类型参数
printf方法的定义
public PrintStream printf(String fmt, Obejct... args){}
剩下的这些参数会依次放入args数组中

枚举类

  • enum关键字定义的枚举类型实际上是由枚举类实现的
  • 构造器值在构造枚举常量时被调用
  • 所有的枚举类型都是Enum类的子类
  • 在枚举类型中可以添加一些构造器、方法、域
    例如:

    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;
        }
    }
    

常用方法——

//java.lang.Enum<E>
static Enum valueOf(Class enumClass, String name)  //返回指定类、指定名字的枚举常量
static toString()   //枚举常量名
int ordinal()    //返回枚举常量在enum声明中的位置(从0开始)
int compareTo(E other)    //如果枚举常量在other前,返回负值;如果this==other,返回0;如果在other后,返回正值

反射

能够分析类能力的程序称为反射(reflective)

  • 反射机制用来——
    • 在运行中分析类的能力
    • 在运行中查看对象
    • 实现通用的数组操作代码
    • 利用Method对象(类似C++中的函数指针)

反射机制通常用于工具构造,而不是应用程序设计

此节暂时跳过
P192-212

继承设计的技巧

  1. 将公共操作和域放在超类
  2. 不要使用受保护域
    protected机制并不能很好的保护域
    第一,子类集合是无限制的,任何人都能派生一个子类并编写代码来直接访问受保护域,破坏了封装性;
    第二,Java中,同一个包中所有类都可以访问受保护域
    受保护域一般用于指示那些不提供一般用途,而应在子类中重新定义的方法
  3. 只使用继承来实现”is a“关系
    不滥用继承
  4. 除非所有继承的方法都有意义,否则不要使用继承
  5. 在覆盖方法时,不要改变预期的行为
    即不要偏离最初设计的想法
  6. 使用多态,而非类型信息
    if(x...)...else if(x...)...(用来判断x的类型,来执行相应的动作)应放在同一个方法里
  7. 不要过多地使用反射
    反射在编译时很难发现错误