本期是Java开发教程系列的第二期,本期主要向各位介绍 Java 面向对象的基础知识——类与对象.若你已经了解类和对象的基本概念,可以跳过本期内容.
每期叠甲:
时间仓促,水平有限,文中难免存在缺点和错误,恳请广大开发者和从业者指正.
一、为什么需要类和对象
第 1 期我们学了变量、方法、数组和集合.但如果要描述"一堆学生",每个学生的姓名、成绩都单独定义变量,代码很快会变成灾难:
String name1 = "小明";
double score1 = 92;
String name2 = "小红";
double score2 = 88;
// 如果有一百个学生呢?这样写根本没法维护,也就是俗称的史山代码.
我们需要一种把"相关的数据 + 相关的操作"打包在一起的方式.Java 用类(class)和对象(object)来做这件事:
类 = 一张图纸,规定了这一类东西"长什么样、能干什么"
对象 = 按图纸造出来的具体东西;同一张图纸,可以造出无数个对象
打个比方:Student 类是"学生"的模板,而"小明""小红"是按这个模板造出来的具体对象——每个对象有自己的姓名、成绩,但结构(有哪些字段、哪些方法)都一样.
二、定义类:字段与方法
2.1 一个最简单的类
用代码定义一个"学生"类:
public class Student {
// 字段(也叫属性):描述学生"是什么样"
String name; // 姓名
double score; // 成绩
// 方法:描述学生"能干什么"
void sayHello() {
System.out.println("你好,我是 " + name);
}
}
public class Student 声明一个公开的类,类名是 Student
类里面的 name、score 叫字段,保存这个对象的"状态"
sayHello() 是方法,定义这个对象的"行为"
注意:这些代码目前是定义,什么都没运行
2.2 使用对象:new
题外话:这也是有时候说找不到对象就new一个的原因.
要在程序里真正用上,得先造一个对象出来,用 new:
public class Main {
public static void main(String[] args) {
Student s1 = new Student(); // 用 new 造出一个学生对象
s1.name = "小明"; // 给字段赋值
s1.score = 92;
s1.sayHello(); // 调用方法
System.out.println(s1.score);
}
}
运行结果:
你好,我是 小明
92.0
new Student() 就是创建对象,s1 是一个指向这个对象的引用
通过 s1.字段名 读/写字段,通过 s1.方法名() 调方法
一个类可以造出任意多个互不干扰的对象:再写 Student s2 = new Student();,s2 是另一个学生
三、构造方法
3.1 为什么需要构造方法
上面的写法要 s1.name = "小明"; s1.score = 92; 两行才设置好初始值,麻烦.
用构造方法(constructor)可以在****创建对象的同时****把初始值填好:
public class Student {
String name;
double score;
// 构造方法:方法名和类名一模一样,而且不写返回类型
Student(String name, double score) {
this.name = name;
this.score = score;
}
}
使用方式立刻清爽了:
Student s1 = new Student("小明", 92); // 创建的同时就初始化好了
Student s2 = new Student("小红", 88);
构造方法的三条规则:
方法名必须和类名一样(包括大小写)
不写返回类型(连 void 都不写)
通过 new 调用,不需要也不能手动调用
3.2 无参构造与有参构造
如果你一个构造方法都没写,编译器会免费送一个无参构造,它什么都不干,字段保持默认值(数字是 0、引用是 null)
一旦你自己写了构造方法,免费的无参构造就消失了.此时再写 new Student() 会编译报错——除非你显式再补一个无参构造:
public class Student {
String name;
double score;
// 无参构造
Student() {
}
// 有参构造
Student(String name, double score) {
this.name = name;
this.score = score;
}
}
这样 new Student() 和 new Student("小明", 92) 都能用.两个构造方法名字相同、参数不同,这正是第 1 期讲过的方法重载的典型应用.
四、this 关键字
this 只有一个含义:“当前这个对象”.你可以把它想成对象的名片——谁在跑这段代码,this 就是谁.
4.1 它在构造方法里做什么
再看刚才的构造方法:
Student(String name, double score) {
this.name = name;
this.score = score;
}
执行 new Student("小明", 92) 时,this 就是那个新造出来的"小明"对象.这行代码里出现了两个 name,它们不是一回事:
右边的 name:是参数.参数就在最近的括号里,所以不带 this 的 name 默认指它
左边的 this.name:是字段.前面加了 this,明确说"这个对象的字段"
所以 `this.name = name;` 的意思就是:**把传进来的参数 name,存进当前对象的 name 字段里**.
这里藏着一条新手最容易懵的规则: 就近原则 :不带 this 的 name,先找参数,找到就指参数,参数里没有才去找字段.构造方法里参数和字段恰好同名,所以必须靠 this 才能指到字段.
4.2 为什么参数要和字段同名
其实换成别的名字也能写:
Student(String n, double s) {
this.name = n;
this.score = s;
}
参数叫 n/s 当然可以,但这样看不出"这个参数是给哪个字段用的".更常见的习惯是参数和字段同名,再用 this 区分——一眼就知道 name 要存进 name,score 要存进 score.所以这不是必须的语法,而是一种让代码更好读的写法.
4.3 新手速记
this 不只出现在构造方法里,但新手阶段只需要记住构造方法里这一个用法——参数和字段同名时,this.字段 指字段.普通方法里没有同名参数,this 通常不用写,现在先别管它.
五、一个文件一个公开类
这里有个新手常搞混的点,先划重点:被限制的是"public 类"的个数,不是"public 成员"的个数.
一个 .java 文件里只能有一个 public 类,而且这个类的名字必须和文件名一模一样(大小写也要一致)
但一个类内部可以有任意多个 public 成员——多个 public 字段、多个 public 方法都没问题:
public class Student { // 文件里唯一的 public 类,必须和文件名 Student.java 一致
public String name; // public 字段,可以有多个
public double score;
public void sayHello() { // public 方法,也可以有多个
System.out.println("你好,我是 " + name);
}
}
- 同一个文件里还能放**不加 public 的辅助类**,但不能再放第二个 public 类:
public class Main {
public static void main(String\[\] args) {
Helper h = new Helper();
h.go();
}
}
// 同一个文件里可以放非 public 的类,但不能再加一个 public 的
class Helper {
void go() {
System.out.println("我是辅助类");
}
}
如果文件名和 public 类名不一致,编译器会直接报错.
为什么这么规定?为了让编译器能快速找到类:它要找 Student,直接去 Student.java 找就行,不用把整个项目的文件都扫一遍.这个习惯要记牢,后面写工程都是这个规则.
六、三元运算符
接下来介绍一个第 1 期没讲过的小语法——三元运算符 条件 ? 值A : 值B:
String result = score >= 60 ? "及格" : "不及格";
意思是:条件成立(为 true)就取 ? 后面的值,不成立就取 : 后面的值.上面这行等价于:
String result;
if (score >= 60) {
result = "及格";
} else {
result = "不及格";
}
看到区别了吗?三元运算符就是把最简单的 if/else 压缩成一行,适合"根据一个条件选一个值"的场合.分支一多就不要用它,可读性会变差.接下来的小项目你就可以试一试三元运算符.
七、小项目:学生成绩统计
把本期内容串成一个能运行的小程序,共两个文件.
文件一 Student.java:
public class Student {
String name;
double score;
// 构造方法:创建对象时初始化
Student(String name, double score) {
this.name = name;
this.score = score;
}
// 判断是否及格
boolean passed() {
return score >= 60;
}
// 格式化打印自己
void printInfo() {
System.out.println(name + " 成绩 " + score
+ (passed() ? " 及格" : " 不及格"));
}
}
文件二 Main.java:
public class Main {
public static void main(String\[\] args) {
// 创建三个学生对象
Student a = new Student("小明", 92);
Student b = new Student("小红", 58);
Student c = new Student("小刚", 76);
// 把对象装进数组,逐个打印
Student\[\] students = {a, b, c};
for (Student s : students) {
s.printInfo();
}
// 计算平均分
double sum = 0;
for (Student s : students) {
sum += s.score;
}
System.out.println("平均分:" + sum / students.length);
// 找出最高分
Student best = students\[0\];
for (Student s : students) {
if (s.score > best.score) {
best = s;
}
}
System.out.println("最高分:" + best.name);
}
}
运行结果:
小明 成绩 92.0 及格
小红 成绩 58.0 不及格
小刚 成绩 76.0 及格
平均分:75.33333333333333
最高分:小明
这个小程序用到了第 1 期的数组和增强 for,加上本期的类和对象.注意看 students 数组里装的不是数据,而是对象的引用——数组 students[0]、students[1]、students[2] 分别指向三个 Student 对象.
朝花夕拾:现在能看懂第 1 期的 File 了
第 1 期我们写过这样一行:
File myFile = new File(new xxxxxx);
当时说它"有问题",但没说清问题在哪.学完本期,你能看懂一大半了:
new 是创建对象,new File(...) 就是创建一个 File 对象
new xxxxxx 之所以写不出来,是因为 File 的构造方法接收的是一个路径字符串,不是另一个对象.正确写法是:
File myFile = new File("D:\\mc\\mod\\config.txt");
其实 File 本身就是一个类,myFile 就是一个 File 对象——对象,正是本期学的核心概念.
总结
本期我们迈出了面向对象的第一步:
类与对象:类是图纸,对象是按图纸造出来的实体;一个类能造出无数个对象
字段与方法:字段描述对象"是什么样",方法描述对象"能干什么"
构造方法:创建对象时初始化;方法名和类名相同、不写返回类型;一旦写了有参构造,无参构造就没了
this:区分字段和同名参数
一个文件一个公开类:public 类名必须和文件名一致
掌握这些,就能用"类"把相关的数据和操作组织起来了.下一期我们将学习分包、注释与访问控制——讲清楚类与类之间怎么组织、谁能访问谁.