设计模式-12章(享元模式)


第十二章 享元模式

上一章中,我们讲了组合模式。组合模式并不常用,主要用在数据能表示成树形结构、能通过树的遍历算法来解决的场景中。今天,我们再来学习一个不那么常用的模式,享元模式(Flyweight Design Pattern)。这也是我们要学习的最后一个结构型模式。

享元模式(Flyweight Pattern)是一种结构型设计模式,用于优化大量相似对象的内存占用和性能。该模式的核心思想是共享对象,以减少重复创建相似对象的开销,从而降低内存消耗和提高性能。享元模式通常适用于需要大量对象实例的情况,其中许多实例具有相似的属性。

以下是享元模式的详细解释和实现要点:

主要角色:

  1. 享元工厂(Flyweight Factory):这是享元模式的核心部分。它负责创建和管理享元对象。它通常维护一个享元对象的池,以确保相同对象的共享,并在需要时返回已存在的享元对象或创建新的享元对象。
  2. 享元(Flyweight):享元是可以被共享的对象。它包含了内部状态和外部状态。内部状态是可以被多个享元对象共享的部分,而外部状态是每个享元对象独有的部分。享元对象通常是不可变的。
  3. 客户端(Client):客户端使用享元工厂来获取享元对象,并根据需要传递外部状态。客户端通常不直接创建享元对象,而是通过享元工厂获取它们。

享元模式的工作流程:

  1. 客户端向享元工厂请求一个享元对象。
  2. 如果该享元对象已存在于工厂的池中,工厂将返回已存在的对象。
  3. 如果该享元对象不存在,工厂将创建一个新的享元对象,将其放入池中,并返回给客户端。
  4. 客户端可以根据需要传递外部状态给享元对象。这可以是通过参数传递或者在对象内部进行设置。
  5. 客户端使用享元对象执行操作。

优点:

  1. 减少内存占用:通过共享相似的对象,享元模式可以显著减少内存消耗,特别是当需要大量对象时。
  2. 提高性能:减少对象的创建和销毁次数,可以提高应用程序的性能。
  3. 分离内部状态和外部状态:享元模式将对象的内部状态和外部状态分离开,使得代码更易于维护和理解。

适用场景:
享元模式适用于以下情况:

  1. 应用程序中存在大量相似的对象。
  2. 对象的大部分状态可以被外部状态替代。
  3. 创建对象的成本很高,需要减少对象的创建次数。
  4. 应用程序需要高性能和较低的内存占用。

示例:
假设有一个文本编辑器应用程序,需要在屏幕上显示大量相似的字符。在这种情况下,享元模式可以用来共享字符的字体、颜色等属性,以减少内存占用和提高性能。每个字符的位置是外部状态,而字符的字体和颜色是内部状态。

import java.util.HashMap;
import java.util.Map;

// 享元工厂
class FlyweightFactory {
    private Map<Character, Flyweight> flyweights = new HashMap<>();

    public Flyweight getFlyweight(Character key) {
        if (flyweights.containsKey(key)) {
            return flyweights.get(key);
        } else {
            Flyweight flyweight = new ConcreteFlyweight(key);
            flyweights.put(key, flyweight);
            return flyweight;
        }
    }
}

// 享元接口
interface Flyweight {
    void display(String position);
}

// 具体享元类
class ConcreteFlyweight implements Flyweight {
    private Character intrinsicState; // 内部状态

    public ConcreteFlyweight(Character intrinsicState) {
        this.intrinsicState = intrinsicState;
    }

    @Override
    public void display(String position) {
        System.out.println("Character: " + intrinsicState + ", Position: " + position);
    }
}

// 客户端
public class FlyweightDemo {
    public static void main(String[] args) {
        FlyweightFactory factory = new FlyweightFactory();

        // 客户端可以共享享元对象,但保留外部状态
        Flyweight flyweight1 = factory.getFlyweight('A');
        flyweight1.display("Position 1");

        Flyweight flyweight2 = factory.getFlyweight('B');
        flyweight2.display("Position 2");

        Flyweight flyweight3 = factory.getFlyweight('A');
        flyweight3.display("Position 3");

        // 外部状态不共享
        Flyweight flyweight4 = factory.getFlyweight('C');
        flyweight4.display("Position 4");
    }
}

在这个示例中,FlyweightFactory是享元工厂,负责管理和共享享元对象。ConcreteFlyweight是具体的享元类,包含了内部状态(intrinsicState),它的display方法可以接收外部状态(position)。客户端可以获取享元对象并传递外部状态,演示了内部状态的共享和外部状态的独立性。

