Core Java 笔记(四)- 继承

继承

 

先明确概念:

  • 继承已存在的类就是在复用这些类的方法和域

  • 超类和子类的关系,就是一般和特殊的关系(sub ⊆ super)

 

一、超类和子类

 

在设计超类和子类时的基本思想是:将通用的方法放在超类中、具有特殊用途的方法放在子类中。经常会遇到超类的方法不适用于子类的情况,这就需要提供一个新的方法来覆盖(override),或者叫重写。

 

覆盖

所谓覆盖就是对超类同名、同参数列表的方法的修改,以体现子类的“特殊性”。

在覆盖超类方法的时候,经常需要访问超类的私有域,但它们是不对子类开放的。解决办法是调用超类的访问器方法,需要一个新的关键字 super :

public class Manager extends Employee {
    private double bonus;
    // ...
    @Override
    public double getSalary() {
        return bonus + super.getSalary();
    }
    // ...
}

super 不是所谓的隐式参数,也不是一个对象的引用,只是一个指示编译器调用超类方法的特殊关键字。

  • 返回类型:允许定义为原返回类型的子类型,也即支持可协变的返回类型。

  • 访问修饰符:子类方法不能低于超类方法的可见性(不能更严格)。

标注 @Override 会告诉编译器下面将要定义的方法是对超类某个方法的覆盖,可以作为检查方法签名是否写对的辅助手段。

 

子类构造器

关键字 super 还有一个用途:调用超类构造器。由于子类的域只是对超类已有的域的补充,在对子类进行初始化时不能遗漏继承得来的域,即便它们是不可见的,所以,大多数时候都要在第一行调用超类的构造器。倘若超类没有不带参数的构造器,并且子类又没有显式地调用超类的其他构造器,将无法通过编译。

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

 

多态

这里有一个 Employee 类型的变量,假定是一个叫小明的雇员,其他的信息暂时不知道:

Employee unknownEmployee;

查阅公司员工名单,发现小明其实是一个经理,于是让这个变量引用一个 Manager 对象:

unknownEmployee = new Manager("Xiao Ming", 6000, 1990, 3, 8, 2500);

到这里,可以明确一点:可以将子类的引用赋给超类的变量。接着,对小明调用 getSalary,会发现 Manager 类覆盖过的方法成功运行。

System.out.println(unknownEmployee.getName() + ": " + unknownEmployee.getSalary()); // Xiao Ming: 8500  

多态指的就是这样一种现象:一个对象引用可以指示多种实际类型。虚拟机能够判断实际引用的对象类型,从而调用相应的方法。这种在运行时能够自动选择调用哪个方法的现象称为动态绑定。与之相对的就是静态绑定,具体体现为 private 方法、static 方法、final 方法和构造器,编译器可以准确知道应该调用哪一个。

在动态绑定的情形下,每次调用方法都要进行搜索,时间开销大,所以虚拟机会预先为每个类创建一个方法表,列出所有方法的签名和实际调用的方法,方便查找。

 

里氏置换法则(LSP)

之前提到过“is-a”规则,它表明子类的每个对象也是超类的对象,换一种说法就是:程序中所有引用基类的地方必须能够透明地使用其子类对象。

但是反过来就不行了,不能将一个超类的引用赋给子类变量,不然,对子类变量调用子类方法时就有可能发生运行时错误

 

final
  • 被 final 修饰的类不允许被继承

  • 被 final 修饰的方法不允许被重写

 

强制类型转换
  • 只能在继承层次内进行类型转换

  • 在将超类转换成子类之前,应该使用 instanceof 操作符进行检查

Manager boss;
if (unknownEmployee instanceof Manager) {
    boss = (Manager) unknownEmployee;
    // ...
}

 

抽象类

在类的继承层次中,位于上层的类更具有通用性,甚至可能更加抽象。有时我们可能只想把一个类作为派生其他类的基类,而不想也不需要实例化,这样的类就可以用 abstract 关键字声明为抽象类,只能定义对象变量而不能创建对象实例

用 abstract 修饰的方法是抽象方法,不需要为它编写实现的代码,只是充当一个占位的角色,在非抽象子类中得到具体实现。

