java面向对象编程(进阶)

继承

类的继承,子类继承父类的所以方法和属性

当多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个 类中无需再定义这些属性和行为,只需要和抽取出来的类构成继承关系

通过 extends 关键字,可以声明一个类 B 继承另外一个类 A,定义格式如下:

[修饰符] classA {
	...
}
[修饰符] classB extendsA {
	...
}

例如

// 动物父类
class Animal{
  String name;
  int age;
  public void eat(){};
}
// 猫类
// 继承动物类的属性和方法
class Cat extends Animal{
  // 新增猫捉老鼠方法
  public void catchMouse(){};
}

继承的优点

  • 减少了代码冗余,提高了代码的复用性,更有利于功能的扩展
  • 现让类与类之间产生了 is-a 的关系,为多态的使用提供了前提
  • 继承描述事物之间的所属关系,这种关系是:is-a 的关系,父类 更通用、更一般,子类更具体

继承性的细节说明

  1. 子类会继承父类所有实例变量和实例方法
  2. 子类不能直接访问父类中私有(private)的属性和方法,可通过 get / set 操作
  3. 继承的关键字用的是 extends,即子类不是父类的子集,而 是对父类的扩展
  4. Java 支持多层继承(继承体系)
  5. 一个父类可以同时拥有多个子类
  6. Java 只支持单继承,不支持多重继承
  7. 所有的类默认继承 Object,作为父类

方法的重写

子类会对父类的所有方法继承,继承过来的方法不适合子类,子类即可进行重写,程序执行时,子类的方法将覆盖父类的方法

例如

// phone 手机父类
public class Phone {
  public void sendMessage(){
    System.out.println("发短信");
  }
  public void call(){
    System.out.println("打电话");
  }
  public void showNum(){
    System.out.println("来电显示号码");
  }
}

//SmartPhone:智能手机
public class SmartPhone extends Phone{
 //重写父类的来电显示功能的方法
	@Override
	public void showNum(){
    //来电显示姓名和图片功能
    System.out.println("显示来电姓名");
    System.out.println("显示头像");
	}
}

@Override 使用说明:

写在方法上面,用来检测是不是满足重写方法的要求。这个注解就算 不写,只要满足要求,也是正确的方法覆盖重写。建议保留,这样编 译器可以帮助我们检查格式,另外也可以让阅读源代码的程序员清晰 的知道这是一个重写的方法

方法重写的要求

  • 子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表
  • 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型(例如: Cat < Animal)
    • 如果返回值类型的 void 基本数据类型 那么必须相同
  • 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限(public > protected > 缺省 > private)
    • 父类私有方法不能重写 、 跨包的父类缺省的方法也不能重写
  • 子类方法抛出的异常不能大于父类被重写方法的异常

注:子类与父类中同名同参数的方法必须同时声明为非 static 的(即为重写), 或者同时声明为 static 的(不是重写)。因为 static 方法是属于类的,子类无法 覆盖父类的方法

super关键字

在子类中通过 super 调用父类的属性、方法与构造器

注意

  • 子父类出现同名成员时,可以用 super 表明调用的是父类中的成员
  • super 的追溯不仅限于直接父类
  • super 和 this 的用法相像,this 代表本类对象的引用,super 代表父类的内存空间的标识

super 的使用场景

调用父类被重写的方法

  1. 如果子类没有重写父类的方法,只要权限修饰符允许,在子类中完全可以直接调用父类的方法
  2. 如果子类重写父类的方法,需要调用父类被重写的方法,使用 super.

例如

// phone 手机父类
public class Phone {
  public void showNum(){
    System.out.println("来电显示号码");
  }
}

//SmartPhone:智能手机
public class SmartPhone extends Phone{
 //重写父类的来电显示功能的方法
	@Override
	public void showNum(){
    // 来电显示姓名和图片功能
    System.out.println("显示来电姓名");
    System.out.println("显示头像");
    // 保留父类来电显示号码的功能
    super.showNum();
    // 此处必须加 super.
    // 否则就是无限递归,那么就会栈内存溢出
	}
}

总结:

  1. 方法前面没有 this.super. ,先从子类匹配方法,如果没有,在父类找,再没有就往上追溯
  2. 方法前有 this. ,同上
  3. 方法前有 super. ,从当前子类的直接父类找,如果没有,继续往上追溯

子类构造器中调用父类构造器

子类继承父类时,不会继承构造器,只能通过 spuer(形参列表) 调用父类的构造器

super(形参列表),必须声明在构造器的首行,this(形参列表) 不能同时存在

