蔡不菜丶

蔡不菜丶 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

搬砖爱好者
@公众号:小菜良记

个人动态

蔡不菜丶 发布了文章 · 2月18日

写了挺久的代码,却还被"异常"支配?

大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚!
死鬼~看完记得给我来个三连哦!

本文主要介绍 Java 中的异常

如有需要,可以参考

如有帮助,不忘 点赞

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

面试官

请说一下你平时比较常遇到的运行时异常

小菜

好的,我平时比较常遇到的异常有:NullPointException (空指针异常) 、ClassNotCastException (类型转换异常) 、IndexOutOfBoundException (下标越界异常),emmm.... 还有些忘记名字了~

面试官

那你说下异常的分类吧

小菜

异常好像是分为运行时异常和 ... ,不好意思,有点想不起来了

面试官

emm, 还有个编译时异常,那你平时写代码提示有异常是怎么处理的

小菜

额,这个, 一般都会直接抛出异常

面试官内心OS就这水平?

小菜内心OS伤害性不高,侮辱性极强

对于常年奋战在代码一线的我们来说,说几个常见异常就像让你数一下你有多少钱一样,虽然有,但是说出来不多。又很轻蔑的觉得,这什么面试题,就这?还能当面试官。但是往往这么简单的问题,你答的不好,一样能让你 面试等通知,录取砍薪资

我们从吐槽中回过神来想一想,自己写的代码还没点 x 数吗,异常、bug 不就是自己的精神伴侣吗,没这点东西的支撑,自己平时怎么冠冕堂皇的划水呢!

是什么导致我们平时遇到的异常很多,却记不起几个。是因为实在太多了,让自己记不住吗!还是习惯了百度呢~ emmm,估计都有,小菜心虚了,赶紧奋笔,摆脱被异常支配的烦恼。电子设备面前的你,为了更有底气的回答上面那几个问题,不妨跟小菜再来复习下 异常 吧!

走进异常

异常就是有异于常态,和正常情况不一样,有错误出现。在 Java 中,阻止当前方法或作用域的情况,称之为异常。我们先来看下异常的结构:

Throwable 作为顶层父类,派生出了 ErrorException 两个子类。

  • Error:错误。Error 类以及它的子类的示例,代表了 JVM 本身的错误,错误不能被程序员通过代码处理,Error 一般很少出现。
  • Exception:异常。Exception 类以及它的子类,代表程序运行时发送的各种不期望发生的时间。可以被 Java 异常 处理机制使用,是异常处理的核心。

我们本文重点关注 Exception

Java 的基本理念是 "结构不佳的代码不能运行"

异常使用

一个简单处理异常的例子:

if(t == null){
    throw new NullPointException();
}

当我们需要引用对象 t,但是有可能 t 对象尚未被初始化,所以在使用这个对象之前,我们会对引用进行检查。可以创建一个代表错误信息的对象,并且将它从当前环境中 “抛出”,这样就把错误信息传播到了 “更大” 的环境中,这种称为 抛出一个异常。这样的好处便是当前环境下就不必再为这个问题操心了,它将会在别的地方得到处理。

异常参数

异常对象与其他 Java 对象一样,都可以通过 new 关键字在 堆上 创建异常对象,因此,这也伴随着存储空间的分配和构造器的调用。

所有标准的异常类都有两个构造器,一个是 默认构造器, 一个是 接受字符串作为参数的构造器 这样子我们能把相关的异常信息放入异常对象的构造器中:

throw new NullPointException("t 对象为空");

通过这样子抛出异常,排查者也能快速的定位问题

我们还可以简单地把异常处理看成一种不同的返回机制:

尽管返回的异常对象其类型与方法设计的返回类型不同,但是从效果上看,它就像从方法中返回的。

异常捕获

在编写代码处理异常时,对于检查异常,有2种不同的处理方式:使用try…catch…finally语句块处理它;或者在函数签名中使用throws声明交给函数调用者去解决。

try 的译思便是 尝试,那么是尝试做什么呢?我们知道如果在方法内部抛出了异常(或者在方法内调用的其他方法抛出了异常),这个方法将会在抛出异常的过程中结束。我们有时候不想这么轻易结束,这个时候就用到了 尝试 的概念,我们可以在方法内设置一个特殊的块来捕获异常,在这个块中 "尝试" 各种(可能产生异常的)方法调用,所以我们将其称之为 try 块

有了异常处理机制,我们可以把所有可以产生异常的动作都放进 try 块 里面,然后只需一个地方就可以捕获所有异常。

但是,这里特别需要注意的是,不要滥用异常!!! 有些人可能有点小聪明,编写了以下代码:

咋看代码可以你觉得很奇怪,为什么有人会优先使用基于异常的循环,大部分会这样写的都会以为错误判断机制性能会比较高,因为 JVM 对每次数组访问都要检查是否越界。

注: 异常应该只用于异常的情况下,它们永远不应该用于正常的控制流,设计良好的 API 不应该强迫它的客户端为了正常的控制流而使用异常

Java 中提供了三种可抛出结构(throwable) : 受检异常(checked exception)、运行时异常(run-time exception)和错误(error)。我们在写代码的时候往往会有所纠结,到底该抛出何种结构?

在决定使用受检异常或者使用未受检异常的时候,我们的主要原则应该是 :如果期望调用者能够适当地恢复程序,这种情况下我们就应该使用受检异常。通过抛出受检异常,我们应该在一个 catch 子句中处理该异常,或者将它传播出去,让调用者处理。

运行时异常错误 都属于 非受检可抛出结构。它们都是不需要也不应该被捕获的可抛出结构。当程序抛出可受检结构的时候,就意味着当前情况属于不可恢复的,如果程序没有捕捉到这样的可抛出结构,将会导致当前线程中断。

我们常用 运行时异常 来表明编程错误。我们实现的所有未受检抛出结构都应该是 RuntimeException 的子类。不应该定义 Error 的子类,虽然 Java 规范 中没有明确要求如此,但是 Error 往往是被 JVM 保留下来使用的,以表明资源不足,约束失败,或者其他使程序无法继续执行的条件。

自定义异常

我们不必深陷 Java 已有的异常类型而无法自拔。 Java 提供的异常体系只是包含了基本的异常,不可能预见所有值得报告的错误。所以我们可以自己定义异常类来表示程序中可能会遇到的特定问题。

要自己定义异常类,就必须从已有的异常类中集成,最好是选择意思相近的异常类继承,但是这并不是一个简单的选择~

我们上面只是简单继承了 Exception ,构造函数中无法传入我们想要表达的错误报告,实现这种方式也很简单,我们只需要为异常类定义一个接受字符串参数的构造器:

getMessage() 方法有点类似于 toString(),可以获取异常类更加详细的信息。

栈轨迹

我们平时可以通过打 断点 的方式来调试代码,跟着代码一行一行的走下去,这是因为栈帧 的帮组。当有异常抛出的时候我们也想要有更加详细的信息来追溯异常的源头。

e.printStackTrace() 这个异常的方式是我们捕获异常的时候,系统会自动为我们生成,它的输出格式如下:

当异常的栈轨迹过长时,控制台会刷出一列下来的错误信息,不知道为什么,每次看到这种信息总有种心烦的感觉,真糟糕~ 不知道小伙伴有没有一样的感触。

我们既然不想要这种输出格式,又想要追溯异常的源头,小伙子有够贪心的~

这里便推荐使用 e.getStackTrace() 方法来获取信息。这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每个元素都表示栈中的一帧。数组第一个元素表示的是栈顶元素,并且是调用序列中的最后一个方法调用;数组最后一个元素是调用序列中的第一个方法调用。

image-20210202214952126

这个数组中的元素是 StackTraceElement 类型,我们还可以看下这个类中有哪些API,我们也可以单独输出调用栈方法的方法名:

image-20210202215210524

异常链

我们可以在捕获一个异常后抛出另一个异常,并且希望将原始异常的信息保存下来,这个称之为异常链。

JDK 1.4 之前,开发人员必须自己编写代码来保存原始异常的信息。而现在所有 Throwable 的子类在构造器中都可以接受一个 cause 对象来作为参数,如上述那样 throw new Exception(e)。这个 cause 就用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链追踪到最初发生的位置。

标准异常

优先使用标准异常
专家级程序员小菜 最主要的区别在于,专家追求并且通常也能够实现高度的代码重用。代码重用 并非谈之尔尔,这是一条通用的规则,异常当然也不例外。Java 平台类库中提供了一组基本的未受检异常,它们满足了绝大多数 API 的异常抛出需求。

为什么要重用标准的异常?

  • 使API更易于学习和使用,因为它与程序员已经熟悉的习惯用法一致
  • 对于用到这些API的程序而言,它们的可读性会更好,因为它们不会出现很多程序员不熟悉的异常
异常描述
NullPointerException访问 Null 对象的方法
IllegalStateException不适合方法调用的对象状态
IllegalArgumentException接收非法参数
IndexOutOfBoundsException下标参数值越界
ConcurrentModificationException禁止并发修改的情况下,检测到对象的并发修改
UnSupportedOperationException对象不支持用户请求的方法
IOException文件读写异常

以上便是我们平时比较常见的可重用异常,开发中应当不要直接用 ExceptionRuntimeExceptionThrowable 或者 Error 。对待这些异常要像对待抽象类一样,你无法可靠地测试这些异常,因为它们是一个方法可能抛出的其他异常的超类。

如果方法抛出的异常与它所执行的任务没有明显的联系,这种情形将会使人不知所措。这个弊端在于除了使排查者感到困惑之外,这也 "污染" 了具有实现细节的更高层的API。

为了避免这个问题,我们需要遵守:更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法称为 异常转移

因此我们不能为了简便,而直接捕获 Exception 这种异常的超类。

甚至不要忽略异常,我们有时候会以为这个方法不会抛出异常,而因为异常属于 受检异常,不得已我们需要捕获这个异常,但是又自作聪明得不在 catch 块中做任何处理操作。

要忽略一个异常非常容易,但是毫无疑问,你已经给自己埋下了一颗不知什么时候会爆炸而不知道何处爆炸的隐患。 空的 catch 块会使异常达不到应有的目的

如果我们一定要选择忽略异常,那么明确的做法应该是:在 catch 块中包含一条注释,说明为什么可以这样做,并且将变量名称命名为 ignored

派生异常

图中 Dog 类继承于 Animal 类,重写了 eat() 方法。当时在我们打算抛出异常的时候,却发现编译器提示报错。纳闷的同时,怀疑了一下这编译器是不是坏了?

事实不是这样的,在继承和覆盖的过程中,某个特定方法的"异常说明的接口"不是变大了而是变小了。这相当于,我父类的方法好好的,被你一继承居然出现了异常,而且我还可能不知道,这不是背地里砸我招牌吗!

finally 使用

对于一些代码,我们希望无论 try 块中的异常是否抛出,它们都能够得到执行。为了达到这个效果,我们可以在异常处理程序后面加上 finally 字句。

这个用处的第一想法便是用来做错误重试,我们可以把 try 块 放入一个循环中,然后加一个计数器或者别的装置,使循环在放弃之前能尝试一定的次数。

finally 内部,无论 try 块 中的代码从哪里返回,都会被执行,何以见得呢?

那么问题又来了!既然 finally 中的语句无论如何都会被执行,那我在 finally 中也有 return ,这个时候返回的是什么?我们不妨试一试。

不知道你是否做对了,答案是返回 finally 中的结果,由此可知:

try 中的 return 语句调用的函数先于 finally 中调用的函数执行,也就是说 try 中的return语句先执行,finally 语句后执行,但try中的 return 并不是让函数马上返回结果,而是 return 语句执行后,将把返回结果放置进函数栈中,此时函数并不是马上返回,它要执行 finally 语句后才真正开始返回。但此时会出现两种情况:

  1. 如果finally中也有return,则会直接返回finally中的return结果,并终止程序,函数栈中的return不会被完成
  2. 如果finally中没有return,则在执行完finally中的代码之后,会将函数栈中保存的try return的内容返回并终止程序

那么如果在 try 中抛出了异常,在 catch 中也有 return,结果又该如何?

没错!还是返回 finally 中的结果,答案已经揭晓,那么我们来总结一下:

1、不管有没有出现异常,finally块中代码都会执行
2、当try和catch中有return时,finally仍然会执行
3、finally是在try中return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在finally执行前确定的
4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值

异常使用指南

上面我们复习了一遍Java 中的异常,下面是一段来自 《Java 编程思想》 的摘要。

应该在下列情况下使用异常:

  • 在恰当的级别处理问题。(在知道该如何处理的情况下菜捕获异常)
  • 解决问题并且重新调用产生异常的方法
  • 进行少许修补,然后绕过异常发生的地方继续执行
  • 用别的数据进行计算,以代替方法预计会返回的值
  • 把当前运行环境下能做的事情尽量做完,然后把相同的异常抛到更高层
  • 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层
  • 终止程序
  • 进行简化(如果ID异常模式是问题变得太复杂,那用起来会非常痛苦也很烦人)
  • 让类库和程序更安全(这既是再为调式做短期投资,也是在微程序的剑专项做长期投资)

END

异常是 Java 程序设计不可分割的一部分,如果不了解如何使用它们,那你只能完成很有限的工作。这篇异常总结写着写着就挺长了,所以你也要读着读着就会了!路漫漫,小菜与你一同求索~

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。💋

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

每篇都是初恋的味道~

查看原文

赞 0 收藏 0 评论 0

蔡不菜丶 发布了文章 · 1月31日

吃透FastJSON,认准此文 !

大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚!
死鬼~看完记得给我来个三连哦!

本文主要介绍 FastJSON 的使用

如有需要,可以参考

如有帮助,不忘 点赞

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

JSON 介绍

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。它使得人们很容易的进行阅读和编写。同时也方便了机器进行解析和生成。它采用一种 "键 : 值" 对的文本格式来存储和表示数据,在系统交换数据过程中常常被使用,是一种理想的数据交换语言。

"XML 的时代已经过去,现在是 JSON 的时代" 。相信现在这个观点很多人已经默默认同,那么我们是否有认真思考过为什么现在 JSON 能够顶替 XML 的地位。我们来简单看下两种的表示方式:

<?xml version="1.0" encoding="gb2312"?>
<class>
    <stu id="001">
        <name>杨过</name> 
        <sex>男</sex>
        <age>20</age>
    </stu>  
    <stu id="002">
        <name>小龙女</name>    
        <sex>女</sex>
        <age>21</age>
    </stu>
</class>
[
    {
        "id": "001",
        "name": "杨过",
        "sex": "男",
        "age": "20"
    },
    {
        "id": "002",
        "name": "小龙女",
        "sex": "女",
        "age": "21"
    }
]

两种方式都是用来描述简单的班级信息,信息不多,但是明显可以看出 JSONXML 更加简洁。具体区别可为以下几点:

  • 可读性: JSON 和 XML 的可读性可谓不相上下,一边是简易的语法,一边是规范的标签形式,很难分出胜负
  • 可扩展性: XML 天生有很好的扩展性,JSON 当然也有,因此 XML 能扩展的,JSON 也可以扩展
  • 编码难度: XML 有丰富的编码工具,比如 DOM4J,JDom 等,JSON 也提供许多工具。但是在没有工具的情况下,因为 XML 有很多结构上的字符,编程难度相对较高。
  • 解码难度: XML 的解析需要考虑到子节点父节点,难度较大,而 JSON 的解析难度几乎为 0,看上去就能理解数据结构

JSON 认知

JSON 具有以下形式
  • JSON 对象

源网侵删

{
    "id": "002",
    "name": "小龙女",
    "sex": "女",
    "age": "21"
}

这就是一个简单的JSON 对象,我们观察可以得出 JSON 的一些语法:

  1. 数据在花括号中 []
  2. 数据以 键 : 值 对的形式出现(其中键多以字符串的形式出现,值可为字符串,数值,以及 JSON 对象)
  3. 每两个 键 : 值 对以逗号分隔 , , 最后一个键值对需省略 ,

我们按照上面的 3 点特征,便可很简单的构建出一个 JSON 对象

  • JSON 数组

源网侵删

["value1","value2","value3"]

[
    {
        "id": "001",
        "name": "杨过",
        "sex": "男",
        "age": "20"
    },
    {
        "id": "002",
        "name": "小龙女",
        "sex": "女",
        "age": "21"
    }
]

数组的表示方式也很简单:

  1. 头尾由 [] 包裹
  2. 数据主键以 , 隔开
  • JSON 字符串

源网侵删

'{"id": "001", "name": "杨过", "sex": "男", "age": "20"}'

JSON 字符串与 Java 的字符串非常相似。

  1. 它必须以 "" 或者 '' 包裹数据,支持字符串的各种操作
  2. 里面的数据格式可以为 json对象,也可以是 json数组亦或者是两个基本形式的组合变形

以上便是 JSON 的基本形式,JSON 可以使用于各种语言,每个语言皆有自己各自的 JSON 实现方式。下面我们主要来熟悉一下 Java 语言中的 FastJSON 的使用。

FastJSON

FastJSON 是由阿里巴巴工程师基于 JAVA 开发的一款 JSON 解析器和生成器,可用于将 Java 对象转换为其 JSON 表示形式,它还可以用于将 JSON 字符串转换为等效的 Java 对象。FastJSON 可以处理任意 Java 对象,包括没有源代码的预先存在的对象

FastJSON 使用十分方便,我们只需要在 Maven 工程的 pom 文件中引入以下依赖即可:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.73</version>
</dependency>

FastJSON API 的入口类是 com.alibaba.fastjson.JSON,常用的序列化操作都可以在JSON类上的静态方法直接完成。

API

我们已经在项目中引入 FastJSON 的依赖,和已经存在了一个用户类:

@Data
public class User {
    private int id;

    private String name;

    private String sex;
    
    private int age;
}
toJSONString(Object o)

这个方法平时最常见了,将JavaBean序列化成 JSON 文本

我们通过传入一个对象,便可以将对象转成 JSON 字符串,这里我们传入的不仅仅是 JavaBean 还可以是一个 Map 对象

传入一个 Map 对象 我们同样可以获取到一个 JSON 字符串。List 对象也很适用:

结果是一个标准的 JSONArray 的字符串

如果说 toJSONString(Object o) 的输出结果只有单调的一行让你看起来有点吃力,那么我们可以使用 toJSONString(Object o, boolean prettyFormat) 来让输出结果看起来舒服点:

通过 JSON 自带的格式化,让输出结果看起来更加清晰,真是贴心~

有小伙伴估计想着这两种我平时都用腻歪了,哪里有的着在你这看,小菜一想,言之有理。那就介绍一下 toJSONString 的扩展用法。

JSON.toJSONString(Object object, SerializerFeature... features)

我们可以看到这个方法里面有个参数 SerializerFeature...,可能感到有点陌生,确实,我也很陌生,我们先来看下什么是 SerializerFeature,通过源码我们可以发现 SerializerFeature 原来是个枚举类:

看到这张图我们不要晕,里面虽然有很多实例,但是大部分都被 @deprecated 注释了,说明这些已经废弃了。那有哪些是我们平时经常用到的呢:

对象描述
SerializerFeature.UseSingleQuotes使用单引号而不是双引号,默认为false
SerializerFeature.PrettyFormat结果是否格式化,默认为false
SerializerFeature.WriteDateUseDateFormat如果时间是data、时间戳类型,按照这种格式初始化时间 "yyyy-MM-dd HH:mm"
SerializerFeature.WriteMapNullValue是否输出值为null的字段,默认为false
SerializerFeature.WriteClassName序列化时写入类型信息,默认为false

使用案例:

  • SerializerFeature.UseSingleQuotes

使用单引号而不是双引号,默认为false

  • SerializerFeature.PrettyFormat

