一、静态变量(Static Variables)
1、静态变量是什么
静态变量,又被称为类变量,它使用 static
关键字修饰。静态变量是类级别的变量,在类加载时初始化。
2、静态变量的特点
类级别共享:静态变量在所有实例间共享,所有对象访问的是同一个静态变量。
内存管理:生命周期与类一致,从类加载到类卸载。静态变量在类加载时分配内存,直到程序结束才释放。
可以通过类名直接访问:静态变量是属于整个类的,所以它不需要实例就可以访问,直接使用类名进行访问。例如 ClassName.staticVariable
。
类级别共享:静态变量在所有实例间共享,所有对象访问的是同一个静态变量。
3、静态变量的定义与声明
public class MyClass {
public static int staticVar = 0; // 静态变量
public int instanceVar = 0; // 实例变量
}
4、静态变量的使用场景
1)计数器
用于记录类被实例化的次数。
public class Counter {
public static int count = 0;
public Counter() {
count++;
}
}
2)共享资源
例如在工具类中共享某些资源或配置。
public class Utility {
private static Scanner input = new Scanner(System.in);
// 静态变量,用于工具类的共享
public static char readAChar(char[] validChars) {
//...
}
}
3)定义常量
常用static final
组合来定义常量。定义全局常量,如数学常数、配置信息等。
public class Constants {
public static final double PI = 3.141592653589793;
}
5、静态变量的存储位置
在JDK 8,Java对内存管理进行了变更。
方法区被移除,取而代之的是元空间 (Metaspace)。对于方法区是在JVM中的,而对于元空间,则是位于本地内存中的。
因此,静态变量和常量池不再存储在永久代(永久代是方法区的实现)中,而是迁移到了堆内存中。这些改变是为了改善内存管理,避免永久代的内存限制问题。
所以说:
JDK 8 之前:
静态变量存储在方法区中。JDK 8 及之后:
永久代被元空间取代。静态变量迁移到堆内存中。元空间用于存储类的元数据,位于本地内存中而非JVM堆中。
二、静态方法(Static Methods)
1、静态方法是什么
静态方法(又叫类方法)是使用static
关键字声明的方法。它们属于类本身,可以在无需创建类实例的情况下调用。
Math.random();
我们可以想到调用 Math 类中的 random() 方法,直接使用类名就可以调用这个方法,所以这个方法是静态方法。
2、静态方法特点
1)特点
无需实例化:可以通过类名直接调用静态方法。
访问限制:静态方法只能直接访问其他静态成员(变量和方法),不能访问实例成员。
不能使用this
和super
关键字:因为静态方法不属于任何实例。
多态性限制:静态方法不支持运行时多态,只能进行静态绑定。静态方法不能被重写,因为静态方法是与类相关联的,不依赖于具体实例。因此,不能通过子类的实例来覆盖父类的静态方法。
2)补充
如果在子类中定义了与父类静态方法同名的方法,这不是重写,而称为方法隐藏。调用时,方法的选择取决于引用变量的类型(也就是说取决于编译时类型,所以这就是静态绑定),而不是对象的实际类型(运行时类型)。
public class Example {
public static void main(String[] args) {
SuperClass sc = new SubClass();
sc.staticMethod();
}
}
class SuperClass {
public static void staticMethod() {
System.out.println("SuperClass staticMethod~");
}
}
class SubClass extends SuperClass{
public static void staticMethod() {
System.out.println("SubClass staticMethod~");
}
}
运行结果:
3、静态方法的最佳实践
1)工具类方法
如java.lang.Math
类的方法,提供数学运算功能。
double result = Math.sqrt(25.0); // 调用静态方法
不仅是Math这种标准库中提供的方法类,我们自己实现的方法类中的方法一般也是使用静态方法。
2)工厂方法
返回类的实例的静态方法。
public class Factory {
public static MyClass createInstance() {
return new MyClass();
}
}
3)主方法
Java程序的入口点是静态方法main
。
public static void main(String[] args) {
// 程序入口
}
三、main 方法详解
1、main 方法介绍
在Java中,main
方法是程序的入口点。它是一个特殊的方法,Java虚拟机(JVM)在运行程序时会首先调用它。
2、main 方法的定义格式
public static void main(String[] args)
这是 main 方法的定义格式,但是,它的定义格式为什么是这样的呢?
-
public: 访问修饰符,表示该方法可以被任何地方调用。因为JVM需要调用
main
方法,因此必须是public
。 -
static: 静态修饰符,意味着该方法属于类而不是类的实例。JVM无需创建类的对象即可调用
main
方法。 -
void: 返回类型,
main
方法不返回任何值。 -
main: 方法名,必须是
main
,因为这是JVM识别的入口点。 -
String[] args: 参数,
args
是一个String
数组,用于接收命令行参数。用户可以在运行程序时向main
方法传递参数。
3、main 方法的参数
我们可以通过命令行传递参数给 main 方法,我们先写一段程序:
public class Arguments {
public static void main(String[] args) {
for(String arg : args) {
System.out.println("Argument: " + arg);
}
}
}
然后我们在命令行中编译这个代码:
然后运行程序,同时传入几个参数,可以看到运行结果是:
这里我们给 main 方法传入了三个参数:
Hello World !
三个字符串。然后这三个字符串通过我们在 main 方法中写的遍历参数数组的语句被打印出来。
4、在 IDEA 中给 main 方法传参
运行结果:
三、代码块(Block)
1、代码块介绍
1)代码块是什么
在Java中,代码块又称为初始化块,有些情况下初始化比较复杂,需要将其封装在代码块中。他没有名字,不能通过对象或者类名调用。一般是加载类时或者创建对象时隐式调用。
代码块分为实例代码块和静态代码块。它们用于在特定情况下执行特定的初始化操作。
2)代码块定义格式
// 前面可以有修饰符可选,但也仅限 static 修饰符
[static]{
// 语句
}
2、实例代码块(Instance block)
1)介绍
实例代码块在创建对象时执行。每次创建对象时,都会执行实例代码块的内容。如果只是使用类的静态成员,实例代码块并不会执行。
实例代码块中可以访问实例成员,也可以访问静态成员。
- 执行时机:在每次创建对象时执行,优先于构造函数执行。
- 使用场景:可以用于所有构造函数中共享的初始化代码,避免代码重复。所以可以将实例代码块理解成构造器的补充机制,用来辅助构造器来初始化对象。
2)示例
public class Block {
public static void main(String[] args) {
new Test();
new Test(1);
}
}
class Test {
private int x;
{
System.out.print("实例代码块:x被初始化为 ");
}
public Test() {
this.x = 0;
System.out.println(this.x);
System.out.println("Test()构造器");
}
public Test(int x) {
this.x = x;
System.out.println(this.x);
System.out.println("Test(int x)构造器");
}
}
运行结果:
3、静态代码块(Static block)
1)介绍
静态代码块用于初始化静态变量,只在类加载时执行一次。可以用于复杂的静态变量初始化或执行需要的配置信息。
静态代码块只能访问静态成员。
特点:
-
类加载时执行:静态代码块在类首次加载到JVM时执行,且只执行一次。
-
顺序执行:如果有多个静态代码块,按照在类中出现的顺序依次执行。
用途:
-
静态变量复杂初始化:当静态变量的初始化不只是简单的赋值时,可以使用静态代码块。
-
加载资源或配置:在类加载时执行一次性操作,如加载配置文件。
2)类的加载
在Java中,一个类在一次程序运行中通常只会被加载一次。类加载的具体机制由类加载器管理,确保每个类在运行期间仅加载一次。
类的加载会由以下行为触发:
-
创建类的实例:使用
new
关键字创建对象时。 -
访问类的静态成员(静态变量或静态方法):此时类会被加载,以便访问这些静态成员。例如包含 main 方法的类在程序启动时加载。因为程序启动时,JVM 会调用静态方法 main 方法。
-
通过反射机制:使用
Class.forName()
或其他反射API。 -
初始化子类:当子类被初始化时,父类也会被加载和初始化。
如果程序中没有触发这些行为,类就不会被加载。
3)示例
public class Block {
public static void main(String[] args) {
System.out.println("Example.staticVar = " + Example.staticVar);
}
}
class Example {
public static int staticVar;
static {
System.out.println("静态代码块");
}
}
运行结果:
4、代码块的执行顺序
1)静态、实例代码块和构造器的执行顺序
在一个类没有加载时,创建此类的第一个对象的调用顺序是:
- 调用静态代码块和静态属性初始化。(静态代码块和静态属性初始化执行的优先级是一样的,如果有多个静态代码块和多个静态属性初始化,则按照它们定义的顺序执行)
- 调用实例代码块和实例属性初始化。(普通代码块和普通属性初始化的执行优先级也是一样的,如果有多个实例代码块和多个实例属性初始化,则按照它们定义的顺序执行)
- 调用构造方法。
示例:
public class Example {
public static void main(String[] args) {
new Test();
}
}
class Test {
private static int staticVar1 = initStaticVar1();
static {
System.out.println("静态代码块 1");
}
static {
System.out.println("静态代码块 2");
}
private static int staticVar2 = initStaticVar2();
private int instanceVar1 = initInstanceVar1();
{
System.out.println("实例代码块 1");
}
{
System.out.println("实例代码块 2");
}
private int instanceVar2 = initInstanceVar2();
public Test() {
System.out.println("构造器");
}
public static int initStaticVar1() {
System.out.println("staticVar1 被初始化");
return 10;
}
public static int initStaticVar2() {
System.out.println("staticVar2 被初始化");
return 10;
}
public int initInstanceVar1() {
System.out.println("instanceVar1 被初始化");
return 20;
}
public int initInstanceVar2() {
System.out.println("instanceVar2 被初始化");
return 20;
}
}
运行结果:
可以看到执行顺序与我们前面提到的一致,优先级一样的执行顺序由它的定义的顺序决定。
2)构造器中的执行顺序
对于一个类的构造器,它首应当是显示或者隐式调用 super() 也就是父类的构造器,然后就是执行子类的实例代码块,然后才是子类构造器的主体。
-
调用父类构造器:创建子类对象时,首先调用父类构造器(通过隐式或显式的
super()
调用)。 -
执行实例代码块:在父类构造器执行完成后,子类的实例代码块被执行。实例代码块的执行并不依赖于构造器的显式调用,而是自动插入在父类构造器调用后和子类构造器主体执行前。
-
执行构造器:最后,执行子类的构造器主体。
示例:
public class Example {
public static void main(String[] args) {
new SubClass();
}
}
class SuperClass {
public SuperClass() {
System.out.println("父类构造器");
}
}
class SubClass extends SuperClass{
{
System.out.println("实例代码块");
}
public SubClass() {
// super(); 首先调用父类构造器
// 执行实例代码块
System.out.println("子类构造器");
}
}
运行结果:
3)创建一个子类的执行顺序
在子类和父类都没有加载时,创建第一个子类对象,执行顺序为:
- 父类的静态代码块和静态属性初始化。(优先级一样,按定义的顺序执行)
- 子类的静态代码块和静态属性初始化。(优先级一样,按定义的顺序执行)
- 父类的实例代码块和实例属性初始化。(优先级一样,按定义的顺序执行)
- 父类的构造器。(取决于子类调用的是哪一个)
- 子类的实例代码块和实例属性初始化。(优先级一样,按定义的顺序执行)
- 子类的构造器。
示例:
public class Example {
public static void main(String[] args) {
new SubClass();
}
}
class SuperClass {
static {
System.out.println("父类的静态代码块");
}
{
System.out.println("父类的实例代码块");
}
public SuperClass() {
System.out.println("父类的构造器");
}
}
class SubClass extends SuperClass {
static {
System.out.println("子类的静态代码块");
}
{
System.out.println("子类的实例代码块");
}
public SubClass() {
System.out.println("子类的构造器");
}
}
运行结果:
可以看到运行结果与我们上面提到的执行顺序一致。
参考资料:
Java学习笔记9---类静态成员变量的存储位置及JVM的内存划分_jdk8静态变量放在jvm哪里-CSDN博客