面向对象编程
1. 类与对象
1.1 类和对象的定义
-
类就是一个数据类型,对象就是一个具体的实例。比如:int 是一个类,int a;a就是一个对象(具体实例)。
class cat{ //定义了一个Cat类 String name; //定义的name、age、color又叫做类的属性 or 成员变量 or 字段 int age; String color; } Cat cat1 = new Cat(); //用自定义的Cat类实例化了一个cat1对象,cat1只是一个对象名(对象的引用),new Cat()创建的对象 //空间(数据)才是真正的对象 cat1.name = "小花"; cat1.age = 3; cat1.color = "白色";
-
类与对象的联系和区别:
类 是抽象的、概念的,代表一类事物,比如人类,猫类…,即它是数据类型。
对象是具体的、实际的,代表一个具体事物,即是 实例。
类是对象的模板,对象是类的一个个体,对应一个实例。
2. 属性/成员变量
⚠️属性并不等价于成员变量:(133条消息) java中 成员变量和属性的区别_Ven%的博客-CSDN博客
2.1 属性的定义
-
属性/成员变量:成员变量 = 属性 ,如 cat 类中定义的 name、age、color 叫做类 Cat 的属性 or 成员变量。属性是类的一个组成部分,一般是基本数据类型,也可以是引用类型(对象,数组)。
-
属性的定义语法同变量:访问修饰符 属性类型 属性名;
访问修饰符用于控制属性的访问范围,有四种: public,protected,默认(不写就是默认),private。
-
属性如果不赋值则会有默认值,默认值规则与数组一样。
int、short、byte 和 long ——0,float、double——0.0,char——\u0000,boolean——false,String——null。
2.2 JAVA 内存的结构分析
- 栈:一般存放基本数据类型(局部变量)。
- 堆:存放对象(Cat cat,数组等)。
- 方法区:常量池(常量,比如字符串),类加载信息。
2.3 类和对象的内存分配机制
- 对于cat对象中的第一个数据(属性)name,因为它是一个引用数据类型(String属于类类型),所以在堆中只存储一个地址,在方法区对应于该地址的空间中存放 name 的内容。第二个数据是 age,因为它是一个 基本数据类型(int类型),所以直接存放在堆中。
-
执行第一句:
Person p1 = new Person();
①. 先加载 Person 类信息(属性和方法信息,只会加载一次)。
②. 在堆中分配空间,进行默认初始化(看规则),把堆中分配空间的地址赋给 p1,p1就指向对象。
执行第二、三句:
③. 进行指定初始化,比如:p1.age = 10; p1.name = “小明”;
p1.age = 10;
p1.name = "小明";
执行第四句:
- 练习题:
3. 成员方法
3.1 成员方法的定义
-
假如定义了一个 Person 类,除了有一些属性(年龄,姓名…),人类还有一些行为,比如:说话、跑步。这时就要通过成员方法完成。
-
成员方法的定义 :
访问修饰符 返回数据类型 方法名 (形参列表) {
//方法体
语句;
return 返回值;
}
访问修饰符:作用是控制方法的使用范围,有四种——public,protected,private,默认(不写就是默认)。
返回数据类型:void——不返回值,int——返回一个整型,int[]——返回一个整型数组(想要返回多个结果就用数组),返回类型可以是任意类型,包含基本数据类型或引用数据类型(数组,对象)。如果方法要求有返回数据类型,则方法中最后的执行语句必须为 return 值,而且返回值类型必须与要求类型一致或兼容。
方法名:最好见名知意,遵循驼峰命名法。
形参列表:可以有多个形参。形参类型可以为任意(基本数据类型 or 引用类型)。调用有形参的方法时,要传入相同数目、相同类型(或者兼容的类型)的实参。
方法体:写实现功能执行的程序,但是方法体中不能再定义方法,即方法不能嵌套定义。
-
比如定义一个在 Person 类中定义一个方法,让它输出两个整数的和。注意:要在 main 函数中创建一个 Person 对象,并调用该方法才能执行 。如下:
class Person{
String name;
int age;
//public:表示方法是公开的
//int:表示方法返回值为int类型
//getSum(int n1, int n2):是方法名,(int n1,int n2)形参列表
//{}:是方法体,可以写要执行的代码
public int getSum(int num1, int num2){
int sum = num1 + num2;
return sum;
}
}
- 同一个类中的方法调用:直接调用即可。
- 跨类中的方法A类调用B类方法:需要通过 B 类对象名调用。比如 对象名.方法名(参数)。
- 跨类的方法调用和方法的访问修饰符相关。
3.2 方法调用机制
注意:
-
上图代码开始执行 main 方法时,就会在栈区开辟一个 main 栈空间,用于执行 main 方法中的程序。
-
执行到 Person p1 = new Person(); 时就会在堆中开辟对象空间。
-
执行到 int returnRes = p1.getSum(10, 20); 时,会在栈中重新开辟一个 getSum 方法的栈空间,并在其中生成相应的变量,然后进行计算,当计算结果返回给 main 中的 returnRes 后,getSum 栈就会被关闭。同理,main 方法执行完毕后,main 栈也会被关闭。
3.3 成员方法传参机制
-
基本数据类型(int,char等),传递的是值,形参的任何改变不影响实参。如:
public class MethodParameter{ public static void main(String[] args){ int a = 10; int b = 20; AA obj = new AA(); obj.swap(a, b); System.out.println(a + " " + b); //输出结果为 10, 20 } } class AA{ public void swap(int a, int b){ int temp = b; b = a; a = temp; System.out.println(a + " " + b); //输出结果为20, 10 } }
-
应用数据类型(数组,对象等),传递的是地址,形参的改变会影响实参。如:
public class MethodParameter{ public static void main(String[] args){ int[] arr = {1, 2, 3}; AA obj = new AA(); obj.test01(arr); for(int i = 0; i < arr.length; i++){ System.out.print(arr[i] + "\t"); //输出 200 2 3 } } } class AA{ public void test01(int[] arr){ arr[0] = 200; for(int i = 0; i < arr.length; i++){ System.out.print(arr[i] + "\t"); //输出 200 2 3 } } }
-
综合练习:
public static void main(String[] args){ AA obj = new AA(); Person per = new Person(); per.name = "jack"; per.age = 5; obj.test02(per); System.out.print(per.age); //输出 999 } } class AA{ public void test02(Person per){ per.age = 999; } } class Person{ String name; int age; }
public static void main(String[] args){ AA obj = new AA(); Person per = new Person(); per.name = "jack"; per.age = 5; obj.test02(per); System.out.print(per.age); //输出5,而不是NULL } } class AA{ public void test02(Person per){ per = NULL; } } class Person{ String name; int age; } //解释:执行obj.test02(per);语句后,在JVM内存里,创建一个新的 test02方法栈,栈里面创建一个新的对象per(因为test02方法 //的形参叫per,故与主方法的per同名),执行per = NULL;将test02方法栈中的per与内存中实际的 per对象的联系断掉,但是main //栈中的per任然保持与内存中per数据空间保持联系,故在main栈中输出的per.age任然是5 //思考:如果把test02方法中的语句per = NULL改为: //p = new Person(); //p.name = "tom"; //p.age = 99; //则输出还是 5,注意,当退出test02方法栈后,它里面的 p 对象(Person类)会因为没有被使用而被销毁回收。
3.4 递归
递归的总要准则:
- 执行一个方法时,就会创建一个新的受保护的独立空间(栈空间)。
- 方法的局部变量是独立的,不会相互影响。但是如果方法中使用的是引用类型变量(比如数组、对象),就会共享该引用数据类型。
- 递归必须向退出递归的条件逼近,否则就是无限递归,出现 StackOverFlowError,死龟了:)
- 当一个方法执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕,相应的栈空间被关闭。
经典题目:
- 使用递归求斐波那契数:1,1,2,3,5,8,13…(即从第三个数开始,每一个数是前面两个数的和),给你一个整数,求出对应顺序的斐波那契数。
public class Febonachi{
public static void main(String[] args) {
int n = 4;
T febo = new T();
int res = febo.febonachi(n);
if(res != -1){
System.out.println("当n = " + n + "时,对应的斐波那契数 = " + res);
}
}
}
class T {
public int febonachi(int n){
if(n > 0){
if(n == 1 || n == 2){
return 1;
}else{
return febonachi(n -1) + febonachi(n -2);
}
}else{
System.out.println("请输入一个正确的数!");
return -1;
}
}
}
- 猴子吃桃子:有一堆桃子,一个猴子每天吃一半,并再多吃一个,吃到第十天就只剩下一个桃子,请问最初有多少个?
public class MonkeyPeach{
public static void main(String[] args) {
int n = 9;
T peach = new T();
int res = peach.Peach(n);
if(res != -1){
System.out.println("第" + n + "天有" + res +"个桃子");
}
}
}
class T{
public int Peach(int n){
if(n == 10){
return 1; //第十天只有一个桃子
}else if(n >= 1 && n <= 9){
return 2*(Peach(n + 1) + 1); //前一天的桃子总数 = 2*(当前天的桃子数 + 1)
}else{
System.out.println("输入错误 n =(1,10)");
return -1;
}
}
}
- 迷宫问题:迷宫由二维数组表示,数组元素 0 表示可以走,1 表示不能走。迷宫要从左上那个空走到右下那个空表示走通了。
public class MIGONG00{
public static void main(String[] args) {
//创建一个8行7列的数组,并给边间、第3行第1列和第2列赋值1,表示迷宫围墙
int[][] map = new int[8][7];
for(int i =0; i < 7; i++){
map[0][i] = 1;
map[7][i] = 1;
}
for(int i = 0; i < 8; i++){
map[i][0] = 1;
map[i][6] = 1;
}
map[3][1] = 1;
map[3][2] = 1;
map[2][2] = 1; //补充,这个位置为1,可以看到 出现 3 的情况
//输出看效果
System.out.println("========当前地图情况=========");
for(int i = 0; i < map.length; i++){
for(int j = 0; j < map[i].length; j++){
System.out.print(map[i][j] + " ");
}
System.out.println();
}
System.out.println("========最终前地图情况=========");
Migong res = new Migong();
res.findWay(map, 1, 1);
for(int i = 0; i < map.length; i++){
for(int j = 0; j < map[i].length; j++){
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
}
class Migong{
//使用递归回溯的思想来解决老鼠出迷宫
//1. findWay方法用来找到迷宫的路径,如果找到出路,返回true,没有找到返回false
//2. map 就是二维数组,即表示迷宫
//3. (i, j) 就是老鼠的位置,初始化位置为(1,1)
//4. 因为我们是递归的找路,先规定 map 数组各个值的含义
// 0 表示能走(但是不一定走得通),1 表示障碍物, 2 表示可以走通(一直走出去), 3 表示走过,但是走不出去,是死路
//5. 当 map[6][5] = 2 就说明找到通路了就可以退出了,否则就继续找
//6. 先确定老鼠找路策略: 下-右-上-左 (策略改变,路径也会变化)
public boolean findWay(int[][] map, int i, int j){
if(map[6][5] == 2){ //说明已经找到
return true;
}else{
if(map[i][j] == 0){ //当前位置是 0 表示可以走(但是不一定走得通)
//假设 map[i][j] 走得通
map[i][j] = 2;
//用 下-右-上-左 的策略,先试试下走不走通,再试右...,如果能走通(即map[6][5] == 2)返回true
if(findWay(map, i + 1, j)){
return true; //走的通,返回true
}else if(findWay(map, i, j + 1)){
return true;
}else if(findWay(map, i - 1, j)){
return true;
}else if(findWay(map, i, j - 1)){
return true;
}else{ //如果都走不通则前面的假设不成立,map[i][j] 设置为3,表示走过,但是走不通
map[i][j] = 3;
return false;
}
}else{
return false; //如果map[i][j] != 0,表示这个位置不能走,返回false
}
}
}
}
- 汉诺塔:有A B C三个塔,A塔上有 num 个从小到大排列的盘子(小的在上),要将A塔上的所有盘子移动到C塔上,且移动过程中必须保持大盘子在下。
public class HanoTower{
public static void main(String[] args) {
Tower tower = new Tower();
tower.move(5, 'A', 'B', 'C');
}
}
class Tower{
//num 表示要移动的个数,a,b,c 表示A,B, C塔
public void move(int num, char a, char b, char c){
//如果只有一个则直接从A塔移动到C塔
if(num == 1){
System.out.println(a + "->" + c);
}else{
//将A塔上的上面的 num-1 个盘子借助C塔移动到B塔上
move(num - 1, a, c, b);
//将A塔上最下面的一个盘子移动到C塔上
System.out.println(a + "->" + c);
//将B塔上的num-1个盘子借助A塔移动到C塔上
move(num - 1, b, a, c);
}
}
}
3.5 重载
-
函数名相同,但是形参不同。比如常用的:
System.out.println(10); System.out.println('a'); System.out.println("123");
上面这三个函数都是 .out 的 println 函数,但是他们的形参分别是 int,char 和 Strng 类型,即根据实参的类型选定不同形参的 println 函数来执行。
-
重载的注意事项:① 方法名必须相同;② 形参列表必须不同(形参类型、个数、顺序,至少有一样不同,参数名无要求);③ 返回类型无要求。构成重载:方法名相同,形参不同(类型 or 数量其中一个都可)!!!
针对 ② 有:
public void method(int n1, int n2); public void method(int a1, int a2); //以上两个函数是相同函数 method( int, int ) ,只有形参名不同,他们两个不是重载,而是重复定义,会报错。
public void method(int n1, int n2);
public int method(int n1, int n2);
//以上两个函数是相同的函数 method( int, int ),虽然返回类型不同,但是还是不构成重载,而是重复定义,会报错。
3.6 可变参数
-
基本概念:java 允许将同一个类中多个同名同功能但参数个数不同的方法,封装成一个方法。
基本语法:
访问修饰符 返回类型 方法名(数据类型 … 形参名){
}
下面的函数表示可以接受多个(0-多)int 类型的参数,如:
public int sum(int ... nums); //相当于输入的是一个数组 nums[i]
-
可变参数的实参可以为0个或任意多个,也可以为数组。可变参数的本质就是数组。
-
可变参数可以和普通类型的参数一起放在形参列表,但必须保证可变参数在最后。如:
public int f2(int... nums, String str){};//就是错误的,应该为:public int f2(String str, int... nums){};
-
一个形参列表中只能出现一个可变参数,如:
public void f2(int... nums, double... dou){}; //是不可以的,报错
3.7 作用域
-
java 中,主要的变量就是属性(成员变量)和局部变量。
全局变量:也就是属性(成员变量),作用于为整个类体。
局部变量:一般是指在成员方法中定义的变量(其实应该不是成员方法,而是定义在代码块 {} 中的变量就叫局部变量,简而言之就是除属性外的其他变量),只能在定义局部变量的方法(代码块)中使用。
-
全局变量(属性)可以不赋值,直接使用,因为有默认值(参考 2.1 第 3 条)。局部变量必须赋值后才能使用,因为没有默认值。如:
class Cat{ double weight; //weight 为属性(成员变量)是全局变量,可以直接使用 ,因为它有默认值 0.0 public void hi(){ int num; System.out.println(weight); //可以使用输出 0.0,虽然 weight 没有赋值,但是它有默认值 System.out.println(num); //报错,因为 num 是局部变量,没有赋初值,且没有默认值 } }
-
属性和局部变量可以重名,访问时遵循就近原则。
-
在同一个作用域中,比如在同一个成员方法中,两个局部变量,不能重名。
-
属性生命周期较长,伴随着对象的创建而创建,伴随着对象的死亡而死亡。局部变量生命周期较短,伴随着它的代码块的执行而创建,伴随着代码块的结束而死亡。
-
作用域范围不同:
全局变量(属性):可以被本类使用,或者其他类使用(通过对象调用)。
局部变量:只能在本类中对应的方法中使用。
-
修饰符不同:全局变量(属性)可以加修饰符;局部变量不能加修饰符。如:
class Cat{ private double weight; }
4. 构造器
4.1 构造器的定义
构造方法又叫构造器,是类的一种特殊的方法,它的主要作用是完成对新对象的初始化(不是创建,而是初始化)。特点有:① 构造器方法名和类名相同;② 构造器没有返回值(也不能写 void);③ 在创建对象时,系统会自动的调用该类的构造器完成对对象的初始化。④ 构造器的修饰符可以默认,也可以是 public, private,protected。
基本语法:
[修饰符] 方法名(形参列表){
方法体;
}
- 一个类可以定义多个构造器,即构造器的重载。
- 如果程序员没有定义构造器,系统会自动给类生成一个默认无参构造器(也叫默认构造器)。如:有一个 Dog 类,我们没有给他定义构造器,系统的默认构造器如下,这也就解释了为什么定义一个没有构造器的类是这样:Dog tt = new Dog(); 小括号就代表了调用默认构造器。
Dog(){
}
- 一旦定义了自己的构造器,默认构造器就被覆盖了,就不会再使用默认的无参构造器了,除非显式的定义一下,即:
Dog(){}
4.1 对象的创建流程
-
执行语句:Person p = new Person(“小倩”, 20);
① 在方法区中加载 Person 类信息(属性和方法信息),只加载一次。
② 在堆中分配空间。
-
完成初始化:
① 先使用默认构造器 Person(){} 对其进行默认初始化,此时 age = 0(int 类型嘛),name = null(String类型嘛)。
② 又因为 Person 类的定义中有显示初始化语句:int age = 90; 故又执行显示初始化:
③ 最后因为 Person 类中定义了构造器:Person(String n, int a){…},同时定义类的语句:Person p = new Person(“小倩”, 20); 也传递了相应的参数,即调用了构造器,所以最后还要执行构造器初始化如下,因为 name 是String 类型,所以需要在方法区的常量池中存放,堆中的空间只存放常量池中相应位置的地址。
- 完成初始化后,将堆中对象(数据空间)的地址赋值给类的变量名(或者叫类的引用)p。
5. this 关键字
java 虚拟机会给每个对象分配 this,代表当前对象。以上图的代码中的构造器为例,this 引出如下:
Person(String name, int age){
name = name;
age = age;
}
//如果执行上面的语句(形参与上图中的 n 和 a 不同,则最后 p 中的 name 为 null,age 为 90,因为构造函数体中的局部变量遵从就
//近原则,故里面的 name 和 age 指的是实参传过来的name和age,而不是类中的name和age。)
//因此引出this,重新定义如下:
Person(String name, int age){
this.name = name;
this.age = age;
}
JVM 内存中可以把 this 理解为类的一个属性,它指向堆中该对象的数据空间地址:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yAZvfhWn-1681975929225)(D:\tools\Typora\插入的图片\typora-user-images\image-20211011104753941.png)]
-
this 关键字可以用来访问本类的属性、方法、构造器
-
this 用于区分当前类的属性和局部变量
-
访问成员方法的语句:this.方法名 (参数列表);访问属性的语句:this.属性
-
访问构造器语法:this (参数列表);
注意:只能在构造器中使用(即只能在构造器中访问另外一个构造器)。且this (参数列表);必须放在第一条语句。
public class Hano{ public static void main(String[] args) { Person per = new Person(); } } class Person{ String name; int age; public Person(){ //构造函数1 this("jack", 18); //只能在构造器中访问另外一个构造器,且this (参数列表);必须放在第一条语句。 System.out.println("构造器---public Person()---被调用"); } public Person(String pName, int pAge){ //构造函数2 System.out.println("构造器---public Person(String pName, int pAge)---被调用"); name = pName; age = pAge; } }
-
this 不能在类定义的外部使用,只能在类定义的方法中使用