类与对象
类就是数据类型,对象就是一个具体的实例。类拥有属性和行为。
- 类是抽象的,概念的,代表一类事物,比如人类,猫类等它是数据类型。
- 对象是具体的,实际的,代表一个具体事物,即是实例。
- 类是对象的模板,对象是类得一个个体,对应一个实例。
对象在内存中的存在形式:
字符串是指向地址保存,数字直接在堆里保存。
- 属性/成员变量:成员变量=属性=field;属性是类的一个组成部分,一般是基本数据类型,也可是引用类型(对象,数组)。(对象的属性默认值遵守数组规则)
class Car { String name;//这些就是属性,成员变量,字段field double price; String color; String[] master; }
类与对象的内存分配机制:
在把p1赋给p2的时候也是属于引用拷贝,复制的是地址。
- Java内存的结构分析,(之前几次分析使用的jvm内存的结构分析)
- 栈:一般存放基本数据类型(局部变量)。
- 堆:存放对象。
- 方法区:常量池(存放常量,比如字符串),类加载信息。
Person p = new Person(); p.name = "jack"; p.age = 10;
这段代码的执行流程是:1.先加载Person类信息(属性和方法信息,只会加载一次)2.在堆中分配空间,进行默认初始化(看规则)3.把地址赋给p,p就指向对象 4.进行指定初始化(示例代码的第二、三步)
成员方法
实例:
public void speak(){
System.out.println("ok");
}
//public 表示方法是公开
//void表示方法没有返回值
//speak是方法名,()是形参列表
//{}是方法体,可以写我们要执行的代码
//当调用方法的时候方法体内的程序才会执行
方法调用机制原理图:
下图的步骤是,当执行左侧的代码时,第一步先new了一个对象p1,并且该代码是在main方法中执行的,所以会在栈区开辟一个main栈,并指向堆中为这个对象开辟的空间;接着调用了方法getSum,前提是getSum被定义(见第二段代码),被调用之后会在栈中开辟一个独立的栈空间来存放数据,这里我们为了区分把它叫做gatSum栈,当执行到调用方法这一句的时候会在栈中存放num1、num2与res的数据,继续执行到return时,会返回到原先执行调用方法那句代码的下一句继续执行,此时调用方法时创建的独立空间getSum栈就会自动销毁,当把main方法中的代码也都执行完毕的时候main栈也会销毁,同时整个程序也执行完毕退出。
方法调用小结:
- 当程序执行到方法时,就会开辟一个独立的空间(栈空间)
- 当方法执行完毕,或者执行到return语句时,就会返回,返回到调用方法的地方
- 返回后,继续执行方法后面的代码
成员方法的好处:
- 提高代码的复用性
- 可以将实现的细节封装起来,然后供其他用户调用即可
成员方法的定义:
public 返回数据类型 方法名(参数列表..){
//方法体
语句;
return 返回值;
}
- 返回数据类型表示成员方法输出,void表示没有返回值
- 方法主体表示为了实现某一功能的代码块
- return语句不是必须的
成员方法注意事项和使用细节
- 访问修饰符:控制方法使用的范围,如果不写,默认访问。有四种:public,protected,默认,private。(具体的到后面章节详细讲)
- 返回数据类型:
- 一个方法最多有一个返回值(想要返回多个值可以返回数组来接收多个结果);
class AA{ public int[] getSumAndSub(int n1,int n2){ int[] resArr = new int[2];//创建一个数据来接受两数之和与两数之差 resArr[0] = n1 + n2; resArr[1] = n1 - n2; } }
-
如果方法要求有返回数据类型,则方法体中最后的执行语句必须为return 值;而且要求返回值类型必须和return的值类型一致或兼容;
-
如果方法是void,则方法体中可以没有return语句,或者只写return;
-
形参列表:
-
一个方法可以有0个参数,也可以有多个参数,中间用逗号隔开,比如getSum(int n1,int n2)
-
参数类型可以为任意类型,包含基本类型或引用类型,如printArr(int [][] map)
-
调用带参数的方法时,一定对应着参数列表传入相同类型或兼容类型的参数
-
调用方法时传入的实际参数要与设定的形式参数类型、顺序、个数一致
-
方法体:方法体里面写完成功能的具体的语句,可以为输入、输出、变量、运算、分支、循环、方法调用,但是里面不能再定义方法即方法不能嵌套定义。
方法调用细节:
- 同一个类中的方法调用:直接调用即可:
class A { public void print(int n) { System.out.println("print()方法被调用 n=" + n); } public void sayOk( ) {//sayOk可以直接调用print print(10); } }
- 跨类中的方法A类调用B类方法,需要通过对象名调用比如A类中有一个方法m1想要调用B类中的方法hi,那么在m1中应先创建B b = new B();再通过b.hi();来调用。
- 特别说明:跨类的方法调用和方法的访问修饰符相关(到后面的章节详细讨论)
成员方法传参机制
- 基本数据类型的传参机制:观察下面这个实例
public class Parameter {
public static void main(String[] args) {
int a = 10;
int b = 20;
AA obj = new AA();
obj.swap(a,b);
System.out.println("main方法 a=“ + a + "b=" + b);
}
}
class AA {
public void swap(int a, int b){
System.out.println("a 和 b交换前的值a=" + a + "b=" + b);
int tmp = a;
a = b;
b = tmp;
System.out.println("a 和 b交换后的值a=" + a + "b=" + b);
}
这段代码的运行结果是:
a 和 b交换前的值a=10 b=20;a 和 b交换后的值a=20 b=10; main方法 a=10 b=20。
分析过程:
- 就像最上面的方法调用机制原理图所说,首先运行程序,在main方法中定义了a、b,并创建了名为obj的对象,所以会在栈中开辟一个main栈存放a、b、obj。
- 在调用swap方法的时候会在堆与方法区中开辟空间,并且也会在栈中开辟一块空间暂且叫它swap栈,此时进入swap方法中即开始在swap栈中操作。
- 栈中先定义了a、b并把初始的a=10,b=20存放,所以此时执行到输出交换前的值是a=10,b=20;然后又定义了tmp,完成a和b的交换,即在swap栈中执行完方法中的三步操作后变成了a=20,b=10,所以执行输出交换后的值时结果是a=20,b=10。
- 之后方法执行结束,退出方法,跳转至调用方法的下一句,也就是从swap栈中回到了main栈,而在main栈中a与b的值并没有改变还是初始定义的值,所以最后执行输出main方法中得到的结果就是a=10,b=20。
- swap方法中a和b的交换并不会影响到main方法中的a与b因为它们两个是互相独立的栈。图解如下:
结论:基本数据类型,传递的是值(值拷贝),形参的任何改变都不影响实参。
- 引用数据类型的传参机制:看下面实例
public class Parameter { public static void main(String[] args) { int[] arr = {1,2,3}; B b = new B(); b.test(arr); System.out.println("main方法数组");//为了简便这里就不for循环了 } } class B { public void test(int[] arr){ arr[0] = 200; System.out.println("test方法中的数组");//为了简便这里就不for循环了 }
这段代码的运行结果是:test方法中的数组输出变成{200,2,3};main方法中的数组输出同样也是{200,2,3}
分析过程:因为数组使用的是引用传递,传递的是存放数组的地址,main方法和test方法都指向了相同的地址(在堆中指向同一个空间),所以数组的值发生改变,两个方法打印出来的数组都会发生改变,因为他们就是指向的同一个地址,同一个数组。
结论:引用类型传递的是地址(实际上传递的也是值,但是这里的值是地址),可以通过形参影响实参。传递对象也是引用传递,对象指向的也是堆中的一个地址。
陷阱:如果定义一个对象p原来是有属性的,但是在调用的方法中令p=null,那么原来main方法中的对象p还会不会有属性?会不会抛出异常?答案是main方法中的p是正常的,属性也会正常输出,原因是虽然传递对象属于引用传递,main方法和调用的方法中提到的p确实都指向同一个地址,指向堆中同一个空间,但是main方法和调用的方法在栈中是独立的空间,把调用方法中的p=null,只会让调用方法的栈不再指向原来p代表的那个地址,栈中的p变成null,并不会影响main主方法栈中p的指向。
方法递归调用
递归就是方法自己调用自己,每次调用时传入不同的变量。递归有助于简化问题,简洁代码,看执行下图左侧代码输出会是怎么样,递归调用的过程中内存中发生的变化。
上图中黑色的线是调用方法蓝色的线则属于递归回溯。详细过程:
- 首先在main主方法中new了一个新的对象,所以由main栈指向堆中开辟一个新空间放新的对象t1。
- 紧接着就执行到了方法调用,调用方法test,开辟一个独立的test栈进入test栈。test方法中会输入一个n的值,然后执行if判断,因为main主方法中初始输入是n=4,所以在第一个test栈中存放n = 4,进行if判断,条件成立,又遇到test(n-1)方法调用,自此在开辟一个新的独立的test栈空间。
- 因为经过上一个方法调用进行了n-1,所以开辟的第二个test栈中存放的n就等于3,进行if判断,条件成立,又开始新一轮的方法调用,再开辟一个新的test栈空间,此时存入的n值因为又经过了n-1,所以存入的n=2。
- 再第三个独立的test栈空间中输入的n=2,进行if判断,条件不成立所以跳过if中的语句进行下一句话,也就是要输出n,此时这个test栈中存放的n为2,所以会先输出一个n=2。
- 当第三个test栈中输出了以后,这个test栈的方法调用就完成了,就会退出这个栈回到执行这个方法之后的语句,也就是蓝色线条,退回到第二个栈,执行方法调用过后的语句也就是输出n的值,这个栈中存放的n=3,所以会再输出n=3。
- 下一步同理,也就是回调到第一个栈去输出n=4,自此才算方法调用完全结束,退回到main主程序中执行调用方法的下一句,在左侧代码中没有下一句,所以程序结束。
这里给给出阶乘的一段代码可以思考一下,如果在main主方法中输入factorial(5)并求最后输出的值,最终输出是什么?参考上段代码以及内存中处理过程思考这段代码递归调用在内存中的过程:
public int factorial(int n){
if(n==1){
return 1;
}else{
return factorial(n-1)*n;
}}
答案是120。
递归重要规则
- 执行一个方法时,就创建一个新的受保护的独立栈空间。
- 方法中的局部变量是独立的,不会相互影响,如n变量。
- 如果方法中使用的是引用类型变量(比如数组,对象),就会共享该引用类型的数据。
- 递归必须向退出递归的条件逼近,否则就是无限递归,出现栈溢出(StackOverflowError)。
- 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。
猴子吃桃问题
问:有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个,以后每天猴子都吃其中的一半,然后再多吃一个。当第10天时,想再吃时,发现在只有一个桃子了。那么最初共有多少个桃子?
思路:重点是找到规律,运用逆推;规律就是 前一天的桃子=(后一天的桃子 + 1) * 2
public int peach(int day){
if(day == 10){
return 1;
}else if( day >= 1 && day <= 9){
return (peach(day + 1) + 1) * 2;
}else {
System.out.println("day在1-10");
return -1;
}
}
迷宫问题
思路:1、先创建迷宫,用二维数组表示;2、规定数组的元素值:0表示可以走,1表示障碍物
public class Digui02 {
//迷宫问题
public static void main(String[] args) {
int[][] map = new int[8][7];
//将第一行和最后一行设置成1
for(int i = 0; i < 7 ; i++){
map[0][i] = 1;
map[7][i] = 1;
}
//将第一列和最后一列全部设置成1
for(int i = 0; i < 8; i++){
map[i][0] = 1;
map[i][6] = 1;
}
map[3][1] = 1;
map[3][2] = 1;
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();
}
T t1 = new T();
t1.findway(map,1,1);//i,j表示初始位置,这里我们的初始位置是(1,1)
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();
}
}
}
class T {
//上面设置0表示可以走(还没走),1表示障碍
//我们再设置2表示可以走的路,3表示尝试走过但是走不通的路
public boolean findway(int[][] map, int i, int j){
if(map[6][5] == 2){//终点在[6][5]处,当map[6][5]=2,说明找到通路可以结束
return true;
}else{
if(map[i][j] == 0){//当前这个位置是0,表示可以走
map[i][j] = 2;//先假设这条路可以走通
//所以把走过的这个路变成2
//使用找路策略,调用找路函数来确定该位置是否真的可以走通
//采用下->右->上->左的方向寻找,注意,当制定的寻找策略顺序不同,找路结果也会不同
if(findway(map, i + 1,j)){//向下
return 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{//都走不通说明最开始我们假设可以走通是错误的
//所以把这个位置设置成3表示已走过且走不通
map[i][j] = 3;
return false;
}
}else{//map[i][j]=1/2/3的情况
//=2的情况也要返回false是因为
//=2说明这个点之前已经访问过,不符合我们继续寻找的条件(不应该重复访问)
//否则会陷入无限循环
return false;
}
}
}
}
上述代码的运行结果:建议自己尝试修改迷宫情况以及找路的方向顺序策略编写代码,多看多悟!
找路的情况和我们初始设置找路的上下左右顺序相关。扩展思考:如何求出最短路径?
汉诺塔
直接上代码,还是建议多思考
public class HanNuoTower {
//汉诺塔问题
public static void main(String[] args) {
T tower = new T();
tower.move(3,'A','B','C');//自己设置
}
}
class T {
//num表示要移动的个数,a,b,c分别表示A塔、B塔、C塔
public void move(int num, char a, char b, char c){
//如果只有一个盘 num =1的情况
if(num == 1){
System.out.println(a + "->" + c);
} else {
//如果有多个盘,可以看成两个,最下面和上面的所有盘(num-1)
//第一步先移动上面所有的盘到b,借助c
move(num - 1, a, c, b);
//第二步,把最下面的盘移动到c
System.out.println(a + "->" + c);
//第三步,再把b上的所有盘移动到c,借助a
move(num -1, b, a, c);
}
}
}
运行结果是:
八皇后问题
任意两个皇后不能处于同一行、同一列或同一斜线上,问有多少种摆法
简单的思路是:
- 第一个皇后先放在第一行第一列
- 第二个皇后放在第二行第一列,然后判断是否ok,如果不ok,继续放在第二列、第三列,一次把所有列都放完,找到一个合适的位置
- 继续第三个皇后,还是第一列、第二列....直到第八个皇后也能放在一个不冲突的位置,至此得到一个正确解
- 当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,我们就会得到第一个皇后放到第一行第一列的所有正确解
- 然后从头重新开始,把第一个皇后放在第一行第二列继续执行2、3、4步骤
理论上这个问题应该用二维数组来表示棋盘,但是实际上因为第几个皇后就在第几行,所以我们只需要考虑列的问题,只用一个一维数组就可以解决问题。比如这是其中一个解:arr = {0, 4, 7, 5, 2, 6, 1, 3},0表示第一个皇后放在第一列,第二个皇后放在第5列,第三个皇后放在第8列.....arr[i]=val;中i就表示第i+1个皇后,val表示放置的位置,表示在val+1列,比如这个arr[0]=0,就表示第一个皇后在第一行第一列,arr[1]=4,表示第二个皇后在第二行第五列。
方法重载(OverLoad)
Java中允许同一个类中,多个同名方法的存在,但要求形参列表不一致。比如我们可以使用System.out.println代码输出多个语句,还可以输出不同类型的语句,这种现象就叫做方法重载。
重载的好处:减轻了起名和记名的麻烦
方法重载的前提是方法名必须相同,要求是形参列表必须不同,形参的类型、个数、顺序至少有一样不同,对参数名和返回类型没有要求即,即使返回类型不同,形参列表相同的话也不构成方法重载。
可变参数
Java允许将同一个类中多个同名且同功能但参数个数不同的方法封装成一个方法,就可以通过可变参数实现。语法定义为:修饰性 返回数据类型 方法名(数据类型... 形参){}
- 注意事项和使用细节
- 可变参数的实参可以为0个或任意多个。
- 可变参数的实参可以为数组。
- 可变参数的本质就是数组。
- 可变参数可以和普通类型的参数一起放在形参列表,但必须保证可变参数在最后。
- 一个形参列表中只能出现一个可变参数。
作用域
局部变量是指在成员方法中定义的变量,即在一个类中定义的一个方法。
全局变量,也就是属性,作用域为整个类体;局部变量也就是除了属性之外的其他变量,它的作用域只在它定义的那个代码块中。
全局变量(属性,属性是有默认值的)可以不赋值直接使用,局部变量必须赋值后才能使用。例:看下图代码,weight不赋值是可以的,但是在hi方法中的输出num语句就会报错,因为是局部变量没有赋值直接使用。
- 作用域的注意事项和使用细节
- 属性和局部变量可以重名,访问时遵循就近原则。
- 在同一个作用域中两个局部变量不能重名。
- 属性生命周期较长,伴随着对象的创建而创建,伴随着对象的死亡而死亡;局部变量生命周期较短,伴随着它的代码块的执行而创建,伴随着代码块的死亡而死亡(存在于一次方法调用的过程中)。
标签:map,调用,Java,int,第六天,面向对象编程,main,方法,public From: https://blog.csdn.net/2402_82490949/article/details/141898029