前言
- Java 设计者引入 "类与对象(OOP) ",根本原因就是现有的技术,不能完美的解决新的新的需求。
- 在本文中整理了面对对象编程基础知识,讲解了:
- 什么是类和对象、成员变量和成员方法;
- 怎么创建和使用类和对象、成员变量和成员方法;
- 类和对象在内存中的存在形式,类和对象的内存分配机制,方法的调用机制原理,成员方法传参机制…这些底层原理;
- 什么是方法重载,方法重载的基本用法;
- 什么是形参列表中的可变参数,基本用法与本质;
- 什么是变量的作用域,它的基本概念与注意事项;
- 什么是构造器,如何使用构造器创建对象;
- 对象创建的流程分析及其底层原理实现;
- 只要理解并掌握了以上的内存中各种底层机制原理,便可以更好地学习接下来更加复杂的面对对象编程的学习内容了。让我们开始吧!
一、类与对象
- 一个程序就是一个世界,有很多事物,每个事务都可以对应一个对象,每个对象有着自己的[属性,行为])。
- 把一些对象的相同属性和行为提取出来,便可以创建一个类。 例如:将地球上所有猫拥有的的相同属性(年龄…)和行为(奔跑、捕猎…)提取出来,便可以创建一个猫类。只要我们新创建了一个猫类对象,该对象就会拥有猫类中的所有属性和行为。
1. 类和对象的关系示意图
- 如下图所示:
- 说明:从猫类到猫类对象,有几种说法:(1)创建一个猫类对象;(2)实例化一个猫类对象;(3)把猫类实例化;这几个说法不同,但本质上是一样的。
- 类和对象的区别和联系:
- 类是抽象的,概念的,代表一类事物,比如人类,猫类…, 即它是我们自定义的一种数据类型;可以类比数组;
- 对象是具体的,实际的,代表一个具体事物, 即 对象=实例;
- 类是对象的模板,对象是类的一个个体,对应着一个实例。
2. 对象在内存中存在形式(重要!)
-
图示如下:
-
补充:Java 内存的结构分析:
- 栈: 一般存放基本数据类型和局部变量 ;
- 堆: 存放对象的内存空间、引用数据类型,如,数组;
- 方法区:常量池(常量,比如字符串)、 类加载信息。
-
解释:在新创建一个猫类对象时:(结合上图理解!)
- 首先在内存中的方法区加载了Cat类的信息,包括属性信息和方法信息,只会加载一次;
- 其次在堆内存中开辟了一个内存空间,空间大小是由类中的属性个数决定的;这个内存空间是真正的猫类对象cat;
- 接着在对象空间中给属性进行默认初始化,存储属性的值为默认值;
- 然后给该对象的指定属性第二次初始化(如,cat.name = “小白”…);
- 基本类型的数据直接存储在对象堆内存空间的空间块中;String类型的数据则会在方法区中开辟一个常量池,然后在池中新开辟一个空间,用来存储String类型的数据;
- 再将常量池中存储String类型数据的空间地址,赋值给对象堆内存空间中的对应属性,让其可以引用。
- 在初始化完成后,最后把该对象空间的地址赋值给栈内存中的 cat (cat 只是一个对象名),让 cat 可以引用该对象的内存空间;
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,这里就不赘述了。
四、方法重载
- java 中允许同一个类中,多个同名方法的存在,但要求这些同名方法的形参列表不一致。 比如:System.out.println()方法,可以传入各种数据类型的实参,并且传入实参个数没有限制,如果不使用方法重载,我们就需要创建多个方法才能实现这些功能。
- 重载的好处:
- 减轻了起名的麻烦;
- 减轻了记名的麻烦;
1. 快速入门
- 举例入门:
在MyCalculator 类中,实现用相同的方法名calculate,可以计算:
- 两个整数的和
- 一个整数,一个double的和
- 一个double ,一个Int和
- 三个int的和
- 代码如下:
public class OverLoad01 {
//编写一个main方法
public static void main(String[] args) {
MyCalculator mc = new MyCalculator();
System.out.println(mc.calculate(1, 2));
System.out.println(mc.calculate(1.1, 2));
System.out.println(mc.calculate(1, 2.1));
System.out.println(mc.calculate(1, 2, 3));
}
}
class MyCalculator {
//下面的四个 calculate方法构成了重载
//两个整数的和
public int calculate(int n1, int n2) {
System.out.println("calculate(int n1, int n2) 被调用");
return n1 + n2;
}
//一个整数,一个double的和
public double calculate(int n1, double n2) {
return n1 + n2;
}
//一个double ,一个Int和
public double calculate(double n1, int n2) {
System.out.println("calculate(double n1, int n2) 被调用..");
return n1 + n2;
}
//三个int的和
public int calculate(int n1, int n2,int n3) {
return n1 + n2 + n2;
}
}
- 说明:若是没有使用方法重载,则要分别给这四个方法重新命名,而重载则可以减少命名的麻烦。
2. 注意事项和使用细节
- 方法名必须相同:构成重载的方法它们的方法名必须相同。
- 形参列表必须不同:构成重载的方法,它们的形参类型、个数、顺序,这三者中必须至少有一种和其他的不同;
- 参数名无影响:形参的命名可以相同也可以不同,不是决定方法重载的因素;
- 方法返回类型无影响:方法的返回类型可以相同也可以不同,不是决定方法重载的因素;
- 举例如下:
五、可变参数
- 引入可变参数:在上面的方法重载中,我们举例了分别计算两个int 类型和三个int 类型变量和的方法 calculate ;这两个方法除了形参个数不同外其他都是相同的。那我们是否可以采用一种机制,将同一个类中多个同名同功能但参数个数不同的方法,封装成为一个方法呢?这时可变参数便出现了。
- java 允许将同一个类中多个同名同功能但参数个数不同的方法,封装成一个方法。 这个机制通过可变参数实现。
1. 基本语法
- 访问修饰符 返回类型 方法名(数据类型… 形参名) { 方法体 }
- 注意:数据类型后面是有(…)跟着的。
- 看一个案例: 创建一个类 HspMethod,创建方法 sum,【可以计算 2 个数的和,3 个数的和,…,n个数的和】。
- 代码如下:
public class VarParameter01 {
//编写一个main方法
public static void main(String[] args) {
HspMethod m = new HspMethod();
System.out.println(m.sum(1, 5, 100)); //106
System.out.println(m.sum(1,19)); //20
}
}
class HspMethod {
//计算 2个数的和,3个数的和,...,n个数的和。
/*
可以使用方法重载,但是要重复创建很多个重构方法;
public int sum(int n1, int n2) {
//2个数的和
return n1 + n2;
}
public int sum(int n1, int n2, int n3) {
//3个数的和
return n1 + n2 + n3;
}
public int sum(int n1, int n2, int n3, int n4) {
//4个数的和
return n1 + n2 + n3 + n4;
}
上面的三个方法名称相同,功能相同, 参数个数不同-> 使用可变参数优化
*/
// 可变参数:
//1. int... 表示接受的是可变参数,类型是int ,即可以接收多个int(0-多) ;
//2. 使用可变参数时,可以当做数组来使用 即 nums 可以当做数组;
//3. 遍历 nums, 求和即可。
public int sum(int... nums) {
int res = 0;
for(int i = 0; i < nums.length; i++) {
res += nums[i];
}
return res;
}
}
2. 注意事项和使用细节
- 细节如下:
- 解释:
- 可变参数,其实int… == int[],可变参数就是数组,传参机制为引用传递;
- 传参给可变参数时,实参传入一个对应数据类型的数组或者是多个对应数据类型的变量都可以;
- 举例说明:
- 有三个重载方法,分别实现返回姓名和两门课成绩(总分), 返回姓名和三门课成绩(总分),返回姓名和五门课成绩(总分)。将它们封装成一个可变参数的方法。
public class VarParameterExercise {
public static void main(String[] args) {
HspMethod hm = new HspMethod();
System.out.println(hm.showScore("milan" , 90.1, 80.0 ));
System.out.println(hm.showScore("terry" , 90.1, 80.0,10,30.5,70 ));
}
}
class HspMethod {
//分析 1. 方法名 showScore; 2. 形参(String ,double... ); 3. 返回类型 String
public String showScore(String name ,double... scores) {
double totalScore = 0;
for(int i = 0; i < scores.length; i++) {
totalScore += scores[i];
}
return name + " 有 " + scores.length + "门课的成绩总分为=" + totalScore;
}
}
六、变量作用域
1. 基本概念
- 基本概念:
- 代码说明:
public class VarScope {
public static void main(String[] args) {
}
}
class Cat {
// 全局变量:也就是属性,作用域为整个类体 即Cat类,cry、eat 等方法可以使用属性;
// 属性在定义时,可以直接赋值;
int age = 10; //指定的值是 10
//全局变量(属性)可以不赋值,直接使用,因为有默认值;
double weight; // 默认值是0.0
public void hi() {
//局部变量必须赋值后,才能使用,因为没有默认值;
int num = 1;
String address = "北京的猫";
System.out.println("num=" + num);//局部变量
System.out.println("weight=" + weight);//属性
}
public void cry() {
//1. 局部变量一般是指在成员方法中定义的变量;
//2. n 和 name 就是局部变量;
//3. n 和 name的作用域在 cry方法中;
int n = 10;
String name = "jack";
System.out.println("在cry中使用属性 age=" + age);
}
public void eat() {
System.out.println("在eat中使用属性 age=" + age);
//System.out.println("在eat中使用 cry的变量 name=" + name);//错误
}
}
2. 注意事项和使用细节
- 细节如下:
- 生命周期不同:
属性生命周期较长,伴随着对象的创建而创建,伴随着对象的销毁而销毁。 局部变量,生命周期较短,伴随着它的代码块的执行而创建, 伴随着代码块的结束而销毁。即在一次方法调用过程中;- 作用域范围不同:
全局变量/属性:可以被本类使用,或其他类使用(通过对象调用);
局部变量:只能在本类中对应的方法中使用;- 修饰符不同:
属性可以加修饰符(public protected private…);局部变量不能加修饰符;- 命名规则:
属性和局部变量可以重名,访问同名的属性和局部变量时遵循就近原则;在同一个作用域中,比如在同一个成员方法中,两个局部变量不能重名。
- 代码说明:
public class VarScopeDetail {
public static void main(String[] args) {
Person p1 = new Person();
/* 细节1:
属性生命周期较长,伴随着对象的创建而创建,伴随着对象的销毁而销毁。
局部变量生命周期较短,伴随着它的代码块的执行而创建,
伴随着代码块的结束而销毁。即在一次方法调用过程中
*/
// p1.say(); 当执行say方法时,say方法的局部变量比如name,会创建;
// 当say执行完毕后,name局部变量就销毁,但是p1的属性(全局变量)仍可以使用;
// 细节2:属性:可以被本类使用,或其他类使用(通过对象调用);
T t1 = new T();
t1.test(); // 第1种跨类访问对象属性的方式
t1.test2(p1);//第2种跨类访问对象属性的方式
}
}
class T {
// 细节2:属性:可以被本类使用,或其他类使用(通过对象调用);
public void test() {
// 创建一个Person 类的对象p1,可以使用p1调用Person类的属性
Person p1 = new Person();
System.out.println(p1.name);// 输出 jack
}
// 传入一个Person类对象的实参给方法test2 ,
// 当调用test2 方法时,便可以访问Person 类的属性;
public void test2(Person p) {
System.out.println(p.name);// 输出 jack
}
}
class Person {
// 细节3: 属性可以加修饰符(public protected private..);
// 局部变量不能加修饰符;
public int age = 20;
String name = "jack";
public void say() {
// 细节4: 属性和局部变量可以重名,访问时遵循就近原则;
String name = "king";
System.out.println("say() name=" + name);// 输出的是最近的name的值
}
public void hi() {
// 细节4:在同一个作用域中,两个局部变量不能重名;
String address = "北京";
// String address = "上海";// 错误,重复命名
String name = "hsp";// 可以
}
}
七、构造器/构造方法
- 我们来看一个需求:前面我们在创建人类的对象时,是先把一个对象创建好后,再给他的年龄和姓名属性赋值;
- 如果现在要求,在创建人类的对象时,就直接指定这个对象的年龄和姓名,该怎么做? 这时就可以使用构造器。
1. 基本概念
- 构造方法又叫构造器(constructor),是类的一种特殊的方法,它的主要作用是完成对新创建对象的初始化。它有几个特点:
- 方法名和类名相同;
- 没有返回值;
- 在创建对象时,系统会自动的调用该类的构造器完成对象的初始化。
- 基本语法:[修饰符] 方法名(形参列表) { 方法体; }
- 说明:
- 构造器的修饰符可以默认, 也可以是 public ,protected ,private ;
- 构造器一定没有返回值 ;
- 方法名 和 类名字必须一样 ;
- 形参列表 和 成员方法使用一样的规则;
- 构造器的调用, 由系统自动完成,使用者不能主动调用构造器。
- 现在我们用构造方法来完成刚才提出的问题:在创建人类的对象时,就直接指定这个对象的年龄和姓名。
- 代码如下:
public class Constructor01 {
public static void main(String[] args) {
//当我们new 一个对象时,直接通过构造器指定名字和年龄
Person p1 = new Person("smith", 80);
System.out.println("p1的信息如下");
System.out.println("p1对象name=" + p1.name);//smith
System.out.println("p1对象age=" + p1.age);//80
}
}
//在创建人类的对象时,就直接指定这个对象的年龄和姓名
//
class Person {
String name;
int age;
//构造器
//1. 构造器没有返回值, 也不能写void
//2. 构造器的名称和类Person一样
//3. (String pName, int pAge) 是构造器形参列表,规则和成员方法一样
public Person(String pName, int pAge) {
System.out.println("构造器被调用~~ 完成对象的属性初始化");
name = pName;
age = pAge;
}
}
2. 注意事项和使用细节
- 如下图所示:
- 代码说明:
public class ConstructorDetail {
public static void main(String[] args) {
Person p1 = new Person("king", 40);//第1个构造器
Person p2 = new Person("tom");//第2个构造器
//Person p2 = new Person(); 不能使用默认的无参构造器,因为被覆盖了
Dog dog1 = new Dog();// 可以使用默认的无参构造器,因为显式地重新定义了
}
}
class Dog {
//如果程序员没有定义构造器,系统会自动给类生成一个默认无参构造器(也叫默认构造器)
/*
默认构造器
Dog() {
}
*/
//一旦定义了自己的构造器,默认的构造器就覆盖了,就不能再使用默认的无参构造器,
//除非显式的定义一下,即: Dog(){} 写 (这点很重要)
public Dog(String dName) {
//...
}
//显式的定义无参构造器,才能使用;
Dog() {}
}
class Person {
String name;
int age;//默认0
//第1个构造器
public Person(String pName, int pAge) {
name = pName;
age = pAge;
}
//第2个构造器, 只指定人名,不需要指定年龄
public Person(String pName) {
name = pName;
}
}
八、对象创建的流程分析(重要!)
- 举例分析:
- 解释:
- Person p = new Person(“小倩”, 20); 首先在方法区中加载了Person类的信息,只会加载一次;
- 其次在堆内存中开辟了一个内存空间(地址),这个内存空间是真正的对象;
- 接着进行对象的初始化,分为3步:
(1)默认初始化,此时对象的各个属性的值都是默认值,上例中 age = 0,name = null;
(2)显式初始化,此时给对象的各个属性赋值,赋的值为类中声明属性时的初始值,上例中 age = 90,name = null;
(3)构造器初始化,此时调用构造器,给对象的各属性赋值,赋的值为创建对象时传入的参数,上例中 age = 20,name = “小倩”。- 完成对象的初始化后,最后将对象的堆空间的地址,返回给 main 栈空间中的对象名 p,此时 p 就可以引用该对象(堆内存空间)了。
- 流程示意图如下:(有些潦草)
九、this 关键字
1. 基本概念
- 什么是this :Java虚拟机会给每个对象分配this,用来代表当前对象。
- 假设我们创建了一个 Dog 类,并在里面定义构造器和一些方法,看下图案例,分析this 的内存机制。
- 内存分析示意图:
- 解释:
- this 关键字,本质是对象在其堆内存空间中隐式地创建了一个引用类型变量 this,然后将自身的内存地址赋值给了 this;简而言之,this 就是该对象的内存空间内的对象名。
- 举例:有一个孩子,他的父母给他取名为小明,而这个孩子在和别人交流时,会自称“我”;这个孩子就是一个人类对象,小明就是对象名,而“我” 就相当于 this。
2. 注意事项和使用细节
- 细节如下:
- this 关键字只能用来访问本类的属性、方法、构造器;
- this 用于区分本类的属性和局部变量;
- 访问本类成员方法的语法:this.方法名(参数列表);
- 访问本类构造器语法:this(参数列表);注意该语句只能在构造器中使用(即只能在构造器中访问另外一个构造器,而且必须放在第一条语句),不能在普通方法中访问构造器;作用是在一个构造器中调用另一个构造器去初始化对象。
- this 不能在类定义的外部使用,只能在类定义的方法中使用。
总结
- 本文是小白博主在学习B站韩顺平老师的Java网课时整理总结的学习笔记,在这里感谢韩顺平老师的网课,如有有兴趣的小伙伴也可以去看看。
- 面向对象编程基础部分的学习总结就结束啦,明天接着学习面向对象编程中级部分的知识。
- 最后,如果本文有什么错漏的地方,欢迎大家批评指正!一起加油!!我们下一篇博文见吧!