不理解这个实例没关系,你看看我下面的具体举例你就明白了。

跟其他所有的设计模式类似,享元模式的原理和实现也非常简单。今天,我会通过棋牌游戏文本编辑器两个实际的例子来讲解。除此之外,我还会讲到它跟单例、缓存、对象池的区别和联系。我还会带你剖析一下享元模式在 Java Integer、String 中的应用。


1. 享元模式原理与实现

所谓“享元”,顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象

具体来讲,当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样可以减少内存中对象的数量,起到节省内存的目的。实际上,不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元。

这里其实就是和单例模式的一点区别。单例模式只允许一个实例,然后供其他应用使用,而享元模式不仅可以存放一份重复的实例,还可以存放相似的对象。

这里我稍微解释一下,定义中的“不可变对象”指的是,一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或者属性)就不会再被修改了。所以,不可变对象不能暴露任何 set() 等修改内部状态的方法。之所以要求享元是不可变对象,那是因为它会被多处代码共享使用,避免一处代码对享元进行了修改,影响到其他使用它的代码。

接下来,我们通过一个简单的例子解释一下享元模式。

假设我们在开发一个棋牌游戏(比如象棋)。一个游戏厅中有成千上万个“房间”,每个房间对应一个棋局。棋局要保存每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。利用这些数据,我们就能显示一个完整的棋盘给玩家。具体的代码如下所示。其中,ChessPiece 类表示棋子,ChessBoard 类表示一个棋局,里面保存了象棋中 30 个棋子的信息。

public class ChessPiece {//棋子
    private int id;
    private String text;
    private Color color;
    private int positionX;
    private int positionY;
    
    public ChessPiece(int id, String text, Color color, int positionX, int positionY){
        this.id = id;
        this.text = text;
        this.color = color;
        this.positionX = positionX;
        this.positionY = positionX;
    }
    
    //静态内部类
    public static enum Color {
        RED, BLACK
    }
        // ...省略其他属性和getter/setter方法...
}

public class ChessBoard {//棋局
    private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
    
    public ChessBoard() {
        init();
    }
    
    private void init() {
        chessPieces.put(1, new ChessPiece(1, "車", ChessPiece.Color.BLACK, 0, 0));
        chessPieces.put(2, new ChessPiece(2,"馬", ChessPiece.Color.BLACK, 0, 1));
        //...省略摆放其他棋子的代码...
    }
    
    public void move(int chessPieceId, int toPositionX, int toPositionY) {
        //...省略...
    }
}

为了记录每个房间当前的棋局情况,我们需要给每个房间都创建一个 ChessBoard 棋局对象。因为游戏大厅中有成千上万的房间(实际上,百万人同时在线的游戏大厅也有很多),那保存这么多棋局对象就会消耗大量的内存。有没有什么办法来节省内存呢?

这个时候,享元模式就可以派上用场了。像刚刚的实现方式,在内存中会有大量的相似对象。这些相似对象的 id、text、color 都是相同的,唯独 positionX、positionY 不同。实际上,我们可以将棋子的 id、text、color 属性拆分出来,设计成独立的类,并且作为享元供多个棋盘复用。这样,棋盘只需要记录每个棋子的位置信息就可以了。具体的代码实现如下所示:

// 享元类
public class ChessPieceUnit {
    private int id;
    private String text;
    private Color color;
    
    public ChessPieceUnit(int id, String text, Color color) {
        this.id = id;
        this.text = text;
        this.color = color;
    }
    
    //静态内部类
    public static enum Color {
        RED, BLACK
    }
    // ...省略其他属性和getter方法...
}

public class ChessPieceUnitFactory {
    private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();
    
    static {
        pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));
        pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
        //...省略摆放其他棋子的代码...
    }
    
    public static ChessPieceUnit getChessPiece(int chessPieceId) {
        return pieces.get(chessPieceId);
    }
}

public class ChessPiece {
    private ChessPieceUnit chessPieceUnit;
    private int positionX;
    private int positionY;
    
    public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) {
        this.chessPieceUnit = unit;
        this.positionX = positionX;
        this.positionY = positionY;
    }
    // 省略getter、setter方法
}

public class ChessBoard {
    private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
    
    public ChessBoard() {
        init();
    }
    private void init() {
        chessPieces.put(1, new ChessPiece(ChessPieceUnitFactory.getChessPiece(1), 0,0));
        chessPieces.put(1, new ChessPiece(ChessPieceUnitFactory.getChessPiece(2), 1,0));
        //...省略摆放其他棋子的代码...
    }
    //移动棋子的方法
    public void move(int chessPieceId,int toPositionX,int toPositionY){
        //省略
    }
}

