面向对象(中)

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2022-06-25

继承性

继承性的好处:

  • 减少了代码冗余,提高了代码的复用性
  • 便于功能的扩展
  • 为之后多态性的使用,提供了前提

继承语法的格式:class A extends B {}

继承的体现:

  • 一旦子类A继承父类B以后,子类A中就获取了父类B中声明的所有的属性和方法。特别的,父类中声明为private的属性或方法,子类继承父类之后,仍然认为获取了父类中私有的结构,只因为封装性的影响,使得子类不能直接调用父类的结构而已。

  • 子类继承父类以后,还可以声明自己特有的属性或方法,实现功能的扩展。

继承的说明:

  • 一个类可以被多个子类继承。
  • Java中类的单继承性:一个类只能有一个父类
  • 子父类是相对的概念
  • 子类直接继承的父类称为直接父类。间接继承的父类称为间接父类
  • 子类继承父类以后,就获取了直接父类以及所有间接父类中声明的属性和方法

关于java.lang.Object类的理解:

  • 如果我们没显式的声明一个类的父类的话,则此类继承于java.lang.Object类。
  • 所有的java类(除java.lang.Object类外)都直接或间接的继承于java.lang.Object类,意味着所有的java类都具有java.lang.Object类声明的功能。

初步使用继承:

// ManKind.java
public class ManKind {
  private int sex;
  private int salary;

  public ManKind() {
  }

  public ManKind(int sex, int salary) {
    this.sex = sex;
    this.salary = salary;
  }

  public void manOrWoman() {
    if (sex == 1) {
      System.out.println("man");
    } else if (sex == 0) {
      System.out.println("woman");
    }
  }

  public void employed() {
    String jobInfo = (salary == 0) ? "no job" : "job";
    System.out.println(jobInfo);
  }

  public int getSex() {
    return sex;
  }

  public void setSex(int sex) {
    this.sex = sex;
  }

  public int getSalary() {
    return salary;
  }

  public void setSalary(int salary) {
    this.salary = salary;
  }
}

// Kids.java
public class Kids extends ManKind {
  private int yearsOld;

  public Kids() {
  }

  public Kids(int yearsOld) {
    this.yearsOld = yearsOld;
  }

  public void printAge() {
    System.out.println("I am " + yearsOld + " years old.");
  }

  public int getYearsOld() {
    return yearsOld;
  }

  public void setYearsOld(int yearsOld) {
    this.yearsOld = yearsOld;
  }
}

// KidTest.java
public class KidTest {
  public static void main(String[] args) {
    Kids someKid = new Kids(12);
    someKid.printAge();

    someKid.setSalary(0);
    someKid.setSex(1);
    someKid.employed();
    someKid.manOrWoman();
  }
}

方法的重写(overwrite)

子类继承父类之后,可以对父类中同名同参数的方法进行重写。

// PersonA.java
public class PersonA {
  String name;
  int age;

  public PersonA () {};

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

  public void eat() {
    System.out.println("吃饭");
  }

  public void walk(int distance) {
    System.out.println("走了" + distance + "公里");
  }
}

class Student extends PersonA {
  String major;

  public Student () {};

  public Student (String major) {
    this.major = major;
  }

  public void study() {
    System.out.println("学习的专业是" + major);
  }

  public void eat() {
    System.out.println("学生应该吃有营养的对象");
  }
}

// PersonATest.java
public class PersonATest {
  public static void main(String[] args) {
    Student s = new Student("计算机科学");
    s.eat(); // 学生应该吃有营养的对象
    s.walk(10); // 走了10公里

    PersonA a = new PersonA();
    a.eat(); // 吃饭
  }
}

重写以后,当创建子类对象以后,通过子类对象调用子父类中的同名同参数的方法时,实际执行的是子类重写父类的方法。

