本期是Java开发教程系列的第五期,本期主要向各位介绍封装和 static 静态成员——把数据保护起来,并认识"属于类而不属于对象"的成员.若你已经熟悉 getter/setter 和 static,可以跳过本期内容.
每期叠甲:
时间仓促,水平有限,文中难免存在缺点和错误,恳请广大开发者和从业者指正.
一、封装:private + getter/setter
第 4 期学了 private——只有本类内部能访问.但光把字段设为 private 还不够,外面完全读不到也不行.封装的典型写法是:字段私有,再用公开的方法来读写
public class Student {
private String name; // 字段私有
private double score;
public Student(String name, double score) {
this.name = name;
this.score = score;
}
// getter:读值
public String getName() {
return name;
}
public double getScore() {
return score;
}
// setter:改值,可以在里面做校验
public void setScore(double score) {
if (score < 0) {
System.out.println("成绩不能为负数,拒绝修改");
return; // 直接结束方法,不修改
}
this.score = score;
}
}
现在外面不能直接写 s.score = -80 了(编译报错),只能通过 setScore() 改——而 setScore() 里拦住了负数.数据就安全了.
命名约定:字段叫 score,getter 叫 getScore(),setter 叫 setScore();boolean 字段的 getter 叫 isXxx().这个约定 IDE 可以一键生成.
为什么说封装是好事:
能校验:setter 里可以拦非法值,像上面的负数成绩
能改内部实现:字段将来换存储方式,只要 getter/setter 签名不变,外面代码不用动
缩小影响面:别人只依赖你公开的方法,内部怎么改都不怕
二、static 静态成员
static 修饰的成员属于类而不是某个对象.看例子:
public class Student {
public static String school = "第一中学"; // 静态字段
public String name; // 实例字段
public static void welcome() { // 静态方法
System.out.println("欢迎来到 " + school);
}
}
Student.welcome(); // 直接调,不需要 new
- 静态方法里不能直接用非静态的字段和方法(因为没有"某个对象"可指);反过来,非静态方法可以用静态成员
什么时候用 static:
工具方法:像 Math.abs(),做一件和任何对象都无关的事
常量:public static final 一起用,表示一个不变的全局值
共享状态:像上面的 school,所有对象要共用
现在你回头看 System.out.println(...):System 是一个类,out 是它一个 public static 的字段——因为静态,不用创建对象,直接用 System.out 就能拿到.
static 与内存:静态成员不参与垃圾回收
讲完用法,还得说一个和内存有关的注意事项——static 字段不会被垃圾回收.
回忆一下:实例字段是跟着对象走的,对象没人用了,整个对象(连同它的实例字段)会被垃圾回收(垃圾回收是 JVM 自动做的事:没人再用的对象,占的内存会被收回去,这个机制后面我们会专门学).但静态字段属于类,而类从加载进 JVM 的那一刻起就一直活着,直到程序结束.所以:
这不一定是坏事(常量本来就该常驻),但有一个经典坑:把会无限增长的东西放进 static 集合:
public class GlobalData {
// 全程序共享的日志列表,每来一条就 add 一条,只增不减
public static List<String> logs = new ArrayList<>();
}
logs 是 static,垃圾回收器永远认为它"还在用"——里面的每一条日志都回收不掉.运行一天两天看不出来,运行几个月,这个 List 越来越大,内存被悄悄吃掉,程序越来越慢.这就是最常见的内存泄漏之一:对象没用了却回收不掉.
三条使用建议:
static 放常量(public static final 的数值、字符串)没问题:值小、数量固定,常驻内存也合理
static 集合要当心:要么限制大小(满了清旧的),要么别用 static 装会增长的数据
static 大对象(大数组、大缓存)动手前先问一句:它真的需要活到程序结束吗?不需要,就别用 static
三、小项目:受保护的成绩
把封装和 static 串起来,做一个带校验的成绩管理.
文件一 Student.java:
public class Student {
private String name;
private double score;
// 静态字段:所有学生共享
public static String school = "第一中学";
public Student(String name, double score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public double getScore() {
return score;
}
// setter:带校验,成绩不能为负
public void setScore(double score) {
if (score < 0) {
System.out.println(name + " 的成绩不能为负数,拒绝修改");
return;
}
this.score = score;
}
}
文件二 Main.java:
public class Main {
public static void main(String\[\] args) {
Student s1 = new Student("小明", 92);
Student s2 = new Student("小红", 58);
// 直接改私有字段会编译报错: s1.score = -10;
s1.setScore(-10); // 走 setter,被校验拦下
s1.setScore(95); // 合法,修改成功
System.out.println(s1.getName() + ":" + s1.getScore());
System.out.println(s2.getName() + ":" + s2.getScore());
// 静态字段通过类名访问,所有对象共享
System.out.println("学校:" + Student.school);
Student.school = "第二中学"; // 改一处,全部对象一起变
System.out.println(s1.getName() + " 的学校:" + Student.school);
System.out.println(s2.getName() + " 的学校:" + Student.school);
}
}
运行结果:
小明 的成绩不能为负数,拒绝修改
小明:95.0
小红:58.0
学校:第一中学
小明 的学校:第二中学
小红 的学校:第二中学
重点看两处:负数成绩被 setter 拦下;Student.school 改一次,两个学生对象读到的学校一起变——静态字段就是"所有对象共享同一份".
朝花夕拾:System.out 背后的 static
从第 1 期到现在,我们写了无数遍 System.out.println(...),它到底怎么工作的?学完 static,终于能拆开:
System 是 java.lang 包里的一个类
out 是它内部的一个 public static 字段,类型是 PrintStream(一个负责输出的类)
因为是 static,不需要 new System(),直接用 System.out 就能拿到那个输出对象
.println(...) 则是这个输出对象的方法
一行天天写的代码,背后是"静态字段 + 对象方法"的组合——这正是本期学的概念.另外,第 3 期说过 java.lang 自动导入,System 就是 java.lang 里的类,所以从第 1 期起我们写它从来没写过 import.
总结
本期完成了"把数据管起来"的关键一步:
封装:private 字段 + getter/setter,setter 里做校验;命名约定 getXxx/setXxx/isXxx\
static:属于类不属于对象;静态方法用类名直接调,静态字段所有对象共享;静态方法里不能直接用非静态成员;静态字段不参与垃圾回收,慎放会增长的数据
回头看 System.out,静态的活教材
接下来我们学习继承与多态——让类之间可以"父子相传",开始真正发挥面向对象的威力.