计算机程序的思维逻辑 (13) - 类

时间:2022-04-25
本文章向大家介绍计算机程序的思维逻辑 (13) - 类,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

程序主要就是数据以及对数据的操作,为方便理解和操作,高级语言使用数据类型这个概念,不同的数据类型有不同的特征和操作,Java定义了八种基本数据类型,其中,四种整形byte/short/int/long,两种浮点类型float/double,一种真假类型boolean,一种字符类型char,其他类型的数据都用类这个概念表达。

前两节我们暂时将类看做函数的容器,在某些情况下,类也确实基本上只是函数的容器,但类更多表示的是自定义数据类型,我们先从容器的角度,然后从自定义数据类型的角度谈谈类。

函数容器

我们看个例子,Java API中的类Math,它里面主要就包含了若干数学函数,下表列出了其中一些:

使用这些函数,直接在前面加Math.即可,例如Math.abs(-1)返回1。

这些函数都有相同的修饰符,public static。

static表示类方法,也叫静态方法,与类方法相对的是实例方法。实例方法没有static修饰符,必须通过实例或者叫对象(待会介绍)调用,而类方法可以直接通过类名进行调用的,不需要创建实例。

public表示这些函数是公开的,可以在任何地方被外部调用。与public相对的有private,如果是private,表示私有,这个函数只能在同一个类内被别的函数调用,而不能被外部的类调用。在Math类中,有一个函数 Random initRNG()就是private的,这个函数被public的方法random()调用以生成随机数,但不能在Math类以外的地方被调用。

将函数声明为private可以避免该函数被外部类误用,调用者可以清楚的知道哪些函数是可以调用的,哪些是不可以调用的。类实现者通过private函数封装和隐藏内部实现细节,而调用者只需要关心public的就可以了。可以说,通过private封装和隐藏内部实现细节,避免被误操作,是计算机程序的一种基本思维方式。

除了Math类,我们再来看一个例子Arrays,Arrays里面包含很多与数组操作相关的函数,下表列出了其中一些:

这里将类看做函数的容器,更多的是从语言实现的角度看,从概念的角度看,Math和Arrays也可以看做是自定义数据类型,分别表示数学和数组类型,其中的public static函数可以看做是类型能进行的操作。接下来让我们更为详细的讨论自定义数据类型。

自定义数据类型

我们将类看做自定义数据类型,所谓自定义数据类型就是除了八种基本类型以外的其他类型,用于表示和处理基本类型以外的其他数据。

一个数据类型由其包含的属性以及该类型可以进行的操作组成,属性又可以分为是类型本身具有的属性,还是一个具体数据具有的属性,同样,操作也可以分为是类型本身可以进行的操作,还是一个具体数据可以进行的操作。

这样,一个数据类型就主要由四部分组成:

  • 类型本身具有的属性,通过类变量体现
  • 类型本身可以进行的操作,通过类方法体现
  • 类型实例具有的属性,通过实例变量体现
  • 类型实例可以进行的操作,通过实例方法体现

不过,对于一个具体类型,每一个部分不一定都有,Arrays类就只有类方法。

类变量和实例变量都叫成员变量,也就是类的成员,类变量也叫静态变量或静态成员变量。类方法和实例方法都叫成员方法,也都是类的成员,类方法也叫静态方法。

类方法我们上面已经看过了,Math和Arrays类中定义的方法就是类方法,这些方法的修饰符必须有static。下面解释下类变量,实例变量和实例方法。

类变量

类型本身具有的属性通过类变量体现,经常用于表示一个类型中的常量,比如Math类,定义了两个数学中常用的常量,如下所示:

E表示数学中自然对数的底数,自然对数在很多学科中有重要的意义,PI表示数学中的圆周率π。与类方法一样,类变量可以直接通过类名访问,如Math.PI。

这两个变量的修饰符也都有public static,public表示外部可以访问,static表示是类变量。与public相对的主要也是private,表示变量只能在类内被访问。与static相对的是实例变量,没有static修饰符。

这里多了一个修饰符final,final在修饰变量的时候表示常量,即变量赋值后就不能再修改了。使用final可以避免误操作,比如说,如果有人不小心将Math.PI的值改了,那么很多相关的计算就会出错。另外,Java编译器可以对final变量进行一些特别的优化。所以,如果数据赋值后就不应该再变了,就加final修饰符吧。

