1.面对对象的概念
1.1.面向过程
- C语言
- 当开始一个功能时,看重中间过程,每一个步骤都需要自己去完成。
- 优点:面向过程的性能比面向对象高,因为面向对象把所有的事务看成对象,涉及到对象的实例化。
- 缺点:不容易维护,不容易扩展
1.2.面向对象
- Java js C#
- 将功能封装成对象,不看重中间过程,有对象则用对象,没有对象则创造对象,之后还要维护对象之间的关系
- 优点:容易维护,容易扩展
- 缺点:内存开销大,性能低
1.3.举例
面向对象相对于面向过程省去的那些步骤都通过对象完成,例如洗衣服通过洗衣机完成,买电脑通过秘书完成,吃饭通过出去买(实际上是店家做完饭)。
比如下列代码有一个数组,我想通过指定格式打印数组。
面向过程我需要把具体的执行流程写出来,然后打印,在此期间,我们注重的是这个过程;但是如果我们创造一个类,把这个过程封装成一个函数,当作这个类的成员方法,然后再创造这个类的对象,通过这个对象调用这个方法也可以实现功能,这就是面向对象。
public static void main(String[] args) {
int[] arrs = {10, 12, 89, 90, 78, 11};
/**
* 把数组按照指定格式输出[10, 12, 89, 90, 78, 11]
*/
//面向过程
System.out.print("[");
for (int i = 0; i < arrs.length; i++) {
if (i == arrs.length - 1) {
System.out.print(arrs[i] + "]");
} else {
System.out.print(arrs[i] + ",");
}
}
//面向对象
//Arrays:把数组按照相应的格式进行输出
System.out.println(Arrays.toString(arrs));
}
1.4.面向对象三大特征(重要)
- 封装
- 继承
- 多态
三大特征在下面会讲。
2.类和对象
2.1.简单说明
-
类是对一类事物描述,是抽象的、概念上的定义。
-
对象是实际存在的该类事物的每个个体,是具体的。
-
以上的描述很抽象,下面用一个实际的例子来说明。
-
如果将对象比作汽车,那么类就是汽车的图纸。
-
-
对象叫做类的实例化(Instance),类不占内存,对象才占内存。
2.2.举例说明
/**
* Car类
* 汽车图纸包含两部分
* 属性:颜色 价格 品牌
* 行为:行驶 停止
* 映射到类里面也包含两部分
* 成员变量(属性):直接定义在类里面方法之外的变量
* 成员方法(行为):把static去掉
*
//汽车图纸类 不能直接使用
public class Car {
//成员变量(属性)
//成员变量保存在堆里,在堆里的变量都有默认值 String -> null
String color;
int price;
String brand;
double width;
double length;
//成员方法(行为)
public void run() {
System.out.println("一辆汽车颜色是" + color + ",价格是" + price
+ ",品牌是" + "brand" + ",长度是" + length + ",宽度是" + width
+ "的小汽车在路上嗡嗡嗡地跑着");
}
public void stop() {
System.out.println("汽车停止行驶了");
}
}
/**
* TestCar类
* 用来测试Car类
*/
public class TestCar {
public static void main(String[] args) {
//创建一辆小汽车
Car car1 = new Car();//new出来的在堆里面
car1.brand = "五菱宏光";
car1.color = "红色";
car1.length = 8;
car1.width = 2;
car1.price = 1000000;
car1.run();
car1.stop();
}
}
3.对象的存储(重要)
3.1.数据默认值问题
当一个对象被创建时,会对其中各种类型的成员变量自动进行初始化赋值。除了基本数据类型之外都是引用类型。
3.2.内存分配
运行一段程序,需要申请内存,内存都是归jvm进行管理的。
内存分为五部分:
- 栈(Stack):主要存放局部变量(定义在方法里的变量)。
- 堆(Heap):主要存放new出来的东西,在堆里面的变量都是成员变量,都有默认值,参考3.1。
- 方法区(Method Area):主要存放class文件。
- 本地方法区(Native Method Area):与操作系统有关。(不做了解)
- 寄存器(Register):主要与CPU有关。(不做了解)
下面用一个具体的例子说明程序运行时内存的具体分配流程,主要用到栈、堆和方法区(只是初步学习,静态变量、常量等的内存分配情况在以后的文章中会涉及到)。
先写一个Car类
public class Car {
//成员变量(属性)
//成员变量保存在堆里,在堆里的变量都有默认值 String -> null
String color;
int price;
String brand;
double width;
double length;
//成员方法(行为)
public void run() {
System.out.println("一辆汽车颜色是" + color + ",价格是" + price
+ ",品牌是" + "brand" + ",长度是" + length + ",宽度是" + width
+ "的小汽车在路上嗡嗡嗡地跑着");
}
public void stop() {
System.out.println("汽车停止行驶了");
}
}
再写一个TestCar类
public class TestCar {
public static void main(String[] args) {
Car car1 = new Car();
car1.brand = "五菱宏光";
car1.color = "红色";
car1.length = 8;
car1.width = 2;
car1.price = 1000000;
car1.run();
car1.stop();
Car car2 = new Car();
car2.brand = "玛莎拉蒂";
car2.color = "白色";
car2.length = 4;
car2.width = 2;
car2.price = 4000000;
car2.run();
}
}
- 首先先把编译后的两个class文件加载到方法区,包括属性和方法,注意Car类和TestCar类都要加载。加载成员方法时,每个成员方法会有一个地址,后面会用到。
- 然后运行TestCar里的main方法,main方法进栈(只要方法执行,那么这个方法就要进栈,执行完后就出栈),注意栈这种数据结构是先进后出,一般先进栈的我们都画在底部。
- 执行Car car1 = new Car();注意这条语句执行的时候分为两步首先Car car1;car1保存在栈,然后car1 = new Car();
(main方法在栈里,方法里的变量成为局部变量,所以说栈里面存放局部变量)。 - new的过程其实是在堆里开辟一块空间,从方法区加载成员变量和成员方法。加载成员变量后给变量赋值,加载成员方法很特殊,它的值为方法区方法的地址值(也可以说指向方法区的方法)。new出来的这一块空间会有一个地址值,栈里的car1的值为这个地址(也可以说成car1指向那块内存)。
- 然后执行car1.brand = “五菱宏光”;通过0x111找到堆里的对象,然后找到对应的成员变量,然后给这些变量赋值,这些变量的值不再是默认值(只简单说明一个属性,其他属性与此相同)。
- 然后执行run(),只要方法执行,那这个方法就要进栈。通过car1找到堆里的内存,通过方法名找到对应方法,堆里的方法是指向方法区的,所以形成了栈指向堆,堆指向方法区,方法体是加载在方法区里的,堆里只存了方法的地址。然后执行方法体,执行完后,此方法出栈(注意是main先进栈,然后run进栈,然后run先出栈,这就对应了栈的特点,与数据结构相联系),这里我们只举例run方法的执行,stop方法的执行与此相同。
- 然后main方法执行完毕,main方法出栈。
废话少说,直接上图👇
4.成员变量和局部变量
我们主要清楚二者区别即可:
- 定义的位置不同
- 成员变量定义在类内部,方法外部
- 局部变量定义在方法内部,形参也可以看作局部变量。
- 作用域不同
- 成员变量作用在类的内部
- 局部变量作用在方法内部
- 默认值
- 成员变量有默认值
- 局部变量没有默认值,在没有赋值的情况下使用会报错
- 内存中的位置
- 成员变量保存在堆里
- 局部变量保存在栈里
- 访问修饰符
- 成员变量可以使用四个修饰符
- 局部变量不能通过访问修饰符修饰,只能在方法内使用
用代码来看一下:
public class Demo2 {
//类的属性=成员变量
String name;
static int age;
public static void setAge(int age1){//age1是形参,是一个局部变量
String sex = "male";//sex没赋值的时候,编译不通过
age1 = age;
System.out.println(sex);
}
//main是静态方法,一个静态方法里面只能调用静态变量和静态方法
public static void main(String[] args) {
System.out.println(age);//0
//System.out.println(age1);//报错:找不到age1变量
}
}
5.参数传递
5.1.按值传递
按值传递是指一个参数传递给一个函数时,函数接受的是原始值的一个副本。因此,如果函数修改了参数,仅仅只是修改了副本,原始值保持不变。
废话少说,直接上代码👇
public class Demo3 {
public static void main(String[] args) {
int a = 10;
int b = 10;
System.out.println("a = " + a);//10
System.out.println("b = " + b);//10
System.out.println("=========================");
change(a, b);// 100 ,100
System.out.println("=========================");
System.out.println("a = " + a);//10
System.out.println("b = " + b);//10
}
//把形参扩大十倍
public static void change(int a, int b) {
a *= 10;
b *= 10;
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
5.2.按引用传递
按引用传递是指当将一个参数传递给一个函数时,函数接受的是原始值的内存地址,而不是值得副本。因此,如果函数修改了参数,原始值也会随之改变。
废话少说,直接上代码👇
public class Demo4 {
public static void main(String[] args) {
Car car = new Car();
car.brand = "奔驰";
System.out.println(car.brand);//奔驰
System.out.println("======================");
info(car);//宝马
System.out.println("======================");
System.out.println(car.brand);//宝马
}
public static void info(Car car) {
car.brand = "宝马";
System.out.println(car.brand);
}
}
6.封装
先来写一段代码
public class Student {
String stuNo;
String stuName;
//private只能在当前类的内部去使用,出了此类不能用
private int age;
//个人描述
public void desc() {
System.out.println("我的学号是:" + stuNo + ",我的姓名是:" + stuName + ",我的年龄是:" + age);
}
public void setAge(int age1) {
//对年龄进行限制
if (age1 >= 0 && age1 < 120) {
age = age1;
} else {
System.out.println("你输入的年龄不合法");
}
}
public int getAge() {
return age;
}
}
public class TestStudent {
public static void main(String[] args) {
Student stu = new Student();//new的就是一个构造方法
stu.stuNo = "1001";
stu.stuName = "尼古拉斯赵四";
//stu.age = -50;
stu.setAge(-50);//年龄不能是负数
//比如我想单独获取age属性的值
System.out.println(stu.getAge());
stu.desc();
}
}
首先,我们写了一个Student类和一个TestStudent类,我们在TestStudent类里面创建一个main方法,在main方法里创建一个Student对象,我们可以直接通过’对象名.属性’的方式赋值,但是这就出现了第一个问题,直接通过这种方式赋值可能会出现逻辑错误,比如给age赋值-50,显然年龄不可能是负数,这明显与实际情况不符,所以我们不能使用’对象名.属性’的方式进行赋值,而是通过创建一个方法,通过’对象名.方法名’进行赋值,因为我们可以在方法里写逻辑代码对属性进行限制,所以我们写了setAge(int age1)方法,并且这个方法是用public进行修饰的,方法里写了对年龄限制的逻辑代码,实现了对年龄的限制,第一个问题解决了。(关于修饰符的问题,我们在以后的文章进行讲解)。
但是还有第二个问题,我们发现还是可以通过’对象名.属性’对属性进行赋值,解决这个问题的方法就是用private对属性进行修饰。用private修饰的变量只能在类内部使用,除了类就不能使用。
解决了以上两个问题,就实现了对象的封装。
总结一下封装实现的步骤:
- 修改属性的可见性为private。
- 创建getter和setter方法(可以自动生成)。
- 在getter和setter方法中写入对属性的业务逻辑代码,以此实现对属性的限制。
7.构造方法
解决了上面两个问题就实现了封装,但是又出现了第三个问题,当类中有很多属性时,用setter方法赋值很繁琐也很容易忘记,为了解决这个问题,我们就会用到了构造器,也称为构造方法。
构造方法是java中的一个特殊的方法,它的最大的作用就是创建对象。
//语法
public School() {
}
/** 注意:
* 1.构造方法无返回值
* 2.构造方法名与类名一致
*/
在定义一个类的时候,如果没有写构造方法,程序默认会提供一个无参的构造方法,所以说在java中至少有一个构造方法,那就是这个默认的构造方法,其实当没有写构造方法的时候,School sc = new School();就是用了程序默认提供的无参构造函数。
如果手动写了构造方法,系统将不会再提供无参构造方法,例如👇:
//一个参数的构造函数
public School(String name1) {
System.out.println("一个参数的构造方法执行了");
this.name = name1;
}
//两个参数的构造函数
public School(String name1, String address1) {
System.out.println("两个参数的构造方法执行了");
this.name = name1;
this.address = address1;
}
上面的代码我们写了两个构造方法,那么当我们再通过School sc = new School();创建对象就会报错。所以说,如果你定义了有参构造方法,一般要再定义上无参构造方法,因为极大情况下你都要使用无参构造方法创建对象。
其实写无参构造方法和有参构造方法原理就是用到了方法的重载,重载会在后面章节讲到。
下面通过一个例子来说明下👇
public class School {
private String name;
private String address;
public String getName() {
return name;
}
public void setName(String name1) {
this.name = name1;
}
public String getAddress() {
return address;
}
public void setAddress(String address1) {
this.address = address1;
}
//没参数的构造函数
public School() {
System.out.println("无参构造方法执行了");
}
//一个参数的构造函数
public School(String name1) {
System.out.println("一个参数的构造方法执行了");
this.name = name1;
}
//两个参数的构造函数
public School(String name1, String address1) {
System.out.println("两个参数的构造方法执行了");
this.name = name1;
this.address = address1;
}
}
public class TestSchool {
public static void main(String[] args) {
School sc = new School();
sc.setName("布鲁弗莱大学");
sc.setAddress("济南");
System.out.println(sc.getName() + sc.getAddress());
School sc1 = new School("春田花花幼稚园");
System.out.println(sc1.getName() + sc1.getAddress());
School sc2 = new School("国际希望小学", "北京海淀区中关村");
sc2.setAddress("济南");
System.out.println(sc2.getName() + sc2.getAddress());
}
}
总结一下构造方法的作用(虽然没啥卵用,但还是要知道官方书面的表达):
无参构造方法:创建对象。
有参构造方法:创建对象的同时给对象的属性赋值。