前言:
面向对象的第一阶段是类及类的成员,第二阶段就是面向对象特征:封装、继承、多态。但这三个特征并不是官方发布的,只是人们自己总结出来方便理解。
封装:
为什么需要封装
面向对象的开发原则是:高内聚、低耦合。高内聚:类的内部数据操作细节自己完成,不允许外部干涉;低耦合:仅暴露少量的方法给外部使用,尽量方便外部调用。
举例来说:就跟咋们要用洗衣机,只需要按一下开关和洗涤模式就可以了。没有必要了解洗衣机内部的结构,也有必要碰电动机。为什么?就是因为封装。电动机这些部件都被封装成一个个体构成洗衣机。洗衣机向外留有按钮,只要按这个按钮。电动机这些内部器件就会自己运行。
什么是封装
所谓封装,就是把客观事物封装成抽象概念的类,并且类可以把自己的数据和方法只向可信的类或者对象开放,向没必要开放的类或者对象隐藏信息。通俗的讲,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。
对于上面的洗衣机来讲:电动机这些用户不了解也没必要了解那就隐藏起来,但我要用洗衣机,你给我留个按钮,我一按,洗衣机就能用。所以就需要将按钮暴露出来。
如何实现封装
- 实现封装就是控制类或成员的可见性范围。这就需要依赖访问控制修饰符,也称为权限修饰符来控制。
- 权限修饰符:public、protected、缺省、private。具体访问范围如下:
修饰符 | 本类内部 | 本包内 | 其他包的子类 | 其他包非子类 |
private 私有的 | √ | × | × | × |
缺省 | √ | √ | × | × |
protected 受保护的 | √ | √ | √ | × |
public 公开的 | √ | √ | √ | √ |
具体修饰的结构:以上符号修饰权限由小到大
外部类:public、缺省
成员变量、成员方法、构造器、成员内部类:public、protected、缺省、private
实现封装也就是将该隐藏的隐藏,该暴露的暴露。有了权限修饰符,要隐藏还是要公开根据需要选择对应的权限修饰符。
封装的体现
成员变量/属性的私有化:私有化类的成员变量,提供公共的get和set方法,对外暴露获取和修改属性的功能。
//① 使用 private 修饰成员变量private 数据类型 变量名 ;
public class Person {
private String name;
private int age;
private boolean marry;
}
//② 提供 getXxx方法 / setXxx 方法,可以访问成员变量,代码如下:
public class Person {
private String name;
private int age;
private boolean marry;
public void setName(String n) {
name = n;
}
public String getName() {
return name;
}
public void setAge(int a) {
age = a;
}
public int getAge() {
return age;
}
public void setMarry(boolean m){
marry = m;
}
public boolean isMarry(){
return marry;
}
}
//③ 测试:
public class PersonTest {
public static void main(String[] args) {
Person p = new Person();
//实例变量私有化,跨类是无法直接使用的
/* p.name = "张三";
p.age = 23;
p.marry = true;*/
p.setName("张三");
System.out.println("p.name = " + p.getName());
p.setAge(23);
System.out.println("p.age = " + p.getAge());
p.setMarry(true);
System.out.println("p.marry = " + p.isMarry());
}
}
成员变量封装的好处:
- 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里面加入控制逻辑,限制对成员变量的不合理访问。还可以进行数据检查,从而有利于保证对象信息的完整性。
- 便于修改,提高代码的可维护性。主要说的是隐藏的部分,在内部修改了,如果其对外可以的访问方式不变的话,外部根本感觉不到它的修改。例如:Java8->Java9,String从char[]转为byte[]内部实现,而对外的方法不变,我们使用者根本感觉不到它内部的修改。
开发中,一般成员实例变量都习惯使用private修饰,再提供相应的public权限的get/set方法访问。对于final的实例变量,不提供set()方法。对于static final的成员变量,习惯上使用public修饰。
继承:
举例:继承就是字面意思,有延续(下一代延续上一代的基因、财富)、扩展(下一代和上一代又有所不同)的意思。
为描述和处理个人信息,定义类Person:
为描述和处理学生信息,定义类Student:
通过继承,简化Student类的定义:
说明:Student类继承了父类Person的所有属性和方法,并增加了一个属性school。Person中的属性和方法,Student都可以使用。
继承的好处:
- 继承的出现减少了代码冗余,提高了代码的复用性。
- 继承的出现,更有利于功能的扩展。
- 继承的出现让类与类之间产生了is-a的关系,为多态的使用提供了前提。
- 继承描述事物之间的所属关系,这种关系是:is-a 的关系。可见,父类更通用、更一般,子类更具体。不要仅为了获取其他类中某个功能而去继承!
继承语法:
通过 extends 关键字,可以声明一个类B继承另外一个类A,定义格式如下:
[修饰符] class 类A {
...
}
[修饰符] class 类B extends 类A {
...
}
类B,称为子类、派生类(derived class)、SubClass
类A,称为父类、超类、基类(base class)、SuperClass
标红:①子类会继承父类所有的实例变量和实例方法②子类不能直接访问父类中私有的(private)的成员变量和方法③在Java 中,继承的关键字用的是“extends”,即子类不是父类的子集,而是对父类的“扩展”④Java支持多层继承(继承体系),即子继父类,父继爷类。子继爷类。⑤一个父类可以同时拥有多个子类⑥Java只支持单继承,不支持多重继承,即一个类只能有一个父类,不可以有多个直接父类。
方法重写、方法重载理解:
class Animal {
void makeSound() {
System.out.println("The animal makes a sound");
}
}
class Dog extends Animal {
// 方法重写
@Override
void makeSound() {
System.out.println("The dog barks");
}
// 方法重载
void makeSound(int times) {
for (int i = 0; i < times; i++) {
System.out.println("The dog barks");
}
}
}
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog();
myDog.makeSound(); // 输出 "The dog barks",因为这是重写后的方法
myDog.makeSound(3); // 输出 "The dog barks" 三次,因为这是重载后的方法
}
}
Dog
类继承了 Animal
类,并重写了 makeSound
方法。同时,Dog
类还提供了一个重载版本的 makeSound
方法,该方法接受一个整数参数 times
,表示狗叫的次数。
多态:
对于多态的理解:
广义上的理解:子类对象的多态性、方法的重写;方法的重载
狭义上的理解:子类对象的多态性。
对于同一个行为,不同的子类对象具有不同的表现形式。多态存在的3个条件:①继承 ②重写 ③父类引用指向子类对象。
Java引用变量有两个类型:编译时类型和运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。简称:编译时,看左边;运行时,看右边。
若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)
多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法) “看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)
多态的体现:
在Java中的体现:对象的多态性:父类的引用指向子类的对象
格式:(父类类型:指子类继承的父类类型,或者实现的接口类型)
父类类型 变量名 = 子类对象;Object obj = new String("hello"); 父类的引用指向子类的对象。
Person p = new Student();
Object o = new Person();//Object类型的变量o,指向Person类型的对象
o = new Student(); //Object类型的变量o,指向Student类型的对象
对象的多态:在Java中,子类的对象可以替代父类的对象使用。所以,一个引用类型变量可能指向(引用)多种不同类型的对象
方法内局部变量的赋值体现多态
public class TestPet {
public static void main(String[] args) {
//多态引用
Pet pet = new Dog();
pet.setNickname("小白");
//多态的表现形式
/*
编译时看父类:只能调用父类声明的方法,不能调用子类扩展的方法;
运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;
*/
pet.eat();//运行时执行子类Dog重写的方法
// pet.watchHouse();//不能调用Dog子类扩展的方法
pet = new Cat();
pet.setNickname("雪球");
pet.eat();//运行时执行子类Cat重写的方法
}
}
方法的形参声明体现多态
public class Person{
private Pet pet;
public void adopt(Pet pet) {//形参是父类类型,实参是子类对象
this.pet = pet;
}
public void feed(){
pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
}
}
package com.atguigu.polymorphism.grammar;
public class TestPerson {
public static void main(String[] args) {
Person person = new Person();
Dog dog = new Dog();
dog.setNickname("小白");
person.adopt(dog);//实参是dog子类对象,形参是父类Pet类型
person.feed();
Cat cat = new Cat();
cat.setNickname("雪球");
person.adopt(cat);//实参是cat子类对象,形参是父类Pet类型
person.feed();
}
}
方法返回值类型体现多态
public class PetShop {
//返回值类型是父类类型,实际返回的是子类对象
public Pet sale(String type){
switch (type){
case "Dog":
return new Dog();
case "Cat":
return new Cat();
}
return null;
}
}
public class TestPetShop {
public static void main(String[] args) {
PetShop shop = new PetShop();
Pet dog = shop.sale("Dog");
dog.setNickname("小白");
dog.eat();
Pet cat = shop.sale("Cat");
cat.setNickname("雪球");
cat.eat();
}
}
多态的好处和弊端
好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。
弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。
开发中:使用父类做方法的形参,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。【开闭原则OCP】对扩展开放,对修改关闭通俗解释:软件系统中的各种组件,如模块(Modules)、类(Classes)以及功能(Functions)等,应该在不修改现有代码的基础上,引入新功能
多态的使用:
①虚拟方法调用:
在Java中虚方法是指在编译阶段不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。
Person e = new Student();
e.getInfo(); //调用Student类的getInfo()方法
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
②“编译看左边,运行看右边”。
③属性,不存在多态性。
多态的逆过程:向下转型,使用强转符()。
因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现类型转换的现象。
但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有,而父类没有的方法了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做类型转换,使得编译通过。
向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型
-
- 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
- 此时,一定是安全的,而且也是自动完成的
向下转型:当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),我们就称为向下转型
-
- 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
- 但是,运行时,仍然是对象本身的类型
- 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断。