表示类变量的时候,static修饰符是必需的,但public和final都不是必需的。

实例变量和实例方法

实例字面意思就是一个实际的例子,实例变量表示具体的实例所具有的属性,实例方法表示具体的实例可以进行的操作。如果将微信订阅号看做一个类型,那"老马说编程"订阅号就是一个实例,订阅号的头像、功能介绍、发布的文章可以看做实例变量,而修改头像、修改功能介绍、发布新文章可以看做实例方法。与基本类型对比,int a;这个语句,int就是类型,而a就是实例。

接下来,我们通过定义和使用类,来进一步理解自定义数据类型。

定义第一个类 我们定义一个简单的类,表示在平面坐标轴中的一个点,代码如下:

我们来解释一下:

public class Point

表示类型的名字是Point,是可以被外部公开访问的。这个public修饰似乎是多余的,不能被外部访问还能有什么用?在这里,确实不能用private修饰Point。但修饰符可以没有(即留空),表示一种包级别的可见性,我们后续章节介绍,另外,类可以定义在一个类的内部,这时可以使用private修饰符,我们也在后续章节介绍。

public int x;
public int y;

定义了两个实例变量,x和y,分别表示x坐标和y坐标,与类变量类似,修饰符也有public或private修饰符,表示含义类似,public表示可被外部访问,而private表示私有,不能直接被外部访问,实例变量不能有static修饰符。

public double distance(){
    return Math.sqrt(x*x+y*y);
}

定义了实例方法distance,表示该点到坐标原点的距离。该方法可以直接访问实例变量x和y,这是实例方法和类方法的最大区别,实例方法和类方法都可以有参数、操作、返回值,但类方法只能操作参数和方法内定义的变量,而实例方法可以直接操作实例变量,可以读也可以改。

实例方法直接访问实例变量,到底是什么意思呢?其实,在实例方法中,有一个隐含的参数,这个参数就是当前操作的实例自己,直接操作实例变量,实际也需要通过参数进行。后续我们还会进一步解释。

使用第一个类

定义了类本身和定义了一个函数类似,本身不会做什么事情,不会分配内存,也不会执行代码。方法要执行需要被调用,而实例方法被调用,首先需要一个实例,实例也称为对象,我们可能会交替使用。下面的代码演示了如何使用:

我们解释一下:

Point p = new Point();

这个语句包含了Point类型的变量声明和赋值,它可以分为两部分:

Point p; 

p = new Point();

Point p声明了一个变量,这个变量叫p,是Point类型的。这个变量和数组变量是类似的,都有两块内存,一块存放实际内容,一块存放实际内容的位置。声明变量本身只会分配存放位置的内存空间,这块空间还没有指向任何实际内容。因为这种变量和数组变量本身不存储数据,而只是存储实际内容的位置,它们也都称为引用类型的变量。

p = new Point();创建了一个实例或对象,然后赋值给了Point类型的变量p,它至少做了两件事:

  1. 分配内存,以存储新对象的数据,对象数据包括这个对象的属性,具体包括其实例变量x和y。
  2. 给实例变量设置默认值,int类型默认值为0。

与方法内定义的局部变量不同,在创建对象的时候,所有的实例变量都会分配一个默认值,这与在创建数组的时候是类似的,数值类型变量的默认值是0,boolean是false, char是'u0000',引用类型变量都是null,null是一个特殊的值,表示不指向任何对象。这些默认值可以修改,我们待会介绍。

p.x = 2;
p.y = 3;

给对象的变量赋值,语法形式是:对象变量名.成员名。

System.out.println(p.distance());

调用实例方法distance,并输出结果,语法形式是:对象变量名.方法名。实例方法内对实例变量的操作,实际操作的就是p这个对象的数据。

我们在介绍基本类型的时候,是先定义数据,然后赋值,最后是操作,自定义类型与此类似:

  • Point p = new Point(); 是定义数据并设置默认值
  • p.x = 2; p.y = 3; 是赋值
  • p.distance() 是数据的操作

可以看出,对实例变量和实例方法的访问都通过对象进行,通过对象来访问和操作其内部的数据是一种基本的面向对象思维。本例中,我们通过对象直接操作了其内部数据x和y,这是一个不好的习惯,一般而言,不应该将实例变量声明为public,而只应该通过对象的方法对实例变量进行操作,原因也是为了减少误操作,直接访问变量没有办法进行参数检查和控制,而通过方法修改,可以在方法中进行检查。

修改变量默认值

之前我们说,实例变量都有一个默认值,如果希望修改这个默认值,可以在定义变量的同时就赋值,或者将代码放入初始化代码块中,代码块用{}包围,如下面代码所示:

private int x = 1;
private int y;
{
    y = 2;
}

x的默认值设为了1,y的默认值设为了2。在新建一个对象的时候,会先调用这个初始化,然后才会执行构造方法中的代码。

静态变量也可以这样初始化:

static private int STATIC_ONE = 1;
static private int STATIC_TWO;

{
    STATIC_TWO = 2;    
}

修改类 - 实例变量改为private

上面我们说一般不应该将实例变量声明为public,下面我们修改一下类的定义,将实例变量定义为private,通过实例方法来操作变量,代码如下:

这个定义中,我们加了四个方法,setX/setY用于设置实例变量的值,getX/getY用于获取实例变量的值。

这里面需要介绍的是this这个关键字,this表示当前实例,在语句this.x=x;中,this.x表示实例变量x,而右边的x表示方法参数中的x。前面我们提到,在实例方法中,有一个隐含的参数,这个参数就是this,没有歧义的情况下,可以直接访问实例变量,在这个例子中,两个变量名都叫x,则需要通过加上this来消除歧义。

这四个方法看上去是非常多余的,直接访问变量不是更简洁吗?而且上节我们也说过,函数调用是有成本的。在这个例子中,意义确实不太大,实际上,Java编译器一般也会将对这几个方法的调用转换为直接访问实例变量,而避免函数调用的开销。但在很多情况下,通过函数调用可以封装内部数据,避免误操作,我们一般还是不将成员变量定义为public。

使用这个类的代码如下:

将对实例变量的直接访问改为了方法调用。

修改类 - 引入构造方法

在初始化对象的时候,前面我们都是直接对每个变量赋值,有一个更简单的方式对实例变量赋初值,就是构造方法,我们先看下代码,在Point类定义中增加如下代码:

这两个就是构造方法,构造方法可以有多个。不同于一般方法,构造方法有一些特殊的地方:

  • 名称是固定的,与类名相同。这也容易理解,靠这个用户和Java系统就都能容易的知道哪些是构造方法。
  • 没有返回值,也不能有返回值。这个规定大概是因为返回值没用吧。

与普通方法一样,构造方法也可以重载。第二个构造方法是比较容易理解的,使用this对实例变量赋值。

我们解释下第一个构造方法,this(0,0)的意思是调用第二个构造方法,并传递参数0,0,我们前面解释说this表示当前实例,可以通过this访问实例变量,这是this的第二个用法,用于在构造方法中调用其他构造方法。

这个this调用必须放在第一行,这个规定应该也是为了避免误操作,构造方法是用于初始化对象的,如果要调用别的构造方法,先调别的,然后根据情况自己再做调整,而如果自己先初始化了一部分,再调别的,自己的修改可能就被覆盖了。

这个例子中,不带参数的构造方法通过this(0,0)又调用了第二个构造方法,这个调用是多余的,因为x和y的默认值就是0,不需要再单独赋值,我们这里主要是演示其语法。

我们来看下如何使用构造方法,代码如下:

Point p = new Point(2,3);

这个调用就可以将实例变量x和y的值设为2和3。前面我们介绍 new Point()的时候说,它至少做了两件事,一个是分配内存,另一个是给实例变量设置默认值,这里我们需要加上一件事,就是调用构造方法。调用构造方法是new操作的一部分。

通过构造方法,可以更为简洁的对实例变量进行赋值。

默认构造方法

