文章目录
- 类与对象
- 1.1 自定义类
- 1.2 第一个类
- 1.3 private变量
- 1.4 变量默认值
- 1.5 构造方法
- 1.6 类和对象的生命周期
类与对象
本文为书籍《Java编程的逻辑》1和《剑指Java:核心原理与应用实践》2阅读笔记
将客观世界中存在的一切可以描述的事物称为对象(实体),即“万物皆对象”。例如,楼底下卖烧饼的大姐、楼下KFC
服务员、楼下停的共享单车,桌子上的水杯,中午的外卖订单等,都可以称为对象。这些众多的对象有些是有相同的属性和功能(或行为)的,按照属性和功能(或行为)等对它们进行分类、抽象,就形成了概念世界中的类。类实际上就是根据生活经验总结抽取出来的概念产物,相当于一组对象的模型。类和对象关系如下图所示。
如上图,类和对象我们可以理解为类等于抽象概念的人,对象等于实实在在的某个人。类是抽象的,对象是具体的,类相当于创建对象的蓝图。例如汽车,类与对象的关系就如汽车设计图与汽车实物的关系。面向对象的程序实现需要通过类创建对应的实例化对象,来对应客观世界中的实体。
1.1 自定义类
一个类由其包含的属性以及该类可以进行的操作组成,属性又可以分为是类本身具有的属性,还是一个具体实例具有的属性,同样,操作也可以分为是类型本身可以进行的操作,还是一个具体实例可以进行的操作。这样,一个类就主要由 4 4 4部分组成:
- 类本身具有的属性,通过类变量体现。
- 类本身可以进行的操作,通过类方法体现。
- 类实例具有的属性,通过实例变量体现。
- 类型实例可以进行的操作,通过实例方法体现。
类变量和实例变量都叫成员变量,也就是类的成员,类变量也叫静态变量或静态成员变量。类方法和实例方法都叫成员方法,也都是类的成员,类方法也叫静态方法。
1、类变量类方法
类型本身具有的属性通过类变量体现,经常用于表示一个类型中的常量。比如Math
类,定义了两个数学中常用的常量,如下所示:
public static final double E = 2.718281828459045;
public static final double PI = 3.141592653589793;
E
E
E表示数学中自然对数的底数,自然对数在很多学科中有重要的意义;
P
I
PI
PI表示数学中的圆周率
π
\pi
π。要使用类变量,可以直接通过类名访问,如Math.PI
。这两个变量的修饰符也都有public static
,public
表示外部可以访问,static
表示是类变量。与public
相对的是private
,表示变量只能在类内被访问。与static
相对的是实例变量,没有static
修饰符。这里多了一个修饰符final
, final
在修饰变量的时候表示常量,即变量赋值后就不能再修改了。使用final
可以避免误操作,比如,如果有人不小心将Math.PI
的值改了,那么很多相关的计算就会出错。另外,Java
编译器可以对final
变量进行一些特别的优化。所以,如果数据赋值后就不应该再变了,就加final
修饰符。表示类变量的时候,static
修饰符是必需的,但public
和final
都不是必需的。
2、类方法
类型本身的方法通过类方法体现,经常用于表示一个类型中的方法。比如Math
中定义的一些数学函数,要使用这些函数,直接在前面加Math.
即可,例如Math.abs(-1)
返回
1
1
1。这些函数都有相同的修饰符:public static
。static
表示类方法,也叫静态方法,与类方法相对的是实例方法。实例方法没有static
修饰符,必须通过实例或者对象调用,而类方法可以直接通过类名进行调用,不需要创建实例。public
表示这些函数是公开的,可以在任何地方被外部调用。与public
相对的是private
。如果是private
,则表示私有,这个函数只能在同一个类内被别的函数调用,而不能被外部的类调用。在Math
类中,有一个函数Random initRNG
就是private
的,这个函数被public
的方法random
调用以生成随机数,但不能在Math
类以外的地方被调用。将函数声明为private
可以避免该函数被外部类误用,调用者可以清楚地知道哪些函数是可以调用的,哪些是不可以调用的。类实现者通过private
函数封装和隐藏内部实现细节,而调用者只需要关心public
就可以了。可以说,通过private
封装和隐藏内部实现细节,避免被误操作,是计算机程序的一种基本思维方式。
3、实例变量和实例方法
实例变量表示具体的实例所具有的属性,实例方法表示具体的实例可以进行的操作。如果将微信订阅号看作一个类型,那“人民日报”订阅号就是一个实例,订阅号的头像、功能介绍、发布的文章可以看作实例变量,而修改头像、修改功能介绍、发布新文章可以看作实例方法。实例变量和实例方法是每个实例独有的,虽然可能和其它类似的实例一样,但是不是同一个东西。比如,一个人,都有头发,但是每个人的头发都是不一样的。
实例方法和类方法的主要区别如下:
- 类方法只能访问类变量,不能访问实例变量,可以调用其他的类方法,不能调用实例方法。
- 实例方法既能访问实例变量,也能访问类变量,既可以调用实例方法,也可以调用类方法。
1.2 第一个类
我们定义一个简单的类来表示直角坐标系中的一个点,代码如下:
package com.ieening;
public class ClassTest {
public static void main(String[] args) {
Point point; // 注释4
point = new Point(); // 注释5
System.out.println("Point:x=" + point.x + ",y=" + point.y); // 注释9
point.x = 3; // 注释6
point.y = 4; // 注释6
System.out.println("Point:x=" + point.x + ",y=" + point.y); // 注释7
System.out.println(point.distance()); // 注释8
}
}
/**
* Point
*/
class Point { // 注释1
public int x; // 注释2
public int y; // 注释2
public double distance() { // 注释3
return Math.sqrt(x * x + y * y);
}
}
- 注释1:定义了名为
Point
的类(class Point
)。 - 注释2:类中定义了两个两个实例变量(
public int x; public int y;
)表示。 - 注释3:类中定义一个实例方法(
public double distance
)。 - 注释4:
Point p
声明了一个变量,这个变量叫p
,是Point
类型的,这个变量和数组变量是类似的,都有两块内存:一块存放实际内容,一块存放实际内容的位置。声明变量本身只会分配存放位置的内存空间,这块空间还没有指向任何实际内容,像这种只保存实际内容位置的变量,也叫做引用类型的变量。 - 注释5:创建了一个实例或对象,然后赋值给了
Point
类型的变量p
,它至少做了两件事:- 分配内存,以存储新对象的数据,对象数据包括这个对象的属性,具体包括其实例变量
x
和y
。 - 给实例变量设置默认值,
int
类型默认值为 0 0 0。
- 分配内存,以存储新对象的数据,对象数据包括这个对象的属性,具体包括其实例变量
- 注释6:给
public
属性赋值; - 注释7:使用
public
属性,格式为:<对象变量名>.<成员名>; - 注释8:调用实例方法
distance
,并输出结果,语法形式是:<对象变量名>.<方法名>; - 注释9:实例变量都会有默认值,
int
默认值为0
;
本例中,我们通过对象直接操作了其内部数据x
和y
,这是一个不好的习惯,一般而言,不应该将实例变量声明为public
,而是private
,并且只应该通过对象的方法对实例变量进行操作。这也是为了减少误操作,直接访问变量没有办法进行参数检查和控制,而通过方法修改,可以在方法中进行检查。
1.3 private变量
一般不应该将实例变量声明为public
,下面我们修改一下类的定义,将实例变量定义为private
,然后通过实例方法来操作变量,比如获取变量值以及修改变量值。修改后,代码如下:
package com.ieening;
public class ClassTest {
public static void main(String[] args) {
Point point;
point = new Point();
System.out.println("Point:x=" + point.getX() + ",y=" + point.getY());
point.setX(3);
point.setY(4);
System.out.println("Point:x=" + point.getX() + ",y=" + point.getY());
System.out.println(point.distance());
}
}
/**
* Point
*/
class Point {
private int x;
private int y;
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public double distance() {
return Math.sqrt(x * x + y * y);
}
}
上面代码中,我们加了
4
4
4个方法,setⅩ/setY
用于设置实例变量的值,getⅩ/getY
用于获取实例变量的值,这就是私有属性的Setter
和Getter
方法。
这里面需要介绍的是this
这个关键字。this
表示当前实例,在语句this.x=x;
中,this.x
表示实例变量x
,而右边的x
表示方法参数中的x
。在实例方法中,有一个隐含的参数,这个参数就是this
,没有歧义的情况下,可以直接访问实例变量,在这个例子两个变量setter
方法中,因为实例属性和方法参数相同,所以需要通过加上this
来消除歧义。这
4
4
4个方法看上去是非常多余的,直接访问变量不是更简洁吗?而且函数调用是有成本的。在这个例子中,意义确实不太大,实际上,Java
编译器一般也会将对这几个方法的调用转换为直接访问实例变量,而避免函数调用的开销。但在很多情况下,通过函数调用可以封装内部数据,避免误操作,我们一般还是不将成员变量定义为public
。
1.4 变量默认值
实例变量都有默认值,但是如果我们想修改该默认值该怎么办呢?如下面代码,如果希望修改这个默认值,有两种方法:
- 注释1:可以在定义变量的同时就赋值;
- 注释2:将代码放入初始化代码块中,代码块用
{}
包围;
package com.ieening;
public class ClassTest {
public static void main(String[] args) {
......
}
}
/**
* Point
*/
class Point {
private int x=6; // 注释1
private int y;
{
y=8; // 注释2
}
......
}
上述代码中,x
的默认值设为了
1
1
1, y
的默认值设为了
2
2
2。在新建一个对象的时候,会先调用这个初始化,然后才会执行构造方法中的代码,关于构造方法,我们稍后介绍。静态变量也可以这样初始化:
static int STATIC_ONE = 1;
static int STATIC_TWO;
static {
STATIC_TWO = 2;
}
语句外面包了一个static {}
,这叫静态初始化代码块。静态初始化代码块在类加载的时候执行,这是在任何对象创建之前,且只执行一次。
1.5 构造方法
在初始化对象的时候,前面我们都是直接对每个变量赋值,有一个更简单的方式对实例变量赋初值,就是构造方法,先看下面代码。在Point
类定义中增加如下代码:
package com.ieening;
public class ClassTest {
public static void main(String[] args) {
......
}
}
/**
* Point
*/
class Point {
private int x;
private int y;
Point() { // 注释1
this(0, 0);
}
Point(int x, int y) { // 注释2
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
......
}
注释1和注释2,这两个就是构造方法,构造方法可以有多个。不同于一般方法,构造方法有一些特殊的地方:
- 名称是固定的,必须与类名相同。这也容易理解,靠这个用户和
Java
系统就都能容易地知道哪些是构造方法。 - 没有返回值,也不能有返回值。构造方法隐含的返回值就是实例本身。
- 与普通方法一样,构造方法也可以重载。第二个构造方法是比较容易理解的,使用
this
对实例变量赋值。我们解释下第一个构造方法,this(0,0)
的意思是调用第二个构造方法,并传递参数0,0
,我们前面解释说this
表示当前实例,可以通过this
访问实例变量,这是this
的第二个用法,用于在构造方法中调用其他构造方法。这个this
调用必须放在第一行,这个规定也是为了避免误操作。构造方法是用于初始化对象的,如果要调用别的构造方法,先调别的,然后根据情况自己再做调整,而如果自己先初始化了一部分,再调别的,自己的修改可能就被覆盖了。
这个例子中,不带参数的构造方法通过this(0,0)
又调用了第二个构造方法,这个调用是多余的,因为x
和y
的默认值就是
0
0
0,不需要再单独赋值,我们这里主要是演示其语法。
我们来看下如何使用构造方法,代码如下:
public class ClassTest {
public static void main(String[] args) {
Point point;
point = new Point(3, 4); // 注释1
System.out.println("Point:x=" + point.getX() + ",y=" + point.getY());
System.out.println(point.distance());
}
}
注释1中调用构造方法,就可以将实例变量x
和y
的值设为
3
3
3和
4
4
4。前面我们介绍new Point()
的时候说,它至少做了两件事,一件是分配内存,另一件是给实例变量设置默认值,这里我们需要加上一件事,就是调用构造方法。调用构造方法是new
操作的一部分。通过构造方法,可以更为简洁地对实例变量进行赋值。
关于构造方法,下面我们讨论两个细节概念:一个是默认构造方法;另一个是私有构造方法。
1、默认构造方法
默认构造方法每个类都至少要有一个构造方法,在通过new
创建对象的过程中会被调用。但构造方法如果没什么操作要做,可以省略。Java
编译器会自动生成一个默认构造方法,也没有具体操作。但一旦定义了构造方法,Java
就不会再自动生成默认的,具体什么意思呢?在这个例子中,如果我们只定义了第二个构造方法(带参数的),则下面语句:Point p = new Point();
就会报错,因为找不到不带参数的构造方法。为什么Java
有时候自动生成,有时候不生成呢?在没有定义任何构造方法的时候,Java
认为用户不需要,所以就生成一个空的以被new
过程调用;定义了构造方法的时候,Java
认为用户知道自己在干什么,认为用户是有意不想要不带参数的构造方法,所以不会自动生成。
2、私有构造方法
构造方法可以是私有方法,即修饰符可以为private
,为什么需要私有构造方法呢?大致可能有这么几种场景:
- 不能创建类的实例,类只能被静态访问,如
Math
和Arrays
类,它们的构造方法就是私有的。 - 能创建类的实例,但只能被类的静态方法调用。有一种常见的场景:类的对象有但是只能有一个,即单例(单个实例)。在这种场景中,对象是通过静态方法获取的,而静态方法调用私有构造方法创建一个对象,如果对象已经创建过了,就重用这个对象。
- 只是用来被其他多个构造方法调用,用于减少重复代码。
1.6 类和对象的生命周期
了解了类和对象的定义与使用,下面我们再从程序运行的角度理解下类和对象的生命周期。
在程序运行的时候,当第一次通过new
创建一个类的对象时,或者直接通过类名访问类变量和类方法时,Java
会将类加载进内存,为这个类分配一块空间,这个空间会包括类的定义、它的变量和方法信息,同时还有类的静态变量,并对静态变量赋初始值。类加载进内存后,直到程序结束一般都不会释放。一般情况下,类只会加载一次,所以静态变量在内存中只有一份。当通过new
创建一个对象的时候,对象产生,在内存中,会存储这个对象的实例变量值,每做new
操作一次,就会产生一个对象,就会有一份独立的实例变量。每个对象除了保存实例变量的值外,可以理解为还保存着对应类型即类的地址,这样,通过对象能知道它的类,访问到类的变量和方法代码。实例方法可以理解为一个静态方法,只是多了一个参数this
。通过对象调用方法,可以理解为就是调用这个静态方法,并将对象作为参数传给this
。对象的释放是被Java
用垃圾回收机制管理的,大部分情况下,我们不用太操心,当对象不再被使用的时候会被自动释放。
具体来说,对象和数组一样,有两块内存,保存地址的部分分配在栈中,而保存实际内容的部分分配在堆中。栈中的内存是自动管理的,函数调用入栈就会分配,而出栈就会释放。堆中的内存是被垃圾回收机制管理的,当没有活跃变量指向对象的时候,对应的堆空间就可能被释放,具体释放时间是Java
虚拟机自己决定的。活跃变量就是已加载的类的类变量,以及栈中所有的变量。
马俊昌.Java编程的逻辑[M].北京:机械工业出版社,2018. ↩︎
尚硅谷教育.剑指Java:核心原理与应用实践[M].北京:电子工业出版社,2023. ↩︎