拥有至少一个抽象方法的类必须声明为抽象类,不过并不要求所有的方法都必须是抽象的,它依然可以包含具体的数据和方法。

 

访问修饰符
  • private :仅对本类可见

  • public :对所有类可见

  • protected : 对本包和所有子类可见

  • 无修饰符(默认):对本包可见

 

二、Object 

 

Java 中每个类都是 Object 的子类,可以使用 Object 类型的变量引用任何对象,作为通用持有者。Object 类中定义的方法是所有类的通用方法。

 

equals

在 Object 类中,这个方法将判断两个对象是否具有相同的引用,即所指是否一个地址。如果两个对象具有相同的引用,那么一定是相等的,不过反过来就不成立。对于多数类来说,这种默认的判断方式没有意义,我们需要的是检测两个对象状态的相等性,所以经常需要覆写 equals 方法。

覆写的原则有以下几点:

  • 自反性:对于任何非空引用 x ,x.equals(x) 应该为 true

  • 对称性:对于任何引用 x 和 y ,x.equals(y) 为 true 的充要条件是 y.equals(x) 也为 true

  • 传递性:对于任何引用 x、y 和 z,如果 x.equals(y) 为 true ,y.equals(z) 为 true,那么 x.equals(z) 也应该为 true

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

  • 对于任何非空引用 x ,x.equals(null) 应该为 false

一个完美的 equals 方法的建议:

public boolean equals(Object otherObject) {
    if (this == otherObject)  return true;
    if (otherObject == null)  return false;
    if (getClass() != otherObjects.getClass())  return false;
    if (!(otherObject instanceof ClassName))  return false;
    ClassName other = (ClassName) otherObject;
    return field1 == other.field1
        && Objects.equals(field2, other.field2)
        && ...;
}

在子类中定义 equals 方法时,可以先调用超类的 equals 。

如果调用 equals 的变量可能为 null ,需要使用 Object.equals(a, b) ,作用是:当其中至少有一个参数为 null 时就返回 false ,否则会调用 a.equals(b) 。  

 

hashCode

散列码(哈希码)是由对象导出的一个整型值,代表了对象的特征。对应的算法应该做到合理组合实例域的散列码,让各个不同的对象产生的散列码的分布更加均匀。

Object 类的 hashCode 方法保证了同一对象的引用能得到相同的散列码,这种机制与默认的 equal 方法是一样的,如果重新定义 equal 方法,就必须重新定义 hashCode 方法,目的是方便在使用时将对象插入到散列表中。String 类的 hashCode 源码如下:

public int hashCode() {
    int h = hash;   // 空串的 hash 为 0
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
} 

需要组合多个散列值时,可以调用 Objects.hash 方法(Objects 用于提供静态方法操作对象)并提供多个参数,它会对各个参数调用 Objects.hashCode,并组合这些散列值。例如:

public int hashCode() {
    return Objects.hash(field1, field2, field3);
}

如果存在数组类型的域,则可以使用 Arrays.hashCode,返回一个由各数组元素的散列码组成的结果。

 

toString

只要对象与一个字符串通过操作符 “+” 连接起来,编译时就会自动调用 toString 方法,另外,System.out.println 也会直接调用参数的 toString 。

toString 方法还可以作为一种调试工具。

 

三、ArrayList

 

这是一个采用类型参数的泛型类,可以把它的对象理解为一个能自动调节容量的数组。

ArrayList<Employee> staff = new ArrayList<>();
// 菱形语法,原先右边是: new ArrayList<Employee>();

ArrayList 对象管理着一个内部数组,使用 add 方法可以往数组中添加新的元素,如果调用 add 且内部数组已满,就将自动创建一个更大的数组,并将所有的对象拷贝到新的数组中去。扩容容量的计算是:

int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩大到原来的 1.5 倍  

如果能够估计出数组可能存储的元素数量,可以把初始容量传递给 ArrayList 构造器,或者调用 ensureCapacity 方法。

ArrayList<Employee> staff = new ArrayList<>(100);
// staff.ensureCapacity(100);

size 方法将返回实际元素数目,等价于数组的 length 。一旦能够确认 ArrayList 的大小不再发生变化,就可以调用 trimToSize 方法,将存储区域的大小调整为当前元素所刚好需要的,便于 GC 回收多余的存储空间。

