类与对象
1. 类的构成
类(class)和对象(object,也被称为实例,instance)。
1.1 类的定义语法
Java定义类的简单语法如下:
[修饰符] class 类名 {
// 0~N个构造器定义...
// 0~N个字段...
// 0~N个方法...
}
Java中不习惯称呼成员变量,而是称相应的内容为字段。
为了方便,以下将类中的字段和方法统称为类的“成员”。
- 类的修饰符可以是
public,final,abstract,省略
。 - 类里各成员之间的定义顺序没有任何影响,各成员之间可以相互调用,但
static
修饰的成员不能访问没有static
修饰的成员。- 构造器用于构造该类的实例,Java通过
new
关键字来调用构造器,从而返回该类的实例; - 字段用于定义该类或该类的实例所包含的状态数据;
- 方法则用于定义该类或该类的实例的行为特征或者功能实现。
- 构造器用于构造该类的实例,Java通过
- 如果程序员没有为一个类编写构造器,系统会为该类提供一个默认的构造器;而如果程序员为一个类提供了一个构造器,系统将不再为该类提供构造器。
类通过一个或多个变量来保存其状态,通过方法实现它的行为。
1.2 定义字段
语法格式:[修饰符] 类型 字段名 [= 默认值];
修饰符:
- 可以省略,也可以是
public, protected, private, static, final
; - 其中
public, protected, private
三个最多只能选其一,可以与static, final
组合起来修饰成员变量。
建议成员变量名使用英文名词命名。
1.3 定义方法
语法格式:
[修饰符] 方法返回值类型 方法名(形参列表){
// 由零条到多条可执行性语句组成的方法体
}
修饰符:
- 可省略,也可以是
public
,protected
,private
,static
,final
,abstract
- 其中
public
,protected
,private
三个最多只能选其一; final
和abstract
最多只能选其一,但它们可以与static
组合起来修饰方法。
建议方法名使用英文动词开头。
1.4 产生对象
类是构造对象的模板,对象是类的具体实例。
语法:
// 定义一个Person类型的变量
Person p;
// 通过new调用Person类的构造方法,返回一个Person实例,并将其赋值给p变量
p = new Person();
当一个对象被创建成功后,这个对象将被保存在堆内存中,Java程序不允许直接访问堆内存中的对象,只能通过该对象的引用操作该对象,这跟数组类似。
当Java创建一个对象时,系统先为该对象的所有实例变量分配内存(前提是该类已经被加载过了),然后程序开始对这些实例变量执行初始化。其初始化顺序是:
- 顺序执行初始化块或声明实例变量时指定的初始值;
- 执行构造器里指定的初始值。
MyClass temp;
tmp = new MyClass();
Myclass temp = new MyClass();
清华大学郑莉——
- 注意,当我们去存储每个对象的时候,占内存空间的是对象的数据成员。我们将数据成员抽象成属性和行为,属性是此对象区分于彼对象的要素,而行为——同一类对象的行为是一模一样的,因而代码只有一份。
- 那么这个时候为什么我们还要区分这个方法是对象的方法还是类的方法呢?这里的落脚点在于这个方法是作用在谁上面的,对象的方法作用在每个对象上,而对于类方法——有些方法是属于整个类的,有时候我们需要即使一个对象都不存在的时候也能调用某些方法,即运行这个方法和对象无关,如诸多的数学函数。
用类来定义的变量称为引用变量,也就是说,所有类都是引用类型。要认识到重要的一点:对象变量并没有实际包含一个对象,它只是引用一个对象。在Java中,任何对象变量的值都是存储在另外一个地方的某个对象的引用。

