关键字
关键字
静态内部类实现的单例如何做到线程安全且可延迟加载?
Java中的关键字有50多个,比如private、public、protected、class、interface、switch等等,大部分用法都比较简单,所以,我们不做讲解。本节,我们重点讲解final和static这两个关键词。这两个关键字既在开发中经常使用,也在面试中经常被考察。它们看似非常简单,但彻底搞懂却不容易,不信?我们来看下面这段代码。
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
public static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
上述代码是使用静态内部类实现单例的经典写法,我想很多同学都会写,但深入到细节,未必每个同学都能说得清楚,不信的话,你可以试着回答下面几个问题。
1)为什么这样实现的单例是线程安全的?
2)为什么这样实现的单例支持延迟加载?
3)为什么SingletonHolder设计为静态内部类,是否可以是普通内部类?
4)为什么将SingletonHolder设计为private的,而不是public?
带着这些问题,我们来学习今天的内容。
一、final关键字
final关键字可以修饰类、方法和变量。我们依次来看下用final修饰的类、方法和变量都具有哪些特性。
1)final修饰的类叫做final类,final类不可被继承。
2)final修饰的方法叫做final方法,final方法在子类中不可被重写。 反过来,子类可以将父类中的非final方法,重写为final方法。在早期的JVM实现中,final修饰的方法叫做内联函数。虚拟机执行编译优化,将内联函数的代码直接展开插入到调用处,以减少函数调用。但这种设计早已废弃,现在的JVM会根据某些其他情况,来判断是否将某个函数视为内联函数,而不是由final关键字来决定。关于这一点,我们在JVM模块中再详细讲解。
3)final修饰变量叫做final变量或常量,final变量只能被赋值一次,之后就不能再修改。 final修饰的变量有三类:类的成员变量、函数的局部变量、函数的参数。
接下来,我们重点讲下final变量。
对于final修饰的成员变量,赋值的方法有两种,一种是在成员变量声明时,另一种是在构造函数中,毕竟构造函数只会被调用一次,所以,对象一旦被创建,final成员变量便不会再被更改。示例代码如下所示。
// 方法一
public class Demo10_1 {
private final int fl = 6;
}
// 方法二
public class Demo10_1 {
private final int fl;
public Demo10_1(int vfl) {
this.fl = vfl;
}
}
对于final修饰的局部变量,赋值的方式也有两种,一种是在局部变量声明时,另一种是在使用前赋值一次,之后就不能再被赋值。使用未被赋值的final局部变量会报编译错误。示例代码如下所示。
// 方法一
public double caculateArea(double r) {
final double pi = 3.1415;
double area = pi * r * r;
return area;
}
// 方法二
public double caculateArea(double r) {
final double pi;
pi = 3.1415; //使用前赋值
double area = pi * r * r;
return area;
}
final修饰的变量既可以是基本类型变量,也可以是引用类型变量。对于引用类型变量,final关键词只限制引用变量本身不可变,但引用变量所引用的对象的属性或者数组的元素是可变。 示例代码如下所示。
public class Demo10_2 {
public static void main(String[] args) {
final Student s = new Student(1, 1);
f(s);
System.out.println(s.id); //打印2
}
public static void f(final Student s) {
// s = new Student(2,2); //编译报错
s.id = 2;
}
}
了解了final的用法之后,我们来看下,final的一个重要应用场景:不可变类。在第8节中,我们讲到String、Integer等都是不可变类。本节,我们就结合String的设计思路,来看下如何设计一个不可变类呢?
1)将类设置为final类,这样类就无法被继承,避免通过如下方式创建可变对象。
public class MyString extends String {
//重写toCharArray()方法,让它直接返回value数组,
//这样就能更改value数组了
@Override
public char[] toCharArray() {
return value.
}
}
String s = new MyString("abc");
char[] chars = s.toCharArray();
chars[0] = 'x';
System.out.println(s); //打印xbc
2)将类中所有的属性都设置为final,在创建对象时设置,之后不再允许修改。 当然,如果能保证类中没有方法会改变这个属性的值,也可以不用将其设置为final。例如,String类中的hash属性,因为其并非在创建对象时设置,并且类中没有方法可以二次修改此属性的值,所以,hash属性也可以不设置为final。
public final class String
implements java.io.Serializable,
Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
//...省略很多方法...
}
3)通过方法返回的属性,如果是引用类型的(数组或对象,如String类中的value数组),需要返回属性的副本而非本身。 否则,外部代码可以通过引用,修改返回对象中的属性或数组中元素。
public char[] toCharArray() {
char result[] = new char[value.length]; //副本
System.arraycopy(value, 0, result, 0, value.length);
return result; //返回副本
}
二、static关键字
static关键字可以修饰变量、方法、代码块、嵌套类。我们依次来看下用static修饰的变量、方法、代码块、嵌套类都具有哪些特性。
1)static变量
前面讲到,final可以修饰的变量包括:类的成员变量、函数的局部变量、函数参数。**而static只能修饰类的成员变量。 **static修饰的变量也叫做静态变量。当类的某个成员变量被修饰为static时,此成员变量隶属于类,为类的所有对象所共享。 这也是为什么在第9节中,我们通过JOL来查看对象内存结构时,不显示静态变量的原因。静态变量跟类的代码一起,存储在方法区。 关于这一点,我们在JVM中详细介绍。
对于静态变量,我们既可以通过类来访问,也可以通过对象来访问,如下示例代码所示。当然,下面的代码并非多线程安全的,关于这点,我们在多线程模块讲解。
public class Obj {
public static int objCount = 0;
public Obj() {
objCount++;
}
}
public class Demo10_4 {
public static void main(String[] args) {
Obj d1 = new Obj();
Obj d2 = new Obj();
System.out.println(Obj.objCount); //打印2
System.out.println(d1.objCount); //打印2
}
}
实际上,我们经常把static和final放在一起来修饰变量,用static final修饰的变量叫做静态常量 。对于一些跟具体对象无关,又不会改变的常量数据,我们一般存储将其存储在静态常量中。 静态常量的命名比较特殊,所有字母都大写。示例代码如下所示。
public final class Integer extends Number
implements Comparable<Integer> {
public static final int MIN_VALUE = 0x80000000;
public static final int MAX_VALUE = 0x7fffffff;
//...省略其他方法和属性...
}
2)static方法
用static修饰的方法叫做静态方法。跟静态变量类似,静态方法也属于类而非对象。所以,我们可以在不创建对象的情况下,调用静态方法,这样使用起来比较方便,所以,很多工具类中的方法都设计为静态方法。比如Math类、Collections类中的方法。
public final class Math {
/**
* Don't let anyone instantiate this class.
*/
private Math() {}
public static final double E = 2.7182818284590452354;
public static final double PI = 3.14159265358979323846;
public static int abs(int a) {
return (a < 0) ? -a : a;
}
}
注意,静态方法只能访问静态成员变量,以及调用类中的其他静态方法。静态方法不能访问类中的非静态成员变量,也不能调用类中的非静态方法。反过来,类中的非静态方法可以访问类中的静态变量和静态方法。 之所以有这样的规定,还是跟静态成员变量和静态方法所有权归类有关。对象可以使用类的数据,但类不能使用具体某个对象的数据。
3)static代码块
对于某些静态成员变量,如果其初始化操作无法通过一个简单的赋值语句来完成,这时,我们可以将静态成员变量的初始化逻辑,放入static修饰的代码块中,如下所示。静态代码块是在类加载时执行,如果类中有多个静态代码块,那么静态代码块的执行顺序跟书写顺序相同。
public class ParserFactory {
private static Map<String, Parser> parsers = new HashMap<>();
static {
parsers.put("json", new JSONParser());
parsers.put("xml", new XMLParser());
parsers.put("yaml", new YAMLParser());
}
//...省略其他方法和属性...
}
4)static嵌套类
final能修饰类,static也可以,不过,static只能修饰嵌套类。 嵌套类是指定义在一个类中的类,所以,也叫做内部类。承载内部类的类叫做外部类。常用的内部类有3种:普通内部类、静态内部类、匿名内部类。
内部类在编译成字节码之后,会独立于外部类,生成一个新的class文件,命名方式为:外部类名内部类名.class。对于匿名内部类,因为内部类没有名字,所以命名方式为:外部类名[序号].class。其中,[序号]为1、2、3...表示此匿名内部类是外部类的第几个匿名内部类。
普通内部类
示例代码如下所示。ArrayList类中定义了一个内部类Itr,负责遍历ArrayList容器中的元素。Itr类独自属于ArrayList类,其他类不会用到它,所以,我们把Itr类定义为ArrayList的内部类。这样,代码的可读性和可维护性更好,更加满足封装原则(不该对外暴露的不暴露)。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess,
Cloneable, java.io.Serializable {
//...省略其他属性和方法...
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = modCount;
Itr() {}
//...省略其他属性和方法...
}
}
注意,private内部类对除外部类之外的代码不可见,如果想在除了外部类之外的代码中使用内部类,要么将内部类设置为public,要么让内部类实现一个外部的接口,外部代码使用接口来访问内部类的代码。示例代码如下所示。
public interface I {}
public class A {
private class B {}
private class C implements I {} //实现外部接口的内部类
public class D {} //public修饰的内部类
public B getB() {
return new B();
}
public I getC() {
return new C();
}
public D getD() {
return new D();
}
}
public class Demo {
public static void main(String[] args) {
A a = new A();
A.B b = a.getB(); //编译报错
I c = a.getC();
A.D d1 = a.getD();
A.D d2 = a.new D();
}
}
静态内部类
静态内部类跟普通内部类主要区别有三个。
1)第一个区别是,在访问权限上,内部类跟外部类中的方法具有相同的访问权限。也就是说,静态内部类跟静态方法一样,只能访问外部类的静态变量和静态方法,而普通内部类可以访问外部类的所有变量和所有方法。
2)第二个区别是,静态内部类可以包含静态变量和静态方法,而普通内部类不行,不过这点在JDK16中有所改变,在JDK16中,普通内部类也可以包含静态变量和静态方法了。
3)第三个区别是,如果要创建普通内部类的对象,需要先创建外部类的对象,而静态内部类的对象可以独立于外部类单独创建。 示例代码如下所示。
public class A {
public class D {}
public static class E {}
}
public class Demo {
public static void main(String[] args) {
A a = new A();
A.D d = a.new D();
A.E e = new A.E();
}
}
根据如下代码,有如下问题:
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
public static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
1)为什么这样实现的单例是线程安全的?为什么这样实现的单例支持延迟加载?
首先,静态变量的初始化是在类加载时,而类是在用到它时才会被加载,什么才算用到它呢?比如,创建对象、创建子类的对象、调用静态方法、调用静态变量、使用反射时,这几种情况下,JVM会先将类加载到方法区。其次,外部类加载并不会导致内部类的加载。再者,类的加载过程是线程安全。
当我们调用Singleton.getInstance()来获取单例对象时,JVM会先将Singleton类加载。紧接着,getInstance()函数访问了SingletonHolder类的静态变量,于是,触发JVM加载SingletonHolder类。而加载SingletonHolder类会触发静态变量的初始化操作,也就是执行SingletonHolder类中的唯一一行代码。
因此,instance的创建是在SingletonHolder类加载过程中完成的,所以是线程安全的。并且,只有在第一次调用getInstance()函数时,才会创建instance,所以,满足延迟加载。再此之后,即便再调用getInstance()函数,因为SingletonHolder类都已经加载到JVM中,instance静态变量也已经初始化完成,不会再重复执行初始化操作,所以,getInstance()函数返回的是同一个Singleton实例。
2)为什么SingletonHolder设计为静态内部类,是否可以是普通内部类?为什么将SingletonHolder设计为private的,而不是public?
普通内部类不能定义静态变量和静态方法,所以,如果SingletonHolder设计为普通内部类,那么instance将不能是static的,这样instance无法在类加载时创建,那么其创建过程又会存在线程安全问题。所以,SingletonHolder设计为静态内部类。
除此之外,因为SingletonHolder类不会被除Singleton之外的代码使用,所以,我们将其设置为private,而不是public。
匿名类
在多线程开发中,我们会经常用到匿名内部类。如下所示。因为实现Runnable接口的类只会被使用一次,所以,没必要单独定义一个新类。回调模式中的回调对象,往往也会被设计成匿名内部类。
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello wangzheng");
}
});
t.start();
匿名内部类跟定义它的外部函数,具有相同的访问权限,如果外部函数是静态函数,那么匿名内部类只能访问外部类的静态成员变量和静态函数;如果外部函数是普通函数,那么匿名内部类可以访问外部类的任何成员变量和成员函数,包括private修饰的成员变量和成员函数。除此之外,匿名内部类还可以访问外部函数的final局部变量。
public class Demo10_5 {
private static int a = 1;
private int b = 2;
private static void f() {}
private void g() {}
public static void main(String[] args) {
final int c = 3;
int d = 4;
Thread t = new Thread(new Runnable() {
@Override
public void run() {
a += 1;
b += 3; //编译报错,非静态成员变量
int y = c + 1;
int x = d + 2; //编译报错,非final局部变量
f();
g(); //编译报错,非静态成员函数
}
});
t.start();
d = 3;//不加这一行会触发编译优化,JVM将变量d当做final变量
}
}
为什么非final局部变量不能被匿名内部类访问呢?
这也是一个比较常考的面试题。
这是因为,外部函数通过类似参数传递的方式,将局部变量传递给匿名内部类来使用。前面讲过,Java的参数传递是值传递。匿名内部类对参数(相当于局部变量的副本)进行修改,不会改变局部变量本身的值。在程序员看来,明明在匿名类中修改了局部变量的值,却没有生效,不符合直觉认知。所以,为了保持匿名内部类跟外部函数的数据一致性,Java在设计上,只允许匿名内部类访问final修饰的局部变量。
public interface ICallable {
void add();
}
public class Demo {
public void test() {
int a = 1;
ICallable callback = new ICallable() {
@Override
public void add() {
a++;
System.out.println(a); //打印2
}
};
System.out.println(a); //被修改了,但却仍然打印1
}
}
三、总结
四、课后思考题
加载外部类并不会导致内部类的加载。请写个代码,做个实验来证明这一点。