结果是否格式化,默认为false

  • SerializerFeature.WriteDateUseDateFormat

如果时间是data、时间戳类型,按照这种格式初始化时间 "yyyy-MM-dd HH:mm"

通过这种方式我们将日期输出成了固定的格式:yyyy-MM-dd HH:mm,有时候我们不想得到这种格式那该怎么办,办法总会有的:

这个方式支持自定义时间格式,但是用到方法是 toJSONStringWithDateFormat(),这里需要注意下,不要到时候用错了方法,还说小菜 渣男 ~

  • SerializerFeature.WriteMapNullValue

是否输出值为null的字段,默认为false

这个用什么用处了,我们应该很清楚开发规范中鼓励用JavaBean传递参数,尽量减少通过 Map 传递参数,因为 Map 相当于一个黑盒,对于使用者来说根本不知道里面存在哪些字段,而对于创建者来说估计也会忘记里面存在哪些字段,为了解决这个痛,JSON 也推出了解决方法:

通过普通方式的 toJSONString() 方法,空值仿佛被 吃掉 了,这很可能会成为一个开发灾难!

  • SerializerFeature.WriteClassName

序列化时写入类型信息,默认为false。这个方法可以在反序列化的时候用到,用法如下:

通过这样我们可以看到我们序列化的对象是什么类型的。

上面这些便是 toJSONString 的扩展用法,小伙伴们有没有满满的收获~

vx 搜:小菜良记

更多干货值得关注,每篇都是初恋的味道,钉~

  • parseObject(String text)

上面说到的是 序列化,那么对应的便是 反序列化

反序列化就是把JSON格式的字符串转化为Java Bean对象。

用法十分简单,可以将一个标准的 JSON 字符串 转为一个 JSONObject 对象,由于 JSONObject 类 实现了 Map 接口,因此我们可以通过 get() 来获取到值。

我们已经知道了 Map 的致命不足,所以我们更希望能得到一个 JavaBean 对象。

当然也是可以的!我们通过传入我们想要转换的对象类型,就可以得到我们想要的 JavaBean

除了 基本反序列化 之外,还有一种 泛型反序列化 可供使用

通过 泛型 ,我们就可以不用传入一个 Class 对象,而直接获取到我们的 JavaBean

FastJSON 序列化还有一个用处那便是进行 深克隆。有看过我前面文章的小伙伴们相信现在对软件设计模式都有一定的了解了,其中原型模式涉及到的 深克隆浅克隆

浅克隆的实现方式十分简单,我们只需要实现 Cloneable 接口,然后重写 clone() 方法 :

结果中我们看到的好人卡 都是属于小王的,这就是 浅克隆 的弊端的了。我们想要实现 深克隆 有许多种方式:

  • 手动为引用属性赋值
  • 借助 FastJSON
  • 使用 java 流的序列化对象

方法有许多,我们重点看下 FastJSON 的实现方式:

通过 FastJSON 的反序列化,我们得到的两个对象实际上是不同的,这也很方便的实现了 深克隆

更多设计模式的了解,各位请移位:

2021还不多学几种创建型模式,创建个对象!

图文并茂走进《结构型模式》,原来这么简单!

敲黑板了!《行为型模式》来袭

parseArray(String text)

这是一个将 JSON字符串 转为 JSONArray 的方法

同样我们也可以通过使用 泛型序列化 来实现同样的功能:

这种方式有个坑就是:我们使用 parseArray() 这个方法的时候第二个参数需要传入我们要反序列化的对象类型,但是我们这里需要传入的是数组,不知道你有没有为数组里放了两个一样的type感到奇怪?没错,这就是这个方法的坑,我们 List 里面有多少个对象, Type[] 这个数组里面的个数要与之匹配,不然会抛出以下错误:

但是如果一个 List 中存在多个不同类型的对象时,我们可以使用这个方法:

toJSONBytes(Object o)

JSON对象转换成Byte(字节)数组

我们平时在进行网络通讯的时候,需要将对象转为字节然后进行传输。我们使用字符串的时候,不由然的可以想到字符串中有个很便捷的 API 可以将字符串转为字节数组

String str = "小菜";
byte[] bytes = str.getBytes();

但是我们如果要将一个 JavaBean 对象转为字节数组的时候,我们得借助 ByteArrayOutputStream 流的帮助:

这种方式也可以很好的将 JavaBean 对象转为字节数组,但是代码不免有点多了!而 FastJSON 中也提供了很方便的 API 以供使用:

而我们要将字节数组转为对象,FastJSON 也同样支持:

parseObject()这个方法中我们又看到了一个奇怪的参数 Feature,我们点击进入源码可以发现这其实也是一个枚举类:

看的同样云里雾里的,这么多对象实例,以下我们对比较常用的做出了注释:

对象描述
AllowUnQuotedFieldNames决定parser是否将允许使用非双引号属性名
AllowSingleQuotes决定parser是否允许单引号来包住属性名称和字符串值
InternFieldNames决定JSON对象属性名称是否可以被String#intern 规范化表示,如果允许,则JSON所有的属性名将会 intern() ;如果不设置,则不会规范化,默认下,该属性是开放的。
AllowISO8601DateFormat设置为true则遇到字符串符合ISO8601格式的日期时,会直接转换成日期类
AllowArbitraryCommas允许多重逗号,如果设为true,则遇到多个逗号会直接跳过
UseBigDecimal设置为true则用BigDecimal类来装载数字,否则用的是double
IgnoreNotMatch忽略不匹配
DisableCircularReferenceDetect禁用循环引用检测
InitStringFieldAsEmpty对于没有值得字符串属性设置为空串
SupportArrayToBean支持数组to对象
OrderedField属性保持原来的顺序
DisableSpecialKeyDetect禁用特殊字符检查
UseObjectArray使用对象数组
writeJSONString(OutputStream os, Object o)

这个方法是将对象写入输出流中的:

传入的参数还可以是一个 Writer 对象:

以上便是我们平时常用的 API,除此之外,在 FastJSON 中还有一个注解 @JSONField 我们也要学会灵活运用

@JSONField
命名重塑

注: 若属性是 私有的,必须要有 set() 方法,否则无法反序列化!

@JSONField 用法简单,可以配置在 getter()setter() 或者 属性字段

测试结果:

这个方法的最大好处便是用来对接奇奇怪怪的文档,为什么说奇奇怪怪呢,有时候我们需要调用第三方的接口,但是这个接口返回的值可能是不符合命名规范的,那我们这边就需要定义一个实体类去接收它(Map虽然也行,但是也不规范)。

这个时候我们定义的实体类的属性名就得按照返回的字段名来命名,这对强迫症程序猿来说是致命打击,这个时候 @JSONField 的用处就来了,我们简单看个例子。有个车牌信息实体的返回字段是这样的:

{"PLATEID" : 01, "PLATE": '闽A6666D', "IMAGEURL":'http://...'}

我们可以看到返回的字段名全都不满足小驼峰规则,我们定义的实体类可不能这样,借助 @JSONField 的写法如下:

测试下是否能够成功接收结果:

可以看到我们已经成功接收到结果了,而且实体类的命名也符合我们的规范,一举两得。

DataFormat

我们也可以使用该注解来将我们的日期格式化成我们想要的样子

控制序列化

在序列化或反序列化的时候我们可以指定字段不序列化,这个有点像 Java 流中的 transient 修饰。FastJSON 中也可以实现相似的功能:

但是反序列化有个缺点就是,虽然值是空的,但是属性名还在~

ordinal

我们可以使用ordinal来指定字段的顺序

通过接收结果可以看到 属性字段 按照我们规定的顺序所排列,用处可以在于我们返回字段给前端过多的时候,将有用的字段优先排列到前面,可以更好的取值,而不用一层一层的查找需要的字段。

定制序列化

万物皆可定制,序列化也不例外~ 我们可以使用serializeUsing制定属性的序列化类

通过这种方式我们针对 age 这个属性进行了处理,给指定字段加上了单位.

这篇带来的就是 FastJSON 的使用,希望能给各位小伙伴带来版主,路漫漫,小菜与你一同求索~

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。💋

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

每篇都是初恋的味道哦~

查看原文

赞 1 收藏 0 评论 0

蔡不菜丶 发布了文章 · 1月18日

敲黑板了!《行为型模式》来袭

大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚! 死鬼~看完记得给我来个三连哦!

本文主要介绍 软件设计模式中的行为型模式

如有需要,可以参考

如有帮助,不忘 点赞

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

设计模式

一、结构型模式

1)模板方法模式

定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤

模板简单来说抽取一部分逻辑,其他具体实现可以直接用。在开发中经常会遇到设计一个系统时,我们会抽取一个抽象类,而有许多个不同的子实现类,但是子实现类中有部分算法逻辑是固定的,那我们只需要在父类中定义好这些逻辑,然后在不同的实现类中扩展额外的算法逻辑即可。

因此我们可以得出一个小结论:模板方法模式是基于继承的

我们顺势根据 UML图 总结下 模板方法模式 中存在的几种角色:

  • 抽象类(AbstractClass)角色: 负责给出一个算法的轮廓和骨架,它由一个模板方法和若干个基本方法构成。

    • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法
    • 基本方法:实现算法各个步骤的方法,是模板方法的组成部分
  • 具体子类(ConcreteClass)角色: 实现抽象类中所定义的抽象方法

接下来我们用个简单的例子来了解一下 模板方法模式 的使用方法:

以上我们以炒菜为例,其中炒菜的步骤是固定的,倒油、下菜、下调料、翻炒,然后其中基本代码由子类来具体实现。

我们可以发现,模板方法模式是通过父类建立框架,子类再重写父类的部分方法之后,产生不同的效果,通过修改子类可以影响到父类行为的结果,那么这种模式又有啥优缺点呢!如下:

优点:

  • 提高代码复用性
  • 通过父类调用子类的操作,子类具体的实现扩展了不同的行为,实现了反向控制

缺点:

  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象
  • 父类中的抽象方法由子类实现,子类执行的结果会影响到父类的结果,虽实现了反向代理,但也提高了代码阅读的难度

2)策略模式

该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

生活中策略模式随处可见,出行方式的选择:

乃至开发工具的选择:

该模式的 UML 图也较为简单:

由图可知存在的角色如下:

  • 抽象策略(Strategy)类: 这是一个抽象角色,通常由一个接口或抽象类实现,此角色给出所有的具体策略类所需的接口
  • 具体策略(ConcreteStrategy)类: 实现了抽象策略定义的接口,提供具体的算法实现或行为
  • 上下文(Context)类: 持有一个策略类的引用,最终给客户端调用

举个生活中的例子如下:

某大型商场开展周年庆活动,根据商家的不同等级(金牌会员银牌会员铜牌会员)做出不同的活动策略,活动 UML 图如下:

码示如下:

通过以下代码我们简单的实现了策略模式,其中的优缺点我们也有必要知道:

优点:

  • 策略类之间可以自由切换。由于策略类都实现同一个接口,所以使它们之间可以自由切换
  • 易于扩展。增加一个新的策略只需要添加一个具体的策略类即可,符合 开闭原则
  • 避免使用多重条件选择语句(if else)

缺点:

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类
  • 策略模式将造成很多策略类,可以通过使用享元模式在一定程度上减少对象的数量

3)命令模式

讲一个请求封装成为一个对象,使发出请求的责任和执行请求的责任分隔开,这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理

看到这张图我们就会想到我们平时去餐店吃饭的场景:我们需要把我们要点的餐写在订单上,然后服务员拿着订单转去柜台,通知厨师需要出的菜,这个时候厨师根据订单上 的内容将食物备好,最后顾客满意的吃上食物。

这个过程便是利用了命令模式,将订单作为一个请求封装成对象,将服务员和厨师之间的责任隔开,两者只需要通过订单进行沟通,方便将命令模式进行存储、传递、调用、增加与管理。

我们顺势得出命令模式的 UML 图角色

  • 抽象命令(Command)角色: 定义命令的接口,声明执行的方法
  • 具体命令(Concrete Command)角色: 具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行操作
  • 实现者/接收者(Receiver)角色: 接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要他能够实现命令要求实现的相应功能
  • 调用者/ 请求者(Invoker)角色: 要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求执行相应操作的地方,也就是说相当于使用命令对象的入口

有了以上基本的了解,我们就这上面点餐的例子,总结一下点餐的 UML 图

码示如下:

看完了示例代码,我们再来盘一盘它的优缺点:

优点:

  • 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦
  • 增加或删除命令比较方便,采用命令模式增加与删除命令不会影响其他类,它满足 开闭原则,对扩展比较灵活
  • 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令

缺点:

  • 使用命令模式可能会导致某些系统有过多的具体命令类
  • 系统结构更加复杂

4)责任链模式

责任链模式又称为职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

现实中我们最经常经历的便是请假,不管是学校请假或是公司请假,我们都得提交请假申请。但是根据请假天数的不同,处理的责任人也不同。在公司请假,可以审批的责任人可能有部门负责人,区域负责人甚至总负责人。但是每个负责人可以批准的天数不同,如果没有用到责任链模式的设计,我们请假就必须自己去找相应的负责人,而我们如果找到部门负责人还比较方便,如果请的天数较多,我们还得去找区域负责人以及总负责人,这是一件十分麻烦的事情。那我们就来了解一下责任链模式到底是怎样运行的。

责任链模式 UML 图 与角色关系如下:

  • 抽象处理者(Handler)角色: 定义一个处理请求的接口,包含处理的抽象方法和一个后继连接
  • 具体处理者(ConcreteHandler)角色: 实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理请求,否则将该请求转给它的后继者
  • 客户端(Client)角色: 创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程

我们根据上面那个例子,总结一下。图示如下:

码示如下:

老规矩盘下 责任链模式 的优缺点:

优点:

非常显著的优点就是将请求和处理分开。请求者不用知道是谁处理的,处理者不用知道请求的全貌。两者解耦,提高系统灵活性

缺点:

  • 性能问题。每个请求都是从链头遍历到链尾,在链比较长的时候,性能是一个非常大的问题
  • 调试不方便,当链比较长,环节比较多的时候,由于采取了类似递归的方式,调试的时候逻辑比较复杂。

5)状态模式

对有状态的对象,把复杂的 “判断逻辑” 提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为

状态是我们生活中用到比较多的词汇之一。万物皆有状态,不同时间不同地点不同状态。我们可以从精神饱满的状态到昏昏欲睡的状态。

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类状态模式 主要解决的问题就是当控制一个对象状态转换的条件表达式过于复杂时,把状态的判断逻辑转移到标识不同状态的一系列类当中。

状态模式 中存在的角色有:

  • 抽象状态(State)角色: 定义一个接口,用以封装环境对象中的特定状态所对应的行为
  • 具体状态(ConcreteState)角色: 实现抽象状态所对应的行为
  • 环境(Context)角色: 也称为上下文,它定义了客户程序需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理

我们如果在网上进行购物的话就会产生订单,不同状态下会有不同的行为,订单状态可分为 加购状态付款状态收货状态评价状态。在整个订单流转的过程中,由不同的事件行为可导致它状态的变更:

一般写法我们是这样的:

 public class OrderServiceImpl implements OrderService {
  private OrderMapper orderMapper;
 ​
  public void process(Long orderId) {
  Order order = orderDao.findById(orderId);
  if (order.getStatus() == OrderStatus.WAIT_PAY) {
  // 待支付,则进行支付
  } else if (order.getStatus() == OrderStatus.PAYING) {
  // 支付中,则进行相应提示
  } else if (order.getStatus() == OrderStauts.PART_PAID) {
  // 部分支付,则进行相应处理
  } else {
  // 其他状态进行对应处理
  }
  }
 }

如果遇到状态比较多的情况,用 if else 处理起来比较乱,那么我们可以用状态模式来改进试下,UML 图如下

码示如下:

抽象状态角色:

具体状态角色 - 加购状态:

具体状态角色 - 付款状态:

具体状态角色 - 收货状态:

具体状态角色 - 评价状态:

上下文角色:

以上便是简单的实现了 状态模式,那我们盘一下它的优缺点

优点:

  • 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为
  • 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块

缺点:

  • 状态模式的使用必然会增加系统类和对象的个数
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱
  • 状态模式对 开闭原则 的支持并不大好

6)观察者模式

观察者模式又称为 发布-订阅模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,是他们能够自动更新自己。

观察者模式相当于一个第三者,比如我们在学校自习的时候,有些人聊天,有些人睡觉,这个时候为了不让老师抓到,我们都会找一个 "放风" 的同学,如果老师来的话就会及时发出通知。

观察者模式UML 图是这样的:

我们可以清晰的看出其中存在的几种角色:

  • 抽象主题(Subject)角色: 抽象主题提供增加和删除观察者对象的接口
  • 具体主题(ConcreteSubject)角色: 该角色将所有观察者对象保存在一个集合中,当具体主题的内部状态发生改变时,给所有注册过的观察者发送通知
  • 抽象观察者(Observer)角色: 抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知更新自己
  • 具体观察者(Concrete)角色: 实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态

接下来我们根据以上学生的例子用 观察者模式 实现一下:

这样子就简单地实现了 观察者模式。但是观察者模式也存在不足之处,我们想说说它的好:

优点:

  • 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系
  • 被观察者发送通知,所有注册的观察者都会受到消息(广播模式)

缺点:

  • 如果观察者非常多的话,所有的观察者收到的被观察者发送的通知会有一定的耗时
  • 如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃

7)中介者模式

中介模式又称为 调停模式,定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互

一般来说,同事对象之间的关系是比较复杂的,多个同事对象之间相互关联的时候,他们之间的关系就会呈现为复杂的网状结构,这是一种过渡耦合的架构,,既不利于类的复用,也不稳定,加入其中一个对象发生变化,其他对象也会相应收到影响。

因此这个时候就引入了中介者模式,那这个时候同事对象之间的关系就变成了星型结构,任何一个类的变动,只会影响到类本身以及中介者,这样就减少了系统的耦合。

中介者模式的 UML 图 和 角色 如下:

  • 抽象中介者(Mediator)角色: 中介者的接口,提供同事对象注册与转发同事对象信息的抽象方法
  • 具体中介者(ConcreteMediator)角色: 实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系
  • 抽象同事者(Colleague)角色: 定义同事者接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能
  • 具体同事者(ConcreteColleague)角色: 是抽象同事类的实现者,当需要与其他同事交互时,由中介者对象负责后续的交互

中介者模式最经典的案例还是 房产中介。现在租房基本都是通过房屋中介,房主将房屋托管给房屋中介,而租房者从房屋中介获取房屋信息。代码实现如下:

通过 中介者模式,房东与租户可以不用直接交互,较少了类与类之间的复杂关系,那么这种模式又有什么优缺点呢?如下:

优点:

  • 松散耦合。把多个同事对象之间的交互都封装在了中介者对象里面,各个同事对象可以独立地变化和复用
  • 集中控制。多个同事对象之间的交互被封装在中介者对象里面集中管理,使得这些交互行为发生变化的时候,只需要修改中介者对象就可以了。

缺点:

  • 如果同事对象过多时,中介者对象会变得过于庞大,而变得复杂,难以维护

8)迭代器模式

提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示

这里说的 迭代器模式 与我们平时在集合中用到的 迭代器 是一回事,我们平时是这样这样使用的:

 List<String> list = new ArrayList<>();
 Iterator<String> iterator = list.iterator(); //list.iterator()方法返回Iterator接口的子实现类对象
 while (iterator.hasNext()) {
  System.out.println(iterator.next());
 }