很多人错误地认为Java中的对象变量就相当于C++中的引用,然而,在C++中没有null
引用,而且引用不能赋值,可以把Java中的对象变量看作类似于C++中的对象指针。如:
Date birthday; // Java
实际上等同于
Date* birthday; // C++
一旦理解了这点,一切问题就迎刃而解了。当然Date*
指针只有使用new
调用才会初始化,就这一点而言,C++与Java的语法几乎是一样的。
Date* birthday = new Date(); // C++
如果把一个变量复制到另一个变量,两个变量就指向同一个日期,即它们是同一个对象的指针。Java中的null
引用对应于C++中的NULL
指针。
所有的Java对象都存储在堆中,当一个对象包含另一个对象变量时,它只是包含着另一个堆对象的指针。
2. static关键字
static
是一个特殊的关键字,它可用于修饰方法、成员变量等成员。
static
的真正作用就是用于区分“字段、方法、内部类、初始化块”这四种成员到底属于类本身还是实于实例。
2.1 类字段与类方法
static
修饰的成员表明它属于这个类本身,而不属于该类的单个实例,因此通常把static
修饰的成员变量、方法也称为类变量、类方法;
对于整个类中所有对象公共的常量,或需要共享的变量,常定义为static
的。
2.2 实例字段与实例方法
不使用static
修饰的普通方法、成员变量则属于该类的单个实例,而不属于该类,因此也相应称之为实例字段、实例方法。
2.3 静态与非静态
由于static
的英文直译是静态的意思,也常常把static
修饰的成员变量和方法称为静态字段和静态方法,把不使用static
修饰的成员变量和方法称为非静态字段和非静态方法。
2.4 设计本质
静态成员不能直接访问非静态成员。
没有使用static
修饰的普通方法和字段,只可通过实例来调用;但static
修饰的方法和字段,虽然理论上它只属于类而不属于实例,但它既可通过类来调用,也可通过实例来调用。
3. 对象的this引用
this
关键字最大的作用就是让类中一个方法,访问该类里的另一个方法或实例变量;
this
可以代表任何对象,当其出现在某个方法中时,它所代表的对象是不确定的,但它的类型是确定的:
this
代表当前类的实例;- 只有当这个方法被调用时,
this
所代表的对象才被确定下来; - 谁在调用这个方法,
this
就代表谁。
this
使用示例:
你最初想象的写法
public class Dog { public void jump(){ System.out.println("正在执行jump方法"); } public void run(){ Dog d = new Dog(); d.jump(); System.out.println("正在执行run方法"); } }
有了
this
以后的写法,这样更节省开销public class Dog { public void jump(){ System.out.println("正在执行jump方法"); } public void run(){ this.jump(); System.out.println("正在执行run方法"); } }
由于
this
的需求普遍性,Java允许对象的一个成员定义中直接调用另一个成员,即省略this
前缀,但需清楚这只是一种简便的写法,真正的原理上还是在调用着this
public class Dog { public void jump(){ System.out.println("正在执行jump方法"); } public void run(){ jump(); System.out.println("正在执行run方法"); } }
static
修饰的方法中无法使用this
关键字,因为static
方法不属于对象,this
指针无法拥有有效的指向,故而,static
方法不能直接调用非static
的成员和方法(如果确实需要在静态方法中调用另一个普通方法,那么只能采用上表中的第一种形式了)。
当方法内部的某个局部变量和成员变量同名,局部变量会覆盖掉该成员变量,为了避免歧义,此时应该显式地使用this
前缀。
程序可以像访问普通引用变量一样来访问this
引用,甚至可以把this
当成普通方法的返回值:
public class ReturnThis {
public int age;
public ReturnThis grow() {
age++;
// return this 返回调用该方法的对象
return this;
}
public static void main(String[] args) {
ReturnThis rt = new ReturnThis();
// 可以连续调用同一个方法
rt.grow().grow().grow();
System.out.println("rt的age成员变量值是:" + rt.age);
}
}
4. 类中的方法
方法在逻辑上要么属于类,要么属于对象。
要完整地描述一个方法,需要指定方法名以及参数类型,这叫作方法的签名(Signature)。注意,返回类型不是方法签名的一部分。
4.1 参数传递
对于程序设计语言,将参数传递给方法(或函数)的方式有:
按......调用是一个标准的计算机科学术语,用来描述各种程序设计语言中方法参数的传递方式。
- 按值调用(call by value):表示方法接收的是调用者提供的值。
- 按引用调用(call by reference):表示方法接收的是调用者提供的变量地址。
显然,方法可以修改按引用传递的变量的值,而不能修改按值传递的变量的值。
很多程序设计语言,特别是C++和Pascal,同时提供了上述两种参数传递的方式。而对于Java而言,其参数的传递方式永远是按值调用的。即将实际参数值的副本传入方法内,而参数本身不会受到任何影响。
值得注意的是,在Java中有两种类型的方法参数,基本类型和对象引用。显然,一个方法不可能修改基本类型的参数,而对象引用作为参数就不同了:

有些程序员认为Java程序设计语言对对象采用的是按引用调用,实际上这种理解是不对的。一个反证的例子是:
编写一个交换两个
Employee
对象的方法// doesn't work public static void swap(Employee x, Employee y) { Employee temp = x; x = y; y = temp; }
如果Java采用的是按引用调用,那么这个方法就应该能实现交换:
var a = new Employee("Alice", ...); var b = new Employee("Bob", ...); swap(a, b); // does a now refer to Bob, b to Alice?
然而,这个方法并没有改变存储在变量
a
和b
中的对象引用,swap
方法的参数x
和y
被初始化为两个对象引用的副本,这个方法交换的是这两个副本。// x refers to Alice, y to Bob Employee temp = x; x = y; y = temp; // now x refers to Bob, y to Alice
因此,最终白费力气。在方法结束时参数变量
x
和y
被丢弃了,原来的变量a和b仍然引用这个方法调用之前所引用的对象:image-20220612171435176
4.2 形参个数可变的方法
有时这些方法被称为变参(varargs)方法。
定义语法:在方法最后一个形参的类型后增加三点...
,则表明该形参可以接受多个参数值,多个参数值被当成数组传入。
个数可变的形参只能处于形参列表的最后,也就是说,一个方法中最多只能有一个个数可变的形参。
// 以可变个数形参来定义方法
public static void test(int a, String... books){};
// 调用上述方法
test(5, "疯狂Java讲义","轻量级Java EE企业应用实战");
// 对比:采用数组形参来定义方法
public static void test(int a, String[] books){};
// 调用上述方法
test(5, new String[]{"疯狂Java讲义","轻量级Java EE企业应用实战"});
显然调用可变个数形参的方式更简洁。
调用形参个数可变的方法时,也可为个数可变的形参传入一个数组(实际上,形参个数可变的参数本质就是一个数组参数)。
对于可变长参数的方法,传递给可变长参数的实际参数可以是多个对象,也可以是一个对象,或者是没有对象。
当同一个类中定义了test(String… books)
方法以及一个重载的test(String book)
方法时,test(String… books)
方法的books
不可能通过直接传入一个字符串参数来调用,因为这会被test(String book)
“截胡”。此时若非要传入一个字符串参数的同时调用test(String… books)
,可采用传入字符串数组的形式,即obj.test(new String[]{"aa"});
4.3 方法重载
方法重载(overload) :同一个类中方法名相同,而参数列表不同。至于方法的其他部分不作要求。
C语言中不允许函数重载,即如果你想写一个关于整型数据的加法函数和一个关于浮点型数据的加法函数,它们的名字不能相同,即必须将这两个函数在名字上就区别开来。
4.4 方法重写/方法覆盖
子类对从父类继承来的属性变量及方法可以重新定义,这对于属性而言叫隐藏,对于方法而言叫覆盖。
方法重写(override) :也叫方法覆盖,子类和父类的方法名相同、参数列表、返回值类型相同。且要么都是类方法,要么都是实例方法。
覆盖方法的访问权限可以比被覆盖的宽松,但不能更为严格。换句话说,父类本身提供给外界的使用接口,它的派生类必须也同样提供。
顾名思义,定义为final
的方法当然不能被覆盖。同理,基类中声明为static
的静态方法也不能被覆盖。
4.5 Java不支持设置形参默认值
值得一提的是,Java中不支持对方法的形参设置默认值,想达到这样的效果通常得由方法重载来实现。
4.6 访问器方法/更改器方法
这里谈及两个小概念,用于在描述类方法的某些特性时使用。
- 访问器方法:只访问对象而不修改对象(数据)的方法。
- 更改器方法:访问对象时同时会修改对象(数据)的方法。
5. 构造器
构造方法/构造器是一个特殊的方法,其定义语法不需要返回类型。语法格式:
[修饰符] 构造器名(形参列表){
// 由零条到多条可执行性语句组成的构造器执行体
}
组成部分 | 说明 |
---|---|
修饰符 | 可省略,也可以是public,protected,private 其中之一 |
构造器名 | 必须和类名相同 |
形参列表 | 和定义方法形参列表的格式完全相同 |
值得注意的是:
构造器不能定义返回值类型,且不能使用
void
。如果为构造器定义了返回值类型或使用了
void
,编译时不会出错,但其实Java已经认定它不是一个构造器了,而是一个跟类名同名的普通方法。不要在构造器里显式使用
return
来返回当前类的对象,因为构造器的返回值是隐式的。由上,
static
关键字不能修饰构造器,这也符合static
的含义。
因为构造器主要用于被其他类调用,用以返回该类的实例,因而通常把构造器设置成public
访问权限。
Java构造器的工作方式与C++一样,但是,要记住所有的java对象都是在堆中构造的,构造器总是结合new
操作符一起使用。
关于无参构造方法
无参的构造方法对其子类的声明很重要,如果在一个类中不存在无参的构造方法,则要求其子类声明必须声明构造方法,否则在子类对象的初始化时会出错。
在声明构造方法时,好的声明习惯是:
- 要么不声明构造方法;
- 如果声明,至少声明一个无参构造方法。
构造器的执行过程
当程序调用构造器时,系统会先为对象分配内存空间,并为这个对象执行默认构造化,此时这个对象在构造器执行前就已经产生了,只是这个对象还不能被外部程序访问,只能在该构造器中通过this
来引用(猜测这又是因为对象分配在堆上的原因?)。当构造器的执行体执行结束后,这个对象作为构造器的返回值被返回。
this调用构造器
如果系统中包含了多个构造器,其中一个构造器B的执行体里完全包含另一个构造器A的执行体,则可在方法B中调用方法A,尽管有时这可以通过使用new
关键字建立一个新对象甚至通过直接在源文件中复制A构造器的执行代码来实现,但这显然不够优雅,为了不重建一个Java对象的同时而引用A的初始化代码,Java中可以this
关键字来调用相应的构造器。


