Skip to content

类与对象

单独使用定义变量的方式和数组的方式进行大量数据的管理,效率低下

在面向对象中,我们使用对象来体现各种各样的事物,对象中有两个关键的内容:属性(内在的特征,与自身关联,如年龄,名字等)和行为(行为方法。如修改名称等),将属性提取出来,就形成了一个类(类是一个数据类型)

类与对象的关系示意图:

image-20250327104828374

将猫类的所有属性提取出来,形成一个猫类的数据类型(自己定义的数据类型,intJava系统提供的数据类型),这个数据类型有以下常用的属性:nameage等,当然所有的猫都有行为,如:跑、叫和吃等

对于一个猫类,我们可以使用这个类定义许多的猫,这些猫就是对象(简而言之,我们可以通过猫类来创建猫对象,即创建一只具体的猫)

java
// 定义一个猫类(自定义的数据类型)
class Cat {
    String name;
    int age;
    String color;
}

// 使用OOP面向对象解决
// 实例化一只猫对象
Cat cat1 = new Cat();
// 为对象添加属性
cat1.name = "小白";
cat1.age = 3;
cat1.color = "白色";

综上所述:类就是一个数据类型,对象就是一个具体的实例,从类到对象有几种说法:

  • 创建了一个对象
  • 实例化一个对象
  • 把类实例化

对象的内存布局

Java内存的结构分析:

  • 栈:一般存放基本数据类型(局部变量)
  • 堆:存放对象(自定义类,数组等)
  • 方法区:常量池(常量、比如字符串),类的加载信息(自定义的类信息只会加载一次)

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

image-20250327111603422

字符串是一个引用类型,在堆中存放的是地址,实际上将数据放在方法区中的常量池中,基础数据类型的值会直接存放到堆中

在执行实例化对象的时候,会将类的信息(属性信息和方法信息)加载到方法区中

Java创建对象流程的简单分析:

  1. 先加载Cat类信息(属性和方法信息,只会加载一次)
  2. 在堆中分配空间,进行默认初始化(和数组初始化类似)
  3. 把地址赋值给cat(创建的对象名),对象名就指向这个对象
  4. 进行指定初始化,如cat.age = 12;

将实例化的对象赋值给另一个对象名,Person p2 = p1;p1赋值给了p2,或者说让p2指向p1,是引用类型,实现的是地址拷贝,指向的都是同一个堆中的对象,其内存分配过程如下:

image-20250327131535351

小练习:

image-20250327132602592


属性的概念

属性也叫成员变量,或者叫field字段,属性是类的一个组成部分,一般是基本数据类型,也可以是引用类型(对象、数组)

java
public class Object {
    // main方法
    public static void main(String[] args) {
        // 实例化一个对象
        // cat1是对象名,new Cat()是真正的对象,在堆中创建了对象空间,将地址与对象名的指向地址联系起来
        Cat cat1 = new Cat();
        // 访问属性
        cat1.name;
    }
}

// 定义一个猫类(自定义的数据类型)
class Cat {
    // 定义属性/成员变量
    String name;
    int age;
    String color;
    String[] master;  // 属性可以是引用数据类型
}

注意事项:

  • 属性的定义语法同变量的定义,但是多了一个访问修饰符的概念:访问修饰符 属性类型 属性名;

    访问修饰符是用于控制属性的范围,有四种访问修饰符:publicproctected默认情况private

  • 属性的定义类型可以为任意类型,包含基本类型或引用类型

  • 属性如果不赋值,有默认值,规则和数组一致


方法的概念

成员方法的定义:

