【转载】计算机程序的思惟逻辑 (13) - 类

html

上节咱们介绍了函数调用的基本原理,本节和接下来几节,咱们探索类的世界。编程

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

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

函数容器微信

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

Math函数函数

功能优化

int round(float a)this

四舍五入spa

double sqrt(double a)

平方根

double ceil(double a)

向上取整

double floor(double a)

向下取整

double pow(double a, double b)

a的b次方

int abs(int a)

绝对值

int max(int a, int b)

最大值

double log(double a)

天然对数

double random()

产生一个大于等于0小于1的随机数


使用这些函数,直接在前面加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里面包含不少与数组操做相关的函数,下表列出了其中一些:

Arrays函数

功能

void sort(int[] a)

排序,按升序排,整数数组

void sort(double[] a)

排序,按升序排,浮点数数组

int binarySearch(long[] a, long key) 

二分查找,数组已按升序排列

void fill(int[] a, int val)

给全部数组元素赋相同的值

int[] copyOf(int[] original, int newLength)

数组拷贝

boolean equals(char[] a, char[] a2)

判断两个数组是否相同

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

自定义数据类型

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

一个数据类型由其包含的属性以及该类型能够进行的操做组成,属性又能够分为是类型自己具备的属性,仍是一个具体数据具备的属性,一样,操做也能够分为是类型自己能够进行的操做,仍是一个具体数据能够进行的操做。

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

  • 类型自己具备的属性,经过类变量体现  
  • 类型自己能够进行的操做,经过类方法体现
  • 类型实例具备的属性,经过实例变量体现
  • 类型实例能够进行的操做,经过实例方法体现

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

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

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

类变量

类型自己具备的属性经过类变量体现,常常用于表示一个类型中的常量,好比Math类,定义了两个数学中经常使用的常量,以下所示:

public static final double E = 2.7182818284590452354;
public static final double PI = 3.14159265358979323846;

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就是实例。

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

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

复制代码
class Point {
    public int x;
    public int y;
    
    public double distance(){
        return Math.sqrt(x*x+y*y);
    }
}
复制代码

咱们来解释一下:

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,这是实例方法和类方法的最大区别。实例方法直接访问实例变量,究竟是什么意思呢?其实,在实例方法中,有一个隐含的参数,这个参数就是当前操做的实例本身,直接操做实例变量,实际也须要经过参数进行。实例方法和类方法更多的区别以下所示:

  • 类方法只能访问类变量,但不能访问实例变量,能够调用其余的类方法,但不能调用实例方法。
  • 实例方法既能访问实例变量,也能够访问类变量,既能够调用实例方法,也能够调用类方法。

关于实例方法和类方法更多的细节,后续会进一步介绍。

使用第一个类

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

复制代码
public static void main(String[] args) {
    Point p = new Point();
    p.x = 2;
    p.y = 3;
    System.out.println(p.distance());
}
复制代码

咱们解释一下:

Point p = new Point();

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

1 Point p;
2 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,而只应该经过对象的方法对实例变量进行操做,缘由也是为了减小误操做,直接访问变量没有办法进行参数检查和控制,而经过方法修改,能够在方法中进行检查。 

修改变量默认值

以前咱们说,实例变量都有一个默认值,若是但愿修改这个默认值,能够在定义变量的同时就赋值,或者将代码放入初始化代码块中,代码块用{}包围,以下面代码所示:

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

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

静态变量也能够这样初始化:

复制代码
static int STATIC_ONE = 1;
static int STATIC_TWO;
static
{
    STATIC_TWO = 2;    
}
复制代码

STATIC_TWO=2;语句外面包了一个 static {},这叫静态初始化代码块。静态初始化代码块在类加载的时候执行,这是在任何对象建立以前,且只执行一次。

修改类 - 实例变量改成private

上面咱们说通常不该该将实例变量声明为public,下面咱们修改一下类的定义,将实例变量定义为private,经过实例方法来操做变量,代码以下:

复制代码
class Point {
    private int x;
    private int y;

    public void setX(int x) {
        this.x = x;
    }
    
    public void setY(int y) {
        this.y = y;
    }
    
    public int getX() {
        return x;
    }
    
    public int getY() {
        return y;
    }
    
    public double distance() {
        return Math.sqrt(x * x + y * y);
    }
}
复制代码

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

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

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

使用这个类的代码以下:

复制代码
public static void main(String[] args) {
    Point p = new Point();
    p.setX(2);
    p.setY(3);
    System.out.println(p.distance());
}
复制代码

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

修改类 - 引入构造方法

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

复制代码
public Point(){
    this(0,0);
}

public Point(int x, int y){
    this.x = x;
    this.y = y;
}
复制代码

这两个就是构造方法,构造方法能够有多个。不一样于通常方法,构造方法有一些特殊的地方:

  • 名称是固定的,与类名相同。这也容易理解,靠这个用户和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类,其属性只有基本数据类型,下节咱们介绍类的组合,以表达更为复杂的概念。

相关文章
相关标签/搜索