只是不愿随波逐流 ...|

lidongdongdong~

园龄:2年7个月粉丝:14关注:8

10、关键字

内容来自王争 Java 编程之美

Java 中的关键字有 50 多个,比如 private、public、protected、class、interface、switch 等等,大部分用法都比较简单,我们不做讲解
本节,我们重点讲解 final 和 static 这两个关键词,这两个关键字既在开发中经常使用,也在面试中经常被考察
它们看似非常简单,但彻底搞懂却不容易,不信?我们来看下面这段代码

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

上述代码是使用静态内部类实现单例的经典写法,我想很多同学都会写,但深入到细节,未必每个同学都能说得清楚,不信的话,你可以试着回答下面几个问题

  • 为什么这样实现的单例是线程安全的?
  • 为什么这样实现的单例支持延迟加载?
  • 为什么 SingletonHolder 设计为静态内部类,是否可以是普通内部类?
  • 为什么将 SingletonHolder 设计为 private 的,而不是 public?

带着这些问题,我们来学习今天的内容

1、final 关键字

final 关键字可以修饰类、方法和变量,我们依次来看下用 final 修饰的类、方法和变量都具有哪些特性

  • final 修饰的类叫做 final 类,final 类不可被继承
  • final 修饰的方法叫做 final 方法,final 方法在子类中不可被重写,反过来,子类可以将父类中的非 final 方法,重写为 final 方法
    在早期的 JVM 实现中,final 修饰的方法叫做内联函数
    虚拟机执行编译优化,将内联函数的代码直接展开插入到调用处,以减少函数调用
    但这种设计早已废弃,现在的 JVM 会根据某些其他情况,来判断是否将某个函数视为内联函数,而不是由 final 关键字来决定
    关于这一点,我们在 JVM 模块中再详细讲解
  • final 修饰变量叫做 final 变量或常量,final 变量只能被赋值一次,之后就不能再修改
    final 修饰的变量有三类:类的成员变量、函数的局部变量、函数的参数

接下来,我们重点讲下 final 变量

1.1、成员变量

对于 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;
}
}

1.2、局部变量

对于 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;
}

1.3、引用类型变量

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;
}
}

1.4、不可变类

了解了 final 的用法之后,我们来看下, final 的一个重要应用场景:不可变类
在第 8 节中,我们讲到 String、Integer 等都是不可变类,本节,我们就结合 String 的设计思路,来看下如何设计一个不可变类呢?

设置 final 类

将类设置为 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

设置 final 属性

将类中所有的属性都设置为 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
// ... 省略很多方法 ...
}

返回副本

通过方法返回的属性,如果是引用类型的(数组或对象,如 String 类中的 value 数组),需要返回属性的副本而非本身
否则,外部代码可以通过引用,修改返回对象中的属性或数组中元素

public char[] toCharArray() {
char result[] = new char[value.length]; // 副本
System.arraycopy(value, 0, result, 0, value.length);
return result; // 返回副本
}

2、static 关键字

static 关键字可以修饰变量、方法、代码块、嵌套类
我们依次来看下用 static 修饰的变量、方法、代码块、嵌套类都具有哪些特性

2.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
System.out.println(d2.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.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;
}
}

注意

  • 静态方法只能访问静态成员变量,以及调用类中的其他静态方法
  • 静态方法不能访问类中的非静态成员变量,也不能调用类中的非静态方法
  • 反过来,类中的非静态方法可以访问类中的静态变量和静态方法

之所以有这样的规定,还是跟静态成员变量和静态方法所有权归类有关,对象可以使用类的数据,但类不能使用具体某个对象的数据

2.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());
}
// ... 省略其他方法和属性 ...
}

2.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 内部类对 "除外部类之外的代码" 不可见(private 内部类只对当前外部类可见),如果想在除了外部类之外的代码中使用内部类

  • 要么将内部类设置为 public
  • 要么让内部类实现一个外部的接口,外部代码使用接口来访问内部类的代码,示例代码如下所示
// 外部接口
public interface I {
}
public class A {
// private 修饰的内部类
private class B {
}
// 实现外部接口的内部类
private class C implements I {
}
// public 修饰的内部类
public class D {
}
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(); // 编译报错(private 修饰的内部类)
I c = a.getC(); // 实现外部接口的内部类
A.D d1 = a.getD(); // public 修饰的内部类
A.D d2 = a.new D(); // public 修饰的内部类
A.D d3 = new A().new D(); // public 修饰的内部类
}
}

静态内部类

