2024年2月

一、前言

为了挑战一下OpenCV的学习成果,最经一直在找各类项目进行实践。机缘巧合之下,得到了以下的需求:

要求从以下图片中找出所有的近似矩形的点并计数,重叠点需要拆分单独计数。

二、解题思路

1.图片作二值化处理

auto image = cv::imread("points.jpg");
cv::Mat border;
// 为了将图片边缘的点加入计算,将图片整体扩大十个单位
cv::copyMakeBorder(image, border, 10, 10, 10, 10, cv::BORDER_CONSTANT, cv::Scalar(255, 255, 255));

// 灰度化
cv::Mat gray;
cv::cvtColor(border, gray, cv::COLOR_BGR2GRAY);
//二值化
cv::Mat binary;
cv::threshold(gray, binary, 10, 255, cv::THRESH_BINARY);

得到以下结果:

2.腐蚀

可以看到二值化后多数点的形状残缺。会对轮廓查找造成影响,主要就是锯齿和空洞。为了解决这个问题,可以采用腐蚀方法,得到边缘较为光滑的点。

cv::Mat erosion;
cv::Mat kernel = cv::Mat(3, 3, CV_8UC1, cv::Scalar(1));
cv::erode(binary, erosion, kernel, cv::Point(-1, -1), 1) ; // 腐蚀

3.提取所有点的轮廓并计数

std::vector<cv::Mat>contours;
cv::findContours(erosion, contours, cv::RETR_TREE,cv::CHAIN_APPROX_SIMPLE) ;// 轮廓提取

用黄色笔迹绘制出所有的轮廓,如下图所示:

至此,基本能够得到点的数量了,前提是没有重叠点,直接获取
contours
的数量即可。当然重叠点的问题肯定要解决。接下来采用面积判断法解决该问题。

重叠部分探索

从图中我们可以看出,大部分独立点的面积是近似的,重叠点处的点数基本可以近似于重叠处的面积除以标准独立点的面积。此外,采用面积法也可以过滤一些面积较小的噪点。

1.标准独立点面积的获取

如何找到那个较为标准的点的面积呢?可以使用众数作为标准值。但由于点面积的离散性,不一定能够找到一个完美的众数。所以这里写了一个分组找中位数的方法来找这个标准值。

首先,计算出各个轮廓的面积,并过滤噪点,此处小于30的将视为噪点:

std::vector<double> listArea;
std::vector<cv::Mat> listContour;
std::vector<double> listLength;
auto count = contours.size();
for(int i = 1; i < count; i++)
{
    auto contour = contours[i];
    auto area = cv::contourArea(contour);
    if (area < 30)
        continue;
    listArea.push_back(area);
    listContour.push_back(contour);
    auto arcLen = cv::arcLength(contour, true);
    listLength.push_back(arcLen);
}

其次,将所有面积分组,分组个数等于总个数的平方根:

int groupByStep(const nc::NdArray<double> &nclt, QMap<double, QList<double>> &map)
{
    auto minV = nc::min(nclt)[0];
    auto maxV = nc::max(nclt)[0];
    auto range = maxV - minV;//极差
    auto count = nclt.size();
    auto group = sqrt(count);//分组数量
    int step =  range / group;//分组步长

    for(size_t i = 0; i < count;  i++)
    {
        auto v = nclt[i];
        auto key = (int)((v - minV) / step);
        map[key].push_back(v);
    }
    return group;
}

接着,找到数量最大的组的索引:

int findMaxCountIdx(QMap<double, QList<double>> &map)
{
    int maxIdx = 0;
    auto maxCount = 0;
    auto ks = map.keys();
    auto count = ks.size();
    for(int i = 0; i < count;  i++)
    {
        auto k = ks[i];
        auto vs = map.value(k);
        if(maxCount < vs.size())
        {
            maxCount = vs.size();
            maxIdx = i;
        }
        qDebug() << __FUNCTION__ << k << vs.size();
    }
    return maxIdx;
}

再者,以上一步的索引为起始点,逐步探求前后的索引组,直至索引组中点数大于总点数的一半:

/**
 * @brief accumulate 累加获取选中分组中点的总个数
 * @param idxs
 * @param vss
 * @return
 */
int accumulate(QList<int> &idxs,  QList < QList<double >> &vss)
{
    auto c = 0;
    foreach (auto idx, idxs)
    {
        c += vss[idx].size();
    }
    return c;
}
/**
 * @brief findMoreThanHalfIdxs 获取分组点数量和大于总点一半时的索引
 * @param idxs
 * @param count
 * @param vss
 */
void findMoreThanHalfIdxs(QList<int> &idxs, int count, QList < QList<double >> &vss)
{
    QList<int> idxsTmp = idxs;
    auto l = idxs.first() - 1;
    auto r = idxs.last() + 1;
    auto count_2 = count / 2;
    auto lb = false;
    auto rb = false;

    if(l > -1)
    {
        idxsTmp.insert(0, l);
        auto cnt = accumulate(idxsTmp, vss);
        if(cnt > count_2)
        {
            lb = true;
        }
    }
    if(r < count)
    {
        idxsTmp << r;
        auto cnt = accumulate(idxsTmp, vss);
        if(cnt > count_2)
        {
            rb = true;
        }
    }
    if(lb && rb)
    {
        auto lc = vss[l].size();
        auto rc = vss[r].size();
        if(lc > rc)
        {
            idxs.insert(0, l);
        }
        else
        {
            idxs << r;
        }
    }
    else if (lb)
    {
        idxs.insert(0, l);
    }
    else if (rb)
    {
        idxs << r;
    }
    else
    {
        idxs.insert(0, l);
        idxs << r;
        findMoreThanHalfIdxs(idxs, count, vss);
    }
}

最后,从索引组中找到中位数,即是我们要找的标准值:

/**
 * @brief majority 舍弃极值后再求中位数
 * @param lt
 * @return
 */
double majority(std::vector<double> &lt)
{
    if(!lt.size())return 0;
    auto count = lt.size();
    std::vector<double> sortLt = lt;
    std::sort(sortLt.begin(), sortLt.end(), std::less<double>());//排序
    auto nclt = nc::NdArray<double>(sortLt);
    QMap<double, QList<double>>map;//分组容器
    groupByStep(lt, map);
    auto idx = findMaxCountIdx(map);//个数最多的组的索引
    auto vss = map.values();
    QList<int> idxs{idx};
    findMoreThanHalfIdxs(idxs, count, vss);//以idx为起点,搜寻数量过半的索引
    QList<double> idxVs;//数量过半的值
    foreach (auto idx, idxs)
    {
        idxVs << vss[idx];
    }
    std::sort(idxVs.begin(), idxVs.end(), std::less<double>());//排序
    std::vector<double> idxVsStd;
    toStd(idxVs, idxVsStd);
    nclt = nc::NdArray<double>(idxVsStd);
    auto ret = nc::median(nclt)[0];//中位数
    return ret;
}

2.重叠处点个数的获取

在上一步,我们获取了标准面积
auto stdArea = majority(listArea);
。接下来只用作简单的除法运算就可以得到点的个数:

 auto countAlone = 0;
auto countLinked = 0;
count = listArea.size();
for(size_t i = 1; i < count; i++)
{
    auto area = listArea[i];
    auto contour = listContour[i];
    auto c = getRectCount(area, stdArea, contour);
    auto m = cv::moments(contour);
    auto cX = int(m.m10 / m.m00);
    auto cY = int(m.m01 / m.m00);
    cv::putText(border, QString::number(c).toStdString(), cv::Point(cX, cY),
                cv::FONT_HERSHEY_COMPLEX, 0.5, cv::Scalar(0, 100, 255), 1);
    auto rect = cv::minAreaRect(contour);
    if (c == 1)
        countAlone += c;
    else
        countLinked += c;
    cv::drawContours(border, listContour, i, cv::Scalar(0, 255, 255), 1);
    // 获取最小外接矩形的4个顶点坐标(ps: cv2.boxPoints(rect) for OpenCV 3.x)
    cv::Mat box;
    cv::boxPoints(rect, box);
    cv::Mat boxInt;
    box.convertTo(boxInt, CV_32S);
    cv::drawContours(border, std::vector<cv::Mat> {boxInt}, 0, cv::Scalar(255, 0, 0), 1);
}
cv::imshow("contoursImg", border);
cv::waitKey();

