JAVA基础——面向对象编程(基础)(韩顺平P192-P263)

目录

一、类与对象

1、基本概念和定义

2、对象在内存中的存在形式(重要)

2.1 内存存在形式

2.2 内存分配形式

3、属性和成员变量

二、成员方法

1、方法简介和小案例

2、方法调用机制原理(内存)

3、小细节

三、成员方法传参机制(重要)

1、基本、引用数据类型传参的不同

2、克隆对象

四、方法递归调用

1、机制原理

2、经典案例

1)编写斐波那契数列1,1,2,3,4,8

2)迷宫问题

3)汉诺塔问题

五、方法重载overload

六、可变参数

七、变量作用域(重要)

八、构造器(构造方法)

九、关键字this


一、类与对象

1、基本概念和定义

场景:比如要进行同样的操作,比如都是加法。要对100组数进行相加。一个个单独写加法没有必要,不利于数据的管理,而且效率低,所以就可以专门来写一个方法,来形成统一的操作。

java设计者引入类与对象(OOP编程),根本原因是基础的奇数,不能完美解决新的需求。

  • 一个程序就是一个世界,有很多事物(即:对象[属性,行为])比如:小狗[年龄,吃饭]
  • 类:int这种是java系统提供的数据类型,像下面“猫”这种定义的类,就是自定义的数据类型
  • 对象(实例):int这个数据类型可以对应很多数,比如10,20。同样“猫”这个类,也可以对应很多个不同的猫,这就称为对象(或者叫实例)。通过“猫”类,可以创建猫对象,即具体的猫,可以有无数个
    • 有很多不同的说法:创建一个对象 or 实例化一个对象 or 把类实例化……

所以把一种事物的共性提取出来,形成了“类”这个自定义数据类型(至少包含属性和行为),有了类以后,就可以创建很多具体的猫。

JAVA最大的特点就是面向对象

public class Object01 {
    public static void main(String[] args){
        // 可以实例化一只猫(创建一个猫对象)
        Cat cat1 = new Cat();
        cat1.name = "小白";
        cat1.age = 3;
        cat1.color = "白色";

        Cat cat2 = new Cat();  // 这样就创建了第二只猫
        cat1.name = "小花";
        cat1.age = 5;
        cat1.color = "花色";

        System.out println("第一只猫的信息" + cat1.name + cat1.age + cat1.color);
        System.out println("第二只猫的信息" + cat2.name + cat2.age + cat2.color);
    }
    }

// 首先定义一个“猫”类
class Cat{
    String name;  // 属性:名字
    int age; // 属性:年龄
    String color; // 属性:颜色
}

2、对象在内存中的存在形式(重要)