java
访问修饰符 返回的数据类型 方法名(形参列表..) {
    方法体语句;
    return 返回值;
}
  • 访问修饰符:控制方法的使用范围,常用的方法修饰符有四个

  • 形参列表:表示成员方法的输入

    • 一个方法可以有0个参数,也可以有多个参数,中间使用逗号隔开
    • 参数类型可以为任意类型,包含基本类型或引用类型,同一列表的参数类型可以是不同的类型
    • 调用参数的方法时,一定对应着参数列表传入相同类型或兼容类型(可以进行自动转换)的参数
    • 方法定义时的参数称为形式参数,简称行参;方法调用时传入的参数称为实际参数,简称实参,实参和形参的类型要一致或兼容,个数、顺序必须一致
  • 返回的数据类型:表示成员方法输出,void表示没有返回值,返回的类型可以为任意类型,包含基本类型和引用类型,但是接收返回值时,需要声明与返回值同类型的变量去接收

    一个方法最多只有一个返回值,如果需要多个返回值,我们可以返回数组

    java
    // 返回两个数的和差
    class Num {
        public int[] getSumAndSub(int n1, int n2) {
            int res[] = new int[2];
            res[0] = n1 + n2;
            res[1] = n1 - n2;
            return res;
        }
    }
  • 方法主体:表示为了实现某一功能代码块,可以为输入、输出、运算、分支、循环、方法调用,但是在方法主体中不能再定义方法,即:方法不能嵌套定义

  • return语句不是必须的,如果方法要求有返回数据类型,则方法体中最后的执行语句必须为return值,而且要求返回值类型必须和return的值类型一致或者兼容(可以自动转换的)

    java
    // 类型一样,编译通过
    public double f1() {
        return 1.1;
    }
        
    // 兼容(可以自动转换),编译通过
    public double f1() {
        int n = 1;
        return n;
    }
        
    // 类型不一致,且不能自动转换,编译不通过
    public int f1() {
        return 1.1;
    }

    如果方法是void,则方法中可以没有return语句,或者只写return;,但是不能返回具体的内容

成员方法简称为方法,用于声明一些具体的行为

java
public class Object {
    // main方法
    public static void main(String[] args) {
        // 实例化一个对象
        Person person1 = new Person();
        // 调用方法
        person1.speak();
        person1.cal(6);
        int data = person1.getSum(3, 6);  // 将方法返回的值赋给data
    }
}

// 定义一个猫类(自定义的数据类型)
class Person {
    // 定义属性/成员变量
    String name;
    int age;
    // 定义方法/成员方法
    // public表示方法是公开的;void表示方法没有返回值;speak表示方法名
    public void speak() {
        System.out.println("我是一个好人");
    }
    // (int n)表示形参列表,表示当前有一个形参n,可以接收调用者的输入
    public void cal(int n) {
        int res = 0;
        for(int i; i <= n; i++) {
            res += i;
        }
        System.out.println("计算结果=" + res);
    }
    // 方法有返回值的形式
    // int表示方法执行后,返回一个int值
    public int getSum(int num1, int num2) {
        int res = num1 + num2;
        return res;
    }
}

方法调用机制的原理图:(重要)

image-20250327135857719

首先会在栈中创建一个栈空间(main栈空间),当执行了方法时,会在开辟一个独立的栈空间(栈和栈之间不会相互影响),进行形参的赋值,执行完方法后,会将结果进行返回,当返回之后,这个临时开辟的独立栈空间就会被释放

成员方法的好处:减少代码冗余,提高了代码的复用性;同时可以将实现的细节封装起来,然后供其他用户来调用

方法调用细节:

  • 同一个类中的方法调用:直接调用即可

    java
    class A {
        public void print(int n) {
            System.out.println("print()方法被调用 n=" + n);
        }
        
        public void sayOk() {
            // 调用同一个类中的print()方法
            print(10);
        }
    }
  • 跨类中的方法A类调用B类的方法:需要通过对象名调用,跨类的方法调用共和方法访问修饰符相关

    java
    class A {
        public void print(int n) {
            System.out.println("print()方法被调用 n=" + n);
        }
    }
    
    class B {
        public void sayOk() {
            // 创建一个A类的对象
            A a = new A();
            // 调用a对象的方法
            a.print(10);
        }
    }

方法的传参机制

