面试官上来就问:
“ 那个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。
- 静态内部类只能直接访问外部类的静态变量和静态方法,不能直接访问实例变量和实例方法。