【Java面向对象】5-1 类与对象

§5-1 类与对象

面向对象的重要概念和工具就是类。本节将介绍类和对象的关系、创建类和对象(构造器),以及 OOP 中的重要概念:封装。

5-1.1 初识面向对象

面向过程(procedure-oriented programming,POP)是一种以过程为中心的编程思想,主要考虑程序应该做什么以及怎么做。而面向对象(object-oriented programming, OOP)是一种涉及分类思维模式的软件开发方法,主要考虑解决问题应当需要哪些分类,并对这些分类单独思考。

面向过程适合去解决一些规模较小的简单问题,但对于大型项目和多人协作这些复杂问题的角度而言,面向对象更为合适。

对于描述复杂的事物,为了从宏观上去把握,从整体上合理分析,我们需要使用面向对象的思路来分析整个系统。因此,面向对象编程常常需要开发者有较强的抽象思维能力。但是,到了具体的微观层面,对于如何操作的问题,此时则需要面向过程的编程方法。

可见,OOP和POP是不可分割的,常常在实践中混合使用。

面向对象编程的特点

面向对象编程的本质是:以类的形式组织代码,以对象的组织封装数据

面向对象的主要特点就是抽象(abstract),具有三大特性:封装(encapsulation)、继承(inheritance)和多态(polymorphism)。

面向对象编程常用的工具就是(class)。简而言之,类 = 属性 + 方法。类是对一类事物的抽象,是对象的模板,而对象是具体的事物,是类的实例。从代码运行的角度来看,是先有类再有对象,而从认识论的角度而言,是先有对象再有类。

5-1.2 类与对象的关系

前文提到,类是一种抽象的数据类型,是对具有同样特征的一类事物的抽象、整体描述和定义,不能代表某一个具体的事物。类常用于描述和定义某一类事物所具有的特征、属性和行为。例如:车辆、人类、宠物等。

而对象是类中某个具体的事物,是一类事物的一个具象化例子(具体实例)。例如,猫、狗、鹦鹉等都属于宠物,它们都是宠物这一类事物的某个具体实例。

这种思想可以通过代码实现。

5-1.3 编写类和创建对象

-- 编写类

注意:在一个 Java 项目中,应只有一个类含有 main 方法,用于调用其它类以实现某种(些)功能。

从代码角度而言,要想创建对象,首先得要有其对应的类。类是一类具有相同特征的事物的抽象,类中可以定义属性(数据)和行为(方法)。

我们以学生为例子,创建一个类:

public class Student {
    //属性(数据)
    String name;    //null
    int age;        //0
    boolean gender; //T: male; F: female; defaults to female

    //行为(方法)
    public void get() {
        System.out.println("Name: " + this.name);
        System.out.println("Age: " + this.age);
        System.out.println("Gender: " + (this.gender ? "Male" : "Female"));
    }

    public void study() {
        System.out.println(this.name + " is currently studying.");
    }
}

注意:类是一种模板,在类中写定义属性(数据)时不能够为变量赋值。

-- 实例化

定义好类后,类通过实例化就会返回一个自己的对象。在同一包中,使用 new 关键字实例化,我们就可以在 main 函数中调用:

public class Application {
    public static void main(String[] args) {
        Student studentA = new Student();

        studentA.get();
        System.out.println("================");

        studentA.name = "Zebt";
        studentA.age = 17;
        studentA.get();
        studentA.study();
    }
}

可以看到,这里我们使用了关键字 thisthis 用于在任何实例方法中指向当前的对象

注意:若二者位于同一包中,则实例化时可以直接通过 new 关键字实现。但若在其他包中调用,由于访问权限的关系,还需要在主类前加上 import 语句。同时还应当在类中保留一部分 public 访问权限的变量用作与外部沟通的接口。这将在[封装](#5-1.6 封装)中有讲解。

至此,编写类和创建对象的工作就完成了。

5-1.4 内存分析

部分内容参考自:java面向对象-创建对象内存分析(jvm)_蚂蚁牙黑147的博客-CSDN博客

在创建对象的过程中,变量和对象在内存空间中的情况是如何的呢?

首先得先明确三个概念:方法区、堆、栈。

方法区:由于类是最先被加载的,方法区最先有数据,用于存储类信息class 对象静态变量static)、字符串常量等,被所有线程共享。JVM 只有一个方法区。

堆用于存储创建好的对象和数组(数组也是对象)、成员变量(实例变量)。可见,凡是通过 new 运算符创建出来的对象,都存储在堆中,new 的作用就是在堆内存中开辟一块空间。方法区实际也是堆,堆被所有线程共享。一个 JVM 只有一个堆。

栈用于存放该线程执行方法的信息(实际参数、局部变量等)。在栈中存储基本数据类型、对象的引用,方法调用时进栈,方法执行完后出栈。创建对象所需要的内存是由栈来分配的。栈为线程私有的,不能实现线程之间的共享。

我们以 5-1.2 中的类为例。

当运行程序时,static 最先被加载Student 类和 Application 类就会出现在方法区。

运行到 main() 方法时,由栈来运行。在 main 方法中实例化了 Student 对象,栈就会为 Student 对象在堆中分配内存空间,并保存该对象的地址,通过一个栈中的引用变量来调用该对象。以下图为例,0x00010x0002 就是这两个对象的所在堆内存地址,通过引用变量 studentA, studentB 来引用对象。

image

