龙空技术网

Java设计模式系列之享元模式

尚硅谷教育 305

前言:

现在同学们对“java纸牌”可能比较重视,看官们都需要剖析一些“java纸牌”的相关知识。那么小编在网络上汇集了一些有关“java纸牌””的相关知识,希望各位老铁们能喜欢,咱们一起来学习一下吧!

在讲解本篇文章之前,想跟大家做个小调研,从你开始写第一句Java代码到现在,你使用过最多的数据类型是什么?我相信大家的答案肯定会有很多种,不同同学的答案也肯定会存在差异。所以呢,我说一种数据类型,我相信大家都不会反对,也会引起共鸣,可能不会夸张到是每个人都是使用最多次数的,但是呢,相信大家使用的频率也是数一数二的。

那么这种类型就是String,即字符串类型,作为一个Java工程师,这种类型我们是再熟悉不过了,几乎我们每完成一段代码都会一次甚至多次出现到它的身影。那么今天为什么在讲解正题之前会提这种类型呢,那是因为String这种类型就和今天讲得享元模式密切相关,或者说String类底层就是通过享元模式进行完成的。

再问大家一个问题,说起String你会想起什么呢?是会想起它那些比较常用的API,比如使用的比较频繁的split、substring、startWith、contains等等呢,还是会想起它独特的两种创建对象的方式呢?

其实String类最独特的一点就是通过字面量创建的对象是存储在常量池中的,比如此时代码String s = "hello",此时它存储的结构是这样的:

此时呢,再定义一个String s1 = "hello"。这个时候呢,由于String内部的缓存机制,也源于常量池没有对象就创建,有则复用的原则,此时常量池并不会创建出第二个hello对象,而是让s1引用上一步创建好的hello内存地址即可。如下图所示:

除此之外呢,像Java中,很多地方都用到过“池”的概念。比如数据库连接池,线程池以及缓存池等等,在拿到对应的连接的过程是这样的:

我们提到过的这些技术都有个很明显的共性,那就是通过共享对象或者说共享内存来解决系统开销问题,充分利用系统的内存与性能,从而达到用最小的系统开销去适应更多的数据响应。

像这种场景不光是开发设计中,在我们生活中应用的场景也是比比皆是。比如共享单车以及共享充电宝的推出,都是让一批资源循环以及多次利用从而更大限度的满足绝大数用户需求。还有就是我们经常在自己闲暇的时候观看游戏直播,作为直播开发方并不可能为每个用户提供一个独立的直播间,一是成本太高,而是对系统资源消耗实在太大,实现起来是根本没有必要或者不现实的,所以呢,为了解决资源消耗问题,就让用户共享一个直播间即可。

说了这么多,也举了那么多案例,那么接下来主要来讲解一下今天的主题。也就是享元模式,这种设计模式实际上在开发场景使用的并不是很多,导致大多数小伙伴可能对它并不是很熟悉。但是呢,它的思想同样是比较重要的,能够被列入23种设计模式,说明也是有它一定的道理的。

享元模式这个名词可能不像其他设计模式一样直接见名知义,可能不像代理模式、单例模式那样一眼看到就知道是干什么的。接下来解释一下享元这个词的含义,享为共享的意思,元就是对象的意思。通俗得来讲享元模式就是共享对象的意思,这种模式一般在系统底层优化使用得比较多,比如前面说过的String类底层、线程池以及缓存池底层。

接下来呢,我们通过一个案例来体会一下享元模式的魅力:

一、享元模式

概念:在前面的讲解中,我们知道了享元模式的核心就是共享对象,通过来回利用对象的内部细节创建更少的对象的方式供外部使用,进而降低系统的内存消耗来提升系统性能。接下来呢,来看一下在享元模式中的一些内部角色:

抽象享元类:在享元模式中,存在一个抽象的享元父类,其中一共包含两部分,第一部分为可共享的内部状态,也就是不随着外部而改变的部分;第二部分为不可共享的外部状态,即可能会随时外部环境随时发生改变的部分。