get 和 set 方法用于访问或改变数组元素,例如要设置第 i 个元素(确认存在):

staff.set(i, vivian);

可以使用 for each 语法循环遍历 ArrayList 对象:

for (Employee e : staff)
    // e.跳槽

将一个原始 ArrayList 引用赋给一个类型化 ArrayList 变量会得到一个警告。

 

API - java.util.ArrayList<E>
  • add

  • set

  • get

  • remove
  • size

  • toArray

  • ensureCapacity

  • trimToSize

 

四、包装器

 

  • int / Integer

  • long / Long

  • float / Float

  • double / Double
  • short / Short

  • byte / Byte

  • char / Character

  • boolean / Boolean

  • Void

前 6 个类派生于 Number 类。包装器类的实例是不可变的

包装类的好处之一在于:为基本类型应用于泛型提供了转换渠道。

ArrayList<Integer> list = new ArrayList<>();
 
自动装箱 / 拆箱

自动装箱的特性便于在集合中添加基本类型:

list.add(8); // 自动变换成:list.add(Integer.valueOf(8));   

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

int n = list.get(i); // 自动变换成:int n = list.get(i).intValue();

可以将自增操作符用于一个 Integer 引用,编译器将自动地插入一条对象拆箱的指令,进行自增计算后,再将结果装箱:

Integer n = 3;
n++;
/*
Integer n = 3;
int m = n.intValue();
m++;
n = Integer.valueOf(m);
*/

自动装箱规范要求 boolean、byte、char <= 127,介于 -128 ~ 127 之间的 short 和 int 会被包装到固定的对象中。举个例子:

Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println((i1 == i2) + ", " + (i3 == i4));  // true, false

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

Integer i = 1;
Double d = 2.0;
System.out.println(true ? i : d);  // 1.0

装箱和拆箱是由编译器负责的,在生成字节码时插入必要的方法调用,再由虚拟机执行这些字节码。

 

将字符串转换为整型
String s = "100";
int x = Integer.parseInt(s);

这与 Integer 对象没有任何关系,但 Java 设计者认为 Integer 类是放置这个 parseInt 静态方法的好地方。

 

API - java.lang.Integer
  • intValue

  • toString

  • parseInt

  • valueOf
  • compare

 

五、变参方法

 

先看一下 printf 方法的定义:

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

Object 后面的 ... 表明这个方法除了参数 fmt 外,可以接收任意数量的对象,这些对象组成了一个 Object[] 数组,参数 args 是这个数组的引用。编译器会对 printf 的每次调用进行转换,将参数绑定到数组上,在必要的时候会自动装箱:

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

定义一个自己的可变参数方法:

public static int max(int... values) {
    int largest = Integer.MIN_VALUE;
    for (int v : values) {
        if (v > largest) {
            largest = v;
        }
    }
    return largest;
}

 

六、枚举类

 

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

以下声明定义了一个枚举类,有 4 个实例。

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

枚举类型的 toString 方法能够返回枚举常量名,静态方法 valueOf 是 toString 的逆方法。

String str = Size.SMALL.toString();  // "SMALL" 
Size s = Enum.valueOf(Size.class, str);  // Size.SMALL 

还有静态方法 values ,可以返回一个包含全部枚举值的数组:

Size[] values = Size.values();
// [Size.SMALL, Size.MEDIUM, Size.LARGE, Size.EXTRA_LARGE] 

ordinal 方法返回枚举常量的次序,从 0 开始计数。

int ord = Size.LARGE.ordinal();  // ord = 2 

 

API - java.lang.Enum<E>
  • valueOf

  • toString

  • values

  • ordinal
  • compareTo

 

七、设计技巧

 

  • 将公共的操作和域放在超类

  • 不要使用 protected 域

  • 使用继承之前分析一下是否为“is-a”关系

  • 除非所有继承的方法都有意义,否则不要继承
  • 覆盖方法时不要改变预期的行为

  • 需要类型检测时考虑多态

  • 不要过多使用反射(下一篇会讲)

 

转载于:https://www.cnblogs.com/zzzt20/p/11442867.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值