在上面的代码实现中,我们利用工厂类来缓存 ChessPieceUnit 信息(也就是 id、text、color)。通过工厂类获取到的 ChessPieceUnit 就是享元。所有的 ChessBoard 对象共享这 30 个 ChessPieceUnit 对象(因为象棋中只有 30 个棋子)。在使用享元模式之前,记录 1 万个棋局,我们要创建 30 万(30*1 万)个棋子的 ChessPieceUnit 对象。利用享元模式,我们只需要创建 30 个享元对象供所有棋局共享使用即可,大大节省了内存。

那享元模式的原理讲完了,我们来总结一下它的代码结构。实际上,它的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 **Map **来缓存已经创建过的享元对象,来达到复用的目的。

2. 享元模式在文本编辑器中的应用

弄懂了享元模式的原理和实现之后,我们再来看另外一个例子:如何利用享元模式来优化文本编辑器的内存占用?

你可以把这里提到的文本编辑器想象成 Office 的 Word。不过,为了简化需求背景,我们假设这个文本编辑器只实现了文字编辑功能,不包含图片、表格等复杂的编辑功能。对于简化之后的文本编辑器,我们要在内存中表示一个文本文件,只需要记录文字和格式两部分信息就可以了,其中,格式又包括文字的字体、大小、颜色等信息。

尽管在实际的文档编写中,我们一般都是按照文本类型(标题、正文……)来设置文字的格式,标题是一种格式,正文是另一种格式等等。但是,从理论上讲,我们可以给文本文件中的每个文字都设置不同的格式。为了实现如此灵活的格式设置,并且代码实现又不过于太复杂,我们把每个文字都当作一个独立的对象来看待,并且在其中包含它的格式信息。具体的代码示例如下所示:

public class Character {//文字
    private char c;
    private Font font;
    private int size;
    private int colorRGB;
    
    public Character(char c, Font font, int size, int colorRGB) {
        this.c = c;
        this.font = font;
        this.size = size;
        this.colorRGB = colorRGB;
    }
}

public class Editor { //编辑器
    private List<Character> chars = new ArrayList<>();
    
    public void appendCharacter(char c, Font font, int size, int colorRGB) {
        Character character = new Character(c, font, size, colorRGB);
        chars.add(character);
    }
}

在文本编辑器中,我们每敲一个文字,都会调用 Editor 类中的 appendCharacter() 方法,创建一个新的 Character 对象,保存到 chars 数组中。如果一个文本文件中,有上万、十几万、几十万的文字,那我们就要在内存中存储这么多 Character 对象。那有没有办法可以节省一点内存呢?

实际上,在一个文本文件中,用到的字体格式不会太多,毕竟不大可能有人把每个文字都设置成不同的格式。所以,对于字体格式,我们可以将它设计成享元,让不同的文字共享使用。按照这个设计思路,我们对上面的代码进行重构。重构后的代码如下所示:

//享元类
public class CharacterStyle {
    private Font font;
    private int size;
    private int colorRGB;
    
    public CharacterStyle(Font font, int size, int colorRGB) {
        this.font = font;
        this.size = size;
        this.colorRGB = colorRGB;
    }
    
    @Override
    public boolean equals(Object o) {
        CharacterStyle otherStyle = (CharacterStyle) o;
        return font.equals(otherStyle.font) && size == otherStyle.size && colorRGB == otherStyle.colorRGB;
    }
}

//工场方法
public class CharacterStyleFactory {
    //通过List来实现共享
    private static final List<CharacterStyle> styles = new ArrayList<>();
    
    public static CharacterStyle getStyle(Font font, int size, int colorRGB) {
        CharacterStyle newStyle = new CharacterStyle(font, size, colorRGB);
        for (CharacterStyle style : styles) {
            //判断新传入的样式是否是已经存在的
            if (style.equals(newStyle)) {
                return style;
            }
        }
        styles.add(newStyle);
        return newStyle;
    }
}

public class Character {
    private char c;
    private CharacterStyle style;
    
    public Character(char c, CharacterStyle style) {
        this.c = c;
        this.style = style;
    }
}

//编辑器
public class Editor {
    private List<Character> chars = new ArrayList<>();
    
    public void appendCharacter(char c, Font font, int size, int colorRGB) {
        Character character = new Character(c, CharacterStyleFactory.getStyle(font,size,colorRGB));
        chars.add(character);
    }
}

3. 享元模式 vs 单例、缓存、对象池