我们平时用的溜溜的,但是具体内部实现你是否有所了解,我们老样子来看下迭代器模式中的 UML 图角色

  • 抽象聚合(Aggregate)角色: 定义添加、删除聚合元素以及创建迭代器对象的接口
  • 具体聚合(ConcreteAggregate)角色: 实现抽象聚合类
  • 抽象迭代器(Iterator)角色: 定义访问和遍历聚合对象的接口
  • 具体迭代器(ConcreteIterator)角色: 实现抽象迭代器接口中所定义的方法, 完成对聚合对象的遍历,记录遍历的当前位置

然后我们用代码来了解一下迭代器的内部实现:

通过以上方式我们简单的实现了 迭代器 的实现,以下说下它的优缺点

优点:

  • 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据的遍历等方法,这样可以简化聚合类的设计
  • 在迭代器模式中,由于引入抽象层,增加新的聚合类和迭代器类都很方便,无需修改原有代码,满足 开闭原则 的要求

缺点:

增加了类的个数,这在一定程度上增加了系统的复杂性

9)访问者模式

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作

简单来说就是想要为一个对象增强新的行为,且不封装具体的实现,那么就可以用访问者模式。 UML 图如下:

其中存在的角色有:

  • 抽象访问者(Visitor)角色: 定义了每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲是与元素类的个数(Element)的实现类个数是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变
  • 具体访问者(ConcreteVisitor)角色:给出对每一个元素访问时所产生的具体行为
  • 抽象元素(Element)角色: 定义了一个接受访问者的方法(accept),意义在于每一个元素都可以被访问者访问
  • 具体元素(ConcreteElement)角色: 提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法
  • 对象结构(ObjectStructure)角色: 定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(Element),并且可以迭代这些元素,供访问者访问

我们假设一个场景,动物园的动物需要饲养员喂养,部分动物(老虎,狮子...)游客也可以喂养,然后我们给这些角色分下类:

  • 抽象访问者角色:喂养者
  • 具体访问者角色: 饲养员、游客
  • 抽象元素角色: 动物
  • 具体元素角色: 老虎,狮子
  • 对象结构角色: 动物园

代码实现如下:

抽象访问者

具体访问者

抽象元素 & 具体元素

对象结构

测试类

输出

 饲养员喂养老虎
 老虎受到了饲养员喂养
 饲养员喂养狮子
 狮子受到了饲养员的喂养
 =========
 游客喂养老虎
 老虎受到了游客喂养
 游客喂养狮子
 狮子受到了游客的喂养

通过 访问者模式,我们可以很清楚的实现这个需求,这个模式的优缺点如下:

优点:

  • 扩展性好。在不修改对象结构中元素的情况下,为对象结构中的元素添加新的功能
  • 复用性好。通过访问者来定义整个对象结构通用的功能,从而提高复用程度
  • 分离无关行为。通过访问者来分离无关的行为。把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一

缺点:

  • 对象结构变化很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加响应的具体操作,违背了 开闭原则
  • 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类

10)备忘录模式

备忘录模式又称为 快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态

其实这个模式最经典的案例便是 ctrl+z 的功能,在我们编辑文本的时候,撤销操作便是。备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原。

UML结构图如下:

  • 发起人(Originator)角色: 记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息
  • 备忘录(Memento)角色: 负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人
  • 管理者(Caretaker)角色: 对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问和修改

有玩过游戏的同学,应该都知道一个称为 存档 的功能,存档的目的在于在游戏进行到一个不理想的程度时,可以恢复到之前巅峰的状态。让我们就用代码来简单模拟一下这个游戏的场景。首先游戏角色肯定会有 生命值 的属性,我们简单整理一下 UML 图

然后我们用代码来表示一下:

通过 备忘录模式 我们就可以实现无线重试的机会,让我们来理下这种模式的优缺点:

优点:

  • 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态
  • 实现了信息的封装,使得用户不需要关心状态的保存细节

缺点:

  • 消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存

11)解释器模式

给定一个语言,定义它的文发表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子

解释器这个名词小伙伴们应该不会很陌生,在编译原理中,一个算术表达式可通过词法分析器形成词法单元,而后这些词法单元再通过语法分析器构建语法分析树,最终形成一颗抽象的语法分析树。

计算器我们肯定都有用过,如果让你实现一个简单的加法运算,我们的第一个想法应该是这样写的:

 public static int plus(Integer ... args) {
  int sum = 0;
  for (Integer i : args) {
  sum += i;
  }
  return sum;
 }

但是这种适合于形式比较单一,有限的场景,如果形式变化比较多,那么这种就不符合要求。

那么现在我们就需要一种翻译识别机器,能够解析由数字、- 、+ 构成的运算序列。我们可以把数字和运算符都看作节点, 然后对逐个节点进行读取解析运算,这种就是 解释器模式 的思维。

然后我们来看下解释器模式的 UML 图

  • 抽象表达式(AbstractExpression)角色: 定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()
  • 终结符表达式(TerminalExpression)角色: 实现抽象表达式,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结符表达式与之相对应
  • 非终结符表达式(NonTerminalExpression)角色: 实现抽象表达式,用来实现文法中与非终结符相关的操作,文法中每条规则都对应与一个非终结符表达式
  • 环境(Context)角色: 通常包含多个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值

那我们接下来就用 解释器模式 来简单实现一下 加减法 的逻辑:

UML 图

代码如下:

表达式角色:

上下文环境:

测试类:

通过 解释器模式 我们可以很清晰的看出代码的逻辑,实现了以上功能。总结下这种模式的优缺点:

优点:

  • 易于改变和扩展文法。在解释器模式中使用类来表示语言的文法规则, 因此可以通过继承等机制来改变或扩展文法,每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言
  • 实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂
  • 增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无需修改,符合 开闭原则

缺点:

  • 对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护
  • 执行效率低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程比较麻烦

END

软件设计模式到这篇就结束啦,一共有 22 个设计模式,量多需消化,小伙伴们好好看哦!路漫漫小菜与你一同求索。

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。💋

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

查看原文

赞 0 收藏 0 评论 0

蔡不菜丶 发布了文章 · 1月10日

图文并茂走进《结构型模式》,原来这么简单!

大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚! 死鬼~看完记得给我来个三连哦!

本文主要介绍 软件设计模式中结构型模式

如有需要,可以参考

如有帮助,不忘 点赞

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

上篇介绍完几种创建型模式,不知道看完的小伙伴创建对象的方式有没有变得更溜了。如果没有的话,那今天更得好好学一学 结构型模式,知己知彼百战百胜嘛,清楚对象是什么结构才能更好的创建出来!

我们老样子开场,先清楚一下今天要学习的内容:

设计模式

一、结构型模式

结构型模式 描述如何将类后对象按某种布局组成更大的结构,它分为 类结构型模式对象结构型模式。前者采用继承机制来组织接口和类,后者采用组合或聚合来组合对象。

由于组合关系或聚合关系比继承关系耦合度低,满足"合成复用原则" ,所以 对象结构型模式类结构型模式 具有更大的灵活性。

我们在上面已经了解都有 7结构性模式 ,我们接下来就先来认识第一种设计模式 代理模式

1)代理模式

由于某些原因需要给某对象提供一个代理以控制对该对象的访问,这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介

代理模式 作为开发的我们肯定也不会感到陌生,JDK 代理、CGLib 代理 随口就来。在 Java 中的代理按照代理类生成实际不同又分为 静态代理动态代理

  • 静态代理是在编译时期就生成的
  • 动态代理是在 Java 运行时动态生成的

其中动态代理又分为 JDK 动态代理CGLib 代理 两种

1. 静态代理

我们结合 静态代理模式UML图 来了解下:

可以看出 静态代理模式 有三种角色,分别是:

  • 抽象主题(Subject)类: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  • 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  • 代理(Proxy)类: 实现了抽象主题,提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

我们举个例子理解一下:比如之前我们如果需要乘坐火车的话,我们需要去火车站买票,但是火车站距离远我们还得坐车去火车站,万一人多还得排队,这显然是十分不方便的。这个时候就出现了 火车站代售点,通过 代售点 我们就可以直接买票。

码示如下:

从上面代码中可以看出测试类直接访问的是 ProxyStation ,它作为访问对象和目标对象的中介,同时也可以对抽象方法进行增加(收取服务费)

2. 动态代理
(1)JDK 动态代理

JDK动态代理 中,抽象主题 Subject真实主题 RealSubject 是不变的,我们需要改的地方是 代理 Proxy,将静态转换成动态,然后客户端的调用方式有所改变,具体代码如下:

我们在代码中看到 ProxyStation 好像不需要实现 SellTicket 这个接口了,但是真的是这样吗。有兴趣的同学可以编译查看下代理类的结构,我这里接直接说结论了:

ProxyStation 在这里面其实不算是 代理模式中所说的代理类,通过 Proxy 创建出来的才是实际上的代理类,它实现了 SellTicket 这个接口,为我们提供了匿名内部类对象传递给了父类,然后在测试类通过代理对象调用的 sell() 方法,实际上是根据多态的特性,执行的是 Proxy 类 中的 sell()方法,因此会出现 代售点收取代理费 的输出。

那么如果我们没有定义 SellTicket 这个接口,只定义了 RailwayStation 这个类,那么 JDK动态代理 是不适用的,因为它必须要求定义接口,对接口进行代理。那我们这个时候就需要使用 CGLib 动态代理 了。

(2) CGLib 动态代理

由于 CGLib 是第三方工具包,我们需要先引入依赖:

 <dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
 </dependency>

然后代码做出以下修改:

CGLib动态代理实际上是利用 ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来实现代理。接下来我们比较以下各个代理:

静态代理 和 动态代理

如果接口方法的数量比较多的话,静态代理需要对每一个方法进行中转,而动态代理是将声明的所有方法都转移到调用处理器一个集中的方法中进行处理。

如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也都需要实现这个方法,而动态代理则不用。

结论: 动态代理相对静态代理来说更加灵活

JDK动态代理 和 CGLib动态代理

JDK 1.6 之前,使用 CGLib 动态代理 效率会比 JDK 动态代理 要高,但是在后面的版本对 JDK 动态代理 进行优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。

结论:CGLib动态代理不能对声明为final的类或者方法进行代理,JDK动态代理 不能对没有接口的类进行代理。

2)适配器模式

将一个类的接口转换为客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作

适配器模式 讲究的便是 适配 两个字,我们生活中的 手机充电器(电压转换)读卡器 等就是使用到了 适配器模式

适配器模式 又分为 类适配器模式对象适配器 模式,前者的耦合度会比后者高,因此应用相对较少。

我们总结一下 适配器模式 中的几种角色:

  • 目标接口(Target): 当前系统业务所期待的接口,可以是抽象类或者接口
  • 适配源类(Adaptee): 它是被访问和适配的现存组件库中的组件接口
  • 适配者类(Adapter): 它是一个转换器,通过集成或引用适配者的对象,把适配源接口转换成目标接口,让客户按目标接口的格式访问适配者
1. 类适配器模式

看图我们就可以知道了个大概,首先就是定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件

我们举个例子来说明一下:

现有一台电脑只能读取 SD 卡,但是我们现在手上只有 TF 卡,如果需要读取 TF 卡就需要使用到适配器模式,创建一个读卡器(适配器)来将 TF 卡中的内容读取出来。

我们按照上面的 UML图 仿写一下:

大致没有什么差别,只不过多了两个实现类,然后我们接下来就可以写代码了:

照代码的功能来看,类适配器 确实实现了我们想要的适配功能,但是从原则上看,类适配器 却违背了 合成复用原则 ,这样会导致的问题就是只有当客户端有一个接口规范的情况下可用,否则不可用。

啥?你还不知道 合成复用原则 是啥,赶紧来复习下吧! 软件设计原则
2. 对象适配器模式
对象适配器模式可采用将现有组件库中已经实现的组件引入适配器中,该类同时实现当前系统的业务接口

怎么个意思呢,其实就是将继承关系 改成了聚合关系,UML示图如下:

然后修改部分代码如下:

3)装饰者模式

在不改变现有对象结构的情况下,动态地给该对象增加一些职责(增加额外功能)的模式

我们老样子看图理一下 装饰者模式 中有几种角色:

  • 抽象构件(Component): 定义一个抽象接口以规范准备接收附加责任的对象
  • 具体构件(ConcreteComponent): 实现抽象构件,通过装饰角色为其添加一些职责
  • 抽象装饰(Decorator): 继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能
  • 具体装饰(ConcreteDecorator): 实现抽象装饰的相关方法,并给具体构件对象添加附加的责任

我们以 炒面 为例,炒面我们可以加 或者加 ,当然,加这两种材料的价钱肯定是不一样的,我们先画 UML 来理解一下:

然后我们用代码实现一下:

功能已经完美的实现,那我们从中能发现什么好处呢?

  • 装饰者模式 可以比继承更加灵活地扩展功能,使用起来更加方便,可以通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果。完美的遵循了开闭原则继承是静态的附加责任,装饰者则是动态的附加责任
  • 装饰类被装饰类可以独立发展,不会相互耦合,装饰者模式是继承的一个替代模式,装饰者模式可以动态扩展一个实现类的功能

有些小伙伴也可以已经发现了 代理模式装饰者模式 有点相似,都是可以通过 聚合 的方式动态的增加额外的责任。下面是 静态代理模式装饰者模式 的比较:

相同点

  • 都要实现与目标类相同的业务接口
  • 在两个类中都要声明目标对象
  • 都可以在不修改目标类的前提下增强目标方法

不同点

  • 目的不同: 装饰者是为了增强目标对象,静态代理是为了保护和隐藏目标对象
  • 获取目标对象构建的地方不同: 装饰者是由外界传递进来,可以通过构造方法传递,静态代理是在代理类内部创建,以此来隐藏目标对象

4)桥接模式

将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度

如果有个需求我们需要创建不同的图形(矩形,圆形,正方形),每个图形需要有不同的颜色(红色,白色,黑色)。当我们不做过多思考,写出第一种解决方案应该是这样的:

这种应该是最简单的实现方式了,但是如果我们需要在增加一种形状或颜色,那么就需要创建爱你更多的类,这跟 工厂方法 模式有点类似,造成的结果就是 类爆炸,那么有没有优化的方案,我们就想到了第二种实现方式如下:

这个方案是根据实际需要对形状和颜色进行组合,对于有两个变化维度(即两个变化的原因)的系统,采用第二个方案进行设计系统,类的数量会更少,系统扩展也会更加方便。而这种模式便是 桥接模式,好处便是降低了类与类之间的耦合度,减少了代码编写量。

接下来我们看下 桥接模式UML图:

然后我们再来分析一下其中有哪几种角色:

  • 抽象化(Abstraction)角色: 定义抽象类,并包含一个对实现化对象的引用
  • 扩展抽象化(RefinedAbstraction)角色: 是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实例化角色中的业务方法
  • 实例化(Implementor)角色: 定义实现化角色的接口,供扩展抽象化角色调用
  • 具体实例化(ConcreteImplementor)角色: 给出实例化角色接口的具体实现

接下来我们举个例子来加深一下理解吧!

现在有两种规格的画笔,分别是 大画笔小画笔 ,而且我们需要能够绘制三种颜色(红黄蓝)的颜色。如果使用传统蜡笔来实现的话,那么需要 2*3=6 支画笔,如果我们将画笔改成颜料笔,那我们只需要 3盒颜料两支画笔

UML 图 如下:

代码如下:

理解完上述的例子,我们也来分析一下 桥接模式 的优缺点

优点:

  • 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统
  • 实现细节对客户透明,可以对用户隐藏实现细节

缺点:

  • 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程
  • 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性

5)外观模式

外观模式又称为 门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式,该模式对外有一个统一的接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性

其实生活中最简单的例子便是 股票和基金 的关。有些人不会炒股,这个时候基金就是一个好的帮手,它将投资者分散的资金集中起来,交由专业的经理人进行管理,投资于股票、债券、外汇等领域,而基金投资的收益归持有者所有,管理机构收取一定比例的托管管理费用。

外观模式是迪米特法则的典型应用

没用 外观模式 之前:

用了 外观模式 之后:

如果理解了上面一层的关系,那么得出 UML图 也是十分容易:

外观模式 中存在以下几种角色:

  • 外观(Facade)角色: 为多个子系统对外提供一个共同的接口
  • 子系统(SubSystem)角色: 实现系统的部分功能,客户可以通过外观角色访问它

那我们就举个例子简单说明一下吧!

小米的生态现在都挺好用的,只需要我们对 小爱同学音箱(Facade) 进行语音控制便可以控制智能家居(灯,空调,窗帘 --- SubSystem) ,这里面其实就是用到 外观模式

代码实现如下:

这样子就是用了 外观模式,客户不同跟具体某个家居交互,直接跟音箱交互即可,然后让我们扒一扒 外观模式 的优缺点吧!

优点:

  1. 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类
  2. 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易

缺点:

  1. 不符合开闭原则,修改起来较为麻烦

6)组合模式

组合模式又称为整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

文字看的麻烦,用张图你就知道了

这种就是一个文件系统,而这种结构我们也称为 树形结构。这里面用到的便是 组合模式

组合模式(Composite),将对象组合成树形结构以表示 "部分-整体" 的层次结构,用户对单个对象和组合对象的使用具有一致性。所以当我们的案例是 树形结构部分-整体 的关系时,就可以考虑使用组合模式

其中 组合模式 又有两种不同的实现,分别是 透明模式安全模式,其中角色都是一致的,如下:

  • 抽象根节点(Component)角色: 定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性
  • 树枝节点(Composite)角色: 定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构
  • 叶子节点(Leaf)角色: 叶子节点对象,其下再无分支,是系统层次遍历的最小单位
1. 透明模式

我们看下透明模式的 UML图:

透明模式是把使用的方法放到抽象类中,不管是 树枝对象 还是 叶子对象 都有相同的结构,代码如下:

这样做的好处便是叶子节点和树枝节点对于外界来说没有区别,它们具备完全一致的行为接口,但是 Leaf 类 本身不具备 add()remove() 方法的功能,所以实现它是没有意义的,这样子就又出现了另外一种模式,便是 组合模式 - 安全模式

2. 安全模式

安全模式UML 图如下:

安全模式是把树枝节点和树叶节点彻底分开,树枝节点单独拥有用来组合的方法,这种方法比较安全。代码实现如下:

这种方式由于不够透明,树叶节点和树枝节点将不具有相同的接口,客户端的调用需要做相应的判断,不能完全整堆抽象编程,必须有区别地对待叶子构件和容器构件。

7)享元模式

运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量,避免大量相似对象的开销,从而提高系统资源的利用率。

享元模式(Flyweight)中存在两种状态:

  • 内部状态,不会随着环境的改变而改变的可共享的部分
  • 外部状态,会随环境改变而改变的不可共享的部分

享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。

享元模式在开发中运行到的场景挺多的,首先能想到的便是各种 池技术了,线程池数据库连接池String常量池等等,所以说 享元模式 是池技术的重要实现方式。

所以 享元模式的用处很简单,便是减少对象的创建

享元模式 中存在着这几种角色:

  • 抽象享元(Flyweight)角色: 通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)
  • 具体享元(ConcreteFlyweight)角色: 它实现了抽象享元类,称为 享元对象,在具体享元类中为内部状态提供了存储空间,通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象
  • 非享元(UnsharedFlyweight)角色: 并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可以设计为非共享具体享元类。当需要一个非共享具体享元类的对象时可以直接通过实例化创建
  • 享元工厂(FlyweightFactory)角色: 负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检查系统中是否存在符合要求的享元对象,如果存在则提供给客户,如果不存在的话,则创建一个新的享元对象

接下来我们用代码来实现一下:

这样子就简单实现了 享元模式,我们老样子来扒一扒 享元模式 的优缺点

优点:

  • 极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
  • 享元模式中的外部状态相对独立,且不影响内部状态

缺点:

为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂

END