2.1 内存存在形式

  • 1、在new创建的时候,首先会把类的信息加载到“方法区”,包含1、属性信息  2、行为(方法信息)。然后会在堆中分配空间,进行默认初始化
  • 2、创建实例之后,在栈里面有一个cat,对应一个地址。该地址指向的堆里面有三个小空间(因为cat有3个属性),cat是对象名(也叫对象引用),实际的对象存放在堆和方法区里面。
  • 3、第一个空间name因为是String类型,所以会指向方法区中的常量池的一个空间,具体内容是“小白”。(color也是String类型的,即一个引用类型,所以也是还要放到方法区的常量池
  • 3、第二个空间age是int基本数据类型,所以直接存在的小空间里面。
  • 可以先申明Cat cat1 (只有一个名字); 再创建对象 cat = new Cat();(这里才开辟空间)

2.2 内存分配形式

比如下面,把对象p1赋给对象p2。会把p1的地址拷贝给p2,即p2也指向堆中的相同地方。相当于一个东西有两个名字,这俩可以共同访问和更改同样的对象。所以下面p2.age=10

3、属性和成员变量

  • 属性properties:年龄、颜色、名字也可以叫成员变量(甚至也有人叫field字段)。
  • 属性一般是基本数据类型,也可以是引用类型(比如数组、另外一个对象)
  • 属性的定义:访问修饰符 属性类型 属性名;(访问修饰符:是用来控制属性的访问范围。有四种:public,protected,默认,private    后面会着重讲这个访问修饰符)
  • 属性如果不赋值,会有默认值,规则和数组一致。

二、成员方法

1、方法简介和小案例

成员方法简称方法。比如人类,除了“年龄、姓名”这种属性,还有一些行为,比如“说话、跑步”

public class Object02 {
    public static void main(String[] args){
        Person p1 = new Person();  //先创建实例 
        p1.speak();  // 这里才是调用了方法,
        // 会去找Peron这个类里面的speak方法,然后执行代码体。
        p1.cal01();
        p1.cal02(5);   // 调用cal02方法,传入值5
        p1.cal02(15);   // 可以多次调用这个方法
        int sum = p1.getSum(5,2);  // 传入两个值,用sum来接收
        System.out println("getSum方法返回的值是:"+ sum);  // 就可以打印输出
    }
    }

// 可以在Person类里面添加成员方法
class Person{
    String name;  
    int age; 
    //public表示这个方法是公开的,void表示没有返回值,speak是方法名, ()是形参列表
    public void speak(){  // {}中的方法体,写具体要执行的代码
        System.out println("我是一个好人");  
    }
    // 添加一个方法,可以计算从1到100的和
    public void cal01(){
        int res = 0;
        for(int i=0;i<=1000;i++){
            res+=i;
        }
        System.out println("计算结果:"+ res); 
    }
    // 添加一个方法,可以接收一个数n,计算从1到n的结果
    public void cal02(int n){  // 表示当前有一个形参n,可以接收用户的输入
        int res = 0;
        for(int i=0;i<=n;i++){
            res += i;
        }
        System.out println("计算结果:"+ res); 
    }
    // 添加一个方法,可以计算两个数的和
    public int getSum(int num1, int num2){  // int表示该方法执行后有一个int类型的返回值
        int res = num1 + num2;
        return res;  // 表示返回res的值,有接收就返回,没有接收就不返回
    }
}

2、方法调用机制原理(内存)

1、第一句话先实例化p1,在栈中一个对象名p1,指向堆中的对象空间

2、第二句话,p1 = getSum(10,20); 会在栈中再开辟一个独立的空间。然后把10和20赋值给num1和num2,计算出res并返回(可以认为返回之后,这个计算的栈空间就被释放了)。把结果赋值给res,然后第三句话print打印。

  • 当程序执行到方法时,就会开辟一个独立的栈空间。
  • 当方法执行完毕,或者执行到return语句,就会返回。
  • 返回到调用方法的地方,继续执行后面的代码。当main栈执行完毕后,整个程序就退出。

3、小细节

  • 方法的优势:1、提高代码的复用性  2、可以将实现的细节封装起来,其他用户来调用即可。
  • 如果是void,那么可以不写return,或者只写一个retun,不能有具体返回值。但是如果有返回数据类型如int,就必须有return,可以return具体的常量、或者是表达式。而且要求返回类型必须一致或者兼容。
  • 访问修饰符可以不写,那就是默认的。  直接void speak(){}
  • 一个方法最多有一个返回值,如果要返回多个数值,可以用数组来实现。然后用arr[0]  arr[1]来访问具体的数值。
  • 方法名最好用驼峰命名法。并且最好有一定的含义。
  • 调用带参数的方法时,必须传入相同类型或者兼容类型的参数
  • 方法定义的时候的参数是形参,调用方法时候传入的参数是实参。形参和实参的顺序也要一致
  • 方法体里面不能再定义方法(方法不能嵌套定义)。
  • 一个类中的方法可以互相直接调用。但是跨类的方法要互相调用,也要先创建对象,再调用。要用对象名.方法名。(注意:方法的跨类调用,和访问修饰符有关)

三、成员方法传参机制(重要)

1、基本、引用数据类型传参的不同

基本数据类型,传递的是值(值拷贝),所以形参的任何改变不影响实参。

public class MtehodParameter01 {
    public static void main(String[] args){
        int a = 10;
        int b = 20;
        AA aa = new AA();
        aa.swap(a,b);  // 调用swap这个方法,传入a和b进行交换,这里面调用方法后会输出20、10
        System.out println("a="+a+"b="+b);  // 这一步会输出10、20(原本ab的值)

    }
}

class AA{
    public void swap(int a, int b){
        int temp = a;
        a = b;
        b = temp;  // 交换ab
        System.out println("a="+a+"b="+b);
    }
}

引用数据类型,传递的是地址,所以在方法中对形参修改,也会影响实参。

public class MtehodParameter02 {
    public static void main(String[] args){
        int[] a = {1,2,3};
        BB bb = new BB();
        bb.test(a);  
        System.out println("main中的输出:"+arr[0]); 
        // 这一步会输出200(引用数据类型,是地址拷贝,所以实参的修改也对该地址中的数据进行了修改)
    }
}

class BB{  // 接收一个数组并修改
    public void test(int[] arr){
        arr[0] = 200;
        System.out println("test中的输出:"+arr[0]);
    }
}

传对象也是一样的,但是注意!下面的两个变化,如果在方法中重新实例化对象,那么就会在堆中重新创建一个对象空间,即!!形参p指向新的对象空间,而实参指向旧的对象空间

public class MtehodParameter02 {
    public static void main(String[] args){
        Person p = new Person();
        p.name = 'jack';
        p.age = 10;
        BB b = new BB();
        b.test(p);  // 传入的是p,一个对象
        System.out println(p.age); 
        // 引用类型,形参影响实参,输出100
    }
}
class Person{
    String name;
    int age;
}
class BB{  // 接收一个数组并修改
    public void test(Person p){
        p.age = 100; // 栈中形参也指向了堆中的同一个地址。
        // 变化1:p = null ;  // 如果是这样的话,main中p.age会输出10
        // 这并不会把堆重点对象空间置空,只是栈中的test空间里面的p不再指向任何空间了
        // 变化2:
        // p = new Person;
        // p.name = "tom";
        // p.age = 99;
        // 现在main方法中输出10.因为test方法中重新new了p,即会指向堆中的一个新的对象空间
        //(而不是指向实参对应的那个对象空间)
    }
}

2、克隆对象

要求克隆的和原始对象相互独立,但是要有相同的属性

public class MtehodParameter03 {
    public static void main(String[] args){
        Person p = new Person();
        p.name = 'jack';
        p.age = 10;
        // 实例化克隆的方法
        MyTools tools = new MyTools();
        Person copyp = tools.copy01(p);  // 调用方法,克隆p,返回一个新对象
        // 新对象和原始对象是相互独立的,只是属性相同。(可以通过输出对象的哈希code来看对象是否是一个)
        System.out.println(p == p2); // 返回false
    }
}
class Person{
    String name;
    int age;
}
 
class MyTools{  // 该方法可以克隆对象(复制person对象)
    public Person copy01(Person p){  // 注意返回的数据类型是Person
        // 创建一个新对象,并复制属性
        Person p2 = new Person();
        p2.name = p.name;
        p2.age = p.age;
        return p2;
    }
}

四、方法递归调用

1、机制原理

递归就是方法自己调用自己,每次调用时传入不同的变量,有助于解决复杂问题,让代码变简洁。

递归Recursion的应用场景有很多,比如8皇后问题、汉诺塔、阶乘问题、迷宫问题。各种算法里面也用的很多,比如快排、归并排序、二分查找、分治计算等等。

  • 对于下面这个打印的递归调用,在类的方法中实现递归思路。每次调用test()方法,都会重新开一个栈,等于说会开3个栈空间,然后直到if条件不成立,才会执行到打印2
  • 蓝色的线:最底层的栈执行完了之后返回,会返回到倒数第二层的栈,打印3,然后再回到倒数第3个栈,打印4。最后才返回到主栈。所以输出的顺序是2、3、4.
  • 每一个栈都会完整执行所有方法。左边是打印,右边这个例子是阶乘:

// 阶乘案例
public class Recursion {
    public static void main(String[] args){
        T t1 = new T();
        int res = t1.factorial(5); // 
    }
}
class T{
    public int factorial(int n){
        if(n == 1){
            return 1;
        }else{
            return factorial(n-1) * n;
            // 会额外开辟5个栈。然后再从底层往外层调用
            // 最底层的栈执行到if结束,然后其余4层都会执行f()*xx
        }
    }
}

 注意:假设递归传的是引用数据类型,那么多次递归就会相互影响。

递归必须向退出递归的条件逼近,否则就是无限递归,出现StackOverflowError

2、经典案例

要返回运用当前方法输出的结果时,就用递归。

1)编写斐波那契数列1,1,2,3,4,8