每个类都至少要有一个构造方法,在通过new创建对象的过程中会被调用。但构造方法如果没什么操作要做,可以省略。Java编译器会自动生成一个默认构造方法,也没有具体操作。但一旦定义了构造方法,Java就不会再自动生成默认的,具体什么意思呢?在这个例子中,如果我们只定义了第二个构造方法(带参数的),则下面语句:

Point p = new Point();

就会报错,因为找不到不带参数的构造方法。

为什么Java有时候帮助自动生成,有时候不生成呢?你在没有定义任何构造方法的时候,Java认为你不需要,所以就生成一个空的以被new过程调用,你定义了构造方法的时候,Java认为你知道自己在干什么,认为你是有意不想要不带参数的构造方法的,所以不会帮你生成。

私有构造方法

构造方法可以是私有方法,即修饰符可以为private, 为什么需要私有构造方法呢?大概可能有这么几种场景:

  • 不能创建类的实例,类只能被静态访问,如Math和Arrays类,它们的构造方法就是私有的。
  • 能创建类的实例,但只能被类的的静态方法调用。有一种常用的场景,即类的对象有但是只能有一个,即单例模式(后续文章介绍),在这个场景中,对象是通过静态方法获取的,而静态方法调用私有构造方法创建一个对象,如果对象已经创建过了,就重用这个对象。
  • 只是用来被其他多个构造方法调用,用于减少重复代码。

关键字小结

本节我们提到了多个关键字,这里汇总一下:

  • public: 可以修饰类、类方法、类变量、实例变量、实例方法、构造方法,表示可被外部访问。
  • private: 可以修饰类、类方法、类变量、实例变量、实例方法、构造方法,表示不可以被外部访问,只能在类内被使用。
  • static: 修饰类变量和类方法,它也可以修饰内部类(后续章节介绍)。
  • this:表示当前实例,可以用于调用其他构造方法,访问实例变量,访问实例方法。
  • final: 修饰类变量、实例变量,表示只能被赋值一次,final也可以修饰实例方法(后续章节介绍)。

类和对象的生命周期

在程序运行的时候,当第一次通过new创建一个类的对象的时候,或者直接通过类名访问类变量和类方法的时候,Java会将类加载进内存,为这个类型分配一块空间,这个空间会包括类的定义,它有哪些变量,哪些方法等,同时还有类的静态变量,并对静态变量赋初始值。

类加载进内存后,一般不会释放,直到程序结束。一般情况下,类只会加载一次,所以静态变量在内存中只有一份。

对象

当通过new创建一个对象的时候,对象产生,在内存中,会存储这个对象的实例变量值,每new一次,对象就会产生一个,就会有一份独立的实例变量。

每个对象除了保存实例变量的值外,可以理解还保存着对应类型即类的地址,这样,通过对象能知道它的类,访问到类的变量和方法代码。

实例方法可以理解为一个静态方法,只是多了一个参数this,通过对象调用方法,可以理解为就是调用这个静态方法,并将对象作为参数传给this。

对象的释放是被Java用垃圾回收机制管理的,大部分情况下,我们不用太操心,当对象不再被使用的时候会被自动释放。

具体来说,对象和数组一样,有两块内存,保存地址的部分分配在栈中,而保存实际内容的部分分配在堆中。栈中的内存是自动管理的,函数调用入栈就会分配,而出栈就会释放。

堆中的内存是被垃圾回收机制管理的,当没有活跃变量指向对象的时候,对应的堆空间就可能被释放,具体释放时间是Java虚拟机自己决定的。活跃变量,具体的说,就是已加载的类的类变量,和栈中所有的变量。

小结

本节我们主要从自定义数据类型的角度介绍了类,谈了如何定义类,以及如何创建对象,如何使用类。自定义类型由类变量、类方法、实例变量和实例方法组成,为方便对实例变量赋值,介绍了构造方法。本节引入了多个关键字,我们介绍了这些关键字的含义。最后我们介绍了类和对象的生命周期。

通过类实现自定义数据类型,封装该类型的数据所具有的属性和操作,隐藏实现细节,从而在更高的层次上(类和对象的层次,而非基本数据类型和函数的层次)考虑和操作数据,是计算机程序解决复杂问题的一种重要的思维方式。

本节介绍的Point类,其属性只有基本数据类型,下节我们介绍如何通过类的组合以表达更为复杂的概念。