重写的规定

  • 子类重写的方法的方法名和形参列表与父类被重写的方法的方法名和形参列表相同。

  • 子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符。但子类不能重写父类中private修饰的方法,如果子类中有一个方法与父类中private修饰的方法的方法名和形参列表一致,也不会报错,但这不是重写,只能认为是在子类中也定义了一个同名的方法。static修饰的方法也是一样,因为static静态方法是随着类的加载而加载的。

  • 返回值类型:

    • 父类被重写的方法的返回值类型是void,则子类中重写的方法的返回值类型也只能是void。

    • 父类被重写的方法的返回值类型是A,则子类中重写的方法的返回值类型只能是A类或A类的子类

    • 父类被重写的方法的返回值类型如果是基本数据类型,则子类中重写的方法的返回值类型也只能是相同的基本数据类型。

  • 子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型。权限修饰符 返回值类型 方法名(形参列表) throws 异常的类型 { 方法体 }

super关键字

  • super指向父类,可以用来调用父类的属性、方法和构造器。

  • 可以在子类的方法或构造器中,可以通过super.属性super.方法的方式,显式的去调用父类声明的属性或方法,但是通常情况下,我们会省略 super.,而当子类和父类中定义了同名的属性/方法时,我们要想在子类中调用父类中声明的属性/方法,则必须显示的使用super.来调用

  • 当子类重写了父类的某个方法,但同时也想调用以下父类的被重写的方法,就可以使用super引用父类的被重写的方法。

  • 我们可以在子类的构造器中显示的使用super(形参列表)的方法,调用父类中声明的指定的构造器。super(形参列表)必须声明在子构造器的首行,因此在类的构造器中 this(形参列表)super(形参列表) 调用构造器的方式只能二选一。如果两个都没有显示的声明,则默认有一个隐形的 super() 被调用了。在类的多个构造器中,至少有一个构造器使用了super(形参列表)调用父类的构造器。

子类对象的实例化过程

1、从结果上来看:

  • 子类继承父类之后,就获取了父类中声明的属性和方法。
  • 创建子类的对象,在堆空间中,就会加载所有父类中声明的属性。

2、从过程上来看:

  • 当我们通过子类的构造器创建子类对象时,我们一定会直接或间接的调用其父类的构造器,进而调用其父类的父类的构造器,直到调用了java.lang.Object类中空参的构造器为止。正因为加载过所有的父类的结构,所以可以看到内存中有父类中的结构,子类对象才可以考虑进行调用。

  • 虽然创建子类对象时,调用了父类的构造器,但是自始至终就创建过一个对象,即为new的子类对象。

多态性

1、多态性可以理解为一个事物的多种形态

// PolyTest.java
class Person {
  String name;
  int age;

  public void eat() {
    System.out.println("吃饭");
  }

  public void walk() {
    System.out.println("走路");
  }
}

class Man extends Person {
  String job;

  public void eat() {
    System.out.println("男人要吃肉");
  }

  public void work() {
    System.out.println("我的工作是" + job);
  }
}

public class PolyTest {
  public static void main(String[] args) {
    Person p1 = new Person();
    p1.eat();

    Man m = new Man();
    m.eat();
    m.age = 20;

    // 对象的多态性,父类的引用指向子类的对象
    // 可以将子类赋值给父类,但是不能将父类赋值给子类,因为父类有的子类都有,但是子类有的,父类不一定有
    Person p2 = new Man();
    // 多态的使用:当调用子父类同名同参数的方法时,实际执行的是子类重写父类的方法---虚拟方法调用
    p2.eat(); // 男人要吃肉
    p2.walk(); // 走路 子类没有,就会调父类的方法
    // p2.work();  // error, 不能调用子类所特有的方法,因为p2是Person类型,是没有这个方法的
  }
}

2、何为多态性?

对象的多态性:父类的引用指向子类的对象(将子类赋值给父类)。

3、多态的使用

有了对象的多态性后,我们在编译器,只能调用父类中声明的方法(不能调子类特有的方法),但是调用了声明在父类中的方法时,实际执行的却是子类重写父类的方法(如果有)。编译看左边,执行看右边。

4、多态性的使用前提:a.类的继承关系; b.方法的重写;

5、为什么要有多态性

