【STL算法的统一性思想与实现:函数对象、谓词、函数适配器】


前言

STL算法为我们提供一些统一的算法模型,在这些算法模型中,只提供了一个统一的壳子,具体实现什么样的功能由我们通过函数对象或回调函数来实现。这是一种非常重要的思想,解耦合或统一性思想,如果不理解这个思想,那么学习STL就像背英语单词,将变得毫无意义。下面将通过for_each、transform、count_if、sort四个算法实例来一步步深入理解这种思想。


1. 准备知识

  • **算法:**STL提供的算法主要包含两大类,一类是不改变所操作容器内容的算法比如计数、搜索、比较等算法;另一类是修改所操作容器内容的算法,比如排序、删除等等。使用STL算法需要包含头文件。

  • 函数对象

    函数对象是指重载了函数调用操作符()的类,其功能类似于回调函数,函数对象一般用于STL算法中来自定义回调行为。

    • 一元函数对象:重载的operator()函数只有一个参数;
    • 二元函数对象:重载的operator()函数有两个参数;
  • 谓词

    谓词可以是仿函数(函数对象),也可以是回调函数,它的返回值是bool类型,作为一个判断式。

    • 一元谓词,只有一个参数的谓词;
    • 二元谓词:含有两个参数的谓词;
  • 函数适配器 :有时候算法中的函数对象只接收一元函数对象,但是我们想要实现的功能需要二元函数对象完成,这时我们就可以通过绑定器把一个二元函数对象和一个参数绑定在一起,适配成一元函数对象。使用函数适配器需要包含头文件。

  • **预定义函数对象:**STL标准模板库已经提前预定义好的函数对象,比如greater、less等,使用预定义函数对象需要包含头文件。

2 for_each算法与一元函数对象

本节目标是使用for_each算法实现遍历容器

2.1 搭建测试框架

搭建一个测试框架

cpp 复制代码
#include <iostream>
using namespace std;

#include <vector>
#include <string>
#include <algorithm> //使用算法
#include <functional> //使用预定义函数对象和适配器

int main()
{
	vector<string> v1;
	v1.push_back("hello");
	v1.push_back("C++");
	v1.push_back("STL");
	v1.push_back("!");
	v1.push_back("!");
	v1.push_back("!");

    /*在此添加测试代码*/

	system("pause");
	return 0;
}

2.2 for_each源码分析

首先转到for_each算法源码

cpp 复制代码
_Fn for_each(_InIt _First, _InIt _Last, _Fn _Func) { // perform function for each element [_First, _Last)
		_Adl_verify_range(_First, _Last);
		auto _UFirst = _Get_unwrapped(_First);
		const auto _ULast = _Get_unwrapped(_Last);
		for (; _UFirst != _ULast; ++_UFirst) {
			_Func(*_UFirst); //通过源码可知,for_each 的函数对象是一元函数对象
		}

		return _Func;
	}

根据源码分析可知,for_each算法接收一个一元函数对象,并返回该对象,它会把传入的容器中的元素一个个的放入回调函数 _Func作为函数参数。由此可见,for_each只是提供了一个壳子,这个壳子的功能是把一个容器的所有元素(起始和结束都有我们通过迭代器传入)依次作为参数传给函数对象 _Func,而具体对这些元素做什么操作,由我们自己通过回调函数 _Func实现。这就是算法统一性和解耦合思想的体现。

2.3 根据for_each源码实现一元函数对象

根据源码可以分析出回调函数的接口,一个参数,无返回值(或未用到返回值)。

cpp 复制代码
//一元函数对象
template<typename _MyType>
class _FroEachClass
{
public:
	void operator()(_MyType& t)
	{
		cout << t << " ";
		m_count++;
	}
public:
	_FroEachClass()
	{
		this->m_count = 0;
	}
public:
	int get_count()
	{
		return this->m_count;
	}
private:
	int m_count;
};

template<typename _MyType>
void _FroEachFunc(_MyType& t)
{
	cout << t << " ";
}

回调函数和函数对象(类)的区别已经在另一篇文章中分析过了,函数对象可以有自己的属性和方法,因为类的封装特性,可以把属性方法一并传入作为函数参数,我们在主函数添加如下测试代码

cpp 复制代码
_FroEachClass<string> for_each_obj;
for_each_obj = for_each(v1.begin(), v1.end(), for_each_obj);
cout << "\nvector size: " << for_each_obj.get_count() << endl;
for_each(v1.begin(), v1.end(), _FroEachFunc<string>);
cout << endl;