在以上的步骤中,我们得筛选出独立点,将他们排除在重叠点的个数计算之外。否则,独立点的个数大概率将被计算为:0.6、1.5、1.2...这样的值,会对最终结果产生极大的影响。独立点采用以下方法判断:

bool isOneRectPoint(double &area, cv::Mat &contour)
{
    auto rect = minAreaRect(contour);//最小外接矩形
    auto areaRect = rect.size.height * rect.size.width;//矩形面积
    auto scale = rect.size.height / rect.size.width;//矩形长宽比。假设矩形是正方形,该值接近1
    scale = std::abs(scale - 1);//为了直观判断,此处减1。假设矩形是正方形,那么该值接近0
    auto scaleArea = area / areaRect;//假设矩形是独立矩形,该值接近1
    scaleArea = std::abs(scaleArea - 1);//为了直观判断,此处减1。假设矩形是独立矩形,该值接近0
    return scale < 0.1 && scaleArea < 0.2;//两个判断都接近0,说明该矩形是个独立矩形
}

至于点个数,采用以下方法简单计算即可:

int getRectCount(double &area, double &stdArea, cv::Mat &contour)
{
    auto isRect = isOneRectPoint(area, contour);
    if (isRect)
        return 1;
    auto c = std::max(1.0, round(area / stdArea));
    return c;
}

三、总结

以上是个人对该问题的一些思考与实践。运用到了一些图像处理、几何、统计等相关的知识,此处既作为记录,也更加希望该方法对您有所帮助。困难点在于重叠处点个数的统计,本文采用面积法计算个数,是一种较为简单的方法。当然,如果有更加巧妙的方法,欢迎探讨交流。

1.问题:

有如下代码:

public class Test {
    static {
        i = 0;// 给变量赋值可以正常编译通过
        System.out.print(i);// 编译器会提示“非法向前引用”(illegal forward reference)
    }
    static int i = 1;
}

这段代码来自于
《深入理解Java虚拟机:JVM高级特性与最佳实践(第三版)》
的第7章。

image

书里没有对
前向引用
的进一步说明,我们自己探究一下。
把这段代码放到
IDEA
中,
System.out.print(i)
直接提示有错误。
image

编译一下看看
image

编译失败,输出的信息是
java:非法前向引用

2.什么是forward reference?

forward reference
可以翻译成
向前引用
或者
前向引用
。百度百科没有收录该词条,在维基百科中有该词条,但是描述很简单。
image

既然是Java编译器报错,那就去查询Java官方资料,在
JLS
(Java语言规范)中找到了该词的说明:
image

References to a field are sometimes restricted, even through the field is in scope. The following rules constrain forward references to a field (where the use textually precedes the field declaration) as well as self-reference (where the field is used in its own initializer).
即使该字段在范围内,对字段的引用有时也会受到限制。以下规则限制对字段的前向引用(其中使用文本在字段声明之前)以及自引用(其中字段在其自己的初始值设定项中使用)。

这一句提到了两个概念,
前向引用

自引用
。在
JLS
中说
前向引用
就是在字段声明之前使用它,再回头看前言的例子中的代码

public class Test {
    static {
        i = 0;
        System.out.print(i);
    }
    static int i = 1;
}

i
在未声明时就在static块中使用了,说明
i = 0;
属于前向引用。
如果注释掉
System.out.print(i);
这一行,程序可以正常编译通过。
将上面的代码稍微改造一下,打印
i
的值,看看是
0
还是
1

public class Test {

    static {
        i = 0;// 给变量赋值可以正常编译通过
    }
    static int i = 1;

    public static void main(String[] args) {
        System.out.println(i);// 输出1
    }
}

i
的值是1,符合预期。
复习一下类初始化的步骤,静态变量(类变量)和静态代码块(static{}块)按照从上到下的顺序执行。
static int i = 1;

i = 0;
后面,所以
i
的值是
1

再来看看
Test
这个类的字节码情况,使用
jclasslib
插件查看很方便。
image

Test类初始化方法<clinit>的字节码
iconst_0 // 把常量0压入操作数栈
putstatic #3 <com/shion/init_code/Test.i : I> // 把栈顶的值0赋值给类变量i i->0
iconst_1 // 把常量1压入操作数栈
putstatic #3 <com/shion/init_code/Test.i : I> // 把栈顶的值1赋值给类变量i i->1
return // 返回void

从字节码看到,类变量
i
确实被赋值了两次,第一次是
0
,第二次是
1
。难道类变量没声明也可以赋值吗?当然不是,答案已经呼之欲出了,我们来看看
Test
这个类的class文件,用
IDEA
查看反编译后的代码。
image

好家伙,原来是Java编译器的功劳。
补充:
Java允许
前向引用
,从JLS的说明上看,不管是
类变量
还是
实例变量
皆可,Java编译器编译时会自动处理。

3.什么情况属于非法的前向引用?

既然知道了前向引用的概念,那什么情况属于非法的前向引用呢?
还是看
JLS
的说明:
image

解释下什么是
简单名称
,就是一个单词或一个字母这种形式的名称,和它相对的就是
限定名称
(以
.
分隔的单词序列,例如java.lang.Object或者System.out)。
JLS给出了一个详细的例子来说明哪些情况属于非法的前向引用:

点击查看代码
class UseBeforeDeclaration {
    static {
        x = 100;
          // ok - assignment
        int y = x + 1;
          // error - read before declaration
        int v = x = 3;
          // ok - x at left hand side of assignment
        int z = UseBeforeDeclaration.x * 2;
          // ok - not accessed via simple name

        Object o = new Object() { 
            void foo() { x++; }
              // ok - occurs in a different class
            { x++; }
              // ok - occurs in a different class
        };
    }

    {
        j = 200;
          // ok - assignment
        j = j + 1;
          // error - right hand side reads before declaration
        int k = j = j + 1;
          // error - illegal forward reference to j
        int n = j = 300;
          // ok - j at left hand side of assignment
        int h = j++;
          // error - read before declaration
        int l = this.j * 3;
          // ok - not accessed via simple name

        Object o = new Object() { 
            void foo(){ j++; }
              // ok - occurs in a different class
            { j = j + 1; }
              // ok - occurs in a different class
        };
    }

    int w = x = 3;
      // ok - x at left hand side of assignment
    int p = x;
      // ok - instance initializers may access static fields

    static int u =
        (new Object() { int bar() { return x; } }).bar();
	    // ok - occurs in a different class

    static int x;

    int m = j = 4;
      // ok - j at left hand side of assignment
    int o =
        (new Object() { int bar() { return j; } }).bar(); 
        // ok - occurs in a different class
    int j;
}

通过查询其他资料,大家总结了一句话:

通过简单名称引用的变量可以出现在左值位置,但不能出现在右值的位置

根据这条规则,再看上面的例子,
int y = x + 1;
这行代码中,
x
出现在了右值的位置。
再回头看问题里面的例子,
System.out.print(i);
这行代码,符合JLS里提到的

The reference appears either in a class variable initializer of C or in a static initializer of C (§8.7);
该引用出现在 C 的类变量初始值设定项(static字段)中或 C 的静态初始值设定项(static代码块)中(第 8.7 节);

4.前向引用的好处?

前向引用在语法上很容易造成误解,特别是刚接触Java编程的新人,那为什么Java还要允许它的存在呢?
以下说明来自:
前向引用 - 为什么这段代码会编译?

前向引用是一种编译技术,允许在当前编译单元中引用其他编译单元中的类型。这种技术可以提高编译速度,并允许在不同的编译单元之间进行更灵活的组织和模块化。
前向引用的优势:

  1. 提高编译速度:通过将类型声明和定义分离,可以减少编译器需要处理的代码量,从而提高编译速度。
  2. 更灵活的组织:前向引用允许在不同的编译单元之间进行更灵活的组织和模块化,这有助于提高代码的可维护性和可读性。
  3. 更好的性能:前向引用可以减少不必要的内存分配和释放,从而提高程序的性能。
    应用场景:
  4. 大型项目:在大型项目中,前向引用可以帮助开发人员更好地组织代码,提高代码的可读性和可维护性。
  5. 模块化开发:在模块化开发中,前向引用可以帮助开发人员将不同的模块分离,从而提高代码的可读性和可维护性。
  6. 多编译单元项目:在多编译单元项目中,前向引用可以帮助开发人员更好地组织代码,提高代码的可读性和可维护性。

