Loading...

【面试真题拆解】Java的Static关键字到底怎么用?

面试官上来就问:

“ 那个Java里面的Static关键字,一般是怎么用?”

一句话:

Static是“类级别的”,不是“实例级别的”。

被Static修饰的东西,属于整个类,所有实例共用一份,不用new对象就能直接用;

而没被Static修饰的东西,属于单个实例,每个实例有自己的一份,必须new对象才能用。

对比项 静态(Static) 实例(非Static)
归属 属于整个类 属于单个实例
内存位置 方法区(JDK8 之后叫元空间) 堆内存
访问方式 类名.(变量/方法),或(实例.变量/方法)(不推荐) 必须通过实例.变量/方法访问
共享性 所有实例共用一份 每个实例有自己的一份
初始化时机 类加载时初始化 创建实例时初始化
访问限制 只能直接访问静态成员 可以访问静态成员和实例成员

1. 静态变量

静态变量就像是整个类共享的“公共储物柜”,所有实例共用这一个柜子,你放进去的东西,别人也能拿到;而实例变量是每个实例自己的“私人储物柜”,互不干扰。

举个例子,用静态变量做全局计数器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
 * 网站访问计数器
 */
public class WebsiteCounter {
    // 静态变量:所有实例共用,统计总访问次数
    public static int totalVisitCount = 0;
    // 实例变量:每个实例自己的,统计单个用户的访问次数
    public int userVisitCount = 0;

    /**
     * 访问方法
     */
    public void visit() {
        // 修改静态变量:总访问次数+1
        totalVisitCount++;
        // 修改实例变量:单个用户访问次数+1
        userVisitCount++;
    }

    public static void main(String[] args) {
        // 用户1访问
        WebsiteCounter user1 = new WebsiteCounter();
        user1.visit();
        user1.visit();
        System.out.println("用户1访问次数:" + user1.userVisitCount); // 输出2
        System.out.println("总访问次数:" + WebsiteCounter.totalVisitCount); // 输出2

        // 用户2访问
        WebsiteCounter user2 = new WebsiteCounter();
        user2.visit();
        System.out.println("用户2访问次数:" + user2.userVisitCount); // 输出1
        System.out.println("总访问次数:" + WebsiteCounter.totalVisitCount); // 输出3
    }
}

2. 静态方法

静态方法是属于类的“公共工具”,不用new对象,直接通过【类名.方法名】就能调用,而实例方法是属于单个实例的,必须new对象之后才能调用。

举个例子,用静态方法做工具类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
 * 字符串工具类
 */
public class StringUtils {
    // 私有构造方法:从根源上禁止外部代码 new StringUtils()
    // 工具类全是静态方法,完全不需要实例化,这样做能强制大家用[类名.方法名]的正确用法
    private StringUtils() {}

    /**
     * 静态方法:判断字符串是否为空
     */
    public static boolean isEmpty(String str) {
        return str == null || str.trim().isEmpty();
    }

    /**
     * 静态方法:字符串反转
     */
    public static String reverse(String str) {
        if (isEmpty(str)) {
            return str;
        }
        return new StringBuilder(str).reverse().toString();
    }

    public static void main(String[] args) {
        // 直接通过类名调用静态方法,不用new对象
        System.out.println(StringUtils.isEmpty("")); // 输出true
        System.out.println(StringUtils.reverse("hello")); // 输出olleh
    }
}

3. 静态代码块

静态代码块是类加载的时候执行一次,且只执行一次的代码块,用来初始化静态变量或者做一些类级别的准备工作(比如加载配置文件、初始化数据库连接池)。

举个例子,用静态代码块初始化静态配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
 * 配置类
 */
public class Config {
    // 静态变量:数据库连接地址
    public static String dbUrl;
    public static String dbUsername;
    public static String dbPassword;

    // 静态代码块:类加载时执行一次,初始化静态变量
    static {
        System.out.println("静态代码块执行,开始初始化配置...");
        // 模拟从配置文件读取配置
        dbUrl = "jdbc:mysql://localhost:3306/test";
        dbUsername = "root";
        dbPassword = "123456";
    }

    // 实例代码块:创建实例时执行,每次new都执行
    {
        System.out.println("实例代码块执行...");
    }

    // 构造方法:创建实例时执行,每次new都执行
    public Config() {
        System.out.println("构造方法执行...");
    }

    public static void main(String[] args) {
        // 第一次访问Config类,触发类加载,静态代码块执行
        System.out.println("数据库地址:" + Config.dbUrl);
        System.out.println("---");
        // new第一个实例,实例代码块和构造方法执行
        new Config();
        System.out.println("---");
        // new第二个实例,实例代码块和构造方法执行,但静态代码块不再执行
        new Config();
    }
}

小贴士

静态代码块只在类第一次被加载时执行一次,之后不管new多少个实例,都不会再执行。

4. 静态内部类

静态内部类是属于外部类的“独立房间”,不用依赖外部类的实例就能直接创建,而非静态内部类是属于外部类实例的“附属房间”,必须先有外部类实例才能创建。

静态内部类的作用是:

利用 Java 的类加载时机,实现了懒加载(用的时候才创建)+ 线程安全(JVM 保证)+ 不用加锁(性能高)的完美单例。

举个例子,用静态内部类实现线程安全的单例模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * 单例模式:静态内部类实现
 */
public class Singleton {
    // 私有构造方法:从根源上禁止外部代码直接 new Singleton()
    private Singleton() {}

    // 静态内部类:只有第一次被访问时才会加载(实现懒加载,用的时候才创建实例,不占内存)
    // 因为是在 Singleton 类内部,所以可以访问上面的 private 构造方法
    private static class SingletonHolder {
        // 静态变量:JVM 类加载过程是天然线程安全的,这里初始化唯一的单例实例(不用加锁,性能高)
        private static final Singleton INSTANCE = new Singleton();
    }

    // 全局唯一的访问点:外部代码只能通过这里获取单例实例
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

    public static void main(String[] args) {
        // 获取单例实例
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        // 两个实例是同一个,完美实现单例
        System.out.println(instance1 == instance2); // 输出true
    }
}

小贴士

  • 静态变量存放在JVM的方法区(元空间),不是堆内存。
  • 静态变量可以通过【类名.变量名】直接访问,也可以通过【实例.变量名】访问(不过,不推荐,因为容易混淆)。
  • 因为所有线程共用同一个静态变量,多线程同时修改会出现并发问题,需要加锁保证线程安全。
  • 因为实例变量和方法依赖具体的实例,静态方法调用时可能还没new对象,所以,静态方法只能直接访问静态变量和静态方法,不能直接访问实例变量和实例方法。
  • this 代表当前实例,静态方法属于类,调用时可能还没创建实例,this 根本不存在,所以,静态方法不能用 this。
  • 静态内部类只能直接访问外部类的静态变量和静态方法,不能直接访问实例变量和实例方法。
Code Road Record