结构型模式 在这里就告一段落啦,不过 软件设计模式 可还未结束,行为型模式正在路上!

关注小菜不迷路,路漫漫小菜与你一同求索!

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。💋

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

查看原文

赞 0 收藏 0 评论 0

蔡不菜丶 发布了文章 · 1月3日

2021还不多学几种创建型模式,创建个对象!

大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚! 死鬼~看完记得给我来个三连哦!

本文主要介绍 软件设计模式中的创建型模式

如有需要,可以参考

如有帮助,不忘 点赞

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

上周讲完软件设计原则,小菜就赶紧又肝了一篇软件设计模式出来,这不就怕你们不满足!

说到设计模式,先自己回忆下看能想出几种出来,然后每种的实现方式是否还记得。说实话,让小菜整理完这篇文章之前也说不上多少个。说不上来几种的无非有两种,一种是真的不会,就记得几个;一种是非常熟了,能够做到手中无剑心中有剑的境界,虽然说不上来,但是写代码的时候不用刻意去想用哪种设计模式,自然而然就写出颇为完美的代码。当然,我们都要成为第二种,既然要成为第二种,那么就从好好看这篇设计模式开始吧!

设计模式(Design Pattern)是一套被反复使用,多数人知晓的,经过分类编写的目的,代码设计经验的总结。使用设计模式是为了可重用代码,让代码更容易被他人理解,保证代码可靠性。毫无疑问,设计模式于己于人于系统都是多赢的。设计模式使代码编制真正工程化。

设计模式

首先我们需要整理一下有哪几种设计模式,上图:

看到原来有这么多设计模式,脑子是不是有些发蒙,不过没关系,接下来小菜带你一个一个攻破!

一、创建型模式

创建型模式的主要关注点在于 如何创建对象,它的主要特点是:将对象的创建与使用分离。这样做的好处便是:降低系统的耦合度,使用者不需要关注对象的创建细节。其中创建者模式又分为以下几种:

1)单例模式

说到单例模式,是不是都感觉自信起来了。没错这个平时说的最多,用到最多,面试中问的也最多的设计模式。

单例模式(Singleton Pattern) 是 Java 中最简单的设计模式之一,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象会被创建。这样子我们就不需要实例化该类的对象,可以直接访问。简单来说:就是这个模式是负责 计划生育 的,只允许创建一个对象,不允许有太多的徒子徒孙。

单例模式 又分为两种实现方式:

  • 饿汉式:类加载就会导致该单例对象被创建
  • 懒汉式:类加载不会导致该单例对象被创建,而是首次使用该对象时才会创建

接下来我们就详细介绍下两种不同的实现方式

1. 饿汉式

静态变量方式

 public class Singleton {
 ​
  // 步骤1 :私有的构造方法
  private Singleton() { }
  
  // 步骤2 :在成员位置创建该类的对象
  public static Singleton singleton = new Singleton();
 ​
  // 步骤3 :对外提供静态方法获取该对象
  public static Singleton getInstance() {
  return singleton;
  }
 }

三步走战略将 单例模式 显得格外简单。该方式中 singleton 对象是随着类的加载而创建的,弊端也很明显:如果该对象足够大,而一直没有被使用到,那么就会造成内存的浪费

静态代码块方式:

 public class Singleton {
 ​
  // 步骤1 :私有的构造方法
  private Singleton() { }
 ​
  // 步骤2 :在成员位置创建该类的对象
  public static Singleton singleton;
 ​
  {
  singleton = new Singleton();
  }
 ​
  // 步骤3 :对外提供静态方法获取该对象
  public static Singleton getInstance() {
  return singleton;
  }
 }

该方式与上一种 静态变量的方式大同小异,该对象的创建也是随着类的加载而创建的,弊端当然也是一样的。

2. 懒汉式

线程不安全:

 public class Singleton {
 ​
  // 步骤1 :私有的构造方法
  private Singleton() { }
 ​
  // 步骤2 :在成员位置创建该类的对象
  public static Singleton singleton;
 ​
  // 步骤3 :对外提供静态方法获取该对象
  public static Singleton getInstance() {
  if (singleton == null) {
  singleton = new Singleton();
  }
  return singleton;
  }
 }

上面代码中我们在成员位置声明 Singleton 类型的静态变量,并没有进行对象的赋值操作,而是当调用 getInstance() 方法时才创建 Singleton 类的对象,这种方式便实现了懒加载的效果,但是弊端便是:多线程环境下,会出现线程安全的问题

线程安全:

提到线程安全问题,我们第一反应便是想到利用 synchronized对共享变量进行上锁,那么便有了以下代码:

 public class Singleton {
 ​
  // 步骤1 :私有的构造方法
  private Singleton() {}
 ​
  // 步骤2 :在成员位置创建该类的对象
  public static Singleton singleton;
 ​
  // 步骤3 :对外提供静态方法获取该对象
  public static synchronized Singleton getInstance() {
  if (singleton == null) {
  singleton = new Singleton();
  }
  return singleton;
  }
 }

该方式也实现了 懒加载 的效果,同时也解决了线程安全的问题。看似完美的代码,那么能否进行进一步的优化。我们都是到经过 synchronized 关键字上锁,执行效率也会变低。上锁的关键在于为了重复创建对象,如果对象已经创建,我们就不需要上锁,那么我们是否可以将 锁细化,不需要把锁加在方法上,而是在对象为空时才加上锁,那么就有了第三种方式.

双重检查锁:

 public class Singleton {
 ​
  // 步骤1 :私有的构造方法
  private Singleton() {}
 ​
  // 步骤2 :在成员位置创建该类的对象
  public static Singleton singleton;
 ​
  // 步骤3 :对外提供静态方法获取该对象
  public static Singleton getInstance() {
  //第一次判断,当 singleton 为空时加锁,否则直接返回实例
  if (singleton == null) {
  synchronized (Singleton.class) {
  //第二次判断,当抢到锁时再判断 singleton 是否为空
  if (singleton == null) {
  singleton = new Singleton();
  }
  }
  }
  return singleton;
  }
 }

看完代码你可能会觉得奇怪,为什么加完锁后还要判断一次是否为空呢,不都已经加上锁,这个时候肯定没人跟自己抢了,再判断一次不是多此一举吗。

在并发环境下,线程A线程B 通过进入 第一步 后,线程A线程B 先获取到锁,这个时候 线程B 会在等待队列中等待获取锁,如果这个时候没有 第三步 的判空,当 线程A 执行完释放锁,线程B 这个时候已经进入了 第一步,它这个时候还以为对象还没有被创建,等到它获取到锁就会又创建一个类对象,这样子就不符合单例模式了。因此 第三步 也是至关重要的。

到现在为止,你应该又有了 该代码已经完美 的想法了,但是如果对小菜之前写的并发文章有熟悉的小伙伴就会知道 并发中还会存在一个指令重排 的情况,下面我们来看一下

正常情况:

 memory=allocate();  //1:分配对象的内存空间
 cteateInstance(memory);              //2:初始化对象
 instance = memory;  //3:设置instance指向刚分配的内存地址

指令重排:

 memory=allocate();          //1:分配对象的内存空间
 instance = memory;          //3:设置instance指向刚分配的内存地址
 //注意,此时对象还没有被初始化!
 cteateInstance(memory);     //2:初始化对象

由于单线程内要遵守 intra-thread semantics,从而能保证 线程A 的执行结果不会被改变。但是,当 线程A线程B 按上图顺序执行时,线程 B 将看到一个还没有被初始化的对象。要解决这个问题,有两个方法:

  1. 不允许 步骤2步骤3 进行重排序
  2. 允许 步骤2步骤3 进行重排序,但是不允许其他线程看到这个重排序

解决思路有了,我们自然会联想到使用 volatile 这个关键字,这个关键字的作用之一便是禁止指令重排,代码也十分简单,只需要使用 volatile 修饰成员变量即可:

 public class Singleton {
 ​
  // 步骤1 :私有的构造方法
  private Singleton() { }
 ​
  // 步骤2 :在成员位置创建该类的对象
  // 使用 volatile 修饰
  public static volatile Singleton singleton;
 ​
  // 步骤3 :对外提供静态方法获取该对象
  public static Singleton getInstance() {
  //第一次判断,当 singleton 为空时加锁,否则直接返回实例
  if (singleton == null) {
  synchronized (Singleton.class) {
  //第二次判断,当抢到锁时再判断 singleton 是否为空
  if (singleton == null) {
  singleton = new Singleton();
  }
  }
  }
  return singleton;
  }
 }

添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。这下总算完美了,但是完美之后我们就要思考还有没有其他方式也能完美的实现单例模式,答案肯定是有的。

静态内部类方式:

上面利用 饿汉式 创建单例,由于对象是随着类的加载而创建的,会占用内部空间而选择了 懒汉式,但是饿汉式也是有改进的地方。我们可以通过内部类创建类对象,由于 JVM 在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用的时候才会被加载,并初始化其静态属性,而且静态属性被 static 修饰,保证只被实例化一次,并且在没有加任何锁的情况下,保证了多线程下的安全,没有任何性能影响和空间的浪费,所以这也是一种实现单例的好方式。

 public class Singleton {
 ​
  // 步骤1 :私有的构造方法
  private Singleton() { }
 ​
  private static class SingletonInner{
  private static final Singleton SINGLETON =  new Singleton();
  }
 ​
  public static Singleton getInstance() {
  return SingletonInner.SINGLETON;
  }
 }

枚举方式:

 public enum Singleton{
  INSTANCE;
 }

从代码上就可以看出这是一种极为简单的单例实现模式,也是极力推荐的。因为枚举类型是线程安全的,并且只会装载一次,而且枚举类型是所有单例实现中唯一一种不会被破坏的单例实现模式。这种方式也是属于 饿汉式 方式。

vx搜: 小菜良记 ,更多干货值得关注!

2)工厂模式

Java 是面向对象开发的,万物皆对象 便是 Java 中的一个理念。对象都是需要创建的,如果我们都是通过 new 方式来创建一个对象,假如我们的类名做了部分修改,那么是否所有通过 new 方式创建对象的地方都需要修改一遍,这显然违背了软件设计中的开闭原则,耦合将会十分严重。

生活中存在工厂的概念,那么我们设计模式也应当有 工厂模式。我们可以通过使用工厂来生产对象,类如果发生变更,我们可以不用理会,我们只和工厂打交道就行。这样就达到了对象解耦的目的,所以 工厂模式 设计的初衷便是为了:解耦

根据需求的不同,工厂模式中又分为三种分别是:

  • 简单工厂模式
  • 工厂方法模式
  • 抽象方法模式
1. 简单工厂模式

简单工厂模式在开发中用到最多,可能你有时候都没发现自己使用的是简单工厂模式

我们举个例子来认识一下 简单工厂模式

有一家奶茶店,里面卖着 草原奶茶椰香奶茶,那么根据顾客不同的喜好,奶茶店就需要制作不同的奶茶。

一般来说我们惯性思维便是创建两个奶茶类 GrasslandsMilkCoconutMilk,然后需要哪种奶茶就 new 哪种。

使用 简单工厂模式来实现是比较简单的,首先我们需要知道 简单工厂模式 中存在这哪几种角色:

  • 抽象产品: 定义产品的规范,描述产品的主要特性和功能
  • 具体产品: 实现或者继承抽象产品的子类
  • 具体工厂: 提供创建产品的方法,调用者通过该方法来获取产品

然后我们给每个角色归类一下:草原奶茶椰香奶茶 我们可以归到 具体产品 中,然后我们可以抽取一个 奶茶 的抽象类出来归到 抽象产品中,最后创建一个制作奶茶的类,归到具体工厂

图示如下:

码示如下:

 public class SimpleMilkFactory {
 ​
  public Milk createMilk(String type) {
  Milk milk = null;
  if (StringUtils.equals("grasslands", type)) {
  milk = new GrasslandsMilk();
  } else if (StringUtils.equals("coconut", type)) {
  milk = new CoconutMilk();
  }
  return milk;
  }
 }

通过代码可以看到我们可以通过传入的type 进行判断需要生产何种奶茶,客户端就不需要字节创建 奶茶,直接从工厂中获取即可。但是虽然解除了客户端和奶茶类的耦合,又增加了工厂和奶茶类的耦合,后期如果需要增加新品种的奶茶,我们就得修改工厂中的获取方法,还是违反了开闭原则

既然工厂和具体产品类产生了耦合,那我们是否考虑 "术业有专攻" 的理念,每个产品都要有它对应的生产工厂,比如一个鞋厂,里面有 运动鞋休闲鞋,那么应该需要一个 运动鞋工厂 和一个 休闲鞋工厂 ,这样子就解决了工厂和具体产品类 之间的耦合。有想法就有办法,这个时候就有了 工厂方法模式

2. 工厂方法模式
定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类

简而言之,工厂方法模式 便是在 简单工厂模式 基础上增加了一个 抽象工厂 的角色

  • 抽象工厂: 提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。

图示如下:

码示如下:

抽象工厂:

 public abstract class MilkFactory {
  abstract Milk createMilk();
 }

具体工厂:

 /**
  * 草原奶茶工厂
  */
 public class GrassLandsMilkFactory extends MilkFactory{
  @Override
  Milk createMilk() {
  return new GrasslandsMilk();
  }
 }
 /**
  * 椰香奶茶工厂
  */
 public class CoconutMilkFactory extends MilkFactory {
  @Override
  Milk createMilk() {
  return new CoconutMilk();
  }
 }

奶茶店类:

 public class MilkStore {
  private MilkFactory milkFactory;
 ​
  public MilkStore(MilkFactory milkFactory) {
  this.milkFactory = milkFactory;
  }
 ​
  public Milk getMilk() {
  return milkFactory.createMilk();
  }
 }

通过以上设计模式我们就将 具体产品工厂类 脱耦出来了,每个 产品 都对应自己的 工厂,我们如果增加某种产品就不用修改 原工厂 ,而是添加 具体产品和对应的具体工厂类

这种方法虽然解耦了,但是弊端也是很明显的,那就是增加了系统复杂度,每增加一个产品就需要增加一个具体的产品类和具体的工厂类,这好像也有些违背我们程序员的 "懒人开发原则"。还是老样子,有想法就与方法,这个时候又有了 抽象工厂模式

3. 抽象工厂模式

工厂方法模式 产生的缺点便是它太专一了,一个工厂只生产一种产品,这不是把路走窄了吗!听到这,你们估计都想骂小菜"渣男"了,前面说 工厂方法模式 好的是你,现在又嫌人家太专一了。其实不然,工厂方法的专一确实是有好处的,但是不能过于极致,也不能过于泛滥,不能说一个工厂什么都生产,那么将毫无意义,又回到原点。

针对一个产品生产过于专一,但我们可以针对一个产品族进行生产

产品族值得便是同一类型的产品,比如衣服、裤子这类就是同一类型的产品,简单来说就是一条龙服务,一家奶茶店我们可以卖奶茶还可以买甜点,这样子客户就可以不用去别家买完蛋糕再来奶茶店买奶茶了。

抽象工厂模式 的角色和 工厂方法模式 一致,分为以下几种:

  • 抽象工厂: 提供了创建产品的接口,包含多个创建产品的方法
  • 具体工厂: 实现抽象工厂中多个抽象方法,完成具体产品的创建
  • 抽象产品: 定义产品的规范,描述产品的主要特性和功能
  • 具体产品: 实现抽象产品中定义的接口

图示如下:

码示如下:

抽象工厂:

 public interface DessertFactory {
  Milk createMilk();
  Cake createCake();
 }

具体工厂:

 /**
  * 草原风格甜点工厂
  */
 public class GrassLandsDessertFactory implements DessertFactory {
  @Override
  public Milk createMilk() {
  return new GrasslandsMilk();
  }
 ​
  @Override
  public Cake createCake() {
  return new GrasslandsCake();
  }
 }
 /**
  * 椰香风格甜点工厂
  */
 public class CoconutDessertFactory implements DessertFactory {
  @Override
  public Milk createMilk() {
  return new CoconutMilk();
  }
 ​
  @Override
  public Cake createCake() {
  return new CoconutCake();
  }
 }

这样子如果加同一个产品族的话,只需要添加一个对应的工厂类即可,不需要修改其他的类,但是这个设计模式也是存在部分缺点的,那就是如果产品族中需要添加一个产品,那么还是需要修改产品族工厂 的,但是金无足赤,我们能做的便是不断完善。

3)原型模式

用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象

原型模式存在的角色相对来说比较简单,一个 抽象原型接口 和一个 具体实现类

在原型模式中我们又可以分为:浅克隆深克隆 两个概念

  • 浅克隆: 创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性, 仍指向原有属性所指向的对象的内存地址
  • 深克隆: 创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象的地址
1. 浅克隆

在 Java 中我们最简单的使用方法便是实现 Cloneable 接口,然后重写 clone() 方法 :

那么这种方式是属于深克隆还是浅克隆呢,我们写个小示例测试一下:

从示例上看好像也成功实现了克隆的效果,也支持修改成员变量,但是示例中的 nameString 类型的,我们将其换成对象类型 Person 再试下:

我们看到结果有些许不对劲,怎么 好人卡 都是属于 小王的?其实这就是浅克隆的效果,对具体原型类中的引用类型的属性进行引用的复制。这种情况下我们就要使用 深克隆 来帮忙了。

2. 深克隆

方式 1:手动为引用属性赋值

我们只需要修改克隆方法即可

 public NiceCard clone() throws CloneNotSupportedException {
  NiceCard niceCard = (NiceCard) super.clone();
  // 手动为引用属性赋值
  niceCard.setPerson(new Person());
  return niceCard;
 }

显然,这种手动的方式在关联对象少的情况是可取的,假设关联的对象里面也包含了对象,就需要层层修改,比较麻烦。不推荐这样使用!

方式 2:借助 FastJSON

这种方法很简单,也不用实现 Cloneable 接口,是一种好的解决方法

方式 3:使用 java 流的序列化对象

我们可以创建一个序列化工具类:

然后我们就可以使用工具类来实现克隆:

以上便是深克隆的三种方法,最后一种方式代码虽然比较多,但是比较高效和容易抽象,也是比较常用的方式。

4)建造者模式

将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示

通俗来说就是,将构造的行为与装配的行为相分离,从而可以构造出复杂的对象,这个模式适用于:某个对象的构建过程复杂的情况

由于事先了构建和装配的解耦,不同的构建器,相同的装配,便可以做出不同的对象;相同的构建器,不同的装配顺序也可以做出不同的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无需知道其内部具体构造细节。

建造者模式有如下角色:

  • 抽象建造者类: 这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的部件对象的创建
  • 具体建造者类: 实现抽象建造者类,完成复杂产品各个不见的具体创建方法,在构造过程完成后,提供产品的实例
  • 产品类: 要创建的复杂对象
  • 指挥者类: 调用具体建造者来创建复杂对象的各个部分,在指挥者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建

图示如下:

我们举个例子说明一下:自行车包含了车架,车座等组件的生产,其中车架可以是 碳纤维铝合金等材质,车座可以是 橡胶真皮等材质。不同厂商可以生产不同的自行车,这种生产模式就可以使用建造者模式。已知有两个厂商:GradeBuilder(高档厂商)NormalBuilder(普通厂商) ,和一个DirectorStore(自行车卖家),我们老样子来归类一下:

  • 具体建造者:GradeBuilder(高档厂商)NormalBuilder(普通厂商)
  • 产品类:Bike(自行车)
  • 指挥者类:DirectorStore(自行车卖家)

码示如下:

看完上面代码,我们也盘下 建造者模式 的优缺点:

优点:

  • 建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性。
  • 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
  • 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
  • 建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。

缺点:

建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。

扩展