编译运行

可以看到,使用函数对象和回调函数都能实现遍历容器,使用函数对象可以通过私有属性记录容器中元素个数。

3. count_if算法与一元谓词

本节目标是使用count_if算法计算某个元素的个数

3.1 count_if源码分析

通过VS转到源码功能

cpp 复制代码
template <class _InIt, class _Pr>
_NODISCARD _Iter_diff_t<_InIt> count_if(_InIt _First, _InIt _Last, _Pr _Pred) { // count elements satisfying _Pred
	_Adl_verify_range(_First, _Last);
	auto _UFirst = _Get_unwrapped(_First);
	const auto _ULast = _Get_unwrapped(_Last);
	_Iter_diff_t<_InIt> _Count = 0;
	for (; _UFirst != _ULast; ++_UFirst) {
		if (_Pred(*_UFirst)) { //由此可见_Pred返回的应该是一个bool
			++_Count;
		}
	}

	return _Count;
}

通过源码分析可知,count_if算法提供了这样一个壳子,他会把我们传入的容器迭代器范围内的元素依次传递给函数对象_Pred,并且函数对象_Pred返回一个bool类型,由此可知count_if的参数是一个一元谓词

cpp 复制代码
if (_Pred(*_UFirst)) { //由此可见_Pred返回的应该是一个bool

如果这个返回结果为真,即传递给函数对象_Pred的参数(容器元素)符合条件,就进行一次计数。最终count_if返回这个计数值。

3.2 根据count_if源码实现一元谓词

根据上一小节分析可知,我们要写的谓词接口形式为bool类型返回值,一个参数,代码如下:

cpp 复制代码
template<typename _MyType>
class _CountOfClass
{
public:
	bool operator()(_MyType& t)
	{
		return (t == this->m_data);
	}
public:
	_CountOfClass(_MyType& t)
	{
		this->m_data = t;
	}
private:
	_MyType m_data;
};

template<typename _MyType>
bool _CountOfFunc(_MyType& t1)
{
	string s = "!";
	return (t1 == s);
}

然后再主函数继续添加如下测试代码,我们使用count_if计算容器中"!"出现的次数

cpp 复制代码
string s("!");
int count = count_if(v1.begin(), v1.end(), _CountOfClass<string>(s));
cout << "count : " << count << endl;
count = count_if(v1.begin(), v1.end(), _CountOfFunc<string>); //使用回调函数也可以编译通过
cout << "count : " << count << endl;

编译运行

通过运行结果看到,容器中总共3个叹号。

4. transform算法与二元函数对象

本节目标,使用transform算法实现把两个容器内容相加放入第三个容器。

4.1 transform源码分析

使用VS查看源码

cpp 复制代码
template <class _InIt1, class _InIt2, class _OutIt, class _Fn>
_OutIt transform(const _InIt1 _First1, const _InIt1 _Last1, const _InIt2 _First2, _OutIt _Dest, _Fn _Func) {
		// transform [_First1, _Last1) and [_First2, ...) with _Func
	_Adl_verify_range(_First1, _Last1);
	auto _UFirst1 = _Get_unwrapped(_First1);
	const auto _ULast1 = _Get_unwrapped(_Last1);
	const auto _Count = _Idl_distance<_InIt1>(_UFirst1, _ULast1);
	auto _UFirst2 = _Get_unwrapped_n(_First2, _Count);
	auto _UDest = _Get_unwrapped_n(_Dest, _Count);
	for (; _UFirst1 != _ULast1; ++_UFirst1, (void) ++_UFirst2, ++_UDest) {
		*_UDest = _Func(*_UFirst1, *_UFirst2); //把两个参数_UFirst1和_UFirst2的元素传入_Func返回结果放入_UDest
	}

	_Seek_wrapped(_Dest, _UDest);
	return _Dest;
}

这只是transform算法的一个重载模型之一,首先传入参数是两个容器的输入迭代器 _First1、 _Last1、 _First2,其中第一个容器给出了起始和结束位置的迭代器,用来限定操作的容器范围,输出迭代器 _Dest。通过这条语句可以看到

cpp 复制代码
*_UDest = _Func(*_UFirst1, *_UFirst2); //把两个参数_UFirst1和_UFirst2的元素传入_Func返回结果放入_UDest

transform算法把两个输入迭代器指示的容器元素传入回调函数_Func作为参数,并返回一个元素装入到输出迭代器_Dest所指示的容器位置中。由此分析可知,transform算法提供了这样一个模型,把两个容器中的元素进行操作,操作结果存放到第三个容器中,所以_Func的返回值应该是和容器类型同类型的一个元素,操作的元素个数由第一个容器的迭代器指定,最终transform返回第三个容器的迭代器位置。transform需要的是一个二元函数对象。

4.2 根据transform源码实现函数对象

根据源码分析,回调函数接口应为二元函数对象,函数返回值为一个元素,代码如下:

cpp 复制代码
template<typename _MyType>
class _TransformClass
{
public:
	_MyType operator()(_MyType& t1, _MyType& t2)
	{
		return (t1 + t2);
	}
};

template<typename _MyType>
_MyType _TransformFunc(_MyType& t1, _MyType& t2)
{
	return (t1 + t2);
}

继续在主函数添加测试代码如下:

cpp 复制代码
vector<string> v3(for_each_obj.get_count() + 1);
v3[6] = "end";
transform(v1.begin(), v1.end(), v1.begin(), v3.begin(), _TransformClass<string>());
for_each(v3.begin(), v3.end(), _FroEachClass<string>());
cout << endl;
vector<string>::iterator it = transform(v1.begin(), v1.end(), v1.begin(), v3.begin(), _TransformFunc<string>);
for_each(v3.begin(), v3.end(), _FroEachClass<string>());
cout << endl;
cout << *it << endl; //transform 返回的是: v1 + v2 最后一个元素相加后,后面的那个位置的迭代器

编译运行

通过输出结果看到,已经实现了把容器1和2相加存入第三个容器的功能。我们在源码分析中说到,transform返回容器3的输出迭代器,那么返回的迭代器位置究竟在哪呢,我们在程序进行了测试,也就是这几句程序

cpp 复制代码
vector<string>::iterator it = transform(v1.begin(), v1.end(), v1.begin(), v3.begin(), _TransformFunc<string>);
for_each(v3.begin(), v3.end(), _FroEachClass<string>());
cout << endl;
cout << *it << endl; //transform 返回的是: v1 + v2 最后一个元素相加后,后面的那个位置的迭代器

通过对返回的迭代器指向的内容打印可以得出结论,transform算法返回的是v1.end()减去v1.begin()后面的一个位置,加入容器v1有6个元素,那么transform返回的应该是v3的第7个位置,具体可见程序中的说明。

5. sort算法与二元谓词

本节目标是通过sort算法实现排序功能。

5.1 sort算法源码分析

源码如下:

cpp 复制代码
template <class _RanIt, class _Pr>
void sort(const _RanIt _First, const _RanIt _Last, _Pr _Pred) { // order [_First, _Last), using _Pred
	_Adl_verify_range(_First, _Last);
	const auto _UFirst = _Get_unwrapped(_First);
	const auto _ULast = _Get_unwrapped(_Last);
	_Sort_unchecked(_UFirst, _ULast, _ULast - _UFirst, _Pass_fn(_Pred));
}
cpp 复制代码
template <class _RanIt, class _Pr>
void _Sort_unchecked(_RanIt _First, _RanIt _Last, _Iter_diff_t<_RanIt> _Ideal, _Pr _Pred) {
    // order [_First, _Last), using _Pred
    _Iter_diff_t<_RanIt> _Count;
    while (_ISORT_MAX < (_Count = _Last - _First) && 0 < _Ideal) { // divide and conquer by quicksort
        auto _Mid = _Partition_by_median_guess_unchecked(_First, _Last, _Pred);
        // TRANSITION, VSO#433486
        _Ideal = (_Ideal >> 1) + (_Ideal >> 2); // allow 1.5 log2(N) divisions

        if (_Mid.first - _First < _Last - _Mid.second) { // loop on second half
            _Sort_unchecked(_First, _Mid.first, _Ideal, _Pred);
            _First = _Mid.second;
        } else { // loop on first half
            _Sort_unchecked(_Mid.second, _Last, _Ideal, _Pred);
            _Last = _Mid.first;
        }
    }

    if (_ISORT_MAX < _Count) { // heap sort if too many divisions
        _Make_heap_unchecked(_First, _Last, _Pred);
        _Sort_heap_unchecked(_First, _Last, _Pred);
    } else if (2 <= _Count) {
        _Insertion_sort_unchecked(_First, _Last, _Pred); // small
    }
}

首先分析源码,sort算法实现的是排序功能,我们可以按自己的排序规则进行排序,比如对一个people类按照成员变量年龄进行排序,一个成绩管理系统,按照成绩排序等,另外STL还提供了预定义函数对象供我们使用,比如less、greater等。sort算法接收的是一个二元谓词。

5.2 根据sort源码实现二元谓词

我们实现从大到小排序,代码如下:

cpp 复制代码
//二元谓词
template<typename _MyType>
class _SortClass
{
public:
	bool operator()(_MyType& t1, _MyType& t2)
	{
		return (t1 > t2);
	}
};

template<typename _MyType>
bool _SortFunc(_MyType& t1, _MyType& t2)
{
	return (t1 > t2);
}

在主函数添加如下测试代码:

cpp 复制代码
v4.push_back(1);
sort(v4.begin(), v4.end(), _SortClass<int>());
for_each(v4.begin(), v4.end(), _FroEachClass<int>());
cout << endl;
sort(v4.begin(), v4.end(), _SortFunc<int>);
for_each(v4.begin(), v4.end(), _FroEachClass<int>());
cout << endl;

编译运行

我们还可以使用预定义函数对象实现从大到小排序

cpp 复制代码
sort(v4.begin(), v4.end(), greater<int>()); //使用预定义函数对象
for_each(v4.begin(), v4.end(), _FroEachClass<int>());
cout << endl;

通过自定义的谓词,我们可以实现自己的数据类型的排序,这就是sort算法为我们提供的统一个排序模型。

6. 函数适配器

有时候算法中的函数对象只接收一元函数对象,但是我们想要实现的功能需要二元函数对象完成,这时我们就可以通过绑定器把一个二元函数对象和一个参数绑定在一起,适配成一元函数对象。比如,我们要用count_if实现计算int型容器中大于5的元素的个数,我们可以借用预定义函数对象greater,但是greater是一个二元谓词,我们在第二节分析过count_if源码,count_if接收的是一个一元函数对象,这时我们就可以使用绑定器bind2nd把元素5绑定到greater的第二个参数位置,把他适配为一元函数对象。

在主函数添加如下测试代码:

cpp 复制代码
int n = 5;
int num = count_if(v4.begin(), v4.end(), bind2nd(greater<int>(), n));
//count_if 接收一元函数对象或一元谓词,我们借用二元谓词greater实现计算大于5的元素个数
//这时可以使用绑定器 bind2nd 把预定义函数对象greater和一个参数n适配为一元谓词
cout << "greater " << n << " : " << num << endl;

编译运行

大于5的个数为5,结果正确。

通过这个例子我们可以再一次体会到STL算法为我们提供的统一性,我们使用count_if算法完成了两个自己的算法,一个是计算容器中某元素的个数,一个是计算容器中大于某元素的个数。只要是我们的回到调函数功能和参数模型能匹配算法提供的模型,就可以套用该算法,这就是统一性。

7. 总结:STL提供的统一性思考

算法的统一性延伸至STL的统一性思考

cpp 复制代码
_OutIt transform(const _InIt _First, const _InIt _Last, _OutIt _Dest, _Fn _Func)
_OutIt transform(const _InIt1 _First1, const _InIt1 _Last1, const _InIt2 _First2, _OutIt _Dest, _Fn _Func)
    //_First1 : 容器1的输入迭代器begin()   _Last1 : 容器1的输入迭代器end()
    //_First2 : 容器2的输入迭代器begin()
    //_Dest : 输出迭代器
    //_Func : 函数对象/回调函数
//算法的核心思想:
    //transform提供了一个统一的操作,把容器1和容器2的元素当作输入,把操作结果放到容器3中
    //容器中的元素遍历由迭代器提供
    //容器1和容器2之间的操作由函数对象提供
    //通过算法实现了数据类型和具体算法的分离
    //transform只提供一个把容器1和容器2的元素运算后放入容器3这个统一模型,这个算法是通用的
    //而具体的算法,是加减乘除还是其他运算由函数对象来提供(由我们自己编写)
    //输入输出的数据类型由容器来决定,容器和算法之间通过迭代器连接
    
    //容器实现了数据类型和容器本身的分离,比如我们通过vector可以放入int\string\class等
    //容器提供了统一的一个模型,里面的元素只是元素本身,不用管是什么类型,他就是一个元素
    
    //迭代器提供了遍历容器的统一方法,不管什么容器vector\deque\queue\set\map,也不管
    //容器中装的什么元素int\string\class,都可以使用迭代器进行遍历,迭代器总是指向容器中的一个元素
    //迭代器这种统一的遍历方法为算法的统一性提供了基础
    
    //算法提供了算法本身的统一性,算法本身提供一个模型,具体操作由函数对象\回调函数提供
    //回调函数实现了任务实现与任务调用的分离
    //容器和算法之间由迭代器提连接的桥梁

8. 完整代码

cpp 复制代码
#include <iostream>
using namespace std;

#include <vector>
#include <string>
#include <algorithm> //使用算法
#include <functional> //使用预定义函数对象和适配器

//一元函数对象
template<typename _MyType>
class _FroEachClass
{
public:
	void operator()(_MyType& t)
	{
		cout << t << " ";
		m_count++;
	}
public:
	_FroEachClass()
	{
		this->m_count = 0;
	}
public:
	int get_count()
	{
		return this->m_count;
	}
private:
	int m_count;
};

template<typename _MyType>
void _FroEachFunc(_MyType& t)
{
	cout << t << " ";
}

//一元谓词
template<typename _MyType>
class _CountOfClass
{
public:
	bool operator()(_MyType& t)
	{
		return (t == this->m_data);
	}
public:
	_CountOfClass(_MyType& t)
	{
		this->m_data = t;
	}
private:
	_MyType m_data;
};

template<typename _MyType>
bool _CountOfFunc(_MyType& t1)
{
	string s = "!";
	return (t1 == s);
}

//二元函数对象
template<typename _MyType>
class _TransformClass
{
public:
	_MyType operator()(_MyType& t1, _MyType& t2)
	{
		return (t1 + t2);
	}
};

template<typename _MyType>
_MyType _TransformFunc(_MyType& t1, _MyType& t2)
{
	return (t1 + t2);
}

//二元谓词
template<typename _MyType>
class _SortClass
{
public:
	bool operator()(_MyType& t1, _MyType& t2)
	{
		return (t1 > t2);
	}
};

template<typename _MyType>
bool _SortFunc(_MyType& t1, _MyType& t2)
{
	return (t1 > t2);
}

int main()
{
	vector<string> v1;
	v1.push_back("hello");
	v1.push_back("C++");
	v1.push_back("STL");
	v1.push_back("!");
	v1.push_back("!");
	v1.push_back("!");

	/*
	_Fn for_each(_InIt _First, _InIt _Last, _Fn _Func) { // perform function for each element [_First, _Last)
		_Adl_verify_range(_First, _Last);
		auto _UFirst = _Get_unwrapped(_First);
		const auto _ULast = _Get_unwrapped(_Last);
		for (; _UFirst != _ULast; ++_UFirst) {
			_Func(*_UFirst); //通过源码可知,for_each 的函数对象是一元函数对象
		}

		return _Func;
	}
	*/
	//通过 for_each 算法实现遍历容器  --- 使用一元函数对象
	_FroEachClass<string> for_each_obj;
	for_each_obj = for_each(v1.begin(), v1.end(), for_each_obj);
	cout << "\nvector size: " << for_each_obj.get_count() << endl;
	for_each(v1.begin(), v1.end(), _FroEachFunc<string>);
	cout << endl;
	
	/*
	template <class _InIt, class _Pr>
	_NODISCARD _Iter_diff_t<_InIt> count_if(_InIt _First, _InIt _Last, _Pr _Pred) { // count elements satisfying _Pred
		_Adl_verify_range(_First, _Last);
		auto _UFirst = _Get_unwrapped(_First);
		const auto _ULast = _Get_unwrapped(_Last);
		_Iter_diff_t<_InIt> _Count = 0;
		for (; _UFirst != _ULast; ++_UFirst) {
			if (_Pred(*_UFirst)) { //由此可见_Pred返回的应该是一个bool
				++_Count;
			}
		}

		return _Count;
	}
	*/
	//用count_if计算 某个元素的个数  ---  使用一元谓词
	string s("!");
	int count = count_if(v1.begin(), v1.end(), _CountOfClass<string>(s));
	cout << "count : " << count << endl;
	count = count_if(v1.begin(), v1.end(), _CountOfFunc<string>); //使用回调函数也可以编译通过
	cout << "count : " << count << endl;

	/*
	template <class _InIt1, class _InIt2, class _OutIt, class _Fn>
	_OutIt transform(const _InIt1 _First1, const _InIt1 _Last1, const _InIt2 _First2, _OutIt _Dest, _Fn _Func) {
		// transform [_First1, _Last1) and [_First2, ...) with _Func
		_Adl_verify_range(_First1, _Last1);
		auto _UFirst1 = _Get_unwrapped(_First1);
		const auto _ULast1 = _Get_unwrapped(_Last1);
		const auto _Count = _Idl_distance<_InIt1>(_UFirst1, _ULast1);
		auto _UFirst2 = _Get_unwrapped_n(_First2, _Count);
		auto _UDest = _Get_unwrapped_n(_Dest, _Count);
		for (; _UFirst1 != _ULast1; ++_UFirst1, (void) ++_UFirst2, ++_UDest) {
			*_UDest = _Func(*_UFirst1, *_UFirst2); //把两个参数_UFirst1和_UFirst2的元素传入_Func返回结果放入_UDest
		}

		_Seek_wrapped(_Dest, _UDest);
		return _Dest;
	}
	*/
	//用transform把两个容器内容相加放入第三个容器  ---  使用二元函数对象
	vector<string> v3(for_each_obj.get_count() + 1);
	v3[6] = "end";
	transform(v1.begin(), v1.end(), v1.begin(), v3.begin(), _TransformClass<string>());
	for_each(v3.begin(), v3.end(), _FroEachClass<string>());
	cout << endl;
	vector<string>::iterator it = transform(v1.begin(), v1.end(), v1.begin(), v3.begin(), _TransformFunc<string>);
	for_each(v3.begin(), v3.end(), _FroEachClass<string>());
	cout << endl;
	cout << *it << endl; //transform 返回的是: v1 + v2 最后一个元素相加后,后面的那个位置的迭代器

	/*
	template <class _RanIt, class _Pr>
	void sort(const _RanIt _First, const _RanIt _Last, _Pr _Pred) { // order [_First, _Last), using _Pred
		_Adl_verify_range(_First, _Last);
		const auto _UFirst = _Get_unwrapped(_First);
		const auto _ULast = _Get_unwrapped(_Last);
		_Sort_unchecked(_UFirst, _ULast, _ULast - _UFirst, _Pass_fn(_Pred));
	}
	*/
	//用sort算法实现排序,使用二元谓词
	vector<int> v4;
	for (int i = 0; i < 10; i++)
	{
		//v4.push_back(rand());
		v4.push_back(i + 1);
	}
	v4.push_back(1);
	sort(v4.begin(), v4.end(), _SortClass<int>());
	for_each(v4.begin(), v4.end(), _FroEachClass<int>());
	cout << endl;
	sort(v4.begin(), v4.end(), _SortFunc<int>);
	for_each(v4.begin(), v4.end(), _FroEachClass<int>());
	cout << endl;
	sort(v4.begin(), v4.end(), greater<int>()); //使用预定义函数对象
	for_each(v4.begin(), v4.end(), _FroEachClass<int>());
	cout << endl;

	//使用 count_if 计算大于5的元素的个数
	int n = 5;
	int num = count_if(v4.begin(), v4.end(), bind2nd(greater<int>(), n));
	//count_if 接收一元函数对象或一元谓词,我们借用二元谓词greater实现计算大于5的元素个数
	//这时可以使用绑定器 bind2nd 把预定义函数对象greater和一个参数n适配为一元谓词
	cout << "greater " << n << " : " << num << endl;
	
	system("pause");
	return 0;
}
相关推荐
_UMR_3 小时前
springboot集成Jasypt实现配置文件启动时自动解密-ENC
java·spring boot·后端
程序员小假3 小时前
我们来说说 Cookie、Session、Token、JWT
java·后端
短剑重铸之日4 小时前
《SpringBoot4.0初识》第一篇:前瞻与思想
java·开发语言·后端·spring·springboot4.0
it_czz4 小时前
LangSmith vs LangFlow vs LangGraph Studio 可视化配置方案对比
后端
蓝色王者4 小时前
springboot 2.6.13 整合flowable6.8.1
java·spring boot·后端
花哥码天下5 小时前
apifox登录后设置token到环境变量
java·后端
hashiqimiya6 小时前
springboot事务触发滚动与不滚蛋
java·spring boot·后端
TeamDev6 小时前
基于 Angular UI 的 C# 桌面应用
前端·后端·angular.js
PPPHUANG6 小时前
一次 CompletableFuture 误用,如何耗尽 IO 线程池并拖垮整个系统
java·后端·代码规范