在上面的讲解中,我们多次提到“共享”“缓存”“复用”这些字眼,那它跟单例、缓存、对象池这些概念有什么区别呢?我们来简单对比一下。

3.1 区别于单例

在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。实际上,享元模式有点类似于之前讲到的单例的变体:多例。

我们前面也多次提到,区别两种设计模式,不能光看代码实现,而是要看设计意图,也就是要解决的问题。尽管从代码实现上来看,享元模式和多例有很多相似之处,但从设计意图上来看,它们是完全不同的。应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数。

3.2 区别于缓存

在享元模式的实现中,我们通过工厂类来“缓存”已经创建好的对象。这里的“缓存”实际上是“存储”的意思,跟我们平时所说的“数据库缓存”“CPU 缓存”“MemCache 缓存”是两回事。我们平时所讲的缓存,主要是为了提高访问效率,而非复用。

3.3 区别于对象池

对象池、连接池(比如数据库连接池)、线程池等也是为了复用,那它们跟享元模式有什么区别呢?

你可能对连接池、线程池比较熟悉,对对象池比较陌生,所以,这里我简单解释一下对象池。像 C++ 这样的编程语言,内存的管理是由程序员负责的。为了避免频繁地进行对象创建和释放导致内存碎片,我们可以预先申请一片连续的内存空间,也就是这里说的对象池。

每次创建对象时,我们从对象池中直接取出一个空闲对象来使用,对象使用完成之后,再放回到对象池中以供后续复用,而非直接释放掉。

虽然对象池、连接池、线程池、享元模式都是为了复用,但是,如果我们再细致地抠一抠“复用”这个字眼的话,对象池、连接池、线程池等池化技术中的“复用”和享元模式中的“复用”实际上是不同的概念。

池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。

享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间

也就是“共享使用”和“重复使用的区别”

4. 享元模式在 Java Integer 中的应用

我们先来看下面这样一段代码。你可以先思考下,这段代码会输出什么样的结果。

Integer i1 = 56;
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2);
System.out.println(i3 == i4);

如果不熟悉 Java 语言,你可能会觉得,i1 和 i2 值都是 56,i3 和 i4 值都是 129,i1 跟 i2值相等,i3 跟 i4 值相等,所以输出结果应该是两个 true。这样的分析是不对的,主要还是因为你对 Java 语法不熟悉。要正确地分析上面的代码,我们需要弄清楚下面两个问题:

  1. 如何判定两个 Java 对象是否相等(也就代码中的“==”操作符的含义)?
  2. 什么是自动装箱(Autoboxing)和自动拆箱(Unboxing)?

所谓的自动装箱,就是自动将基本数据类型转换为包装器类型。所谓的自动拆箱,也就是自动将包装器类型转化为基本数据类型。具体的代码示例如下所示:

Integer i = 56; //自动装箱
int j = i; //自动拆箱

数值 56 是基本数据类型 int,当赋值给包装器类型(Integer)变量的时候,触发自动装箱操作,创建一个 Integer 类型的对象,并且赋值给变量 i。其底层相当于执行了下面这条语句:

Integer i = 59;底层执行了:Integer i = Integer.valueOf(59);

反过来,当把包装器类型的变量 i,赋值给基本数据类型变量 j 的时候,触发自动拆箱操作,将 i 中的数据取出,赋值给 j。其底层相当于执行了下面这条语句:

int j = i; 底层执行了:int j = i.intValue();

弄清楚了自动装箱和自动拆箱,我们再来看,如何判定两个对象是否相等?不过,在此之前,我们先要搞清楚,Java 对象在内存中是如何存储的。我们通过下面这个例子来说明一下。

User a = new User(123, 23); // id=123, age=23

针对这条语句,我画了一张内存存储结构图,如下所示。a 存储的值是 User 对象的内存地址,在图中就表现为 a 指向 User 对象。

当我们通过“==”来判定两个对象是否相等的时候,实际上是在判断两个局部变量存储的地址是否相同,换句话说,是在判断两个局部变量是否指向相同的对象。

了解了 Java 的这几个语法之后,我们重新看一下开头的那段代码。

Integer i1 = 56;
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2);
System.out.println(i3 == i4);

前 4 行赋值语句都会触发自动装箱操作,也就是会创建 Integer 对象并且赋值给 i1、i2、i3、i4 这四个变量。根据刚刚的讲解,i1、i2 尽管存储的数值相同,都是 56,但是指向不同的 Integer 对象,所以通过==来判定是否相同的时候,会返回 false。同理,i3==i4 判定语句也会返回 false