有用过 Lombok 的小伙伴,都会觉得真香,但是不知道你们有没有用过 Lombok 里面的一个注解@Builder,具体用法如下:

 @Builder
 public class Computer {
 ​
  private String cpu;
 ​
  private String hardDisk;
 ​
  private String memory;
 ​
  public static void main(String[] args) {
  Computer.builder().cpu("英特尔")
  .hardDisk("希捷")
  .memory("金士顿")
  .build();
  }
 }

是不是感到挺好用的,其实这就是用到建造者模式,那我们自己手动实现一下:

使用这种方式我们就可以不用 new 一个对象出来,再一个个 set 值了,代码也简洁了不少。

END

软件设计模式中的 创建者模式 就介绍完了,不过不要着急,其他两种设计模式 结构型设计模式行为型设计模式 也在路上了呢!记得关注就不会迷路啦!路漫漫,小菜与你一同求索!

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。💋

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

查看原文

赞 0 收藏 0 评论 0

蔡不菜丶 发布了文章 · 2020-12-27

软件设计原则讲解,昭昭在目!

大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚! 死鬼~看完记得给我来个三连哦!

本文主要介绍 软件的设计原则

如有需要,可以参考

如有帮助,不忘 点赞

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

忆往昔,学习软件设计原则的时候还是在大学,那时候编程入坑不深, 只觉得可学可不学,毕竟课程有些无聊,看今朝,是自己年轻了!

一、UML 图

不要觉得奇怪为什么不讲软件设计原则而说到了 UML 图,因为软件设计原则和软件设计模式中你讲到最多的便是利用类图来表示 类与类之间的关系,因此我们需要 先会看,再会理,最后再来写!

1. 什么是 UML

统一建模语言(Unified Modeling Language,UML),是用来设计软件的可视化建模语言,它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。

UML 从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。而我们来重点了解一下 类图

2. 类图

1)类图的概念

类图(Class disgram)是显示了模型的静态结构,特别是模型中存在的类、类的内部结构以及它们与其他类的关系等。类图不显示暂时性的信息,类图是面向对象建模的主要组成部分。

2)类图的作用

类图是一种静态的结构图,描述了系统中类的集合、类的属性和类之间的关系,可以简化对系统的理解。

3)类图的表示

类图的表示是使用包含类名(className)、属性(field)和方法(method)且带有分割线的矩形来表示,格式如下:

示例如下:

我们可以看到 属性/方法 前面存在 + / - ,他们表示了这个属性或方法的可见性,表示可见性的符号有三种,如下:

  • + : 表示 public
  • - : 表示 private
  • # : 表示 protected

从上面的示例中我们可以总结出:

  • 属性的完整表示方式是:可见性 名称 : 类型 [ = 缺省值]
  • 方法的完整表示方式是:可见性 名称(参数列表) [ : 返回类型]
中括号里的内容是可选的

也可以将类型放在变量名前面,返回值类型放在方法名前面

4)类图的关系

1. 关联关系

关联关系是对象之间的一种引用关系,用于表示一个类对象与另一个类对象之间的联系。比如老师和学生,学生与课程,校长与学校。关联关系是类与类之间最常用的一种关系,分为 一般关联关系聚合关系组合关系

一般关联关系又分为:单向关联双向关联自关联

  • 单向关联

从图中我们可以很直观的看出以下几点:

  • 有两个类,分别是 Company(公司类)Address(地址类)
  • 每个Company(公司类)中都有一个 Address(地址类)
  • 单向关联是用一个带箭头的实线表示

简而言之:每个公司都有一个地址,通过让 Company(公司类) 持有一个类型为 Address(地址类) 的成员变量来实现。

  • 双向关联

双向关联通俗的意思就是:你中有我,我中有你。从上图中我们可以看出:双方类中各自持有对方类型的成员变量。在 Company(公司类) 中持有一个 Employee(员工类)的集合,表示一个公司有可以有多个员工,而在 Employee(员工类) 中持有一个 Company(公司类) 的成员变量,表示这个员工所属于哪个公司。

需要注意的是,双向关联的连线与单向关联有所不同。双向关联是用一个 不带箭头的直线 表示。

  • 自关联

自关联又是一种比较特殊的关联关系,通俗来说就是 我中有我,是使用一个 带有箭头且指向自身的线表示,这种关联关系我们一般在迭代器模式中比较常见。

2. 聚合关系

聚合关系其实也是关联关系的一种,只不过这种关联关系属于 强关联关系,是 整体和部分之前的关系

聚合关系也是通过成员变量来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。比如一个公司和员工的关系,公司中存在员工,但是如果公司倒闭了,这些员工依然存在,他们可以去别的公司。

注: 聚合关系是使用 带空心菱形的实线 表示的,特别需要注意菱形的方向,菱形是指向整体的

3. 组合关系

既然聚合关系是一种 "好聚好散"的关系,那有没有那种 "鱼死网破"的关系呢?肯定是有的,那就是聚合关系的加强版—组合关系。这是一种更加强烈的聚合关系。

在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在,有点 "一荣俱荣,一损俱损" 的内味了。这就好像我们头和嘴的关系,如果头不存在了,那么嘴也就不存在了。

注: 组合关系是使用 带实心菱形的实线 表示的,同样我们需要注意菱形的方向,菱形是指向整体的

4. 依赖关系

依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,我们通常是某个类的方法通过局部变量、方法参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来实现一些职责。通俗的理解就是:我需要你的时候,咱们就存在了临时性的关系,我不需要你的时候,咱们之间就毫无关系。说着说着,越来越感觉像现实中 女神和舔狗 的关系。就比如以下图示:

注: 依赖关系是使用 带箭头的虚线来表示,箭头从使用类指向被依赖的类。

5. 继承关系

继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系。比如 Student 类Teacher 类 都是 Person 类的子类。

注: 继承关系是使用 空心三角箭头的实线 来表示,箭头从子类指向父类。

6. 实现关系

实现关系是接口与实现类之间的关系,在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。

注: 实现关系是使用 带空心三角箭头的虚线来表示,箭头从实现类指向接口。

微信搜:小菜良记

更多干货值得关注

二、软件设计原则

上面铺垫了那么多知识,主角终于可以上场了。我们也不卖关子了,直接来看下我们将要了解到的软件设计原则:

从导图中我们了解到需要的设计原则总共有 6 种,不必感到害怕,接下来小菜带你一个一个去了解!

1. 开闭原则

开闭原则属于比较基础的设计原则,理解起来也比较简单,就一句话:对扩展开放,对修改关闭,但是往往就是这么简单的一句话,做起来却格外的困难。在程序需要进行扩展的时候,不能去修改原有的代码,实现一个热插拔的效果。

咱们先理一下思路,既然是对修改关闭,对扩展开放,那肯定是不能对原有类进行修改了,不能对原有类进行修改,又想要达到这种效果,那我们就得使用 接口和抽象类 了。

因为抽象出来的东西灵活性较好,适用性较广,只要抽象的合理,就可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象类中派生出新的实现类来进行扩展,当需求发生变化时候,我们只需要根据需求重新派生一个实现类来扩展就可以了。

示例: 我们玩游戏都有一个英雄角色, 这个时候为了氪金,就有了皮肤的需求,但是不同皮肤我们就不能对原有的英雄类进行修改,这是不合理的,所以我们就得从原有的英雄类中派生出新的英雄出来。

2. 里氏代换原则

里氏代换原则:任何基类可以出现的地方,子类一定可以出现。简单来说就是子类可以扩展父类的功能,但不能改变父类原有的功能,也就是子类继承父类时,除了添加新的方法完成新增功能外,尽量不要重写父类的方法。

里氏代换原则也是面向对象设计中最基本的原则之一,通俗意思就是父类总能被子类替代。如果我们通过重写父类的方法来完成新的功能,这样写起来虽然比较简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。

示例:正方形是一个特殊的长方形,只不过是正方形的长宽都一样,那我们就用反证法来试一下是否符合里氏代换原则

我们先理一下目前的所看到的东西,有三个类:Rectangle(长方形类)、Square(正方形类)、Client(客户端类),其中 正方形类 继承自 长方形类,因此这两个类属于继承关系。客户端类 其中有个方法需要依赖 长方形类,因此这两个类属于 依赖关系。一般来说长方形的长会大于长方形的宽,因此客户端类中的方法实现的功能就是当长方形的宽大于长时,长的值就会加1,直到长的值大于宽的值。

接下来我们用代码来实现一下:

Rectangle

Square

Client

我们可以看到有个长为10,宽为15的长方形,我们也成功实现了功能,扩容后的长方形长为16,宽为15。根据里氏代换原则,我们如果传入的是 正方形类也是可以实现这个功能的,我们继续来试一下:

 public static void main(String[] args) {
  Square square = new Square();
  square.setWidth(10);
  System.out.println(square);
  resize(square);
 }

我们在控制台等了许久,发现并没有输出结果,直到栈溢出。这是长一直与宽相等,导致退出不了循环。那么这个时候我们得出结论,这种设计是不符合里氏代换原则的,因此这种设计是错误的。所以我们平时在设计系统的时候,就需要考虑我们设计的父子类是否符合里氏代换原则,那么根据以上例子我们可以作出改进,既然 长方形类 不适合做 正方形类 的子类,那我们是否应该考虑抽象出一个 四边形类 出来,来作为两个类的父类。

其中我们抽象出了 Quadrangle(四边形类) 其中定义了获取长和宽的两个方法,Square(正方形类)Rectangle(长方形类)分别实现 四边形类,接下来我们用代码来实现:

Quadrangle

 public interface Quadrangle {
 ​
  double getWidth();
 ​
  double getLength();
 }

Square

Rectangle

Client:

看完代码我们改变的只有增加了 四边形类 和修改了 正方形类,这样子我们使用扩容方法的时候需要传的是 长方形类,而 正方形类 不继承与 长方形类,扩容这个方法不适用,因此满足了里氏代换原则。

3. 依赖倒转原则

依赖倒转高层模块不应该依赖底层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。

简单来说依赖倒转的核心就是对抽象进行编程,不要对实现进行编程,这样就会降低客户与实现模块之间的耦合。

示例:我们如果要组装一台电脑,组装电脑需要用到的配件有硬盘,内存和CPU,每个配件都有不同的品牌以供选择,我们这里选择希捷的硬盘,英特尔的CPU,金士顿的内存,图示如下:

代码实现如下:

XIJieHardDisk

 @Data
 public class XIJieHardDisk {
  private String capacity;
 }

KingstonMemory

 @Data
 public class KingstonMemory {
  private String capacity;
 }

IntelCpu

 public class IntelCpu {
  public void run() {
  System.out.println("英特尔处理器开始运行");
  }
 }

Computer

 @Data
 public class Computer {
 ​
  private XIJieHardDisk hardDisk;
 ​
  private IntelCpu cpu;
 ​
  private KingstonMemory memory;
 ​
  public void run() {
  System.out.println("计算机开始运行,参数如下:");
  System.out.println("硬盘容量为 : " + hardDisk.getCapacity());
  System.out.println("内存容量为 : " + memory.getCapacity());
  cpu.run();
  }
 ​
  public static void main(String[] args) {
  Computer computer = new Computer();
  XIJieHardDisk hardDisk = new XIJieHardDisk();
  hardDisk.setCapacity("1T");
  IntelCpu cpu = new IntelCpu();
  KingstonMemory memory = new KingstonMemory();
  memory.setCapacity("16G");
  computer.setHardDisk(hardDisk);
  computer.setCpu(cpu);
  computer.setMemory(memory);
  computer.run();
  }
 }
 /** OUTPUT:
 计算机开始运行,参数如下:
 硬盘容量为 : 1T
 内存容量为 : 16G
 英特尔处理器开始运行
 **/

根据运行结果我们计算机也顺利组装成功,但是目前看起来好像是没问题,如果我们想要换个品牌的 CPU 或者 内存条,我们除了增加一个 对应品牌的类之外我们是不是还要修改 Computer 类,刚看完上部分的小伙伴肯定马上意识到这不就违反了 开闭原则 吗,真是瞎胡闹。

那既然这种设计是错误的,我们就按照 依赖倒装原则 来改进一下:高层模块不应该依赖底层模块,两者都应该依赖其抽象,我们需要修改 Computer 类,让Computer类 依赖抽象(各个配件的接口),而不是依赖于各个组件具体的实现类。图示如下:

代码中我们只需要修改 Computer 类 和增加三个配件的主接口,让子品牌分别实现对应的接口即可:

HardDisk:

 public interface HardDisk {
 ​
  void setCapacity(String data);
 ​
  String getCapacity();
 }

Memory:

 public interface Memory {
 ​
  void setCapacity(String data);
 ​
  String getCapacity();
 ​
 }

Cpu:

 public interface Cpu {
  void run();
 }

Computer:

 @Data
 public class Computer {
 ​
  private HardDisk hardDisk;
 ​
  private Cpu cpu;
 ​
  private Memory memory;
 ​
  public void run() {
  System.out.println("计算机开始运行,参数如下:");
  System.out.println("硬盘容量为 : " + hardDisk.getCapacity());
  System.out.println("内存容量为 : " + memory.getCapacity());
  cpu.run();
  }
 ​
  public static void main(String[] args) {
  Computer computer = new Computer();
  XIJieHardDisk hardDisk = new XIJieHardDisk();
  hardDisk.setCapacity("1T");
  IntelCpu cpu = new IntelCpu();
  KingstonMemory memory = new KingstonMemory();
  memory.setCapacity("16G");
  computer.setHardDisk(hardDisk);
  computer.setCpu(cpu);
  computer.setMemory(memory);
  computer.run();
  }
 }
 /** OUTPUT:
 计算机开始运行,参数如下:
 硬盘容量为 : 1T
 内存容量为 : 16G
 英特尔处理器开始运行
 **/

这样子让高层模块依赖抽象模块,就可以实现解耦了,更加方便扩展。

4. 接口隔离原则

客户端不应该被迫依赖于它不适用的方法,一个类对另一个类的依赖应该建立在最小的接口上。

简单来说就是强扭的瓜不甜,不适合自己的就不要强行加成。比如说一台手机可以打电话,发短信,上网,但是上网这个功能对于老人机就不适用了,我们就不应该把上网这个功能强行加给老人机。原本的设计应该是这样的:

这样子明显是不合理的,我们根据 接口隔离原则进行改进:一个类对另一个类的依赖应该建立在最小接口上,那我们应该把每个功能都抽取成各个接口,然后每种手机通过依赖的方式引入功能,图示如下:

代码实现如下:

InternetFun:

 public interface InternetFun {
  void internet();
 }

MessageFun:

 public interface MessageFun {
  void message();
 }

PhoneFun:

 public interface PhoneFun {
  void phone();
 }

NewPhone:

 public class NewPhone implements InternetFun, MessageFun, PhoneFun {
  @Override
  public void internet() {
  System.out.println("上网功能已具备");
  }
 ​
  @Override
  public void message() {
  System.out.println("发短信功能已具备");
  }
 ​
  @Override
  public void phone() {
  System.out.println("打电话功能已具备");
  }
 }

OldPhone:

 public class OldPhone implements PhoneFun, MessageFun{
  @Override
  public void message() {
  System.out.println("发短信功能已具备");
  }
 ​
  @Override
  public void phone() {
  System.out.println("打电话功能已具备");
  }
 }

通过接口隔离原则,我们可以需要什么功能,就实现什么接口,满足了最小依赖。

5. 迪米特法则

迪米特法则又成为最少知识原则。只和你的直接朋友交谈,不跟 "陌生人说话"。

简单来说就是如果两个软件实体无需直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用,目的就是为了降低类之间的耦合度,提高模块间的相对独立性。

示例: 对于明星来说,他不必和经纪公司还有粉丝直接打交道,这些事情只需要交给经纪人来处理就好了,经纪人就相当于是第三方,粉丝见面会和签约这种事都是通过经纪人在中间处理,而不用明星自己打交道。图示如下:

代码实现如下:

Star:

 @Data
 public class Star {
  private String name;
 ​
  public Star(String name) {
  this.name = name;
  }
 }

Company:

 @Data
 public class Company {
  private String name;
 ​
  public Company(String name) {
  this.name = name;
  }
 }

Fans:

 @Data
 public class Fans {
  private String name;
 ​
  public Fans(String name) {
  this.name = name;
  }
 }

Agent:

 @Data
 public class Agent {
 ​
  private Star star;
 ​
  private Company company;
 ​
  private Fans fans;
 ​
  public Agent(Star star, Company company, Fans fans) {
  this.star = star;
  this.company = company;
  this.fans = fans;
  }
 ​
  public void meeting() {
  System.out.println("经纪人安排" + star.getName() + "与粉丝:" + fans.getName() + " 见面了");
  }
 ​
  public void bussiness() {
  System.out.println("经纪人安排" + star.getName() + "与娱乐公司:" + company.getName() + " 签约了");
  }
 ​
  public static void main(String[] args) {
  Star star = new Star("小菜");
  Company company = new Company("Cbuc娱团");
  Fans fans = new Fans("小菜菜");
  Agent agent = new Agent(star, company, fans);
  agent.meeting();
  agent.bussiness();
  }
 }
 /** OUTPUT:
 经纪人安排小菜与粉丝:小菜菜 见面了
 经纪人安排小菜与娱乐公司:Cbuc娱团 签约了
 **/

迪米特法则就是通过中间人来实现双方及多方的交互,这样每方之间的关系就不会很杂乱。

6. 合成复用原则

合成复用原则是指尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现

通常类的复用分为 继承复用合成复用 两种

继承复用 相对来说会比较简单易实现,但是也存在以下缺点:

  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类。父类对子类是透明的,所以这种复用又称为 "白箱"复用
  2. 子类与父类的耦合度高。父类的任何改变都会导致子类实现发生变化,这不利于类的扩展与维护
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:

  1. 它维护了类的封装性,因为成分对象的内部细节是新对象看不见的,所以这种复用又称为"黑箱"复用
  2. 对象间的耦合度低,可以在类的成员位置声明抽象
  3. 复用的灵活性高,这种复用可以在运行时动态进行,新对象可以动态地引用于成分对象类型相同的对象

我们来看下不同复用的图示:

继承复用:

首先有个汽车的抽象类,这个时候如果按照"动力源"划分的话,我们又可以扩展为 "汽油汽车" 和 "能源汽车",接着我们又可以针对颜色来划分,分为黑色和红色乃至更多其他颜色。这样子有个很明显的问题就是,通过继承复用就会产生很多子类。那我们接下来就用 聚合复用 来实现一下:

我们将颜色单独抽取了出来,以聚合的方式来实现,可以看到整个设计也比较简单,这样就达到了实现合成复用原则

END

软件设计原则就已经讲完啦,往往觉得不以为然的东西才更加重要,这就是我们的基础,得更加牢固才行。放个小彩蛋,听说小菜会接着出软件设计模式讲解,赶紧关注起来吧! 微信搜:小菜良记

路漫漫,小菜与你一同求索!

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。💋

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

查看原文

赞 9 收藏 8 评论 0

蔡不菜丶 发布了文章 · 2020-12-21

前端妹子对我提出这种要求,我???

大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚! 死鬼~看完记得给我来个三连哦!

本文主要介绍 Jenkins部署前端项目

如有需要,可以参考

如有帮助,不忘 点赞

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

这不上周搭了个 Jenkins 来部署项目,自从使用上这个工具后,每个后端同学脸上都洋溢着喜悦。这不,在茶水间打杯热水的功夫

“小菜,我也想要”一阵声音从身后飘过,着实吓了我一跳,心跳不禁加快了几下,脑子瞬间闪过几百个画面

“你想要啥啊你要”我强行镇定下来,问了下

“就是你们后端部署的那个神器啊,我们前端每次最新的包也都得打包完上传到 Nginx 下,可麻烦了!”

