ZB-012-java组合和继承

什么是继承

  • 程序员的宿命:复制 & 粘贴
    • 真的只能这样吗?
    • DRY(Donot Repeat Youself)
    • 事不过三,三则重构
  • 继承的本质是提炼代码,避免重复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Cat {
private String name;
public void setName(String name) {
this.name = name;
}
public void walk(){}
}

public class Dog {
private String name;
public void setName(String name) {
this.name = name;
}
public void walk(){}
}

public class Pig {
private String name;
public void setName(String name) {
this.name = name;
}
public void walk(){}
}

// 它们又很多共性和重复
// 一旦walk改变了,要改三个地方

继承,避免重复

1
2
3
4
5
6
7
8
public class Animal{
private String name;
public void walk(){}
}

public class Cat extends Amimal{...}
public class Dog extends Amimal{...}
public class Pig extends Amimal{...}

Java的继承体系

  • 单继承
  • Object 是所有类的基类,所有对象都继承了Object,所以所有对象都有 Object 的非私有成员属性/方法

    • equals

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      Object 的 equals 默认实现是 
      public boolean equals(Object obj){
      return this == obj; // 比较的是 addr
      }

      针对其他类 如 Order 根据 id就可以确定是不是同一订单
      public class Order {
      private int id;
      private String name;

      @Override
      public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      Cat cat = (Cat) o;
      return id == cat.id;
      }

      @Override
      public int hashCode() {
      return Objects.hash(id);
      }
      }
    • toString

      1
      2
      3
      4
      5
      6
      7
      8
      Object 的 toString 默认实现是 
      public boolean toString(){
      return getClass().getName() + "@" + Integer.toHexString(hashCode()) ;
      }

      任何时候打印对象的时候 都会提供字符串表示
      System.out.println(xxx); // 隐式调用
      xxx.toString();

单根继承的好处是:

  • 所有对象都有某一行为,因为都继承了 Object
  • 方便处理

如 python支持多继承。会带来很多问题——菱形继承

  • 父类都有 xxx()函数的处理!!!不知道该调用谁,但是python解决了!不再详细解释

equals 约定

  • 自反性,任何非空的 x ,x.equals(x) 应该返回true
  • 对称性,对于任意非空的x、y ,如果x.equals(y) 返回true,那么 y.equals(x)也该返回 true
  • 传递性,对于任意非空的x、y、z,如果x.equals(y) ,y.equals(z)返回truex.equals(z)也该返回 true
  • 一致性,对于任意非空的x、y, 任何时候 x.equals(y) 都应该返回 true 或者 false,不能一会返回true、一会返回false
  • 非空性,对于不等于null的x,x.equals(null)必须返回false。要遵守这个约定,只需要在equals里面加上这么一段代码 if(o == null) return false,然而,这样做往往是没有必要的,因为我们都会在equals方法的第一步,做instanceof校验,检查参数是否为正确的类型。而a.instanceOf(null)会返回false。

每当你想覆盖 equals方法的时候,都会同时帮你生成一个 hashCode

  • java约定:当你覆盖 equals的时候 就要同时覆盖hashCode
  • equals 判断的是 两个对象里的内容是不是相等的
  • == 判断的是 两个对象 是不是相同的 addr一致

类的结构和初始化顺序

  • 子类拥有父类一切的数据和行为
  • 父类先于子类
  • 必须拥有匹配的构造器
    • super
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Animal {
String name;
{
System.out.println("初始化父类块");
}
public Animal(String name) {
this.name = name;
}
}


public class Cat extends Animal{
int age;
{
System.out.println("初始化子类块");
}
public Cat(String name,int age) {
super(name);
this.age = age;
}
}
  • 过程就是自顶向下依次初始化
    • step01 Animal 成员初始化
    • step02 Animal 成员初始化块
    • step03 Animal 构造器
    • step04 Cat 成员初始化
    • step05 Cat 成员初始化块
    • step06 Cat 构造器

实例方法的覆盖

  • 又称为重写/覆盖
  • 永远使用@Override注解来防止手残
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public class Animal {
    String name;

    public Animal(String name) {
    this.name = name;
    }

    public void sayHello(){
    System.out.println("hello");
    }
    }

    // ----------------------------------
    public class Cat extends Animal{
    int age;

    public Cat(String name,int age) {
    super(name);
    this.age = age;
    }

    @Override
    public void sayHello(){
    System.out.println("cat hello");
    }
    }

模版方法模式

  • spring的初始化代码就是模版方法,定义了一些预定义好的方法。自定义的实现可以覆盖模版的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 模版方法
public class BookWriter {
// 写书
public void wtiteBook(){
writeTitle();
writeContent();
writeEnding();
}
public void writeTitle(){
System.out.println("标题");
}
public void writeContent(){
System.out.println("内容");
}
public void writeEnding(){
System.out.println("结尾");
}

}