使用this
调用构造器只能在构造器中使用,而且必须作为构造器执行体的第一条语句,前半部分意味着this
调用构造器的必要条件是有重载的构造器。
对象创建过程
当Java创建一个对象时,系统先为该对象的所有实例字段分配内存(前提是该类已经被加载过了),然后程序开始对这些实例字段执行初始化。其初始化顺序是:
- 顺序执行初始化块或声明实例字段时指定的初始值;
- 执行构造器里指定的初始值。
6. 类的成员变量(及局部变量)
这里的实例变量指实例字段,类变量指类字段。
在Java中,变量根据定义位置不同可分为两大类:成员变量、局部变量。

6.1 类的成员变量
成员变量之类字段与实例字段:
- 类字段从该类的准备阶段起开始存在,直到系统完全销毁这个类,类字段的作用域与这个类的生存区域相同;
- 而实例字段则从该类的实例被创建起开始存在,直到系统完全销毁这个实例,实例字段的作用域与对应实例的生存范围相同。
同一个类的所有实例访问类字段时,实际上访问的是该类本身的同一个变量,也即访问了同一片内存区。
在同一个类里,成员变量的作用范围是整个类内有效,一个类里不能定义两个同名的成员变量,即使一个是类变量一个是实例变量也不行。
可以在类定义中直接为字段进行赋值,此时相应的赋值操作会在执行构造器之前完成:
class Employee {
private String name = "";
// ...
}
6.2 局部变量
局部变量分为方法局部变量、代码块局部变量,直接理解即可。
局部变量都必须明确地初始化,但是对于类的字段,如果没有显式初始化,将会自动初始化为相应的默认值(0, false, null
),这是字段与局部变量的一个重要区别。
若方法局部变量与成员变量同名,局部变量会覆盖成员变量(若仍需引用相应成员变量可使用this
)。
6.3 成员变量与局部变量的存储位置
成员变量
定义一个成员变量时,成员变量将被内置到堆内存中,成员变量的作用域将扩大到类/对象存在的范围,这种范围的扩大有两个害处:
- 增大了变量的生存时间,导致更大的内存开销
- 扩大了变量的作用域,不利于提高程序的内聚性
局部变量
由于局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存中(这种因果关系似乎要搞清楚一些):
- 若该局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对应的内存中;
- 若该局部变量是一个引用类型的变量,则这个变量里存放的是地址,通过该地址引用到该变量实际引用的对象或数组。
因为局部变量只保存基本类型的值或者对象的引用,因此局部变量所占的内存区通常比较小。
6.4 属性的隐藏(清华大学郑莉)
属性的隐藏:
子类将拥有两个相同名字的变量,一个继承自父类,一个由自己声明。
当子类执行继承自父类的方法时,处理的是继承自父类的变量;而当子类执行它自己声明的方法时,所操作的就是它自己声明的变量。
// 父类 class Parent { Number num; } // 子类 class Child extends Parent { Float num; }
子类不能继承父类中的静态属性,但可以对父类中的静态属性进行操作。静态属性在整个类体系中只有一份拷贝。
7. 访问权限控制——封装和隐藏
为了方便,我们暂且把“类的成员”这个名词当作类的成员变量、方法、构造器等的统称。
类只有public
和空的控制;类成员可为public, (default), protected, private
。
7.1 类成员访问权限
Java为类成员提供了4个访问控制权限:private,(default),protected,public
(要注意,事实上没有default
这个关键字):