“害,你说这个啊,我还以为你想啥呢,放心放心,我等会就给你整个!”

刚说完,那妹子脸上就笑开了花,道了声谢就走了,看着渐远的背影不禁想着:原来女孩子也有这么容易满足的时候啊!

Jenkins 的安装已经在上篇后端项目部署中详细介绍过了,没看过的小伙伴可以前往学习一番别当工具人了,手摸手教会你 Jenkins,那咱们就话不多说,直接来看一下 Jenkins 如何部署前端项目,毕竟不能让前端妹子久等了。

Jenkins前端项目部署

老样子我们画张图来了解下部署的一个大致过程:

从图中我们可以看出来我们需要的内容:

  • 两台服务器(一台服务器也行,这样就可以不用进行 SSH 远程调用了)
  • JenkinsNginxNode 三个工具的安装

准备好以上需要的内容,部署前端那就是 手捏把掐

1. Nginx 安装

如果服务器上已经安装了 Nginx ,这一个步骤可以跳过

Nginx下载地址:点击下载

我们将下载好的 nginx-1.9.9.tar.gz 上传到服务器上的 /usr/local

执行以下命令:

 # 解压
 tar -zxvf nginx-1.9.9.tar.gz
 ​
 #进入nginx目录
 cd nginx-1.9.9
 ​
 # 配置
 ./configure --prefix=/usr/local/nginx
 ​
 # 编译
 make && make install

执行以上命令后,我们可以在 /usr/local/nginx中看到以下目录结构:

然后我们进入 conf/nginx.conf文件中修改下默认的监听的端口号:

然后输入 ./sbin/nginx 进行启动,然后我们在网页上访问http://服务器IP:8090/,看到以下页面说明 Nginx 启动成功:

2. Node 安装

然后我们需要在我们的 Jenkins 所在的服务器上也安装上 NodeJS 工具,步骤如下:

  • 下载完我们把安装包放到服务器上的 /usr/local 目录下(存放目录没有要求)
  • 然后输入以下指令
 # 解压
 tar -xvf node-v14.15.1-linux-x64.tar.xz
 ​
 # 建立软连接
 ln -s /usr/local/node-v14.15.1-linux-x64/bin/node /usr/local/bin/
 ln -s /usr/local/node-v14.15.1-linux-x64/bin/npm /usr/local/bin/
  • 测试是否安装成功
     node -v
     npm -v
![](https://cbuc.top/1608355786031.png)

3. Jenkins 配置

完成 NginxNode 的安装后,我们就可以进行下一步操作了。我们需要在 Jenkins 中安装上 NodeJS 插件:

然后依次点击 Manage Jenkins -> Global Tool Configuration,进行 NodeJS 的配置

配置里面选择我们/usr/local/node-v14.15.1-linux-x64/,因为我们已经自己安装了 Node,所以这个自动安装的选项可以取消勾选。

4. 前端部署

以上都是前期的准备工作,到这里我们就完成了前期工作的准备,接下来我们就可以来部署项目了!

  • 首先创建一个前端流水线项目:

然后建立Jenkinsfile构建脚本,示例如下:

 //gitlab的凭证
 def git_auth = "89f2087f-a034-4d39-a9ff-1f776dd3dfa8"
 node {
      stage('拉取代码') {
          checkout([$class: 'GitSCM', branches: [[name: '*/master']],
          doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [],
          userRemoteConfigs: [[credentialsId: "${git_auth}", url:
          'git@192.168.66.100:cbuc_group/test_front.git']]])
      }
      stage('打包,部署网站') {
          //使用NodeJS的npm进行打包
          nodejs('nodejs12'){
          sh '''
          npm install
          npm run build
          '''
          }
          //=====以下为远程调用进行项目部署========
          sshPublisher(publishers: [sshPublisherDesc(configName: 'master_server',
          transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: '',
          execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes:
          false, patternSeparator: '[, ]+', remoteDirectory: '/usr/local/nginx/html',
          remoteDirectorySDF: false, removePrefix: 'dist', sourceFiles: 'dist/**')],
          usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
      }
 }

保存后我们点击 Build Now,等待一会就可以看到项目已经成功部署上去啦!

END

以上便是前端项目的部署,掌握了这个软技能后,赶紧为你们的前端妹子搭建一下吧!也许可以优先获取择偶权哦!

更加具体的 Jenkins 使用,请点击 链接 跳转学习使用哦,路漫漫,小菜与你一同求索!

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。💋

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

查看原文

赞 7 收藏 6 评论 1

蔡不菜丶 发布了文章 · 2020-12-13

别当工具人了,手摸手教会你 Jenkins !

大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚! 死鬼~看完记得给我来个三连哦!

本文主要介绍 Jenkins

如有需要,可以参考

如有帮助,不忘 点赞

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

“唉,每天提交完代码都得自己打包再部署到测试环境和开发环境,好麻烦啊!都快变成运维了”

“啊?哦!确实,每天打包部署确实都成为了工具人了”

一段简白的对话快速的隐灭在办公室中,却引发了我的思考,“这么麻烦的过程肯定已经有了很好的解决方案,毕竟程序员是面向懒惰编程,自己对 Jenkins 这个工具有所耳闻已经很久了,看来今天得对它下手了”

说干就干,今天咱们就来求索一下 JenKins,看完你不妨也给你们项目整一个,给自己多增加点划水的时间!

读前须知: 本文较长,从安装到使用,一步步带你超神!

微信公众号关注:[ 小菜良记 ] ,带你领略技术风骚!

一、Jenkins 是什么

Jenkins是一个开源软件项目,是基于java开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件的持续集成变成可能。

简单来说,它就是一个 持续集成 的工具!

1. 持续集成

持续集成(Continuous Integration),简称 CI。频繁地将代码集成到主干之前,必须通过自动化测试,只要有一个测试用例失败,就不能集成。通过持续集成,团队可以快速从一个功能到另外一个功能。

好处:

  • 降低风险,由于持续集成不断去构建,编译和测试,可以很早发现问题
  • 减少重复性的工作
  • 持续部署,提供可部署单元包
  • 持续交付可供使用的版本

2. Jenkins 持续集成

我们先通过这张图来看到 Jenkins 在其中起到的作用:

  • 首先,开发人员将代码提交到 Git 仓库
  • 然后 Jenkins 使用 Git 插件来拉取 Git 仓库的代码,然后配合 JDK、Maven 等软件完成代码编译,测试、审查、、测试和打包等工作
  • 最后 Jenkins 将生成的 jar/war 推送到 测试/生产 服务器 ,供用户访问

整套步骤下来,作为开发人员我们只需要提交下代码,剩下的工作都交给了 Jenkins ,真是美滋滋,怎么没有早点上这个工具的车!

二、Jenkins 安装

磨刀不误砍柴工,没刀的情况下说再多都是虚的。我们就先来看下 Jenkins 是如何安装的吧!

1. 安装JDK

因为 Jenkinsjava 写的,所以要运行起来必须要配置 java 运行环境。这里就不赘诉 JDK 的安装过程了

2. 下载安装 Jenkins

  • 下载

我们可以进入下载页面选择我们要安装的版本:下载地址, 我们这里使用的版本是 :jenkins-2.190.3-1.1.noarch.rpm

  • 安装

然后把下载好的 rpm 包上传到我们的服务器,通过 rpm -ivh jenkins-2.190.3-1.1.noarch.rpm 进行安装,然后编辑 etc 目录下的 jenkins 配置文件:vim /etc/sysconfig/jenkins,需要改的地方如下(也可以选择不改):

 JENKINS_USER="root"
 JENKINS_PORT="8888"

  • 启动

systemctl start jenkins

  • 访问

通过浏览器访问 http://服务器IP:8888/,看到以下页面说明启动成功了

然后我们在服务器上从指定文件中获取密码,进行下一步。

这一步我们可以先跳过插件安装,因为Jenkins插件需要连接默认官网下载,速度非常慢:

然后我们添加一个管理员账号来管理:

看到以下页面就说明设置成功了:

微信公众号关注:小菜良记 ,带你领略技术风骚!

三、Jenkins 使用

1. 插件加速

工欲善其事,必先利其器

贴心的小菜是不会让你遭受等待的痛苦的,首先我们进入 Jenkins -> Manage Jenkins -> Manage Plugins ,点击 install

然后我们在安装 Jenkins 的服务器上进入 /var/lib/jenkins/updates 目录,可以看到有个 default.json 文件,第一步:我们需要替换里面的部分字段,输入命令如下:

 sudo sed -i 's#updates.jenkins.io/download/plugins#mirrors.tuna.tsinghua.edu.cn/jenkins/plugins#g' default.json && sudo sed -i 's#www.google.com#www.baidu.com#g' default.json

第二步:我们进入到 /var/lib/jenkins目录,编辑 hudson.model.UpdateCenter.xm,将里面的 https://updates.jenkins.io/update-center.json修改为 http://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json

最后一步: 输入以下命令进行重启 Jenkins

 systemctl restart jenkins

通过以上步骤,我们就可以愉快的安装插件了!

2. 用户管理

Jenkins 中我们也可以进行用户权限管理,这个时候我们需要借助插件 Role-based Authorization Strategy

  • 首先安装 Role-based Authorization Strategy 插件

  • 开启全局安全配置

将授权策略切换为 "Role-Based Strategy"

  • 创建用户

更改完授权策略,我们就可以来创建用户了,进入系统管理页面中的Manage Users

这里我们创建了两个用户,分别是 cbuc1cbuc2

  • 创建角色

创建好用户,我们就可以来创建角色了,在系统管理页面进入 Manage and Assign Roles

角色主要分为 Global roles(全局角色)Item roles(项目角色)

Global roles(全局角色): 管理员等高级用户可以创建基于全局的角色

Item roles(项目角色): 针对某个或者某些项目的角色

我们系统现在已经存在了两个用户,然后我们就可以给这两个用户绑定对应的角色

3. 凭证管理

什么是凭证呢? 凭证 可以用来存储需要密文保护的数据库密码,GitLab 密码信息,Docker 私有仓库的登录密码。保存了这些信息后,Jenkins 就可以和这些第三方的应用进行交互。当然,这还是得借助 Jenkins 的插件!

1)安装

首先安装 Credentials Binding 插件

安装好插件后,在系统首要的菜单栏中就会多了个 凭证 菜单

点击进去,我们可以看到可以添加的凭证有 5 种:

  1. Username with password :用户名和密码
  2. SSH Username with private key: 使用 SSH 用户和密钥
  3. Secret file: 需要保密的文本文件,使用时 Jenkins 会将文件复制到一个临时目录中,再将文件路径设置到一个变量中,等构建结束后,所复制的 Secret file 就会被删除
  4. Secret text: 需要保存的一个加密的文本串,如钉钉机器人或 GitHubapi token
  5. Certificate: 通过上传证书文件的方式

我们平时比较常用的类型为:Username with passwordSSH Username with private key

2)Git 凭证管理

我们如果要使用 JenkinsGitLab 拉取项目代码,我们就得使用凭证来验证。

  • 安装 Git 插件

我们需要在 Jenkins 中安装 Git插件 来拉取项目代码

然后我们在服务器上也需要安装 Git 工具

 # 安装命令
 yum install git -y
 # 验证命令
 git --version

1. 方式1:用户密码类型

我们可以使用 用户密码 登录后拉取项目代码,这个时候我们需要用到 凭证的 Username with password 类型

创建成功我们就可以测试是否可用,我们先创建一个 FreeStyle 项目

然后在 GitLab 中复制我们项目的 URL

Credentials 中选择我们刚刚创建的凭证,保存配置后,我们点击 Build Now 来构建项目:

这个时候在控制台可以看到输出

然后在进入服务器的 /var/lib/jenkins/workspace 目录中看到我们拉取的项目:

说明我们已经成功使用 用户密码 凭证模式拉取到 Git项目了

2. 方式2:SSH密钥类型

除了用账号密码方式来验证 Git ,我们还可以用 SSH密钥 来验证,步骤流程如下:

从图上我们可以得知,第一步需要生成 公私钥,我们在 Jenkins服务器 上输入以下指令生成:

ssh-keygen -t rsa 输入指令后,一路回车,便可在 /root/.ssh/ 目录下生成公私钥:

  • id_rsa:私钥文件
  • id_rsa.pub:公钥文件

然后我们把生成的公钥放在 GitLab 中,root账户登录->点击头像->Settings->SSH Keys,复制 id_rsa.pub 中的内容,点击 "Add key"

然后我们再回到 Jenkins 系统页面中添加凭证,选择 SSH Username with private key ,把刚刚生成的私有文件内容复制过来

添加后就会生成一条凭证

创建成功我们就可以测试是否可用,我们先创建一个 FreeStyle 项目

然后在 GitLab 中复制我们项目的 URL

Credentials 中选择我们刚刚创建的凭证,保存配置后,我们点击 Build Now 来构建项目:

这个时候在控制台可以看到输出

然后在进入服务器的 /var/lib/jenkins/workspace 目录中看到我们拉取的项目:

说明我们已经成功使用 SSH Username with private key 凭证模式拉取到 Git项目了

4. 项目管理

1)Maven 安装

我们现在开发中的项目大部分都是 Maven 项目,使用 Maven 项目,我们就需要进行 依赖管理,因此我们应当在服务器上安装 Maven 来下载项目依赖。

  • 安装 Maven

我们可以从 Maven 官网上下载压缩包,然后上传到服务器上进行解压

tar -xzf apache-maven-3.6.0-bin.tar.gz

  • 配置环境变量

vim /etc/profile

 export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk
 export MAVEN_HOME=/home/maven/apache-maven-3.6.2
 export PATH=$PATH:$JAVA_HOME/bin:$MAVEN_HOME/bin

编辑后使配置文件生效:

 source /etc/profile

验证:

 mvn -v

然后设置 Mavensettings.xml

 # 创建本地仓库目录
 mkdir /data/localRepo
 vim /home/maven/apache-maven-3.6.2/conf/settings.xml

将本地仓库改为: /root/repo/

添加阿里云私服地址:alimaven aliyun maven http://maven.aliyun.com/nexus/content/groups/public/ central

  • Jenkins配置

Jenkins 我们也需要配置 JDKMaven 的关联.

进入 Jenkins -> Global Tool Configuration -> JDK

进入 Jenkins -> Global Tool Configuration -> Maven

添加全局变量

进入Manage Jenkins->Configure System->Global Properties,添加三个全局变量

JAVA_HOME、M2_HOME、PATH+EXTRA

然后我们进入项目中点击 configure

然后添加 shell 执行脚本:

保存后重新构建,查看控制台,可以看到 mvn 构建成功:

2)war 包部署

如果我们的项目是打成 war 包的形式,那么我们需要借助 tomcat 容器来运行,那么我们首先便是要先安装一个 tomcat

Tomcat 安装

我们将事先下载好的 Tomcat 安装包上传到服务器上,通过 tar -xzf apache-tomcat-8.5.47.tar.gz 解压,然后运行 bin目录下的 start.sh启动 Tomcat ,看到以下结果则说明启动成功:

下一步我们需要配置Tomcat用户角色权限,默认情况下Tomcat是没有配置用户角色权限的

首先我们需要修改 tomcat/conf/tomcat-users.xml 文件:

(复制)内容如下:

 <role rolename="tomcat"/>
 <role rolename="role1"/>
 <role rolename="manager-script"/>
 <role rolename="manager-gui"/>
 <role rolename="manager-status"/>
 <role rolename="admin-gui"/>
 <role rolename="admin-script"/>
 <user username="tomcat" password="tomcat" roles="manager-gui,manager-script,tomcat,admin-gui,admin-script"/>

然后修改 /tomcat/webapps/manager/META-INF/context.xml 文件,将以下内容注释:

然后进入tomcat 页面,点击进入:

账号密码都是 tomcat

成功页面如下:

这样子我们就完成了 tomcat 的安装,然后接下来就可以进行部署了

Tomcat 部署
  • jenkins 中安装 Deploy to container 插件
  • 添加 Tomcat 凭证

  • 构建配置

在项目的 configure 中配置

然后点击构建,查看控制台输出:

显示已经部署成功,然后访问项目页面,可以看到 war 包项目部署成功:

3)jar 包部署

上面说完了 war 包项目是如何部署的,但是我们现在项目用到比较多的还是 SpringBoot ,这个时候打出来的是 jar 类型,但是 SpringBoot 里面内置了 tomcat 容器,这样子我们就不需要借助外部 tomcat 容器的使用了。

  • 首先我们在 Jenkins 中下载 Maven 插件,这个时候新建项目的时候会有个 Maven 项目的选项

然后在项目的 configure 中作如下配置:

Repository URL:库地址 Credentials:凭证 Branch Specifier (blank for ‘any’):分支

Run only if build succeeds:在构建成功时执行后续步骤 Add post-build step:添加构建后的步骤 Send files or execute commands over SSH:通过ssh发送文件或执行命令
  • 安装 Publish Over SSH 插件

因为我们要部署的服务器与 Jenkins 不在同一个服务器上,所以我们需要这个插件来远程部署

安装好插件后我们需要先配置远程服务器,在 Jenkins 服务器上输入 ssh-copy-id 远程服务器IP 将公钥拷贝到远程服务器上,然后在 Jenkins 系统配置中添加服务器信息,如下:

完成以上步骤后,我们就可以回到项目的 configure 中添加我们刚刚配置的服务器信息:

Name:SSH Servers中配置的服务器 Source files:源文件 Remove prefix:删除前缀 Remote directory:上传到服务器的目录 Exec command:执行的脚本

完成以上步骤,我们就可以愉快的点击 Build Now 了!

4)流水线项目

Jenkins 中自动构建项目的类型有很多,常用的有以下三种:

  • 自由风格软件项目(FreeStyle Project)
  • Maven 项目(Maven Project)
  • 流水线项目(Pipeline Project)

每种类型的构建其实都可以完成一样的构建过程与结果,只是在操作方式、灵活度等方面有所区别,其中流水线类型灵活度比较高,其他两种类型我们在上面的例子中都已经尝试过了,下面我们就来介绍如何构建流水线项目。

1. 概念

Pipeline 就是一套运行在 Jenkins 上的工作流框架,将原来独立运行与单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂流程编排和可视化工作

2. 优点
  • 代码Pipeline 以代码的形式实现,通常被检入源代码控制,使团队能够编辑,审查和迭代其传送流程。
  • 持久性: 无论是计划内的还是计划外的服务器重启,Pipeline 都是可恢复的
  • 可停止:Pipeline 可接收交互式输入,以确定是否继续执行 Pipeline
  • 多功能:Pipeline 支持现实世界中复杂的持续交付要求,它支持 fork/join 、循环执行、并行执行任务的功能
  • 可扩展:Pipeline 插件支持其 DSL 的自定义扩展,以及与其他插件集成的多个选项
3. 创建

创建 Pipeline 项目之前我们需要安装 Pipeline 插件:

然后在创建项目的时候便会多了 Pipeline 类型:

选择好项目类型之后我们就可以在项目中的 configure 进行配置了:

  • 首先老样子配置好 git 地址,跟上面一样,这里不多作赘诉
  • 然后配置 Pipeline 脚本

Pipeline 项目是统一通过 Pipeline 脚本来管理,这样也更好的提高灵活性

Hello World 模板:

 pipeline {
  agent any
  stages {
  stage('Hello') {
  steps {
  echo 'Hello World'
  }
  }
  }
 }

stages: 代表整个流水线的所有执行阶段,通常 stages 只有1个,里面包含多个 stage

stage: 代表一个阶段内需要执行的逻辑,steps 里面是 shell 脚本,git 拉取代码,ssh 远程发布等任意内容