如果在子类构造器的首行既没this(形参列表),也没 super(形参列表), 则子类此构造器默认调用super(),即调用父类中空参的构造器

结论:子类的任何一个构造器中,要么会调用本类中重载的构造器,要么会调用父类的构造器

例如

class A{
  A(){
    System.out.println("A 类无参构造器");
  }
  A(int a){
    System.out.println("A 类有参构造器");
  }
}
class B extends A{
  B(){
    super();//可以省略,调用父类的无参构造
    System.out.println("B 类无参构造器");
  }
  B(int a){
    super(a);//调用父类有参构造
    System.out.println("B 类有参构造器");
  }
}

多态性

多态性,是面向对象中最重要的概念,在 Java 中的体现:对象的多态性:父类的引用指向子类的对象

格式:父类类型 变量名 = 子类类型

例如

Person person = new Student();

// Obejct 类型的 obj 指向 Person 类型的对象 
Object obj = new Person();
// Obejct 类型的 obj 指向 Student 类型的对象 
obj = new Student();

对象的多态:在 Java 中,子类的对象可以替代父类的对象使用

所以,一个引用类型变量可能指向(引用)多种不同类型的对象

多态的理解

Java 引用变量有两个类型:编译时类型和运行时类型

  • 编译时类型由声明该变量时使用的类型决定

  • 运行时类型由实际赋给该变量的对象决定

总结:编译时,看左边;运行时,看右边

若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)

多态情况下

  • 看左边:看的是父类的引用(父类中不具备子类特有的方法)

  • 看右边:看的是子类的对象(实际运行的是子类重写父类的方法)

多态的使用前提:①类的继承关系 ②方法的重写

例如

方法内局部变量的赋值体现多态

// Pet 父类
public class Pet{
  private String name;
  public String getName(){
    return name;
  }
  public void setName(String name){
    this.name = name;
  }
  public void eat(){
    System.out.printIn(pet + "吃食物");
  }
} 
// Cat 子类 继承 Pet 父类
public class Cat(){
  //子类重写父类的方法
  @Override
  public void eat() {
    System.out.println("猫咪" + getNickname() + "吃鱼仔");
  }
  //子类扩展的方法
  public void catchMouse() {
    System.out.println("抓老鼠");
  }
}

// 测试类 PetTest
public Class PetTest(){
  public static void main(String[] args) {
    // 多态引用
    Pet pet = new Cat();
    pet.setName("小白");
    
    // 执行子类Cat 重写的方法
    pet.eat(); 
    // 不能子类扩展的方法,报错
    pet.catchMouse();
  }
}

多态的表现形式:

  • 编译时看父类:只能调用父类声明的方法,不能调用子类扩展的方法
  • 运行时看子类,如果子类重写了方法,一定是执行子类重写的方法体

方法的形参声明体现多态

public Class Person{
  private Pet pet;
  // 方法的形参是父类类型,接收到的实参是子类类型
  public void adopt(Pet pet){
    this.pet = pet;
  }
  public void feed(){
    //pet实际引用的对象类型不同,执行的eat方法也不同
    pet.eat();
  }
}

public Class PersonTest{
  public static void main(String[] args) {
    Person person = new Person();
    
    Cat cat =new Cat();
    cat.setName("mimi");
    // 方法的形参是父类类型,接收到的实参是子类类型
    person.adopt(cat);
    person.feed();
  }
}

方法返回值类型体现多态

public class PetShop{
  //返回值类型是父类类型,实际返回的是子类对象
  public pet sale(String type){
  	if(type == "cat"){
      return new Cat();
    } else if(type == "dog" ){
      return new Dog();
    } else return null;
  }
}

public class PetShopTest{
  public static void main(String[] args) {
    PetShop petShop = new PetShop();
    
    //返回值类型是父类类型,实际返回的是子类对象
    pet cat = petShop.sale("cat");
    cat.setName("mimi");
    cat.eat();
  }
}

我们在设计一个数组、或一个成员变量、或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型,此时可以考虑多态

多态的好处和弊端

好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。

弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法

开发中:

使用父类做方法的形参,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。

【开闭原则OCP】

  • 对扩展开放,对修改关闭
  • 通俗解释:软件系统中的各种组件,如模块(Modules)、类(Classes)以及功能(Functions)等,应该在不修改现有代码的基础上,引入新功能

虚方法调用

在Java中虚方法是指在编译阶段不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法

Person e = new Student();
e.getInfo();	//调用Student类的getInfo()方法

子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的

成员变量没有多态性

  • 若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。

  • 对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量

