static 静态成员与工具类 | JavaSE

static 静态成员与工具类

一、学习目标

学完本章,你应该能够:

  • 能够解释 static 的基本含义,理解“属于类”和“属于对象”的区别。
  • 能够区分静态变量(Static Field / Class Variable)与实例变量(Instance Variable)。
  • 能够解释为什么静态变量可以被同一个类的所有对象共享。
  • 能够正确使用 类名.静态成员对象.实例成员
  • 能够区分静态方法与实例方法,并判断两者能够直接访问哪些成员。
  • 能够解释为什么静态方法中不能直接使用 this
  • 能够独立设计一个由静态方法组成的工具类(Utility Class)。
  • 能够使用静态变量完成对象数量统计等共享数据场景。

二、核心知识

2.1 为什么需要 static

先看一个没有 static 的类:

public class Student {

    String schoolName;
    String name;
}

创建三个学生:

Student s1 = new Student();
s1.schoolName = "山西大学";
s1.name = "张三";

Student s2 = new Student();
s2.schoolName = "山西大学";
s2.name = "李四";

Student s3 = new Student();
s3.schoolName = "山西大学";
s3.name = "王五";

这里:

name

显然应该属于每一个具体学生。

因为每个学生姓名不同:

张三
李四
王五

但是:

schoolName

却可能完全相同。

如果整个类的所有对象都共享同一份学校名称,那么让每个对象分别保存一份:

"山西大学"

就不符合数据本身的含义。

这时就可以使用:

static

把数据定义为:

类级别共享的数据。


2.2 static 是什么

static 是 Java 中的关键字,可以修饰类的成员。

当前阶段重点学习:

static 成员变量
static 成员方法

例如:

public class Student {

    static String schoolName;

    String name;
}

其中:

static String schoolName;

属于类。

而:

String name;

属于对象。

可以建立最基本的模型:

Student 类
│
├── static schoolName
│      整个类共享
│
├── Student 对象 s1
│      └── name
│
├── Student 对象 s2
│      └── name
│
└── Student 对象 s3
       └── name

2.3 静态变量与实例变量

成员变量根据是否具有 static 修饰,可以分成两类。

| 类型 | 是否有 static | 属于谁 | 数据特点 | | -------- | ------------- | ------ | ---------------- | | 静态变量 | 有 | 类 | 同一个类共享 | | 实例变量 | 无 | 对象 | 每个对象各自拥有 |

例如:

public class Student {

    static String schoolName;

    String name;
    int age;
}

其中:

Student.schoolName

属于:

Student 类

而:

s1.name
s1.age

属于:

s1 对象

另一个对象:

s2

则拥有自己的:

s2.name
s2.age

2.4 静态变量是共享的

观察:

public class Student {

    static String schoolName;

    String name;
}

测试:

public class Test {

    public static void main(String[] args) {

        Student.schoolName = "山西大学";

        Student s1 = new Student();
        s1.name = "张三";

        Student s2 = new Student();
        s2.name = "李四";

        System.out.println(Student.schoolName);
    }
}

静态变量:

schoolName

并不是:

s1 一份
s2 一份

而是:

Student 类共享一份

因此,如果修改:

Student.schoolName = "星雨大学";

再次访问:

System.out.println(Student.schoolName);

看到的就是修改后的共享数据。


2.5 为什么静态变量推荐使用类名访问

静态变量推荐:

Student.schoolName

而不是:

s1.schoolName

Java 语法允许在很多情况下通过对象引用访问静态成员:

s1.schoolName = "山西大学";

但是不推荐。

因为这会产生一种错误视觉:

schoolName 好像属于 s1 对象

实际上它属于:

Student 类

因此代码应该主动表达真实语义:

Student.schoolName = "山西大学";

推荐记忆:

静态成员 → 类名.静态成员

实例成员 → 对象.实例成员

2.6 静态变量什么时候使用

判断一个变量是否应该设计成 static,可以问一个问题:

这份数据应该属于每个对象各自一份,还是整个类共享一份?

例如学生:

姓名
年龄
成绩

不同对象数据不同:

private String name;
private int age;
private double score;

通常属于实例变量。

而:

学校名称
学生对象创建总数
系统统一配置

如果在当前设计中整个类只需要共享一份,就可能适合静态变量。

例如:

public class User {

    static int count;

    private String username;
}

这里:

username

属于每个用户。

但:

count

表示整个 User 类共享的统计数据。


2.7 使用 static 统计对象数量

例如:

public class User {

    static int count = 0;

    private String username;