我们再来看静态内部类,静态内部类跟普通内部类主要区别有三个

  • 在访问权限上,内部类跟外部类中的方法具有相同的访问权限
    也就是说,静态内部类跟静态方法一样,只能访问外部类的静态变量和静态方法
    而普通内部类可以访问外部类的所有变量和所有方法
  • 静态内部类可以包含静态变量和静态方法
    而普通内部类不行,不过这点在 JDK 16 中有所改变,在 JDK 16 中,普通内部类也可以包含静态变量和静态方法了
  • 如果要创建普通内部类的对象,需要先创建外部类的对象
    而静态内部类的对象可以独立于外部类单独创建,示例代码如下所示
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(); // 静态内部类创建对象
}
}

匿名内部类

我们再来看匿名类,在多线程开发中,我们会经常用到匿名内部类,如下所示
因为实现 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; // 编译报错, 非静态成员变量
f();
g(); // 编译报错, 非静态成员函数
int y = c + 1;
int x = d + 2; // 编译报错, 非 final 局部变量
}
});
t.start();
d = 3; // 不加这一行会触发编译优化, JVM 将变量 d 当做 final 变量
}
}

为什么非 final 局部变量不能被匿名内部类访问呢?这也是一个比较常考的面试题

假设匿名内部类可以访问非 final 局部变量,如下代码所示,外部函数通过类似参数传递的方式,将局部变量传递给匿名内部类来使用
前面讲过,Java 的参数传递是值传递,因此,匿名内部类对参数(相当于局部变量的副本)进行修改,不会改变局部变量本身的值
在程序员看来,明明在匿名类中修改了局部变量的值,却没有生效,不符合直觉认知
所以,为了保持匿名内部类跟外部函数的数据一致性,Java 在设计上,只允许匿名内部类访问 final 修饰的局部变量

// 此示例为假设情况, 假设匿名内部类可以访问非 final 局部变量
public interface ICallable {
void add();
}
public class Demo {
public void test() {
int a = 1;
ICallable callback = new ICallable() {
@Override
public void add() {
a++; // a 变成了 2
}
};
System.out.println(a); // 匿名内部类对 a 的修改没生效, 仍然打印 1
}
}

3、单例

有了以上知识积累,我们来回答开篇的问题,为了方便查看,我将问题和代码重写拷贝了一份,贴到了下面

  • 为什么这样实现的单例是线程安全的?
  • 为什么这样实现的单例支持延迟加载?
  • 为什么 SingletonHolder 设计为静态内部类,是否可以是普通内部类?
  • 为什么将 SingletonHolder 设计为 private 的,而不是 public?
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
private static class SingletonHolder {
public static final Singleton instance = new Singleton();
}
}

我们先来看前两个问题

回答前两个问题需要三个前置知识

  • 首先:静态变量的初始化是在类加载时,而类是在用到它时才会被加载,怎么才算用到它呢?
    比如:调用静态变量、调用静态方法、创建对象、创建子类的对象、使用反射时,这几种情况下, JVM 会先将类加载到方法区
  • 其次:外部类加载并不会导致内部类的加载,你可以写个代码,做个实验来证明这一点,留给你作为思考题
  • 再次:类的加载过程是线程安全

根据以上前置知识,当我们调用 Singleton.getInstance() 来获取单例对象时, JVM 会先将 Singleton 类加载
紧接着,getInstance() 函数访问了 SingletonHolder 类的静态变量,于是,触发 JVM 加载 SingletonHolder 类
而加载 SingletonHolder 类会触发静态变量的初始化操作,也就是执行 SingletonHolder 类中的唯一一行代码

因此,instance 的创建是在 SingletonHolder 类加载过程中完成的,所以是线程安全的
并且,只有在第一次调用 getInstance() 函数时,才会创建 instance,所以,满足延迟加载
再此之后,即便再调用 getInstance() 函数,因为 SingletonHolder 类都已经加载到 JVM 中,instance 静态变量也已经初始化完成,不会再重复执行初始化操作
所以,getInstance() 函数返回的是同一个 Singleton 实例

前两个问题算是回答了,我们再看后两个问题

前面讲到普通内部类不能定义静态变量和静态方法
所以,如果 SingletonHolder 设计为普通内部类,那么 instance 将不能是 static 的,这样 instance 无法在类加载时创建,那么其创建过程又会存在线程安全问题
所以,SingletonHolder 设计为静态内部类
除此之外,因为 SingletonHolder 类不会被除 Singleton 之外的代码使用,所以,我们将其设置为 private,而不是 public

4、课后思考题

加载外部类并不会导致内部类的加载,请写个代码,做个实验来证明这一点

public class Out {
static {
System.out.println("Out class load");
}
private static class In {
static {
System.out.println("Inner class load");
}
}
}
public class Test {
public static void main(String[] args) {
Out out = new Out(); // 打印结果为: Out class load
}
}
posted @   lidongdongdong~  阅读(79)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
展开