实现封装。创建和使用单例类和不可变类。开发在初始化块、变量、方法和类上使用 static 关键字的代码。开发使用最终关键字的代码。

封装

最好尽可能多地隐藏类的内部实现。减轻变化的影响。

class MyClass {
    String myField = "you";
    void myMethod() {
        System.out.println("Hi " + myField);
    }
}

如果 myField 将其类型从 String 更改为 int 会怎样?
如果我们在五十个类中使用这个属性,我们将不得不在程序中做五十个更改。
但是如果我们隐藏属性并使用 setter 方法,我们可以避免五十个变化:

// Hiding the attr to the outside world with the private keyword
private int myField = 0;

void setMyField(String val) { // Still accepting a String
    try {
        myField = Integer.parseInt(val);
    } catch(NumberFormatException e) {
        myField = 0;
    }
}

如果某个东西被定义为 protected,那么它只能被定义它的类、它的子类和同一个包的类访问。子类是否在另一个包中并不重要,这使得这个修饰符的限制比 private 少。
如果没有修饰符,则它具有默认访问权限,也称为包访问权限,因为它只能由同一个包中的类访问。如果在另一个包中定义了一个子类,则它不能看到默认的访问属性或方法。这是 protected 修饰符的唯一区别,这使得它更具限制性。

package street21;
public class House {
    protected int number;
    private String reference;

    void printNumber() {
        System.out.println("Num: " + number);
    }
    public void printInformation() {
        System.out.println("Num: " + number);
        System.out.println("Ref: " + reference);
    }
}
class BlueHouse extends House {
    public String getColor() { return "BLUE"; }
}

...

package city;
import street21.*;
class GenericHouse extends House {
    public static void main(String args[]) {
        GenericHouse h = new GenericHouse();
        h.number = 100;
        h.reference = ""; // Compile-time error  reference为private
        h.printNumber(); // Compile-time error  printNumber为默认default,只有同一个包下才可以访问,子包下也不行
        h.printInformation();
        BlueHouse bh = new BlueHouse(); // Compile-time error  BlueHouse为default,默认访问权限
        bh.getColor(); // Compile-time error
    }
}

单例

某个特定类只有一个实例。这样的类是一个单例类,是一个设计模式。
有一些类是用 Java 单例模式编写的,例如:

  • java.lang.Runtime
    image.png

  • java.awt.Desktop
    image.png

Java 中,一般来说,有两种方法来实现单例类:

  • 构造函数私有化,提供静态工厂返回实例
  • 作为枚举类型

第一种类型

有两种方式实现

使用一个私有静态属性和一个获取它的方法

class Singleton {
    private static final Singleton instance = new Singleton();
    private Singleton() { }
    public static Singleton getInstance() {
        return instance;
    }
}
  • 属性必须是私有的,这样其他类只能通过它的 getter 来使用它。
  • 它必须是静态的,以便在任何人都可以使用它之前,在加载类时可以创建实例,因为静态成员属于类,而不属于特定的实例。
  • 它必须是最终的,这样以后就不能创建新的实例。

变种,使用静态内部类

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

这样做的好处是,在第一次引用内部类之前,不会创建实例。

get 方法中创建实例

有时候创建对象并不像调用 new 那么简单

class Singleton {
    private static Singleton instance;
    private Singleton() { }
    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
            // more code to create the instance...
        }
        return instance;
    }
}

第一次调用此方法时,将创建实例。
但是用这种方法,我们面临另一个问题。在多线程环境中,如果两个或多个线程并行执行此方法,则很有可能最终出现类的多个实例。
一个解决方案是同步这个方法,这样一次只有一个线程可以访问它。

class Singleton {
    private static Singleton instance;
    private Singleton() { }
    public static synchronized Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

这样做的问题是,严格来说,效率并不高。我们只需要在第一次创建实例时进行同步,而不需要在每次调用方法时进行同步。
一个改进是只锁定创建实例的代码的一部分。为了正确地工作,我们必须再次检查实例是否为 null,一个没有锁定(检查实例是否已经创建) ,然后在同步块中再创建一个实例(以安全地创建实例)。

class Singleton {
    private static Singleton instance;
    private Singleton() { }
    public static Singleton getInstance() {
        if(instance == null) {
            synchronized (Singleton.class) {
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这种实现还不是完美的。这一次,问题出在 Java 虚拟机(JVM)级别。JVM (有时是编译器)可以通过重新排序或缓存变量的值(并且不使更新可见)来优化代码。
解决方案是使用 volatile 关键字,它保证许多线程共享的变量的任何读/写操作都是原子操作,而不是缓存操作。

class Singleton {
    private static volatile Singleton instance;
    private Singleton() { }
    public static Singleton getInstance() {
        if(instance == null) {
            synchronized (Singleton.class) {
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

不可变对象

在多个类使用时,还可能不希望修改对象的值或状态。这样的对象将会是一个不可变对象。
在 Java JDK 中有一些不可变的类,例如:
java.lang.String
image.png
不可变对象在创建后不能更改。这意味着它们不能有 setter 方法或公共变量,因此设置其属性的唯一方法是通过构造函数。
不可变性还意味着,如果一个方法必须更改对象的属性,因为对象或其值不能更改,那么它必须返回对象的副本和新的值(这正是 String 类的工作方式)。
image.png

  • 我个人认为使用lombok插件在构造set和get方法时,因为get没有返回属性的副本,其实严格来说并没有把对象封装好,换言之,只需要得到返回的内存地址就可以直接修改对象属性,并不需要使用set方法

另一个需要考虑的问题是继承。如果不可变类可以被继承,那么子类可以改变方法来修改类的实例,所以不可变类不允许这样做。

不可变类需要满足的条件

  • 通过构造函数设置它的所有属性
  • 不定义 setter 方法
  • 声明它的所有属性private(有时候还要加上final)
  • 声明类final,防止继承
  • 保护对任何可变状态的访问成员,例如又一个成员属性对象为List,则引用不能在对象外部访问,或者必须返回副本(如果对象的内容必须更改,则应该返回副本)

静态static关键字

可以将某个静态的东西看作是属于该类的东西,而不是属于该类的某个特定实例。
静态属性将在类的所有实例之间共享(因为它同样不属于实例)
在谈论静态方法时,该方法属于类,这意味着我们不需要一个实例来调用它(顺便说一句,同样适用于属性),不能使用关键字 super 和 this,因为和对象无关
静态块就像静态方法一样不能引用实例属性,也不能使用关键字 super 和 this。不能包含返回语句,如果该块不能正常完成(例如,由于未捕获的异常) ,那么这是一个编译时错误

最终final关键词

可以应用于变量、方法和类

  • 变量
    当 final 应用于变量时,不能在变量初始化后更改其值。这些变量可以是属性(静态和非静态)或参数。Final 属性既可以在声明时初始化,也可以在构造函数中初始化,或者在初始化器块中初始化。
    变量只能初始化一次,之后不能更改其值
public class Example {
    private final int number = 0;
    private final String name;
    private final int total;
    private final int id;

    {
        name = "Name";
    }

    public Example() {
        number = 1; // Compile-time error
        total = 10;
    }

    public void exampleMethod(final int id) {
        id = 5; // Compile-time error
        this.id = id; // Compile-time error
    }
}
  • 方法
    当 final 应用于某个方法时,不能重写它。

  • 当 final 应用到一个类时,不能子类化

这个家伙很懒,啥也没有留下😋