文章目录
- 前言
- 一、封装
- 1.封装的概念
- 2.访问修饰限定符
- 3.初识 “ 包 ”
- 4. static 成员
- static修饰成员变量
- static修饰成员方法
- static成员初始化
- 二、继承
- 1.为什么要继承
- 2.继承的概念
- 3.语法
- 4.父类成员访问
- 1.子类中访问父类的成员变量
- 2.子类中访问父类的成员方法
- 5. super 关键字
- 6. 子类构造方法
- 7. super 和 this
- 8. protected 访问限定符
- 三、多态
- 1.什么是多态
- 2.多态实现的条件
- 3.重写
- 4.向上转型
- 总结
前言
上篇文章介绍了Java里【类和对象】的相关知识,了解了什么是面向对象,什么是对象,如何创建对象等等
在面向对象程序中,有 三大特点:封装,继承, 多态
本篇将介绍这三大特点的相关知识
提示:是正在努力进步的小菜鸟一只,如有大佬发现文章欠佳之处欢迎评论区指出讨论~ 废话不多说,发车
一、封装
1.封装的概念
简单来说:就是套壳屏蔽细节
比如:对于电脑这样一个复杂的设备,提供给用户的就只是:开关机、通过键盘输入,显示器,USB插孔等,让用户来和计算机进行交互,完成日常事务
但实际上:电脑真正工作的却是CPU、显卡、内存等一些硬件元件
计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可
那么封装就是如此:
将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互
那么如何进行隐藏呢?下面来认识一下“ 访问修饰限定符 ”
2.访问修饰限定符
Java中主要通过类和访问权限来实现封装:类可以将数据以及封装数据的方法结合在一起,更符合人类对事物的认知,而访问权限用来控制方法或者字段能否直接在类外使用
Java中提供了四种访问限定符:
protected 主要是用在继承中( 继承部分详细介绍 )
default 权限指:什么都不写时的 默认权限
访问权限除了可以限定类中成员的可见性,也可以控制类的可见性
我们看这段代码:
因为在 PetDog 这个类当中,定义“ 名字 ”这个成员变量时用 private 修饰,所以在 Test 这个类中就不能通过对象名 + 点号 直接访问这个变量了
那怎么办呢?
不能直接访问,那就 间 接 访 问 !
活人还能让尿憋死?
定义 getter 和 setter 这两个成员方法:
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
快捷键:Alt + Insert —— 点击 Getter and Setter 即可
然后就可以通过 get 和 set 这两个成员方法进行访问:
PetDog dog1 = new PetDog();
dog1.setName("坦克");
System.out.println(dog1.getName());
我们把 name 这个成员变量隐藏起来,不对外公开,只提供 setter 和 getter 这两个方法可以访问 name ,而用户也不需要关心 setter 和 getter 是如何实现的,这就是封装
一般情况下成员变量设置为 private,成员方法设置为 public,具体还得看业务需求
3.初识 “ 包 ”
在面向对象体系中,提出了一个软件包的概念即:为了更好的管理类,把多个类收集在一起成为一组,称为软件包。
有点类似于文件夹。比如:为了更好的管理电脑中的歌曲,一种好的方式就是将相同属性的歌曲放在相同文件下,也可以对某个文件夹下的音乐进行更详细的分类
在Java中也引入了包,包是对类、接口等的封装机制的体现,是一种对类或者接口等的很好的组织方式,比如:一个包中的类不想被其他包中的类使用。
包还有一个重要的作用:在同一个工程中允许存在相同名称的类,只要处在不同的包中即可。
所以同一个包中不允许出现相同名称的类
4. static 成员
我们拿学生这个类来举例:
class Student{
// 成员属性
public String name;
public String classroom;
//构造方法
public Student(String name, String classroom) {
this.name = name;
this.classroom = classroom;
}
// 成员方法
public void doClass(){
System.out.println(this.name + "在" + this.classroom + "上课");
}
}
public class Test {
public static void main(String[] args) {
Student student1 = new Student("张三", "高三一班");
Student student2 = new Student("李四", "高三一班");
Student student3 = new Student("王五", "高三一班");
student1.doClass();
student2.doClass();
student3.doClass();
}
}
//输出结果:
//张三在高三一班上课
//李四在高三一班上课
//王五在高三一班上课
这样的代码合适吗?
这三个同学是同一个班的,那么他们上课肯定是在同一个教室,那既然在同一个教室,那 能否给类中再加一个成员变量 classroom,来保存同学上课时的教室呢 ?
在 Student 类中定义的成员变量( 例如name ),每个对象中都会包含一份( 称之为实例变量 ),因为需要使用这些信息来描述具体的某个学生
而现在要表示学生上课的教室,这个 classroom 并不需要每个学生对象中都存储一份,而是需要让所有的学生来 共享
在Java中,被 static 修饰的成员,称之为静态成员,也可以称为类成员,其不属于某个具体的对象,是所有对象所 共享 的
所以这里的 classroom 应该用 static 修饰:
public static String classroom;
static修饰成员变量
static 修饰的成员变量,称为静态成员变量,静态成员变量最大的特性:不属于某个具体的对象,是所有对象所共享的
静态成员变量特性:
不属于某个具体的对象,是类的属性,所有对象共享的,不存储在某个对象的空间( 堆 )中
既可以通过 对象 访问,也可以通过 类名 + 静态变量名 访问,但一般更推荐使用类名访问
存储在方法区当中
生命周期伴随类的一生( 随 类的加载而创建,随 类的卸载而销毁 )
既然 static 修饰的成员属性不属于对象了,那么刚才的代码中:" this.classroom "就是错误的写法
那么 doClass 的方法体就要变成:
public void doClass(){
System.out.println(this.name + "在" + classroom + "上课");
}
static修饰成员方法
一般类中的数据成员都设置为 private ,而成员方法设置为 public ,那设置之后,Student 类中 classRoom 属性如何在类外访问呢?
private static String classroom;
静态成员一般是通过静态方法来访问的
Java中,被 static 修饰的成员方法称为静态成员方法,是类的方法,不是某个对象所特有的
...
public static String getClassRoom(){
return classRoom;
}
public static void main(String[] args) {
System.out.println(Student.getClassRoom());
}
不需要实例化对象,只需要用 类名 + 点号 就可以访问到 classroom 了
静态方法特性:
不属于某个具体的对象,是类方法
可以通过对象调用,也可以通过 类名 + 点号静态方法名 的方式调用,更推荐使用后者
不能在静态方法中直接访问任何非静态成员变量
静态方法中不能调用任何非静态方法,因为非静态方法有this参数,在静态方法中调用时候无法传递this引用
static成员初始化
注意:静态成员变量一般不会放在构造方法中来初始化,构造方法中初始化的是与对象相关的实例属性
静态成员变量的初始化分为两种:就地初始化 和 静态代码块初始化
就地初始化:
private static String classroom = "高三一班";
静态代码块初始化:
使用 static 定义的代码块称为静态代码块。一般用于初始化静态成员变量
...
static {
classRoom = "高三一班";
}
...
二、继承
1.为什么要继承
Java中使用类对现实世界中实体来进行描述,类经过实例化之后的产物对象,则可以用来表示现实中的实体,但是现实世界错综复杂,事物之间可能会存在一些关联,那在设计程序是就需要考虑。
比如:狗和猫,它们都是一个动物。使用Java语言来进行描述,就会设计出:
// Dog 类
public class Dog {
string name;
int age;
float weight;
public void eat() {
System.out.println(name + "正在吃饭");
}
public void sleep() {
System.out.println(name + "正在睡觉");
}
void Bark() {
System.out.println(name + "汪汪汪~~~");
}
}
// Cat 类
public class Cat {
string name;
int age;
float weight;
public void eat() {
System.out.println(name + "正在吃饭");
}
public void sleep() {
System.out.println(name + "正在睡觉");
}
void mew() {
System.out.println(name + "喵喵喵~~~");
}
}
通过观察上述代码会发现,猫和狗的类中存在大量重复:
比如猫和狗这两个类都定义了 name 、 age 、 weight 这些成员属性,也都定义了 eat 和 sleep 这两个成员方法
有一处不同的是:狗是汪汪叫,猫是喵喵叫
那能否将这些共性抽取呢?
面向对象思想中提出了 继承 的概念,专门用来进行共性抽取,实现代码 复用
2.继承的概念
继承( inheritance )机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能,这样产生新的类,称派生类
继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用
上述图示中,Dog 和 Cat 都继承了 Animal 类
其中:Animal 类称为 父类 / 基类 / 超类
Dog 和 Cat 可以称为 Animal 的 子类 / 派生类
继承之后,子类可以复用父类中成员,子类在实现时只需关心自己新增加的成员即可。
从继承概念中可以看出继承最大的作用就是:实现代码复用,还有就是来实现多态( 后序讲 )
3.语法
在 Java 中如果要表示类之间的继承关系,需要借助** extends 关键字**,具体如下:
修饰符 class 子类 extends 父类 {
// ...
}
我们把刚刚的代码用 继承 体现一下:
// Animal 类
public class Animal {
String name;
int age;
public void eat() {
System.out.println(name + "正在吃饭");
}
public void sleep() {
System.out.println(name + "正在睡觉");
}
}
// Dog 类
public class Dog extends Animal {
void bark() {
System.out.println(name + "汪汪汪~~~");
}
}
// Cat 类
public class Cat extends Animal {
void mew() {
System.out.println(name + "喵喵喵~~~");
}
}
// TestExtend 类
public class TestExtend {
public static void main(String[] args) {
Dog dog = new Dog();
// dog 类中并没有定义任何成员变量
// name 和 age 属性肯定是从父类 Animal 中继承下来的
System.out.println(dog.name);
System.out.println(dog.age);
// dog 访问的 eat() 和 sleep() 方法也是从 Animal 中继承下来的
dog.eat();
dog.sleep();
// bark()方法是自己特有的
dog.bark();
}
}
注意:
子类会将父类中的成员变量或者成员方法继承到子类中了
子类继承父类之后,必须要新添加自己特有的成员,体现出与父类的不同,否则就没有必要继承了
4.父类成员访问
在继承体系中,子类将父类中的方法和字段( 属性 )继承下来了,那在子类中能否直接访问父类中继承下来的成员呢?
1.子类中访问父类的成员变量
当子类和父类中的成员变量不重名时:
class A {
int a = 10;
int b = 20;
}
class B extends A {
int c = 30;
public void method() {
System.out.println(this.a);// 访问从父类中继承下来的a
System.out.println(this.b);// 访问从父类中继承下来的b
System.out.println(this.c);// 访问子类自己特有的c
}
}
public class Test {
public static void main(String[] args) {
B b = new B();
b.method();
}
}
// 输出结果是:
// 10
// 20
// 30
当子类和父类中的成员变量重名时:
class A {
int a = 10;
int b = 20;
int c = 30;
}
class B extends A {
int c = 40;
public void method() {
System.out.println(this.a);// 访问从父类中继承下来的 a
System.out.println(this.b);// 访问从父类中继承下来的 b
System.out.println(this.c);// 访问从父类中继承下来的 c ?还是子类自己的 c ?
}
}
public class Test {
public static void main(String[] args) {
B b = new B();
b.method();
}
}
此时 输出的 c 的结果是 30 还是 40 呢?----答案是 40
总结:
在子类方法中 或者 通过子类对象访问成员时
如果访问的成员变量子类中有,优先访问自己的成员变量。
如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
如果访问的成员变量与父类中成员变量同名,则优先访问自己的。
变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找
2.子类中访问父类的成员方法
原则和上文所总结的一致
但我们有了一个问题:
如果子类中存在与父类中相同的成员时,那如何在子类中访问父类相同名称的成员呢?
下面我们来认识一下 super 关键字
5. super 关键字
由于设计不好,或者因场景需要,子类和父类中可能会存在相同名称的成员,如果要在子类方法中访问父类同名成员时,该如何操作?
直接访问是无法做到的,Java提供了 super 关键字
该关键字主要作用:在子类方法中访问父类的成员
class A {
int a = 10;
int b = 20;
int c = 30;
public void func() {
System.out.println(this.c);
}
}
class B extends A {
int c = 40;
public void func() {
System.out.println(this.c);
}
public void method() {
super.func();// 访问父类的 func 方法
System.out.println(super.c);// 访问从父类中继承下来的 c
}
}
public class Test {
public static void main(String[] args) {
B b = new B();
b.method();
}
}
// 运行结果:
// 30
// 30
在子类方法中,如果想要明确访问父类中成员时,借助super关键字即可
注意事项:
只能在非静态方法中使用
在子类方法中,访问父类的成员变量和方法
6. 子类构造方法
子类是类,所以在子类中同样也可以有构造方法
父子父子,先有父再有子
即:子类对象构造时,需要先调用父类构造方法,然后执行子类的构造方法
子类对象中成员是有两部分组成的, 基类继承下来的 以及 子类新增加的 两部分
在构造子类对象时候 ,先要调用基类的构造方法,将从基类继承下来的成员构造完整,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整
注意:
若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super() 调用,即调用基类构造方法
如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败
在子类构造方法中,super(…) 调用父类构造时,必须是子类构造函数中第一条语句
super(…) 只能在子类构造方法中出现一次,并且不能和 this 同时出现
7. super 和 this
super 和 this 都可以在成员方法中用来访问:成员变量和调用其他的成员函数,都可以作为构造方法的第一条语句,那他们之间有什么区别呢?
相同点:
都是Java中的关键字
只能在类的非静态方法中使用,用来访问非静态成员方法和字段
在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
不同点:
this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成员的引用
在非静态成员方法中,this 用来访问本类的方法和属性,super 用来访问父类继承下来的方法和属性
在构造方法中:this(…) 用于调用本类构造方法,super(…) 用于调用父类构造方法,两种调用不能同时在构造方法中出现
子类的构造方法中一定会存在 super(…) 的调用,用户没有写编译器也会增加,但是 this(…) 用户不写则没有
8. protected 访问限定符
在类和对象章节中,为了实现封装特性, Java 中引入了访问限定符,主要限定:类 或者 类中成员能否在类外 或者 其他包中被访问
// 为了掩饰基类中不同访问权限在子类中的可见性,为了简单类B中就不设置成员方法了
// extend01 包中
public class B {
private int a;
protected int b;
public int c;
int d;
}
// extend01 包中
// 同一个包中的子类
public class D extends B {
public void method() {
// super.a = 10; // 编译报错,父类private成员在相同包子类中不可见
super.b = 20; // 父类中protected成员在相同包子类中可以直接访问
super.c = 30; // 父类中public成员在相同包子类中可以直接访问
super.d = 40; // 父类中默认访问权限修饰的成员在相同包子类中可以直接访问
}
}
// extend02包中
// 不同包中的子类
public class C extends B {
public void method() {
// super.a = 10; // 编译报错,父类中private成员在不同包子类中不可见
super.b = 20; // 父类中protected修饰的成员在不同包子类中可以直接访问
super.c = 30; // 父类中public修饰的成员在不同包子类中可以直接访问
//super.d = 40; // 父类中默认访问权限修饰的成员在不同包子类中不能直接访问
}
}
// extend02包中
// 不同包中的类
public class TestC {
public static void main(String[] args) {
C c = new C();
c.method();
// System.out.println(c.a); // 编译报错,父类中private成员在不同包其他类中不可见
// System.out.println(c.b); // 父类中protected成员在不同包其他类中不能直接访问
System.out.println(c.c); // 父类中public成员在不同包其他类中可以直接访问
// System.out.println(c.d); // 父类中默认访问权限修饰的成员在不同包其他类中不能直接访问
}
}
注意:父类中private成员变量虽然在子类中不能直接访问,但是也继承到子类中了
什么时候下用哪一种呢?
我们希望类要尽量做到 “封装”,即隐藏内部实现细节,只暴露出必要的信息给类的调用者
因此我们在使用的时候应该尽可能的使用比较严格的访问权限,例如如果一个方法能用 private,就尽量不要用 public
三、多态
1.什么是多态
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
听不懂,说人话:
举例1:打印机,都是具备打印功能,但是可以打印出黑白图片,和彩色图片
举例2:吃,都是吃这个动作,人吃饭,狗吃 shit,猫吃 fish
耐心往下看,看完之后,回过头再看多态的概念,你会恍然大悟的
2.多态实现的条件
在java中要实现多态,必须要满足如下几个条件,缺一不可:
必须在继承体系下
子类必须要对父类中方法进行重写
通过向上转型调用重写的方法
重写是啥?向上转型是啥?往下看!
3.重写
重写( override ):
也称为覆盖。重写是子类对父类非静态、非 private 修饰,非 final 修饰,非构造方法等的实现过程进行重新编写,返回值和形参都不能改变。 即外壳不变,核心重写!
比如你和你老婆吵架了,老婆让你写一份检讨书,你写完了给老婆大人过目
老婆大人说:“ 不合格,重写!”
那你重写的当然还是这份检讨书,开头还是“ 亲爱的老婆大人 ”,格式还是检讨书的格式,结尾还是“ 老婆别生气了~ ”,但内容重写了
难不成你要重写成一篇《逍遥游》交给你老婆吗?
在程序中,重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
class A {
public void func() {
System.out.println("在父类中打印");
}
}
class B extends A {
public void func() {
System.out.println("在子类中打印");
}
}
方法重写的规则:
子类在重写父类的方法时,一般必须与父类方法原型一致: 返回值类型 方法名和参数列表要完全一致
被重写的方法返回值类型可以不同,但是必须是具有父子关系的
访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被 public 修饰,则子类中重写该方法就不能声明为 protected;父类被 static、private 修饰的方法、构造方法都不能被重写。
重写的方法, 可以使用 @Override 注解来显式指定,有了这个注解能帮我们进行一些合法性校验,例如不小心将方法名字拼写错了 ( 比如写成 aet ),那么此时编译器就会发现父类中没有 aet 方法,就会编译报错,提示无法构成重写
重写的设计原则:
对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容
例如:若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手机在来电显示的时候,不仅仅可以显示号码,还可以显示头像,地区等
在这个过程当中,我们不应该在原来的类上进行修改,因为原来的类,可能还在有用户使用,正确做法是:新建一个新手机的类,对来电显示这个方法重写就好了,这样就达到了我们当今的需求了。
我们来看一段体现多态的代码:
public class Animal {
String name;
int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + "吃饭");
}
}
public class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
@Override // 重写
public void eat() {
System.out.println(name+"吃鱼~~~");
}
}
public class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}
@Override // 重写
public void eat() {
System.out.println(name+"吃骨头~~~");
}
}
// ---------------------分割线--------------------------------
public class TestAnimal {
// 编译器在编译代码时,并不知道要调用Dog 还是 Cat 中eat的方法
// 等程序运行起来后,形参a引用的具体对象确定后,才知道调用那个方法
// 注意:此处的形参类型必须时父类类型才可以
public static void eat(Animal a) { // 这里发生了向上转型
a.eat();
}
public static void main(String[] args) {
Cat cat = new Cat("元宝",2);
Dog dog = new Dog("小七", 1);
eat(cat);
eat(dog);
}
}
在上述代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的
当类的调用者在编写 eat 这个方法的时候,参数类型为 Animal (父类), 此时在该方法内部并不知道,也不关注当前的 a 引用指向的是哪个类型( 哪个子类 )的实例,此时 a 这个引用调用 eat 方法可能会有多种不同的表现( 和 a 引用的实例
相关 ),这种行为就称为 多态
public static void eat(Animal a) { // 这里发生了向上转型
a.eat();
}
这里可能有人就迷惑了,什么玩意,看不懂
这就是接下来要讲的:向上转型
4.向上转型
实际就是创建一个子类对象,将其当成父类对象来使用
语法格式:父类类型 对象名 = new 子类类型()
Animal animal = new Cat();
Animal 是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换
上面这行代码可以理解为,我 new 了一只猫,这个猫当然是动物,或者我 new 一条狗,这个狗当然也是动物,这样就是向上转型
当然,这是在实例化对象的时候发生的向上转型,也可以在方法传参以及返回值接收的时候发生向上转型
意思就是,我实参是猫,形参是动物 这里也发生了向上转型(见上述代价),或者我方法的返回值返回的是 new Cat() ,接收返回值用Animal animal = 方法名 这里也是向上转型
小伙伴们可以自己试一下~
总结
以上就是今天要讲的关于Java 面向对象的【三大特征】的内容,封装,继承,多态,相关的知识尤为重要,与君共勉
如果本篇对你有帮助,请点赞收藏支持一下,小手一抖就是对作者莫大的鼓励啦🤪🤪🤪
上山总比下山辛苦
下篇文章见