image-20220918121719900

面向对象基础篇

我们在前面已经学习了面向过程编程,也可以自行编写出简单的程序了。我们接着就需要认识 面向对象程序设计(Object Oriented Programming)它是我们在Java语言中要学习的重要内容,面向对象也是高级语言的一大重要特性。

面向对象是新手成长的一道分水岭,有的人秒懂,有的人直到最后都无法理解。

这一章开始难度就上来了,所以说请各位小伙伴一定认真。

类与对象

类的概念我们在生活中其实已经听说过很多了。

人类、鸟类、鱼类... 所谓类,就是对一类事物的描述,是抽象的、概念上的定义,比如鸟类,就泛指所有具有鸟类特征的动物。比如人类,不同的人,有着不同的性格、不同的爱好、不同的样貌等等,但是他们根本上都是人,所以说可以将他们抽象描述为人类。

对象是某一类事物实际存在的每个个体,因而也被称为实例(instance)我们每个人都是人类的一个实际存在的个体。

image-20220919203119479

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

  • A:是谁拿走了我的手机?
  • B:是个人。(某一个类)
  • A:我还知道是个人呢,具体是谁呢?
  • B:是XXX。(具体某个对象)

而我们在Java中,也可以像这样进行编程,我们可以定义一个类,然后进一步创建许多这个类的实例对象。像这种编程方式,我们称为面向对象编程。

类的定义与对象创建

前面我们介绍了什么是类,什么是对象,首先我们就来看看如何去定义一个类。

比如现在我们想要定义一个人类,我们可以右键src目录,点击创建新的类:

image-20220919204004526

我们在对类进行命名时,一般使用英文单词,并且首字母大写,跟变量命名一样,不能出现任何的特殊字符。

image-20220919204159248

可以看到,现在我们的目录下有了两个.java源文件,其中一个是默认创建的Main.java,还有一个是我们刚刚创建的类。

我们来看看创建好之后,一个类写了哪些内容:

public class Person {
  
}

可以发现,这不是跟一开始创建的Main中写的格式一模一样吗?没错,Main也是一个类,只不过我们一直都将其当做主类在使用,也就是编写主方法的类,关于方法我们会在后面进行介绍。

现在我们就创建好了一个类,既然是人类,那么肯定有人相关的一些属性,比如名字、性别、年龄等等,那么怎么才能给这个类添加一些属性呢?

我们可以将这些属性直接作为类的成员变量(成员变量相当于是这个类所具有的属性,每个实例创建出来之后,这些属性都可能会各不相同)定义到类中。

public class Person {   //这里定义的人类具有三个属性,名字、年龄、性别
    String name;   //直接在类中定义变量,表示类具有的属性
    int age;
    String sex;
}

可能会有小伙伴疑问,这些变量啥时候被赋值呢?实际上这些变量只有在一个具体的对象中才可以使用。

那么现在人类的属性都规定好了,我们就可以尝试创建一个实例对象了,实例对应的应该是一个具体的人:

new 类名();
public static void main(String[] args) {
    new Person();   //我们可以使用new关键字来创建某个类的对象,注意new后面需要跟上 类名()
  	//这里创建出来的,就是一个具体的人了
}

实际上整个流程为:

image-20220919205550104

只不过这里仅仅是创建出了这样的一个对象,我们目前没有办法去操作这个对象,比如想要修改或是获取这个人的名字等等。

对象的使用

既然现在我们知道如何创建对象,那么我们怎么去访问这个对象呢,比如我现在想要去查看或是修改它的名字。

我们同样可以使用一个变量来指代某个对象,只不过引用类型的变量,存储的是对象的引用,而不是对象本身:

public static void main(String[] args) {
  	//这里的a存放的是具体的某个值
  	int a = 10;
  	//创建一个变量指代我们刚刚创建好的对象,变量的类型就是对应的类名
  	//这里的p存放的是对象的引用,而不是本体,我们可以通过对象的引用来间接操作对象
    Person p = new Person();
}

至于为什么对象类型的变量存放的是对象的引用,比如:

public static void main(String[] args) {
    Person p1 = new Person();
    Person p2 = p1;
}

这里,我们将变量p2赋值为p1的值,那么实际上只是传递了对象的引用,而不是对象本身的复制,这跟我们前面的基本数据类型有些不同,p2和p1都指向的是同一个对象(如果你学习过C语言,它就类似于指针一样的存在)

image-20220919211443657

我们可以来测试一下:

public static void main(String[] args) {
    Person p1 = new Person();
    Person p2 = p1;
    System.out.println(p1 == p2);    //使用 == 可以判断两个变量引用的是不是同一个对象
}

但是如果我们像这样去编写:

public static void main(String[] args) {
    Person p1 = new Person();   //这两个变量分别引用的是不同的两个对象
    Person p2 = new Person();
    System.out.println(p1 == p2);   //如果两个变量存放的是不同对象的引用,那么肯定就是不一样的了
}

