继承
继承是一种关系,继承者拥有被继承者的特征关系,一个大群体包含一个小群体,这时我们就可以用到继承
语法
Java中如果一个类继承于另一个类,使用extends关键字 : class A extends B
示例:
public class Animal{
public String name;
public int age;
public String color;
public void eat(){
System.out.println("吃东西");
}
}
//继承关系
public class dog extends Animal{
public void dark(){
System.out.println("狗在汪汪的叫");
}
}
通过继承,子类可以拥有父类所有的属性和方法,上例中Dog类创建的对象具有父类Animal类的特征
Dog dog = new dog();
dog.name;
dog.eat();
dog.dark();
并不会报错
继承的作用
主要作用为使用继承能够对代码进行重用,它也是多态的一种
根据上面的Animal父类 我们可以省略大部分重复代码,因为动物具有许多相同特征
public class Cat extends Animal{
public void catchMouse(){
System.out.println("猫正在抓老鼠");
}
}
Cat类和Dog类一样,都具有名字,年龄,颜色,所以都可以使用一个父类的属性
父类更加通用,抽象,而子类更加具体
继承的三个价值:
- 重用代码,简化代码和工作量,重用能够增加代码的可用价值
- 扩展功能,细化类型,详细描述对象的类型
- 实现对象的多种形态,即多态
继承的注意事项
- 单一继承原则 :子类只允许继承一个父类,不能有多继承
- 单方向继承原则:只允许子类获得父类的非私有成员
- 父类private私有特征子类不能继承!只有public/protected属性方法可以被子类访问
- 所有类的默认父类均为Object,因此都具有Object的一些方法
super关键字
在类中可以使用this关键字代表自己,而对于继承的子类,可以使用super表示父类
public class A{
public int a1;
public void a2(){}
}
public classe B extends A{
public B(){
super(); //调用父类的构造函数
super.a = 100; //父类属性,用this也可调用
super.a2(); //父类方法
}
public void b(){
this.a = 10;
}
}
实际上,继承在Java中子类在使用new创建对象使用时会先把父类new创建出来,子类在调用构造方法的第一行,会先调用父类的构造方法,即隐式调用了父类构造 super() 方法,父类实例化后再子类实例化
public class A{
public A(){
System.out.println("aaa");
}
}
public class B extends A {
public B(){
System.out.println("bbb");
}
}
//测试
B b = new B(); //输出aaa bbb
当然如果父类有多个构造方法,我们可以在子类中运用super改变自动调用的情况
调用关系
由上可知,this 和 super 有相同的作用,super 只有在子类和父类属性或者方法调用冲突的时候才必须使用
假设子类有属性 name,调用该属性时
- 如果 name 是自己没有的,那么就往父类寻找,父类没有继续往上找到为止
- 如果 name 自己有,那么就会调用自己的
- 如果都有,那么就用 super.name 和 this.name 进行区分
- 如果父类有子类没有,用 this.name 和 super.name 都能调用
组合优先于继承
如果一个类中有多个方法可以使用,如果要扩展这个类,有两种方案:继承和组合
如:
class A{
public void a1() { }
public void a2() { }
public void a3() { }
}
// 使用继承
class B1 extends A{
// 自动继承父类方法
// 重写扩展父类方法
@Override
public void a1() {
super.a1();
}
// 添加其他扩展方法
public void b() { }
}
// 使用组合
class B2{
// 依赖于A对象
private A a = new A();
// 通过A对象实现的中介方法
public void a1() {
this.a.a1();
}
// 扩展方法
public void a2() {
this.a.a2();
}
// 添加其他扩展方法
public void b() { }
}
优先使用继承还是组合
- 需要使用多态时优先考虑继承
- 结构稳定,继承关系层次简单可以使用
组合与继承的区别
组合 | 继承 |
---|---|
has-a 的关系 | is-a 的关系 |
运行期决定 | 编译器决定 |
不破坏封装 | 破坏封装,子类依赖于父类 |
支持扩展,随意增加 | 只能单继承,包含所有父类方法 |
动态选择组合类方法 | 全盘复用父类方法 |
多态
引用类型转换
基本数据类型中
从小空间到大空间:
- 类型必须兼容,除Boolean以外都兼容
- 自动转换(隐式转换)
- 发生在进行赋值的时候
从大空间到小空间
- 强制转换(显示转换)
- 可能产生精度丢失的情况
引用数据类型中
在继承过程中,有父类和子类,是大空间包含小空间的情况
- 向上转型:也叫隐式转换,自动转换,从子类小群体对象到父类大群体对象是自动类型转换,比如
Animal a = new Cat()
- 反之为向下转型 如
Cat a = new Animal()
,会报错
class Cat extends Animal{
public void cry(){
System.out.println("猫在叫:喵喵");
}
public void catch(){
System.out.println("猫能吃老鼠");
}
}
// 入口测试
Animal a = new Cat();
a.catch(); // 编译失败
引用数据类型强制类型转换方法如下
Cat cat = new Cat();
Animal animal = new Cat(); // 向上转型
((Cat)animal).catch(); // 向下转型:强制转换
总结:
父类 对象 = new 子类()
编译成功子类 对象 = new 父类()
编译失败子类 对象 = (子类)父类引用子类
强制转换父类 对象 = new 子类(); 对象.方法();
访问的是子类的方法(如果有重写);
多态的概念
多态是同一个行为具有多个不同表现形式或形态的能力,多应用于数组、方法的重写与重载中,使用多态让程序更具有扩展性
相同的行为,由于不同的参数、不同的执行者,导致执行不同的代码,得到的结果也不相同,即多态形式。
Animal a = new Dog();
a.cry();
a = new Dog();
a.cry();
一共有两种方法可以实现多态:
- 方法重载:也叫 Overload 行为相同,由于参数不同,得到不同的结果。也称静态多态,在编译期就能知道结果,方法签名已知
- 方法重写:也叫 Override 行为相同,由于执行者不同,得到不同的结果。也称动态多态,只有在运行时才知道结果
重载在于同一个类,重写涉及多个类
多态的实现种类
第一种多态:方法重载产生的多态
public class Pig{
public void pk(){
System.out.println("猪在pk");
}
}
public class WhitePig extends Pig{
public void pk(){
System.out.println("白猪用锤子打");
}
}
public class RedPig extends Pig{
public void pk(){
System.out.println("红猪用刀打");
}
}
// 测试类,方法重载
public class Game{
public void control(WhitePig p){
p.pk();
}
public void control(RedPig p){
p.pk();
}
public static void main(String[] args){
Game game = new Game();
Pig p1 = new WhitePig();
Pig p2 = new RedPig();
game.control(p1);
game.control(p2);
}
}
第二种多态:向上转型和方法重写产生的多态
第三种多态:数组产生的多态
String[] array = new String[] {new A(), new B(), new C()} // 对象数组
第四种多态:方法参数和重写实现多态
(方法重载实现的多态过于麻烦,可以通过父类对象作为参数对方法进行重新设计)
public class Game{
// 使用父类作为参数
public void control(Pig p){
p.pk();
}
}
第五种多态:由方法返回值不同产生的多态
public Pig createPig(){
Pig p = null;
int rand = (new Random()).nextInt(3);
switch(rand) {
case 0:
p = new WhitePig();
break;
case 1:
p = new BlackPig();
break;
case 2:
p = new RedPig();
break;
}
return p;
}
方法的重写
方法重写发生在不同的类中,比如子类不满足于父类方法的实现方式,允许重写父类的方法,实现不一样的需求。
示例:
class Animal{
public void cry(){
System.out.println("动物在叫");
}
}
class Dog extends Animal{
public void cry(){
System.out.println("狗在叫:旺旺");
}
}
class Cat extends Animal{
public void cry(){
System.out.println("猫在叫:喵喵");
}
}
// 测试
Animal animal = new Animal();
animal.cry(); // 动物在叫
Dog dog = new Dog();
dog.cry(); // 狗在叫:旺旺
Cat cat = new Cat();
cat.cry(); // 猫在叫:喵喵
上述代码中cry()方法则为重写
- 必须有继承关系
- 方法名必须完全一样
- 发生向上转型,父类型的变量引用子类对象
Override注解
我们重写父类的方法时,一般都在方法前面添加@Override
,如:
public class B extends A{
@Override
public void f1(){}
@Override
public void f2(){}
}
重载与重写的区别
重载与重写是为了实现面向对象中的多态
重载 | 重写 | |
---|---|---|
位置 | 发生在同一个类中 | 发生在两个有继承关系的类中 |
方法名 | 方法同名 | 方法同名 |
参数 | 参数列表必须不同 | 参数列表必须相同 |
返回值 | 返回类型可以不同,可以相同 | 返回类型必须相同(或者继承关系) |
静态方法 | 支持重载 | 不能重写 |
严格来讲重载的各个方法是完全不同的,重载的方法最终根据参数区分,即给我什么就干什么;
而重写实际上是子类重写了父类的方法,重写的方法最终根据当前所操作的对象来区分,即让谁去做就做出什么结果。
方法重载调用时
- 如果没有改匹配的类型则自动向上转型
- 以类型定义为准,非实际类型
- 类型匹配直接调用