static关键字的用法

在Java类里只能包含成员变量、方法、构造器、初始化块、内部类(接口和枚举),5种成员,其中除了构造器以外的四种成员都可以被static修饰,而用static修饰的成员就是类成员,类成员属于整个类不属于单个对象。

类变量

类变量属于整个类,当系统第一次准备使用类的时候,会为该类变量分配内存空间,类变量开始生效,直到该类被卸载,类变量所占有的内存才会被垃圾回收机制回收,类变量的生存范围几乎等同于该类的生存范围,类初始化完成后,类变量也被初始化完成。

类变量既可以通过类来访问,也可以通过对象来访问,但通过对象来访问类变量的时候实际上并不是访问该对象所拥有的变量,在类初始化的时候类变量已经被初始化完成了,它只属于该类,即便创建类的对象也不会再为类变量分配内存并初始化它,对象能够访问类变量只是表面现象,实际上是系统在底层转换为通过类来访问类变量
实际上很多语言都不允许通过对象来访问类变量,对象只能访问实例变量,类变量必须通过类来访问

类方法

类方法也是同样的道理,通常情况下直接使用类作为调用者来调用类方法,但也可以使用对象来调用类方法,然而即使是用对象来调用类方法,其效果也与采用类来调用类方法完全一样

当使用实例来访问类成员时,实际上依然是委托类来访问类成员,因此即使某个实例为null,它依然可以它所属的类的类成员

public class NullAccessStatic
{
private static void test()
{
System.out.println("static修饰的类方法");
}
public static void main(String[] args)
{
// 定义一个NullAccessStatic变量,其值为null
NullAccessStatic nas = null;
// 使用null对象调用所属类的静态方法
nas.test();
}
}

如果一个null对象访问实例成员例如实例变量或实例方法,系统将会报NullPointerException异常,因为null表明实例根本不存在,既然实例不存在,它的实例变量和实例方法自然也不存在

静态初始化块

静态初始化块用于执行类初始化操作,在类的初始化阶段,系统会调用该类的静态初始化块来对类进行初始化,一旦类初始化结束后,静态初始化块将永远不会获得执行的机会

对static关键字而言,有一条非常重要的规则:类成员(包括方法、初始化块、内部类和枚举)不能访问实例成员(包括成员变量、方法、初始化块、内部类和枚举类)因为类成员属于类,其作用域比实例成员的作用域大,完全可能出现类成员已经初始化完成,但是实例成员还不曾初始化的情况。

实现单例类

大部分时候都把类的构造器定义为public访问权限,允许任何类自由创建该类的对象,但某些时候允许其他类随意创建对象也没有太大意义,而且还能造成因为频繁创建对象回收对象导致的性能下降,如果一个类始终只能创建一个实例,则这个类就被称为单例类

在一些特殊场景下,要求不允许自由创建该类的对象,而只允许为该类创建一个对象。
为了避免其他类自由创建该类的实例,应该把类的所有构造器使用private修饰,从而隐藏所有构造器,避免随意实例化。

根据良好的封装原则:一旦把类的构造器隐藏了,就需要另外提供一个public方法作为该类的访问点,用于创建该类的对象,且该方法必须使用static修饰(因为调用该方法之前还不存在对象,因此调用该方法的不可能是对象,只能是类)

此外该类还必须缓存已经创建的对象,否则该类无法知道是否曾经创建过对象,也就无法保证只创建一个对象,为此该类需要使用一个成员变量来保存曾经创建的对象,因为该成员变量需要被上面的静态方法访问,因此该成员变量必须是static修饰

class Singleton
{
// 使用一个类变量来缓存曾经创建的实例
private static Singleton instance;
// 将构造器使用private修饰,隐藏该构造器
private Singleton(){}
// 提供一个静态方法,用于返回Singleton实例
// 该方法可以加入自定义的控制,保证只产生一个Singleton对象
public static Singleton getInstance()
{
// 如果instance为null,表明还不曾创建Singleton对象
// 如果instance不为null,则表明已经创建了Singleton对象,
// 将不会重新创建新的实例
if (instance == null)
{
// 创建一个Singleton对象,并将其缓存起来
instance = new Singleton();
}
return instance;
}
}
public class SingletonTest
{
public static void main(String[] args)
{
// 创建Singleton对象不能通过构造器,
// 只能通过getInstance方法来得到实例
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // 将输出true
}
}

通过getInstance()方法提供的自定义控制(这也是封装的优势,不允许自由访问类成员变量和实现细节,而是通过方法来控制合适的暴露),保证Singleton类只能产生一个实例。因此在SingletonTest类的main()方法中看到两次产生的Singleton对象实际上是同一个对象

final关键字的用法

final关键字可用于修饰类、变量和方法,有点类似于C#中的sealed关键字,用于表示它所修饰的类、变量和方法不可改变
修饰变量时表示该变量一旦获得了初始值就不可被改变,final既可以修饰成员变量(包括类变量和实例变量),也可以修饰局部变量、形参数。
因为final变量获得初始值之后就不能被重新赋值,因此final修饰成员变量和修饰局部变量时有一定的不同

