面向对象—【类与对象】【类的定义与对象创建】【对象的使用】【方法创建与使用】【方法进阶使用】【构造方法】
@
源码:
Gitee https://gitee.com/drip123456/java-se
GIthub https://github.com/Drip123456/JavaSE
专栏: JavaSE笔记专栏
面向对象基础篇
我们在前面已经学习了面向过程编程,也可以自行编写出简单的程序了。我们接着就需要认识 面向对象程序设计(Object Oriented Programming)它是我们在Java语言中要学习的重要内容,面向对象也是高级语言的一大重要特性。
面向对象是新手成长的一道分水岭,有的人秒懂,有的人直到最后都无法理解。
这一章开始难度就上来了,所以说请各位小伙伴一定认真。
类与对象
类的概念我们在生活中其实已经听说过很多了。
人类、鸟类、鱼类... 所谓类,就是对一类事物的描述,是抽象的、概念上的定义,比如鸟类,就泛指所有具有鸟类特征的动物。比如人类,不同的人,有着不同的性格、不同的爱好、不同的样貌等等,但是他们根本上都是人,所以说可以将他们抽象描述为人类。
对象是某一类事物实际存在的每个个体,因而也被称为实例(instance)我们每个人都是人类的一个实际存在的个体。
所以说,类就是抽象概念的人,而对象,就是具体的某一个人。
- A:是谁拿走了我的手机?
- B:是个人。(某一个类)
- A:我还知道是个人呢,具体是谁呢?
- B:是XXX。(具体某个对象)
而我们在Java中,也可以像这样进行编程,我们可以定义一个类,然后进一步创建许多这个类的实例对象。像这种编程方式,我们称为面向对象编程。
类的定义与对象创建
前面我们介绍了什么是类,什么是对象,首先我们就来看看如何去定义一个类。
比如现在我们想要定义一个人类,我们可以右键src
目录,点击创建新的类:
我们在对类进行命名时,一般使用英文单词,并且首字母大写,跟变量命名一样,不能出现任何的特殊字符。
可以看到,现在我们的目录下有了两个.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后面需要跟上 类名()
//这里创建出来的,就是一个具体的人了
}
实际上整个流程为:
只不过这里仅仅是创建出了这样的一个对象,我们目前没有办法去操作这个对象,比如想要修改或是获取这个人的名字等等。
对象的使用
既然现在我们知道如何创建对象,那么我们怎么去访问这个对象呢,比如我现在想要去查看或是修改它的名字。
我们同样可以使用一个变量来指代某个对象,只不过引用类型的变量,存储的是对象的引用,而不是对象本身:
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语言,它就类似于指针一样的存在)
我们可以来测试一下:
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类型,也是一个引用类型,我们会在下一章详细讨论。我们在上一章介绍的都是基本类型,而类使用的都是引用类型。
现在我们有了对象的引用之后,我们就可以进行操作了:
我们可以直接访问对象的一些属性,也就是我们在类中定义好的那些,对于不同的对象,这些属性都具体存放值也会不同。
比如我们可以修改对象的名字:
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);
}
我们来尝试运行一下这段代码:
此时程序在运行的过程中,出现了异常,虽然我们还没有学习到异常,但是各位可以将异常理解为程序在运行过程中出现了问题,此时不得不终止程序退出。
这里出现的是空指针异常,很明显是因为我们去操作一个值为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);
}
我们来看看运行结果:
我们可以看到,如果直接创建对象,那么对象的属性都会存在初始值,如果是基本类型,那么默认是统一为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+" 岁了!");
}
}
注意,方法名称同样可以随便起,但是规则跟变量的命名差不多,也是尽量使用小写字母开头的单词,如果是多个单词,一般使用驼峰命名法最规范。
现在我们给人类定义好了一个方法(行为)那么怎么才能让对象执行这个行为呢?
public static void main(String[] args) {
Person p = new Person();
p.name = "小明";
p.age = 18;
p.hello(); //我们只需要使用 . 运算符,就可以执行定义好的方法了,只需要 .方法名称() 即可
}
像这样执行定义好的方法,我们一般称为方法的调用,我们来看看效果:
比如现在我们要让人类学会加法运算,我们也可以通过定义一个方法的形式来完成,只不过,要完成加法运算,我们需要别人给人类提供两个参与加法运算的值才可以,所以我们这里就要用到参数了:
//我们的方法需要别人提供参与运算的值才可以
//我们可以为方法设定参数,在调用方法时,需要外部传入参数才可以
//参数的定义需要在小括号内部编写,类似于变量定义,需要填写 类型和参数名称,多个参数用逗号隔开
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
关键字之后,方法就会直接结束并返回结果,所以说在这之后编写的任何代码,都是不可到达的:
在return
后编写代码,会导致编译不通过,因为存在不可达语句。
如果我们的程序中出现了分支语句,那么必须保证每一个分支都有返回值才可以:
只要有任何一个分支缺少了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); //最后的结果会变成什么样子呢?
}
我们来看看结果是什么:
我们发现a和b的值并没有发生交换,但是按照我们的方法逻辑来说,应该是会交换才对,这是为什么呢?实际上这里仅仅是将值复制给了函数里面的变量而已(相当于是变量的赋值)
所以说我们交换的仅仅是方法中的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会是什么?
}
我们来看看结果:
不对啊,前面不是说只是值传递吗,怎么这里又可以修改成功呢?
确实,这里同样是进行的值传递,只不过各位小伙伴别忘了,我们前面可是说的清清楚楚,引用类型的变量,仅仅存放的是对象的引用,而不是对象本身。那么这里进行了值传递,相当于将对象的引用复制到了方法内部的变量中,而这个内部的变量,依然是引用的同一个对象,所以说这里在方法内操作,相当于直接操作外面的定义对象。
方法进阶使用
有时候我们的方法中可能会出现一些与成员变量重名的变量:
//我们希望使用这个方法,来为当前对象设定名字
void setName(String name) {
}
此时类中定义的变量名称也是name
,那么我们是否可以这样编写呢:
void setName(String name) {
name = name; //出现重名时,优先使用作用域最接近的,这里实际上是将方法参数的局部变量name赋值为本身
}
我们来测试一下:
public static void main(String[] args) {
Person p = new Person();
p.setName("小明");
System.out.println(p.name);
}
我们发现,似乎这样做并没有任何的效果,name依然是没有修改的状态。那么当出现重名的时候,因为默认情况下会优先使用作用域最近的变量,我们怎么才能表示要使用的变量是类的成员变量呢?
Person p = new Person();
p.name = "小明"; //我们之前在外面使用时,可以直接通过对象.属性的形式访问到
同样的,我们如果想要在方法中访问到当前对象的属性,那么可以使用this
关键字,来明确表示当前类的示例对象本身:
void setName(String name) {
this.name = name; //让当前对象的name变量值等于参数传入的值
}
这样就可以修改成功了,当然,如果方法内没有变量出现重名的情况,那么默认情况下可以不使用this
关键字来明确表示当前对象:
String getName() {
return name; //这里没有使用this,但是当前作用域下只有对象属性的name变量,所以说直接就使用了
}
我们接着来看方法的重载。
有些时候,参数类型可能会多种多样,我们的方法需要能够同时应对多种情况:
int sum(int a, int b){
return a + b;
}
public static void main(String[] args) {
Person p = new Person();
System.out.println(p.sum(10, 20)); //这里可以正常计算两个整数的和
}
但是要是我们现在不仅要让人类会计算整数,还要会计算小数呢?
当我们使用小数时,可以看到,参数要求的是int类型,那么肯定会出现错误,这个方法只能用于计算整数。此时,为了让这个方法支持使用小数进行计算,我们可以将这个方法进行重载。
一个类中可以包含多个同名的方法,但是需要的形式参数不一样,方法的返回类型,可以相同,也可以不同,但是仅返回类型不同,是不允许的!
int sum(int a, int b){
return a + b;
}
double sum(double a, double b){ //为了支持小数加法,我们可以进行一次重载
return a + b;
}
这样就可以正常使用了:
public static void main(String[] args) {
Person p = new Person();
//当方法出现多个重载的情况,在调用时会自动进行匹配,选择合适的方法进行调用
System.out.println(p.sum(1.5, 2.2));
}
包括我们之前一直在使用的println
方法,其实也是重载了很多次的,因为要支持各种值的打印。
注意,如果仅仅是返回值的不同,是不支持重载的:
当然,方法之间是可以相互调用的:
void test(){
System.out.println("我是test"); //实际上这里也是调用另一个方法
}
void say(){
test(); //在一个方法内调用另一个方法
}
如果我们这样写的话:
void test(){
say();
}
void say(){
test();
}
各位猜猜看会出现什么情况?
会导致栈溢出异常,这里大家不用细想,之后我们会在异常一章列讲解
此时又出现了一个我们不认识的异常,实际上什么原因导致的我们自己都很清楚,方法之间一直在相互调用,没有一个出口。
方法自己也可以调用自己:
void test(){
test();
}
像这样自己调用自己的行为,我们称为递归调用,如果直接这样编写,会跟上面一样,出现栈溢出错误。但是如果我们给其合理地设置出口,就不会出现这种问题,比如我们想要计算从1加到n的和:
int test(int n){
if(n == 0) return 0;
return test(n - 1) + n; //返回的结果是下一层返回的结果+当前这一层的n
}
是不是感觉很巧妙?实际上递归调用在很多情况下能够快速解决一些很麻烦的问题,我们会在后面继续了解。
构造方法
我们接着来看一种比较特殊的方法,构造方法。
我们前面创建对象,都是直接使用new
关键字就能直接搞定了,但是我们发现,对象在创建之后,各种属性都是默认值,那么能否实现在对象创建时就为其指定名字、年龄、性别呢?要在对象创建时进行处理,我们可以使用构造方法(构造器)来完成。
实际上每个类都有一个默认的构造方法,我们可以来看看反编译的结果:
public class Person {
String name;
int age;
String sex;
public Person() { //反编译中,多出来了这样一个方法,这其实就是构造方法
}
}
构造方法不需要填写返回值,并且方法名称与类名相同,默认情况下每个类都会自带一个没有任何参数的无参构造方法(只是不用我们去写,编译出来就自带)当然,我们也可以手动声明,对其进行修改: