static
关键字的作用就是:方便在没有创建实例的情况下来进行调用(方法/变量)。也就是说,被static
关键字修饰的方法或者变量不需要创建实例去调用,直接根据类名就可以去访问。
1. static
关键字修饰的类
用static
修饰内部类,普通类是不允许声明为静态的,只有内部类才可以:
public class StaticTest {
// static关键字修饰内部类
public static class InnerClass {
InnerClass() {
System.out.println("===========静态内部类===========");
}
public void innerMethod() {
System.out.println("===========静态内部方法===========");
}
}
public static void main(String[] args) {
// 直接通过StaticTest类名访问静态内部类InnerClass
InnerClass inner = new StaticTest.InnerClass();
// 静态内部类可以和普通类一样使用
inner.innerMethod();
}
}
//===========静态内部类===========
//===========静态内部方法===========
如果没有用static
修饰InnerClass
,则只能new
一个外部类实例,再通过外部类实例创建内部类。
另外,静态内部类不持有外部类的引用,是因为其构造函数内没有传入外部类的引用。
public class OutClass {
public static long OUTER_DATE = System.currentTimeMillis(); // 1
static {
System.out.println("外部类静态块加载" + System.currentTimeMillis()); // 1
}
public OutClass() {
timeElapsed();
System.out.println("外部类构造函数" + System.currentTimeMillis()); // 2
}
static class InnerStaticClass {
public static long INNER_STATIC_DATE = System.currentTimeMillis(); // 4
}
class InnerClass {
public long INNER_DATE = 0;
public InnerClass() {
timeElapsed();
INNER_DATE = System.currentTimeMillis(); // 3
}
}
// 扩大时间差
private void timeElapsed() {
for (int i = 0; i < 100000000; i++) {
int a = new Random(100).nextInt(), b = new Random(100).nextInt();
a = a + b;
}
}
public static void main(String[] args) {
OutClass outer = new OutClass();
System.out.println("外部类静态变量加载时间:" + OutClass.OUTER_DATE);
System.out.println("非静态内部类加载时间:" + outer.new InnerClass().INNER_DATE);
System.out.println("静态内部类加载时间:" + InnerStaticClass.INNER_STATIC_DATE);
}
}
//外部类静态块加载1626405022945
//外部类构造函数1626405025424
//外部类静态变量加载时间:1626405022945
//非静态内部类加载时间:1626405027582
//静态内部类加载时间:1626405027583
由此可知,静态内部类和非静态内部类一样,都是在被调用时才会被加载。
public class OutClass {
public static long OUTER_DATE = System.currentTimeMillis();
public static int a = 1;
static {
System.out.println("外部类静态块加载" + System.currentTimeMillis());
}
public OutClass() {
timeElapsed();
System.out.println("外部类构造函数" + System.currentTimeMillis());
}
static class InnerStaticClass {
static {
System.out.println("内部类静态块加载时间:" + System.currentTimeMillis());
}
public static long INNER_STATIC_DATE = System.currentTimeMillis();
}
public static void hello() {
System.out.println("Hello");
}
class InnerClass {
public long INNER_DATE = 0;
public InnerClass() {
timeElapsed();
INNER_DATE = System.currentTimeMillis();
}
}
// 扩大时间差
private void timeElapsed() {
for (int i = 0; i < 100000000; i++) {
int a = new Random(100).nextInt(), b = new Random(100).nextInt();
a = a + b;
}
}
public static void main(String[] args) {
// System.out.println("外部类静态变量加载时间:" + OutClass.OUTER_DATE);
OutClass.hello();
OutClass outer = new OutClass();
System.out.println("外部类静态变量加载时间:" + OutClass.OUTER_DATE);
}
}
//外部类静态块加载1626406024641
//Hello
//外部类构造函数1626406027103
//外部类静态变量加载时间:1626406024641
调用外部类的静态变量、静态方法可以让外部类得到加载,但是不能让静态内部类被加载。
public class OutClass {
public static long OUTER_DATE = System.currentTimeMillis();
public static int a = 1;
static {
System.out.println("外部类静态块加载" + System.currentTimeMillis());
}
public OutClass() {
timeElapsed();
System.out.println("外部类构造函数" + System.currentTimeMillis());
}
static class InnerStaticClass {
static {
System.out.println("内部类静态块加载时间:" + System.currentTimeMillis());
}
public static long INNER_STATIC_DATE = System.currentTimeMillis(); /
}
public static void hello() {
System.out.println("Hello");
}
class InnerClass {
public long INNER_DATE = 0;
public InnerClass() {
timeElapsed();
INNER_DATE = System.currentTimeMillis(); // 3
}
}
// 扩大时间差
private void timeElapsed() {
for (int i = 0; i < 100000000; i++) {
int a = new Random(100).nextInt(), b = new Random(100).nextInt();
a = a + b;
}
}
public static void main(String[] args) {
System.out.println("内部类静态变量加载时间" + InnerStaticClass.INNER_STATIC_DATE);
System.out.println("外部类静态变量加载时间" + OutClass.OUTER_DATE);
}
}
//外部类静态块加载1626406319875
//内部类静态块加载时间:1626406319876
//内部类静态变量加载时间1626406319876
//外部类静态变量加载时间1626406319875
加载静态内部类的时候,还是会先加载外部类,才加载静态内部类。
静态内部类不会自动初始化,只有调用静态内部类的成员变量、方法,静态域或者构造方法的时候才会加载静态内部类。
2 static
关键字修饰方法
static
修饰方法称作静态方法,由于静态方法不依赖于任何对象就可以访问,因此对于静态方法来说,是没有this
的,因为它不依附于任何对象,既然没有对象,就谈不上this
了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。但是,虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的。
public class Person {
private String str1 = "property";
private static String str2 = "static_property";
public Person() {
}
public void print1() {
System.out.println(str1);
System.out.println(str2);
print2();
}
public static void print2() {
// System.out.println(str1); // 报错
System.out.println(str2);
// print1(); // 报错
}
}
修饰方法是,其实跟类一样,可以直接通过类名来进行调用:
public class StaticTest {
public static void test() {
System.out.println("=========静态方法==========");
}
public static void main(String[] args) {
// 方式一:直接通过类名
StaticTest.test();
// 方式二:
StaticTest staticTest = new StaticTest();
staticTest.test();
}
}
//=========静态方法==========
3 static
关键字修饰代码块
static
关键字还有一个比较关键的作用就是用来生成静态代码块以优化程序性能。static
块可以置于类中的任何地方,类中可以有多个static
块。在类被初次加载的时候,会按照static
块的顺序来执行每一个static
块,并且只会执行一次。为什么说static
块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次:
public class Person {
private long birthDate;
public Person(long birthDate) {
this.birthDate = birthDate;
}
boolean isBornBoomer() {
long startDate = Date.parse("1946-1-1");
long endDate = Date.parse("1964-12-31");
return birthDate > startDate && birthDate < endDate;
}
}
isBornBoomer
是用来判断这个人是否是1946-1964
年出生的,而每次isBornBoomer
被调用的时候,都会生成startDate
和endDate
着两数值,造成空间浪费,改成这样效率会更好:
public class Person {
private long birthDate;
private static long startDate, endDate;
static {
startDate = Date.parse("1946-1-1");
endDate = Date.parse("1946-12-31");
}
public Person(long birthDate) {
this.birthDate = birthDate;
}
boolean isBornBoomer() {
return birthDate > startDate && birthDate < endDate;
}
}
因此,很多时候会将一些只需要进行一次的初始化操作都放在static
代码块中进行。
验证一下类初始化的顺序:父类静态变量、父类静态代码块、子类静态变量、子类静态代码块、父类普通变量,父类普通代码块,父类构造函数,子类普通变量,子类普通代码块,子类构造函数
public class Father {
static {
System.out.println("Father static");
}
public Father() {
System.out.println("Father constructor");
}
}
public class Son extends Father {
static {
System.out.println("Son Static");
}
public Son() {
System.out.println("Son constructor");
}
public static void main(String[] args) {
new Son();
}
}
//Father static
//Son Static
//Father constructor
//Son constructor
4 static
关键字修饰变量
static
变量也称为静态变量也叫类变量,说明这个变量是属于这个类的,而不是属于对象,没有被static
修饰的变量成员叫做实例变量,这个变量是属于某个具体的对象的。
静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅单在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
static
成员变量的初始化顺序按照定义的顺序进行初始化。
public class StaticTest {
private static String name = "java的架构技术栈";
public static void main(String[] args) {
String name = StaticTest.name;
}
}
静态变量存放在方法区中,被所有线程所共享的。 以下是内存图解:
堆区
- 存储的全部是对象
JVM
只有一个堆区(heap
)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身
栈区
- 每个线程包含一个栈区,栈中只保存原始数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
- 每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问
方法区
- 又叫静态区,跟堆一样,被所有线程共享。
- 方法区中包含的都是在整个程序中永远唯一的元素,如
static
变量
比如以下代码:
public class Person {
static String firstName;
String lastName;
public void showName() {
System.out.println(firstName + lastName);
}
public static void showFirstName() {
System.out.println(firstName);
}
public static void main(String[] args) {
Person p = new Person();
Person.firstName = "萧";
p.lastName = "峰";
p.showName();
Person p2 = new Person();
Person.firstName = "段";
p2.lastName = "誉";
p2.showName();
}
}
内存模型:
从上面可以看出,静态方法是从方法区调用的。堆内存中的成员变量lastName
是随着对象的产生而产生,随着对象的消失而消失。
通过以下代码来具体分析:
public class Person {
String name;
int age;
static String country;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person(String name, int age, String c) {
this.name = name;
this.age = age;
country = c;
}
public void show() {
System.out.println(name + "---" + age + "---" + country);
}
}
class PersonDemo {
public static void main(String[] args) {
Person p1 = new Person("邓丽君", 16, "中国");
p1.show();
Person p2 = new Person("林青霞", 22);
p2.show();
Person p3 = new Person("张曼玉", 20);
p3.show();
Person.country = "美国";
p3.show();
p1.show();
p2.show();
}
}
//邓丽君---16---中国
//林青霞---22---中国
//张曼玉---20---中国
//张曼玉---20---美国
//邓丽君---16---美国
//林青霞---22---美国
当程序运行时,.class
文件进行加载,这时在方法区会有.class
文件区:
而.class
文件区内会有两个区,代码中有两个类,一个是Person
,一个是PersonDemo
:
.class
区域内有成员属性和成员方法,而对于静态属性和静态方法,系统会自动的把它们分配到静态区(方法内有一个静态区)。静态区也有两块区域一个是PersonDemo
类标记的main
方法,一个是Person
标记的变量country
(String
类型出事默认值为null
):
得出结论:静态的内容随着.class
文件加载而加载。
接着main
方法进栈,main
是静态修饰,静态方法可以通过类名调用,所以,根本不需要创建对象,main
方法一进栈系统就会为它开辟空间:
接着它创建了第一个对象Person p1 = new Person("邓丽君", 16, "中国");
:
然后new Person()
在进入堆内存之后系统会为它分配空间和一个内存地址,假如是:0X0001
,同时这块内存区域内有name = nulll; age = 0;
、静态标记和方法标记:
静态标记是: 静态区域有一个变量country
,它有一个内存地址,假如是0X01
,它是跟静态标记是有关联的,这个时候静态标记是指向静态区的country
的:
方法标记: 系统在创建对象的时候,会自动生成一个方法区,这个方法区是属于当前对象的Person
,而这个方法区也有一个内存地址,假如是0X02
;构造方法与show
方法等方法都存储在方法区内,它是由Person
标记的内存地址是0X02
,这是堆内存中的方法标记则与方法区产生关联,直到这时它所有的初始化才完成。
当进行完以上步骤,它的值又被改变了:Person p1 = new Person("邓丽君",16,"中国");
,这是给name,age,country
进行赋值,通过走构造方法进内存,邓丽君
给了name
,16
给了age
。
这时找静态变量country = 中国
,但此时在堆内存中并没有这个静态变量,那么找静态标记0X01
,通过内存地址0X01
找到静态区0X01
标记的country
,这时null
则被中国
替代。
截止到此时Person p1 = new Person("邓丽君",16,"中国")
的值才赋完。
运行到p1.show()
方法:
public void show() {
System.out.println(name + "---" + age + "---" + country);
}
在方法区找有没有show
方法,则show
方法进栈,为其开辟一个空间;那么show
方法在里面输出的时候首先找name
,因为是p1
调用,所以通用Person p1
找name
值,再找age
值,再通过静态标注进入静态区找country
值,所以第一次输出结果是:
当这些值在控制台显示之后,然后这个方法调用完毕就从内存中消失:
接着运行Person p2 = new Person("林青霞", 22);
,步骤Person p2
则在站内存开辟出第二个空间并给出内存地址0X0002
;
然后new Person()
在堆内存开辟空间。然后Person p2 = new Person("林青霞",22)
将林青霞
、22
赋值给name
、age
,此时并没有给country
值,但是它是有值的,因为他是静态变量,它的静态标记0X01
指向静态区域0X01
,它的值还是以前的,但是由它们两个共享一个静态区域所以不需要再次进行赋值。然后找到p2.show()
方法.使show()
进栈,然后show()
方法通过Person p2 -> 0X0002
进堆内存通过name
、age
找值,再通过静态标记找country
的值。然后进行输出销毁内存。p3
同理:
在当运行到p3.country = 美国
;时,美国
通过Person p3
进堆,通过静态标记对country
的值进行更改,因为所有方法共享一个静态区所以p3.show();
、p1.show();
、p2.show();
,输出的country
值都是美国。
5 总结
5.1 特点
static
是一个修饰符,用于修饰成员(变量,函数,内部类,代码块)。static
修饰的成员变量称之为静态变量或类变量static
修饰的成员被所有的对象共享,而普通变量是对象特有的数据static
优先于对象存在,因为static
的成员随着类的加载就已经存在static
修饰的成员可以直接被类名所调用(类名.静态成员
)
5.2 成员变量和静态变量的区别
- 生命周期的不同:成员变量随着对象的创建而存在,随着对象的回收而释放;静态变量随着类的加载而存在,随着类的消失而消失
- 调用方式不同:成员变量被对象调用;静态变量可以被类名调用
- 别名不同:成员变量也称为实例变量;静态变量成为类变量
- 数据存储位置不同:成员变量数据存储在堆内存的对象中,所以也叫对象的特有数据;静态变量数据存储在方法区(共享数据区)的静态区,所以也叫对象的共享数据
5.3 静态使用时需要注意的事项
- 静态方法只能访问静态成员。(非静态既可以访问静态,又可以访问非静态)
- 静态方法中不可以使用
this
或者super
关键字 - 主函数是静态的
5.4 常见误区
5.4.1 static
关键字会改变类中成员的访问权限吗
Java
中的static
关键字不会影响到变量或者方法的作用域。在Java
中能够影响到访问权限的只有private
、public
、protected
(包括访问权限)这几个关键字。
public class StaticTest {
public static void main(String[] args) {
// System.out.println(new Person().name); // 报错
System.out.println(new Person().age);
}
}
class Person {
private String name = "哈哈";
public int age = 10;
}
5.4.2 能通过this
访问静态成员变量吗?
虽然对于静态方法来说是没有this
,那么在静态方法中能够通过this
访问静态成员变量吗?
public class StaticTest {
static int value = 33;
public static void main(String[] args) {
new StaticTest().print(); // 33
}
public void print() {
int value = 3;
System.out.println(this.value);
}
}
这里面主要考察this
和static
的理解,this
代表什么?this
代表当前对象,那么通过new StaticTest()
来调用print
的话,当前对象就是通过new StaticTest()
生成的对象。而static
变量是被对象所共享的, 因此在print
中的this.value
的值毫无疑问是33
。在print
方法内部的value
是局部变量,根本不可能与this
关联,所以输出结果是33
。这里要注意,静态成员变量虽然独立于对象,但是不代表不可以通过对象区访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)。
5.4.3 static
能作用于局部变量吗
在Java
中,static
是不允许用来修饰局部变量,这是语法规定。
5.4.4 static
与final
的对比
static
关键字在修饰变量时,会使该变量在类加载时就被初始化,不会因为对象的创建再次被加载,当变量被static
修饰时就代表该变量只会被初始化一次:
public class StaticTest {
public static double i = Math.random();
public final double j = Math.random();
public static void main(String[] args) {
StaticTest staticTest1 = new StaticTest();
StaticTest staticTest2 = new StaticTest();
System.out.println(staticTest1.i == staticTest2.i); // true
System.out.println(staticTest1.j == staticTest2.j); // false
}
}
参考
https://baijiahao.baidu.com/s?id=1636927461989417537&wfr=spider&for=pc
https://blog.csdn.net/weixin_43610698/article/details/90734373
https://blog.csdn.net/yibin18566767255/article/details/79763791
https://www.cnblogs.com/maohuidong/p/7843807.html