5.总结

前向引用
是Java语言层面允许的,Java编译器进行编译时会检查非法的前向引用,其目的是避免循环初始化和其他非正常的初始化行为。

最后再简单提一下什么是循环引用,看一下下面这个例子:

private int i = j;
private int j = i;

如果没有前面说的强制检查,那么这两句代码就会通过编译,但是很容易就能看得出来,
i

j
并没有被真正赋值,因为两个变量都是未初始化的(Java规定所有变量在使用之前必须被初始化),而这个就是最简单的循环引用的例子。

理解前向引用等概念,可能对提高写CRUD代码的水平没有什么帮助,但是能帮助我们更好的理解这门编程语言。

参考链接:
https://stackoverflow.com/questions/14624919/illegal-forward-reference-java-issue
https://www.imooc.com/wenda/detail/557184
https://cloud.tencent.com/developer/information/前向引用 - 为什么这段代码会编译?
https://docs.oracle.com/javase/specs/jls/se14/html/jls-8.html#jls-8.3.3

这是总结的下半部分,上半在这:
https://www.cnblogs.com/DAYceng/p/18037696
可以见到,下半出现了一些Java相关的东西,懂的都懂,唉

Java基础

Java静态类型有哪些

在Java中,静态类型(Static Type)是指在编译时就确定了变量的类型,并且在运行时保持不变的类型。也就是说,每个变量都有一个静态类型,该类型在编译时就已经确定,并且在程序运行期间不能改变。

主要包括以下几种:

1、基本数据类型:byte、short、int、long、float、double、char和boolean

2、引用类型:类(Class)、接口(Interface)、数组(Array)等

Java数据类型的长度?按字节说

常见数据类型的长度(按字节计算):

  • byte:1字节
  • short:2字节
  • int:4字节
  • long:8字节
  • float:4字节
  • double:8字节
  • char:2字节
  • boolean:1字节(但实际上通常不会只占用1字节)

需要注意的是,这些长度是基于大多数Java虚拟机实现的默认情况。

此外,还有一些引用类型的长度也是固定的:

  • 对象引用:4字节(32位系统)或8字节(64位系统)
  • 数组:根据数组元素类型和数组长度来计算

异常处理的常用场景

调试代码的时候,在适当的地方加入异常处理逻辑可以有效捕获并定位代码的问题,防止程序崩溃;

对于用户来说,异常捕获可以在程序发生错误的时候返回提示,使用户更加容易理解和解决问题。

此外,程序发生异常的时候通常来不及处理获取的资源,这有可能会导致更严重的问题,因此异常处理中我们还可以对资源进行释放操作。

string和stringbuffer

在Java中,
String

StringBuffer
都是用于处理字符串的类

简单来说:

  • String
    是一个
    不可变
    的类,创建之后就不能修改。当进行字符串拼接或修改操作时会创建一个新的
    String
    对象,原有的不变
  • StringBuffer
    是一个
    可变
    的类,进行字符串的修改操作不会创建新的对象,因此需要考虑线程安全问题,多线程环境下需要进行同步操作
  • 如果需要频繁进行字符串拼接、修改或者使用在多线程环境下,建议使用
    StringBuffer
  • 如果字符串不需要被修改,或者只进行少量的字符串操作,可以使用
    String
  1. String(字符串):
    • String
      是一个不可变(immutable)的类,一旦创建,其值就不能被修改。
    • 字符串拼接或修改操作会创建新的
      String
      对象,而原始的
      String
      对象保持不变。
    • 由于不可变性,
      String
      在多线程环境下是线程安全的。
    • String
      提供了丰富的方法用于字符串操作,如获取子串、拼接、替换等。
    • 由于不可变性,频繁的字符串拼接操作会导致内存开销和性能问题。
  2. StringBuffer(字符串缓冲区):
    • StringBuffer
      是一个可变(mutable)的类,可以进行字符串的修改和拼接操作。
    • 字符串的修改操作不会创建新的对象,而是直接在原始的
      StringBuffer
      对象上进行修改。
    • 由于可变性,
      StringBuffer
      在多线程环境下使用时需要进行同步操作,否则可能出现线程安全问题。
    • StringBuffer
      提供了丰富的方法用于字符串的增删改查操作,如追加、插入、删除、替换等。
    • 由于可变性,适合频繁进行字符串拼接和修改的场景,避免了频繁创建新对象的开销。

hashmap和hashtable的区别,扩容机制

HashMap和Hashtable都是Java中的映射容器,用于存储键值对。
【防盗链提醒:爬虫是吧?原贴在:
https://www.cnblogs.com/DAYceng】
主要区别如下:

  1. 线程安全性:
    Hashtable是线程安全
    的,而HashMap则不是。如果需要在多线程环境下使用HashMap,可以使用ConcurrentHashMap。
  2. 空键/值:
    Hashtable不允许空键或空值
    ,而
    HashMap
    则允许
    一个空键和多个空值
  3. 继承关系:Hashtable是Dictionary类的子类,而HashMap是AbstractMap类的子类。
  4. 性能:由于
    Hashtable是线程安全的
    ,因此
    在单线程环境下,HashMap的性能通常比Hashtable更好

关于扩容机制,

当HashMap或Hashtable中的元素数量
超过了其容量的75%时,就会进行扩容操作

HashMap
的扩容机制是
将容量增加到原来的两倍
,并将所有元素重新分配到新的桶中。

Hashtable
的扩容机制是
将容量增加到原来的两倍加一
,并将所有元素重新分配到新的桶中。

在扩容期间,HashMap或Hashtable可能会暂停对外部的操作,以便将元素重新分配到新的桶中。这可能会导致一些操作的延迟和性能下降。因此,在设计应用程序时,需要考虑到HashMap或Hashtable的扩容机制对性能的影响。

Java文件编译过程

Java文件的编译过程可以分为几个主要步骤。

首先,
编译器会对源代码进行词法分析和语法分析
。词法分析将源代码分解为标记,例如关键字、标识符和运算符等。语法分析阶段会检查这些标记是否按照正确的语法结构组织。

接下来,
编译器会生成一个抽象语法树(AST)
,它以源代码的语法结构为基础,用于表示程序的抽象语法结构。AST可以帮助编译器理解源代码的结构和含义。

在语义分析阶段,编译器会对AST进行进一步的分析,检查语义错误和类型匹配等问题。如果发现错误,编译器会生成相应的错误信息。同时,编译器还会生成中间代码,例如Java字节码。

然后,
编译器将中间代码转换为字节码
,字节码是一种与特定平台无关的二进制格式,可以在Java虚拟机(JVM)上运行。此外,编译器还会进行一些优化操作,以提升程序的性能和效率。

最后,
生成的字节码可以被Java虚拟机加载和执行
。虚拟机会负责将字节码解释或编译成特定平台的机器码,并执行程序的逻辑。

JVM

Jvm工作流程

JVM是Java程序的运行环境,它
负责将Java字节码转换为特定平台的机器码,并执行程序的逻辑

首先,JVM会加载字节码文件
。字节码文件通常是由Java编译器生成的,其中包含了程序的指令和数据。

JVM会逐行读取字节码文件,并将其转换为内部表示形式。

接下来,JVM会对字节码进行验证。

验证过程主要包括三个方面:文件格式验证、元数据验证和字节码验证。

验证通过后,JVM会将字节码解释或编译成机器码。

解释执行是逐条解释字节码指令并执行相应操作,而编译执行是将字节码转换为本地机器码,以提高执行效率。JVM通常会使用即时编译器(Just-In-Time Compiler,JIT)来进行动态编译,将热点代码(经常执行的代码)编译成机器码。

在执行过程中,JVM会提供内存管理和垃圾回收功能。JVM会将内存划分为不同的区域,如堆、栈和方法区。

栈用于存储方法调用和局部变量,方法区用于存储类信息和静态变量。

谈谈什么是反射

反射(Reflection)是Java语言的一个特性,它允许程序在运行时动态地获取类的信息并操作类或对象。

有点类似于c++中的运行时类型信息这么一个特性

通过反射,我们可以获取到类的信息,还可以通过newInstance()方法动态的创建对象并调用类方法

抽象类和接口的区别