方法的传参机制在编程中是非常重要的:

  • 对于基本数据类型的传参机制,传递的是值(值拷贝),形参的任何改变不影响实参

    java
    public class MethodParameter {
    	public static void main(String[] args) {
            int a = 10;
            int b = 20;
            // 实例化AA对象
            AA obj = new AA();
            obj.swap(a, b);
            System.out.prinln(a, b);    // 10 20
    	}
    }
    
    class AA {
        public void swap(int a, int b) {
            System.out.prinln(a, b);   // 10 20
            int temp = a;
            a = b;
            b = temp;
            System.out.prinln(a, b);   // 20 10
        }
    }

    通过内存来进行辅助理解:

    调用obj.swap(a, b);方法,传递的是10和20,swap()方法中接收的是10和20,两个栈是独立的空间,其基本数据类型的变量是不冲突的

  • 对于引用数据类型的传参机制,传递的是地址(传递的也是值,但这个值是地址),形参的改变会影响实参

    java
    public class MethodParameter {
    	public static void main(String[] args) {
            int[] arr = {1, 2, 3};
            BB obj = new BB();
            obj.test(arr);    // 200 2 3
            
            // 遍历数组
            for(int i = 0; i < arr.length; i++) {
                System.out.print(arr[i] + "\t");
            }
            System.out.println();    // 200 2 3
    	}
    }
    
    class BB {
        public void test(int arr) {
            arr[0] = 200;   // 修改了传递进来的数组元素
            // 遍历数组
            for(int i = 0; i < arr.length; i++) {
                System.out.print(arr[i] + "\t");
            }
            System.out.println();
        }
    }
    java
    public class MethodParameter {
    	public static void main(String[] args) {
            Person p = new Person();
            p.name = "jlc";
            p.age = 25;
            BB b = new BB();
            b.test(p);
            System.out.println(p.age);   // 100
    	}
    }
    
    class Person {
        String name;
        int age;
    }
    
    class BB {
        public void test(Person p) {
            p.age = 100;
        }
    }

    小变化:

    java
    public class MethodParameter {
    	public static void main(String[] args) {
            Person p = new Person();
            p.name = "jlc";
            p.age = 25;
            BB b = new BB();
            b.test(p);
            System.out.println(p.age);   // 10
    	}
    }
    
    class Person {
        String name;
        int age;
    }
    
    class BB {
        public void test(Person p) {
            p = null;
        }
    }

    BB类中的将传入的p对象设置为null,是切断了当前方法栈空间中的P指向的堆中的对象,但是不会影响原先main栈空间中p的指向,p还是指向堆中的对象,因此,其值还是25

    image-20250327211100538

方法的递归调用

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

递归调用的本质仍然是方法的调用

递归使用规则:

  • 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
  • 一般传递的是基本类型数据,所以方法的局部变量是独立的,不会相互影响
  • 如果方法中使用的是引用类型的变量,如数组,就会共享该引用类型的数据
  • 递归必须向退出递归的条件逼近,否则就是无限递归
  • 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕了,开辟的空间也会清除掉

image-20250328131533079

每一次调用方法的递归,都会在栈空间中开辟一块方法空间,当方法执行完后,方法空间会消失,同时会返回到上一个调用空间,在哪里调用就返回给哪里,依次递归的返回,最后返回到main方法中,执行完后退出

阶乘
java
// 递归调用---阶乘
public int factorial(int n) {
    if(n == 1) {
        return 1;
    }
    else {
        return factorial(n - 1) * n;
    }
}

image-20250328133518263

斐波那契

斐波那契是指,一个数为前面两个数的和