具体享元类:抽象享元类的具体实现,它主要实现了抽象享元类的内部状态,对于外部状态我们需要外部提供,通常在调用该方法时通过参数传递的方式进行动态改变即可;

享元工厂类:主要用于创建享元对象的一个角色,其中聚合一种数据结构,用于起一个缓存以及复用作用。

讲解完享元模式的内部角色后,是不是感觉有点懵,完全不知道在说些什么,接下来我们通过一个案例实现享元模式相信你就能够明白了。

1.2享元模式案例

大家都玩过扑克牌,那就都知道扑克牌抛弃掉大小王后还剩下52张卡片,分别为A-K。对于卡片来说,每张卡片都会有点数以及花色两种属性,所以根本不可能存在两张一模一样的卡片。如果是通过原始面向对象的方式,因为每张牌不一样,需要创建一个卡片类,定义两个属性,然后需要创建52个对象分别表示不同的卡片。但是呢,这样一来呢,就会存在大量的对象,每个对象肯定定会占据一定的内存,那么呢,有没有一种方式,可以减少对象的内存开销呢,就是对这个过程进行改造,创建更少的对象根据不同的表现进行复用呢。

比如我们通过最原始面向对象的方式;

package com.ignorance.flyweght;

import java.util.ArrayList;

import java.util.List;

public class Demo {

public static void main(String[] args) {

String[] colorTypeArray = {"黑桃","红桃","梅花","方块"};

String[] pointArray = {"A","2","3","4","5","6","7","8","9","10","J","Q","K"};

List<Card> cardList = new ArrayList<>();

for (String colorType : colorTypeArray){

for (String point : pointArray){

cardList.add(new Card(colorType,point));

}

}

cardList.forEach(System.out::println);

}

}

这种方式我相信是所有人都能够想到的方式也是比较容易想到的方式,但是这种情况我们说过会大量创建对象,造成内存浪费。

那么我们怎么优化呢,其实编程设计往往都是来源于生活,比如我们早上去买豆浆,因为每个人的口味不一样,有的人喜欢吃加糖的,有的人喜欢吃加辣的。卖主不可能每种口味都做一锅,第一是浪费精力,其次就是万一第二天某种口味不受欢迎就会造成极大的浪费。所以卖主在出摊前会做好一锅纯豆浆,然后售卖时根据顾客的口味,只需在原有的纯豆浆中加入相应的调料即可。这样一来呢,卖主就不会担心某种豆浆因为不收欢迎而造成浪费,也不会担心豆浆的口味做得太少而造成亏损。相比较做纯豆浆而言,能够充分利用豆浆,卖的时候针对每位顾客的口味不同,只需要调好对应的调料就可以了。

上面的例子我相信对大家来说应该很好理解,都说享元模式中有两个重要的概念,一个是共享的内部状态,其次就是不可变的外部状态。这个光看理论是不是大家都不知道说得是什么,我们不妨来分析一下卖豆浆案例,其实对于卖家来说,他能够保证得是做好一锅纯豆浆,对他来说做好一锅纯豆浆是能够控制的,所以说呢,这个时候内部状态则为纯豆浆,什么是外部状态呢?就是对卖家来说,他不能够把握顾客会喜欢哪种口味的豆浆,所以对卖家来说,顾客的口味需求就是可变的外部状态。

下面再分析一下扑克牌案例,对于扑克牌来说每种牌点的花色是固定的,即黑桃、红桃、梅花以及方块,所以可以把四种花色看作共享状态。那对每种花色而言,会根据当前的牌点范围又是在不断变化的,所以可以将扑克牌的点数看作不共享的外部状态。

那么接下来我们就使用享元模式的思想对扑克牌案例进行改进:

第一步:创建享元抽象类

package com.ignorance.flyweght;

