一、前言
记录时间 [2024-05-17]
系列文章简摘:
面向对象 04:三大特性之——封装,封装的含义和作用,以及在 Java 中的使用方式,附完整的测试案例代码
面向对象 05:三大特性之——继承,继承在 Java 中的相关使用,区分关键字 super 和 this,方法重写的注意点
面向对象 07:抽象类相关知识,抽象类的基本概念,使用方式,以及一些注意事项
面向对象 08:接口的相关知识,接口的基本特性、作用、使用方法等,以及普通类 / 抽象类 / 接口三者的区分
更多 Java 相关文章,请参考专栏哦。
本文是对 Java 中的关键字——static 的相关补充,主要包括静态变量、静态方法、静态代码块、静态导入包,以及静态内部类。
在 Java 中,static
关键字是一个非常重要的概念,它主要用于内存管理和访问权限的控制。下面详细介绍 static
关键字的几个关键应用场景和含义。
二、静态变量
静态变量(Static Variables)是 Java 中的一种特殊类型的变量。
1. 相关概念
特点
- 类级别:静态变量,也称为类变量,使用
static
关键字进行修饰,属于类级别,而不是实例级别。无论创建类的多少个实例,所有实例共享同一个静态变量的副本,修改一个实例中的静态变量会影响其他实例。 - 存储位置:在 Java 8 之后,静态变量存储在 Java 的永久代或元空间,而不是堆或栈中。这意味着静态变量即使在没有类实例的情况下也会存在,直到类被卸载。
- 生命周期:静态变量的生命周期与类相同,从类加载到类被卸载期间一直存在。相比之下,实例变量的生命周期与对象实例相同,存在于从对象创建到对象被垃圾回收期间。
- 访问方式:静态变量可以通过类名直接访问,不需要创建类的实例。例如,如果有一个类
Example
,它有一个静态变量staticVar
,可以通过Example.staticVar
来访问它。
使用场景
- 常量:静态变量常用于定义类的常量,如配置参数、数学常数等,这样可以在不创建对象的情况下访问这些值。
- 计数器:如果需要追踪类的实例数量或其他与类相关的统计信息,可以使用静态变量作为计数器。
- 单例模式:在实现单例设计模式时,静态变量常用来持有该类的唯一实例,确保全局访问点。
- 全局设置:当需要为应用程序提供全局设置或配置时,静态变量可以作为一个方便的存储位置。
注意事项
静态变量初始化
静态变量的初始化发生在类加载阶段,且只会初始化一次。静态变量的初始化顺序遵循它们在代码中出现的顺序。
线程安全
由于静态变量是共享资源,当多个线程同时访问并修改静态变量时,需要注意同步问题,以避免数据不一致。
2. 案例分析
静态变量 / 非静态变量
静态变量和非静态变量(也称为实例变量)在 Java 中有着本质的区别,以下通过一个具体例子来说明这两种变量的不同之处。
假设我们有一个员工信息管理类 Employee
,它包含员工的基本信息。
public class Employee {
// 非静态变量(实例变量)
public int num;
// 静态变量(类变量):所有员工共享的公司名称
public static String companyName = "TechCorp";
}
通过测试类进行测试:
- 其中,
num
属性是非静态的,通过对象调用; companyName
属性是静态的,通过类名调用。
public static void main(String[] args) {
// 创建员工实例
Employee emp1 = new Employee();
// 调用静态变量
// 修改公司名称,这是一个静态变量的修改,影响所有员工实例
Employee.companyName = "NewTechCorp";
// 调用非静态变量
emp1.num = 100;
}
计数器
假设我们想跟踪一个类的实例被创建了多少次,可以使用静态变量作为计数器。
在这个例子中,instanceCount
是一个静态变量,每当创建一个新的 CounterClass
实例时,它的值就会增加。我们不需要实例化对象就能访问这个计数值,因为它通过类名直接访问。
class CounterClass {
private static int instanceCount = 0;
public CounterClass() {
instanceCount++;
}
public static int getInstanceCount() {
return instanceCount;
}
}
// 使用示例
public class CounterTest {
public static void main(String[] args) {
CounterClass obj1 = new CounterClass();
CounterClass obj2 = new CounterClass();
System.out.println("Number of instances created: " + CounterClass.getInstanceCount()); // 输出:2
}
}
3. 一些区别
静态变量和非静态变量主要存在以下区别。
存储位置与共享性
- 静态变量:静态变量存储在方法区的静态区,所有类的实例共享这一份内存空间,因此当修改静态变量时,所有实例都会看到这个变化。
- 非静态变量:非静态变量是每个实例独有的,存储在堆内存中,每个对象有自己独立的这两项数据。
生命周期
- 静态变量:随着类的加载而创建,随着类的卸载而销毁,与任何对象实例的生命周期无关。
- 非静态变量:随着对象的创建而存在,随着对象的销毁而消失,其生命周期与对象实例绑定。
访问方式
- 静态变量:可以通过类名直接访问,无需创建类的实例,如
Employee.companyName
。 - 非静态变量:必须通过类的实例来访问,如
emp1.num
。
作用域
- 静态变量:属于类,作用于整个类及所有对象。
- 非静态变量:属于对象,作用于对象实例内部。
三、静态方法
静态方法(Static Methods)是面向对象编程中的一种方法类型,使用 static
关键字进行修饰。
1. 相关概念
特点
- 类级别而非对象级别:静态方法属于类本身,而不属于类的任何特定实例。不需要创建类的对象就可以调用静态方法。
- 访问方式:静态方法通过类名直接调用,如
ClassName.staticMethodName()
,而不是通过对象实例。 - 内存中单一副本:静态方法在内存中只有一份实例,所有对该方法的调用共享这同一份代码。
- 不能直接访问非静态成员:静态方法内部不能直接访问非静态属性(实例变量)或非静态方法,因为这些属于特定对象实例,而静态方法不依赖于任何实例。
- 常用于工具方法或通用功能:静态方法通常用于执行不依赖于对象状态的操作,如数学运算、字符串处理或者与类状态有关的操作。
使用场景
静态方法主要适用于那些不依赖于对象状态、需要全局访问或执行逻辑相对独立的情况。
- 工具类或辅助函数:静态方法非常适合用于创建工具类,其中包含不依赖于类实例的实用方法。例如,字符串处理、数学计算、日期格式化等。这类方法可以直接通过类名调用,无需实例化对象。
- 常量或计算:当有一些计算逻辑或常量与类的实例无关,且不依赖于类的状态时,可以将其定义为静态方法或静态变量。例如,定义一个类来表示圆周率
PI
,其中PI
就是一个静态常量,而基于此计算圆的面积可以用静态方法实现。 - 单例模式:在实现单例设计模式时,通常会使用一个静态方法来获取类的唯一实例。这样做的好处是可以保证系统中该类只有一个对象,并且可以全局访问。
- 通用功能封装:如果有些功能或操作在多个类中通用,可以考虑将这些功能封装到一个类的静态方法中,便于复用。例如,日志记录、缓存管理等。
- 系统初始化与配置:在程序启动或系统初始化时,可能需要执行一些一次性或配置性的操作,这些操作适合定义为静态方法,因为它们通常与任何特定对象实例无关。
- 工厂方法:静态工厂方法是一种设计模式,用于代替构造函数来创建对象。它可以提供更多的灵活性,如返回子类实例、处理复杂对象创建逻辑等,而无需直接暴露构造函数。
- 入口点方法:在某些框架或应用中,静态方法作为程序的入口点,如 Java 的
main
方法,它是程序开始执行的地方,不依赖于任何特定对象实例。
2. 案例分析
静态方法 / 非静态方法
静态方法与非静态方法(实例方法)在 Java 中有着明显的区别,下面通过一个具体的例子来详细说明这两种方法的不同之处。
假设我们有一个图书管理类 Book
,它包含图书相关信息。
- 静态方法:获取图书馆名称
getLibraryName()
- 非静态方法:获取书籍详情
getBookDetails()
public class Book {
// 非静态变量(实例变量):每本书都有独立的标题和作者
private String title;
private String author;
// 静态变量(类变量):所有书籍共享的图书馆名称
public static String libraryName = "City Central Library";
// 构造方法
public Book(String title, String author) {
this.title = title;
this.author = author;
}
// 非静态方法:获取书籍详情
public String getBookDetails() {
return "Title: " + title + ", Author: " + author;
}
// 静态方法:获取图书馆名称
public static String getLibraryName() {
return libraryName;
}
}
通过测试类进行测试:
- 访问静态方法:通过类名
- 访问非静态方法:通过实例对象
public static void main(String[] args) {
// 创建两本书的实例
Book book1 = new Book("To Kill a Mockingbird", "Harper Lee");
Book book2 = new Book("1984", "George Orwell");
// 访问非静态方法
System.out.println(book1.getBookDetails()); // 输出:Title: To Kill a Mockingbird, Author: Harper Lee
System.out.println(book2.getBookDetails()); // 输出:Title: 1984, Author: George Orwell
// 访问静态方法
System.out.println(Book.getLibraryName()); // 输出:City Central Library
// 修改静态变量
Book.libraryName = "New Library";
System.out.println(book1.getLibraryName()); // 输出:New Library
System.out.println(book2.getLibraryName()); // 输出:New Library
}
计算圆的面积
假设我们要创建一个 Circle
类来计算圆的面积,其中 π
是一个常数,我们可以将其作为静态字段,而计算面积的方法可以设计为静态方法,因为计算过程并不依赖于圆的具体实例。
public class Circle {
// 静态变量:π 的值,所有圆共用
public static final double PI = 3.14159;
// 静态方法:计算圆的面积
public static double calculateArea(double radius) {
// 面积公式 A = π * r^2
return PI * Math.pow(radius, 2);
}
}
// 主函数中调用静态方法
class Main {
public static void main(String[] args) {
// 给定圆的半径
double radius = 5.0;
double area = Circle.calculateArea(radius);
System.out.println("The area of the circle with radius " + radius + " is: " + area);
}
}
在这个例子中:
calculateArea
是一个静态方法,它接受圆的半径作为参数并返回圆的面积。- 调用这个方法时,我们不需要创建
Circle
类的实例,直接通过Circle.calculateArea(radius)
即可完成计算。 - 同时,
PI
作为圆周率的值,被声明为一个静态常量,体现了静态变量的使用场景。
3. 一些区别
静态方法与非静态方法主要存在以下区别。
调用方式
- 非静态方法需要通过类的实例来调用,如
book1.getBookDetails()
。 - 静态方法可以直接通过类名调用,无需创建类的实例,如
Book.getLibraryName()
。
访问权限
- 非静态方法可以访问实例变量和静态变量。
- 静态方法只能访问静态变量,不能直接访问实例变量,因为静态方法不属于任何特定实例。
生命周期与存储
- 非静态变量和非静态方法与对象实例绑定,每个对象实例有自己的副本,随对象创建而生,随对象销毁而亡。
- 静态变量和静态方法属于类,由类加载器加载到内存,所有对象共享,仅在类首次加载时初始化,直到程序结束或类被卸载才释放。
内存分配
- 非静态成员占用的空间随对象实例的数量成比例增长。
- 静态成员在整个类的生命周期内只占用一份固定的内存空间。
四、静态代码块
1. 代码块 / 构造方法
静态代码块
静态代码块在 Java 中是一种特殊的代码结构,用于在类加载时执行初始化操作。
相关特点:
- 执行时机:静态代码块在类被加载到内存时执行,且只执行一次。静态代码块中的代码会在任何对象实例化之前运行,也不需要创建类的实例。
- 初始化静态资源:静态代码块常用于初始化类的静态成员变量,比如设置静态变量的初始值,或者执行与类相关的、只需一次的初始化配置。
- 执行顺序:如果一个类中有多个静态代码块,它们将按照在类中出现的顺序依次执行。这对于需要按特定顺序进行初始化的场景非常有用。且静态代码块的执行优先于构造函数和非静态初始化块。
应用场景:
- 资源初始化:比如初始化数据库连接池、读取配置文件、设置日志系统等,这些操作通常只需要在应用启动时进行一次。
- 静态变量赋值:给类的静态成员变量赋予复杂的初始值,特别是那些需要执行一定逻辑才能确定的值。
- 执行一次性任务:比如注册驱动、检查环境配置等。
代码应用:
在这个例子中,当 MyClass
被加载时,静态代码块会立即执行,设置 appName
并打印一条消息,同时调用 readVersionFromConfig
方法来初始化 version
。之后,无论创建多少个 MyClass
的实例,静态代码块都不会再次执行。
public class MyClass {
// 静态变量
public static String appName;
public static int version;
// 静态代码块
static {
appName = "My Application";
System.out.println("Static block executed.");
// 假设这里还有其他复杂的初始化逻辑,比如从配置文件读取版本号
version = readVersionFromConfig();
}
// 静态方法读取版本号
private static int readVersionFromConfig() {
// 这里模拟从配置文件读取版本号的逻辑
return 1;
}
public static void main(String[] args) {
System.out.println("Application Name: " + MyClass.appName);
System.out.println("Version: " + MyClass.version);
}
}
匿名代码块
匿名代码块(也称为实例代码块或初始化块)是在 Java 中位于类成员变量声明之后、构造器之前的花括号包围的代码块。它不是静态的,意味着每次创建类的新实例时都会执行一次,常用于初始化非静态成员变量。
匿名代码块相比于直接在构造器中初始化变量,提供了更灵活的初始化逻辑和优先级控制。
相关特点:
- 执行时机:每当创建类的一个新对象时,匿名代码块会在对应的构造器之前执行。如果有多个匿名代码块,按照它们在类中出现的顺序执行。
- 初始化顺序:匿名代码块的执行顺序优于构造器中的初始化代码,但晚于静态代码块。可以使用匿名代码块来设定实例变量的初始状态,即使这些状态依赖于构造器参数。
- 灵活性:对于需要在不同构造器中执行相同或相似初始化逻辑,但逻辑较为复杂或不适合直接放入构造器时,匿名代码块提供了一个清晰的组织方式。
- 实例相关:匿名代码块操作的是非静态成员,因此它与特定的对象实例绑定,允许根据实例的具体情况执行不同的初始化操作。
代码应用:
在这个例子中,当创建 Person
类的一个实例时,首先执行匿名代码块,设置 greeting
变量并打印消息。
public class Person {
String name;
int age;
String greeting;
// 匿名代码块
{
greeting = "Hello, ";
System.out.println("匿名代码块执行了!");
}
public static void main(String[] args) {
Person person = new Person();
}
}
构造方法
构造方法(Constructor)主要用于初始化新创建的对象。构造方法的名字必须与它所在的类名完全相同,并且没有返回类型,甚至 void
也不需要。当创建一个类的新实例时,构造方法会被自动调用。
每当使用 new
关键字创建类的实例时,相应的构造方法会被自动执行。
例如,下面的例子展示了如何定义默认构造方法和带有参数的构造方法,以及它们如何在创建对象时被调用。
public class Student {
String name;
int age;
String major;
// 默认构造方法
public Student() {
System.out.println("默认构造方法被调用");
}
// 参数化构造方法
public Student(String name, int age, String major) {
this.name = name;
this.age = age;
this.major = major;
System.out.println("参数化构造方法被调用");
}
public static void main(String[] args) {
// 使用默认构造方法创建对象
Student student1 = new Student();
// 使用参数化构造方法创建对象
Student student2 = new Student("Alice", 20, "Computer Science");
}
}
2. 三者的执行顺序
在实例化对象时,程序会按顺序执行:
- 静态代码块(只执行一次)
- 匿名代码块
- 构造方法
例如,我们在 User
类中同时编写静态代码块、匿名代码块,以及构造方法。
public class User {
/*
实例化对象时,执行顺序
1. 静态代码块
2. 匿名代码块
3. 构造方法
其中:静态代码块只执行一次
*/
// 第一个执行
static {
System.out.println("静态代码块");
}
// 第二个执行
{
System.out.println("匿名代码块");
}
// 第三个执行
public User() {
System.out.println("构造方法");
}
}
测试它们的执行顺序:
public static void main(String[] args) {
System.out.println("User 类第一次实例化");
User user1 = new User();
System.out.println("User 类第二次实例化");
User user2 = new User();
}
得到结果:
静态代码块 # 静态代码块最先执行,且只在第一次实例化时执行
User 类第一次实例化
匿名代码块
构造方法
User 类第二次实例化
匿名代码块
构造方法
五、静态导入包
静态导入包(Static Import)是 Java 语言中的一个特性,它允许直接访问另一个类中的静态成员(包括静态方法和静态变量),而无需显式地指定所属类名。
通过此,我们可以像使用本地方法一样直接调用静态方法,或者直接使用静态变量,而不需要类名作为前缀。
1. 基本语法
静态导入的语法是在 import
语句前加上关键字 static
,有两种形式的静态导入。
导入单个静态成员
// 导入单个静态成员
import static 包名.类名.静态成员;
导入所有静态成员
// 导入所有静态成员
import static 包名.类名.*;
2. 使用示例
假设要频繁使用 java.lang.Math
类中的静态方法,如 Math.PI
和 Math.sin()
,一般情况下,我们会这样调用:
double result = Math.sin(Math.PI / 2);
使用静态导入后,可以直接这样写:
import static java.lang.Math.PI;
import static java.lang.Math.sin;
// ...
double result = sin(PI / 2);
或者,如果导入所有静态方法:
import static java.lang.Math.*;
// ...
double result = sin(PI / 2);
3. 注意事项
- 静态导入虽然可以简化代码,提高可读性,但过度使用可能导致命名冲突和代码的可读性下降,特别是当导入了很多同名静态成员时。
- 选择性地导入所需的静态成员,保持代码清晰,减少潜在的命名冲突。
- 静态导入应该谨慎使用,确保它们确实提高了代码的清晰度和简洁性,而不是仅仅为了省略类名前缀。
六、静态内部类
静态内部类(Static Nested Class)是 Java 中的一种特殊类型的内部类,它使用 static
关键字进行修饰。
- 当一个内部类被声明为静态时,它被称为静态内部类或嵌套类。
- 静态内部类不依赖于外部类的实例,可以直接通过外部类名访问,就像访问静态变量一样。
- 静态内部类可以有静态成员,包括静态变量和静态方法。
先做简单了解,在下篇内部类中进行补充。
七、总结
本文是对 Java 中的关键字——static 的相关补充,主要包括静态变量、静态方法、静态代码块、静态导入包,以及静态内部类。
一些参考资料
狂神说 Java 零基础:https://www.bilibili.com/video/BV12J41137hu/
TIOBE 编程语言走势: https://www.tiobe.com/tiobe-index/
Typora 官网:https://www.typoraio.cn/
Oracle 官网:https://www.oracle.com/
Notepad++ 下载地址:https://notepad-plus.en.softonic.com/
IDEA 官网:https://www.jetbrains.com.cn/idea/
Java 开发手册:https://developer.aliyun.com/ebook/394
Java 8 帮助文档:https://docs.oracle.com/javase/8/docs/api/