public class Recursion {
    public static void main(String[] args){
        T t1 = new T();
        int res = t1.fibonacci(7); 
    }
}
class T{
    public int fibonacci(int n){
        if(n == 1 || n == 2){
            return 1;
        }else{
            return fibonacci(n-1) + fibonacci(n-2);
        }
    }
}

2)迷宫问题

要从左上角走到右下角。把下面这个迷宫看成8行7列的数组。用0表示可以走的路1表示不能走的

下面这个代码用递归实现,根据策略,相当于先递归一直往下走,能走通的标2,直到走不通了,再向右走。。。再往上走,再往左走。直到map[6][5]这个位置被标位2

当然这个找路策略也可以改变(不一定非得下右上左)

public class Migong {
    public static void main(String[] args){
        // 先来画迷宫地图
        int[][] map = new int[8][7];
        for(int i=0;i<7;i++){
            map[0][i] = 1;  // 最上面和最下面一行为1
            map[7][i] = 1;
        } 
        for(int i=0;i<8;i++){
            map[i][0] = 1;  // 最左边和最右边一列为1
            map[i][7] = 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;);  // 因为数组为引用类型,所以在方法中的修改,对于实参t也有影响,直接打印输出即可
        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{
    public boolean findway(int[][] map, int i, int j){  // ij表示初始位置
        // 递归找路。用0表示可以走、1表示障碍物、2表示可以走、3表示已经走过但是是死路
        // 当map[6][5]的值为2的时候,就表明找到通路了,否则继续找。
        // 确定老鼠找路的策略  下-右-上-左
        if(map[6][5] == 2){
            return true; // 已经找到了(没走到的都为0)
        }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{
                    map[i][j] = 3;  // 如果上下左右都为1,都走不通,把当前值设为3 是死路,返回false
                    return false;
                }
            }else{
                return false;
            }
        }
    }
}