public abstract class FlyWeightAbstactCard {

protected String cardPoint;

protected String colorType;

//花色为内部共享状态,将其设置为抽象方法

public abstract void setColorType();

//牌点为不共享的外部状态

public void setCardPoint(String cardPoint){

this.cardPoint = cardPoint;

}

public void show(){

System.out.println(colorType + ":" + cardPoint);

}

}

在抽象类中,主要定义了花色和牌点属性。前面分析了将花色看为可共享的内部状态,所以将花色的赋值设置为抽象方法,牌点为不可共享状态,将其设置为非抽象方法,并且带有形参,具体的值需要从外部调用传入进来。

接下来呢,分析花色一共四种,分别为黑桃、红桃、梅花以及方块,所以下一步需要创建四个享元子类去继承上面的抽象父类,并对共享状态方法,也就是花色赋值方法进行实现。如下所示:

第二步:创建黑桃花色子类

package com.ignorance.flyweght;

public class Spade extends FlyWeightAbstactCard {

@Override

public void setColorType() {

super.colorType = "黑桃";

}

}

第三步:创建红桃花色子类:

package com.ignorance.flyweght;

public class RedHeart extends FlyWeightAbstactCard{

@Override

public void setColorType() {

super.colorType = "红桃";

}

}

第四步:创建梅花花色子类

package com.ignorance.flyweght;

public class PlumBlossom extends FlyWeightAbstactCard {

@Override

public void setColorType() {

super.colorType = "梅花";

}

}

第五步:创建方块花色子类

package com.ignorance.flyweght;

public class Block extends FlyWeightAbstactCard {

@Override

public void setColorType() {

super.colorType = "方块";

}

}

定义好享元子类后,接下来需要定义享元工厂,用于创建真正的共享对象。

第六步:创建享元工厂

package com.ignorance.flyweght;

import java.util.HashMap;

import java.util.Map;

public class FlyWeightCardFactory {

private static final Map<String,FlyWeightAbstactCard> CARD_CACHE_MAP = new HashMap<>();

public static FlyWeightAbstactCard getInstance(String colorType){

if (!CARD_CACHE_MAP.containsKey(colorType)){

if ("黑桃".compareTo(colorType) == 0){

CARD_CACHE_MAP.put("黑桃",new Spade());

}else if("红桃".compareTo(colorType) == 0){

CARD_CACHE_MAP.put("红桃",new RedHeart());

}else if("梅花".compareTo(colorType) == 0){

CARD_CACHE_MAP.put("梅花",new PlumBlossom());

}else if("方块".compareTo(colorType) == 0) {

CARD_CACHE_MAP.put("方块",new Block());

}

}

return CARD_CACHE_MAP.get(colorType);

}

}

下面呢,主要来分析一下以上的代码,在享元工厂中,定义了一个Hash结构的缓存对象CARD_CACHE_MAP,主要是模拟“池”的作用。通过这个map,可以将所有需要创建的对象进行缓存,第二次使用到相似对象时就不需要再重新创建,之间从内存中获取即可,这样一来呢,不仅效率得到了提高,内存相比较而言也得到了有效的利用。

在该类中,同样定义了一个getInstance()方法,主要供外部调用,用于给外部返回一个合适的享元对象。在该方法内部中,每次都优先从缓存对象中获取,如果不存在则创建,并将其加入到缓存对象中。这样一来了,第二次再调用getInstance()方法时,就不用再重复创建对象,从而让内存得到了有效利用,防止了进一步去创建对象,消耗内存。

第七步:进行测试

package com.ignorance.flyweght;

import java.util.ArrayList;

import java.util.List;

public class Demo {

public static void main(String[] args) {

FlyWeightAbstactCard instance = FlyWeightCardFactory.getInstance("黑桃");

instance.setColorType();

instance.setCardPoint("3");

instance.show();

FlyWeightAbstactCard instance1 = FlyWeightCardFactory.getInstance("黑桃");

instance1.setColorType();

instance1.setCardPoint("K");

instance1.show();

System.out.println(instance == instance1);

FlyWeightAbstactCard instance2 = FlyWeightCardFactory.getInstance("方块");

instance2.setColorType();

instance2.setCardPoint("K");

instance2.show();

System.out.println(instance == instance2);

}

}