向上转型与向下转型

对象在new的时候创建对象类型,它从头至尾都不会变。即这个对象的运行时类型,本质的类型用于不会变。但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同

因为多态,会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现类型转换的现象

在父类变量接收子类对象之后,不能调用子类特有的方法,此时可通过类型转换,去想要调用子类特有的方法

  • 向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型

    • 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
    • 此时,一定是安全的,而且也是自动完成的
  • 向下转型:当左边的变量的类型(子类)< 右边对象/变量的编译时类型(父类),我们就称为向下转型

    • 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型
    • 不是所有通过编译的向下转型都是正确的,可能会发生 ClassCastException,为了安全,可以通过 isInstanceof 关键字进行判断

向上转型:自动完成

举例:向下转型:(子类类型)父类变量

public class ClassCastTest {
  public static void main(String[] args){
    //没有类型转换
    Dog dog = new Dog();//dog的编译时类型和运行时类型都是Dog
    //向上转型
    //pet的编译时类型是Pet,运行时类型是Dog
    Pet pet = new Cat();
    pet.setName("mimi");
    //可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
    pet.eat();

    // 向下转型
    Cat c = (Cat) pet;
    System.out.println("d.name = " + d.getName());
    //可以调用eat方法
    C.eat();
    //可以调用子类扩展的方法catchMouse
    C.catchMouse();
  }
}

instanceof关键字

为了避免 ClassCastException 的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验。如下代码格式:

//检验对象a是否是数据类型A的对象,返回值为boolean型
对象a instanceof 数据类型A

说明:

  • 只要用 instanceof 判断返回 true 的,那么强转为该类型就一定是安全的,不会报ClassCastException 异常。
  • 如果对象a属于类A的子类B,a instanceof A值也为 true。
  • 要求对象a所属的类与类A必须是子类和父类的关系,否则编译错误。

Object 类

java.lang.Object是类层次结构的根类,即所有其它类的父类。每个类都使用 Object 作为超类

  • Object类型的变量与除Object以外的任意引用数据类型的对象都存在多态引用

    method(Object obj){} //可以接收任何类作为其参数
    
    Person o = new Person();  
    method(o);
  • 所有对象(包括数组)都实现这个类的方法

  • 如果一个类没有特别指定父类,那么默认则继承自Object类

Object类的方法

equals()

= =运算符:

  • 基本类型比较值:只要两个变量的值相等,即为true
  • 引用类型比较引用(是否指向同一个对象):只有指向同一个对象时,== 才返回true

equals() 方法:所有类都继承了Object,也就获得了equals()方法。还可以重写

  • 比较引用类型,Object类源码中 equals() 的作用与 ==相同 :比较是否指向同一个对象
  • 特例:对类File、String、Date及包装类(Wrapper Class)来说,是比较类型及内容而不考虑引用的是否是同一个对象,这些类中重写了 Object 类的 equals() 方法

重写 equals() 方法的原则

  • 对称性:如果 x.equals(y) 返回是 true ,那么 y.equals(x) 也应该返回是 true

  • 自反性:x.equals(x) 必须返回是 true

  • 传递性:如果 x.equals(y) 返回是 true ,而且 y.equals(z) 返回是true,那 z.equals(x) 也返回是 true

  • 一致性:如果 x.equals(y) 返回是 true ,只要 x 和 y 内容一直不变,不管你重复x.equals(y) 多少次,返回都是 true

  • 任何情况下,x.equals(null),永远返回是 false ;x.equals(和x不同类型的对象)永远返回是 false

==和equals的区别

  • == 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址

  • equals 的话,它是属于 java.lang.Object 类里面的方法,如果该方法没有被重写过默认也是==; 我们可以看到 String 等类的 equals 方法是被重写过的,而且 String 类在日常开发中用的比较多,久而久之,形成了equals 是比较值的错误观点

  • 具体要看自定义类里有没有重写 Object 的 equals 方法来判断

  • 通常情况下,重写 equals 方法,会比较类中的相应属性是否都相等

toString()

  • 默认情况下,toString() 返回的是对象的运行时类型 @ 对象的hashCode值的十六进制形式
  • 在进行String与其它类型数据的连接操作时,自动调用 toString() 方法
  • 如果我们直接 System.out.println(对象),默认会自动调用这个对象的 toString()
  • 根据需要在用户自定义类型中重写toString()方法,如String 类重写了toString()方法,返回字符串的值

重写 toString() 方法

public class Person {  
  private String name;
  private int age;

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

参考资料

尚硅谷Java基础