    public User(String username) {
        this.username = username;
        count++;
    }
}

创建对象:

new User("A");
new User("B");
new User("C");

每创建一次对象:

count++;

操作的都是同一个:

User.count

因此它可以用来保存:

当前程序运行过程中创建过多少个 User 对象。

访问:

System.out.println(User.count);

这里体现的就是:

实例数据 → 每个对象独立

类级统计数据 → 所有对象共享

2.8 static 修饰成员方法

static 也可以修饰方法。

例如:

public class MathUtil {

    public static int add(int a, int b) {
        return a + b;
    }
}

这个方法叫:

静态方法(Static Method),也常称类方法。

调用:

int result = MathUtil.add(10, 20);

不需要:

MathUtil util = new MathUtil();

再:

util.add(10, 20);

因为:

add()

本身就是类级成员。


2.9 实例方法

没有 static 修饰的成员方法称为实例方法(Instance Method)。

例如:

public class Student {

    private String name;

    public void study() {
        System.out.println(name + "正在学习");
    }
}

调用:

Student student = new Student();

student.study();

实例方法需要依赖具体对象。

因为:

study()

执行时很可能需要访问这个对象自己的状态:

name

2.10 静态方法与实例方法对比

| 对比 | 静态方法 | 实例方法 | | ----------------- | ------------- | ------------- | | static | 有 | 无 | | 属于 | 类 | 对象 | | 推荐调用方式 | 类名.方法() | 对象.方法() | | 是否依赖具体对象 | 通常不依赖 | 依赖 | | 能否直接使用 this | 不能 | 可以 |

例如:

public class Student {

    static String schoolName = "山西大学";

    String name;

    public static void printSchool() {
        System.out.println(schoolName);
    }

    public void printInfo() {
        System.out.println(name);
        System.out.println(schoolName);
    }
}

调用:

Student.printSchool();

Student student = new Student();
student.name = "张三";
student.printInfo();

2.11 static 方法能直接访问什么

这是本章最重要的规则之一。

静态方法

静态方法中可以直接访问:

静态成员

例如:

public class Demo {

    static int count = 10;

    public static void show() {
        System.out.println(count);
    }
}

合法。


但是静态方法不能直接访问实例成员:

public class Demo {

    String name;

    public static void show() {
        System.out.println(name);
    }
}

这段代码会产生编译错误。

为什么?

因为:

name

属于对象。

但是:

show()

可以这样调用:

Demo.show();

此时根本没有指定:

到底是哪一个 Demo 对象的 name

假设存在:

Demo d1 = new Demo();
Demo d2 = new Demo();

那么静态方法如果直接写:

name

Java 无法判断你想访问:

d1.name

还是:

d2.name

因此不允许直接访问。


2.12 静态方法并不是永远不能访问实例变量

一定要区分:

不能直接访问实例成员。

和:

完全不能访问实例成员。

静态方法内部如果明确拥有一个对象,就可以通过对象访问:

public class Student {

    String name;

    public static void print(Student student) {
        System.out.println(student.name);
    }
}

这里:

student.name

已经明确指出:

访问的是参数 student 所引用对象的 name

因此是合法的。

真正的问题不是:

static 永远不能碰实例数据

而是:

static 上下文本身没有隐含的当前对象

2.13 实例方法能访问什么

实例方法中可以直接访问:

实例成员
静态成员

例如:

public class Student {

    static String schoolName = "山西大学";

    String name;

    public void printInfo() {
        System.out.println(name);
        System.out.println(schoolName);
    }
}

原因在于实例方法执行时存在一个具体当前对象。

所以:

name

可以确定是哪一个对象的数据。

同时静态成员本来就属于整个类,因此也可以访问。


2.14 static 方法为什么不能直接使用 this

前面已经学过:

this

代表当前对象。

例如:

public void show() {
    System.out.println(this);
}

但是:

public static void show() {
    System.out.println(this);
}

不能编译。

因为静态方法可以直接通过:

Demo.show();

调用。

调用过程中可能根本没有 Demo 对象。

既然不存在:

当前对象

自然就不存在:

this

可以总结为:

实例方法
    ↓
存在当前对象
    ↓
存在 this

静态方法
    ↓
不依赖某个当前对象
    ↓
没有 this

三、使用方法

3.1 设计共享学校名称

public class Student {

    static String schoolName = "山西大学";

    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void printInfo() {
        System.out.println(
                "姓名:" + name
                        + ",年龄:" + age
                        + ",学校:" + schoolName
        );
    }
}

测试:

public class Test {