public class AnimalTest {
  public static void main(String[] args) {
    AnimalTest at = new AnimalTest();
    at.func(new Dog()); // 等同于做了这样的赋值 Animal animal = new Dog()

    at.func(new Cat());

    // 如果不存在多态性,那么func方法只能接收Animal类型
    // 要实现以上的调用场景 就需要重载func方法 public void func(Dog dog) { ... }
  }

  public void func(Animal animal) {
    animal.eat();
    animal.shout();
  }
}

class Animal {
  public void eat() {
    System.out.println("动物进食");
  }

  public void shout() {
    System.out.println("动物叫");
  }
}

class Dog extends Animal {
  public void eat() {
    System.out.println("狗吃骨头");
  }

  public void shout() {
    System.out.println("汪汪");
  }
}

class Cat extends Animal {
  public void eat() {
    System.out.println("猫吃鱼");
  }

  public void shout() {
    System.out.println("喵喵");
  }
}

6、对象的多态性只适用于方法(编译看左边,运行看右边),不适用于属性(编译和运行都看右边)。

class Person2 {
  int id = 1001;
}

class Man2 extends Person2 {
  int id = 1002;
}

public class PolyTest2 {
  public static void main(String[] args) {
    Person2 p2 = new Man2();
    System.out.println(p2.id); // 1001
    // 因为在堆空间中,其实是存在两个id,但是声明的p2是Person2类型的,所有访问父类Person2的id属性
  }
}

7、什么是虚拟方法调用(Virtual Method Invocation)

子类定义了与父类同名同参数的方法,在多态的情况下,将此时的父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法,这样的方法在编译期是无法确定的。编译时会“认为”调用的是父类的方法,实际执行时执行的却是子类重写的该方法。

8、证明多态性是运行时行为

import java.util.Random;

public class AnimalTest {
  public static void main(String[] args) {
    int key = new Random().nextInt(2);
    System.out.println(key); // 0 or 1

    Animal a = getInstance(key); 
    // getInstance(key) 返回Cat实例对象或者Dog实例对象
    // 但是这个结果是随机的,在编译器是无法确定的,只有在运行时才能知道
    a.eat();
  }

  public static Animal getInstance(int key) {
    switch (key) {
      case 0:
        return new Cat();
      default:
        return new Dog();
    }
  }
}

class Animal {
  public void eat() {
    System.out.println("动物进食");
  }
}

class Dog extends Animal {
  public void eat() {
    System.out.println("狗吃骨头");
  }
}

class Cat extends Animal {
  public void eat() {
    System.out.println("猫吃鱼");
  }
}

9、与重载的比较

重载,是指允许存在多个同名方法,而这些方法的参数不同,编译器根据方法不同的参数列表,对同名方法作区分, 对于编译器而言,这些同名方法就成了不同的方法,它们的调用地址在编译期就绑定了。Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。所以对于重载而言,在方法调用之前,编译期就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”。

而对于多态,只有等到方法调用的那一刻,解释运行期才会确定所有调用的具体方法,这称为“晚绑定”或“动态绑定”。

10、instanceof操作符

public class InstanceofTest {
  public static void main(String[] args) {
   Person p1 = new Person();
    p1.eat();

    Man m = new Man();
    m.eat();
    m.age = 20;

    Person p2 = new Man();
    //p2.work(); // error
    // 有了对象的多态性以后,内存中实际上是加载了子类的特有的属性和方法
    // 但是由于变量声明为父类类型,导致编译时,
    // 只能调用父类中声明的属性和方法,子类特有的属性和方法不能调用

    // 如何才能调用子类特有的属性和方法?
    // 使用强制类型转换 向下转型
    Man m2 = (Man)p2;
    m2.work();
    // 强制类型转换:较高级的基本数据类型 -> 较低级的基本数据类型
    // 自动类型提升:较低级的基本数据类型 -> 较高级的基本数据类型

    // 向下转型: 父类 -> 子类 (instanceof 判断)
    // 向上转型: 子类 -> 父类 (多态)

    // 使用强转可能会出现异常 ClassCastException
    // 比如,将变量p2转换成Person的另一个子类
    // Woman w = (Woman)p2; // error,因为实际上p2是一个Man

    // 为避免可能出现的异常 所以可以使用instanceof进行判断。 a instanceof A 判断对象a是否是类A的实现,如果是,返回true
    // 同时,若 a instanceof A 返回true,同时 a instanceof B 也返回true,则类A和类B存在继承关系
    if (p2 instanceof Man) {
      ((Man) p2).work();
    }
  }
}

