类与对象
前面写 Java 程序时,我们已经用过变量和方法。到了面向对象这一部分,可以进一步把一类事物的属性和行为放在一起描述,再根据这份描述创建具体的对象。
我觉得可以先把类理解成一张设计图,把对象理解成按照设计图创建出来的具体事物:
- 类(class):描述一类事物共同的属性和行为。
- 对象(object):根据类创建出来的具体实例。
例如,手机都有品牌、价格等属性,也能打电话、玩游戏。我们可以先定义一个 Phone 类,再根据它创建具体的手机对象。
定义一个类
在类中,常见的内容包括成员变量和成员方法:成员变量描述属性,成员方法描述行为。构造方法、代码块、内部类等内容也可以写在类里,后面会逐步接触。
先创建 Phone.java,定义一个手机类:
public class Phone {
//属性(成员变量)
String brand;
double price;
//行为(方法)
public void call() {
System.out.println("打电话");
}
public void play_game() {
System.out.println("玩游戏");
}
}这里的 brand、price 是成员变量,call()、play_game() 是成员方法。此时我们只是定义了手机这类事物,还没有创建出某一部具体的手机。
创建和使用对象
定义好类之后,就可以使用 new 创建对象。格式为:
类名 对象名 = new 类名();例如:
Phone p = new Phone();创建对象后,可以通过 . 访问它的成员:
- 访问属性:
对象名.成员变量 - 调用方法:
对象名.方法名()
我们把创建对象、设置属性和调用方法放在一起看:
public class Test1 {
public static void main(String[] args) {
//创建手机对象
Phone p1 = new Phone();
//给p1这个对象赋值
p1.brand = "小米";
p1.price = 1999.9;
//获取p1对象的值
System.out.println(p1.brand);
System.out.println(p1.price);
//调用手机类中的方法
p1.call();
p1.play_game();
}
}运行结果:
小米
1999.9
打电话
玩游戏这里的 p1 引用着新创建的 Phone 对象。我们给这部手机设置了品牌和价格,再调用它的两个方法。以后再创建一个 Phone 对象,就可以为它设置另一组属性,而不需要重新定义一个手机类。
类的命名与文件组织
编写类时,还有几个常用约定:
- 类名建议见名知意,采用大驼峰命名法,例如
Phone、Student。 - 一个
.java文件可以包含多个顶层类,但最多只有一个public顶层类。如果存在这样的类,文件名必须与它的类名一致,例如public class Phone应放在Phone.java中。 - 学习时常把存放数据的类和带
main()的测试类分开:前者描述事物,后者负责创建对象、赋值、调用方法等操作。
比如学生管理系统中,可以定义 Student、Teacher 等类。以后增加一名学生时,只需要创建一个新的 Student 对象,再为姓名、年龄等属性赋值,不必为每名学生单独写一个类。
成员变量的默认值
成员变量的基本定义格式是 修饰符 数据类型 变量名 = 初始值;。如果没有显式指定初始值,成员变量会获得对应类型的默认值。
| 数据类别 | 常见类型 | 默认值 |
|---|---|---|
| 整数类型 | byte、short、int、long | 0 |
| 浮点类型 | float、double | 0.0 |
| 字符类型 | char | \u0000(空字符) |
| 布尔类型 | boolean | false |
| 引用类型 | 类、接口、数组,如 String | null |
这里的默认值适用于成员变量和数组元素。方法中的局部变量不一样,使用前需要先赋值,后面会专门对比。
封装:从学生年龄开始理解
面向对象常说的三大特性是封装、继承和多态,这里先记录封装。
我的理解是:一个对象代表什么,就应该组织好与它相关的数据和行为。外部需要做什么,可以通过对象提供的方法完成,而不一定要直接操作它内部的所有变量。
为什么需要 private
假设我们定义一个 Student 类,保存学生的姓名和年龄。先不做任何访问限制,直接给年龄赋值:
public class Test1 {
public static void main(String[] args) {
Student s1 = new Student();
s1.age = -1;
System.out.println(s1.age);
}
}
class Student {
String name;
int age;
}这段代码能正常运行,但问题也很明显:s1.age = -1 可以把年龄设成不合理的负数。每次都让使用对象的代码自己检查年龄,既麻烦,也容易遗漏。
这时就可以用到 private:
private是 Java 的一种访问权限修饰符。- 它可以修饰成员变量和成员方法。
- 被
private修饰的成员,只能在声明它的类内部直接访问。
例如把 Student 中的 int age; 改为 private int age; 后,Test1 类就不能再通过 s1.age = -1 直接修改它;这样写会出现编译错误。
不过,private 的作用只是限制外部直接访问,并不会自动判断年龄是否合理。既然不允许外部直接修改,又该怎样正常设置和查看年龄呢?
使用 set 和 get 方法
可以在 Student 类中提供公共方法:setAge() 负责设置年龄,getAge() 负责读取年龄。这样就能把校验逻辑集中放在 setAge() 中,而不用每次在外部重新判断。
public class Test1 {
public static void main(String[] args) {
Student s1 = new Student();
s1.setAge(-1);
System.out.println(s1.getAge());
s1.setAge(100);
System.out.println(s1.getAge());
}
}
class Student {
String name;
private int age;
public void setAge(int a) {
if (a >= 0 && a < 150)
age = a;
else
System.out.println("年龄非法");
}
public int getAge() {
return age;
}
}运行结果:
年龄非法
0
100第一次传入 -1,不满足 0 <= a < 150,所以打印“年龄非法”,age 仍然保持 int 的默认值 0;第二次传入 100,校验通过,年龄就被成功修改了。
这样一来,不管谁调用 setAge(),都会经过同一处校验。这比公开成员变量、让外部代码随意赋值更容易管理数据。
this 关键字
刚才的 setAge() 使用形参 a 来区分成员变量 age。但如果把形参也命名为 age,看起来会更直观:
class Student {
String name;
private int age;
public void setAge(int age) {
if (age >= 0 && age < 150)
age = age;
else
System.out.println("年龄非法");
}
public int getAge() {
return age;
}
}这里有个容易搞混的地方:age = age; 不会编译报错,但它也不会修改对象的成员变量。因为方法的形参和成员变量同名时,直接写 age 会优先指向形参;这一句只是把形参的值赋给它自己。
要明确修改当前对象的成员变量,可以写成:
this.age = age;this.age:当前对象的成员变量age。- 右侧的
age:传进来的形参age。
所以,this 可以帮助我们在同名变量之间明确指出当前对象的成员。
这个现象也可以结合“就近原则”理解。看下面的例子:
public class Test1 {
public static void main(String[] args) {
Student s1 = new Student();
s1.method();
}
}
class Student {
String name;
private int age;
public void method() {
int age = 10;
System.out.println(age);
System.out.println(this.age);
}
}运行结果:
10
0方法里新定义了局部变量 age = 10,直接打印 age 得到的是局部变量的值;使用 this.age,访问的则是对象中的成员变量。成员变量没有主动赋值,因此仍然是默认值 0。
构造方法
前面的例子都是先创建对象,再给成员变量赋值。那么,能不能在创建对象的时候就把初始数据传进去?这就需要用到构造方法,也叫构造器。
构造方法的名称必须与类名相同,并且没有返回值类型,连 void 都不写。它通常用来完成对象创建时的初始化工作。
格式可以简单理解为:
class 类名 {
类名(参数列表) {
// 创建对象时要执行的代码
}
}使用 new 创建对象时,会执行对应的构造方法。创建几个对象,就分别执行几次构造过程。构造方法不能像普通成员方法那样,通过 对象名.方法名() 手动调用,也不能使用 return 返回一个具体结果。
空参构造
空参构造就是不需要传入参数的构造方法。比如下面创建两次 Student 对象:
public class Test1 {
public static void main(String[] args) {
Student s1 = new Student();
Student s2 = new Student();
}
}
class Student {
String name;
int age;
public Student(){
System.out.println("???");
}
}因为每创建一个对象都会执行一次构造方法,所以这里会打印两次 ???。
还有一个容易忽略的规则:如果类中没有声明任何构造方法,Java 编译器会自动提供一个默认的无参构造方法。但只要我们自己声明了构造方法,编译器就不会再自动补上这个默认构造方法。
所以,如果已经写了有参构造,又希望能使用 new Student() 创建对象,就需要自己再定义一个无参构造方法。
有参构造
如果创建对象时已经知道姓名和年龄,就可以把它们作为参数直接传进去:
public class Test1 {
public static void main(String[] args) {
Student s1 = new Student("张三", 18);
System.out.println(s1.name);
System.out.println(s1.age);
}
}
class Student {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}new Student("张三", 18) 会调用接收 String 和 int 两个参数的构造方法。通过 this.name = name; 和 this.age = age;,我们就可以在创建对象时完成赋值,不需要再分别调用设置方法。
那是不是有参构造更方便,以后只写它就行了?也不一定。如果创建对象时还不知道具体的属性值,先使用无参构造创建对象、以后再设置也很常见。
同一个类中可以同时定义无参构造和有参构造。它们名称相同、参数列表不同,正好也是前面学过的方法重载思想在构造方法上的应用。
常见的 JavaBean 类
前面说过,开发时经常需要一个类来保存学生、老师等对象的数据。对于符合一定编写约定、主要用于封装数据的类,我们经常会接触到 JavaBean 的写法。
在这部分学习中,可以先按照下面的结构来写:
- 类名见名知意,例如
Student。 - 成员变量通常使用
private修饰。 - 提供无参构造方法,常常也会提供包含全部属性的有参构造方法。
- 为需要读写的属性提供相应的
getXxx()、setXxx()方法。 - 如果类还有其他行为,也可以继续定义成员方法。
要注意的是:全参构造是常见的便利写法,并不是所有 JavaBean 都必须具备的硬性要求。这里先掌握课程中最常用的组合即可。
把之前的学生类整理一下,就有了下面的形式:
class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}这样创建对象时,可以选择无参构造后再设置属性,也可以通过有参构造直接传入姓名和年龄;需要读取属性时则使用 getName()、getAge()。
这段示例主要展示类的完整结构。set 方法并不会自动进行数据校验:如果希望年龄必须在某个范围内,仍然要像前面的 setAge() 示例一样,主动加入判断。
成员变量与局部变量的区别
在 Student 类中,我们已经同时接触过成员变量和局部变量:
- 成员变量:定义在类中、方法外,例如
private int age;。 - 局部变量:定义在方法、构造方法或代码块内,例如
setAge(int age)的形参age,或者方法里的int age = 10;。
它们的区别可以整理成一张表:
| 对比项 | 成员变量(以实例成员变量为例) | 局部变量 |
|---|---|---|
| 定义位置 | 类中、方法外 | 方法、构造方法或代码块中;形参也属于局部变量 |
| 初始值 | 没有显式赋值时有默认值 | 不会自动获得可直接使用的默认值,使用前必须赋值 |
| 所属关系 | 属于对象,可通过对象的成员进行访问 | 属于当前方法调用或代码块的执行过程 |
| 内存理解 | 随对象保存,通常可以理解为对象位于堆内存中 | 初学时可结合方法调用的栈帧来理解 |
| 生命周期 | 通常随对象存在,直到对象不再可达并可能被回收 | 只在所属作用域内有效,离开作用域后无法再访问 |
| 作用范围 | 受成员访问权限控制,可被类中的多个方法使用 | 只能在声明它的作用域内使用 |
这里的内存位置是便于入门理解的简化说明,表格比较的是实例成员变量和方法内局部变量,不包括 static 成员等其他情况。
回到前面的代码:this.age 是当前对象的成员变量,而 setAge(int age) 中的 age 是形参。把它们区分清楚,this、构造方法中的赋值,以及封装的写法就更容易理解了。
聊两句
评论审核后显示有疑问、补充,或者想交流一下,都可以写在这里。
评论区将在滚动到这里时加载。