final成员变量

  • 成员变量是随类初始化或对象初始化而初始化的,当类初始化时,系统会为该类的类变量分配内存,并分配默认值;当创建对象时,系统会为该对象的实例变量分配内存,并分配默认值,也就是说当执行静态初始化块时可以对类变量赋初始值;当执行普通初始化块、构造器的时候可以对实例变量赋初始值。
  • 因此,成员变量的初始值可以在定义该变量时指定默认值,也可以在初始化块、构造器中指定初始值,对于final修饰的成员变量而言,一旦有了初始值,就不能被重新赋值,如果既没在定义成员变量时指定初始值,也没有在初始化块、构造器中为成员变量指定初始值,那么这些成员变量的值将一直是系统默认分配的0、‘\u0000’、false或null,也因此这些变量完全失去了存在的意义。
  • Java语法规定:final修饰的成员变量必须由程序员显示的指定初始值
  1. 类变量:必须在静态初始化块中指定初始值或者声明该类变量时指定初始值,而且只能在两者之一指定
  2. 实例变量:必须在非静态初始化块、声明该实例变量或者构造器中指定初始值,并且只能在三者之一指定
public class FinalVariableTest
{
// 定义成员变量时指定默认值,合法。
final int a = 6;
// 下面变量将在构造器或初始化块中分配初始值
final String str;
final int c;
final static double d;
// 既没有指定默认值,又没有在初始化块、构造器中指定初始值,
// 下面定义的ch实例变量是不合法的。
// final char ch;
// 初始化块,可对没有指定默认值的实例变量指定初始值
{
//在初始化块中为实例变量指定初始值,合法
str = "Hello";
// 定义a实例变量时已经指定了默认值,
// 不能为a重新赋值,因此下面赋值语句非法
// a = 9;
}
// 静态初始化块,可对没有指定默认值的类变量指定初始值
static
{
// 在静态初始化块中为类变量指定初始值,合法
d = 5.6;
}
// 构造器,可对既没有指定默认值、有没有在初始化块中
// 指定初始值的实例变量指定初始值
public FinalVariableTest()
{
// 如果在初始化块中已经对str指定了初始化值,
// 构造器中不能对final变量重新赋值,下面赋值语句非法
// str = "java";
c = 5;
}
public void changeFinal()
{
// 普通方法不能为final修饰的成员变量赋值
// d = 1.2;
// 不能在普通方法中为final成员变量指定初始值
// ch = 'a';
}
public static void main(String[] args)
{
var ft = new FinalVariableTest();
System.out.println(ft.a);
System.out.println(ft.c);
System.out.println(ft.d);
}
}
public class FinalErrorTest
{
// 定义一个final修饰的实例变量
// 系统不会对final成员变量进行默认初始化
final int age;
{
// age没有初始化,所以此处代码将引起错误。
System.out.println(age);
printAge(); // 这行代码是合法的,程序输出0
age = 6;
System.out.println(age);
}
public void printAge(){
System.out.println(age);
}
public static void main(String[] args)
{
new FinalErrorTest();
}
}

final成员变量在显示初始化之前不能直接访问,但可以通过方法来访问,基本上可以断定是java设计的一个缺陷,正常逻辑final成员变量在显示初始化之前是不应该允许被访问的。

final局部变量

系统不会对局部变量进行初始化,局部变量必须由程序员显示的初始化,因此在使用final修饰局部变量的时候,既可以在定义时指定默认值,也可以不指定。

public class FinalLocalVariableTest
{
public void test(final int a)
{
// 不能对final修饰的形参赋值,下面语句非法
// a = 5;
}
public static void main(String[] args)
{
// 定义final局部变量时指定默认值,则str变量无法重新赋值
final var str = "hello";
// 下面赋值语句非法
// str = "Java";
// 定义final局部变量时没有指定默认值,则d变量可被赋值一次
final double d;
// 第一次赋初始值,成功
d = 5.6;
// 对final变量重复赋值,下面语句非法
// d = 3.4;
}
}

形参在调用该方法的时候,由系统根据传入的参数来完成初始化,因此使用final修饰的形参不能被赋值

final修饰基本类型变量和引用类型变量的区别

  • 当使用final修饰基本类型变量时,不能对基本类型的变量重新赋值,因此基本类型的变量不能被改变
  • 对于引用类型变量来说,它保存的仅仅是一个引用,final只是保证这个引用类型变量所引用的地址不会变,但地址内的对象完全可以改变
class Person
{
private int age;
public Person(){}
// 有参数的构造器
public Person(int age)
{
this.age = age;
}
// age的setter和getter方法
public void setAge(int age)
{
this.age = age;
}
public int getAge()
{
return this.age;
}
}
public class FinalReferenceTest
{
public static void main(String[] args)
{
// final修饰数组变量,iArr是一个引用变量
final int[] iArr = {5, 6, 12, 9};
System.out.println(Arrays.toString(iArr));
// 对数组元素进行排序,合法
Arrays.sort(iArr);
System.out.println(Arrays.toString(iArr));
// 对数组元素赋值,合法
iArr[2] = -8;
System.out.println(Arrays.toString(iArr));
// 下面语句对iArr重新赋值,非法
iArr = null;
// final修饰Person变量,p是一个引用变量
final var p = new Person(45);
// 改变Person对象的age实例变量,合法
p.setAge(23);
System.out.println(p.getAge());
// 下面语句对p重新赋值,非法
p = null;
}
}

