【Java关键字】理解static关键字

前言

关于Java关键字,笔者打算有些结合具体Java基础一起写,如transient关键字就写在你真的有好好了解过序列化吗:Java序列化实现的原理里面了,有些就单独拿出来介绍,如static、final等。本文主要对static关键字进行介绍:

  • static修饰成员变量
  • static修饰成员方法
  • static静态块
  • static静态导包
  • static静态内部类

(若文章有不正之处,或难以理解的地方,请多多谅解,欢迎指正)

1. static修饰成员变量

static最常用的功能就是修饰类的属性和方法,让他们成为类的成员属性和方法,通常我们称static修饰的成员成为类成员、静态成员或静态变量。

那么静态变量和非静态变量有什么区别呢?举个栗子:

public class Tangyuan {
    private static String skin;
    private String stuffing;
    @Override
    public String toString() {
        return "Tangyuan{ skin='"+ skin + "',stuffing='" + stuffing + "'}";
    }
    public static void main(String[] args){
        Tangyuan.skin = "糯米皮";
        Tangyuan t1 = new Tangyuan();
        t1.stuffing = "花生馅";
        Tangyuan t2 = new Tangyuan();
        t2.stuffing = "芝麻馅";
        System.out.println(t1);
        System.out.println(t2);
    }
}

输出结果为:

Tangyuan{ skin='糯米皮',stuffing='花生馅'}
Tangyuan{ skin='糯米皮',stuffing='芝麻馅'}

我们可以看到,Tangyuan(汤圆)类构造出来的对象是独立的个体,每个对象中都有保存自己独立的非静态变量,相互不会影响。而static修饰的成员变量的改变,却会影响所有对象的对应值。为了静态变量会产生这种影响呢?

这由它们在内存中的存储结构决定的(JDK1.8):
在这里插入图片描述
由上图我们可以知道,我们创建的两个对象t1和t2存储在堆上,它们的引用地址是存放在栈中的,而且这两个对象是相互独立的,所以图中的两个汤圆对象,分别有着不同的馅料。但是汤圆类有一个静态成员变量skin,存储在汤圆对象都可以访问的地方。

在堆上会开辟一块内存空间,专门用于存储已加载的方法、static修饰的成员变量等,称为静态存储区。也就是说skin变量存储在汤圆类在静态存储区的内存中。虽然我们看到static可以让对象共享属性,但是实际上是不推荐这么用的,因为这样会让属性变得能以控制,因为它在任何地方都有可能被改变。

PS:本文是采用的是JDK1.8时运行时数据区域的结构。在JDK1.6时,静态存储区是存放在方法区中的,而且方法区不属于堆;但在JDK1.7时,方法区中的静态存储区搬到堆中了;且在JDK1.8之后,方法区被“元空间”所取代。

总结一下,static修饰的成员变量的生命周期与类的生命周期相同,不随对象的消亡而消亡,但是可以被所有对象共享,在内存中只有一个副本。我们可以通过**“类名.静态变量”“对象名.静态变量”这两种方式来直接调用静态变量。而非静态变量是对象独有的属性,非静态变量随着对象的消亡而消亡**,各对象所拥有的副本不受影响。

2. static修饰成员方法

当然,如果用static关键字修饰成员方法,可以直接通过**“类名.静态方法名()”“对象名.静态方法名()”**的方式来调用:

public class Tangyuan {
    ......
    public static void printTangyuan(Tangyuan tangyuan){
        System.out.println(tangyuan);
    }
    public static void main(String[] args){
        ......
        t1.printTangyuan(t1);
        Tangyuan.printTangyuan(t2);
    }
}

运行结果为:

Tangyuan{ skin='糯米皮',stuffing='花生馅'}
Tangyuan{ skin='糯米皮',stuffing='芝麻馅'}

static修饰成员方法最大的用处在于,可以直接使用“类名.方法名”的方式操作方法,避免要先new出一个对象的资源消耗。

需要注意的是,因为静态方法是直接可以通过类名来调用的,所以在静态方法的世界里,是不存在对象的,即在静态方法中不能有this与super关键字。而且因为这个特性,在静态方法中不能访问类的非静态成员变量和方法,因为非静态成员变量/方法都必须依赖具体的对象才有意义,才能被调用。不然即使存在实例化的对象,也不清楚调用的是哪一个对象的非静态成员变量/方法。