    public static void main(String[] args) {

        Student s1 = new Student("张三", 18);
        Student s2 = new Student("李四", 20);

        s1.printInfo();
        s2.printInfo();

        Student.schoolName = "星雨大学";

        s1.printInfo();
        s2.printInfo();
    }
}

这里:

name、age

各对象独立。

而:

schoolName

所有学生共享。


3.2 设计工具类

工具类(Utility Class)通常用于提供:

与某个具体对象状态无关、可以被不同代码重复使用的通用功能。

例如:

public class ArrayUtil {

    private ArrayUtil() {
    }

    public static int max(int[] numbers) {

        int max = numbers[0];

        for (int number : numbers) {
            if (number > max) {
                max = number;
            }
        }

        return max;
    }

    public static int min(int[] numbers) {

        int min = numbers[0];

        for (int number : numbers) {
            if (number < min) {
                min = number;
            }
        }

        return min;
    }

    public static int sum(int[] numbers) {

        int sum = 0;

        for (int number : numbers) {
            sum += number;
        }

        return sum;
    }

    public static double average(int[] numbers) {
        return sum(numbers) * 1.0 / numbers.length;
    }
}

使用:

public class Test {

    public static void main(String[] args) {

        int[] numbers = {10, 30, 20, 50, 40};

        System.out.println(ArrayUtil.max(numbers));
        System.out.println(ArrayUtil.min(numbers));
        System.out.println(ArrayUtil.sum(numbers));
        System.out.println(ArrayUtil.average(numbers));
    }
}

调用者不需要:

new ArrayUtil()

而是直接:

ArrayUtil.max(numbers);

语义非常清楚:

调用 ArrayUtil 提供的数组工具功能。


3.3 为什么工具类构造器常私有化

前面的工具类:

public class ArrayUtil {

    private ArrayUtil() {
    }
}

故意把构造器声明为:

private

这样外部不能:

new ArrayUtil();

为什么?

因为这个类中没有需要保存到对象里的实例状态。

如果所有功能都通过:

ArrayUtil.xxx()

提供,那么创建:

ArrayUtil util = new ArrayUtil();

本身没有明确业务意义。

因此私有化构造器可以直接表达设计意图:

这个类只作为工具功能集合使用,不用于创建业务对象。


3.4 验证码工具类

例如:

public class VerifyCodeUtil {

    private VerifyCodeUtil() {
    }

    public static String getCode(int n) {

        String code = "";

        for (int i = 0; i < n; i++) {

            int type = (int) (Math.random() * 3);

            switch (type) {
                case 0 -> {
                    int number = (int) (Math.random() * 10);
                    code += number;
                }
                case 1 -> {
                    char letter =
                            (char) ('A' + (int) (Math.random() * 26));
                    code += letter;
                }
                default -> {
                    char letter =
                            (char) ('a' + (int) (Math.random() * 26));
                    code += letter;
                }
            }
        }

        return code;
    }
}

调用:

String code = VerifyCodeUtil.getCode(6);

System.out.println(code);

这里不需要验证码工具对象自身保存任何状态,因此设计成静态工具方法比较自然。


四、原理与进阶

4.1 “属于类”比“只有一份”更重要

初学阶段经常说:

静态变量只有一份。

这个说法有助于理解共享。

但学习时更应该首先掌握:

静态成员属于类,而不是某个具体对象。

“共享”其实是这个语义产生的结果。

所以代码:

Student.schoolName

比:

s1.schoolName

更能够表达设计本质。


4.2 static 不是“所有对象的公共变量”这么简单

不能仅仅看到:

很多对象都需要

就使用 static

例如:

name
age
balance
score

即使每个对象都有这些字段,也不能因此变成静态。

判断标准是:

这些对象应该共享同一个值,还是每个对象拥有自己的状态?

例如:

BankAccount.balance

显然不同银行账户通常有不同余额。

如果写:

static double balance;

就意味着:

所有账户共享同一个余额。

通常完全错误。


4.3 static 不代表“全局变量可以随便用”

静态变量很容易被程序中不同位置访问和修改。

例如:

public static int count;

如果设计不当,就可能导致:

  • 数据来源不清楚;
  • 修改位置过多;
  • 状态难以追踪;
  • 类之间产生隐藏耦合。

所以不能因为:

static 调用方便

就把大量业务状态全部做成静态变量。

静态变量应当服务于明确的:

类级状态
共享状态

而不是成为“到处都能访问的数据仓库”。


4.4 工具类为什么适合 static

考虑:

int max = ArrayUtil.max(numbers);

这个操作只取决于:

输入数组

而不依赖:

某个 ArrayUtil 对象自己的状态

这种函数式、无对象状态的功能就非常适合静态方法。

反过来,如果某个功能需要保存不同对象自己的状态,例如:

student.study();
account.withdraw();
player.attack();

通常更适合实例方法。


4.5 不要把“节省内存”作为工具类 static 的主要理由

工具类使用静态方法,最重要的理由不是单纯追求:

省几个对象的内存

而是:

这个行为本身不依赖某个工具对象的实例状态,因此从模型和调用语义上属于类级功能。

例如:

ArrayUtil.max(numbers);

比:

new ArrayUtil().max(numbers);

更准确地表达:

这里没有一个真正有业务意义的 ArrayUtil 对象。

这比单纯背诵“static 更省内存”更加重要。


五、实践应用

5.1 对象计数器

public class Student {

    private static int count = 0;

    private String name;

    public Student(String name) {
        this.name = name;
        count++;
    }

    public static int getCount() {
        return count;
    }

    public String getName() {
        return name;
    }
}

测试:

new Student("张三");
new Student("李四");
new Student("王五");

System.out.println(Student.getCount());

这里:

name

是对象状态。

而:

count

是类级统计状态。


5.2 为什么 main 方法是 static

Java 程序入口写作:

public static void main(String[] args) {
}

这里的:

static

意味着:

JVM 启动程序时不需要先由我们创建某个启动类对象,再调用 main。

而可以从类级入口开始执行。

初学阶段只需要理解这一层语义即可。


5.3 static 与面向对象并不冲突

有些初学者会产生误解:

学了面向对象以后是不是所有方法都必须通过对象调用?

不是。

Java 类中既可以存在:

对象级成员

也可以存在:

类级成员

关键是判断:

这个数据或行为真正属于谁?

例如:

student.study();

显然属于某个学生对象。

而:

Math.max(10, 20);

则并不需要创建一个具有独立状态的“Math 对象”。


六、常见问题

6.1 static 可以修饰什么?

本章重点掌握:

成员变量
成员方法

后续还会在其他知识中遇到更多 static 使用场景,本章不提前展开。


6.2 静态变量和实例变量最大的区别是什么?

静态变量属于:

实例变量属于:

对象

因此静态变量通常是共享状态,实例变量通常保存不同对象自己的状态。


6.3 静态变量应该用对象访问吗?

语法上某些情况下可以:

student.schoolName

但不推荐。

推荐:

Student.schoolName

因为它能准确表达:

schoolName 属于 Student 类。


6.4 静态方法必须创建对象才能调用吗?

不需要。

推荐:

ArrayUtil.max(numbers);

6.5 可以使用对象调用静态方法吗?

Java 语法允许某些形式:

util.max(numbers);

但不推荐。

应该写成:

ArrayUtil.max(numbers);

6.6 static 方法为什么不能直接访问实例变量?

因为静态方法执行时没有隐含的当前对象。

而实例变量必须依附于一个具体对象。

因此 Java 不知道应该访问哪个对象的数据。


6.7 static 方法完全不能访问对象数据吗?

不是。

只要明确拥有对象引用,就可以:

public static void print(Student student) {
    System.out.println(student.getName());
}

不能做的是:

System.out.println(name);

这种没有指定对象的直接访问。


6.8 实例方法能访问静态变量吗?

可以。

因为静态变量属于类,而实例对象本身也属于这个类。

但为了表达清晰,在一些场景中仍然可以主动写:

Student.schoolName

6.9 静态方法中为什么不能使用 this?

因为:

this

代表当前对象。

静态方法不依赖具体当前对象,因此没有 this


6.10 所有通用方法都应该 static 吗?

不是。

要判断该方法是否:

不依赖具体对象状态

如果某个方法的行为明显依赖某个对象:

account.withdraw();
student.study();
player.attack();

那么实例方法更加合理。


6.11 工具类为什么要私有构造器?

如果一个工具类没有创建对象的意义,通过:

private ToolClass() {
}

可以阻止外部无意义地创建实例。


七、练习与验收