“宏变量”

对于一个final变量来说,无论是类变量,实例变量或者局部变量,只要满足三个条件就相当于一个直接量,即

  • 使用final修饰符修饰
  • 定义该final变量时指定了初始值
  • 该初始值可以在编译时就被确定下来
public class FinalLocalTest
{
public static void main(String[] args)
{
// 定义一个普通局部变量
final var a = 5;
System.out.println(a);
}
}
  • 对于程序来说,变量a其实根本不存在,当程序执行System.out.println(a);代码时,实际转换为执行System.out.println(5);
  • 程序中的变量a实际上就是宏变量,它满足了上边的三个条件,编译器会把用到宏变量的地方直接替换成该变量的值进行执行

此外,如果被赋值的表达式只是基本的算数表达式或字符串链接运算,没有访问普通变量或调用方法,Java编译器同样会将这种final变量当成宏变量处理

public class FinalReplaceTest
{
public static void main(String[] args)
{
// 下面定义了4个final“宏变量”
final var a = 5 + 2;
final var b = 1.2 / 3;
final var str = "疯癫" + "Java";
final var book = "疯癫Java:" + 99.0;
// 下面的book2变量的值因为调用了方法,所以无法在编译时被确定下来
// 调用了String类的方法编译器无法在编译时就确定book2的值,不会当成宏变量处理
final var book2 = "疯癫Java:" + String.valueOf(99.0);
// 返回true,因为book为当成了宏变量,它用的原本就是"疯癫Java:" + 99.0去比较的
System.out.println(book == "疯癫Java:99.0");
// 返回false
System.out.println(book2 == "疯癫Java:99.0");
}
}

Java会使用常量池来管理曾经用过的字符串直接量,例如执行String a = “java”;语句之后,常量池中就会缓存一个字符串“java”;如果程序再次执行String b = “java”;系统将会让b直接指向常量池中的“java”字符串,因此a=b将返回true

public class StringJoinTest
{
public static void main(String[] args)
{
var s1 = "疯狂Java";
// s2变量引用的字符串可以编译时就确定出来,
// 因此s2直接引用常量池中已有的"疯狂Java"字符串
var s2 = "疯狂" + "Java";
System.out.println(s1 == s2); // 输出true
// 定义2个字符串直接量
var str1 = "疯狂"; // ①
var str2 = "Java"; // ②
// 将str1和str2进行连接运算
var s3 = str1 + str2;
System.out.println(s1 == s3); // 输出false

}
}

对于s3而言,它是由str1和str2链接运算得到,但str1和str2是两个普通变量,编译器不会执行“宏替换”因此编译器无法在编译时确定s3的值,也无法让s3指向字符串池中缓存的“疯狂Java”,因此s1 == s3 返回false
要想让它返回true,那么只需要让编译器进行宏替换,要进行宏替换,也就是说定义str1和str2的时候使用final修饰
对于实例变量而言,既可以在定义该变量时赋初始值,也可以在非静态初始化块、构造器中赋初始值,在这三个地方指定初始值效果基本一样,但对于final实例变量而言,只有在定义该变量时指定初始值才会有“宏变量”的效果

final方法

final方法不能被重写,如果不希望子类重写父类的某个方法,则可以直接用final修饰
例如Object类里有个final方法getClass(),因为Java不希望任何类重写这个方法,所以用final把这个方法密封起来,但是toString()和equals()方法都允许重写并没有使用final修饰

public class FinalMethodTest
{
public final void test(){}
}
class Sub extends FinalMethodTest
{
// 下面方法定义将出现编译错误,不能重写final方法
public void test(){}
}
  • 对于一个private方法,因为它仅在当前类中可见,子类访问不到,所以子类无法重写它,即便子类定义了一个与父类private方法相同方法名、相同形参列表、相同返回值的方法也不是重写,只是定义了一个新方法
  • 即使使用final修饰了一个private访问权限的方法,依然可以在其子类中定义与该方法具有相同方法名、相同形参列表、相同返回值类型的方法
public class PrivateFinalMethodTest
{
private final void test(){}
}
class Sub extends PrivateFinalMethodTest
{
// 下面方法定义将不会出现问题
public void test(){}
}

final方法仅仅是不能被重写,但可以被重载

public class FinalOverload
{
// final修饰的方法只是不能被重写,但可以被重载
public final void test(){}
public final void test(String arg){}
}

final类

final修饰的类不能有子类,例如java.lang.Math类就是一个final类,一个子类继承了父类,将可以访问到父类的内部数据,并可以通过重写父类方法来改变父类方法的实现细节,这可能导致不可预估的后果,如果某个类不想被继承,则可以使用final修饰

public final class FinalClass{}
//下边的类定义将出现编译错误
class Sub extends FinalClass{}