// 子类继承父类的模版方法
public class MyBookWriter extends BookWriter {
@Override
public void writeContent(){
System.out.println("我的内容");
}

public static void main(String[] args) {
new MyBookWriter().wtiteBook();
}
}

向上和向下转型

  • 一个子类对象一定是父类类型的对象

    • 正如一只猫同时也是一个动物,同时也是一个对象
    • instanceof 判断一个对象是不是一个类的实例

      1
      2
      3
      4
      5
      Object a = new Integer(1234);
      System.out.println(a instanceof Integer); // true
      System.out.println(a instanceof Number); // true
      System.out.println(a instanceof Object); // true
      System.out.println(null instanceof Integer); // false
    • null installceof ? 都是 false

  • 当需要一个父类型时,可以传递一个子类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public static void main(String[] args) {
    setList(new ArrayList<Integer>());
    }

    public static void setList(AbstractList<Integer> list){
    }

    // <Integer> 是泛型, ArrayList 是 AbstractList 的子类

    // 还可以这样
    public static void main(String[] args) {
    setList(new ArrayList<Integer>());
    }

    public static void setList(AbstractList<? super Number> list){
    }
  • 但是,有的时候你必须进行一些转型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     public static void main(String[] args) {
    Animal a = new Animal();
    setAnimalName((Dog)animal);
    }

    public static void setAnimalName(Dog dog){
    }

    // 报错
    Exception in thread "main" java.lang.ClassCastException: Animal cannot be cast to Dog
    • 转型是不安全的
    • 失败了怎么办?

final

  • final声明变量是不可变的(必须初始化)

    1
    2
    final int a = 1; // 正确
    final int a ; // 错误 没有初始化
    • 局部变量/方法参数

      1
      2
      3
      4
      public void f(final Date date){

      }
      // date的地址不能改变, 地址指向的东西可以改变
    • 成员变量

    • 常量和单例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // 命名规则是全部大写 ,然后以下划线区分 如 MAX_VALUE
      public static final double PI = 3.1415926;

      // 单例模式
      public class World{
      private static final World SINGLETON_INSTANCE = new World();
      private World(){}

      public static World getInstance(){
      return SINGLETON_INSTANCE;
      }
      }
  • final声明: 禁止继承(在class上)/覆盖(方法)/重写此方法(方法)

  • final在类上声明: 禁止继承此类
    • 继承提供了灵活性,也埋下了隐患
    • 为什么String/Integer 等是 final的呢?
      1. 假设 MyInteger extends Integer 此时重写 compare 本来 1<2 而你偏偏 改写成 1>2 这样就破坏所有使用Integer方法 的约定
      2. JDK通过把 String/Integer 定义为 final 来阻止恶意的客户端继承常见类,破坏程序的约定

final在 类/方法上有什么优点

  1. 使用 final 可以保证类无法被继承,方法无法被重写,别人无法通过继承来破坏约定,你可以大胆的做一些事情,软件设计的一个约定——放心大胆的做一些事,我可以保证这个约定不会被打破
  2. 这个方法是 final 时方法是被确定的无法多态

组合和继承

  • 继承:is-a
  • 组合:has-a
    • 代码复用
    • 推荐使用组合,因为非常灵活

… 暂存 组合和继承

effective java

  • 一个类设计的时候就该确定,是否可以继承,还是final的(不可以继承的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class CountingSet extends HashSet<Object> {
/** 统计"有史以来"向该集合中添加过的元素个数 */
private int count = 0;

@Override
public boolean add(Object obj) {
count++;
return super.add(obj);
}

@Override
public boolean addAll(Collection c) {
count += c.size();
return super.addAll(c);
}

public int getCount() {
return count;
}

// 我们希望创建一个Set,能够统计"有史以来"添加到其中去的元素个数
// 但是,现在结果明显不对
// 请尝试修复此问题
public static void main(String[] args) {
CountingSet countingSet = new CountingSet();
countingSet.add(new Object());
countingSet.addAll(Arrays.asList(1, 2, 3));

System.out.println(countingSet.getCount());
}
}
  • // 正确姿势是注释addAll里的 count += c.size()本行

    为啥 ?

  • 因为 我们的 类继承了 HashSet

  • HashSet 的 addAll 里会默认调用 add,所以结果是double
  • 这就是 “组合/继承” 使用的例子,因为 我们的 CountingSet 过分依赖了 父类的实现
  • 正确姿势
1
2
3
4
5
public class CountingSet{
// 组合的方式: 内部有一个 容器
HashSet<Object> innerSet;
// 用内部的 innerSet 去统计个数,而不是继承
}