堆内存就会开辟一块内存空间存储栈中所分配的对象,并且堆内存会给每个对象分配一个内存地址。new 运算符的作用就是在堆中开辟内存空间。

Java 中的数据类型有两大类:引用类型和基本类型。对象是类的实例,属于引用类型,通过引用来操作,即栈中一个引用变量名(同指针)来操作。

对于成员变量(实例变量),无论是什么数据类型,都是存储在堆中。而局部变量的基本类型存储在栈中,为线程所私有,生命周期和作用域都很短,为提高效率,没有必要放在堆中。

5-1.5 构造器

以 5-1.2 中的类为例,创建对象时,即使没有为对象中的数据赋值(初始化),使用 get() 函数获取对象信息时,对象中的数据已经被初始化。

使用 new 实例化对象时,需要为对象分配内存空间,同时给创建好的对象进行默认的初始化。另外,其本质上是在调用构造器(或构造方法)。

特点

  1. 必须与类的名字相同;
  2. 必须不能有返回值类型,也不能写 void
  3. 当类中缺省构造器时,编译器会自动添加一个空的无参构造器;

构造器在类中十分重要,是必须要掌握的。

语法:无参构造

public 类名() {
    ...
}

语法:有参构造

public 类名(形参列表) {
    ...
}

IDEA 快捷键:按下快捷键 alt + insert,在弹出菜单中选择 构造器(Constructor) 后,选择参数后单击 确定(OK) 生成有参构造器,选择 无选择(Select None) 生成无参构造器;

注意

  • 由于构造器是在对象实例化时必须调用的,因此其访问修饰符必须为 public

  • 若已经显式定义了有参构造,则必须显式定义无参构造(原因见 [继承 - super 关键字](5 - 2 继承.md) );

  • 除了可以在构造器中为成员变量赋初始值,也可以在构造器外,类中的成员变量直接赋值,例:

    public class Student {
        String name = "Default Name";
        int age = 0;
        boolean gender = false;
    }
    

    值得注意的是,在上述例子中,若需要定义一个有参构造器,为避免初始值冗余,还需显式定义一个无参构造器

  • 除了无参有参构造器、构造器重载,还存在一种构造器:复制构造器。但使用较少,一般考虑使用 [clone() 方法](5 - 2 继承.md)。

结合 5-1.3 的内存分析来看,创建对象时构造器是必须被调用的。若用户已经显式定义了一个构造器,那么实例化时构造器就会在堆中的方法区中被调用。以这个例子来看,对象就有了初始化后的值。

5-1.6 封装

程序设计追求 “高内聚低耦合”。高内聚即类的内部数据操作细节由自己完成,不允许外部干涉(也无需使用者掌握);低耦合指只暴露少量的方法给外部使用。简而言之,就是该露的露,该藏的藏。一般来说,选择隐藏数据,暴露方法。

通常来说,应当禁止访问一个对象中的数据实际表示,而应当使用接口来访问,这称为信息隐藏。这种隐藏对象的属性和实现细节,只对外公开接口的过程称为封装。

使用关键字 private 用于限制成员的访问权限,使得成员只能够被当前类访问

若缺省,默认default,使得成员只能够在同一类中和同一包中(子类与无关类)访问。

关键字 public 可以使得成员能够被所有类访问

在[继承](5 - 2 继承.md)中,将会介绍 protected 访问修饰符。

不妨来重写 5-1.2 的类,以实现封装:

public class Student {
    private String name;
    private int age;
    private boolean gender;
    
    public Student(String name, int age, boolean gender) {
        this.name = name;
        this.gender = gender;
        
        if (age < 0 || age >= 120) {
            this.age = 0;
        } else {
            this.age = age;
        }
    }		//构造器
    
    public String getName() {
        return this.name;
    }		//获取姓名
    
    public int getAge() {
        return this.age;
	}		//获取年龄
    
    public boolean getGender() {
        return this.gender;
    }		//获取性别
    
    public void setName(String name) {
        this.name = name;
    }		//设置姓名
    
    public void setAge(int age) {
        if (age < 0 || age >= 120) {
            this.age = 0;
        } else {
            this.age = age;
        }
    }		//判断数据是否合法,设置年龄
    
    public void setGender(boolean gender) {
        this.gender = gender;
    }		//设置性别
}

这样子,就实现了信息隐藏,也保留了一个可供外部访问的接口用于读取 / 修改信息。同时也注意到,通过封装,我们可以通过方法来判定数据是否合法,从而作进一步的操作。

setget 方法建议如上构造,构造器可以通过方法重载实现多种构造方法。至此,封装完成。在 IDEA 中,可以通过按下 alt + insert 在弹出菜单中选择 Getter and Setter 从而实现快速构造 set get 方法。

使用封装的优点

  1. 提高程序的安全性,保护数据;
  2. 隐藏代码的实现细节;
  3. 统一接口;
  4. 提高系统可维护性;

5-1.7 总结:标准 JavaBean

总而言之,标准的 JavaBean 类(咖啡豆):

  1. 类名要见名知意;
  2. 成员变量使用 private 修饰;
  3. 至少提供两个构造方法:
    • 无参构造方法;
    • 带全部参数的构造方法;
  4. 成员方法:
    • 提供每一个成员变量的对应的 Getter & Setter
    • 若还有其他行为,也必须要写上。

在 IDEA 中,可安装 PTG 插件快速一键生成标准 JavaBean

posted @ 2023-02-26 17:12  Zebt  阅读(89)  评论(0)    收藏  举报