十五、static关键字

15.1 概述

static 关键字可以用来修饰成员变量和成员方法,被修饰的成员是属于类的,而不是单单是属于某个对象的。也就是说,既然属于类,就可以不靠创建对象来调用了。

关于 static 关键字的作用可以用一句话来描述:**方便在没有创建对象的情况下进行调用,包括变量和方法。**也就是说,只要类被加载了,就可以通过类名进行访问。

小贴士:
static 关键字,可以修饰变量、方法和代码块。在使用的过程中,其主要目的还是想在不创建对象的情况 下,去调用方法。Arrays 和 Math 两个工具类,就可以来体现 static 方法的便利。

15.2 静态变量

在声明变量的时候使用 static 关键字,那么这个变量就被称为静态变量。静态变量只在类加载的时候获取一次内存空间,这就使得静态变量很节省内存空间。

把一个变量声明为静态变量通常基于几个目的:

  1. 作为共享变量使用;
  2. 减少对象的创建;
  3. 保留唯一副本。

01、使用

有一个学生类 Student.java

/**
 * @author QHJ
 * @date 2022/9/20  08:58
 * @description: 学生类
 */
public class Student {
    String name;
    int age;
    String school = "郑州大学";
}

引出一个问题:假设 “郑州大学” 录取了一万名新生,那么在创建一万个 Student 对象的时候,所有的字段(name、age、school)都会获取到一块内存,也就是说可能要获取一万块内存。虽然学生的姓名和年纪不尽相同,但都是属于 " 郑州大学" 的,如果每创建一个对象,school 这个字段都要占用一块内存的话,就会造成浪费。

因此,最好将 school 这个字段设置为 static,这样就会只占用一块内存,而不是一万块:

/**
 * @author QHJ
 * @date 2022/9/20  08:58
 * @description: 学生类
 */
public class Student {
    String name;
    int age;
    static String school = "郑州大学";

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public static void main(String[] args) {
        Student student1 = new Student("青花椒", 22);
        Student student2 = new Student("青花椒", 25);
    }
}

这样的话,内存分布是这样的:student1 和 student2 这两个引用变量存放在栈区(stack),青花椒22 这个对象和 青花椒25 这个对象存放在堆区(heap),school 这个静态变量存放在静态区。
在这里插入图片描述

02、栗子

  • 非静态成员变量

    /**
     * @author QHJ
     * @date 2022/9/20  09:54
     * @description: static
     */
    public class Counter {
        int count = 0;
    
        Counter() {
            count++;
            System.out.println(count);
        }
    
        public static void main(String[] args) {
            Counter counter1 = new Counter();
            Counter counter2 = new Counter();
            Counter counter3 = new Counter();
        }
    }
    

    输出结果:
    1
    1
    1

    创建一个成员变量 count,并且在构造函数中让它自增。因为成员变量会在创建对象的时候获取内存,因此每一个对象都会有一个 count 副本,count 的值并不会随着对象的增多而递增。每创建一个 Counter 对象,count 的值就从 0 自增到 1。

  • 静态成员变量

    /**
     * @author QHJ
     * @date 2022/9/20  10:26
     * @description: 静态成员变量
     */
    public class StaticCounter {
        static int count = 0;
    
        StaticCounter() {
            count++;
            System.out.println(count);
        }
    
        public static void main(String[] args) {
            StaticCounter staticCounter1 = new StaticCounter();
            StaticCounter staticCounter2 = new StaticCounter();
            StaticCounter staticCounter3 = new StaticCounter();
        }
    }
    

    输出结果:
    1
    2
    3

    由于静态变量只会获取一次内存空间,所以任何对象对它的修改都会得到保留,所以每创建一个对象,count 的值就会加 1,所以最终的结果是 3。这也就是静态变量和成员变量之间的差别。

    另外,由于静态变量属于一个类,所以不要通过对象引用来访问,而应该直接通过类名来访问,否则编译器会发出警告:
    在这里插入图片描述

15.3 静态方法

在方法上加 static 关键字,它就是静态方法。将一个方法声明为静态方法,通常是为了方便在不创建对象的情况下使用。

静态方法有以下几个特征:

  1. 静态方法属于这个类而不是这个类的对象;
  2. 调用静态方法的时候不需要创建这个类的对象;
  3. 静态方法可以访问静态变量。
/**
 * @author QHJ
 * @date 2022/9/20  10:36
 * @description:
 */
public class StaticMethodStudent {
    String name;
    int age;
    static String school = "郑州大学";

    public StaticMethodStudent(String name, int age) {
        this.name = name;
        this.age = age;
    }

    static void change() {
        school = "河南科技大学";
    }

    void out() {
        System.out.println(name + age + school);
    }

    public static void main(String[] args) {
        StaticMethodStudent.change();

        StaticMethodStudent student1 = new StaticMethodStudent("青花椒", 22);
        StaticMethodStudent student2 = new StaticMethodStudent("青花椒", 25);

        student1.out(); // 青花椒22河南科技大学
        student2.out(); // 青花椒25河南科技大学
    }
}

