面向对象基础

Java面向对象编程基础概念

第二课:JAVA面向对象基础

上一节课,我们学习了java的基本语法。这节课我们来学习“高级”一点的东西,面向对象。

面向对象的概念

面向对象编程(Object-Oriented Programming,OOP)是一种程序设计范式,它将程序中的数据和操作数据的方法封装成对象,以实现更清晰、模块化和可维护的代码。

看不懂对吧?看不懂就对了。(我第一次看这玩意也没看懂)所以接下来我们来通过案例具体学习面向对象基础。

类和对象的定义

我们先来看一个案例:

假如我们需要在程序中定义这样的几个变量:人的名字,人的年龄,狗的名字,狗的年龄。在不能随便给变量命名的前提下,我们一般会这么写。

1
2
3
4
5
6
7
// 人  
public String personName;
public int personAge;  
  
// 狗  
public String dogName;  
public int dogAge; 

这样的方式也是可行的,但假如我们还要加入更多的种类,比如猫,老鼠,…,这时候命名就会显得十分繁琐。并且,数据全部都集中在一起,代码的格式不规范的话非常容易混淆。这个时候,使用面向对象技术就会显得十分方便。

1
2
3
4
5
6
7
8
9
public class Person {  
public String name;  
public int age  
}  
  
public class Dog {  
public String name;  
public int age;  
}

正常来说两个类会分别写在两个文件中,我这里只是为了图个方便写在一起,别学。

上述代码中,我们就将人和狗分别封装成了一个__类__。在面向对象基础中,类的概念可以简单的理解为一个事物的种类。接下来,我们根据我们创建的类,来构建一个具体的人和狗。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import pojo.Person  
import poj.Dog  
  
public class demo1{  
public static void main(String[] args) {  
    // 创建人和狗的对象  
    Person person = new Person();  
    Dog dog = new Dog();  
      
    // 给人和狗的属性赋值  
    person.name = "张三";  
    person.age = 20;  
      
    dog.name = "旺财";  
    dog.age = 5;  
      
    //打印人和狗的属性  
    System.out.println(person.name + person.age);  
    System.out.println(dog.name + dog.age);  
      
    //tips:  
    //System.out.println() 可以用sout快捷输入  
}  
}

由此可以看出,__对象__可以看成一个类中具体的某一个个体。一个对象对应着一个类,而一个类可以有很多个对象。

我们再来看类和对象的定义:

: 类是一个模板,它描述了一类对象的行为和状态。例如,狗就是一个类,它的属性有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。

对象: 对象就是一个类的实例(instance),有行为和属性。比如旺财。

所以说,类就是抽象概念的狗,而对象,就是具体的某一条狗。

在Java编程中,我们会先抽象出一个类,然后再进一步的创建出这个类的实例,这样的编程方式也就是面向对象编程。

方法

在继续讲解面向对象的知识点之前,我们需要插入一个新的知识点:方法。

我们先来看看方法的定义:

方法的定义

1
2
3
4
5
6
修饰符 返回值类型 方法名(参数类型 参数名) {  
...  
方法体  
...  
return 返回值;  
}

修饰符:修饰符,这是可选的,告诉编译器如何调用该方法。定义了该方法的访问类型。具体有哪些上节课有具体讲解,不熟悉的话可以回去看看课件。

返回值类型 :方法可能会有返回值。返回值类型是方法返回值的数据类型。有些方法执行所需的操作,不需要返回值。在这种情况下,返回值类型是关键字void。

方法名:是方法的实际名称。方法名和参数表共同构成方法签名。

参数类型:参数像是一个占位符。当方法被调用时,传递值给参数。这个值被称为实参或变量。参数列表是指方法的参数类型、顺序和参数的个数。参数是可选的,方法可以不包含任何参数。

方法体:方法体包含具体的语句,定义该方法的功能。

return关键字

关于return关键字,我们还需要进行进一步的介绍。

return 关键字具有以下两种用途:

返回值: 当一个方法声明了返回类型时,必须使用 return 关键字返回一个与该类型兼容的值。返回值可以是基本类型、对象或者 null。

1
2
3
public int add(int a, int b) {  
return a + b; // 返回两个数的和作为方法的返回值  
}

方法终止: return 关键字会终止方法的执行,即在方法中的某个位置直接返回,则方法会直接终止。利用这个特性也可以提前结束方法的执行,跳出方法体。

1
2
3
4
5
6
7
8
9
public void printNumbers() {  
for (int i = 0; i < 10; i++) {  
    if (i == 5) {  
        return; // 当 i 等于 5 时,直接返回,跳出方法体  
    }  
    System.out.println(i);  
}  
return;  
}

需要注意的是,return 关键字只能在__方法内部__使用,不能在类的构造函数或静态代码块中使用。同时,如果方法声明了返回类型,那么在方法的所有执行路径上都必须包含返回语句,以保证方法总是返回一个值。

