JavaSE基础(八)——面向对象三个特性
前言
上篇笔记说到了面向对象的编程思维,然而并没有说全,无关乎与本篇笔记的内容,只是有些东西在初学者这里不能说的太多,后期会重复补充相关知识点,其中包括:对象克隆、对象内存、Java虚拟机、对象序列化与反序列化等很多内容。
大家不要害怕,在大家逐渐对于对象的操作得心应手之后,我们就会开始对于Java高级阶段的记录,在其中会记录相关的知识点,也有可能是在算法或者杂谈系列中。
文章目录
Java面向对象的三大特性
之前说过,Java面向对象的三大特性分别为:封装、继承、多态,有些版本的教材还会加上一个抽象。其实个人认为抽象是前面三大特性的基础,更是Java语言的基础,Java语言中所有出现的插件、框架其实都在默默地遵循抽象的概念。
封装
封装的思想就是将一些代码、功能等封装成一个整体,并且隐藏一些东西。隐藏需要遵循以下几个规则:
-
该暴露的要暴露出来,该隐藏的需要隐藏起来;
比如账号可以提供给其他人,但密码只能自己知道。
-
遵循”高内聚,低耦合“的原则,高内聚指的是累的内部数据操作仅自己完成,类似赋值,修改,获取值等;低耦合指的是仅仅暴露少量的方法给外部使用。
-
封装其实是对数据的隐藏,对象应该禁止外部直接对于对象数据的直接访问,应该通过相应的操作接口(属性)来访问。
修饰符的介绍
修饰符 | 同类 | 同包 | 子类 | 不同包 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
以上四个修饰符中访问权限按照这个表格来区分。
其中,如果普通类的方法没有写修饰符默认为default(还有其他情况后续说明)。
封装的相关代码规范
以上是封装的一些要点,封装规定了我们在创建Java类的时候应该具有的一些东西,具体如下:
public class Student {
// 成员变量
// 学号
private String id;
// 学生姓名
private String name;
// 学生性别
private String gender;
// 学生年龄
private int age;
// 有参构造器
public Student(String id, String name, String gender, int age) {
this.id = id;
this.name = name;
this.gender = gender;
this.age = age;
}
// 无参构造器
public Student() {
super();
}
// 属性
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", gender='" + gender + '\'' +
", age=" + age +
'}';
}
}
在此我直接书写一个标准的Java类(JavaBean),每当要定义一个类的时候我们都要按照这个规范来说写,以后进行标准的Java开发以及相关的使用一些框架的时候都是这么规定的(也有一些技术、平台等可以通过其他手段来来简化我们创建类的操作),下面进行详细说明:
- 首先我们定义的类中的字段(实例化的对象都会有的字段)被称之为成员变量,成员变量属于对象,只有实例化时才会在堆内存中分配空间。成员变量的就是需要封装起来的值,避免外界直接对其进行操作,所以会被设定为private,只允许本类中调用;
- 成员变量已经被设置成私有,那么我们就需要提供一些访问接口来共对象在其他类中调用,所以我们定义了属性,对象通过调用属性方法来访问数据;
- 最后一个toString方法可写可不写,只是单纯的格式化输出对象的所有数据(之前直接输出对象输出的是一串编号,编号是每个对象的唯一标识,以后再说。现在直接输出对象就可以输出数据)。
上述操作具体如下:
public static void main(String[] args){
Student student = new Student();
student.setId("1001");
System.out.println(student.getId());
System.out.println(student);
}
封装小结
在本小节对于封装的描述中,我们介绍了封装在Java语言中的基本表现形式。大家不能单纯的记住Java的封装应该怎么写,更主要的是记住思想。上述JavaBean的书写规范仅仅适用于大部分情况,在Java中还有其他情况我们需要另行规定。
继承
继承的本质是堆某一批类的抽象。
就好比我们定义了一个类只为了描述车这个客观存在,关于车的特征我们设定了动力构成以及车轮数(姑且按照这两种特征来分类),但是由这一个类进行实例化各种车的对象会很复杂,所以我们需要创建一些类具有车的基本特征并且还可以通过定义其他特征来更加细化车的分类。
例如,我们除了动力构成和车轮数以外还可以定义车的用途(拉货的、载人的),由此就可以更加细化分出电动三轮和皮卡。这就是继承存在的一个主要原因。具体关系如下:
public class Vehicle{
// 动力
private String power;
// 车轮数
private int wheelNum;
}
public class car extends Vehicle{
// 车的用途
private String purpose;
}
在Java中我们使用extends来完成类与类之间的继承关系(与C语言一样),但是与C语言不大相同的是,在Java中只能存在单继承,所以C语言中那个比较具有代表性的钻石继承结构在Java中使用继承是无法实现的——但是在Java中我们有其他概念来实现钻石继承,后续会有讲解。
以下总结一下继承的一些要点:
- 继承是类与类之间的一种关系。除此之外还有很多关系,比如:组合、聚合等;
- 存在继承关系的两个类,一个为基类(父类),另一个为派生类(子类),具体关系为子类继承父类;
public class Father{
}
public class Son extends Father{
}
- 所有的类都有一个最终父类(老祖宗)——Object,Java中所有的类都基于Object;
- 子类继承父类后会拥有父类全部的方法(public修饰的方法),父类的私有的成员变量、属性以及方法子类不可用;
- 最重要的一点,如果一个类被final关键字修饰,则这个类无法被继承。
super关键字
解释之前先复习一下this关键字。之前解释过,this关键字表示当前对象的引用,即使用this的时候,系统会自动通过this关键字找到当前执行操作的对象;super与this相似,只不过super代表当前类的父类对象的引用,而在代码中的**super()**表示的是调用父类的无参构造方法,这个是默认执行的,就像无参构造器一样系统自动生成的。
public class Father{
private String name;
public Father(){
System.out.println("我已经被调用");
}
public String getName(){
return this.name;
}
public void setName(String name){
this.name = name;
}
}
public class Son extends Father{
int i;
public Son(){
// 这句可有可无,如果手动书写,系统会自动帮我们执行这句
// super关键字如果手写,则必须出现在方法的第一句,不能改变代码顺序
super();
// 同样this也是要放在方法的第一行中,所以我们在同一个方法中,super和this只能存在一个
//this.i = 0;
System.out.println("我不能方法super之前");
}
public static void main(String[] args){
Son son = new Son();
}
}
上述代码的最后输出结果为
我已经被调用
我不能方法super之前
方法重写
有些教材会简称为重写,其实道理一样都是对于方法的重写(重写只针对方法,与成员变量无关)。方法的重写主要目的是子类在继承父类的时候可能父类的方法并不完全适合子类,所以我们可以在子类中定义一个父类中已经存在的方法,并将其内部逻辑修改为适合自己的方法。具体如下:
public class A{
public void show(){
System.out.println("A");
}
}
public class B extends A{
@Override // 该注解表示重写,后续会详细描述注解的概念
public void show(){
System.out.println("B");
}
public void action(){
System.out.println("我是子类独有的方法");
}
}
public class Demo{
public static void main(String[] args){
A a = new A();
B b = new A();
a.show();
b.show();
}
}
上述代码的输出结果为:
A
A
原因很简单,刚刚说过重写的目的,所以我们在使用子类的对象时会自动调用子类重写的方法,而不执行父类的同名方法(其实自动生成的重写方法中,会默认调用父类的方法,只是我们可以修改而已)。
代码中父类引用指向子类对象的时候依旧可以正常生成对象,并且依旧执行实际对象所对应的类的重写方法。这个知识点很复杂,下面的解释可能会很难,请诸位谨慎观看。如果看不懂也没事,还有一个简单版本的:
首先说明,父类引用指向子类对象的操作被称为向上转型。向上转型的意义在于程序在执行的过程中,系统没必要关注究竟是哪个对象在工作(属于GOF23中设计模式中的门面模式,也叫外观模式),所以一般我们在使用子类的时候都会把子类对象的引用定义为父类。具体好处是什么呢?
好处就是可以根据类与类中方法之间的关系来动态的执行对应的方法,具体解释需要先介绍一下动态链接:
在父类引用指向子类对象的前提下,被调用的父类中的方法没有被子类重写,则该对象调用这个方法时就会访问父类;而被调用的方法在子类中被重写,则会自动调用子类的方法。
所以我们才会发现上面的代码都会调用子类重写的方法。说到这里在介绍一个重写的要点:父类中被定义为静态的方法无法被重写。套用动态链接的概念我们可以得知,就算子类中也写了一个同名方法,向上转型的对象也无法调用。
至于为什么不能重写,我们可以理解为:静态方法属于类本身的,在代码编译阶段便已经判定在当前类上,所以我们可以继承该方法,但是无法重写。
上面的如果看不太懂可以看下面简易版本:
以父类引用指向子类对象为前提:
- 在调用父类中存在且没有被子类重写的方法时会直接调用父类的方法(因静态方法无法被重写,所以即使存在同名方法也不会调用子类同名方法);
- 无法直接调用子类独有的方法,需要强制类型转换;
// 在上面代码的主方法中加入以下代码
A a = new B();
// 以下为将A类型引用强制转化为B类型
((B)a).action();
- 如果父类存在的方法被子类重写后,则会直接调用子类重写方法;
- 如果子类重写方法有使用super关键字的话,要看情况来判定是否执行父类方法;
- 最后,静态方法、final修饰的方法、私有方法都无法被重写。
多态
多态为不同的对象调用或者通过同一个方法时因为状态不同从而展现出不同的行为或结果。
多态同样为一种思想不是Java本身的一个技术,它更像是一种编程的方式或者规范。在一个父类拥有多个子类的时候,每个子类在某种条件下可以分为多种不同的状态,在执行同一个功能的时候会有不同的表现形式。
多态一般是基于继承以及向上转型的前提下实现的。在重写的模块中我们说明了向上转型和动态链接的概念,其实这就是一种多态的思想(根据实际对象的不同从而调用不同的方法,或者在同一个方法中执行不同的行为)——同中方法中执行不同行为可能比较难以理解,不是实现过于复杂,而是在于如何区分不同状态。
具体实现在此不做赘述,参考上面关于重写中向上转型的代码。下面总结多态的要点:
- 多态依旧是方法的多态,属性无法多态;
- 父类和子类有继承关系,否则强制类型转换将会出现异常;
- 多态存在条件为:存在继承关系、方法被重写、向上转型的对象。
多态中我们主要基于下面新的关键字和概念。
instanceof关键字
该关键字可以判断两个类之间是否有继承关系。具体用法如下:
public class A{
}
public class B extends A{
}
public class C{
}
public class Demo{
public static void main(String[] args){
A a = new A();
B b = new B();
C c = new C();
System.out.println(b instanceof A);
System.out.println(a instanceof C);
}
}
上述代码中,因为A和B有继承关系,所以会输出true;A和C之间没有继承关系,所以会输出false。代码输出结果为:
true
false
【注】:instanceof左边是对象,右边是类;而且左边的对象的类是右边类的父类也不可以。
static
给大家补充一个关键字static,一直以来也没有一个明确的解释。static修饰的代码都被称为静态,比如:静态常量、静态方法等。之前说过静态修饰的成员变量、方法等属于类本身的,其实,我们可以很简单理解为static关键字修饰的代码是存放在常量池中的。代码编译的时候就会与当前类绑定;而成员变量以及普通方法等都是在实例化对象的时候在对象所在的堆内存中开辟空间存放。
至于为什么静态方法中无法直接调用普通方法,原因是普通方法只有在对象实例化后才会存在,而静态方法一直存在,如果我们在没有实例化对象的前提下通过静态方法调用普通方法很有可能因为没有实例化对象而找不到对应方法从而报错。
代码块
说到static,就不得不说一些很少见到的情况,比如代码块,代码块的存在有很多目的和意义。现在大家可能理解不到,我也不会对此有过多说明,单纯说明一下他们的执行规则:
匿名代码块
匿名代码块会在对象实例化的时候自动执行,而且会在对象构造器之前执行:
{
// 别疑惑,这就是匿名代码块
// 随便写什么,输出一些语句或者执行一些操作,干嘛都行
}
静态代码块
只要类加载(编译)的时候就会被执行,且只会被执行一次:
static{
}
静态导入包
这个不解释,现阶段没啥作用的骚操作,以后会有用的:我们可以在import关键字后加一个static来导入某一类中的方法:
import static java.lang.Math.random;
总结
本篇笔记记录了Java语言的三个特性:封装、继承、多态。在最后我不会加上抽象。接下来的话各位能理解就看,不能理解就右上角×掉。
抽象只是一个思想,可以理解为编程的核心。任何一门编程语言,无论是面向对象还是面向过程都离不开抽象这个概念,那么何为抽象?
之前说过,世间万物总会有一些能准确标示出它客观存在的特征,将这些按照一定条件归类的客观存在的特征抽取出来,就能组成一个宏观描述这一类存在的类,而这些将特征定性的存在就是这一类的实例化。
编程语言是人创造出来,它其中必然有人的思想,如果说学习编程语言就一定要按照计算机思想的话,那么就学不到编程的核心。我们在现实中解决问题的时候,都会遵循一个原则:透过现象看本质,在本质中抓到核心问题,只要解决核心问题,那么大多数问题就会迎刃而解。变成就是这个思想,获取解决问题的办法不同,但是殊途同归,思想都是一样的。
以上说法自己体会,我也不想解释什么,我懒。如果觉得不对呢,就不看,我也不想辩解。大家加油!