前言
OOP 语言:也就是面向对象编程。
面向对象的语言有三大特性:封装、继承、多态。三大特性是面向对象编程的核心。下面就来介绍一下面向对象的三大特性。
一、封装
1、封装的概念
在我们写代码的时候经常会涉及两种角色:类的实现者和类的调用者
封装的本质就是让类的调用者不必太多的了解类的实现者是如何实现类的,把属性和动作隐藏,只提供响应的方法来调用即可,只要知道如何使用类就行了。当类的实现者把内部的逻辑发生变化时,类的调用者根本不用因此而修改方法。这样就降低了类使用者的学习和使用成本,从而降低了复杂度,也保证了代码的安全性。
2、private实现封装
private访问限制修饰符,被它修饰的字段或者方法就只能在当前类中使用。
如果我们直接使用public修饰字段
class People{
public String name;
public int age;
}
public class Test {
public static void main(String[] args) {
People people = new People();
people.name = "小明";
people.age = 18;
System.out.println("姓名:"+people.name+" 年龄:"+people.age);
}
}
运行结果:
这样的代码必须要了解People这个类内部的实现,才能够使用这个类,学习成本较高。而且一旦类的实现者把name这两个字段改成myName,外部就无法调用了,那么类的调用者就需要大量的修改代码,维护成本就非常高了。
使用private封装属性,并提供方法public方法供类的调用者使用。
class People{
private String name;
private int age;
public void show() {
System.out.println("姓名:"+name+" 年龄:"+age);
}
}
public class Test {
public static void main(String[] args) {
People people = new People();
people.show();
}
}
那么问题来了,我们前面说过 private 修饰的字段只能在当前类中使用。也就是说现在我们访问不到了name和age了。这就得用到 ger 和 set 方法了。
3、getter和setter方法
当我们用private修饰字段后,这个字段就无法被直接使用了。
这个时候就用到了get和set方法了。
class People{
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void show() {
System.out.println("姓名:"+name+" 年龄:"+age);
}
}
public class Test {
public static void main(String[] args) {
People people = new People();
people.setName("小明");
people.setAge(18);
people.show();
}
}
运行结果:
getName 即为 getter 方法, 表示获取这个成员的值.
setName 即为 setter 方法, 表示设置这个成员的值
不是所有的字段都一定要提供 setter / getter 方法, 而是要根据实际情况决定提供哪种方法。
在 IDEA中快速生成 get 和 set 方法
Alt+Insert 键或者点鼠标右建找到Generate
4、封装的好处
- 提高了数据的安全性
- 别人不能够通过变量名来修改某个私有的成员属性
- 操作简单
- 封装后,类的调用者在使用的时候,只需调用方法即可。
- 隐藏了实现
代码中创建的类。主要是为了抽象现实中的一些事物(包含属性和方法)
有的时候客观事物之间就存在一些关联关系,那么在表示成类和对象的时候也会存在一定的关联。
来看一段代码:
class Animal {
public String name;
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
class Dog {
public String name;
public void eat() {
System.out.println(this.name+"吃东西");
}
}
class Bird {
public String name;
public void eat() {
System.out.println(this.name+"吃东西");
}
public void fly() {
System.out.println(this.name+"起飞");
}
}
这个代码我们发现其中存在了大量的冗余代码.
仔细分析, 我们发现 Animal 和 Cat 以及 Bird 这几个类中存在一定的关联关系。
- 这三个类都有相同的eat方法
- 这三个类都有一个name属性
- 从逻辑上讲,Cat和Bird都是一种Animal(is - a语义)。
此时我们就可以让Cat和Bird分别继承Animal类来达到代码重用的效果。
2、extends实现继承
基本语法
class 子类 extends 父类 {
}
- 使用 extends 指定父类.
- Java不同于C++/Python,JAVA中一个子类只能继承一个父类(单继承)
- 子类会继承父类的所有public 的字段和方法.
- 对于父类的 private 的字段和方法, 子类中是无法访问的.
- 子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用
我们再把上面的代码修改一下,用extends关键字实现继承,此时我们让Cat和Bird继承自Animal类,那么Cat在定义的时候就不必再写name字段和eat方法。
class Animal {
public String name;
public void eat() {
System.out.println(this.name + " 正在吃");
}
}
class Dog extends Animal {
}
class Bird extends Animal{
public void fly() {
System.out.println(this.name+"起飞");
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "金毛";
dog.eat();
}
}
运行结果:
此时, Animal 这样被继承的类, 我们称为 父类 , 基类 或 超类, 对于像 Cat 和 Bird 这样的类, 我们称为 子类,或者派生类
和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果
此时我们来简单看一下内存中的存储:
3、super关键字
我们在类和对象讲过当一个类没有写构造方法的时候,系统默认会有一个没有参数且没有任何内容的构造方法。
来看一个例子:
当我们自己给父类写了一个构造方法后,两个子类都报错了,是什么原因呢?
因为当子类继承了父类后,在构造子类之前,就必须先帮父类进行构造。(重点)
就用到了关键字super
super 表示获取到父类实例的引用.,和this类似共有三种用法
1、super.父类的成员变量 2、super.父类的成员方法 3、super():调用父类的构造方法
注意:super和this一样不能在静态方法里使用!
class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(this.name + " 正在吃");
}
}
class Bird extends Animal{
public String name = "乌鸦";
public Bird(String name) {
super(name);// 使用 super 调用父类的构造方法
}
public void fly() {
System.out.println(super.name);//调用父类的成员变量
super.eat();//调用父类的构造方法
System.out.println(this.name+"起飞");//调用自己的成员变量
}
}
public class Test {
public static void main(String[] args) {
Bird bird = new Bird("麻雀");
bird.fly();
}
}
运行结果:
当子类和父类有了同名的成员变量的内存结够图
注意:在用super关键字在子类的构造方法里帮父类构造的时候一定要在第一行
Object
如果一个类没有指定父类的时候,默认继承的就是Object类。
class Animal {//默认继承Object类
public String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(this.name + " 正在吃");
}
}
4、访问权限
(1)private
当我们把父类的访问权限改成private的时候,子类就无法访问了。但并不是没有继承,而是无法直接访问了,因为被private修饰的只能在当前类里使用!
private是可以修饰构造方法的,在类外不能实例化对象,要提供一个静态方法来来帮助构造一个对象。这样的操作在以后的单例设计模式会用到。
(2)prodected
刚才我们发现,如果把字段设为private,子类不能访问,但是设成public,又违背了我们“封装”的初衷,两全其美的方法就是protected关键字。
- 对于类的调用者来说,protected修饰的字段和方法是不能访问的
- 对于类的子类和同一个包的其他类来说,protected修饰的字段和方法是可以访问的。
(3)default
当一个类什么修饰符都不加的时候就是默认的访问权限,也就是包访问权限default,相当于这个类只能在当前包中使用。
class Cat extends Animal{//没有任何访问权限修饰符
Cat(String name) {
super(name);
this.name = name;
}
}
(4)小结
总结:Java中对于字段和方法共有四种访问权限
- private:类内部能访问,类外部不能访问
- 默认(也叫包访问权限default):类内部能访问,同一个包中的类可以访问,其他类不能访问
- protected:类内部能访问,子类和同一个包中的类可以访问,其他类不能访问
- public:类内部和类的调用者都能访问
5、更复杂的继承
这样的继承方式称为多层继承,即子类还可以进一步的再派生出新的子类。
虽然语法上可以继承很多层,但不建议超过三层,超过三层的话就用final修饰最后一层,如果再往下继承的话编译器就会报错。
class Animal {
public String name;
public void eat() {
System.out.println(this.name + " 正在吃");
}
}
class B extends Animal {
}
class C extends B {
}
final class D extends C {
}
6、final关键字
- final修饰变量(常量,这个常量不能再被修改)
- final修饰类,密封类:当前类不能再继承
- final修饰方法,密封方法:该方法不能进行重写
三、组合
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果.
例如表示一个学校: ```java public class Student {
} public class Teacher {
} public class School { public Student[] students; public Teacher[] teachers; }
组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段.<br />这是我们设计类的一种常用方式之一
> 组合表示 has - a 语义 在刚才的例子中, 我们可以理解成一个学校中 “包含” 若干学生和教师.
> 继承表示 is - a 语义 在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “是” 一种动物
一定要理解组合和继承的区别
<a name="gFiD8"></a>
## 四、多态
<a name="afbAh"></a>
### 1、向上转型
<a name="R7PQW"></a>
#### (1)概念
> 向上转型就是把一个子类引用给一个父类引用,也就是父类引用引用了子类的对象。
```java
class Animal {
public String name;
public void eat() {
System.out.println(this.name + " 正在吃");
}
}
class Cat extends Animal {
}
public class Test extends TestDemo {
public static void main(String[] args) {
//父类引用 引用了 子类引用所引用的对象
Cat cat = new Cat();
Animal animal = cat;//向上转型
}
}
我们把一个Animal类型引用了它的子类Cat这就是向上转型。
(2)向上转型发生的几种时机
直接赋值.
public static void main(String[] args) {
//父类引用 引用了 子类引用所引用的对象
Animal animal = new Cat();;//向上转型
}
方法传参
我们这里把一个Cat的子类传给一个Animal类型的父类,这里也是能发生向上转型的
public class Test extends TestDemo {
public static void func(Animal animal) {
}
public static void main(String[] args) {
//父类引用 引用了 子类引用所引用的对象
Cat cat = new Cat();
func(cat);
}
}
- 方法返回
这里func方法的返回类型是Animal但返回的确是一个Cat类型,这里也是发生了向上转型
public class Test extends TestDemo {
public static Animal func() {
Cat cat = new Cat();
return cat;
}
public static void main(String[] args) {
Animal animal = func();
}
}
(3)注意事项
注意:当发生向上转型的时候,通过父类引用只能调用父类的自己的方法和成员变量
2、向下转型
(1)概念
知道了向上转型,那么向下转型就好理解了。向下转型就是父类对象转成子类对象。
我们把一个父类引用Animal类型的引用给了一个Bird类型的引用,这就是向下转型
注意:向下转型的时候一定要进行强制类型转换
class Animal {
public String name;
public void eat() {
System.out.println(this.name + " 正在吃");
}
}
class Cat extends Animal {
}
class Bird extends Animal {
public int age;
public void fly() {
System.out.println(this.name+"起飞");
}
}
public class Test extends TestDemo {
public static void main(String[] args) {
Animal animal = new Animal();
Bird bird = (Bird) animal;//必须进行强制类型转换
}
}
(2)instanceof关键字
向下转型我们一般不建议使用,因为它非常不安全。
来看一段代码:
运行结果:
运行之前并没有报错,但运行之后这里报出了一个类型转换异常。
因为这里Animal本身引用的就是一个Cat对象,然后把它强制强转为Bird,因为Cat里根本没有fly()方法,就相当于你让一只猫去飞,它能飞起来吗?
所以向下转型非常的不安全,如果要让它安全就要加上关键字instanceof来判断一下。
public class Test extends TestDemo {
public static void main(String[] args) {
Animal animal = new Bird();
if (animal instanceof Bird) {
Bird bird = (Bird) animal;
bird.fly();
}
}
}
instanceof可以判定一个引用是否是某个类的实例,如果是,则返回true,这时再进行向下转型就比较安全了
所以向下转型我们一般不建议使用,如果非要使用就一定要用instanceof关键字判断一下。
3、动态绑定(运行时绑定)
(1)动态绑定概念
动态绑定发生的前提
- 先向上转型
- 通过父类引用来调用父类和子类同名的覆盖方法
来看一段代码:
运行结果:
我们发现这里我们通过父类引用调用了 Animal 和 Cat
同名的覆盖方法(重写),运行的是子类Cat的eat方法。此时这里就发生了动态绑定
动态绑定也就叫运行时绑定,因为程序在编译的时候调用的其实是父类的 eat 方法,但是程序在运行时运行的则是子类的 eat 方法,运行期间发生了绑定。
(2)重写(Override)
前面的博客中我们提到了重载,那么重写又是什么时候发生的呢?
重写发生的条件
- 方法名相同
- 方法的参数列表相同(返回类型和数据类型)
- 方法的返回值相同
返回值构成父子类关系也是可以发生重写的,此时叫做:协变类型
注意:
- 子类的重写的这个方法,他的访问修饰符,一定要大于等于父类方法的访问修饰符
- 被final和static修饰的方法是不能发生重写的
(3)@Override注解
被@Override注解修饰的方法代表是要重写的方法,一旦方法被这个注解修饰,只要方法的方法名,返回值,参数列表有一个地方不满足重写的要求,编译器就会报错。
(4)动态绑定的一个坑
来看一段代码,我们实例化一个Cat类,因为Cat是子类,所以要帮父类先构造,那么在父类的构造方法里有一个 eat 方法,那么会执行哪个类里的 eat 方法呢?
运行结果
我们发现这里调用的不是 是Animal 的 eat 方法,而是 Cat 的,因为这里也发生了动态绑定。
所以构造方法当中也是可以发生动态绑定的
注意:这样的代码以后不要轻易写出来!
4、多态
(1)理解多态
多态其实就是一种思想,一个事物表现出不同的形态,就是多态。
通过代码来理解,这里我要打印一些形状
class Shape {
public void draw() {
}
}
class Rect extends Shape{
public void draw() {
System.out.println("♦");
}
}
class Cycle extends Shape{
public void draw() {
System.out.println("●");
}
}
class Flower extends Shape{
public void draw() {
System.out.println("❀");
}
}
class Triangle extends Shape{
public void draw() {
System.out.println("△");
}
}
public class Test {
public static void main(String[] args) {
Shape shape = new Rect();
shape.draw();
Shape shape1 = new Cycle();
shape1.draw();
Shape shape2 = new Flower();
shape2.draw();
Shape shape3 = new Triangle();
shape3.draw();
}
}
运行结果
这不就是动态绑定吗?和多态有什么关系吗?
当我们在这个代码中添加一个drawMap方法后
运行结果
这不就是动态绑定吗?
我们细看会发现这是同样一个引用调用同样一个方法,能表现出不同的形态,这不就是多态思想?其实多态用到的就是动态绑定。
在这个代码中, 前面的代码是 类的实现者 编写的, Test这个类的代码是 类的调用者 编写的.
当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当 前的shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 多态
(2)多态的好处
- 类调用者对类的使用成本进一步降低
封装是让类的调用者不需要知道类的实现细节 多态能让类的调用者连这个类的类型是什么都不必知道,只需知道这个对象具有某个方法即可
- 可拓展能力更强
如果要新增一种新的形状,使用多态的方法代码改动成本也比较低 对于类的调用者来说(shawShapes方法),只要创建一个新类的实例就可以了,改动成本很低
总结
- 封装:安全性
- 继承:为了代码的复用(java是单继承)
- 多态:一个事物表现出不同的形态
- 注意重载和重写的区别
- 注意this和supe的区别