以下是一些关于 return 关键字的注意事项:

在一个方法中,可以有多个 return 语句。但是,只有其中一个 return 语句会被执行,其余的 return 语句都将被忽略。

如果一个方法声明了返回类型,但没有在所有执行路径上包含返回语句,编译器会报错。

实际上如果方法过于复杂编译器在编译时可能会无法确认该执行路径是否有return,但是在运行过程中执行改路径会抛出一个编译器无法捕获的异常,所以规范代码是非常有必要的

return 关键字也可以在条件语句中使用,例如在 if 或 switch 语句中根据条件返回不同的值。

当方法返回类型为 void 时,表示方法不返回任何值,但可以使用 return 关键字来提前结束方法的执行。

在一条执行路径中return后面是"不可达到的地方",所以说在这之后不能编写的任何代码。

在之前对类的定义中,我们可以看到类中除了状态,还可以有行为,而行为的具体实现就是方法,比如:

1
2
3
4
5
6
7
8
9
public class Person {  
public String name;  
public int age  
  
// 我们给人添加一个吃饭的方法,输出“吃饭”  
public void eat() {  
    System.out.println("吃饭");  
}  
}

方法重载

方法重载(Method Overloading)是指在一个类中定义多个方法,它们具有相同的名称但不同的参数列表。方法重载允许在同一个类中使用相同的方法名来执行不同的操作,根据传递给方法的参数的类型、个数和顺序来确定要调用的具体的方法。

方法重载的特点如下:

方法名称相同:重载的方法必须具有__相同的名称__。

参数列表不同:重载的方法的__参数列表必须不同__,可以是__参数的类型__、__个数或顺序__的不同。

返回类型可以不同:重载的方法的返回类型可以相同也可以不同,但仅根据返回类型不能区分重载方法。 (即两个方法不能仅有返回值不同!)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Calculator {  
public int add(int a, int b) {  
    return a + b;  
}  
  
public double add(double a, double b) {  
    return a + b;  
}  
  
// 注意,以下函数的定义会报错  
//public int add(double a, double b) {  
//    return a + b;  
//}  
// 原因即上面提到的第三点,两个方法不能仅有返回值不同  
  
public int add(int a, int b, int c) {  
    return a + b + c;  
}  
}

构造方法

此处我们介绍一个概念:构造方法。每个类都有构造方法。如果没有手动为类定义构造方法,Java 编译器将会为该类提供一个默认构造方法(即下面案例中的Person()方法,方法体中没有任何语句,也没有参数)。在__创建一个对象__的时候,至少要调用一个构造方法。构造方法的名称必须与类同名,一个类可以有多个构造方法。

我们来看看案例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Person {  
public String name;  
public int age  
  
public void eat() {  
    System.out.println("吃饭");  
}  
  
public Person() {  
      
}  
  
// 这里也是方法重载的一种,比上面的Person()方法多了两个参数  
public Person(String name, int age) {  
    this.name = name;  
    this.age = age;  
}  
  
// 以上两个都是构造方法  
// 没有参数的方法称为 无参构造  
// 有参数的方法称为 有参构造  
// 有参构造中,如果方法的参数包括了这个类所有的属性,则可以称为 全参构造  
}

在大部分情况下,构造方法的使用是为了给我们即将创建的对象进行__初始化__,但也不局限于这一个功能。运用构造方法,我们可以将之前创建对象的代码简化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import pojo.Person  
import pojo.Dog  

public class demo1 {  
public static void main(String[] args) {  
    Person person = new Person("张三", 20);  
    Dog dog = new Dog();  
      
    // Dog类中没有写全参构造,所以不可以通过全参构造进行初始化,只能用无参构造初始化后再对成员变量进行赋值  
    dog.name = "旺财";  
    dog.age = 5;  
      
    System.out.println(person.name + person.age);  
    System.out.println(dog.name + dog.ag);  
      
}  
}

this关键字

在上述代码中,我们发现Person的全参构造中出现了this关键字,他在上述代码中的作用是区分类属性中的两个变量和方法中的两个同名参数。那么this到底是什么呢?

this是一个引用数据类型,他的值是__调用该方法的对象__。在上述代码中,构造函数中的this就是指向person这个对象的引用数据类型。

大部分时候,普通方法访问其他方法、成员变量时无须使用this前缀,但如果方法里有个局部变量和成员变量同名,但程序又需要在该方法里访问这个被覆盖的成员变量,则必须使用this前缀。

小tip:我个人喜欢在类中的所有成员变量前都加上一个this关键字,这样方便区分变量是属于这个类的还是外部的。

类的三要素

封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)是面向对象编程的三大特性。

封装,把对象的属性和方法结合成一个独立的整体,隐藏实现细节,并提供对外访问的接口。