声明式 Pipeline 模板:

 pipeline {
  agent any
  stages {
      stage('拉取代码') {
          steps {
            echo '拉取代码'
          }
      }
      stage('编译构建') {
          steps {
            echo '编译构建'
          }
      }
      stage('项目部署') {
          steps {
            echo '项目部署'
          }
      }
  }
}

你也完全不用担心不会书写 Pipeline 脚本,我们可以点击 [Pipeline Syntax] 跳转到 Pipeline 代码生成页面

书写好脚本后点击构建,可以看到整个构建过程:

如果我们需要部署到不同环境,比如生产环境和开发环境,我们还可以在项目的 configure 中进行配置:

  • 首先需要安装 Extended Choice Parameter 插件
  • 然后在配置中添加 Extended Choice Parameter 参数

完成以上配置后,点击保存,这个时候我们就可以在构建的时候选择需要部署的服务器了

然后我们就可以从 Pipeline 脚本中读取我们选择的参数,贴上该项目的构建脚本,如下:

 node {
  //git凭证ID
  def git_auth = "7fdb3fa3-74eb-4862-b36f-c03701f71250"
  //git的url地址
  def git_url = "git@192.168.100.131:cbuc_group/cbuc_web.git"
  //获取当前选择的服务器名称
  def selectedServers = "${publish_server}".split(",")
 ​
      stage('开始拉取代码') {
        checkout([$class: 'GitSCM', branches: [[name: '*/v3.0']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: git_auth, url: git_url]]])
      }
      stage('开始打包') {
        sh "mvn -Dmaven.test.skip=true clean package"
      }
      stage('开始远程部署') {
          //遍历所有服务器,分别部署
          for(int j=0;j<selectedServers.length;j++){
          //获取当前遍历的服务器名称
          def currentServerName = selectedServers[j]
          //生产环境部署目录
          def pro_address = "/home/pro/java"
          //开发环境部署目录
          def dev_address = "/home/dev/java"
         ​
          //根据不同的profile来部署服务器
          if(currentServerName=="pro"){
          sshPublisher(publishers: [sshPublisherDesc(configName: 'pro_server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: 'sh build.sh', execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: pro_address, remoteDirectorySDF: false, removePrefix: 'target', sourceFiles: 'target/cbuc_web-0.0.1-SNAPSHOT.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
          }else if(currentServerName=="dev"){
          sshPublisher(publishers: [sshPublisherDesc(configName: 'dev_server', transfers: [sshTransfer(cleanRemote: false, excludes: '', execCommand: "sh build.sh", execTimeout: 120000, flatten: false, makeEmptyDirs: false, noDefaultExcludes: false, patternSeparator: '[, ]+', remoteDirectory: dev_address, remoteDirectorySDF: false, removePrefix: 'target', sourceFiles: 'target/cbuc_web-0.0.1-SNAPSHOT.jar')], usePromotionTimestamp: false, useWorkspaceInPromotion: false, verbose: false)])
      }
    }
  }
}

还有一种情况就是如果部署 Jenkins 的服务器宕机了,这个时候就会丢失 Pipeline 脚本文件,重新书写是一件很麻烦的事情,那么我们就可以将脚本文件放到我们的项目的根目录下,然后在 configure 中配置 Pipeline 脚本文件的位置:

然后我们点击构建,可以看到结果也是成功的:

5)构建触发器

上面我们讲完了几种项目的构建方式,其中都是通过手动点击构建进行构建的,我们也可以通过触发器来构建

常用的有:

1. Build After Other Projects Are Built

其他工程构建后触发。在选项中填写我们关注的项目,其中也支持3个选择以供选择:

Trigger only if build is stable: 仅在项目稳定构建时执行

Trigger even if the build is unstable: 即使项目构建不稳定也执行

Trigger even if the build fails: 即使项目构建失败也执行

2. Build Periodically

定时构建。语法类型如 cron 表达式,定时字符串从左往右分别为: 分 时 日 月 周

3. Poll SCM

轮询 SCM。指定时间扫描本地代码仓库的代码是否有变更,如果代码有变更就触发项目构建。

4. Trigger builds remotely

远程触发构建。通过使用我们定义的密钥,然后访问构建地址:http://192.168.100.131:8888/job/test01/build?token=123123

5. 自动触发构建

刚才我们看到在Jenkins的内置构建触发器中,轮询SCM可以实现Gitlab代码更新,项目自动构建,但是该方案的性能不佳。那有没有更好的方案呢? 有的。就是利用Gitlabwebhook实现代码push到仓库,立即触发项目自动构建。

完成自动触发构建我们需要在 Jenkins 安装插件:GitLab HookGitLab

然后我们在 Build Trigger 中就可以看到多了一个选项:

复制这串 WebHook 地址,跟着到 GitLab 页面进行设置:

路径步骤:Admin Area -> Settings -> Network

然后我们在对应的项目中进行设置:

最后再回到 Jenkins 页面中做以下配置:Manage Jenkins->Configure System

做完以上配置,我们就可以愉快的代码进行自动触发构建了!

END

这篇文章较长,都是满满的干货,从安装到使用,一步步带你入 运维 的坑,学完这篇快给你的项目用上吧!路漫漫,小菜与你一同求索!

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。💋

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

查看原文

赞 30 收藏 21 评论 4

蔡不菜丶 发布了文章 · 2020-12-06

提高生产力,最全 MyBatisPlus 讲解!

大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚! 死鬼~看完记得给我来个三连哦!

本文主要介绍 MybatisPlus的使用

如有需要,可以参考

如有帮助,不忘 点赞

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

如果你每天还在重复写 CRUDSQL,如果你对这些 SQL 已经不耐烦了,那么你何不花费一些时间来阅读这篇文章,然后对已有的老项目进行改造,必有收获!

一、MP 是什么

MP 全称 Mybatis-Plus ,套用官方的解释便是成为 MyBatis 最好的搭档,简称基友。它是在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

1. 三大特性

1)润物无声

只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑。

2)效率至上

只需简单配置,即可快速进行单表 CRUD 操作,从而节省大量时间。

3)丰富功能

代码生成、物理分页、性能分析等功能一应俱全。

2. 支持数据库

  • mysqlmariadboracledb2h2hsqlsqlitepostgresqlsqlserverprestoGaussFirebird
  • PhoenixclickhouseSybase ASEOceanBase 、达梦数据库 、虚谷数据库 、人大金仓数据库 、南大通用数据库

3. 框架结构

实话说,以上这些内容只要你打开官网也能看到,那么我们接下来就先来实际操作一番!

二、MP实战

1. 手摸手式项目练习

1)数据库及表准备

sql 语句:

 use test;
 CREATE TABLE `student`  (
  `id` int(0) NOT NULL AUTO_INCREMENT,
  `dept_id` int(0) NULL DEFAULT NULL,
  `name` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `remark` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
 -- ----------------------------
 -- Records of student
 -- ----------------------------
 INSERT INTO `student` VALUES (1, 1, '小菜', '关注小菜不迷路!');
 INSERT INTO `student` VALUES (2, 2, '小明', '好好学习,天天向上!');

2)pom 依赖

 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
 </dependency>
 <!--lombok-->
 <dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.16.16</version>
 </dependency>
 <!--MP插件-->
 <dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.2.0</version>
 </dependency>
 <!--Mysql-->
 <dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.21</version>
 </dependency>
 <!-- 连接池 -->
 <dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid</artifactId>
  <version>1.2.1</version>
 </dependency>
 <!--JUNIT-->
 <dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.13.1</version>
 </dependency>

3)配置文件

 spring:
  datasource:
  url: jdbc:mysql://localhost:3306/test
  username: root
  password: 123456
  driver-class-name: com.mysql.cj.jdbc.Driver

4)实体类

 @Data
 @Builder
 @TableName("student")
 public class User {
 ​
  @TableId(type = IdType.AUTO)
  private Integer id;
 ​
  private Integer deptId;
 ​
  private String name;
 ​
  private String remark;
 }

5)Mapper

 public interface UserMapper extends BaseMapper<User> {}

6)测试类

 @RunWith(SpringRunner.class)
 @SpringBootTest
 public class MapperTest {
 ​
  @Autowired
  private UserMapper userMapper;
 ​
  @Test
  public void getAll() {
  List<User> users = userMapper.selectList(null);
  users.forEach(System.out::println);
  }
 }
 /** OUTPUT:
 User(id=1, deptId=1, name=小菜, remark=关注小菜不迷路!)
 User(id=2, deptId=1, name=小明, remark=好好学习,天天向上!)
 **/

小菜结:

在以上的结果,我们可以看到已经打印出了数据库中的全部数据(两条)。而并没有看到平时我们需要写的 mapper.xml 文件,只是用到了 usermapper 中的 selectList() 方法,而 UserMapper 继承了 BaseMapper 这个接口,这个接口便是 MybatisPlus 提供给我们的,我们再来看下这个接口给我们提供了哪些方法。

2. CRUD 基操

1)insert

 @Test
 public void insert() {
  //这里使用了 lombok 中的建造者模式构建对象
  User user = User.builder().deptId(1).name("小华").remark("小华爱学习").build();
  int insertFlag = userMapper.insert(user);
  log.info("插入影响行数,{} | 小华的ID: {}", insertFlag, user.getId());
 }
 /** OUTPUT:
 插入影响行数,1 | 小华的ID: 8
 **/

可以看到我们不仅插入了数据,而且还获取到了插入数据的ID,但是值得注意的是这里的 ID 虽然是自增的,但并非是 MP 默认的 ID生成策略,而是我们在实体类中指定的:

MP 中支持的主键生成策略有以下几种:

我们既然已经看到了 @TableId 这个注解,那我们再来关注一个常用注解 @TableField

从注解名上我们就可以看出,@TableId 是用来标记主键 ID 的,而 @TableField 是用来标记其他字段的。

可以看得出来这个注解中存在的值还是比较多的,下面介绍几个常用的值:

  • value

用于解决字段名不一致问题和驼峰命名,比如实体类中属性名为 remark,但是表中的字段名为 describe ,这个时候就可以使用 @TableField(value="describe") 来进行转换。驼峰转换如果在全局中有配置驼峰命名,这个地方可不写。

  • exist

用于在数据表中不存在的字段,我们可以使用 @TableField(exist = false) 来进行标记

  • condition

用在预处理 WHERE 实体条件自定义运算规则,比如我配置了 @TableField(condition = SqlCondition.LIKE),输出 SQL 为:select 表 where name LIKE CONCAT('%',值,'%'),其中 SqlCondition 值如下:

  • update

用在预处理 set 字段自定义注入,比如我配置了 @TableField(update = "%s+1"),其中 %s 会填充字段,输出 SQL 为:update 表名 set 字段 = 字段+1 where 条件

  • select

用于是否查询时约束,如果我们有个字段 remarktext 类型的,查询的时候不想查询该字段,那么就可以使用 @TableField(select = false) 来约束查询的时候不查询该字段

2)update

MybatisPlus 的更新操作存在两种:

 int updateById(Param("et") T entity);
 ​
 int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);
根据 ID 更新
 @Test
 public void update() {
  User user = User.builder().id(3).name("小华").remark("小华爱玩游戏").build();
  userMapper.updateById(user);
 }
 /** 更新结果:
 User(id=3, deptId=1, name=小华, remark=小华爱玩游戏)
 **/
根据条件更新
 @Test
 public void update() {
  UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
  updateWrapper.eq("name","小华").set("remark","小华爱下棋");
  userMapper.update(null, updateWrapper);
 }
 /** 更新结果:
 User(id=3, deptId=1, name=小华, remark=小华爱下棋)
 **/

我们也可以将要更新的条件放进 user 对象 里面:

 @Test
 public void update() {
  UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
  updateWrapper.eq("name","小华");
  User user = User.builder().remark("小华爱游泳").build();
  userMapper.update(user, updateWrapper);
 }
 /** 更新结果:
 User(id=3, deptId=1, name=小华, remark=小华爱游泳)
 **/

3)delete

MybatisPlus 中删除的方式相对于更新多,总共有四种:

 int deleteById(Serializable id);
 ​
 int deleteByMap(@Param("cm") Map<String, Object> columnMap);
 ​
 int delete(@Param("ew") Wrapper<T> wrapper);
 ​
 int deleteBatchIds(@Param("coll") Collection<? extends Serializable> idList);
根据 ID 删除
 @Test
 public void deleteById() {
  userMapper.deleteById(3);
 }
 /** SQL语句:
 DELETE FROM student WHERE id = 3;
 **/
根据 Map 删除
 @Test
 public void deleteByMap() {
  HashMap<String, Object> columnMap = new HashMap<>();
  columnMap.put("name","小华");
  columnMap.put("remark","小华爱游泳");
  userMapper.deleteByMap(columnMap);
 }
 /** SQL语句:
 DELETE FROM student WHRE name = '小华' AND remark = '小华爱游泳';
 **/
根据 Wrapper 删除
 @Test
 public void delete() {
  UpdateWrapper<User> wrapper = new UpdateWrapper<>();
  wrapper.eq("remark","小华爱下棋");
  userMapper.delete(wrapper);
 }
 /** SQL语句:
 DELETE FROM student WHRE remark = '小华爱下棋';
 **/

根据 Wrapper 删除还有另外一种方式,直接将实体类放入 Wrapper 中包装:

 @Test
 public void delete() {
  User user = User.builder().remark("小华爱下棋").build();
  UpdateWrapper<User> wrapper = new UpdateWrapper<>(user);
  userMapper.delete(wrapper);
 }
 /** SQL语句:
 DELETE FROM student WHRE remark = '小华爱下棋';
 **/
根据 ID 批量删除
 @Test
 public void deleteBatchIds() {
  List<Integer> idList = new ArrayList<>();
  idList.add(4);
  idList.add(7);
  userMapper.deleteBatchIds(idList);
 }
 /** SQL语句:
 DELETE FROM student WHERE id In (4,7)
 **/

4)select

查询操作在我们开发中是最经常用到的,也是重中之重。MybatisPlus 中支持查询的方法也比较多,如下:

 T selectById(Serializable id);
 ​
 List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
 ​
 List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);
 ​
 T selectOne(@Param("ew") Wrapper<T> queryWrapper);
 ​
 Integer selectCount(@Param("ew") Wrapper<T> queryWrapper);
 ​
 List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);
 ​
 List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);
 ​
 List<Object> selectObjs(@aram("ew") Wrapper<T> queryWrapper);
 ​
 IPage<T> selectPage(IPage<T> page, @Param("ew") Wrapper<T> queryWrapper);
 ​
 IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param("ew") Wrapper<T> queryWrapper);

可以看到总共有 10 个方法,我们接下来一个一个测试

查询所有
 @Test
 public void selectList() {
  List<User> users = userMapper.selectList(null);
  users.forEach(System.out::println);
 }
 /** 
  OUTPUT:
 User(id=1, deptId=1, name=小菜, remark=关注小菜不迷路!)
 User(id=2, deptId=1, name=小明, remark=好好学习,天天向上!)
  SQL语句:
 SELECT id, dept_id, name, remark FROM student;
 **/
查询数量
 @Test
 public void selectCount() {
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  queryWrapper.like("name","小");
  System.out.println(userMapper.selectCount(queryWrapper));
 }
 /** 
  OUTPUT:
 2
  SQL语句:
 SELECT COUNT( 1 ) FROM student WHERE (name LIKE '%小%');
 **/
根据 ID 查询
 @Test
 public void selectById() {
  User user = userMapper.selectById(1);
  System.out.println(user);
 }
 /** 
  OUTPUT:
 User(id=1, deptId=1, name=小菜, remark=关注小菜不迷路!)
  SQL语句:
 SELECT id, dept_id, name, remark FROM student WHERE ID = 1;
 **/
根据 ID 批量查询
 @Test
 public void selectBatchIds() {
  List<User> users = userMapper.selectBatchIds(Arrays.asList(1, 2));
  users.forEach(System.out::println);
 }
 /** 
  OUTPUT:
 User(id=1, deptId=1, name=小菜, remark=关注小菜不迷路!)
 User(id=2, deptId=1, name=小明, remark=好好学习,天天向上!)
  SQL语句:
 SELECT id, dept_id, name, remark FROM student WHERE ID IN (1, 2);
 **/
根据条件查询单条
 @Test
 public void selectOne() {
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  queryWrapper.eq("name","小菜");
  User user = userMapper.selectOne(queryWrapper);
  System.out.println(user);
 }
 /**
  OUTPUT:
 User(id=1, deptId=1, name=小菜, remark=关注小菜不迷路!)
  SQL语句:
  SELECT id, name, dept_id, remark FROM student WHERE (name = '小菜');
 **/
根据条件查询多条

通过 map 传递参数,不是通过 LIKE 查询,而是通过 = 查询

 @Test
 public void selectByMap() {
  HashMap<String, Object> columnMap = new HashMap<>();
  columnMap.put("name","小");
  List<User> users = userMapper.selectByMap(columnMap);
  users.forEach(System.out::println);
 }
 /**
  OUTPUT:
 null
  SQL语句:
 SELECT id, name, dept_id, remark FROM student WHERE name = '小';
 **/

如果我们没有新建实体类进行结果封装,我们还可以用 Map 来接收结果集:

 @Test
 public void selectMaps() {
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  queryWrapper.like("name","小");
  List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
  maps.forEach(System.out::println);
 }
 /**
  OUTPUT:
 {name=小菜, remark=关注小菜不迷路!, id=1, dept_id=1}
 {name=小明, remark=好好学习,天天向上!, id=2, dept_id=1}
  SQL语句:
 SELECT id, name, dept_id, remark FROM student WHERE (name LIKE '%小%');
 **/

也可以用 Object 对象来接收结果集:

 @Test
 public void selectObjs() {
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  queryWrapper.like("name", "小");
  List<Object> objects = userMapper.selectObjs(queryWrapper);
 }
 /**
  OUTPUT:
 {name=小菜, remark=关注小菜不迷路!, id=1, dept_id=1}
 {name=小明, remark=好好学习,天天向上!, id=2, dept_id=1}
  SQL语句:
 SELECT id, name, dept_id, remark FROM student WHERE (name LIKE '%小%');
 **/
分页查询
 @Test
 public void selectPage() {
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  queryWrapper.like("name", "小");
  Page<User> page = new Page<>(1, 1);
  IPage<User> userIPage = userMapper.selectPage(page, queryWrapper);
  System.out.println("数据总数:" + userIPage.getTotal());
  System.out.println("总页数:" + userIPage.getPages());
  System.out.println("当前页:" + userIPage.getCurrent());
  System.out.println("页大小:" + userIPage.getSize());
  userIPage.getRecords().forEach(System.out::println);
 }
 /**
  OUTPUT:
 数据总数:2
 总页数:2
 当前页:1
 页大小:1
 User(id=1, deptId=1, name=小菜, remark=关注小菜不迷路!)
  SQL语句:
  SELECT id, name, dept_id, remark
  FROM student
  WHERE (name LIKE '%小%')
  LIMIT 0,1;
 **/

3. 条件构造器

CRUD 的基本操作中,我们想要通过条件查询都是通过 Wrapper 类进行封装的,上面只是简单的用到 eqlike 操作。事实上这个类十分强大,我们在下面会详细进行介绍。

1)allEq

全部 eq 或个别 isNull

 allEq(Map<R, V> params)
 allEq(Map<R, V> params, boolean null2IsNull)
 allEq(boolean condition, Map<R, V> params, boolean null2IsNull)
  
 allEq(BiPredicate<R, V> filter, Map<R, V> params)
 allEq(BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull)
 allEq(boolean condition, BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull) 

参数说明:

param: key 为数据库字段名,value 为字段值

