Java面向对象(继承、多态、抽象类、接口)
面向对象三大特性:封装、继承、多态。
继承
- 现实生活中事物和事物之间可能存在或多或少的联系,例如:
- 兔子、山羊、奶牛属于食草动物,狮子、老虎、豹子属于食肉动物。
- 食草动物和食肉动物都属于动物。
- 因此继承需要符合
is-a
的关系,子类是父类的具体体现。
为什么要使用继承
- 若我们没有继承,那定义如下两个类:
Dog
和Cat
。会存在大量重复的代码,两个类都有属性name
和age
,都有eat()
和sleep()
行为。
- 我们可以将这些共同的属性和行为整合起来,定义一个动物类父类,让狗和猫继承动物这个父类。
继承的语法
- 子类使用
extends
关键字来继承父类。
class A extends B {}
由此,上面的设计代码实现:
class Animal { // 动物类 父类
public String name;
public int age;
public void eat() {
System.out.println(name + "在吃饭");
}
public void sleep() {
System.out.println(name + "在睡觉");
}
}
class Dog extends Animal{ // 子类继承父类
public void bark() {
System.out.println(name + "在汪汪叫");
}
}
class Cat extends Animal {
public void mew() {
System.out.println(name + "在喵喵叫");
}
}
继承的类型
- 还有一种Java不支持的继承类型。
访问父类成员
子类访问父类成员变量
访问不同名成员变量
class Base {
int a;
int b;
}
class Sub extends Base {
int c;
public void method() {
a = 10;
b = 20;
c = 30;
}
}
public class Test {
public static void main(String[] args) {
Sub s = new Sub();
s.method();
System.out.println(s.a);
System.out.println(s.b);
System.out.println(s.c);
}
}
// 输出结果:
// 10
// 20
// 30
- 只要父类的成员变量不是
private
修饰的,且子类、父类在一个包下,子类可以直接访问父类的成员变量。
访问同名成员变量
class Base {
int a;
int b;
int c = 99;
}
class Sub extends Base {
int c;
}
public class Test {
public static void main(String[] args) {
Sub s = new Sub();
System.out.println(s.c);
}
}
// 输出结果:
// 0
- 若子类的成员变量和父类成员变量重名,则访问子类的成员变量。若想访问父类同名成员变量,需使用
super
引用来访问。
class Base {
int a;
int b;
int c = 99;
}
class Sub extends Base {
int c;
public void method() {
System.out.println(super.c);
}
}
public class Test {
public static void main(String[] args) {
Sub s = new Sub();
s.method();
}
}
// 输出结果:
// 99
成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找。
子类访问父类成员方法
访问不同名成员方法
class Base {
public void method1() {
System.out.println("Base中的method1方法");
}
}
class Sub extends Base {
public void method2() {
System.out.println("Sub中的method2方法");
}
public void method3() {
method1();
method2();
}
}
public class Test {
public static void main(String[] args) {
Sub s = new Sub();
s.method3();
}
}
// 输出结果:
// Base中的method1方法
// Sub中的method2方法
- 只要父类的成员方法不是
private
修饰,子类就可以直接访问。
访问同名成员方法
class Base {
public void method1() {
System.out.println("Base中的method1方法");
}
}
class Sub extends Base {
public void method1() {
System.out.println("Sub中的method2方法");
}
public void method3() {
method1();
}
}
public class Test {
public static void main(String[] args) {
Sub s = new Sub();
s.method3();
}
}
// 输出结果:
// Sub中的method2方法
- 若子类的成员方法和父类成员方法重名,则访问子类的成员方法。若想访问父类同名成员方法,需使用
super
引用来访问。
class Base {
public void method1() {
System.out.println("Base中的method1方法");
}
}
class Sub extends Base {
public void method1() {
System.out.println("Sub中的method2方法");
}
public void method3() {
method1();
super.method1();
}
}
public class Test {
public static void main(String[] args) {
Sub s = new Sub();
s.method3();
}
}
// 输出结果:
// Sub中的method2方法
// Base中的method1方法
通过子类对象访问父类与子类中不同名方法时,优先在子类中找,找到则访问,否则在父类中找,找到则访问,否则编译报错。
通过子类对象访问父类与子类同名方法时,如果父类和子类同名方法的参数列表不同(重载),根据调用方法适传递的参数选择合适的方法访问,如果没有则报错。
构造方法
- 当我们写这样一段代码时,是会报错的。
class Animal {
private String name;
private int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
class Dog extends Animal{
}
- 子类是不能继承父类的构造方法的,子类只是调用父类隐式或显式的构造方法。因此,才进行子类的构造前要对父类完成构造。
- 如上代码,父类创建了构造方法
public Animal(String name, int age)
,不会自动生成隐式的构造方法public Animal()
。因此,若子类不提供任何构造方法,父类无法完成构造,就会报错。 - 如上代码,子类必须提供
public Dog(String name, int age)
构造方法,使用super()
调用父类构造方法。
class Animal {
private String name;
private int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
class Dog extends Animal{
public Dog(String name, int age) {
super(name, age);
}
}
若父类未自行提供构造方法,系统提供隐式构造方法。
class Animal {
private String name;
private int age;
// 以下为隐式构造方法
public Animal() {}
}
子类可以不写构造方法,系统会默认提供隐式构造方法public Dog()
帮助完成父类构造,如:
class Dog extends Animal{
// 以下为隐式构造方法
public Dog() {
super();
}
}
构造方法调用验证
class Animal {
private String name;
private int age;
public Animal() {
System.out.println("Animal类无参构造方法");
}
public Animal(String name) {
System.out.println("Animal类name参数构造方法");
}
public Animal(String name, int age) {
System.out.println("Animal类两个参数构造方法");
}
}
class Dog extends Animal{
public Dog() {
System.out.println("Dog类无参构造方法");
}
public Dog(String name) {
super(name);
System.out.println("Dog类name参数构造方法");
}
public Dog(String name, int age) {
super(name, age);
System.out.println("Dog类两个参数构造方法");
}
}
public class AnimalTest {
public static void main(String[] args) {
Dog d1 = new Dog();
Dog d2 = new Dog("旺财");
Dog d3 = new Dog("旺财", 3);
}
}
输出结果:
Animal类无参构造方法
Dog类无参构造方法
Animal类name参数构造方法
Dog类name参数构造方法
Animal类两个参数构造方法
Dog类两个参数构造方法
- 以上代码可以看出创建子类对象前会调用父类适配的构造方法。
代码调试演示:
继承中代码块的执行顺序
在普通类当中,静态代码块在类加载时执行,因此最先执行。构造代码块在创建对象时执行。
因此我们可以猜测在有继承关系中,代码块的执行顺序是:
- 父类的静态代码块。
- 子类的静态代码块。
- 父类的构造代码块。
- 子类的构造代码块。
代码验证:
class Animal {
private String name;
private int age;
static {
System.out.println("父类的静态代码块");
}
{
System.out.println("父类的构造代码块");
}
public Animal() {
System.out.println("Animal类无参构造方法");
}
public Animal(String name) {
System.out.println("Animal类name参数构造方法");
}
public Animal(String name, int age) {
System.out.println("Animal类两个参数构造方法");
}
}
class Dog extends Animal{
static {
System.out.println("子类的静态代码块");
}
{
System.out.println("子类的构造代码块");
}
public Dog() {
System.out.println("Dog类无参构造方法");
}
public Dog(String name) {
System.out.println("Dog类name参数构造方法");
}
public Dog(String name, int age) {
System.out.println("Dog类两个参数构造方法");
}
}
public class AnimalTest {
public static void main(String[] args) {
Dog d1 = new Dog();
}
}
输出结果:
父类的静态代码块
子类的静态代码块
父类的构造代码块
Animal类无参构造方法
子类的构造代码块
Dog类无参构造方法
代码调试演示:
执行顺序结论:
父类静态代码块->子类静态代码块->父类构造代码块->父类构造方法->子类构造代码块->子类构造方法
关键字
this
this
关键字依旧指代本类对象,this()
调用本类构造方法。
注:
this()
必须放在构造方法中的第一行。class Animal { private String name; private int age; public Animal() { age = 0; this("旺财"); // error } public Animal(String name) { this.name = name; } }
super
super
关键字指代父类,用法和this
类似。
class Animal {
private String name;
private int age;
public void func() {
System.out.println("父类的func方法");
}
}
class Dog extends Animal{
public void func() {
System.out.println("子类的func方法");
}
public void func1() {
func();
super.func();
}
}
public class AnimalTest {
public static void main(String[] args) {
Dog d1 = new Dog();
d1.func1();
}
}
输出结果:
子类的func方法
父类的func方法
注:和
this()
一样,子类构造方法要想调用父类的构造方法,使用super()
也需要放在构造方法的第一行。class Animal { public String name; public int age; public Animal() { } public Animal(String name, int age) { this.name = name; this.age = age; } } class Dog extends Animal{ public Dog() { name = "旺财"; age = 0; super(); // error } }
final
final关键字修饰变量
final
关键字修饰变量,不能再改变变量中储存的值。
public class FinalTest {
public static void main(String[] args) {
final int a = 10;
a = 20; // error
}
}
- 因此我们常称被
final
修饰的变量就是常量。
注:
final
修饰的引用数据类型,引用不能再指向其他对象,但是对象内的属性值是可以改变的。public class FinalTest { public static void main(String[] args) { final int[] arr = {1,2,3,4,5}; arr[0] = 100; for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } } // 输出结果:100 2 3 4 5
但若代码时这样:
public class FinalTest { public static void main(String[] args) { final int[] arr = {1,2,3,4,5}; arr[0] = 100; for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); arr = new int[10]; // error } }
就会报错。
final关键字修饰类
final
关键字修饰类,该类不能被继承。- 一个典型的例子就是
String
类,当我们打开原码,就会看到:
String
这个类是无法被继承的。
final class Animal {
public String name;
public int age;
}
class Dog extends Animal{}
final关键字修饰方法
final
关键字修饰方法,该方法不能被子类重写。
class Animal {
public String name;
public int age;
public final void func() {
System.out.println("final方法");
}
}
class Dog extends Animal{
public void func() {
System.out.println("子类final方法");
}
}
// error: java: com.max.classes.Dog中的func()无法覆盖com.max.classes.Animal中的func()被覆盖的方法为final
protected
- 被
private
修饰的,可以在同一个包中同一个类中访问。class Base { private int a; public void baseFun() { System.out.println(a); // 可以访问 } }
- 被
default
修饰的,可以在同一个包中不同类访问。class Base { int a; public void baseFun() { System.out.println(a); } } public class BaseTest { public static void main(String[] args) { Base b = new Base(); System.out.println(b.a); // 可以访问 } }
public
范围很广,在任意位置都可以访问,不同包的非子类也可以访问。package com.max.demo1; public class Base { public int a; }
package com.max.demo2; import com.max.demo1.Base; public class BaseTest { public static void main(String[] args) { Base b = new Base(); System.out.println(b.a); // 可以访问 } }
- 被
protected
修饰的,可以在不同包中的子类访问。
package com.max.demo1;
public class Base {
public int a;
}
package com.max.demo2;
import com.max.demo1.Base;
class Sub extends Base {
public void func() {
System.out.println(super.a); // 可以访问
}
}
多态
多态就是同一个行为的不同表现形式。
多态存在必要条件
- 继承。
- 方法重写。
- 父类引用指向子类对象。
方法重写
重写是子类对父类可以访问的方法进行重新实现,重写的方法名、形参列表、返回值都不能改变。
class Animal { public void run() { System.out.println("动物在跑"); } } class Dog extends Animal{ public void run() { System.out.println("狗在跑"); } } public class AnimalTest { public static void main(String[] args) { Animal a = new Dog(); a.run(); } } // 输出结果:狗在跑
虽然引用
a
的类型是Animal
,但它指向的是一个Dog
对象,若Animal
成员方法被重写,则编译器会动态绑定Dog
中重写的Animal
成员方法。
重写的规则
- 参数列表必须相同。
- 返回值类型可以不同,但必须是父类返回值的子类。
- 重写的方法访问权限不能比父类成员方法的访问权限更低。
class Animal {
public void run() {
System.out.println("动物在跑");
}
}
class Dog extends Animal{
protected void run() { // error
System.out.println("狗在跑");
}
}
- 父类的成员方法只能被它的子类重写。
final
修饰的方法不能被重写。static
修饰的方法不能被重写,但是能够被再次声明。- 构造方法不能被重写。
方法重载
重载是在一个类中,方法名相同,参数列表不同(顺序不同、类型不同、个数不同)。
同一个类中多个合法的构造方法就构成方法重载。
重载和重写的区别
区别点 | 重载 | 重写 |
---|---|---|
参数列表 | 必须不同 | 必须相同 |
返回类型 | 可以不同 | 必须相同 |
异常 | 可以不同 | 可以减少或删除,但不能抛出更多异常 |
访问 | 可以不同 | 可以放大,但一定不能缩小 |
向上转型
当我们有多个不同动物类:
class Animal {
public void run() {
System.out.println("动物在跑");
}
}
class Dog extends Animal{
public void run() {
System.out.println("狗在跑");
}
}
class Cat extends Animal {
public void run() {
System.out.println("猫在跑");
}
}
class Bird extends Animal {
public void run() {
System.out.println("鸟在飞");
}
}
向要调用一个方法,传入对象,调用该对象的run()
方法。在没有多态的情况下,我们需要重载多个方法,比如:
public class AnimalTest {
public static void func(Dog d) {
d.run();
}
public static void func(Cat c) {
c.run();
}
public static void func(Bird b) {
b.run();
}
public static void func(Animal a) {
a.run();
}
public static void main(String[] args) {}
}
这样未免太过麻烦。
在我们使用向上转型,就只需要写一个方法就可以了。
向上转型:创建一个子类对象,将其当成父类对象来使用。
public class AnimalTest {
public static void func(Animal a) {
a.run();
}
public static void main(String[] args) {
Dog d = new Dog();
Cat c = new Cat();
Bird b = new Bird();
func(d);
func(c);
func(b);
}
}
// 输出结果:
// 狗在跑
// 猫在跑
// 鸟在飞
向下转型
- 子类对象通过向上转型当成父类后,是无法调用子类特有的方法的。若想调用自己特有的方法,需要强制类型转换成子类对象,即向下转型。
向下转型是非常危险的,比如:狗类和猫类都继承于动物类。
Dog
类对象向上转型为Animal
类,再向下转型为Cat
类想调用mew()
方法是无法办到的。
class Animal {
public void run() {
System.out.println("动物在跑");
}
}
class Dog extends Animal{
public void bark() {
System.out.println("狗在叫");
}
}
class Cat extends Animal {
public void mew() {
System.out.println("猫在叫");
}
}
public class AnimalTest {
public static void func(Animal a) {
Cat c = (Cat)a;
c.mew();
}
public static void main(String[] args) {
func(new Dog());
}
}
// Exception in thread "main" java.lang.ClassCastException: com.max.classes.Dog cannot be cast to com.max.classes.Cat
// 造成类型转换异常
因此我们需要使用关键字instanceof
来判断是否属于该子类。
class Animal {
public void run() {
System.out.println("动物在跑");
}
}
class Dog extends Animal{
public void bark() {
System.out.println("狗在叫");
}
}
class Cat extends Animal {
public void mew() {
System.out.println("猫在叫");
}
}
public class AnimalTest {
public static void func(Animal a) {
if(a instanceof Dog) {
((Dog) a).bark();
} else if(a instanceof Cat) {
((Cat) a).mew();
}
}
public static void main(String[] args) {
func(new Dog());
}
}
// 输出结果:狗在叫
多态的优缺点
- 优点:
- 消除类型之间的耦合关系。
- 扩展能力强。
- 缺点:
- 代码运行效率降低。
抽象类
当我们定义一个动物类,编写一个吃饭的成员方法,但因为子类都有自己的吃饭动作,实现动物类的吃饭动作显得没有必要。
因此可以把动物类定义为一个抽象类,吃饭成员方法定义为抽象方法,等待被子类重写。
- 使用
abstract
关键字来修饰抽象类或抽象方法。
注:抽象方法必须在抽象类当中,抽象类中可以有非抽象的成员方法。
abstract class Animal {
public abstract void eat();
}
class Dog extends Animal{
public void bark() {
System.out.println("狗在叫");
}
@Override
public void eat() {
System.out.println("狗在啃骨头");
}
}
class Cat extends Animal {
public void mew() {
System.out.println("猫在叫");
}
@Override
public void eat() {
System.out.println("猫在吃小鱼干");
}
}
抽象类的特性
- 抽象类不能实例化对象。
abstract class Animal {
public abstract void eat();
}
public class AnimalTest {
public static void main(String[] args) {
Animal a = new Animal();
}
}
// java: com.max.classes.Animal是抽象的; 无法实例化
- 抽象方法不能用
private
修饰。 - 抽象方法不能被
final
和static
修饰。final
修饰的成员方法不能被重写,违背了抽象方法的特点。static
修饰的方法是类方法,为满足给类调用,类方法必须有方法体,这样就不是抽象方法了。
- 抽象类必须被继承,且子类必须重写抽象方法。
接口
接口可类比于提供规范的载体。只在于提供规范,不在乎如何实现。
语法
public interface 接口名 {
规范;
}
public interface Animal {
public abstract void eat();
public abstract void sleep();
}
上述代码就是一个Animal
接口。
当我们使用IDEA
编写接口方法是,public abstract
会变成浅色。
这表明接口中的方法,默认是public abstract
修饰的,因此可以不写。
接口的实现
当类实现接口时,需要重写接口中的所有方法。否则类必须声明为抽象类。
使用
implements
实现接口。
public interface Animal {
void eat();
void sleep();
}
class Dog implements Animal {
@Override
public void eat() {
System.out.println("狗啃骨头");
}
@Override
public void sleep() {
System.out.println("狗睡觉");
}
}
接口的特性
- 不能实例化接口对象。
- 接口中的方法是不能实现的,只能在实现类中实现。
- 重写接口方法时,不能使用默认访问权限,只能是
public
修饰的,因为接口方法是public abstract
。 - 接口中可以有变量,默认是
public static final
修饰的。
- 接口中不能有静态/构造代码块和构造方法。
- jdk8中,接口还可以包含
default
方法。
public interface Animal {
void eat();
void sleep();
default void sound() {
System.out.println("动物发出声音");
}
}
实现多个接口和接口的继承
Java中不支持多继承,但是可以实现多个接口。
interface A {
void a();
}
interface B {
void b();
}
class C implements A, B {
@Override
public void a() {
System.out.println("a");
}
@Override
public void b() {
System.out.println("b");
}
}
-
实现多个接口的类,必须重写实现的多个接口的所有方法。否则就必须定义为抽象类。
-
接口之间也可以使用
extends
继承,以达到复用的效果。
interface A {
void a();
}
interface B extends A{
void b();
}
抽象类和接口的区别
- 抽象类中的方法可以实现,接口中的方法不能实现(jdk8引入的
default
方法除外)。 - 抽象类中的成员变量权限可以是各种类型的,接口中的变量只能是
public static final
修饰的常量。 - 接口中不能有静态/构造代码块,也不能有静态方法;抽象类中都可以有。
- 一个类只能继承一个抽象类,但可以实现多个接口。