继承,从已知的一个类中派生出一个新的类,叫子类。子类实现了父类所有非私有化的属性和方法,并根据实际需求扩展出新的行为。

多态,多个不同的对象对同一消息作出响应,同一消息根据不同的对象而采用各种不同的方法。

封装

封装是将数据和操作数据的方法捆绑在一起的机制。通过封装,对象的内部状态和行为被隐藏起来,只对外部提供必要的访问接口。也就是说,封装思想其实就是把实现细节给隐藏了,外部只需知道这个方法是什么作用,而无需关心实现,要用什么由类自己来做,不需要外面来操作类内部的东西去完成。这样可以保证数据的安全性和一致性,并提高代码的可维护性和可复用性。封装就是通过访问权限控制来实现的。在Java中,使用访问修饰符(如private、protected、public,其中常用的只有public和private)来控制对类的成员的访问权限,同时提供公共的方法(getter和setter)来对数据进行操作。

我们据此来修改之前封装的Person类和Dog类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class Person {  
private String name;  
private int age;  
  
public void setName(String name) {  
    this.name = name;  
}  
  
public String getName() {  
    return this.name;  
}  
  
public void setAge(int age) {  
    this.age = age;  
}  
  
public int getage() {  
    return this.age;  
}  
  
public void eat() {  
    System.out.println("吃饭");  
}  
  
public Person() {  
      
}  
  
public Person(String name, int age) {  
    this.name = name;  
    this.age = age;  
}  
}  
  
public class Dog {  
private String name;  
private int age;  
  
public void setName(String name) {  
    this.name = name;  
}  
  
public String getName() {  
    return this.name;  
}  
  
public void setAge(int age) {  
    this.age = age;  
}  
  
public int getage() {  
   return this.age;  
}  
}

之前写的demo1也要随之改动:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import pojo.Person  
import pojo.Dog  
  
public class demo1 {  
public static void main(String[] args) {  
    Person person = new Person("张三", 20);  
    Dog dog = new Dog();  
      
    dog.setName("旺财");  
    dog.setage(5);  
      
    System.out.println(person.getName() + person.getAge());  
    System.out.println(dog.getName() + dog.getAge());  
      
}  
}

继承

继承是一种通过创建新类来扩展现有类的机制。子类(派生类)继承__父类__(基类)的属性和方法,并可以添加新的属性和方法。通过继承,可以实现代码的重用和层次化的组织。子类可以继承父类的__非私有成员__和__方法__(目前先这样简单记忆,具体子类如何继承父类中的成员有点复杂,有兴趣可以先自己去B站看一下黑马程序员的java课程,我们下节课细讲),并可以通过方法的覆盖(重写)来改变父类的行为。Java中使用关键字 extends 来实现继承关系。

比如说我们一开始创建的狗类,根据种类,我们可以将这个大类进一步地细分出来,例如:柯基类,柴犬类。实际上这些划分出来的类,本质上还是狗类,也就是说狗类具有的属性,这些划分出来的类同样具有,但是,这些划分出来的类同时也会拥有他们自己独特的属性和技能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Dog {  
private String name;  
private int age;  
  
public void setName(String name) {  
    this.name = name;  
}  
  
public String getName() {  
    return this.name  
}  
  
public void setAge(int age) {  
    this.age = age;  
}  
  
public int getAge() {  
    return this.age;  
}  
  
public void eat() {  
    System.out.println("吃饭");  
}  
}  
  
public class Husky extends Dog {  
public void eat() {  
    System.out.println("哈士奇吃饭");  
}  
  
public void dismantle() {  
    System.out.println("哈士奇拆家");  
}  
}  
  
public class Corgi extends Dog{  
public void eat() {  
    System.out.println("柯基吃饭");  
}  
  
public void sleep() {  
    System.out.println("柯基睡觉");  
}  
}

类的继承可以不断向下,但是同时只能继承一个类,同时,标记为final的类不允许被继承。(final关键字我们下节课细讲)

是不是感觉非常人性化,子类继承了父类的全部能力,同时还可以扩展自己的独特能力,就像一句话说的: 龙生龙凤生凤,老鼠儿子会打洞。

方法重写

上述案例中,Husky和Corgi类中都重写了父类(Dog类)中的eat方法。

我们如果不希望子类重写某个方法,我们可以在方法前添加final关键字,表示这个方法已经是最终形态

我们在重写父类方法时,如果希望调用父类原本的方法实现,那么同样可以使用super关键字

然后就是访问权限的问题,子类在重写父类方法时,不能降低父类方法中的可见性

因为子类实际上可以当做父类使用,如果子类的访问权限比父类还低,那么在被当做父类使用时,就可能出现无视访问权限调用的情况,这样肯定是不行的,但是相反的,我们可以在子类中提升权限

多态