nullsIsNull:为 true 则在 map 的 value 为 null 时调用 isNull 方法,为 false 是则忽略 value 为 null 时不调用 isNull 方法

filter: 过滤函数,判断是否允许字段传入比对条件中

使用示例:

  • allEq(Map<R, V> params)
 @Test
 public void testAllEq() {
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  Map<String,Object> params = new HashMap<>();
  params.put("name","小菜");
  params.put("dept_id",1);
  params.put("remark",null);
  queryWrapper.allEq(params); //会调用 isNull 方法
  userMapper.selectList(queryWrapper);
 }
 /** 
  结果:
 {}
  SQL语句:
  SELECT id,name,dept_id,remark
  FROM student
  WHERE (name = '小菜' AND dept_id = 1 AND remark IS NULL);
  **/
  • allEq(Map<R, V> params, boolean null2IsNull)
 @Test
 public void testAllEq() {
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  Map<String,Object> params = new HashMap<>();
  params.put("name","小菜");
  params.put("dept_id",1);
  params.put("remark",null);
  queryWrapper.allEq(params, false); //不会调用 isNull 方法
  userMapper.selectList(queryWrapper);
 }
 /** 
  结果:
 User(id=1, deptId=1, name=小菜, remark=关注小菜不迷路!)
  SQL语句:
  SELECT id,name,dept_id,remark
  FROM student
  WHERE (name = '小菜' AND dept_id = 1);
  **/
  • allEq(boolean condition, Map<R, V> params, boolean null2IsNull)
 @Test
 public void testAllEq() {
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  Map<String,Object> params = new HashMap<>();
  params.put("name","小菜");
  params.put("dept_id",1);
  params.put("remark",null);
  queryWrapper.allEq(false,params,false); //不会带入条件进行查询
  userMapper.selectList(queryWrapper);
 }
 /** 
  结果:
 {name=小菜, remark=关注小菜不迷路!, id=1, dept_id=1}
 {name=小明, remark=好好学习,天天向上!, id=2, dept_id=1}
  SQL语句:
  SELECT id,name,dept_id,remark
  FROM student;
  **/
  • allEq(BiPredicate<R, V> filter, Map<R, V> params)
 @Test
 public void testAllEq() {
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  Map<String, Object> params = new HashMap<>();
  params.put("name", "小菜");
  params.put("dept_id", 1);
  params.put("remark", null);
  //只有 key 中含有 “m” 才会用作条件判断
  queryWrapper.allEq((k, v) -> (k.contains("m")), params);
  userMapper.selectList(queryWrapper);
 }
 /** 
  结果:
 0
  SQL语句:
  SELECT id,name,dept_id,remark
  FROM student
  WHERE (name = '小菜' AND remark IS NULL);
  **/

2)比较操作

  • eq: 相当于 =
  • ne: 相当于 !=
  • gt: 相当于 >
  • ge: 相当于>=
  • lt: 相当于 <
  • le: 相当于<=
  • between: 相当于between ... and ...
  • notBetween: 相当于not between ... and ...
  • in: 相当于in(.., .., ..)
  • notIn: 相当于not in(.., .., ..)

3)模糊查询

  • like:like("name","小菜") --> name like "%小菜%"
  • notLike:notLike("name","小菜") --> name not like "%小菜%"
  • likeLeft:like("name","小菜") --> name like "%小菜"
  • likeRight:like("name","小菜") --> name like "小菜%"

4)排序

  • orderBy:
 orderBy(boolean condition, boolean isAsc, R... columns)

orderBy(true, true, "id", "name") --> order by id ASC, name ASC

  • orderByAsc:
`orderByAsc("id","name") --> order by id ASC, name ASC`
  • orderByDesc:
orderByDesc("id","name) --> order by id Desc, name Desc`

5)逻辑查询

  • or:

拼接:主动调用 or 表示紧接着下一个方法不是用 and 连接!(不调用 or 则默认为使用 and 连接), eq("id",1).or().eq("name","老王")

嵌套:or(i -> i.eq("name", "李白").ne("status", "活着"))

  • and:

嵌套:and(i -> i.eq("name", "李白").ne("status", "活着"))

6)select

在MP查询中,默认查询所有的字段,如果有需要也可以通过select方法进行指定字段,如select("id", "name")

4. 配置讲解

1)基本配置

  • configLocation

用于指明 MyBatis 配置文件的位置,如果我们有 MyBatis 的配置文件,需将配置文件的路径配置到 configLocation

SpringBoot:

 mybatis-plus.config-location = classpath:mybatis-config.xml

SpringMvc:

 <bean id="sqlSessionFactory"
 class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
 <property name="configLocation" value="classpath:mybatis-config.xml"/>
 </bean
  • mapperLocations

用于指明 Mapper 所对应的 XML 的文件位置,我们在 通用 CRUD 中用到的 Mapper 是直接继承 MP 提供的 BaseMapper ,我们也可以自定义方法,然后在 XML 文件中自定义 SQL,而这时我们需要告诉 Mapper 所对应 XML 文件的位置

SpringBoot:

 mybatis-plus.mapper-locations = classpath*:mybatis/*.xml

SpringMVC:

 <bean id="sqlSessionFactory"
 class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
 <property name="mapperLocations" value="classpath*:mybatis/*.xml"/>
 </bean>
  • typeAliasesPackage

用于 MyBatis 别名包扫描路径,通过该属性可以给包中的类注册别名,注册后在 Mapper 对应的 XML 文件中可以直接使用类名,而不用使用全限定的类名

SpringBoot:

 mybatis-plus.type-aliases-package = cbuc.life.bean

SpringMVC:

 <bean id="sqlSessionFactory"
 class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
 <property name="typeAliasesPackage"
 value="com.baomidou.mybatisplus.samples.quickstart.entity"/>
 </bean>

2)进阶配置

  • mapUnderScoreToCamelCase

是否开启自动驼峰命名规则映射,这个配置的默认值是 true,但是这个属性在 MyBatis 中的默认值是 false,所以在我们平时的开发中都会将这个配置开启。

 #关闭自动驼峰映射,该参数不能和mybatis-plus.config-location同时存在
 mybatis-plus.configuration.map-underscore-to-camel-case = false

  • cacheEnabled

全局地开启或关闭配置文件中的所有映射器已经配置的任何缓存,默认为 true

 mybatis-plus.configuration.cache-enabled = false

3)DB 策略配置

  • idType

全局默认主键类型,设置后,即可省略实体对象中的@TableId(type = IdType.AUTO)配置。该配置的默认值为 ID_WORKER

SpringBoot:

 mybatis-plus.global-config.db-config.id-type = auto

SpringMVC:

 <bean id="sqlSessionFactory"
 class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
      <property name="dataSource" ref="dataSource"/>
      <property name="globalConfig">
          <bean class="com.baomidou.mybatisplus.core.config.GlobalConfig">
              <property name="dbConfig">
                  <bean         class="com.baomidou.mybatisplus.core.config.GlobalConfig$DbConfig">
                  <property name="idType" value="AUTO"/>
                  </bean>
              </property>
          </bean>
      </property>
 </bean>
  • tablePrefix

表名前缀,全局配置后可省略@TableName()配置。该配置的默认值为 null

SpringBoot:

 mybatis-plus.global-config.db-config.table-prefix = yq_

SpringMVC:

 <bean id="sqlSessionFactory"
 class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
      <property name="dataSource" ref="dataSource"/>
      <property name="globalConfig">
          <bean class="com.baomidou.mybatisplus.core.config.GlobalConfig">
              <property name="dbConfig">
                  <bean            class="com.baomidou.mybatisplus.core.config.GlobalConfig$DbConfig">
                      <property name="idType" value="AUTO"/>
                      <property name="tablePrefix" value="yq_"/>
                  </bean>
              </property>
          </bean>
      </property>
 </bean>

5. 其他扩展

1)自动填充

有时候我们在插入或更新数据的时候,希望有些字段可以自动填充。比如我们平时数据表里面会有个 插入时间 或者 更新时间 这种字段,我们会默认以当前时间填充,在 MP 中我们也可以进行配置。

首先我们需要借助 @TableField(fill = FieldFill.INSERT) 这个注解,在插入时进行填充。

 @TableField(fill = FieldFill.INSERT)
 private String remark;

其中自动填充的模式如下:

 public enum FieldFill {
  /**
  * 默认不处理
  */
  DEFAULT,
  /**
  * 插入时填充字段
  */
  INSERT,
  /**
  * 更新时填充字段
  */
  UPDATE,
  /**
  * 插入和更新时填充字段
  */
  INSERT_UPDATE
 }

然后我们再编写自定义的填充处理模式:

 @Component
 public class MyMetaObjectHandler implements MetaObjectHandler {
  @Override
  public void insertFill(MetaObject metaObject) {
  Object remark = getFieldValByName("remark", metaObject);
  if (null == remark) {
  setFieldValByName("remark", "好好学习", metaObject);
  }
  }
 ​
  @Override
  public void updateFill(MetaObject metaObject) {
  //自定义更新时填充
  }
 }

测试:

 @Test
 public void testObjectHandler() {
  User user = User.builder().deptId(1).name("小明").build();
  userMapper.insert(user);
 }
 /**
  SQL语句:
 INSERT INTO student ( name, dept_id, remark )
 VALUES ( '小明', 1, '好好学习' );
 **/

可以看到插入时,已经自动将我们填充的字段合并进去。

2)逻辑删除

在开发中,很多时候我们删除数据并不需要真正意义上的物理删除,而是使用逻辑删除,这样子查询的时候需要状态条件,确保被标记的数据不被查询到。 MP 当然也支持这样的功能。

我们需要先为 student 表添加一个字段 status 来声明数据是否被删除,0 表示被删除,1表示未删除,然后也需要在实体类上增加这个属性:

 @TableLogic
 private Integer status;

application.yaml 中配置:

 mybatis-plus:
  global-config:
  db-config:
  logic-delete-value: 0
  logic-not-delete-value: 1

测试:

 @Test
 public void testLogicDelete() {
  userMapper.deleteById(1);
 }
 /**
  SQL语句:
 UPDATE student SET status=0
 WHERE id=1 AND status=1;
 **/

可以看出这段 SQL 并没有真正删除,而是进行了逻辑删除,只是更新了删除标识

3)通用枚举

如果有性别之类的字段,我们通常会用 01 来表示,但是查出来我们得进行值转换,这个时候我们就可以使用枚举来解决这个问题:

首先为 student 表添加一个 sex 字段来表示性别,0 表示女性,1 表示男性,然后定义一个枚举类:

 public enum SexEnum implements IEnum<Integer> {
  MAN(1, "男"),
  WOMEN(0, "女");
 ​
  private int code;
 ​
  private String value;
 ​
  SexEnum(int code, String value) {
  this.code = code;
  this.value = value;
  }
 ​
  @Override
  public Integer getValue() {
  return this.code;
  }
  
  //注意要重写此方法,不然会将值转换成 ‘MAN’,而不是 ‘男’
  @Override
  public String toString() {
  return this.value;
  }
 }

然后在实体类中添加对应属性:

 private SexEnum sex;

application.yaml 中配置:

 mybatis-plus:
  type-enums-package: cbuc.life.enums

测试:

 @Test
 public void selectOne() {
  QueryWrapper<User> queryWrapper = new QueryWrapper<>();
  queryWrapper.eq("name", "小菜");
  User user = userMapper.selectOne(queryWrapper);
  System.out.println(user);
 }
 /**
  输出结果:
 User(id=1, deptId=1, name=小菜, remark=关注小菜不迷路!, status=1, sex=男)
  SQL语句:
  SELECT id,sex,name,dept_id,remark,status
  FROM student
  WHERE status=1 AND (name = '小菜');
 **/

END

这篇文章写到这里就告一段落了哦,内容有点长,不过如果能完整看下来,我相信你肯定能够很好的使用 MybatisPlus 啦!路漫漫,小菜与你一同求索!

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。💋

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

查看原文

赞 1 收藏 0 评论 0

蔡不菜丶 发布了文章 · 2020-11-29

精通Java,却不了解泛型?

大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚! 死鬼~看完记得给我来个三连哦!

本文主要介绍 Java中泛型的使用

如有需要,可以参考

如有帮助,不忘 点赞

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

我们上面既然都说到了泛型,那么我们这篇文章就来重新复习一下泛型吧!

一、初识泛型

在没有泛型的出现之前,我们通常是使用类型为 Object 的元素对象。比如我们可以构建一个类型为 Object 的集合,该集合能够存储任意数据类型的对象,但是我们从集合中取出元素的时候我们需要明确的知道存储每个元素的数据类型,这样才能进行元素转换,不然会出现 ClassCastException 异常。

1. 什么是泛型

泛型是在 Java1.5 之后引入的一个新特性,它特性提供了编译时类型安全监测机制,该机制允许我们在编译时监测出非法的类型数据结构。

它的本质就是参数化类型,也就是所操作的数据类型被指定为一个参数。这种参数类型可以用在 类、接口和方法 中,分别被称为 泛型类、泛型接口和泛型方法

2. 使用好处

  • 类型安全

有了泛型的存在,只要编译时没有出现警告,那么运行时就不会出现 ClassCastException 异常

  • 消除强制类型转换

从泛型集合中取出元素我们可以不用进行类型的转换

  • 可读性更高

可以直接看出集合中存放的是什么数据类型的元素

二、泛型的使用

1. 使用场景

1)泛型类

基本语法
 class 类名称 <泛型标识,泛型标识,…> {
  private 泛型标识 变量名; 
  .....
 }
使用示例
 class Result<T>{
  private T data;
 }

注:

  • Java 1.7 之后可以进行类型推断,后面 <> 中的具体的数据类型可以省略不写:

 类名<具体的数据类型> 对象名 = new 类名<>();

  • 如果我们使用的时候没有用到 <> 来制定数据类型,那么操作类型则是 Object
  • 泛型内的类型参数只能是 类型,而不能是基本数据类型,例如int,double,float...
  • 当我们传入不同数据类型进行构造对象时,逻辑上可以看成是多个不同的数据类型,但实际上都是相同类型

以上便是泛型类的简单用法,我们想要使用哪种类型,就在创建的时候指定类型,使用的时候,该类就会自动转换成用户想要使用的类型。

那么如果我们定义了一个泛型类,构造对象的时候却没有声明数据类型,那么默认为 Object 类型,取出数据的时候则需要进行类型转换:

 Result objectRes = new Result("testObejct");
 String str = (String) objectRes.getData();
 System.out.println(str);

规则:

  • 子类也是泛型类,那么子类和父类的泛型类型要一致
 public class ResultChild<T> extends Result<T> {}
  • 子类不是泛型类,那么父类要指定数据类型
 public class ResultChild extends Result<String> {}

2)泛型接口

基本语法
 public 接口名称 <泛型标识, 泛型标识, ...>{
  泛型标识 方法名();
  ...
 }
使用示例
 public interface ResultInterface<T> {
  T getData();
 }

泛型接口与泛型类一样,有以下规则:

  • 实现类不是泛型类,接口要明确数据类型
  • 实现类也是泛型类,实现类和接口的泛型类型要一致

3)泛型方法

Java 中,泛型类和泛型接口的定义相对比较简单,但是 泛型方法 就比较复杂。

  • 泛型类,是在实例化类的时候指明泛型的具体类型
  • 泛型方法,是在调用方法的时候指明泛型的具体类型
基本语法

 修饰符 <T, E, ...> 返回值类型 方法名(形参列表){}

  • 修饰符与返回值类型之间的 <T> 用于声明此方法为泛型方法
  • 只有声明了 <T> 的方法才是泛型方法,就算返回值类型中的泛型类使用泛型的成员方法也并不是泛型方法
  • <T> 表明该方法将使用泛型类型 T,此时才可以在方法中使用泛型类型 T
使用示例
 private <T> Result<T> getResult(T data) {
  return new Result<T>(data);
 }

泛型方法与可变参数:

 private <T> void printData(T... data) {
  for (T t : data) {
  System.out.println(t);
  }
 }

注:

  • 泛型方法能使方法独立于类而产生变化
  • 如果 静态(static) 方法 要使用泛型能力,就必须使其成为泛型方法

2. 类型通配符

1)什么是类型通配符

  • 类型通配符一般使用 " ? " 代替具体的实参类型
  • 类型通配符是 实参类型 ,而不是 形参类型

类型通配符又分为 类型通配符的上限类型通配符的下限

2)基本语法

类型通配符的上限

 类/接口<? extends 实参类型>

注: 要求该泛型的类型,只能是实参类型,或实参类型的 子类 类型

类型通配符的下限:

 类/接口<? super 实参类型>

注:要求该泛型的类型,只能是实参类型,或实参类型的 父类 类型

2)使用示例

类型通配符的上限

如果我们要打印一个 List 的值,我们可能会这么做:

 private void printData(List list) {
  for (int i = 0; i < list.size(); i++) {
  System.out.println(list.get(i));
  }
 }

看上去没啥问题,但是又觉得怪怪的。因为这就跟黑匣子一样,我根本不知道这个 List 里面装的是什么类型的参数。那我们就在传参的时候定义一下类型:

 private void printData(List<Object> list) {
  for (Object o : list) {
  System.out.println(o);
  }
 }

但是这样定义又太广泛了,Object 是所有类型的父类,如果说我想这个方法只能操作数字类型的元素,那我就能用上 类型通配符的上限 来解决这个问题了:

 private void printData(List<? extends Number> numList) {
  for (Number number : numList) {
  System.out.println(number);
  }
 }

但我们需要使用这个方法时候我们就很直观的可以看出来,这个方法传的实参只能是 Number 的子类。

printData(Arrays.asList(1, 2, 3));
printData(Arrays.asList(1L, 2L, 3L));

类型通配符的下限

上面我们了解到 类型通配符上限的使用 ,那么 类型通配符上限 是如何使用的?

类型通配符上限 在我们平时开发中使用的频率也相对较少。编译器只知道集合元素是下限的父类型,但具体是哪一种父类型是不确定的。因此,从集合中取元素只能被当成Object类型处理(编译器无法确定取出的到底是哪个父类的对象)。

我们可以自定义一个复制集合的函数:

  • 首先定义两个类:
public class Animal {
}
public class Pig extends Animal{
}
  • 定义一个复制函数:
private static <T> Collection<? super T> copy(Collection<? super T> parent, Collection<T> child) {
    for (T t : child) {
        parent.add(t);
    }
    return parent;
}
  • 使用:
List<Animal> animals = new ArrayList<>();
List<Pig> pigs = new ArrayList<>();
pigs.add(new Pig());
pigs.add(new Pig());
copy(animals,pigs);
System.out.println(animals);

3. 类型擦除

因为泛型信息只存在于代码编译阶段,所以在进入 JVM 之前,会把与泛型相关的信息擦除,这就称为 类型擦除

1)无限制类型擦除

类型擦除前:

public class Result<T>{
    private T data;
}

类型擦除后:

public class Result{
    private Object data;
}

2)有限制类型擦除

类型擦除前:

public class Result<T extends Number>{
    private T data;
}

类型擦除后:

public class Result{
    private Number data;
}

3)擦除方法中类型定义的参数

类型擦除前:

private <T extends Number> T getValue(T value){
    return value;
}

类型擦除后:

private Number getValue(Number value){
    return value;
}

END

泛型的介绍就告一段落啦,这篇文章中我们介绍了泛型的使用场景,类型通配符和类型擦除。内容不多,但也要好好看哦!路漫漫,小菜与你一同求索!

看完不赞,都是坏蛋

今天的你多努力一点,明天的你就能少说一句求人的话!

我是小菜,一个和你一起学习的男人。💋

微信公众号已开启,小菜良记,没关注的同学们记得关注哦!

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 52 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-06-19
个人主页被 2.7k 人浏览