下面呢,我们再看一下运行结果:

可以看出,通过我们的案例,不管扑克牌有多少张,自始至终程序最多只会创建四个对象。即黑桃、红桃、梅花以及方块。每次在获取一张扑克牌时,它的花色只会是这四种之一,所以说呢,只需要获取到对应的花色,根据外部的需要给花色动态的填充上点数即可。就好比前面的豆浆案例,只需要定义一个没有任何污染的对象,每次都会根据这个对象改造成最后真正需要的对象。从而让对象本身得到复用,从而避免了内存的消耗以及浪费。

二、享元模式在源码中的使用

上面呢,讲解了享元模式的思想以及小案例,接下来呢,我们来看一下在Java中,有哪些地方使用到了享元模式。

2.1Integer缓存

Integer类作为在开发中比较常用的类,我相信大家对它并不是很陌生,接下来呢,一起来看一段代码:

package com.ignorance.flyweght;

public class IntegerDemo {

public static void main(String[] args) {

Integer num1 = 127;

Integer num2 = 127;

Integer num3 = 128;

Integer num4 = 128;

System.out.println(num1 == num2);

System.out.println(num3 == num4);

}

}

以上代码比较简单,定义了四个Integer对象,然后判断各个对象之间的关系。在Java语言中,针对八种基本类型,JDK提供了每种类型所对应的包装类型,从而让Java成为真正意义上的万物皆对象。

在上述代码中,127和128都是int类型,在将其赋值为Integer类型时,会自动触发装箱操作,同时创建一个Integer类型的对象,将其赋值给对应的引用。

这个时候通过面向对象的知识就知道,每次创建对象它们的内存地址是不相同的,使用双等于进行比较对于引用类型而言比较的是他们的内存地址是否一致。在上面的代码中,可以很明显的知道,此时是创建了四个对象的,那么它们的内存地址应该就是不同的。接下来呢,我们带着推断结果验证一下代码执行结果:

可以看出,程序的执行结果和我们想得还是有一定差距,同样都是Integer对象,为什么127就是true,而128就是false呢?为了搞懂这个问题,我们就需要通过研究源码去找到答案,值得一提的是,Integer类型通过字面值赋值时,是通过调用Integer的valueOf方法进行创建对象的,所以说呢,我们需要进入这个方法进行研究:

通过以上的源码截图来看,我们可以很轻松的知道,Integer设计时并不是每次都会返回new的Integer对象,每次返回对象都会有一个条件限制。即需要判断当前i的值是否在两个常量之间,接下来我们具体来看一下这两个常量所对应的值是多少。

可以看出这个返回值就是在-128-127之间,在Integer底层,对这一区间的整数是做了缓存的,即有就复用,无则创建的原则。对于我们之前的num1对象来说,此时没有任何对象,所以就会创建一个对象,并将其加入到缓存中,第二次再创建num2时,此时发现当前值在-128-127之间,且缓存池存在相应的享元对象,所以就直接返回,此时num1和num2指向的是同一个内存地址,所以num1和num2的比较结果就为true,同理128已经超过了缓存池的范围,每次都会创建新的对象,所以说num3和num4的比较结果为false。

总结

在本篇文章中呢,我们讲解了一种新的设计模式:享元模式。说白一点呢,就是共享对象,通过共享技术来节约系统内存,进而提高内存使用率。这种模式我们很多同学比较陌生,但是呢,讲完后,我们发现很多学过的技术底层都是通过享元模式的思想来节约内存的,比如我们前面提到过的String类、Integer类,还有就是连接池技术。

今天讲得知识并不是很复杂,我相信代码大家一看都懂,缓存思想我相信大家一定不是第一次接触了,比如Mybatis的一二级缓存,redis缓存等等。学会这种思想,对我们优化程序的执行还是具有比较重要的意义的。

标签: #java纸牌