Object类

关于java.lang.Object类:

  • Object类是所有Java类的根父类
  • 如果在类的声明中未使用extends关键字指明其父类,则默认父类为java.lang.Object
  • Object类中的功能(方法)具有通用性,没有属性,只有方法,也只声明了一个空参的构造器

equals 和 == 的区别

1、== 是运算符,可以使用在基本数据类型变量中(比较值,不一定类型要相同)和引用数据类型变量中(比较地址值)。

2、equals 是一个方法,只能适用于引用数据类型。

String str1 = new String('jack');
String str2 = new String('jack');
System.out.println(str1.equals(str2)) // true 在String中重写过

Student s1 = new Student('jack', 18);
Student s2 = new Student('jack', 18);
System.out.println(s1.equals(s2)) // false 在Object定义的equals方法与==的作用是相同的, this == obj

3、像StringDateFile、包装类等都重写了Object类中的equals()方法,重写以后,比较的不是两个引用的地址是否相同,而是比较两个对象的“实体内容”是否相同。

4、如何重写equals方法,使得自定义类进行比较的时候也是比较的是实体内容。

public class CustomerTest {
  public static void main(String[] args) {
    Customer c1 = new Customer("jack", 18);
    Customer c2 = new Customer("jack", 18);
    System.out.println(c1.equals(c2)); // true
  }
}

class Customer {
  private String name;
  private int age;

  public Customer() {
  }

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

  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }

    if (obj instanceof Customer) {
      Customer cust = (Customer) obj;
      if (this.age == cust.age && this.name.equals(cust.name)) {
        return true;
      }
    }

    return false;
  }

  /** idea 自动构建生成的equals的写法 */
  public boolean equals2(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Customer customer = (Customer) o;
    return age == customer.age && Objects.equals(name, customer.name);
  }
}

重写的原则:比较两个对象的实体内容(即属性)是否相同。

toString方法

1、当我们输出一个对象的引用时,实际上就是调用当前对象的toString方法

Customer c1 = new Customer("jack", 18);
System.out.println(c1); // Customer@19469ea2
System.out.println(c1.toString()); // Customer@19469ea2

2、Object类中toString()的定义