抽象类和接口是面向对象编程中的两个重要概念。首先,抽象类是一个类,可以包含抽象方法和非抽象方法,而接口是一种完全抽象的类,只包含抽象方法和常量的定义。

抽象类可以被继承,一个子类只能继承一个抽象类。子类需要实现抽象类中的抽象方法,并可以覆盖非抽象方法。而接口可以被实现,一个类可以实现多个接口。实现接口的类需要提供接口中定义的所有方法的具体实现。

抽象类可以有构造函数,用于初始化抽象类的成员变量和执行其他必要的操作。但是接口不能有构造函数,因为接口只是一个行为和结构的定义,没有具体的实例化对象。

==和equal有什么区别?

在Java中,使用"=="和"equals"方法来比较两个String对象是有区别的。

"
"用于比较两个对象的引用是否相等,也就是两个对象是否指向同一块内存地址。如果两个String对象的引用相同,则"
"返回true;否则返回false。例如:

String str1 = "hello";
String str2 = "hello";

System.out.println(str1 == str2); // true

在这个例子中,str1和str2都是指向相同的字符串常量池中的"hello"对象,因此"=="运算符返回true。

然而,在下面的例子中,尽管s1和s2的内容相同,但它们不是指向同一个对象,因此"=="返回false:

String s1 = new String("hello");
String s2 = new String("hello");

System.out.println(s1 == s2); // false

"equals"方法用于比较两个对象的内容是否相等。当两个String对象的内容相同时,"equals"方法返回true。例如:

String s1 = new String("hello");
String s2 = new String("hello");

System.out.println(s1.equals(s2)); // true

总之,"=="用于比较两个对象的引用是否相等,而"equals"方法用于比较两个对象的内容是否相等。在比较两个字符串时,通常应该使用"equals"方法。

有了解什么中间件吗?

什么是中间件

中间件是指在客户端和服务器端之间,或者在不同的应用程序之间起到连接、沟通、协调作用的软件。它可以协助多个应用程序之间进行数据传输、消息传递、负载均衡、流程控制等操作,提高服务器的可用性、性能、可伸缩性和安全性。

中间件根据功能和设计理念可以分为多种类型,包括消息队列中间件、缓存中间件、Web应用服务器、集群中间件、RPC中间件等。每种中间件都有着特定的应用场景和功能特点,开发人员需要根据实际需求选择合适的中间件来解决问题。

举例来说,消息队列中间件如ActiveMQ、RabbitMQ可以支持异步通信、解耦系统;缓存中间件如Memcached、Redis可以提高数据读取性能;Web应用服务器如Apache、Nginx可以解决Web应用程序之间的负载均衡、静态和动态内容分离等问题;而集群中间件如HAProxy、Keepalived可以提高服务器的可用性和性能。

在实际应用中,中间件的使用需要结合具体业务需求和技术特点来选择,并且要了解中间件的正确使用方法和注意事项,以确保其有效运行。中间件的合理使用可以提高查询效率、降低IO负载,增加服务器性能和可靠性,促进数据共享和流程协同,但同时也要注意避免过度使用,以免造成不必要的负担和浪费。

Kafka

Redis

RabbitMQ

spring

什么是spring?

一般说的spring是指spring framework,是一种轻量级框架,其中集合了很多的模块以方便开发。

例如:Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现I0C和DI的基础,AOP
组件用来实现面向切面编程。

什么是Bean?

在Spring框架中,
Bean是一个被实例化、组装并通过Spring容器进行管理的对象

这个对象可以是任何Java对象,例如POJO(Plain Old Java Object)、JavaBean、数据访问对象(DAO)、服务类等等。

在Spring框架中,开发者可以通过声明式配置或基于注解的方式来定义Bean。

通过配置文件或注解,可以指定Bean的作用域、依赖关系、初始化和销毁方法等属性。Spring容器会根据这些配置信息在需要的时候自动创建Bean,并将它们注入到其他对象中。

Bean与容器的关系:

  1. Bean是Spring框架中由容器管理的对象
    ,它可以是任何普通的Java对象,由Spring容器负责实例化、装配和管理。
  2. Spring容器负责创建、配置和管理Bean对象,它通过BeanDefinition来描述Bean的配置元数据
    ,包括Bean的类名、属性值、作用域等信息。
  3. 容器根据BeanDefinition来实例化Bean对象
    ,并将它们装配成完整的应用程序,开发者可以通过配置文件或注解来描述Bean的定义和依赖关系。
  4. 容器还提供了对Bean的生命周期管理、依赖注入、AOP等功能,使得开发者可以更加方便地开发和维护应用程序。

Bean的生命周期

ps:对于prototype类型的bean,Spring在创建好交给使用者使用之后,就不在管理其后续的生命周期了

首先是容器启动阶段

BeanDefinitionReader通过XML文件读取到每个Bean的配置信息之后,使用BeanDefinition表示。在Bean实例化之前,Spring框架提供了一个重要接口BeanFactoryPostProcessor用于对Bean进行一些额外处理。

​ BeanFactoryPostProcessor主要有两个常见的使用场景:

​ (1)在容器初始化阶段对Bean的定义进行修改;

​ (2)注册新的Bean定义,在容器实例化Bean之前就可以将新的Bean定义加入到容器中;

假设我们有一个基于 Spring 的应用程序,并且在应用程序中有一些需要加密处理的属性,比如数据库的用户名和密码。这时候,我们希望在应用程序启动的时候自动对这些敏感信息进行加密,而不是在每个 Bean 中手动编写加密逻辑。

编写一个自定义的 BeanFactoryPostProcessor,它可以在容器实例化 Bean 之前拦截 BeanDefinition,并对其中需要加密的属性进行加密处理。

具体的实现可以通过扫描 BeanDefinition 中的属性,判断是否需要加密,如果需要加密,则对属性值进行加密处理,然后更新 BeanDefinition。

接下来到了实例化Bean的阶段

​ 在实例化阶段,容器通过
反射机制
创建 Bean 的实例。这通常是通过调用类的构造方法来实现的。容器为 Bean 的实例设置属性值,包括基本类型、引用类型以及其他 Bean 的引用。

​ 如果 Bean 实现了一些 Aware 接口,容器会通过回调的方式将特定的资源或接口注入到 Bean 中,比如ApplicationContextAware 可以获得容器上下文。

容器还提供了一个扩展点(BeanPostProcessor接口),开发者可以通过实现该接口,在 Bean 初始化前后添加自定义的逻辑。

上述步骤完成后就算创建好一个Bean了,使用完成后还要进行销毁

销毁Bean

Spring容器关闭时,它会对所有的 Bean 进行销毁(当然也可以手动销毁Bean但是不建议)

关闭有以下步骤:

  • 关闭前进行回调,如果 Bean 实现了 DisposableBean 接口,则容器会在执行销毁动作前调用
    destroy()
    方法。


    Spring也提供了拓展接口在 Bean 销毁前后添加自定义的逻辑。

  • 最后,容器会调用 Bean 的
    finalize()
    方法进行销毁操作,释放占用的资源。

流程总结

在 Spring 中,Bean 的生命周期是由 Spring 容器来管理的。

当我们启动 Spring 容器时,它会读取 Bean 的配置信息,将其转化为 BeanDefinition 对象并注册到 BeanDefinitionRegistry 中。在创建 Bean 实例时,Spring 容器会先通过反射或者 CGLIB 等方式创建 Bean 的实例,然后根据 BeanDefinition 中的属性信息对 Bean 进行初始化和装配。在这个过程中,Spring 容器会检测 Bean 是否实现了一些特定的接口,如 BeanFactoryAware、ApplicationContextAware 等,从而将一些必要的资源注入到 Bean 中。

在 Bean 初始化的过程中,Spring 容器还提供了两个拓展点:BeanFactoryPostProcessor 和 BeanPostProcessor。BeanFactoryPostProcessor 允许我们在 Bean 实例化之前修改 BeanDefinition 的信息,而 BeanPostProcessor 则允许我们在 Bean 实例化完成之后对 Bean 进行增强处理。对于 BeanPostProcessor 接口的实现,它主要用于 AOP 的实现。

在 Bean 初始化完成后,如果 Bean 实现了 InitializingBean 接口,则会执行其 afterPropertiesSet() 方法,如果 Bean 在 XML 中配置了 init-method,则会执行对应的自定义初始化方法。在 Spring 容器关闭时,会触发对所有单例 Bean 的销毁操作,包括调用实现了 DisposableBean 接口的 destroy() 方法和 XML 中配置的自定义销毁方法。