回溯:比如要是在某一个位置上下左右都走不通了,就会回到上一个位置,即回到上一个栈。

3)汉诺塔问题

就是要把A塔上的盘移到C塔。要求永远都必须大盘在下,小盘在上。

把所有盘,都看成最下面一个盘和上面一堆盘(简化问题)

public class HanoiTower {
    public static void main(String[] args){
        Tower tower = new Tower();
        tower.move(3,'A','B','C');
    }
}
class Tower{
    // move这个方法表示把num个盘,从a塔移动到c塔,借助b
    public void move(int num, char a, char b, char c){
        if(num == 1){  // 如果只有1个盘,那么直接从a放到c
            System.out.println(a + "->" + c);
        }else{  
            // 如果有多个盘,也先看成两个盘(最下面的,和上面的所有盘num-1)
            move(num-1,a,c,b); // 借助c,把上面所有的盘移到b
            System.out.println(a + "->" + c);  // 把最下面的这个盘移动到c
            move(num-1,b,a,c); // 借助a,把上面所有的盘从b移动到c
        }
    }
}

五、方法重载overload

java中允许同一个类中,多个同名方法的存在,但要求形参列表不一致(类型or顺序or个数不一样 都可以)。不然就会报错“重复定义方法”。(返回类型不要求,仅返回类型不一样也是重复定义)

比如System.out.println( ) 这个方法可以传入不同的值。其实在out这个对象下面有很多个名为“println()”的方法,但是他们的形参列表不一样println(int a); println(char a)……

好处:减轻了起名的麻烦、有利于接口编程。

如下,下面这个类里面实现了3个方法的重载(方法名一样,形参列表不同)。在main中实例化对象调用后,传入值,根据传入数值的类型进行匹配,程序会自己看应该具体调用哪个方法。

public class MyMethod{
    public static void main(String[] args){
        Mycalculator AA = new Mycalculator();
        AA.calculate(3,4);
        AA.calculate(3,4.0);
        AA.calculate(3.0,4);
    }
}
class Mycalculator{
    public int calculate(int n1, int n2){
        return n1+n2;
        }
    public double calculate(int n1, double n2){
        return n1+n2;
        }
    public double calculate(double n2, int n1){
        return n1+n2;
        }
}

如果方法是(double,double,double),传入的是(double,double,int),那么也可以把int自动转化为double,从而匹配到这个方法。但是要是有(double,double,int)这个方法,那优先级更高。

  • 可以自动类型转换,但还是优先匹配最为相符的

六、可变参数

  • java允许将同一个类中多个同名同功能但参数个数不同的方法,封装成一个方法。就可以通过可变参数实现。语法: 访问修饰符 返回类型 方法名(数据类型形参名){ }
  • 可以把参数当做一个数组
  • 可变参数可以和普通参数一起放在形参列表,但必须保证可变参数在最后。一个形参列表中只能出现一个可变参数
public class MyMethod{
    public static void main(String[] args){
        Method AA = new Method();
        int result = AA.sum(1,2,3);
    }
}
// 方法 求2个数的和、3个数的和……,可以不用每个都写一个方法
class Method{
    public int sum(int... nums){  // 表示可以接收多个int参数,可以吧nums当作数组
        System.out println("接收的参数个数=" + nums.length);
        // 求和
        int res = 0;
        for(int i=0; i<nums.length; i++){
            res += nums[i];
        }
        return res;
        }
}

