面向对象程序设计(OOP)语言有三个重要特征,分别是:封装、继承和多态
下面将详细讲述这三大特性。
封装
封装,就是将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
在Java中,主要通过类和访问权限来实现封装
访问权限 \ 访问修饰限定符 | private | protected | public |
同一包中的同一类 | √ | √ | √ |
同一包中的不同类 | √ | √ | |
不同包中的子类 | √ | √ | |
不同包中的非子类 | √ |
访问权限除了可以限定类中成员的可见性,还可以控制类的可见性。
一般情况下,成员变量设置为private,成员方法设置为public
继承
继承,是面向对象程序设计中是代码可以复用的最重要的手段,它允许程序员在保持原有特性的基础上进行扩展,增加新功能,从而产生新的类,称为派生类,或者被称为子类。被继承的类被称为父类或基类。
【注意】:
- 子类会将父类中的成员变量或者成员方法继承到子类中
- 子类继承父类之后,要添加自己特有的成员,体现出与父类的不同,否则就没有必要继承了
- 访问修饰限定符只能决定其访问权限,而不能决定其是否能被继承
- Java不支持多继承,可以使用接口来解决多继承的问题
父类成员访问
子类访问父类的成员变量
子类和父类不存在同名的成员变量
class Base {
int a;
int b;
}
class Derived extends Base{
int c;
public void method(){
a = 10; // 访问从父类中继承下来的a
b = 20; // 访问从父类中继承下来的b
c = 30; // 访问子类自己的c
}
}
子类中有和父类同名的成员变量
优先访问子类中的同名成员变量
在子类方法中或者通过子类对象访问成员时:
- 如果访问的成员变量子类中有,优先访问子类的成员变量
- 如果访问的成员变量子类中无,则访问父类继承下来的。如果父类中也没有定义,则编译失败
- 如果访问的成员变量与父类中成员变量同名,则优先访问自己的
总的来说:成员变量的访问遵循就近原则。
子类访问父类的成员方法
与访问父类中的成员变量的方法类似:
- 通过子类对象访问父类与子类中不同名的方法时,优先在子类中找,找到则访问;否则在父类中找,找到则访问,否则编译报错。
- 通过派生类对象访问父类与子类同名方法时,如果父类与子类的同名方法的参数列表不同,根据调用方法传递的参数选择合适的方法访问,没有则报错。
如果要在子类方法中访问父类的成员,可以使用super关键字
//super 的三个用法
super.成员变量
super.成员方法
super() // 调用父类的构造方法
【注意】:只能在非静态方法中使用
super和this的异同点
this.成员变量
this.成员方法
this() //构造方法
super.成员变量
super.成员方法
super() // 调用父类的构造方法
【相同点】
- 都是Java中的关键字
- 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
- 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
【不同点】
- this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成员的引用
- 在非静态成员方法中,this用来访问本类的方法和属性,super用来访问从父类继承下来的方法和属性
- 在构造方法中:this()用于调用本类构造方法,super()用于调用父类构造方法,两种调用不能同时在构造方法中出现
- 构造方法中一定会存在super()的调用,即使用户没有写编译器也会增加,但是this()用户不写则没有
子类构造方法
子类对象构造时,需要先调用父类构造方法,然后执行子类的构造方法(即子类在构造完成之前,要先帮助父类进行初始化,然后再将自己新增加的成员初始化)
【注意】:
- 若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用父类构造方法
- 如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败
- 在子类构造方法中,super()调用父类构造时,必须是子类构造函数中第一条语句
super()只能在子类构造方法中出现一次,并且不能和this同时出现
代码块
代码块的执行顺序:
- 父类静态代码块优先于子类静态代码块执行,且是最早执行
- 父类实例代码块和父类构造方法紧接着执行
- 子类的实例代码块和子类构造方法紧接着再执行
- 第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
final关键字
final关键字可以用来修饰成员变量、成员方法以及类。
- 修饰变量或字段,表示常量(即不能修改)
- 修饰类:表示此类不能被继承
- 修饰方法:表示该方法不能被重写
多态
多态,通俗来说,即多种形态。展开来说就是当完成某个行为时,不同的对象会展现不同的状态
重写
重写,也成为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的实现过程进行重新编写, 返回值和形参都不能改变。
方法重写的规则
- 子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名 (参数列表) 要完全一致
- 被重写的方法返回值类型可以不同,但是必须是具有父子关系的
- 访问权限不能比父类中被重写的方法的访问权限更低。
- 父类中被static、private修饰的方法、构造方法都不能被重写。
- 重写的方法, 可以使用 @Override 注解来显式指定. 有了这个注解能帮我们进行一些合法性校验
重载与重写的区别
区别点 | 重写 | 重载 |
参数列表 | 一定不能修改 | 必须修改 |
返回类型 | 一定不能修改(除非有父子关系) | 可以修改 |
访问限定符 | 不能比被重写方法的访问权限更低 | 可以修改 |
方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现
重写的设计原则
对于已经投入使用的类,尽量不要进行修改。而是重新定义一个类,来重复利用其中共性的内容,并且添加新的或改动的内容
动态绑定与静态绑定
静态绑定
静态绑定:也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表函数重载。
动态绑定
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用那个类的方法。
在java中要实现多态,必须要满足如下几个条件,缺一不可:
- 必须在继承体系下
- 子类必须要对父类中方法进行重写(子类和父类必须有同名的覆盖/重写方法)
- 通过父类的引用调用重写的方法
完成以上三部分,会发生动态绑定。动态绑定是多态的基础
向上转型与向下转型
向上转型
向上转型:实际就是创建一个子类对象,将其当成父类对象来使用。
语法格式:父类类型 对象名 = new 子类类型();
常见的3个可以发生向上转型的时机:
-
直接赋值
-
方法的参数,传参的时候进行向上转型
-
返回值 向上转型
优点:
让代码实现更简单灵活。
缺陷:
不能调用到子类特有的方法
向下转型
将父类引用再还原为子类对象即可,即向下转换。
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常。Java中为了提高向下转型的安全性,引入了 instanceof ,如果该表达式为true,则可以安全转换。
class Animal {
public String name;
public int age;
public Animal(String name,int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(this.name+"正在吃饭");
}
}
class Dog extends Animal {
public Dog(String name,int age) {
super(name,age);
}
public void bark() {
System.out.println(this.name+"汪汪");
}
/**
* 如果在继承关系上,满足:
* 1.方法的返回值一样
* 2.方法名一样
* 3.方法的参数列表一样
* 那么就说这两个方法之间的关系是重写
*/
@Override
public void eat() { //重写
System.out.println(this.name+"正在吃狗粮");
}
}
class Cat extends Animal {
public Cat(String name,int age) {
super(name,age);
}
public void bark() {
System.out.println(this.name+"喵喵");
}
@Override
public void eat() { //重写
System.out.println(this.name+"正在吃猫粮");
}
}
/**
* 常见的可以发生向上转型的3个时机:
* 1.直接赋值
* 2.方法的参数,传参的时候进行向上赋值
* 3.返回值,向上转型
*/
public class Test {
public static void main(String[] args) {
Animal animal = new Dog("yuanyuan",2);
//animal.bark();//向上转型的缺点:不能调用到子类特有的方法。
Dog dog = (Dog) animal; //向下转型
dog.bark();//可以成功但不安全
//如果animal引用的对象是Cat对象的实例 ,如果安全则是true
if (animal instanceof Cat) {
Cat cat = (Cat) animal;
cat.bark();
} else {
System.out.println("123123");
}
}
public static void eatFun(Animal animal) {
animal.eat();
//当父类引用的子类对象不一样时,调用这个重写的方法所表现出来的行为是不一样的
//这种思想就叫做多态
}
public static void main5(String[] args) {
Dog dog = new Dog("远远",2);
eatFun(dog);
Cat cat = new Cat("咪咪",2);
eatFun(cat);
}
public static void main4(String[] args) {
Animal animal = new Dog("远远",2);
animal.eat();
Animal animal1 = new Cat("咪咪",2);
animal.eat();
}
public static void func1(Animal animal) {
/* 2.方法的参数,传参的时候进行向上赋值*/
}
public static Animal func2() {
/*3.返回值,向上转型*/
Dog dog = new Dog("huahua",10);
return dog;
}
public static void main3(String[] args) {
Dog dog = new Dog("huahua",10);
func1(dog); //这也是向上转型
func2(); //这也是向上转型
}
public static void main2(String[] args) {
/*1.直接赋值*/
/*Dog dog = new Dog("Tom",10);
//animal这个引用指向了dog这个引用所指向的对象
Animal animal = dog;*/
//合并为
Animal animal = new Dog("Tom",10); //向上转型
animal.eat(); //重写后,调用子类的eat
//animal.bark(); //不能调用
}
public static void main1(String[] args) {
Dog dog = new Dog("abc",10);
dog.eat();
dog.bark();
System.out.println();
/*Animal animal = new Animal("huahua",10);
animal.eat();
//通过父类引用,只能调用父类自己特有的成员方法或者成员变量
*/
}
}
多态的优缺点
【优点】
- 能够降低代码的“圈复杂度”,避免使用大量的if-else
- 可扩展能力更强
【缺点】
- 代码的运行效率降低