6面向对象编程(基础)

本文详细介绍了Java中的面向对象编程概念,包括类与对象的定义、属性和成员变量、内存分配机制、方法的定义和调用、递归、重载以及构造器的使用。特别强调了对象的创建过程、成员方法的调用机制以及this关键字的作用。此外,还提供了实例来解释这些概念,如递归计算斐波那契数列和解决迷宫问题等。
摘要由CSDN通过智能技术生成

面向对象编程

1. 类与对象

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. 类与对象的联系和区别:

    类 是抽象的、概念的,代表一类事物,比如人类,猫类…,即它是数据类型。

    对象是具体的、实际的,代表一个具体事物,即是 实例。

    类是对象的模板,对象是类的一个个体,对应一个实例。

2. 属性/成员变量

⚠️属性并不等价于成员变量:(133条消息) java中 成员变量和属性的区别_Ven%的博客-CSDN博客

2.1 属性的定义

  1. 属性/成员变量:成员变量 = 属性 ,如 cat 类中定义的 name、age、color 叫做类 Cat 的属性 or 成员变量。属性是类的一个组成部分,一般是基本数据类型,也可以是引用类型(对象,数组)。

  2. 属性的定义语法同变量:访问修饰符 属性类型 属性名;

    访问修饰符用于控制属性的访问范围,有四种: public,protected,默认(不写就是默认),private

  3. 属性如果不赋值则会有默认值,默认值规则与数组一样。

    int、short、byte 和 long ——0,float、double——0.0,char——\u0000,boolean——false,String——null

2.2 JAVA 内存的结构分析

  1. 栈:一般存放基本数据类型(局部变量)。
  2. 堆:存放对象(Cat cat,数组等)。
  3. 方法区:常量池(常量,比如字符串),类加载信息。

2.3 类和对象的内存分配机制

  1. 对于cat对象中的第一个数据(属性)name,因为它是一个引用数据类型(String属于类类型),所以在堆中只存储一个地址,在方法区对应于该地址的空间中存放 name 的内容。第二个数据是 age,因为它是一个 基本数据类型(int类型),所以直接存放在堆中。

在这里插入图片描述

  1. 执行第一句:

    Person p1 = new Person();
    

    ①. 先加载 Person 类信息(属性和方法信息,只会加载一次)。

    ②. 在堆中分配空间,进行默认初始化(看规则),把堆中分配空间的地址赋给 p1,p1就指向对象。

在这里插入图片描述

执行第二、三句:

​ ③. 进行指定初始化,比如:p1.age = 10; p1.name = “小明”;

p1.age = 10;
p1.name = "小明";

在这里插入图片描述

执行第四句:

在这里插入图片描述

  1. 练习题:

在这里插入图片描述

3. 成员方法

3.1 成员方法的定义

  1. 假如定义了一个 Person 类,除了有一些属性(年龄,姓名…),人类还有一些行为,比如:说话、跑步。这时就要通过成员方法完成。

  2. 成员方法的定义 :

    访问修饰符 返回数据类型 方法名 (形参列表) {

    ​ //方法体

    ​ 语句;

    ​ return 返回值;

    }

    访问修饰符:作用是控制方法的使用范围,有四种——public,protected,private,默认(不写就是默认)。

    返回数据类型:void——不返回值,int——返回一个整型,int[]——返回一个整型数组(想要返回多个结果就用数组),返回类型可以是任意类型,包含基本数据类型或引用数据类型(数组,对象)。如果方法要求有返回数据类型,则方法中最后的执行语句必须为 return 值,而且返回值类型必须与要求类型一致或兼容。

    方法名:最好见名知意,遵循驼峰命名法。

    形参列表:可以有多个形参。形参类型可以为任意(基本数据类型 or 引用类型)。调用有形参的方法时,要传入相同数目、相同类型(或者兼容的类型)的实参。

    方法体:写实现功能执行的程序,但是方法体中不能再定义方法,即方法不能嵌套定义

  3. 比如定义一个在 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;
    }
}
  1. 同一个类中的方法调用:直接调用即可。
  2. 跨类中的方法A类调用B类方法:需要通过 B 类对象名调用。比如 对象名.方法名(参数)。
  3. 跨类的方法调用和方法的访问修饰符相关

3.2 方法调用机制

在这里插入图片描述

注意:

  1. 上图代码开始执行 main 方法时,就会在栈区开辟一个 main 栈空间,用于执行 main 方法中的程序。

  2. 执行到 Person p1 = new Person(); 时就会在堆中开辟对象空间。

  3. 执行到 int returnRes = p1.getSum(10, 20); 时,会在栈中重新开辟一个 getSum 方法的栈空间,并在其中生成相应的变量,然后进行计算,当计算结果返回给 main 中的 returnRes 后,getSum 栈就会被关闭。同理,main 方法执行完毕后,main 栈也会被关闭。

3.3 成员方法传参机制

  1. 基本数据类型(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
        }
    }
    
  2. 应用数据类型(数组,对象等),传递的是地址,形参的改变会影响实参。如:

    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
            }
        }
    }
    
  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 递归

在这里插入图片描述

递归的总要准则:

  1. 执行一个方法时,就会创建一个新的受保护的独立空间(栈空间)。
  2. 方法的局部变量是独立的,不会相互影响。但是如果方法中使用的是引用类型变量(比如数组、对象),就会共享该引用数据类型。
  3. 递归必须向退出递归的条件逼近,否则就是无限递归,出现 StackOverFlowError,死龟了:)
  4. 当一个方法执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕,相应的栈空间被关闭。