java
// 递归调用---斐波那契
public int fibonacci(int n) {
    if(n == 1 || n == 2) {
        return 1;
    }
    else {
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}

方法的重载

Java中允许同一个类中有多个同名方法的存在,但要求形参列表不一致,方法重载有利于减轻了起名和记名的麻烦,如系统定义的System.out.println()方法,方法名都是println(),但是可以输出数值,字符串等,是由于使用了方法的重载,设置了同名的方法,但是传递的形参列表不一致

java
class MyCalculator {
    // 计算两个整数的和
    public int calculate(int n1, int n2) {
        return n1 + n2;
    }
    // 计算一个整数,一个浮点数的和
    public double calculate(int n1, double n2) {
        return n1 + n2;
    }
}

方法重载的注意事项:

  • 方法名必须相同
  • 形参列表必须不同(形参类型或个数或顺序,至少有一个是不同的,参数名无要求)
  • 返回类型无要求(返回类型不是构成方法重载的条件)

可变参数

Java允许将同一个类中多个同名同功能但参数个数不同的方法,重载成一个方法,但是这样要不断的重复编写,对于这个问题,我们可以通过可变参数进行简化

可变参数的基本语法:

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

// 对传入的所有参数进行求和
// int...表示接收的是可变参数,类型为int类型,可以接收多个int类型的参数(0-n个)
public int sum(int... nums) {
    int res = 0;
    // 使用可变参数的时候,我们可以将其当作数值进行使用,即传入的nums可以当作数组
    for(int i = 0; i < nums.length; i++) {
        res += nums[i];
    }
    return res;
}

注意事项:

  • 可变参数的实参可以为0个或者任意多个

  • 可变参数的实参可以为数组

    java
    public class VarParameterDetail {
    	public static void main(String[] args) {
            int[] arr = {1, 2, 3};
            T t = new T();
            t.f1(arr);
    	}
    }
    
    class T {
        public void f1(int... nums) {
            System.out.println(nums.length);
        }
    }
  • 可变参数的本质就是数组

  • 可变参数可以和普通类型的参数一起放在形参列表中,但必须保证可变参数在最后

  • 一个形参列表中只能出现一个可变参数


克隆对象

克隆对象,要求新对象和原来对象是两个独立的对象,只是他们的属性和属性值完全相同

java
public class MethodParameter {
	public static void main(String[] args) {
        Person p = new Person();
        p.name = "jlc";
        p.age = 25;
        MyTools myTools = new MyTools();
        Person p2 = myTools.copyPerson(p);
        // p和p2都是Person对象,但是是两个独立的对象,其属性和属性值相同
	}
}

class Person {
    String name;
    int age;
}

class MyTools {
    public Person copyPerson(Person p) {
        // 创建一个新对象
        Person p2 = new Person();
        p2.name = p.name;   // 将原来对象的名字赋值给p2.name
        p2.age = p.age;
        return p2;
    }
}

作用域

在面向对象编程中,变量的作用域是非常重要的知识点

Java中,主要的变量就是属性(成员变量)和局部变量(在成员方法或者代码块中定义的变量)

  • 全局变量:也就是属性,作用域为整个类体,全局变量可以不赋值,直接使用,因为有默认值
  • 局部变量:除了属性之外的其他变量,作用域定义在它的代码块中,局部变量必须赋值后才能使用,因为它没有默认值
java
class T {
    // 定义全局变量,作用域在整个T类中,可以在这个类的任意地方使用
    // 全局变量可以不进行赋值
    int age = 25;
    
    public void show() {
        // 定义局部变量,只能在当前的作用域中进行使用
        // 局部变量必须要进行赋值
        int n = 10;
        System.out.println(age);  // 在方法中使用全局变量
    }
}

注意事项:

  • 属性和局部变量是可以重名的,访问时遵循就近原则
  • 在同一个作用域中,比如同一个成员方法中,两个局部变量是不能重名的
  • 属性(全局变量)生命周期较长,伴随着对象的创建而创建,伴随着对象的销毁而销毁;局部变量的生命周期较短,伴随着它的代码块的执行而创建,伴随着代码块的销毁而销毁,即在一次方法调用过程中,通过内存空间进行理解

全局变量/属性可以被本类使用,也可以被其他类使用(通过对象调用)

java
class T {
    // 属性
    int age = 25;
}

class D {
    // 在其他类中调用T类中的属性
    // 先定义一个T类的对象
    public void test() {
        T t = new T();
        System.out.println(t.age);
    }
}

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

全局变量/属性可以加修饰符;但是局部变量不能加修饰符


构造器

构造器也叫构造方法,主要作用是完成对新对象的初始化,我们可以通过构造器,在创建对象的时候,直接指定这个对象的具体属性值(即在创建对象时,系统会自动调用该类的构造器,完成对象的初始化)

基本语法:

java
修饰符 方法名(形参列表) {
    方法体;
}
  • 构造器的修饰符可以默认,也可以是publicprotectedprivate
  • 构造器没有返回值,也不能写void
  • 构造器的方法名和类的名字必须一样
  • 参数列表和成员方法一样的规则
  • 构造器的调用由系统完成,即在new的时候,系统会自动完成构造器中的方法
java
public class Constructor01 {
	public static void main(String[] args) {
		// 当我们new一个对象时,直接通过构造器指定属性值
        Person p = new Person("jlc", 25);
	}
}

class Person {
    String name;
    int age;
    // 构造器
    public Person(String pName, int pAge) {
        name = pName;
        age = pAge;
    }
}

注意事项:

  • 一个类可以定义多个不同的构造器,即构造器的重载,如我们可以再给Person类定义一个构造器,用来创建对象的时候,只指定姓名,不需要指定年龄,那么后续初始化对象的时候,年龄就是默认值
  • 构造器是完成对象的初始化,并不是创建对象
  • 如果程序员没有定义构造器,系统会自动给类生成一个默认无参构造器(也叫默认构造方法),如Person() {},可以通过反编译javap Person.class进行查看
  • 一旦定义了自己的构造器,默认的构造器就被覆盖了,就不能再使用默认的无参构造器,除非显示的定义一个,即:Person() {}

引入构造器后的对象创建流程

引入构造器后的对象创建流程是一个经典的面试题

image-20250329134442728

  1. 先在方法区中加载类信息
  2. 在堆中开辟空间
  3. 对属性进行默认初始化
  4. 根据是否赋值,进行显式的初始化(如age = 90;
  5. 通过构造器进行属性初始化,引用类型的属性值在常量池中,通过地址进行指向,基本类型的数据直接在堆空间中修改
  6. 最后将堆中的地址进行放回,供实例化的对象p进行指向(真正的对象在堆中,p只是对象的引用,只是一个对象名)

上述流程简而言之:

  1. 加载Person类信息(Person.class),只会加载一次
  2. 在堆中分配空间(地址)
  3. 完成对象的初始化
    1. 默认初始化 age = 0 name = null
    2. 显式初始化 age = 90
    3. 构造器的初始化 age = 20 name = “小倩”
  4. 对象在堆中的地址,返回给p(对象名,也可以理解为对象的引用)

this关键字

构造器的输入参数名不是非常友好,如果能将pName改成name就好了,但是由于变量作用域的原因,name的值最后是null,这时我们就要引入this关键字来解决

java
public class Constructor01 {
	public static void main(String[] args) {
		// 当我们new一个对象时,直接通过构造器指定属性值
        Person p = new Person("jlc", 25);
	}
}

class Person {
    String name;
    int age;
    // 构造器
    public Person(String name, int age) {
        // this.name表示当前对象的name属性   后面的name是局部变量,就近访问,找不到在访问属性
        this.name = name;   
        this.age = age;
    }
}

java虚拟机给每个对象分配了this,代表当前对象,比如,人是一个对象,这个人说我的,就表示this

image-20250329141242726

在堆中开辟空间后,除了属性,还有一个隐藏的属性this,引用的地址是对象的地址,指向的是对象自己

总之,哪个对象调用,this就代表哪个对象

注意事项:

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

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

  • 访问成员方法的语法:this,方法名(参数列表);

    java
    class T {
        public void f1() {
            System.out.println("f1方法");
        }
        public void f2() {
            System.out.println("f2方法");
            // 调用本类的f1()方法
            // 方式一:传统方法
            f1();
            // 方式二:使用this关键字
            this.f1();
        }
    }
  • 访问构造器语法:this(参数列表); 只能在构造器中使用(即只能在构造器中访问另一个构造器)

    java
    class T {
        public T() {
            // 通过this关键字访问另一个构造器
            this("jlc", 25);
            System.out.println("T()构造器");
        }
        public T(String name, int age) {
            System.out.println("T(String name, int age)构造器");
        }
    }

    注意:访问构造器语法:this(参数列表);,必须放在第一条语句中

  • this不能再类定义的外部使用,只能在类定义的方法中使用

    java
    class Person {
        String name = "jlc";
        int age = 25;
        public void f() {
            String name = "abc";
            System.out.println(name, age);   // abc, 25
            System.out.println(this.name, this.age);   // jlc, 25
        }
    }

    this.name表示当前对象的name属性 后面的name是局部变量,就近访问,找不到在访问属性

Released under the MIT License.