总的来说,Spring 容器通过管理 Bean 的生命周期,帮助开发者实现了 Bean 的自动化装配和管理,减少了代码的冗余性和复杂度。同时,通过拓展点的机制,也允许开发者对 Spring 容器进行扩展和定制,以满足更多的业务需求。

如何定义Bean的范围?

在Spring中定义一个时,我们也可以为Bean声明一个范围,它可以通过Bean定义中的scope属性定义。

例如,当Spring每次需要生成一个新的Bean实例时,bean'sscope属性就是原型。

另一方面,当每次需要Spring都必须返回相同的Bean实例时,Bean scope属性必须设置为singleton。

Bean的作用域

  • singleton:唯一Bean实例,Spring中的Bean默认都是单例的。
  • prototype: 每次请求都会创建一个新的Bean实例。
  • request:每一次HTTP请求都会产生一个新的Bean,该bean仅在当前HTTP request内有效
  • session:每一次HTTP请求都会产生一个新的Bean,该bean仅在当前HTTP session内有效。
  • global-session: 全局session作用域,仅仅在基于Portlet的Web应用中才有意义,Spring5中已经没有了。Portlet是能够生成语义代码 (例如HTML) 片段的小型Java Web插件。它们基于Portlet容器,可以像Servlet一样处理HTTP请求。但是与Servlet不同,每个Portlet都有不同的会话。

将一个类声明为Spring的bean的注解有哪些?

我们一般使用@Autowired注解去自动装配bean。而想要把一个类标识为可以用@Autowired注解自动装配的bean,可以采用以下的注解实现:
1.@Component注解。
通用的注解
,可标注任意类为Spring组件。如果一个Bean不知道属于哪一个层,可以使用@Component注解标注

​ 2.@Repository注解。
对应持久层,即DAO层,主要用于数据库相关操作

​ 3.@Service注解。
对应服务层,即Service层
,主要涉及一些复杂的逻辑,需要用到DAO层(注入)。

​ 4.@Controller注解。
对应Spring MVC的控制层,即Controller层
,主要用于接受用户请求并调用Service层的方法返回数据给前端页面。

@Component和@Bean的区别是什么?

@Component 和 @Bean 都是在Spring框架中用来注册Bean的注解,它们的主要区别在于作用对象和作用范围。

@Component注解适用于标识任意类型的Bean
,由Spring自动扫描并注册;
而@Bean注解适用于手动配置Bean的创建过程
,通常用于定制化的Bean创建场景,例如第三方库的Bean、不易改动的类的Bean等。

​ 1.作用对象不同。@Component注解作用于类,而@Bean注解作用于方法。

​ 2.@Component注解通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用@ComponentScan注解定义要扫描的路径)。@Bean注解通常是在标有该注解的方法中定义产生这个bean,告诉Spring这是某个类的实例,当我需要用它的时候还给我。

​ 3.@Bean注解比@Component注解的自定义性更强,而且很多地方只能通过@Bean注解来注册bean。比如当引用第三方库的类需要装配到Spring容器的时候,就只能通过@Bean注解来实现。

什么是IOC?如何实现?

I0C (lnversion OfControll,控制反转)是一种设计思想,就是将原本在程序中手动创建对象的控制权,交给I0C容器来管理,并由IOC容器完成对象的注入。

意思就是将创建对象的控制权从自己硬编码new的一个对象反转到了第三方身上

I0C的主要实现方式是依赖注入

Spring中的依赖注入方式有: 构造方法注入、settter注入、接口注入

目的: 帮助我们接耦各种有依赖关系的业务对象之间的绑定关系

I0C容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。

IOC-Provider

虽然不需要我们自己来做绑定关系,但是这部分的工作还是需要有人来实现的,所以IOC Provider就担任了这个角色,同时IOC Provider的职责也不仅仅这些,其基础职责如下:

1、业务对象的构建管理:

​ IOC中,业务对象不需要关心所依赖的对象如何构建如何获取,这部分任务交由IOC Provider

2、业务对象之间的依赖绑定:

​ 通过结合之前构建和管理的所有业务对象,以及各个业务对象之间可识别的依赖关系,将这些对象所依赖的对象注入绑定。从而保证每个业务对象在使用的时候,可以处于就绪状态。

Spring的IOC容器

担任了IOC Provider的职责,同时在此基础上,还增加了对Bean生命周期的管理、AOP支持内容。

从整体来看Spring的IOC容器的作用,共分为两部分:

1、容器启动阶段:

​ 以某种方式将配置的Bean信息 (XML、注解、Java编码) 加载如整个Spring应用

2、Bean实例化阶段:

​ 将加载的Bean配置信息组装成应用需要的业务对象。在此基础上,还充分运用了这两个阶段不同的特点,都预留了拓展钩子,供我们根据业务场景进行自定义拓展。

一些核心的接口、类

Resource

用于解决IOC容器中的内容从哪里来的问题,也就是 配置文件从哪里读取、配置文件如何读取 的问题

BeanDefinition

用于解决 Bean 的具体定义问题,包括 Bean 的名字是什么、它的类型是什么,它的属性赋予了哪些值或者引用。

也就是 如何在IOC容器中定义一个 Bean,使得IOC容器可以根据这个定义来生成实例 的问题。

BeanFactoy

用于解决IOC容器在 已经获取 Bean 的定义的情况下,如何装配、获取 Bean 实例 的问题。

ApplcationContext

对上述的内容进行了功能的封装,解决 根据地址获取 IOC 容器并使用的问题。

什么是AOP? 有哪些AOP的概念?

