继承
概述
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那一个类即可。多个类可以称为子类,派生类,衍生类;单独那一个类称为父类、超类(superclass)或者基类。 继承描述的是事物之间的所属关系,这种关系是: is-a 的关系。父类更通用,子类更具体。我们通过继承,可以使多种事物之间形成一种关系体系
定义:
- 继承:就是子类继承父类的属性和行为,使得子类对象具有与父类相同的属性、相同的行为。子类可以直接访问父类中的非私有的属性和行为。(子类还可以有自己特有的成员)
好处:
- 提高代码的复用性。
- 类与类之间产生了关系,是多态的前提。
- 提高了代码的维护性
弊端:
继承使得类与类之间产生了关系,类的耦合性增强,当父类发生变化时,子类实现也不得不跟着变化,削弱了子类的独立性
(开发的原则:高内聚 低耦合 )
什么时候使用继承?
继承体现的是一种关系:is a
继承的使用,一定是当两个类之间满足is a的关系的时候 才使用继承。不要为了继承而继承(不要仅仅为了减少代码的编写 ,提高代码的复用而去使用继承)
继承的格式
通过 extends 关键字,可以声明一个子类继承另外一个父类,定义格式如下:
class 父类 {
...
}
class 子类 extends 父类 {
...
}
继承后的特点——成员变量
当类之间产生了关系后,其中各类中的成员变量,又产生了哪些影响呢?
- 成员变量不重名:如果子类父类中出现不重名的成员变量,这时的访问是没有影响的。
- 成员变量重名: 如果子类父类中出现重名的成员变量,这时的访问是有影响的。
- 子父类中出现了同名的成员变量时,在子类中需要访问父类中非私有成员变量时,需要使用 super 关键字,修饰 父类成员变量,类似于之前学过的 this 。
使用格式:
super.父类成员变量名
Fu 类中的成员变量是非私有的,子类中可以直接访问。若Fu 类中的成员变量私有了,子类是不能 直接访问的。通常编码时,我们遵循封装的原则,使用private修饰成员变量,那么如何访问父类的私有成员 变量呢?对!可以在父类中提供公共的getXxx方法和setXxx方法。
1 在java中 变量的使用遵循就近原则
2 在继承关系的类中,优先在子类的局部返回中找
3 子类的成员范围中找
4 父类的成员找
5 如果以上都没有(不考虑的父类的父类) 则报错。
继承后的特点——成员方法
当类之间产生了关系,其中各类中的成员方法,又产生了哪些影响呢?
- 成员方法不重名: 如果子类父类中出现不重名的成员方法,这时的调用是没有影响的。对象调用方法时,会先在子类中查找有没有对 应的方法,若子类中存在就会执行子类中的方法,若子类中不存在就会执行父类中相应的方法,如果都没有 就报错。
- 成员方法重名——重写(Override): 如果子类父类中出现重名的成员方法,这时的访问是一种特殊情况,叫做方法重写 (Override)。
- 方法重写 :子类中出现与父类一模一样的方法时(返回值类型,方法名和参数列表都相同),会出现覆盖效果,也称为重写或者复写。声明不变,重新实现。
- 子类对父类的扩展:子类可以有自己的特有的成员变量和成员方法,子类中还可以定义和父类同名的成员变量和方法。
- 注意事项:
- 子类方法覆盖父类方法,必须要保证权限大于等于父类权限
- 子类方法覆盖父类方法,返回值类型、函数名和参数列表都要一模一样。
继承后的特点——构造方法
当类之间产生了关系,其中各类中的构造方法,又产生了哪些影响呢?
首先我们要回忆两个事情,构造方法的定义格式和作用。
- 构造方法的名字是与类名一致的。所以子类是无法继承父类构造方法的。
- 构造方法的作用是初始化成员变量的。所以子类的初始化过程中,必须先执行父类的初始化动作。子类的构 造方法中默认有一个 super() ,表示调用父类的构造方法,父类成员变量初始化后,才可以给子类使用。
- 在子类的构造方法中,默认会调用父类的无参构造
- 因为子类会继承父类中的数据,可能还会使用父类的数据,所以 ,子类初始化之前 一定要先使用父类的数据的初始化
- 每一个子类的构造方法中 第一条语句默认都是super();
如果父类中没有无参构造 会发生什么?
- 通过super区显式的调用父类的其他的带参构造
super(num);
- 在父类中提供一个无参构造
推荐:自己提供无参构造
super和this
父类空间优先于子类对象产生:
在每次创建子类对象时,先初始化父类空间,再创建其子类对象本身。目的在于子类对象中包含了其对应的父类空 间,便可以包含其父类的成员,如果父类成员非private修饰,则子类可以随意使用父类成员。代码体现在子类的构 造方法调用时,一定先调用父类的构造方法。理解图解如下:
super和this的含义:
- super:代表父类的存储空间标识(可以理解为父亲的引用)。
- this :代表当前对象的引用(谁调用就代表谁)。
super和this的用法:
- 访问成员
this.成员变量 ‐‐ 本类的
super.成员变量 ‐‐ 父类的
this.成员方法名() ‐‐ 本类的
super.成员方法名() ‐‐ 父类的
使用super去访问成员变量或者方法 成员变量或者成员方法 一定是子类和父类拥有同名的变量和方法的时候
- 访问构造方法
this(...) ‐‐ 本类的构造方法
super(...) ‐‐ 父类的构造方法
子类的每个构造方法中均有默认的super(),调用父类的空参构造。手动调用父类构造会覆盖默认的super()。 super() 和 this() 都必须是在构造方法的第一行,所以不能同时出现。
继承的特点
- Java只支持单继承,不支持多继承。
- 子类和父类是一种相对的概念。
- Java支持多层继承(继承体系)。
顶层父类是Object类。所有的类默认继承Object,作为父类。
继承所产生的影响
-
继承似的类与类之间产生了关系,父子类关系,产生父子类关系后,子类可以使用父类中非私有的成员。
-
子类继承了父类,就继承了父类的非私有的方法和属性
-
在子类中,可以使用父类中定义的方法和属性,也可以创建新的属性和方法
-
在java中,继承的关键字用的是extends 及子类不是父类的自己 而是对父类的"扩展"
方法的重写
-
方法重写 (Override)。在子类中如果创建了一个与父类中相同名称、相同返回值类型、相同参数列表的方法,只是方法体中的实现不同,以实现不同于父类的功能,这种方式被称为方法重写(override),又称为方法覆盖。当父类中的方法无法满足子类需求或子类具有特有功能的时候,需要方法重写。
-
方法重写的应用场景:子类可以根据需要,定义特定于自己的行为。既沿袭了父类的功能名称,又根据子类的需要重新实现父类方法,从而进行扩展增强。
-
在重写方法时,需要遵循下面的规则:
-
参数列表必须完全与被重写的方法参数列表相同。
-
返回的类型必须与被重写的方法的返回类型相同(Java1.5 版本之前返回值类型必须一样,之后的 Java 版本放宽了限制,返回值类型必须小于或者等于父类方法的返回值类型)。
-
访问权限不能比父类中被重写方法的访问权限更低(public>protected>default>private)。
-
重写方法一定不能抛出新的检査异常或者比被重写方法声明更加宽泛的检査型异常。例如,父类的一个方法声明了一个检査异常 IOException,在重写这个方法时就不能抛出 Exception,只能拋出 IOException 的子类异常,可以抛出非检査异常。
-
-
方法重写的注意事项:
- 重写的方法可以使用 @Override 注解来标识。
- 父类的成员方法只能被它的子类重写。
- 声明为 final 的方法不能被重写。
- 声明为 static 的方法不能被重写,但是能够再次声明。
- 构造方法不能被重写。
- 子类和父类在同一个包中时,子类可以重写父类的所有方法,除了声明为 private 和 final 的方法。
- 子类和父类不在同一个包中时,子类只能重写父类的声明为 public 和 protected 的非 final 方法。
- 如果不能继承一个方法,则不能重写这个方法。
多态
概述
多态是继封装、继承之后,面向对象的第三大特性。 生活中,比如跑的动作,小猫、小狗和大象,跑起来是不一样的。再比如飞的动作,昆虫、鸟类和飞机,飞起来也 是不一样的。可见,同一行为,通过不同的事物,可以体现出来的不同的形态。多态,描述的就是这样的状态。
- 多态: 是指同一行为,具有多个不同表现形式。
前提【重点】
- 继承或者实现【二选一】
- 方法的重写【意义体现:不重写,无意义】
- 父类引用指向子类对象【格式体现】
多态要点:
-
多态是方法的多态,不是属性的多态(多态和属性无关)
- 多态的存在要有三个必要条件
- 要有继承关系(实现关系)
- 要有方法的重写
- 要有父类的引用指向子类对象。
-
父类的引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了。
具体来讲: 多态指的是同一个方法调用,由于对象不同可能会有不同的行为。
多态的体现
多态体现的格式:
父类类型 变量名 = new 子类对象;
变量名.方法名();
父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
如果在编译时和运行时类型不一致,就出现了对象的多态性。
多态情况下成员方法
看左边:看的是父类的引用(父类中不具有子类特有的方法)
看右边:看的是子类的对象(实际运行时的子类重写父类的方法)
成员变量:不具备多态性,只要看引用变量声明的类型即可。
总结:
成员方法:编译看左边 运行看右边
成员变量:编译看左边 运行看左边
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的是子类重写 后方法。
public class Animal {
public int age = 3;
public void eat(){
System.out.println("动物进食.....");
}
}
public class Cat extends Animal{
public int age = 2;
public int weight = 10;
@Override
public void eat(){
System.out.println("猫吃鱼...");
}
public void palyGame(){
System.out.println("猫抓老鼠玩....");
}
}
public class Dog extends Animal{
public int age = 4;
public String name = "旺财";
@Override
public void eat(){
System.out.println("狗吃骨头....");
}
public void work(){
System.out.println("狗看家....");
}
}
测试类
public class AnimalTest {
public static void main(String[] args) {
// 父类引用指向子类对象
Animal a1 = new Cat();
a1.eat();// 猫吃鱼
//a1.palyGame();//访问不到
System.out.println(a1.age);//3
Animal a2 = new Dog();
a2.eat();//狗吃骨头
System.out.println(a2.age);//3
}
}
虚拟方法调用
子类中定义了与父类同名同参数的方法,在多态的情况下,将此事父类的方法称为虚拟方法。父类根据赋给他的不同的子类对象,动态调用属于子类的该方法,这样的方法 的调用在编译器是无法确定的。
Animal a1 = new Cat();
a1.eat();// 猫吃鱼
编译时类型和运行时类型
编译时 a1 是Animal类型,而方法调用时在运行时确定的,所以表用的是Cat类的eat方法。这种称为==动态绑定==。
多态的好处和弊端
好吃:实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。所以,多态的好处,体现在,可以使程序编写的更简单,并有良好的扩展。
弊端:不能访问子类特有的成员。
重写和重载的区别
-
重载:是指允许存在多个同名方法,而这些方法的参数不同。编译器根据方法不同的参数列表,对同名方法的名称做修饰。对于编译器而言,这些方法变成了不同的方法。他们的调用地址在编译期就绑定了。java重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法,所以,对于重载而言,在方法调用之前,编译器就已经确定了要调用的方法,这种称为早绑定或静态绑定
-
而对于多态,只有等到方法调用时,执行器才会确定所要调用的具体的方法,这种称为晚绑定或动态绑定。
-
重载:可以使本类方法的重载,子类也可以重载父类的方法。重载的要求是方法名称相同,参数列表不同(个数不同,类型不同,顺序不同)
-
重写:一定是子类对父类的重写,重写要求方法的声明形式必须完全一致。
引用类型转换
- 只能在继承层次内进行类型转换。
- 在将父类转换成子类之前,应该使用 instanceof进行检查
多态的转型分为向上转型与向下转型两种:
-
向上转型:多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。 当父类引用指向一个子类对象时,便是向上转型。
使用格式:
父类类型 变量名 = new 子类类型();
如:Animal a = new Cat();
-
向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。
使用格式:
子类类型 变量名 = (子类类型) 父类变量名;
如:Cat c =(Cat) a;
为什么要转型
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥 有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子 类特有的方法,必须做向下转型。
instanceof
严格来说 instanceof 是 Java 中的一个双目运算符,由于它是由字母组成的,所以也是 Java 的保留关键字。在 Java 中可以使用 instanceof 关键字判断一个对象是否为一个类(或接口、抽象类、父类)的实例,返回值是boolean类型
语法格式如下所示:
boolean result = obj instanceof Class
其中,obj 是一个对象,Class 表示一个类或接口。obj 是 class 类(或接口)的实例或者子类实例时,结果 result 返回 true,否则返回 false。
1. 声明一个 class 类的对象,判断 obj 是否为 class 类的实例对象(很普遍的一种用法),如以下代码:
Integer integer = new Integer(1);
System.out.println(integer instanceof Integer); // true
2. 声明一个 class 接口实现类的对象 obj,判断 obj 是否为 class 接口实现类的实例对象,如以下代码:
Java 集合中的 List 接口有个典型实现类 ArrayList。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
所以我们可以用 instanceof 运算符判断 ArrayList 类的对象是否属于 List 接口的实例,如果是返回 true,否则返回 false。
ArrayList arrayList = new ArrayList();
System.out.println(arrayList instanceof List); // true
或者反过来也是返回 true
List list = new ArrayList();
System.out.println(list instanceof ArrayList); // true
3. obj 是 class 类的直接或间接子类
我们新建一个父类 Person.class,代码如下:
public class Person {
}
创建 Person 的子类 Man,代码如下:
public class Man extends Person {
}
测试代码如下:
Person p1 = new Person();
Person p2 = new Man();
Man m1 = new Man();
System.out.println(p1 instanceof Man); // false
System.out.println(p2 instanceof Man); // true
System.out.println(m1 instanceof Man); // true
第 4 行代码中,Man 是 Person 的子类,Person 不是 Man 的子类,所以返回结果为 false。
值得注意的是 obj 必须为引用类型,不能是基本类型。例如以下代码:
int i = 0;
System.out.println(i instanceof Integer); // 编译不通过
System.out.println(i instanceof Object); // 编译不通过
所以,instanceof 运算符只能用作对象的判断。
当 obj 为 null 时,直接返回 false,因为 null 没有引用任何对象。
Integer i = 1;
System.out.println(i instanceof null); // false
所以,obj 的类型必须是引用类型或空类型,否则会编译错误。
当 class 为 null 时,会发生编译错误,错误信息如下:
Syntax error on token "null", invalid ReferenceType
所以 class 只能是类或者接口。
编译器会检查 obj 能否转换成右边的 class 类型,如果不能转换则直接报错,如果不能确定类型,则通过编译。这句话有些难理解,下面我们举例说明。
Person p1 = new Person();
System.out.println(p1 instanceof String); // 编译报错
System.out.println(p1 instanceof List); // false
System.out.println(p1 instanceof List<?>); // false
System.out.println(p1 instanceof List<Person>); // 编译报错
上述代码中,Person 的对象 p1 很明显不能转换为 String 对象,那么p1 instanceof String
不能通过编译,但p1 instanceof List
却能通过编译,而instanceof List<Person>
又不能通过编译了。关于这些问题,可以查看Java语言规范Java SE8版寻找答案,如图所示。
可以理解成以下代码:
boolean result;
if (obj == null) {
result = false; // 当obj为null时,直接返回false
} else {
try {
// 判断obj是否可以强制转换为T
T temp = (T) obj;
result = true;
} catch (ClassCastException e) {
result = false;
}
}
在 T 不为 null 和 obj 不为 null 时,如果 obj 可以转换为 T 而不引发异常(ClassCastException),则该表达式值为 true ,否则值为 false 。所以对于上面提出的问题就很好理解了,p1 instanceof String
会编译报错,是因为(String)p1
是不能通过编译的,而(List)p1
可以通过编译。
instanceof 也经常和三目(条件)运算符一起使用,代码如下:
A instanceof B ? A : C
判断 A 是否可以转换为 B ,如果可以转换返回 A ,不可以转换则返回 C。下面通过一个例子来讲解,代码如下:
public class Test {
public Object animalCall(Animal a) {
String tip = "这个动物不是牛!";
// 判断参数a是不是Cow的对象
return a instanceof Cow ? (Cow) a : tip;
}
public static void main(String[] args) {
Sheep sh = new Sheep();
Test test = new Test();
System.out.println(test.animalCall(sh));
}
}
class Animal {
}
class Cow extends Animal {
}
class Sheep extends Animal {
}
以上代码中,我们声明了一个 Animal 类作为父类,Cow 类和 Sheep 类为 Animal 的子类,在 Test 类的 main 函数中创建类 Sheep 的对象作为形参传递到 animalCall 方法中,因为 Sheep 类的对象不能转换为 Cow 类型,所以输出结果为“这个动物不是牛!”。
继承成员变量和继承方法的区别
package cn.lanqiao.oop;
public class FieldMethodTest {
public static void main(String[] args) {
Sub sub = new Sub();
System.out.println(sub.count);//20
sub.display();//20
Base b = sub;
System.out.println(b == sub);//true
System.out.println(b.count);//10
b.display();//20
}
}
class Base{
int count =10;
public void display(){
System.out.println(count);
}
}
class Sub extends Base{
int count = 20;
public void display(){
System.out.println(count);
}
}
若子类重写父类的方法,就意味着子类中定义的方法彻底覆盖了父类中的同名方法,系统将不被坑不父类中的方法转移到子类。
对于实例变量则不存在这样的现象,即使子类定义了与父类完全相同的实例变量 这个实例变量依然不可能覆盖父类中的实例变量。
案例:计算图形面积
定义三个类,父类GeometricObject代表几何形状,子类Circle代表圆形, MyRectangle代表矩形。
定义一个测试类GeometricTest, 编写equalsArea方法测试两个对象的面积是否相等(注意方法的参数类型,利用动态绑定技术),编写displayGeometricObject方法显示对象的面积(注意方法的参数类型,利用动态绑定技术)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XJUDGQIC-1627739917931)(F:\study\笔记\截图\clipboard1.png)]
抽象类
概述
父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了。我们把没有方法主体的方法称为抽象方法。Java语法规定,包含抽象方法 的类就是抽象类。
定义:
- 抽象方法 :用abstract修饰方法,没有方法体的方法。
- 抽象类:用abstract修饰的类,包含抽象方法的类。
abstract使用格式
抽象方法
使用 abstract 关键字修饰方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体。
定义格式:
修饰符 abstract 返回值类型 方法名 (参数列表);//(不能含有({}))
抽象类
如果一个类包含抽象方法,那么该类必须是抽象类。
定义格式:
abstract class 类名字 { }
抽象的使用
继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该父 类的抽象方法,否则,从最初的父类到最终的子类都不能创建对象,失去意义。此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做***实现***方法。
注意事项
-
抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。抽象类不能被实例化
理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。
-
抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。抽象类是用来被继承
理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。
-
抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。可以通过多态来访问其中的成员
理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。
-
抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。( 抽象类和抽象方法的关系)
理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。
{ }
**抽象的使用**
继承抽象类的子类必须重写父类所有的抽象方法。否则,该子类也必须声明为抽象类。最终,必须有子类实现该父 类的抽象方法,否则,从最初的父类到最终的子类都不能创建对象,失去意义。此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做***实现***方法。
## 注意事项
1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。抽象类不能被实例化
> 理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。
2. 抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。抽象类是用来被继承
> 理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。
3. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。可以通过多态来访问其中的成员
> 理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。
4. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。( 抽象类和抽象方法的关系)
> 理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。