目录
前言
- java 设计者引入 类与对象(OOP) ,根本原因就是现有的技术,不能完美的解决新的新的需求。
- 在本文中只整理了面对对象编程基础其中第一部分,讲解了:
- 什么是类和对象、成员变量和成员方法;
- 怎么创建和使用类和对象、成员变量和成员方法;
- 最最重要的是整理和总结了类和对象在内存中的存在形式,类和对象的内存分配机制,方法的调用机制原理,成员方法传参机制…这些底层原理;
- 只要理解并掌握了以上的内存中各种底层机制原理,便可以更好地学习接下来更加复杂的面对对象编程的学习内容了。大家一起加油!
一、类与对象
- 一个程序就是一个世界,有很多事物,每个事务都可以对应一个对象,每个对象有着自己的[属性,行为])。
- 把一些对象的相同属性和行为提取出来,便可以创建一个类。 例如:将地球上所有猫拥有的的相同属性(年龄…)和行为(奔跑、捕猎…)提取出来,便可以创建一个猫类。只要我们新创建了一个猫类对象,该对象就会拥有猫类中的所有属性和行为。
1. 类和对象的关系示意图
- 如下图所示:
- 说明:从猫类到猫类对象,有几种说法:(1)创建一个猫类对象;(2)实例化一个猫类对象;(3)把猫类实例化;这几个说法不同,但本质上是一样的。
- 类和对象的区别和联系:
- 类是抽象的,概念的,代表一类事物,比如人类,猫类…, 即它是我们自定义的一种数据类型;可以类比数组;
- 对象是具体的,实际的,代表一个具体事物, 即 对象=实例;
- 类是对象的模板,对象是类的一个个体,对应着一个实例。
2. 对象在内存中存在形式(重要!)
- 图示如下:
- 补充:Java 内存的结构分析:
1)栈: 一般存放基本数据类型和局部变量 ;
2)堆: 存放对象的内存空间、引用数据类型,如,数组;
3)方法区:常量池(常量,比如字符串)、 类加载信息。 - 解释:在新创建一个猫类对象时:(结合上图理解!)
- 首先在内存中的方法区加载了Cat类的信息,包括属性信息和方法信息,只会加载一次;
- 接着在堆内存中开辟了一个内存空间,空间大小是由类中的属性个数决定的;这个内存空间是真正的猫类对象cat;
- 该对象空间中初始划分有几个空间块(属性个数),存储数据的值为默认值;
- 然后把该对象空间的地址赋值给栈内存中的 cat (cat 只是一个对象名),让 cat 可以引用该对象的内存空间;
- 然后给该对象的指定属性初始化(cat.name = “小白”…);
- 基本类型的数据直接存储在对象堆内存空间的空间块中;
- String类型的数据则会在方法区中开辟一个常量池,然后在池中新开辟一个空间,用来存储String类型的数据;
- 最后将常量池中存储String类型数据的空间地址,赋值给对象堆内存空间中的对应空间块,让其可以引用。
3. 属性/成员变量/字段
- 基本介绍:
- 从概念或叫法上看: 成员变量 = 属性 = field(字段) (即 成员变量是用来表示属性的,授课中,统一叫 属性);
- 属性是类的一个组成部分,一般是基本数据类型,也可是引用类型(对象、数组)。比如我们前面定义猫类 的 age 就是属性。
- 注意事项和细节说明:
- 属性的定义语法同变量,示例:
访问修饰符 属性类型 属性名;
- 这里简单的介绍访问修饰符: 控制属性的访问范围 ;
- 有四种访问修饰符 public, proctected, 默认, private ,后面会详细介绍 ;
- 属性的定义类型可以为任意类型,包含基本类型或引用类型 ;
- ==属性如果不赋值,有默认值,规则和数组一致。具体如下: ==
int 0,short 0,byte 0,long 0,float 0.0,double 0.0,char \u0000,boolean false,String null。
4. 如何创建对象
- 先声明再创建:
Cat cat ; // 声明对象 cat
cat = new Cat(); // 创建 = 给对象在堆内存中开辟了空间 - 直接创建:
Cat cat = new Cat(); // 声明对象 cat,并同时给对象在堆内存中开辟了空间
5. 如何访问属性
- 基本语法:
对象名.属性名;
举例:cat.name ; cat.age; cat.color;
6. 类和对象的内存分配机制(重要!)
看一个思考题:我们定义一个人类(Person)(包括 名字,年龄)。
回答下图问题:
- 内存图如下:
- Java 创建对象的流程简单分析:
- 先加载 Person 类信息(属性和方法信息, 只会加载一次) ;
- 在堆中分配空间, 进行默认初始化(看规则) ;
- 把地址赋给 p , p 就指向对象;
- 进行指定初始化, 比如 p.name = ”jack” 、p.age = 10;
再看一个练习题,并分析画出内存布局图,进行分析
如下图:
- 内存分析图如下:
- 解释:
- 执行b = a 时,将a 的内存地址赋值给b (引用传递),此时b 指向了a 的内存空间;
- 执行b.age = 200,则改变了该内存空间中的值,所以a.age 也同时变成了200;
- 接着执行了 b = null,此时b 将指向一个空值;
- 所以最后输出 b.age 时,会报错,因为b 已经没有指向任何内存空间(对象)了,自然没有age 这个属性。
二、成员方法
- 在某些情况下,我们要需要定义成员方法(简称方法)。比如人类:除了有一些属性外( 年龄,姓名…),我们人类还有一些行为,比如:说话、跑步…这时就要用成员方法才能完成这些行为。
- 成员方法的好处:
- 提高代码的复用性 ;
- 可以将实现的细节封装起来,然后供其他用户来调用即可。
1. 方法快速入门
- 创建一个Person 类,在里面定义几个成员方法:
- 添加 speak 成员方法,输出 “我是一个好人”
- 添加 cal01 成员方法,可以计算从 1+…+1000 的结果
- 添加 cal02 成员方法,该方法可以接收一个数 n,计算从 1+…+n 的结果
- 添加 getSum 成员方法,可以计算两个数的和
- 代码如下:
// 主类
public class Method01 {
//编写一个 main 方法
public static void main(String[] args) {
//方法使用
//1. 方法写好后,如果不去调用(使用),不会输出
//2. 先创建对象 ,然后调用方法即可
Person p1 = new Person();
p1.speak(); //调用 speak 方法
p1.cal01(); //调用 cal01 方法
p1.cal02(5); //调用 cal02 方法,同时传入 n = 5
p1.cal02(10); //调用 cal02 方法,同时传入 n = 10
//调用 getSum 方法,同时传入 num1=10, num2=20
//把方法 getSum 返回的值,赋给变量 returnRes
int returnRes = p1.getSum(10, 20);
System.out.println("getSum 方法返回的值=" + returnRes);
}
}
// 创建Person类
class Person {
String name;
int age;
//方法(成员方法)
//添加 speak 成员方法,输出 “我是一个好人”
//1. public 表示方法是公开 ;
//2. void : 表示方法没有返回值 ;
//3. speak() : speak 是方法名,() 里面是形参列表 ;
//4. {} 方法体,可以写我们要执行的代码 ;
//5. System.out.println("我是一个好人"); 表示我们的方法就是输出一句话 。
public void speak() {
System.out.println("我是一个好人");
}
//添加 cal01 成员方法,可以计算从 1+..+1000 的结果
public void cal01() {
//循环完成
int res = 0;
for(int i = 1; i <= 1000; i++) {
res += i;
}
System.out.println("cal01 方法 计算结果=" + res);
}
//添加 cal02 成员方法,该方法可以接收一个数 n,计算从 1+..+n 的结果;
// (int n) 形参列表, 表示当前有一个形参 n, 可以接收用户输入 。
public void cal02(int n) {
//循环完成
int res = 0;
for(int i = 1; i <= n; i++) {
res += i;
}
System.out.println("cal02 方法 计算结果=" + res);
}
//添加 getSum 成员方法,可以计算两个数的和 ;
//1. public 表示方法是公开的 ;
//2. int :表示方法执行后,返回一个 int 值 ;
//3. getSum 方法名 ;
//4. (int num1, int num2) 形参列表,2 个形参,可以接收用户传入的两个数 。
//5. return res; 表示把 res 的值, 返回
public int getSum(int num1, int num2) {
int res = num1 + num2;
return res;
}
}
2. 方法的调用机制原理(重要!)
- 以上面Person类中的 getSum 方法为例,解释方法的调用机制。
- 执行流程图如下:
- 解释:
- 每次调用一个方法,虚拟机都会在栈内存中开辟一个独立的空间,方法之间不会相互影响。上图中,程序首先执行main 方法,在栈内存中开辟了一个main 栈空间;
- 在main 栈中,创建了一个Person 类的对象p1,此时在堆内存中开辟了一个空间,并把该空间的地址返回给 p1;
- 接着,p1 调用了getSum 方法,并传入了10、20 这两个实参,此时虚拟机在栈内存中开辟了一个独立的getSum 栈空间,程序跳到该栈空间中执行,直到getSum 方法执行完毕或者遇到 return 语句才会退出该栈空间,回到main 栈空间;
- 在getSum 栈中,执行了形参的赋值,相当于执行了:
int num1 = 10;
int num2 = 20;
int res = num1 + num2;
return res;
getSum 方法执行结束,程序退出该栈并返回main 栈,继续执行main 栈剩下的语句;- 程序返回main 栈后,声明了int 类型变量 returnRes 来接收getSum 方法返回的int 类型的值,然后输出;
- 最后,main 方法也执行完毕,退出main 方法,相当于退出了程序,程序终止。
3. 成员方法的定义
- 基本语法:
访问修饰符 返回数据类型 方法名(形参列表..) {
方法体 语句;
return 返回值;
}
- 解释:
- 形参列表:表示成员方法需要传入的数据,例如, cal(int n) , getSum(int num1, int num2),这里的n、num1、num2 都是在调用方法时需要传入的数据;
- 返回数据类型:表示成员方法返回的数据, void 表示没有返回值;
- 方法主体:表示为了实现某一功能的代码块;
- return 语句不是必须的。
4. 注意事项和使用细节
- ==注意事项: ==
- 调用带参数的方法时,一定要对应着形参列表传入 相同类型或兼容类型 的实参,且实参和形参的个数、顺序必须一致;
- 方法不能嵌套定义;
- 细节:
- 一个方法最多有一个返回值;若想返回多个结果,可以返回一个数组;
- 返回类型可以为任意类型,包含基本类型或引用类型(数组,对象);
- 如果方法要求有返回数据类型,则方法体中最后的执行语句必须为 return + 返回值,而且要求返回值类型必须和 return 的值类型一致或兼容;
- 如果方法返回值类型是 void,则方法体中可以没有 return 语句,或者 只写 return;
- 方法命名:遵循驼峰命名法,最好见名知义,表达出该功能的意思即可, 比如得到两个数的和 用 getSum, 开发中按照规范。
- 补充细节,如下图所示:
- 代码说明:
// 定义一个A类
class A {
//同一个类中的方法调用:直接调用即可
public void print(int n) {
System.out.println("print()方法被调用 n=" + n);
}
public void sayOk() {
//sayOk 调用 print(直接调用即可)
print(10); // 不用另外创建A 类对象
System.out.println("继续执行 sayOK()~~~");
}
//跨类中的方法 A 类调用 B 类方法:需要通过对象名调用
public void m1() {
//必须先创建一个 B类对象, 然后才能调用B类中的方法
System.out.println("m1() 方法被调用");
B b = new B();
b.hi();
System.out.println("m1() 继续执行:)");
}
}
// 定义一个B类
class B {
public void hi() {
System.out.println("B 类中的 hi()被执行");
}
}
三、成员方法传参机制(非常非常重要!)
1. 基本数据类型的传参机制
- 举例如下:
- 代码分析:
public class MethodExercise01 {
public static void main(String[] args) {
// 创建一个AA 类的对象aa
AA aa = new AA();
int a = 10;
int b = 20;
aa.swap(a, b);// 调用AA 类中的swap 方法,看看会不会影响到main 方法中的a、b。
System.out.println("a=" + a + "b=" + b);// 输出: a = 10, b = 20,结论是不会影响。
}
}
class AA {
// 基本数据类型的传参机制,值传递,两个方法是独立的栈空间,不会相互影响。
public void swap (int a, int b) {
System.out.println("\na和b交换前的值\na=" + a + "\tb=" + b);
// 输出: a = 10 ,b = 20
int tmp;
tmp = a;
a = b;
b = tmp;
System.out.println("\na和b交换后的值\na=" + a + "\tb=" + b);
// 输出: a = 20, b = 10
}
}
- 解释:
- 基本数据类型的传参机制是值传递/值拷贝,这是大前提;
- 在main 方法中调用了swap 方法后,会在栈内存中新建一个独立的swap 栈空间,程序跳到swap 栈中执行;
- 在swap 栈中隐形地执行了 int a = a(前者是swap 栈中新声明的变量a,后者是main 栈中传递给 swap 栈 的a,两者不同),由于基本类型变量的赋值方式的值传递,因此新声明的a 只是得到了main 方法中a 的值,也就是10,两个方法中的a 是相互独立的,只是变量名相同。同理,变量b 也是如此;
- 因此,在swap 栈中的a、b 这两个变量交换了值,但是当退出swap 栈返回 main 栈后,a、b 的值没有任何改变。
2. 引用数据类型的传参机制
2.1 引用数据类型的传参机制
- 案例引入:
1. AA 类中编写一个方法 test,可以接收一个数组,在方法中修改该数组,看看原来的数组是否变化?答案:会变化
- 代码分析:
public class MethodExercise01 {
public static void main(String[] args) {
// 创建一个AA 类的对象aa
AA aa = new AA();
int[] arr = new int[3];
arr[0] = 10;
aa.test(arr);
System.out.println("main方法中arr[0]= " + arr[0]);
// arr[0] = 200,被test 方法影响
}
}
class AA {
//引用数据类型的传参机制,引用传递
public void test (int[] arr) {
arr[0] = 200;
System.out.println("test方法中arr[0]= " + arr[0]);
// arr[0] = 200
}
}
- 解释:
- 引用数据类型的传参机制是引用传递/地址拷贝,这是大前提;
- 在main 方法中调用了test 方法后,会在栈内存中新建一个独立的test 栈空间,程序跳到test 栈中执行;
- 在test 栈中隐形地执行了 int[] arr = arr(前者是test 栈中新声明的数组arr,后者是main 栈中传递给 test 栈 的数组arr,两者不同),由于引用类型变量的赋值方式的引用传递,因此新声明的arr 得到了main 方法中arr 的地址,此时这两个arr 都可以改变存在于堆内存中的数组空间。这两个方法中的a 是相互独立的,只是变量名相同,但是它们对于内存空间的改变是相互影响的。
- 因此,在test 栈中的arr 改变了堆内存空间中arr[0] 的值,当退出test 栈返回 main 栈后,main 栈的 arr[0] 也会发生改变。
- 结论及示意图:
- 结论:引用类型传递的是地址(传递也是值,但是值是地址),可以通过形参影响实参。
- 示意图(网课老师的图,有点潦草):
2.2 对象的传参机制
2.在AA 类中编写一个方法 test200,可以接收一个 Person(age,sal)对象,在方法中修改该对象属性,看看原来的对象是否变化。答案:会变化。
- 当传入的实参是一个对象时,其传参机制类似于引用类型的传参机制,可以类比分析。这里只放示意图。
- 示意图如下:
2.3 思考题
若在test200 方法中执行了下面的语句,会对main 方法中原来的对象有影响吗?
- p = null;
- p = new Person();
- 第一条语句:p = null;
- 相当于在test200 方法中 隐式地执行了 Person p = p,再执行 p = null 这两条语句;
- test200 方法中的新创建了一个Person 类对象 p ,一开始其指向 main 方法中 的p 对象所指向的地址空间,但接着 新建的p = null, 也就是新建的 p 指向了空值;
- 所以最后 test200 方法中新建的p 对main 方法中原来的 p 指向的内存空间是不会有任何影响的。
-
示意图如下:
-
第二条语句:p = new Person();
- 相当于在test200 方法中 隐式地执行了 Person p = p;再执行 p = New Person() 这两条语句;
- 此时在 test200 方法中的新创建了一个Person 类对象 p,其指向一个新开辟的堆内存空间;
- 所以最后 test200 方法中新建的 p 对main 方法中原来的 p 指向的内存空间是不会有任何影响的。
- 示意图如下:
3. 对象克隆
- 克隆对象, 要求得到新对象和原来的对象是两个独立的对象(拥有独立的堆内存空间),只是他们的属性相同。
举例:编写一个方法 copyPerson,可以复制一个 Person 对象,返回复制的对象。
- 代码如下:
public class MethodExercise01 {
public static void main(String[] args) {
// 创建一个AA 类的对象aa
AA aa = new AA();
Person01 p = new Person01();
p.name = "钢铁侠";
p.sal = 3000;
// 创建一个新的Person01类的对象p1 接收克隆出来的对象(内存空间)
Person01 p1 = aa.copyPerson(p);// p 和 p1 是两个独立的对象,只是他们的属性相同。
System.out.println("copy方法中这个超级英雄是:" + p1.name + "他的工资是:" + p1.sal);
}
}
class AA {
// 克隆对象
public Person01 copyPerson (Person01 p) {
//创建一个新的对象/开辟了一个新的堆内存空间
Person01 p1 = new Person01();
p1.name = p.name;//把原来对象的名字赋给 p1.name
p1.sal = p.sal;//把原来对象的工资赋给 p1.sal
return p1;
}
}
class Person01 {
String name;
int sal;
}
- 示意图如下:
- 对象克隆的分析类比上面的对象的传参机制的思考题2,这里就不赘述了。
总结
- 本文是小白博主在学习B站韩顺平老师的Java网课时整理的学习笔记,在这里感谢韩顺平老师的网课,如有有兴趣的小伙伴也可以去看看。
- 第三篇第一部分的学习总结就结束啦,明天接着学习第三篇的第二部分内容。如果本文有什么错漏的地方,欢迎大家批评指正!一起加油!!