AOP (Aspect-Oriented Programming,面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。

可以理解为utils

spring AOP是基于动态代理的
,如果要代理的对象实现了某个接口,那么Spring AOP就会使用IDK动态代理去创建代理对象,而对于没有实现接口的对象,就无法使用IDK动态代理,转而使用CGib动态代理生成一个被代理对象的子类来作为代理。

什么tm的是动态代理?后面讲

AOP包含的几个概念

Jointpoint (连接点)

​ 具体的切面点的抽象概念,可以是在字段、方法上,Spring中具体表现形式是PointCut (切入点),仅作用在方法上

Advice(通知)

​ 在连接点进行的具体操作,如何进行增强处理的,分为前置、后置、异常、最终、环绕五种情况。

目标对象

​ 被AOP框架进行增强处理的对象,也被称为被增强的对象。

AOP代理

​ AOP框架创建的对象,简单的说,代理就是对目标对象的加强。Spring中的AOP代理可以是IDK动态代理,也可以是CGLIB代理。

Weaving(织入)

​ 将增强处理添加到目标对象中,创建一个被增强的对象的过程

【防盗链提醒:爬虫是吧?原贴在:
https://www.cnblogs.com/DAYceng】

总结为一句话就是:

​ 在目标对象 (target object)的某些方法 (jintpoint)添加不同种类的操作(通知、增强操处理),最后通过某些方法 (weaving、织入操作)实现一个新的代理目标对象。

AOP的应用场景

  • 记录日志(调用方法后记录日志)
  • 监控性能(统计方法运行时间)
  • 权限控制(调用方法前校验是否有权限)
  • 事务管理(调用方法前开启事务,调用方法后提交关闭事务)
  • 缓存优化(第一次调用查询数据库,将查询结果放入内存对象,第二次调用,直接从内存对象返回,不需要查数据库)

AOP Advice通知类型有哪些?

拦截器(Interceptor)是在AOP中用于拦截和处理JoinPoint的组件。它可以在JoinPoint的周围执行一系列的操作,比如添加额外的逻辑、修改参数、记录日志等。拦截器可以在目标方法执行前、执行后或抛出异常时进行拦截,并根据需要进行相应的处理。

在Spring AOP中,拦截器被称为Advice(通知),它是实现了特定接口的类或者使用注解进行标记的方法。Spring AOP提供了多种类型的Advice,包括:

  1. 前置通知(Before Advice):在目标方法执行之前执行的通知。可以通过前置通知在目标方法执行前添加额外的逻辑。
  2. 后置通知(After Advice):在目标方法执行之后执行的通知。无论目标方法是否抛出异常,后置通知都会被执行。
  3. 返回通知(After Returning Advice):在目标方法成功执行并返回结果后执行的通知。可以获取目标方法的返回值,并在需要时进行处理。
  4. 异常通知(After Throwing Advice):在目标方法抛出异常后执行的通知。可以捕获目标方法抛出的异常,并根据需要进行处理。
  5. 环绕通知(Around Advice):在目标方法执行前后都执行的通知。可以控制目标方法的执行,包括是否执行目标方法、修改参数、处理返回值等。

在Spring AOP中,这些Advice可以根据需要组合成拦截器链,按照指定的顺序依次执行。拦截器链中的每个拦截器都有机会在JoinPoint周围执行自己的逻辑,从而实现对目标方法的拦截和处理。

AOP的实现方式

主要分类两类:
静态代理

动态代理

静态代理

指使用AOP框架提供的命令进行编译,在编译阶段就可以生成AOP代理类(也叫
编译时增强

动态代理


运行时
在内存中"临时"生成AOP动态代理类(也叫
运行时增强

JDK动态代理

JDK Proxy是Java语言自带的功能,无需通过加载第三方类实现。

JDK Proxy是通过拦截器+反射的方式实现的,只能代理实现接口的类

CGLIB动态代理

CGLIB是第三方工具,基于ASM实现,性能比较好;

ASM(全称为"直接操作字节码的框架",英文为"Bytecode Manipulation Framework")是Java字节码操纵框架,它允许以程序方式动态修改已编译的Java类文件的字节码。ASM提供了一组API和工具,使开发者能够在不加载类文件的情况下直接对字节码进行操作,包括添加、修改、删除字节码指令等。

CGLIB(Code Generation Library)是一个基于ASM的字节码生成库。CGLIB通过继承目标类来创建代理类,因此可以代理没有实现接口的类。

CGLIB无需通过接口来实现,它是针对类实现代理,主要是对指定的类生成一个子类,它是通过实现子类的方式来完成调用的。

对CGLIB的理解

Spring AOP和AspectJ AOP有什么区别?

Spring AOP是属于运行时增强,而Aspect AOP是编译时增强。

Spring AOP基于代理(Proxying),而Aspect AOP基于字节码操作 (Bytecode Manipulation)

Spring AOP已经集成了AspectJ,AspectJ应该算得上是Java生态系统中最完整的AOP框架了。AspectJ相比于Spring AOP功能更加强大,但是Spring AOP相对来说更简单。

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择AspectJ,它比SpringAOP快很多。

SpringMVC

说说SpringMVC

MVC是一种设计模式,而SpringMVC是MVC的一个实现。

Spring MVC下我们⼀般把后端项⽬分为Service层(处理业务)、DAO层(数据库操作)、Entity层(实体类)、Controller层(控制层,返回数据给前台⻚⾯)

image-20231126153154303

SpringMVC的原理

总的来说,整个流程就是客户端发送请求到DispatcherServlet,DispatcherServlet通过HandlerMapping找到对应的Controller,再由HandlerAdapter调用处理器执行业务逻辑,最后经过ViewResolver和View的处理,将渲染后的View返回给客户端。

当客户端(比如浏览器)发送请求时,请求会直接到达DispatcherServlet,它是Spring MVC中的核心控制器。

DispatcherServlet会根据请求信息调用HandlerMapping,这个过程就是为了找到请求对应的Handler,也就是我们平常说的Controller控制器。

一旦找到了对应的Controller,HandlerAdapter就会根据这个Controller来调用真正的处理器来处理请求并执行相应的业务逻辑。

当处理器完成业务逻辑后,会返回一个包含数据对象和逻辑View的ModelAndView对象。其中,Model包含了返回的数据对象,View则是逻辑上的View。

接着,ViewResolver会根据逻辑View去查找实际的View,并将Model传递给View进行渲染。

最后,DispatcherServlet会把渲染后的View返回给请求者,也就是浏览器。

Spring框架中用到了哪些设计模式?

举几个例子

工厂设计模式

​ Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象

代理设计模式

​ SpringAOP功能的实现。

单例设计模式

​ Spring中的bean默认都是单例的。

模板方法模式

​ Spring中的jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式。

包装器设计模式

​ 如果项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。

观察者模式

​ Spring事件驱动模型就是观察者模式很经典的一个应用

适配器模式

​ Spring AOP的增强或通知 (Advice) 使用到了适配器模式、Spring MVC中也是用到了适配器模式适配Controller。

springboot

spring和springboot是什么,区别又是什么?

Spring和Spring Boot是两个常用的Java开发框架,主要用于在开发Java应用过程中简化步骤,提高效率

Spring框架是面向切面编程(AOP)的,提供了很多现成的模块和功能,可以在开发过程中直接使用

Spring Boot则是一个基于Spring框架的快速开发框架,旨在简化Spring应用程序的配置和部署,使得开发者可以更专注于业务逻辑的实现。

使用Spring时,我们需要手动配置和集成各个组件,而使用Spring Boot时,我们可以通过起步依赖和自动配置来简化开发过程。

Spring适用于复杂的应用程序开发,而Spring Boot适用于快速构建简单和微服务应用。

SpringBoot自动配置的原理?

在Spring程序main方法中,添加 @SpringBootApplication 或者 @EnableAutoConfiguration 会自动去maven中读取每个starter中的 spring.factories 文件,该文件里配置了所有需要被创建的 Spring 容器中的Bean

Spring Boot的核心注解是哪些? 主要哪几个注解组成的?

启动类上面的注解是@SpringBootApplication,他也是SpringBoot的核心注解,主要组合包含了以下3个注解:

  • @SpringBootConfiguration: 组合了@Configuration注解,实现配置文件的功能;
  • @EnableAutoConfiguration: 打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置的功能:
  • @SpringBootApplication(exclude={DataSourceAutoConfiguration.class));
  • @ComponentScan: Spring组件扫描

SpringBoot的核心配置文件有哪几个? 他们的区别是什么?

SpringBoot的核心配置文件是:
application

bootstrap
两个配置文件

application配置文件这个容易理解,主要用于Spring Boot项目的自动化配置

bootstrap配置文件有以下几个应用场景:

  • 使用Spring CloudConfig配置中心时,这时需要在bootstrap配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;
  • 一些固定的不能被覆盖的属性;
  • 一些加密/解密的场景;

什么是Spring Boot Starter?有哪些常用的?

Spring Boot Starter是一种用于简化Spring Boot应用程序依赖管理的机制。它们是预配置的依赖项集合,可以通过引入特定的Starter来轻松地添加所需的功能和模块到Spring Boot项目中,从而减少了手动配置的工作量。

spring-boot-starter-web-services - SOAP Web Services

spring-boot-starter-web-用于构建Web应用程序的Starter,包含了Spring MVC、Tomcat等相关依赖。

spring-boot-starter-test-用于编写测试代码的Starter,包含了JUnit、Mockito等测试相关的依赖。

spring-boot-starter-jdbc-传统的JDBC

spring-boot-starter-hateoas-为服务添加 HATEOAS功能

spring-boot-starter-security-使用 SpringSecurity 进行身份验证和授权s

pring-boot-starter-data-jpa-用于支持使用Spring Data JPA进行数据持久化的Starter,包含了JPA相关依赖以及数据库驱动。

spring-boot-starter-data-rest-使用Spring Data REST 公布简单的 REST 服务

至今为止遇到的手撕

单例模式

class Singleton{ //懒汉式
private:
    static Singleton* instance;
    Singelton(){}
public:
	//提供一个获取实例的方法供外界调用
    static Singleton* getInstace(){
        if(instance == null){//判断一下当前是否存在实例
            instance = new Singleton();//在getInstance中创建实例
        }
        return instance;
    }
};

Singleton* Singleton::instance = nullptr;//实例空指针,还没创建,当调用get方法时才真正创建
int main(){
    Singleton* singletonObj = Singleton::getInstance();//创建一个单例类对象并调用getInstance获取一个实例
    return 0
}
class Singleton{ //饿汉式
private://私有成员属性:声明实例、构造函数
    static Singleton* instance;
    Singleton(){}
public://对外暴露:getInstance
    static Singleton* getInstace(){
        return instance;//直接就返回实例
    }
};
Singleton* Singleton::instance = new Singleton();//在初始化静态成员时直接创建一个实例

int main(){
    Singleton* singletonObject = Singleton::getInstance();//
    return 0;
}

删除链表重复节点

#include <iostream>
using namespace std;
struct ListNode{
	int val;
    ListNode* next;
    ListNode(int x): val(int), next(nullptr){}
};

ListNode* deleteNode(ListNode* head){
    ListNode* cur = head;
    while(cur->next){
        if(cur->val = cur->next->val){
            cur->next = cur->next->next;
        }else{
            cur = cur->next;
        }
    }
    return head;
}
int main(){
    ListNode* head = new ListNode(1);
    head->next = new ListNode(2);
    head->next->next = new ListNode(4);
    head->next->next->next = new ListNode(4);
    head->next->next->next->next = new ListNode(5);
    ListNode* res = deleteNode(haed);
    
    while(res){
        cout << res->val << endl;
        res = res->next;
    }  
    return 0;
}

合并两个有序数组

class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        int k = nums1.size() - 1;
        int j = nums2.size() - 1;
        int i = nums1.size() - nums2.size() - 1;
        while(j >= 0){
            if(i < 0 || nums2[j] > nums1[i]){
                swap(nums1[k], nums2[j]);
                j--;
                k--;
            }else{
                swap(nums1[k], nums1[i]);
                i--;
                k--;
            }
        }
    }
};

【防盗链提醒:爬虫是吧?原贴在:
https://www.cnblogs.com/DAYceng】

合并两个无序数组

#include <iostream>
#include <vector>

using namespace std;

vector<int> mergeArrays(vector<int>& arr1, vector<int>& arr2) {
    vector<int> res;
    int i = 0, j = 0;//设置两个指针分别指向两个数组

    while (i < arr1.size() || j < arr2.size()) {//当两个指针都小于各自的数组长度时
        //将小的数不断添加到结果数组中
        if (arr1[i] < arr2[j]) {
            result.push_back(arr1[i]);
            i++;
        } else {
            result.push_back(arr2[j]);
            j++;
        }
    }
	//当一个数组遍历完成,剩下的另一个数组中的元素一定都是比目前结果数组中的元素大的
    //按顺序加入结果数组即可
    while (i < arr1.size()) {
        result.push_back(arr1[i]);
        i++;
    }
    while (j < arr2.size()) {
        result.push_back(arr2[j]);
        j++;
    }

    return res;
}
int main() {
    int n, m;
    cin >> n >> m; // 输入两个数组的长度
    vector<int> arr1(n);
    vector<int> arr2(m);
    for (int i = 0; i < n; i++) cin >> arr1[i]; // 输入第一个数组的元素
    for (int i = 0; i < m; i++) cin >> arr2[i]; // 输入第二个数组的元素

    vector<int> merged = mergeArrays(arr1, arr2);
    for (int num : merged) {
        cout << num << " ";
    }
    return 0;
}

LRU

class LRUCache {
    struct ListNode{//定义节点结构体
        int key;
        int val;
        ListNode* next;
        ListNode* pre;
    };
    ListNode* dummy;
    int maxSize;//最大缓存数量
    int nodeNums;//当前缓存中的节点数量
    //定义哈希表,key是int,val是节点
    unordered_map<int, ListNode*> hash;
    
public:
    LRUCache(int capacity): maxSize(capacity), dummy(new ListNode){//不用参数列表也行
        nodeNums = 0;
        //dummy的 next 和 prev 指针都指向自身,这样当缓存为空时,dummy既是头节点也是尾节点
        dummy->next = dummy;
        dummy->pre = dummy;
    }
    
    int get(int key) {// 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
        if(hash.find(key) != hash.end()){
            //找到对应节点,取出
            ListNode* node = hash[key];
            //将node从当前位置移除
            node->pre->next = node->next;
            node->next->pre = node->pre;
            //把node插到dummy的后面,也就是链表头部
            node->next = dummy->next;
            node->pre = dummy;
            dummy->next->pre = node;//令dummy后面节点的前面节点为node
            dummy->next = node;//令dummy的后面节点为node
            return node->val;      
        }
        return -1;//没找到对应节点返回-1
    }
    //如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 
    void put(int key, int value) {
        //检查是否存在对应键值
        if(hash.find(key) != hash.end()){//存在
            hash[key]->val = value;//键已经存在于哈希表中,那么需要更新这个键对应的节点的值
            get(key);//调用 get(key) 函数,将这个节点移动到链表头部,表示最近访问过它
        }else{//不存在,添加进链表
            if(nodeNums < maxSize){//缓存没满
                nodeNums++;//缓存中当前节点数增加
                //创建新节点
                ListNode* node = new ListNode;
                node->key = key;
                node->val = value;
                //哈希表对应位置进行记录
                hash[key] = node;
                //将新节点插到dummy后面,也就是链表头部
                node->next = dummy->next;
                node->pre = dummy;
                dummy->next->pre = node;
                dummy->next = node;
            }else{//缓存满了,删除此时链表末尾的节点
                //取链表最后一个节点,即dummy的pre指针指向的节点
                ListNode* node = dummy->pre;
                hash.erase(node->key);//在哈希表中删除对应节点
                hash[key] = node;//在哈希表中添加新的键值对,其中 key 是缓存节点的键,node 则是新的节点。
				node->key=key;//更新 node 节点的键值为新的 key。	
				node->val=value;
                get(key);
            }
        }      
    }
};

lc78子集

class Solution {
private:
    vector<int> path;
    vector<vector<int>> res;
    void backtracking(vector<int>& nums, int beginIndex){
        res.push_back(path);
        if(path.size() == nums.size()) return;

        for(int i = beginIndex; i < nums.size(); ++i){
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
        return;
    }
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        backtracking(nums, 0);
        return res;
    }
};

按规则翻转字符串

#include <iostream>
#include <string>
#include <algorithm>

using namespace std;

void reverseWords(string &s) {
    // 翻转整个字符串
    reverse(s.begin(), s.end());

    // 翻转每个单词
    int start = 0;
    for (int end = 0; end < s.length(); end++) {
        if (s[end] == ' ') {
            reverse(s.begin() + start, s.begin() + end);
            start = end + 1;
        }
    }
    reverse(s.begin() + start, s.end());
}

int main() {
    string str = "how are you";
    reverseWords(str);
    cout << str << endl;

    return 0;
}

问题背景

有同事联系我说,在生产环境上,访问不了我负责的common服务,然后我去检查common服务的health endpoint, 没问题,然后我问了下异常,timeout导致的System.OperationCanceledException。那大概率是客户端的问题,会不会是端口耗尽,用netstat看下是不是有大量的端口占用,果然如此,大概如图:

image

什么是端口耗尽

端口耗尽(Port Exhaustion)是指当系统中可用的TCP/IP端口号被全部占用,而无法建立新的网络连接时产生的一种情况。例如,在进行大量的网络连接,如客户端与服务器之间频繁地建立和断开连接时,可能会发生端口耗尽。如果客户端在短时间内打开了大量的短暂连接,并且由于TCP协议的TIME_WAIT状态,这些端口不能立即被重用,就可能出现端口耗尽的现象。此时,新的出站连接可能会失败,因为找不到可用的源端口号。

端口是有限的

每个TCP或UDP连接由一个四元组唯一标识:源IP地址、源端口号、目的IP地址以及目的端口号。在一个给定的源IP地址中,端口号是有限的,通常是从1024到65535(0到1023保留给系统和知名端口)。

拓展阅读,如何查看Linux中的可用端口范围

cat /proc/sys/net/ipv4/ip_local_port_range

什么是TIME_WAIT状态

在TCP/IP网络中,TIME_WAIT状态是TCP连接结束过程的一部分。这是一个正常的状态,发生在一个连接关闭的准备阶段,此时通常是客户端已经发送了一个FIN(结束)数据包来表示它没有更多数据要发送,并且也已经收到了另一侧(通常是服务器)的确认。

下面是TIME_WAIT状态在TCP连接终止过程中的作用:

  • 主动关闭:客户端向服务器发送一个FIN数据包,表示它已经完成数据发送。
  • 服务器接收到FIN,发送一个ACK(确认),并进入CLOSE_WAIT状态,表示它已经确认了客户端结束连接的请求。
  • 服务器在发送完所有剩余的数据后,发送自己的FIN数据包。
  • 客户端接收到服务器的FIN,发送回一个ACK,并进入TIME_WAIT状态。

image

TIME_WAIT状态会持续一个时间段,这个时间是最大报文生命周期(MSL)的两倍,这是一个确保连接相关的所有数据包不再存在于网络中的定义期间。这个时间通常被设置为2分钟,
因此TIME_WAIT状态的持续时间通常是4分钟

TIME_WAIT状态的主要原因有:

  • 确保网络上延迟的数据包被丢弃,避免可能干扰后续新的连接。
  • 允许TCP连接可靠地结束,确保FIN-ACK握手过程正确结束。
  • 确保如果客户端到服务器的最后一个ACK丢失,客户端仍处于TIME_WAIT状态,这样如果服务器因为没有收到ACK而重新发送FIN,客户端可以重新发送ACK。

在高流量的服务器中,TCP连接经常地开启和关闭,TIME_WAIT状态以及它的持续时间可能导致大量的套接字处于这个状态,从而可能耗尽可用的端口。

如何解决

  • 改变TIME_WAIT时间,即减少端口被占用的时间;不推荐,尽量使用默认设置
  • 启用端口重用,例如TCP端口复用(SO_REUSEADDR)选项;
  • 增加可用的端口范围。
  • 保证端口尽早尽快的释放

Redis 高可用(High Availability,HA)是指 Redis 通过一系列技术手段确保在面临故障的情况下也能持续提供服务的能力。

Redis 作为一个内存数据库,其数据通常存储在内存中,一旦发生故障,可能导致数据丢失或服务中断,所以,为了保证 Redis 的高可用,它主要采用了以下两种手段:

  1. 持久化
    :持久化机制能够在一定程度上保证即使在服务器意外停止后,数据还能被恢复。
  2. 多机部署
    :将原本为单机的 Redis 服务,变为多个 Redis 节点,主节点用来处理数据的写操作,然后再把最新的数据同步给从节点,这样即使其中有一个节点宕机了,那么其他节点依然保存了最新的数据,从而避免了 Redis 的单机故障。

但持久化和多机部署又有很多种实现方式,接下来一起来看。

1.持久化

持久化是指将数据从内存中存储到持久化存储介质中(如硬盘)的过程,以便在程序重启或者系统崩溃等情况下,能够从持久化存储介质中恢复数据。
Redis 4.0 之后支持以下 3 种持久化方案:

  1. RDB(Redis DataBase)持久化
    :快照方式持久化,将某一个时刻的内存数据,以二进制的方式写入磁盘;
  2. AOF(Append Only File)持久化
    :文件追加持久化,记录所有非查询操作命令,并以文本的形式追加到文件中;
  3. 混合持久化
    :RDB + AOF 混合方式的持久化,Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。

1.1 RDB 持久化

RDB(Redis Database)是将某一个时刻的内存快照(Snapshot),以二进制的方式写入磁盘的持久化机制。
RDB 持久化机制有以下优缺点:
优点:

  1. 速度快
    :相对于 AOF 持久化方式,RDB 持久化速度更快,因为它只需要在指定的时间间隔内将数据从内存中写入到磁盘上。
  2. 空间占用小
    :RDB 持久化会将数据保存在一个压缩的二进制文件中,因此相对于 AOF 持久化方式,它占用的磁盘空间更小。
  3. 恢复速度快
    :因为 RDB 文件是一个完整的数据库快照,所以在 Redis 重启后,可以非常快速地将数据恢复到内存中。
  4. 可靠性高
    :RDB 持久化方式可以保证数据的可靠性,因为数据会在指定时间间隔内自动写入磁盘,即使 Redis 进程崩溃或者服务器断电,也可以通过加载最近的一次快照文件恢复数据。

缺点:

  1. 数据可能会丢失
    :RDB 持久化方式只能保证数据在指定时间间隔内写入磁盘,因此如果 Redis 进程崩溃或者服务器断电,从最后一次快照保存到崩溃的时间点之间的数据可能会丢失。
  2. 实时性差
    :因为 RDB 持久化是定期执行的,因此从最后一次快照保存到当前时间点之间的数据可能会丢失。如果需要更高的实时性,可以使用 AOF 持久化方式。

所以,RDB 持久化方式适合用于对数据可靠性要求较高,但对实时性要求不高的场景,如 Redis 中的备份和数据恢复等。

1.2 AOF 持久化

AOF(Append Only File)它是将 Redis 每个非查询操作命令都追加记录到文件(appendonly.aof)中的持久化机制。
AOF 持久化机制有以下优缺点:
优点:

  1. 数据不容易丢失
    :AOF 持久化方式会将 Redis 执行的每一个写命令记录到一个文件中,因此即使 Redis 进程崩溃或者服务器断电,也可以通过重放 AOF 文件中的命令来恢复数据。
  2. 实时性好
    :由于 AOF 持久化方式是将每一个写命令记录到文件中,因此它的实时性比 RDB 持久化方式更好。
  3. 数据可读性强
    :AOF 持久化文件是一个纯文本文件,可以被人类读取和理解,因此可以方便地进行数据备份和恢复操作。

缺点:

  1. 写入性能略低
    :由于 AOF 持久化方式需要将每一个写命令记录到文件中,因此相对于 RDB 持久化方式,它的写入性能略低。
  2. 占用磁盘空间大
    :由于 AOF 持久化方式需要记录每一个写命令,因此相对于 RDB 持久化方式,它占用的磁盘空间更大。
  3. AOF 文件可能会出现损坏
    :由于 AOF 文件是不断地追加写入的,因此如果文件损坏,可能会导致数据无法恢复。

所以,AOF 持久化方式适合用于对数据实时性要求较高,但对数据大小和写入性能要求相对较低的场景,如需要对数据进行实时备份的应用场景。

1.3 混合持久化

Redis 混合持久化是指将 RDB 持久化方式和 AOF 持久化方式结合起来使用,以充分发挥它们的优势,同时避免它们的缺点,它的优缺点如下:
优点
:混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。
缺点

  1. 实现复杂度高
    :混合持久化需要同时维护 RDB 文件和 AOF 文件,因此实现复杂度相对于单独使用 RDB 或 AOF 持久化方式要高。
  2. 可读性差
    :AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差;
  3. 兼容性差
    :如果开启混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了。

所以,Redis 混合持久化方式适合用于,需要兼顾启动速度和减低数据丢失的场景。但需要注意的是,混合持久化的实现复杂度较高、可读性差,只能用于 Redis 4.0 以上版本,因此在选择时需要根据实际情况进行权衡。

2.多机部署

Redis 多机部署主要包含以下 3 种方式:

  1. 主从同步
  2. 哨兵模式
  3. Redis Cluster(Redis 集群)

2.1 主从同步

主从同步 (主从复制) 是 Redis 高可用服务的基石,也是多机运行中最基础的一个。我们把主要存储数据的节点叫做主节点 (master),把其他通过复制主节点数据的副本节点叫做从节点 (slave),如下图所示:
主从同步.png
在 Redis 中一个主节点可以拥有多个从节点,一个从节点也可以是其他服务器的主节点,如下图所示:
主从同步-从从模式.png

2.2 哨兵模式

主从同步存在一个致命的问题,当主节点奔溃之后,需要人工干预才能恢复 Redis 的正常使用。
所以我们需要一个自动的工具——Redis Sentinel (哨兵模式) 来把手动的过程变成自动的,让 Redis 拥有自动容灾恢复 (failover) 的能力。
哨兵模式如下所示:
哨兵模式.png

小贴士:Redis Sentinel  的最小分配单位是一主一从。

2.3 Redis Cluster

Redis Cluster 是 Redis 3.0 版本推出的 Redis 集群方案,它将数据分布在不同的服务区上,以此来降低系统对单主节点的依赖,并且可以大大的提高 Redis 服务的读写性能。
Redis Cluster 架构图如下所示:
image.png
从上图可以看出 Redis 的主从同步只能有一个主节点,而 Redis Cluster 可以拥有无数个主从节点,因此 Redis Cluster 拥有更强大的平行扩展能力,也就是说当 Redis Cluster 拥有两个主从节点时,从理论上来讲 Redis 的性能相比于主从来说性能提升了两倍,并且 Redis Cluster 也有自动容灾恢复的机制。

课后思考

Redis 有了持久化机制之后数据一定不会丢失吗?Redis 持久化策略有哪些?

本文已收录到我的面试小站
www.javacn.site
,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。