实际上我们之前使用的String类型,也是一个引用类型,我们会在下一章详细讨论。我们在上一章介绍的都是基本类型,而类使用的都是引用类型。

现在我们有了对象的引用之后,我们就可以进行操作了:

image-20220919210058797

我们可以直接访问对象的一些属性,也就是我们在类中定义好的那些,对于不同的对象,这些属性都具体存放值也会不同。

比如我们可以修改对象的名字:

public static void main(String[] args) {
    Person p = new Person();
    p.name = "小明";   //要访问对象的属性,我们需要使用 . 运算符
    System.out.println(p.name);   //直接打印对象的名字,就是我们刚刚修改好的结果了
}

注意,不同对象的属性是分开独立存放的,每个对象都有一个自己的空间,修改一个对象的属性并不会影响到其他对象:

public static void main(String[] args) {
    Person p1 = new Person();
    Person p2 = new Person();
    p1.name = "小明";   //这个修改的是第一个对象的属性
    p2.name = "大明";   //这里修改的是第二个对象的属性
    System.out.println(p1.name);  //这里我们获取的是第一个对象的属性
}

关于对象类型的变量,我们也可以不对任何对象进行引用:

public static void main(String[] args) {
    Person p1 = null;  //null是一个特殊的值,它表示空,也就是不引用任何的对象
}

注意,如果不引用任何的对象,那肯定是不应该去通过这个变量去操作所引用的对象的(都没有引用对象,我操作谁啊我)

虽然这样可以编译通过,但是在运行时会出现问题:

public static void main(String[] args) {
    Person p = null;   //此时变量没有引用任何对象
    p.name = "小红";   //我任性,就是要操作
    System.out.println(p.name);
}

我们来尝试运行一下这段代码:

image-20220919213732810

此时程序在运行的过程中,出现了异常,虽然我们还没有学习到异常,但是各位可以将异常理解为程序在运行过程中出现了问题,此时不得不终止程序退出。

这里出现的是空指针异常,很明显是因为我们去操作一个值为null的变量导致的。在我们以后的学习中,这个异常是出现频率最高的。

我们来看最后一个问题,对象创建成功之后,它的属性没有进行赋值,但是我们前面说了,变量使用之前需要先赋值,那么创建对象之后能否直接访问呢?

public static void main(String[] args) {
    Person p = new Person();
    System.out.println("name = "+p.name);
    System.out.println("age = "+p.age);
    System.out.println("sex = "+p.sex);
}

我们来看看运行结果:

image-20220919214248053

我们可以看到,如果直接创建对象,那么对象的属性都会存在初始值,如果是基本类型,那么默认是统一为0(如果是boolean的话,默认值为false)如果是引用类型,那么默认是null。

方法创建与使用

前面我们介绍了类的定义以及对象的创建和使用。

现在我们的类有了属性,我们可以为创建的这些对象设定不同的属性值,比如每个人的名字都不一样,性别不一样,年龄不一样等等。只不过光有属性还不行,对象还需要具有一定的行为,就像我们人可以行走,可以跳跃,可以思考一样。

而对象也可以做出一些行为,我们可以通过定义方法来实现(在C语言中叫做函数)

方法是语句的集合,是为了完成某件事情而存在的。完成某件事情,可以有结果,也可以做了就做了,不返回结果。比如计算两个数字的和,我们需要得到计算后的结果,所以说方法需要有返回值;又比如,我们只想吧数字打印在控制台,只需要打印就行,不用给我结果,所以说方法不需要有返回值。

方法的定义如下:

返回值类型 方法名称() {
		方法体...
}

首先是返回值类型,也就是说这个方法完成任务之后,得到的结果的数据类型(可以是基本类型,也可以是引用类型)当然,如果没有返回值,只是完成任务,那么可以使用void表示没有返回值,比如我们现在给人类编写一个自我介绍的行为:

public class Person {
    String name;
    int age;
    String sex;

  	//自我介绍只需要完成就行,没有返回值,所以说使用void
    void hello(){
      	//完成自我介绍需要执行的所有代码就在这个花括号中编写
      	//这里编写代码跟我们之前在main中是一样的(实际上main就是一个函数)
      	//自我介绍需要用到当前对象的名字和年龄,我们直接使用成员变量即可,变量的值就是当前对象的存放值
        System.out.println("我叫 "+name+" 今年 "+age+" 岁了!");
    }
}

注意,方法名称同样可以随便起,但是规则跟变量的命名差不多,也是尽量使用小写字母开头的单词,如果是多个单词,一般使用驼峰命名法最规范。

image-20220920101033325

现在我们给人类定义好了一个方法(行为)那么怎么才能让对象执行这个行为呢?

public static void main(String[] args) {
    Person p = new Person();
    p.name = "小明";
    p.age = 18;
    p.hello();    //我们只需要使用 . 运算符,就可以执行定义好的方法了,只需要 .方法名称() 即可
}

像这样执行定义好的方法,我们一般称为方法的调用,我们来看看效果:

image-20220919220837991

比如现在我们要让人类学会加法运算,我们也可以通过定义一个方法的形式来完成,只不过,要完成加法运算,我们需要别人给人类提供两个参与加法运算的值才可以,所以我们这里就要用到参数了:

//我们的方法需要别人提供参与运算的值才可以
//我们可以为方法设定参数,在调用方法时,需要外部传入参数才可以
//参数的定义需要在小括号内部编写,类似于变量定义,需要填写 类型和参数名称,多个参数用逗号隔开
int sum(int a, int b){   //这里需要两个int类型的参数进行计算

}

那么现在参数从外部传入之后,我们怎么使用呢?

int sum(int a, int b){   //这里的参数,相当于我们在函数中定义了两个局部变量,我们可以直接在方法中使用
    int c = a + b;   //直接c = a + b
}

那么现在计算完成了,我们该怎么将结果传递到外面呢?首先函数的返回值是int类型,我们只需要使用return关键字来返回一个int类型的结果就可以了:

int sum(int a, int b){
    int c = a + b;
    return c;   //return后面紧跟需要返回的结果,这样就可以将计算结果丢出去了
  	//带返回值的方法,是一定要有一个返回结果的!否则无法通过编译!
}

我们来测试一下吧:

public static void main(String[] args) {
    Person p = new Person();
    p.name = "小明";
    p.age = 18;
    int result = p.sum(10, 20);    //现在我们要让这个对象帮我们计算10 + 20的结果
    System.out.println(result);    //成功得到30,实际上这里的println也是在调用方法进行打印操作
}

注意:方法定义时编写的参数,我们一般称为形式参数,而调用方法实际传入的参数,我们成为实际参数。

是不是越来越感觉我们真的在跟一个对象进行交互?只要各位有了这样的体验,基本上就已经摸到面向对象的门路了。

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

在我们使用return关键字之后,方法就会直接结束并返回结果,所以说在这之后编写的任何代码,都是不可到达的:

image-20220919222813469

在return后编写代码,会导致编译不通过,因为存在不可达语句。

如果我们的程序中出现了分支语句,那么必须保证每一个分支都有返回值才可以:

image-20220919223037197

只要有任何一个分支缺少了return语句,都无法正常通过编译,总之就是必须考虑到所有的情况,任何情况下都必须要有返回值。

当然,如果方法没有返回值,我们也可以使用return语句,不需要跟上任何内容,只不过这种情况下使用,仅仅是为了快速结束方法的执行:

void test(int a){
    if(a == 10) return;    //当a等于10时直接结束方法,后面无论有没有代码都不会执行了
    System.out.println("Hello World!");   //不是的情况就正常执行
}

最后我们来讨论一下参数的传递问题:

void test(int a){   //我们可以设置参数来让外部的数据传入到函数内部
    System.out.println(a);
}

实际上参数的传递,会在调用方法的时候,对参数的值进行复制,方法中的参数变量,不是我们传入的变量本身,我们来下面的这个例子:

void swap(int a, int b){   //这个函数的目的很明显,就是为了交换a和b的值
    int tmp = a;
    a = b;
    b = a;
}

那么我们来测试一下:

public static void main(String[] args) {
    Person p = new Person();
    int a = 5, b = 9;   //外面也叫a和b
    p.swap(a, b);
    System.out.println("a = "+a+", b = "+b);   //最后的结果会变成什么样子呢?
}

我们来看看结果是什么:

image-20220919224219071

我们发现a和b的值并没有发生交换,但是按照我们的方法逻辑来说,应该是会交换才对,这是为什么呢?实际上这里仅仅是将值复制给了函数里面的变量而已(相当于是变量的赋值)

image-20220919224623727

所以说我们交换的仅仅是方法中的a和b,参数传递仅仅是值传递,我们是没有办法直接操作到外面的a和b的。

那么各位小伙伴看看下面的例子:

void modify(Person person){
    person.name = "lbwnb";   //修改对象的名称
}
public static void main(String[] args) {
    Person p = new Person();
    p.name = "小明";     //先在外面修改一次
    p.modify(p);        //调用方法再修改一次
    System.out.println(p.name);    //请问最后name会是什么?
}

我们来看看结果:

image-20220919224957971

不对啊,前面不是说只是值传递吗,怎么这里又可以修改成功呢?

确实,这里同样是进行的值传递,只不过各位小伙伴别忘了,我们前面可是说的清清楚楚,引用类型的变量,仅仅存放的是对象的引用,而不是对象本身。那么这里进行了值传递,相当于将对象的引用复制到了方法内部的变量中,而这个内部的变量,依然是引用的同一个对象,所以说这里在方法内操作,相当于直接操作外面的定义对象。

image-20220919225455752

方法进阶使用

有时候我们的方法中可能会出现一些与成员变量重名的变量:

//我们希望使用这个方法,来为当前对象设定名字
void setName(String name) {
   
}

该博客转载自B站UP青空的霞光