change() 方法是一个静态方法,所以它可以直接访问静态变量 school,把它的值更改为 “河南科技大学”;并且,可以通过类名直接调用该方法:StaticMethodStudent.change();

需要注意的是,静态方法不能访问非静态变量和调用非静态方法:
JavaSE基础之(十五)static关键字-小白菜博客
JavaSE基础之(十五)static关键字-小白菜博客
静态方法调用的注意事项:

  1. 静态方法可以直接访问类变量和静态方法。
  2. 静态方法不能直接访问普通成员变量或成员方法(非静态变量和非静态方法)。
  3. 反之,成员方法可以直接访问类变量或静态方法。
  4. 静态方法中,不能使用

补充:main() 方法为什么是静态的?
如果 main() 方法不是静态的,就意味着 Java 虚拟机在执行的时候需要先创建一个对象才能调用 main() 方法,而 main() 方法作为程序的入口,创建一个额外的对象是非常多余的,

15.4 静态代码块

静态代码块通常来说是为了对静态变量进行一些初始化操作,比如单例模式、定义枚举类。

01、使用

定义在成员位置,使用一个 static 关键字,外加一个大括号括起来的代码被称为静态代码块。静态代码块通常用来初始化一些静态变量,它会优先于 main() 方法执行:

/**
 * @author QHJ
 * @date 2022/9/20  11:02
 * @description: 静态代码块
 */
public class StaticBlock {
    static {
        System.out.println("静态代码块");
    }

    public static void main(String[] args) {
        System.out.println("main方法");
    }
}

输出结果:
静态代码块
main方法

既然静态代码块优先于 main() 方法执行,那没有 main() 方法的 Java 类能执行成功吗?
Java 1.6 是可以的,但是从 Java 7 开始就无法执行了:
在这里插入图片描述

02、作用

给类变量进行初始化赋值。

public class Game {   
	public static int number;   
	public static ArrayList<String> list;     
	static {     
		// 给类变量赋值     
		number = 2;     
		list = new ArrayList<String>();    
		// 添加元素到集合中     
		list.add("张三");     
		list.add("李四");   
	} 
}
/**
 * @author QHJ
 * @date 2022/9/20  11:08
 * @description: 静态代码块中初始化集合
 */
public class StaticBlockDemo {
    public static List<String> writes = new ArrayList<>();

    static {
        writes.add("青花椒1");
        writes.add("青花椒2");
        writes.add("青花椒3");

        System.out.println("part1");
    }

    static {
        writes.add("青花椒4");
        writes.add("青花椒5");
        writes.add("青花椒6");

        System.out.println("part2");
    }
}

writes 是一个静态的 ArrayList,所以不太可能在声明的时候完成初始化,因此需要在静态代码块中完成初始化。

静态代码块在初始集合的时候非常有用。在实际的项目开发中,通常使用静态代码块来加载配置文件到内存当中。

15.5 静态内部类

01、使用

Java 允许我们在一个类中声明一个,它提供了一种令人信服的方式,允许我们只在一个地方使用一些变量,使代码更具有条理性和可读性。

/**
 * @author QHJ
 * @date 2022/9/20  13:34
 * @description: 静态内部类
 */
public class Singleton {
    private Singleton() {
    }

    private static class SingletonHolder {
        public static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

第一次加载 Singleton 类时并不会初始化 instance,只有第一次调用 getInstance() 方法时 Java 虚拟机才开始加载 SingletonHolder 并初始化 instance,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。

需要注意的是,静态内部类不能访问外部类的所有成员变量;静态内部类可以访问外部类的所有静态变量,包括私有静态变量;外部类不能声明为 static。

02、静态内部类与非静态内部类的区别

非静态内部类对象持有外部类对象的引用(编译器会隐式地将外部类对象的引用作为内部类的构造器参数);而静态内部类对象不会持有外部类对象的引用;

由于非静态内部类的实例创建需要有外部类对象的引用,所以非静态内部类对象的创建必须依托于外部类的实例;而静态内部类的实例创建只需依托外部类;

并且由于非静态内部类对象持有了外部类对象的引用,因此非静态内部类可以访问外部类的非静态成员;而静态内部类只能访问外部类的静态成员;

两者的根本性区别其实也决定了用 static 去修饰内部类的真正意图:

  1. 内部类需要脱离外部类对象来创建实例;
  2. 避免内部类使用过程中出现内存溢出。

15.6 static变量和普通成员变量的区别

  1. 所属不同。static 变量属于类,不单属于任何对象;普通成员变量是属于某个对象的。
  2. 存储区域不同。static 变量位于方法区;普通成员变量位于堆区。
  3. 生命周期不同。static 变量生命周期与类的生命周期相同;普通成员变量和其所属的对象的生命周期相同。
  4. 在对象序列化时,static 变量会被排除在外(因为 static 变量是属于类的,不属于对象)。

15.6 静态原理图解

static 修饰的内容:

  • 是随着类的加载而加载的,且只加载一次。
  • 存储于一块固定的内存区域(静态区),所以,可以直接被类名调用。
  • 它优先于对象存在,所以,可以被所有对象共享。
    在这里插入图片描述