七、变量作用域(重要

  • 在java编程中,主要的变量就是属性(成员变量)和局部变量
  • 局部变量一般是指在成员方法中定义的变量(比如上面代码中的res)。局部变量的作用域只在这个方法中。
  • 成员变量在类中定义(方法外),在成员方法中都可以使用。除了属性之外的全部变量,可以认为是局部变量。(比如写在代码块里面的变量即使在方法之外,但是也不算成员变量)
  • 属性可以不赋值,直接使用,因为有默认值。但是局部变量必须赋值后才能使用
class Cat{
    int age = 10;
    double weight;  // 不赋值,也会有一个默认值0.0
    public void hi(){  
        int num;  // 局部变量,不会给默认值。
        System.out println(num);  // 会报错,没有初始化变量
        }
}

细节:

  • 属性和局部变量是可以重名的,使用的时候遵循就近原则
  • 同一个作用域中,两个局部变量不能重名
  • 属性的生命周期较长,伴随着对象的创建而创建,对象的销毁而销毁。局部变量的生命周期较短,方法执行完了之后就销毁了。
  • 属性能被本类内部或者其他对象调用使用。局部变量只能在本类的本方法中使用。
  • 属性可以加修饰符,但是局部变量不能加修饰符

八、构造器(构造方法)

需求场景:比如之前要先创建一个Person对象,再给他的年龄和姓名属性赋值。现在想在创建对象的时候就直接制定他的年龄和名字

constructor构造器: 是类的一种特殊方法,它主要作用是完成对新对象的初始化。并不是创建对象,只是初始化对象:  [修饰符] 方法名(形参列表){ 方法体;}

  • 构造器没有返回值。方法名和类名必须一样。参数列表必须和成员方法一样的规则。
  • 构造器的调用,由系统完成。即new的时候,就自动调用构造器给属性赋值
public class Constructor01 {
    public static void main(String[] args){
        Person p1 = new Person("jack",80);
        // new一个对象时,直接通过构造器来给属性赋值
    }
}

class Person{
    int age;  // 本来默认为0
    String name;
    // 下面这个就是构造器Person(名字和类名一样)
    public Person(String pname, int page){ // 形参列表规则和成员变量一样 
        name = pname;  // 这里就初始化属性了
        age = page;
        }
}

细节:

  • 一个类可以定义多个构造器,为构造器的重载。(比如可以再有一个构造器只初始化name)
  • 如果没有定义构造器,系统会给类自动生成一个默认无参构造器。(可以用javap反编译看)一旦定义了自己的构造器,默认的构造器就被覆盖了,new Person()就会报错。除非再显式定义下Person(){}才能直接用。
    • 比如自己定义无参构造器,方法中可以写age=18.这样new Person()之后,age都初始18.

构造器的流程:加载类信息(只会加载一次)——在堆中分配空间——默认初始化——显式初始化——对象的初始化(看有没有自定义的构造器)——把对象在堆中的地址返回给栈中的p对象名

九、关键字this

想让构造器的形参直接写成属性名。不再用写pname、page。但是name=name,age=age的时候,程序不知道name是属性还是形参(根据就近原则,两个等式左右的name都是局部变量形参了

  • java虚拟机会给每个对象分配this,代表当前对象
  • 原代码中用this.name可以表示当前对象的属性。所以就可以用this.name = name;
  • 可以认为创建对象的时候,就也有一个隐藏的属性this,指向对象本身。

可以用哈希code(把该对象的实际地址转化成一个整数)来看。调用哪个对象,this就指向谁。

public class This01 {
    public static void main(String[] args){
        Person p1 = new Person("jack",20);
        System.out println("p1的hashcode的值为:" + p1.hashCode());  // 打印hashcode,相当于地址

    }
}
class Person{
    int age;
    String name;
    public Person(String name, int age){ 
        this.name = pname;  // 用this表示该对象的属性
        this.age = page;
        System.out println("this的hashcode的值为:" + this.hashCode());  // 这和main中打印的结果是一样的
        }
}
  • this关键字可以用来访问本类的属性、方法、构造器。
  • this访问成员方法的语法: this.方法名(参数列表)
  • this还能访问构造器:  this(参数列表)  注意只能在构造器中调用另一个构造器。
// 判断一个对象和另一个对象属性是否相同,相同则返回true
public class This02 {
    public static void main(String[] args){
        Person p1 = new Person("mary",20);
        Person p1 = new Person("jack",30);
        p1.compareTo(p2);  // 比较当前对象p1和传进去的p2是否相同
    }
}
class Person{
    int age;
    String name;
    public Person(String name, int age){ 
        this.name = pname;  
        this.age = page;
        }
        public boolean compareTo(Person p){
            return this.name.equals(p.name) && this.age == p.age;
        }
}

  • 14
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值