经典题目:

  1. 使用递归求斐波那契数: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;
		}
	}
}
  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;
		}
	}
}
  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
			}
		}

	}
}

在这里插入图片描述

  1. 汉诺塔:有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 重载

  1. 函数名相同,但是形参不同。比如常用的:

    System.out.println(10); 
    System.out.println('a');
    System.out.println("123");
    

    上面这三个函数都是 .out 的 println 函数,但是他们的形参分别是 int,char 和 Strng 类型,即根据实参的类型选定不同形参的 println 函数来执行。

  2. 重载的注意事项:① 方法名必须相同;② 形参列表必须不同(形参类型、个数、顺序,至少有一样不同,参数名无要求);③ 返回类型无要求。构成重载:方法名相同,形参不同(类型 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 可变参数

  1. 基本概念:java 允许将同一个类中多个同名同功能但参数个数不同的方法,封装成一个方法。

    基本语法:

    访问修饰符 返回类型 方法名(数据类型 形参名){

    }

    下面的函数表示可以接受多个(0-多)int 类型的参数,如:

    public int sum(int ... nums);	//相当于输入的是一个数组 nums[i]
    
  2. 可变参数的实参可以为0个或任意多个,也可以为数组。可变参数的本质就是数组。

  3. 可变参数可以和普通类型的参数一起放在形参列表,但必须保证可变参数在最后。如:

    public int f2(int... nums, String str){}//就是错误的,应该为:public int f2(String str, int... nums){};
    
  4. 一个形参列表中只能出现一个可变参数,如:

    public void f2(int... nums, double... dou){};	//是不可以的,报错
    

3.7 作用域

  1. java 中,主要的变量就是属性(成员变量)和局部变量。

    全局变量:也就是属性(成员变量),作用于为整个类体。

    局部变量:一般是指在成员方法中定义的变量(其实应该不是成员方法,而是定义在代码块 {} 中的变量就叫局部变量,简而言之就是除属性外的其他变量),只能在定义局部变量的方法(代码块)中使用。

  2. 全局变量(属性)可以不赋值,直接使用,因为有默认值(参考 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 是局部变量,没有赋初值,且没有默认值
        }
    }
    
  3. 属性和局部变量可以重名,访问时遵循就近原则

  4. 在同一个作用域中,比如在同一个成员方法中,两个局部变量,不能重名。

  5. 属性生命周期较长,伴随着对象的创建而创建,伴随着对象的死亡而死亡。局部变量生命周期较短,伴随着它的代码块的执行而创建,伴随着代码块的结束而死亡。

  6. 作用域范围不同:

    全局变量(属性):可以被本类使用,或者其他类使用(通过对象调用)。

    局部变量:只能在本类中对应的方法中使用。

  7. 修饰符不同:全局变量(属性)可以加修饰符;局部变量不能加修饰符。如:

    class Cat{
        private double weight;
    }
    

4. 构造器

4.1 构造器的定义

构造方法又叫构造器,是类的一种特殊的方法,它的主要作用是完成对新对象的初始化(不是创建,而是初始化)。特点有:① 构造器方法名和类名相同;② 构造器没有返回值(也不能写 void);③ 在创建对象时,系统会自动的调用该类的构造器完成对对象的初始化。④ 构造器的修饰符可以默认,也可以是 public, private,protected。

基本语法:

[修饰符]	方法名(形参列表){
	方法体;
}
  1. 一个类可以定义多个构造器,即构造器的重载。
  2. 如果程序员没有定义构造器,系统会自动给类生成一个默认无参构造器(也叫默认构造器)。如:有一个 Dog 类,我们没有给他定义构造器,系统的默认构造器如下,这也就解释了为什么定义一个没有构造器的类是这样:Dog tt = new Dog(); 小括号就代表了调用默认构造器。
Dog(){
}
  1. 一旦定义了自己的构造器,默认构造器就被覆盖了,就不会再使用默认的无参构造器了,除非显式的定义一下,即:Dog(){}

4.1 对象的创建流程

  1. 执行语句:Person p = new Person(“小倩”, 20);

    ① 在方法区中加载 Person 类信息(属性和方法信息),只加载一次。

    ② 在堆中分配空间。

在这里插入图片描述

  1. 完成初始化:

    ① 先使用默认构造器 Person(){} 对其进行默认初始化,此时 age = 0(int 类型嘛),name = null(String类型嘛)。

在这里插入图片描述

② 又因为 Person 类的定义中有显示初始化语句:int age = 90; 故又执行显示初始化

在这里插入图片描述

③ 最后因为 Person 类中定义了构造器:Person(String n, int a){…},同时定义类的语句:Person p = new Person(“小倩”, 20); 也传递了相应的参数,即调用了构造器,所以最后还要执行构造器初始化如下,因为 name 是String 类型,所以需要在方法区的常量池中存放,堆中的空间只存放常量池中相应位置的地址。

在这里插入图片描述

  1. 完成初始化后,将堆中对象(数据空间)的地址赋值给类的变量名(或者叫类的引用)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)]

  1. this 关键字可以用来访问本类的属性、方法、构造器

  2. this 用于区分当前类的属性和局部变量

  3. 访问成员方法的语句:this.方法名 (参数列表);访问属性的语句:this.属性

  4. 访问构造器语法: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;
    	}
    }
    
  5. this 不能在类定义的外部使用,只能在类定义的方法中使用


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值