7.1 知识问答

  1. static 是什么?
  2. static 修饰成员变量以后,该变量属于类还是对象?
  3. 什么是静态变量?
  4. 什么是实例变量?
  5. 静态变量和实例变量的共享关系有什么区别?
  6. 什么场景适合使用静态变量?
  7. 为什么学生姓名不应该轻易设计成 static?
  8. 为什么学校名称可能适合设计成 static?
  9. 静态变量推荐使用什么方式访问?
  10. 为什么不推荐 对象.静态变量
  11. 什么是静态方法?
  12. 什么是实例方法?
  13. 为什么工具类中的方法经常使用 static?
  14. 静态方法能直接访问哪些成员?
  15. 静态方法为什么不能直接访问实例成员?
  16. 静态方法能否通过对象引用访问实例成员?
  17. 实例方法能否直接访问静态成员?
  18. 为什么静态方法中不能出现 this
  19. 工具类为什么常将构造器私有化?
  20. static 是否应该被理解成一种“为了省内存的技巧”?为什么?

7.2 代码阅读

阅读:

class User {

    static int count = 0;

    String name;

    public User(String name) {
        this.name = name;
        count++;
    }
}

public class Test {

    public static void main(String[] args) {

        new User("A");
        new User("B");
        new User("C");

        System.out.println(User.count);
    }
}

不运行程序,回答:

  1. 一共创建了几个 User 对象?
  2. count 属于对象还是类?
  3. 三个对象是否分别拥有不同的 count
  4. 最后一行输出什么?
  5. 如果去掉 static,还能否使用 User.count

阅读:

class Student {

    static String school = "A大学";

    String name;

    public static void showSchool() {
        System.out.println(school);
    }

    public void showInfo() {
        System.out.println(name);
        System.out.println(school);
    }
}

回答:

  1. showSchool() 为什么可以直接访问 school
  2. 如果在 showSchool() 中直接输出 name 会发生什么?
  3. showInfo() 为什么既能访问 name 又能访问 school
  4. showSchool() 推荐通过什么方式调用?
  5. showInfo() 推荐通过什么方式调用?

7.3 手写代码

任务一:对象统计

设计:

User

要求:

  • 包含实例变量 username
  • 包含静态变量 count
  • 每创建一个对象,count 自动增加。
  • 提供静态方法查询总对象数量。
  • 创建至少五个对象进行测试。

任务二:ArrayUtil

从零设计:

ArrayUtil

要求提供:

max(int[] arr)
min(int[] arr)
sum(int[] arr)
average(int[] arr)

要求:

  • 所有功能采用静态方法。
  • 外部不能创建 ArrayUtil 对象。
  • 使用 类名.方法() 调用。
  • 不依赖 IDE 自动生成代码。

任务三:VerifyCodeUtil

设计:

VerifyCodeUtil

提供:

public static String getCode(int n)

要求:

  • 返回指定长度验证码。
  • 验证码可以由数字、大写字母、小写字母组成。
  • 工具类不能被外部实例化。
  • 编写测试类连续调用 5 次。

7.4 Debug

下面代码存在问题:

public class Student {

    private String name;

    public static void printName() {
        System.out.println(name);
    }
}

回答:

  1. 为什么不能编译?
  2. name 属于谁?
  3. printName() 属于谁?
  4. 应该如何修改设计?

再分析:

public class Tool {

    public Tool() {
    }

    public int add(int a, int b) {
        return a + b;
    }
}

业务需求:

Tool 只是提供无状态的通用计算功能。

回答:

  1. 当前为什么每次使用都可能需要创建对象?
  2. add() 是否适合改成静态方法?
  3. 构造器是否有必要公开?
  4. 修改后的调用方式应该是什么?

7.5 综合训练

设计一个:

Student

要求:

name        每个学生不同
age         每个学生不同
schoolName  全部学生共享
count       记录创建学生总数

并提供:

printInfo()
getCount()

要求你自行判断:

  • 哪些成员应该 static;
  • 哪些成员不应该 static;
  • 哪些方法应该 static;
  • 哪些方法应该是实例方法。

完成后解释每一个设计决策。


7.6 本章验收

不查看资料,检查自己是否能够:

  • [ ] 解释 static 的核心含义。
  • [ ] 区分静态变量与实例变量。
  • [ ] 判断一个数据应该属于类还是对象。
  • [ ] 正确使用 类名.静态变量
  • [ ] 正确使用 对象.实例变量
  • [ ] 独立写出静态方法。
  • [ ] 独立写出实例方法。
  • [ ] 解释静态方法为什么不能直接访问实例成员。
  • [ ] 解释实例方法为什么可以访问静态成员。
  • [ ] 解释静态方法为什么没有 this
  • [ ] 使用 static 完成对象计数。
  • [ ] 从零手写 ArrayUtil
  • [ ] 从零手写 VerifyCodeUtil
  • [ ] 正确私有化工具类构造器。

如果你只是记住:

static = 静态

但无法判断一个成员到底应该属于“类”还是“对象”,那么这一章还没有真正掌握。