多态是指对象在不同情境下表现出不同的行为。在Java中,多态性允许使用基类类型的引用变量来引用派生类的对象。这样可以在运行时根据__实际对象的类型__选择执行对应的方法。多态性通过__方法的重写__和__方法的重载__来实现。方法的重写是子类对父类方法的重新定义,而方法的重载是在同一个类中根据不同的参数列表来定义多个方法。

例如,在之前继承那部分的代码的基础上,我们可以这么写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import pojo.Dog  
import pojo.Husky  
import pojo.Corgi  
  
public class demo1 {  
public static void main(String[] args) {  
    Dog dog = new Husky();  
    dog.eat();  
    //dog.dismantle();  
      
    dog = new Corgi();  
    dog.eat();  
    //dog.sleep();  
}  
}

上述代码中,dog变量的类型虽然是Dog类,但调用的eat()函数却是Husky和Corgi的函数,这就是多态的具体表现,即__父类变量引用子类对象,从而实现子类的方法__。其中值得注意的是,dog变量虽然引用的是Husky和Corgi的对象,但他的类型仍然是Dog,所以不能调用子类独有的 dismantle() 和 sleep() 方法。

通过__子类重写父类中的方法__(要求两个方法拥有相同的方法签名(方法名称、参数列表和返回类型))来实现的多态,叫做__动态绑定__或__运行时多态;__

在这里我们便可以看出,多态的实现有以下三种必要的条件

满足继承关系

要有重写

父类引用指向子类对象

除了这种方式之外,还有一种实现多态的方法,叫做__接口回调__(Interface Callback)。

接口

接口(Interface)是一种特殊的类,它只包含方法,基本上不包含变量。在Java中,类的继承不支持多继承,即一个类只能继承一个类,但可以实现多个接口。我们来用接口修改一下之前的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
interface Action {  
// 声明一个方法,接口中不用给出方法的实现,这叫”抽象“,具体细节下节课讲  
public void act();  
}  
  
public class Husky extends Dog implements Action {  
public void eat() {  
    System.out.println("哈士奇吃饭");  
}  
  
@Override  
public void act() {  
    System.out.println("哈士奇拆家");  
}  
}  
  
public class Corgi extends Dog implements Action {  
public void eat() {  
    System.out.println("柯基吃饭");  
}  
  
@Override  
public void act() {  
    System.out.println("柯基睡觉");  
}  
}

@Override是一个__注解__,表示这个方法是重写的父类中的方法或实现的接口中的方法。注解不是必须的,但能更好的规范我们的代码。更多关于注解的知识我们以后会学习。

接下来我们通过接口回调的方式再来实现一次多态。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import pojo.Dog  
import pojo.Husky  
import pojo.Corgi  
import pojo.Action  
  
public class demo1 {  
public static void main(String[] args) {  
    Action action = new Husky();  
    action.act();  
      
    action = new Corgi();  
    action.act();  
}  
}

总的来说,接口回调看上去和动态绑定没有很大区别,唯一的区别就在于声明的变量的类型从一个类变成了一个接口。

接下来我们再介绍一个运算词instanceof,它可以方便我们更直观的观察多态的实现

instanceof

他的语法形式如下

1
对象 instanceof /接口

instanceof 运算符返回一个布尔值,如果对象是指定类的实例或者实现了指定接口,返回 true;否则,返回 false。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import pojo.Dog  
import pojo.Husky  
import pojo.Corgi  
import pojo.Action  
  
public class demo1 {  
public static void main(String[] args) {  
    Husky husky = new Husky();  
    System.out.println(hunsky instanceof Action);  
    System.out.println(hunsky instanceof Dog);  
    System.out.println(hunsky instanceof Corgi);  
}  
}

从结果可以看出,Husky继承了Dog类,实现了Action接口,但没有继承Corgi类;

使用 instanceof 运算符可以在运行时根据对象的实际类型进行条件判断。它可以避免类型转换的错误和异常,并增加程序的安全性和灵活性。

需要注意的是,instanceof 运算符也可以用于判断对象是否是其父类或祖先类的实例。例如,如果一个对象是某个类的实例,那么它一定也是该类的父类或祖先类的实例。

课后作业

1.先去自己实现一下上课讲的代码,看看还有没有什么问题。

2.自己写一个动物园类,里面可以包括各种动物类,属性和方法都可以自己定,尽量去使用多态(多态这块确实难以理解,建议用课余时间多看看课件或者写写代码熟悉一下)。

3.下节课还是鼠鼠我上,这节课有一部分知识点本来是打算放到下节课讲的,但真放下节课了我觉得讲不完,所以这节课提了一嘴,很多细节知识点我没有讲。如果自己感兴趣,可以去B站上自己看看java的基础课程(鼠鼠我很多东西也是自己在B站上学的,我也在此建议自己多用课后时间看看网上的课程或者文章),再不懂也可以来问我。