不过,上面的分析还是不对,答案并非是两个 false,而是一个 true,一个 false。看到这里,你可能会比较纳闷了。实际上,这正是因为 Integer 用到了享元模式来复用对象,才导致了这样的运行结果。当我们通过自动装箱,也就是调用 valueOf() 来创建 Integer 对象的时候,如果要创建的 Integer 对象的值在 -128 到 127 之间,会从 IntegerCache 类中直接返回,否则才调用 new 方法创建。看代码更加清晰一些,Integer 类的 valueOf() 函数的具体代码如下所示:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

IntegerCache是一个静态内部类

实际上,这里的 IntegerCache 相当于,我们讲的生成享元对象的工厂类,只不过名字不叫 xxxFactory 而已。我们来看它的具体代码实现。这个类是 Integer 的内部类,你也可以自行查看 JDK 源码。

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

为什么 IntegerCache 只缓存 -128 到 127 之间的整型值呢?

在 IntegerCache 的代码实现中,当这个类被加载的时候,缓存的享元对象会被集中一次性创建好。毕竟整型值太多了,我们不可能在 IntegerCache 类中预先创建好所有的整型值,这样既占用太多内存,也使得加载 IntegerCache 类的时间过长。所以,我们只能选择缓存对于大部分应用来说最常用的整型值,也就是一个字节的大小(-128 到 127 之间的数据)。

实际上,JDK 也提供了方法来让我们可以自定义缓存的最大值,有下面两种方式。如果你通过分析应用的 JVM 内存占用情况,发现 -128 到 255 之间的数据占用的内存比较多,你就可以用如下方式,将缓存的最大值从 127 调整到 255。不过,这里注意一下,JDK 并没有提供设置最小值的方法

方法一:

-Djava.lang.Integer.IntegerCache.high=255

方法二:

-XX:AutoBoxCacheMax=255

现在,让我们再回到最开始的问题,因为 56 处于 -128 和 127 之间,i1 和 i2 会指向相同的享元对象,所以 i1==i2 返回 true。而 129 大于 127,并不会被缓存,每次都会创建一个全新的对象,也就是说,i3 和 i4 指向不同的 Integer 对象,所以i3==i4返回 false

实际上,除了 Integer 类型之外,其他包装器类型,比如 Long、Short、Byte 等,也都利用了享元模式来缓存 -128 到 127 之间的数据。比如,Long 类型对应的 LongCache 享元工厂类及 valueOf() 函数代码如下所示:

private static class LongCache {
    private LongCache(){}

    static final Long cache[] = new Long[-(-128) + 127 + 1];

    static {
        for(int i = 0; i < cache.length; i++)
            cache[i] = new Long(i - 128);
    }
}

public static Long valueOf(long l) {
    final int offset = 128;
    if (l >= -128 && l <= 127) { // will cache
        return LongCache.cache[(int)l + offset];
    }
    return new Long(l);
}

在我们平时的开发中,对于下面这样三种创建整型对象的方式,我们优先使用后两种。

Integer a = new Integer(123);
Integer a = 123;
Integer a = Integer.valueOf(123);

第一种创建方式并不会使用到 IntegerCache,而后面两种创建方法可以利用IntegerCache 缓存,返回共享的对象,以达到节省内存的目的。举一个极端一点的例子,假设程序需要创建 1 万个 -128 到 127 之间的 Integer 对象。使用第一种创建方式,我们需要分配 1 万个 Integer 对象的内存空间;使用后两种创建方式,我们最多只需要分配256 个 Integer 对象的内存空间。

5. 享元模式在 Java String 中的应用

刚刚我们讲了享元模式在 Java Integer 类中的应用,现在,我们再来看下,享元模式在Java String 类中的应用。同样,我们还是先来看一段代码,你觉得这段代码输出的结果是什么呢?

String s1 = "念心卓";
String s2 = "念心卓";
String s3 = new String("念心卓");

System.out.println(s1 == s2);
System.out.println(s1 == s3);

上面代码的运行结果是:一个 true,一个 false。跟 Integer 类的设计思路相似,String 类利用享元模式来复用相同的字符串常量(也就是代码中的“念心卓”)。JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作“字符串常量池”。

不过,String 类的享元模式的设计,跟 Integer 类稍微有些不同。Integer 类中要共享的对象,是在类加载的时候,就集中一次性创建好的。但是,对于字符串来说,我们没法事先知道要共享哪些字符串常量,所以没办法事先创建好,只能在某个字符串常量第一次被用到的时候,存储到常量池中,当之后再用到的时候,直接引用常量池中已经存在的即可,就不需要再重新创建了。


文章作者: 念心卓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 念心卓 !
  目录