private
:私有权限,该成员只能在当前类的内部被访问default
:包访问权限(默认情况),该成员可以被相同包下的其他类访问protected
:子类访问权限,该成员在上述权限基础上,还可以被其子类访问。- 通常情况下,若使用
protected
修饰一个方法,其用意是希望其子类来重写这个方法。 - 《Java核心技术·卷I》中声称,“不要使用受保护的字段”,Java的
protect
的机制并不能带来更多的保护。
- 通常情况下,若使用
public
:公共访问权限,public
的成员可以被所有类访问public
不在乎它们是否处于同一个包中或是否具有继承关系
private | [default] | protected | public | |
---|---|---|---|---|
同一个类中 | √ | √ | √ | √ |
同一个包中 | √ | √ | √ | |
子类中 | √ | √ | ||
全局范围内 | √ |
7.2 类访问权限
类访问权限针对的是外部类,因为内部类属于类的成员了,其访问权限控制参见上节。
对于一个外部类而言,其只有两种访问控制级别,public
和default
(默认),上述大多数情况是在描述类成员的访问控制级别情况。
public
级别的外部类可以被所有类使用;default
级别的外部类只能被同一个包中的其他类使用。
7.3 访问权限控制的基本原则
关于访问控制符的使用,可参考如下几条基本原则:
- 类里的绝大部分成员变量都应该使用
private
修饰,只有一些static
修饰的、类似全局变量的成员变量,才应考虑使用public
修饰; - 有些方法只用于辅助实现该类的其他方法,通常称之为工具方法,它们应该使用
private
修饰; - 若希望某个类主要用做其他类的父类,该类里包含的大部分方法可能仅希望被其子类重写,而不想被外界直接调用,应该使用
protected
修饰; - 希望暴露出来给其他类自由调用的方法应该使用
public
修饰; - 外部类通常都希望被其他类自由使用,所以大部分外部类应使用
public
修饰。