总结一下,静态方法只能访问所属类的静态成员变量/方法,而非静态方法可以访问静态成员变量/方法。

3. static修饰静态块

static修饰的静态代码块可以用于优化程序性能,而且可以置于类中的任何地方,一个类中可以有多个静态代码块。在类的初次加载时,会按照静态代码块的顺序来执行每一个静态代码块,而且只会执行一次。

为什么说静态代码块可以优化程序性能呢?正是因为其特性:只能在类加载的时候执行一次。比如某个比较大的配置文件需要在创建对象的时候加载,这时候为了节省内存,我们可以将该配置文件的加载放到静态代码块中,这样,无论我们创建多少个对象,该配置文件也就加载了一次。

举个栗子:

public class Person {
    private Date date;
    public Person(Date date) {
        this.date = date;
    }
    boolean isBornIn90s() {
        Date startDate = Date.valueOf("1990");
        Date endDate = Date.valueOf("1999");
        return date.compareTo(startDate) >= 0 && date.compareTo(endDate) < 0;
    }
}

可以看到,如果我想看看这汤圆是不是芝麻馅时,每次都需要创建一个“芝麻馅”的字符串对象,这样就比较浪费资源,如果这样子改效率会比较高:

public class Person {
    private Date date;
    private static Date startDate, endDate;
    static {
        startDate = Date.valueOf("1990");
        endDate = Date.valueOf("1999");
    }
    public Person(Date date) {
        this.date = date;
    }
    boolean isBornIn90s() {
        return date.compareTo(startDate) >= 0 && date.compareTo(endDate) < 0;
    }
}

而关于静态代码块、构造代码块、构造函数、普通代码块的执行顺序可以参考笔者这篇博客:Java的成员变量、局部变量(深入版)

4. static静态导包

静态导包是JDK1.5以后的特性,用*import static com…ClassName.代替传统的import com…ClassName方式,意思是导入这个类里的静态方法,好处就是可以简化一些操作,相对于上述的三个功能来说,比较冷门。举个栗子:

静态导包之前:

public class Tangyuan {
    public static void main(String[] args){
        System.out.println("我想吃花生馅的汤圆");
    }
}

静态导包之后:

import static java.lang.System.out;
public class Tangyuan {
    public static void main(String[] args){
        out.println("我想吃花生馅的汤圆");
    }
}

不同于非static静态导包,采用static静态导包之后,无需使用“类名.方法名”的方式来调用类方法了,可以直接使用“方法名”去调用类方法,就好像是该类自己的方法一样使用即可。但是,静态导包只是减少了程序员的代码量,对于性能是没有任何提升的,反而会降低代码的可读性,所以需要权衡使用。

5. 静态内部类

内部类就是定义在类的内部的类,而相对的外部类就是包含内部类的类,用static修饰的内部类便是将要介绍的静态内部类。定义内部类的好处在于,外部类可以访问内部类的所有方法和属性,包括私有方法和私有属性。

如果我们要访问普通内部类,我们需要先创建外部类的对象,然后通过外部类名.new的方式创建内部类的实例

class OutClass {
    public class InnerClass{
        private String name = "InnerName";
    }
    public void getInnerName(){
        InnerClass innerClass = new OutClass().new InnerClass();
        System.out.println(innerClass.name);
    }
}
public class Test{
    public static void main(String[] args){
        OutClass oc = new OutClass();
        oc.getInnerName();
    }
}

访问静态内部类,我们不需要创建外部类的对象,而是直接通过外部类名.内部类名来创建实例。

class OutClass {
    public static class InnerClass{
        private String name = "InnerName";
    }
    public void getInnerName(){
        InnerClass innerClass = new InnerClass();
        System.out.println(innerClass.name);
    }
}
public class Test{
    public static void main(String[] args){
        OutClass oc = new OutClass();
        oc.getInnerName();
    }
}

结语

写这篇文章的时候,进度很慢。因为杰伦的歌虽然很好听*10,但有时候不知道他唱什么,只能边看歌词边写…

如果觉得文章不错,请点一个赞吧,这会是我最大的动力~

参考资料:

Java中static关键字的作用与用法

Java关键字(三)——static

static关键字的四种用法

posted @ 2020-04-14 17:36  NYfor2018  阅读(191)  评论(0编辑  收藏  举报