public String toString() {
  return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

3、String、Date、File、包装类也重写了Object类中的toString方法。使得在调用对象的toString()时,返回“实体内容”信息。

4、IDEA 也能自动构建一个重写的toStirng方法

@Override
public String toString() {
  return "Customer{" +
          "name='" + name + '\'' +
          ", age=" + age +
          '}';
}

单元测试

import org.junit.Test;

/**
 * 导入junit
 * File -> Project Structure -> Modules -> Dependencies -> + -> JARs or Directiories
 * 找到安装IDEA的路径(mac就是应用的包内容),下面的 /lib/junit.jar 导入进来
 */
public class JUnitTest {

  /**
   * 要求:方法的权限是public,没有返回值,没有形参
   *      声明注解@Test,并在单元测试类中导入 import org.junit.Test
   *      每个设置了@Test注解的单测都可以单独执行run
   */
  @Test
  public void testEquals() {
    String s1 = "MM";
    String s2 = "MM";
    System.out.println(s1.equals(s2));
  }

  @Test
  public void testToString() {
    String s1 = "MM";
    System.out.println(s1.toString());
  }
}

包装类

针对八种基本数据类型定义相应的引用类型,即包装类(封装类)。

基本数据类型 包装类
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
char Character

基本数据类型、包装类、String三者之间的相互转换

import org.junit.Test;

public class WrapperTest {
  // 基本数据类型 -> 包装类,调用包装类的构造器
  @Test
  public void test1() {
    Integer i1 = new Integer("123");
    System.out.println(i1.toString()); // 123

    //Integer i2 = new Integer("123a"); // error
    //System.out.println(i2.toString());

    Float f1 = new Float(12.3f);
    System.out.println(f1); // 12.3
    Float f2 = new Float("12.3f");
    System.out.println(f1); // 12.3

    Boolean b1 = new Boolean(true); // true
    Boolean b2 = new Boolean("true"); // true
    Boolean b3 = new Boolean("true123"); // false,只要传入的参数如果是字符串,当忽略大小写后与true不全等则都是false

    Order o = new Order();
    System.out.println(o.b1); // false
    System.out.println(o.b2); // b2是包装类,即引用类型,默认值即为null
  }

  // 包装类 -> 基本数据类型:调用包装类的 xxxValue() 方法
  @Test
  public void test2() {
    Integer i1 = new Integer(12);
    int i2 = i1.intValue();
    System.out.println(i2 + 1); // 13

    Float f1 = new Float(12.3);
    float f2 = f1.floatValue();
    System.out.println(f2 + 1); // 13.3
  }

  @Test
  public void test3() {
    int num1 = 10;
    // JDK 5.0+
    // 自动装箱,省略包装类构造器的调用,可以直接将基本数据类型直接赋值给它的包装类
    Integer num2 = num1; // 等同于 Integer num2 = new Integer(num1);

    // 自动拆箱,直接将包装类赋值给基本数据类型,省略了 xxxValue();
    int num3 = num2; // 等同于 int num3 = num2.intValue();

    methodA(num3); // 也可以直接将int的num3传递给接收Object的方法,因为有一个隐式转换
  }

  public void methodA(Object obj) {
    System.out.println(obj);
  }

  // 基本数据类型、包装类 -> String类型
  @Test
  public void test4() {
    int num1 = 10;
    // 1. 连接运算
    String s1 = num1 + "";

    // 2. valueOf(),String重写过
    float f1 = 12.3f;
    String s2 = String.valueOf(f1); // "12.3"

    Float f2 = new Float(12.3f);
    String s3 = String.valueOf(f2); // "12.3"
  }

  // String类型 -> 基本数据类型、包装类:调用包装类的parseXxx()方法
  @Test
  public void test5() {
    // 错误的情况,使用强转,没有子父类关系的强转编译都不会通过
    String s1 = "123";
    //int num1 = (int)s1; // error
    //Integer in1 = (Integer) s1; // error

    int num1 = Integer.parseInt(s1);
    System.out.println(num1 + 1); // 124

    boolean b1 = Boolean.parseBoolean("true");
    System.out.println(b1); // true
  }
}

class Order {
  boolean b1;
  Boolean b2;
}

关于包装类的使用

public class TestWrapper {

  @Test
  public void test1() {
    Object o1 = true ? new Integer(1) : new Double(2.0);
    System.out.println(o1); // 1.0
    // 因为三元运算符,会进行一个类型提升,会根据后面Double将Integer提升为Double
    // 编译时,不知道最终赋值给o1的是Integer还是Double,所以要提升


    Object o2;
    if (true) {
      o2 = new Integer(1)
    } else {
      o2 = new Double(2.0)
    }
    System.out.println(o1); // 1,if...else没有相关的类型提升
  }

  @Test
  public void test3() {
    Integer i = new Integer(1);
    Integer j = new Integer(1);
    System.out.println(i == j); // false, 两个对象比较地址值

    Integer m = 1;
    Integer n = 1;
    System.out.println(m == n); // true

    Integer x = 128;
    Integer y = 128;
    System.out.println(x == y); // false

    // Integer内部定义了一个IntegerCache结构
    // 会缓存-128 ~ 127 共256个数字(经常用到的范围)
    // 当取值这256个数字其中一个的时候,会从缓存数字数组cache中去取得这个地址
    // 所以m,n都是取得同一个地址,而128不存在缓存数组中需要重新创建,所以是x == y的结果为false
  }
}