百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

面试 | .NET基础知识快速通关(7)

bigegpt 2024-08-23 11:56 2 浏览


此系列文章为我在2015年发布于博客园的.NET基础拾遗系列,它十分适合初中级.NET开发工程师在面试前进行一个系统的复习,因此我将其搬到头条号分享与你。

本文为第七篇,我们会对.NET的流与序列化相关考点进行基础复习,全文会以Q/A的形式展现,即以面试题的形式来描述。

1 能说说流的概念吗?.NET中有哪些流?

流是一种针对字节流的操作,它类似于内存与文件之间的一个管道。在对一个文件进行处理时,本质上需要经过借助OS提供的API来进行打开文件,读取文件中的字节流,再关闭文件等操作,其中读取文件的过程就可以看作是字节流的一个过程。

常见的流类型包括:文件流、终端操作流以及网络Socket等,在.NET中,System.IO.Stream类型被设计为作为所有流类型的虚基类,所有的常见流类型都继承自System.IO.Stream类型,当我们需要自定义一种流类型时,也应该直接或者间接地继承自Stream类型。下图展示了在.NET中常见的流类型以及它们的类型结构:


从上图中可以发现,Stream类型继承自MarshalByRefObject类型,这保证了流类型可以跨越应用程序域进行交互。所有常用的流类型都继承自System.IO.Stream类型,这保证了流类型的统一性,并且屏蔽了底层的一些复杂操作,使用起来非常方便

下面的代码中展示了如何在.NET中使用FileStream文件流进行简单的文件读写操作:

public class Program
{
    private const int bufferlength = 1024;


    public static void Main(string[] args)
    {
        //创建一个文件,并写入内容
        string filename = @"C:\TestStream.txt";
        string filecontent = GetTestString();


        try
        {
            if (File.Exists(filename))
            {
                File.Delete(filename);
            }


            // 创建文件并写入内容
            using (FileStream fs = new FileStream(filename, FileMode.Create))
            {
                Byte[] bytes = Encoding.UTF8.GetBytes(filecontent);
                fs.Write(bytes, 0, bytes.Length);
            }


            // 读取文件并打印出来
            using (FileStream fs = new FileStream(filename, FileMode.Open))
            {
                Byte[] bytes = new Byte[bufferlength];
                UTF8Encoding encoding = new UTF8Encoding(true);
                while (fs.Read(bytes, 0, bytes.Length) > 0)
                {
                    Console.WriteLine(encoding.GetString(bytes));
                }
            }
            // 循环分批读取打印
            //using (FileStream fs = new FileStream(filename, FileMode.Open, FileAccess.Read))
            //{
            //    Byte[] bytes = new Byte[bufferlength];
            //    int bytesRead;
            //    do
            //    {
            //        bytesRead = fs.Read(bytes, 0, bufferlength);
            //        Console.WriteLine(Encoding.UTF8.GetString(bytes, 0, bytesRead));
            //    } while (bytesRead > 0);
            //}
        }
        catch (IOException ex)
        {
            Console.WriteLine(ex.Message);
        }


        Console.ReadKey();
    }


    // 01.取得测试数据
    static string GetTestString()
    {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < 10; i++)
        {
            builder.Append("我是测试数据\r\n");
            builder.Append("我是长江" + (i + 1) + "号\r\n");
        }
        return builder.ToString();
    }
}

上述代码的执行结果如下图所示:

在实际开发中,我们经常会遇到需要传递一个比较大的文件,或者事先无法得知文件大小(Length属性抛出异常),因此也就不能创建一个尺寸正好合适的Byte[]数组,此时只能分批读取和写入,每次只读取部分字节,直到文件尾。例如我们需要复制G盘中一个大小为4.4MB的mp3文件到C盘中去,假设我们对大小超过2MB的文件都采用分批读取写入机制,可以通过如下代码实现:

public class Program
{
    private const int BufferSize = 10240; // 10 KB
    public static void Main(string[] args)
    {
        string fileName = @"G:\My Musics\BlueMoves.mp3"; // Source 4.4 MB
        string copyName = @"C:\BlueMoves-Copy.mp3"; // Destination 4.4 MB
        using (Stream source = new FileStream(fileName, FileMode.Open, FileAccess.Read))
        {
            using (Stream target = new FileStream(copyName, FileMode.Create, FileAccess.Write))
            {
                byte[] buffer = new byte[BufferSize];
                int bytesRead;
                do
                {
                    // 从源文件中读取指定的10K长度到缓存中
                    bytesRead = source.Read(buffer, 0, BufferSize);
                    // 从缓存中写入已读取到的长度到目标文件中
                    target.Write(buffer, 0, bytesRead);
                } while (bytesRead > 0);
            }
        }
        Console.ReadKey();
    }
}

上述代码中,设置了缓存buffer大小为10K,即每次只读取10K的内容长度到buffer中,通过循环的多次读写和写入完成整个复制操作。

2 你知道如何使用压缩流吗?

由于网络带宽的限制、硬盘内存空间的限制等原因,文件和数据的压缩是我们经常会遇到的一个需求。因此,.NET中提供了对于压缩和解压的支持:GZipStream类型和DeflateStream类型,它们位于System.IO.Compression命名空间下,且都继承于Stream类型(对文件压缩的本质其实是针对字节的操作,也属于一种流的操作),实现了基本一致的功能。

下面的代码展示了GZipStream的使用方法,DeflateStream和GZipStream的使用方法几乎完全一致:

public class Program
{
    // 缓存数组的长度
    private const int bufferSize = 1024;


    public static void Main(string[] args)
    {
        string test = GetTestString();
        byte[] original = Encoding.UTF8.GetBytes(test);
        byte[] compressed = null;
        byte[] decompressed = null;
        Console.WriteLine("数据的原始长度是:{0}", original.LongLength);
        // 1.进行压缩
        // 1.1 压缩进入内存流
        using (MemoryStream target = new MemoryStream())
        {
            using (GZipStream gzs = new GZipStream(target, CompressionMode.Compress, true))
            {
                // 1.2 将数据写入压缩流
                WriteAllBytes(gzs, original, bufferSize);
            }
            compressed = target.ToArray();
            Console.WriteLine("压缩后的数据长度:{0}", compressed.LongLength);
        }
        // 2.进行解压缩
        // 2.1 将解压后的数据写入内存流
        using (MemoryStream source = new MemoryStream(compressed))
        {
            using (GZipStream gzs = new GZipStream(source, CompressionMode.Decompress, true))
            {
                // 2.2 从压缩流中读取所有数据
                decompressed = ReadAllBytes(gzs, bufferSize);
            }
            Console.WriteLine("解压后的数据长度:{0}", decompressed.LongLength);
            Console.WriteLine("解压前后是否相等:{0}", test.Equals(Encoding.UTF8.GetString(decompressed)));
        }
        Console.ReadKey();
    }


    // 01.取得测试数据
    static string GetTestString()
    {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < 10; i++)
        {
            builder.Append("我是测试数据\r\n");
            builder.Append("我是长江" + (i + 1) + "号\r\n");
        }
        return builder.ToString();
    }


    // 02.从一个流总读取所有字节
    static Byte[] ReadAllBytes(Stream stream, int bufferlength)
    {
        Byte[] buffer = new Byte[bufferlength];
        List<Byte> result = new List<Byte>();
        int read;
        while ((read = stream.Read(buffer, 0, bufferlength)) > 0)
        {
            if (read < bufferlength)
            {
                Byte[] temp = new Byte[read];
                Array.Copy(buffer, temp, read);
                result.AddRange(temp);
            }
            else
            {
                result.AddRange(buffer);
            }
        }
        return result.ToArray();
    }


    // 03.把字节写入一个流中
    static void WriteAllBytes(Stream stream, Byte[] data, int bufferlength)
    {
        Byte[] buffer = new Byte[bufferlength];
        for (long i = 0; i < data.LongLength; i += bufferlength)
        {
            int length = bufferlength;
            if (i + bufferlength > data.LongLength)
            {
                length = (int)(data.LongLength - i);
            }
            Array.Copy(data, i, buffer, 0, length);
            stream.Write(buffer, 0, length);
        }
    }
}

上述代码的运行结果如下图所示:

需要注意的是:使用 GZipStream 类压缩大于 4 GB 的文件时将会引发异常。

通过GZipStream的构造方法可以看出,它是一个典型的Decorator装饰者模式的应用,所谓装饰者模式,就是动态地给一个对象添加一些额外的职责。对于增加新功能这个方面,装饰者模式比新增一个之类更为灵活。就拿上面代码中的GZipStream来说,它扩展的是MemoryStream,为Write方法增加了压缩的功能,从而实现了压缩的应用。

扩展:许多资料表明.NET提供的GZipStream和DeflateStream类型的压缩算法并不出色,也不能调整压缩率,有些第三方的组件例如SharpZipLib实现了更高效的压缩和解压算法,我们可以在nuget中为项目添加该组件。

3 能说说Serializable特性的作用吗?

通过上面的流类型可以方便地操作各种字节流,但是如何把现有的实例对象转换为方便传输的字节流,就需要使用序列化技术。对象实例的序列化,是指将实例对象转换为可方便存储、传输和交互的流。在.NET中,通过Serializable特性提供了序列化对象实例的机制,当一个类型被申明为Serializable后,它就能被诸如BinaryFormatter等实现了IFormatter接口的类型进行序列化和反序列化

[Serializable]
public class Person
{
    ......
}

但是,在实际开发中我们会遇到对于一些特殊的不希望被序列化的成员,这时我们可以为某些成员添加NonSerialized特性。例如,有如下代码所示的一个Person类,其中number代表学号,name代表姓名,我们不希望name被序列化,于是可以为name添加NonSerialized特性:

public class Program
{
    public static void Main(string[] args)
    {
        Person obj = new Person(26, "Edison Chou");
        Console.WriteLine("初始状态:");
        Console.WriteLine(obj);


        // 序列化对象
        byte[] data = Serialize(obj);
        // 反序列化对象
        Person newObj = DeSerialize(data);


        Console.WriteLine("经过序列化和反序列化后:");
        Console.WriteLine(newObj);


        Console.ReadKey();
    }


    // 序列化对象
    static byte[] Serialize(Person p)
    {
        // 使用二进制序列化
        IFormatter formatter = new BinaryFormatter();
        using (MemoryStream ms = new MemoryStream())
        {
            formatter.Serialize(ms, p);
            return ms.ToArray();
        }
    }


    // 反序列化对象
    static Person DeSerialize(byte[] data)
    {
        // 使用二进制反序列化
        IFormatter formatter = new BinaryFormatter();
        using (MemoryStream ms = new MemoryStream(data))
        {
            Person p = formatter.Deserialize(ms) as Person;
            return p;
        }
    }
}

上述代码的运行结果如下图所示:

注意:当一个基类使用了Serializable特性后,并不意味着其所有子类都能被序列化。事实上,我们必须为每个子类都添加Serializable特性才能保证其能被正确地序列化。

4 .NET中提供了哪些可进行序列化操作的类型?

我们已经理解了如何把一个类型声明为可序列化的类型,但是万里长征只走了第一步,具体完成序列化和反序列化的操作还需要一个执行这些操作的类型。为了序列化具体实例到某种专用的格式,.NET中提供了三种对象序列格式化类型:BinaryFormatter、SoapFormatter和XmlSerializer。

(1)BinaryFormatter

顾名思义,BinaryFormatter可用于将可序列化的对象序列化成二进制的字节流,在前面Serializable特性的代码示例中已经展示过,这里不再重复展示。

(2)SoapFormatter

SoapFormatter致力于将可序列化的类型序列化成符合SOAP规范的XML文档以供使用。在.NET中,要使用SoapFormatter需要先添加对于SoapFormatter的引用:

using System.Runtime.Serialization.Formatters.Soap;

Note:SOAP是一种位于应用层的网络协议,它基于XML,并且是Web Service的基本协议。

(3)XmlSerializer

XmlSerializer并不仅仅针对那些标记了Serializable特性的类型,更为需要注意的是,Serializable和NonSerialized特性在XmlSerializer类型对象的操作中完全不起作用,取而代之的是XmlIgnore属性。XmlSerializer可以对没有标记Serializable特性的类型对象进行序列化,但是它仍然有一定的限制:

① 使用XmlSerializer序列化的对象必须显示地拥有一个无参数的公共构造方法;

因此,我们需要修改前面代码示例中的Person类,添加一个无参数的公共构造方法:

[Serializable]
public class Person
{
    ......
    public Person()
    {
    }
    ......
}

② XmlSerializer只能序列化公共成员变量;

因此,Person类中的私有成员_number便不能被XmlSerializer进行序列化:

[Serializable]
public class Person
{
    // 私有成员无法被XmlSerializer序列化
    private int _number;
}

(4)综合演示SoapFormatter和XmlSerializer的使用方法:

① 重新改写Person类

[Serializable]
public class Person
{
    // 私有成员无法被XmlSerializer序列化
    private int _number;
    // 使用NonSerialized特性标记此成员不可被BinaryFormatter和SoapFormatter序列化
    [NonSerialized]
    public string _name;
    // 使用XmlIgnore特性标记此成员不可悲XmlSerializer序列化
    [XmlIgnore]
    public string _univeristy;


    public Person()
    {
    }


    public Person(int i, string s, string u)
    {
        this._number = i;
        this._name = s;
        this._univeristy = u;
    }


    public override string ToString()
    {
        string result = string.Format("学号是:{0},姓名是:{1},大学是:{2}", _number, _name, _univeristy);
        return result;
    }
}

② 新增SoapFormatter和XmlSerializer的序列化和反序列化方法

#region 01.SoapFormatter
// 序列化对象-SoapFormatter
static byte[] SoapFormatterSerialize(Person p)
{
    // 使用Soap协议序列化
    IFormatter formatter = new SoapFormatter();
    using (MemoryStream ms = new MemoryStream())
    {
        formatter.Serialize(ms, p);
        return ms.ToArray();
    }
}


// 反序列化对象-SoapFormatter
static Person SoapFormatterDeSerialize(byte[] data)
{
    // 使用Soap协议反序列化
    IFormatter formatter = new SoapFormatter();
    using (MemoryStream ms = new MemoryStream(data))
    {
        Person p = formatter.Deserialize(ms) as Person;
        return p;
    }
} 
#endregion


#region 02.XmlSerializer
// 序列化对象-XmlSerializer
static byte[] XmlSerializerSerialize(Person p)
{
    // 使用XML规范序列化
    XmlSerializer serializer = new XmlSerializer(typeof(Person));
    using (MemoryStream ms = new MemoryStream())
    {
        serializer.Serialize(ms, p);
        return ms.ToArray();
    }
}


// 反序列化对象-XmlSerializer
static Person XmlSerializerDeSerialize(byte[] data)
{
    // 使用XML规范反序列化
    XmlSerializer serializer = new XmlSerializer(typeof(Person));
    using (MemoryStream ms = new MemoryStream(data))
    {
        Person p = serializer.Deserialize(ms) as Person;
        return p;
    }
} 
#endregion

③ 改写Main方法进行测试

static void Main(string[] args)
{
    Person obj = new Person(26, "Edison Chou", "CUIT");
    Console.WriteLine("原始对象为:");
    Console.WriteLine(obj.ToString());


    // 使用SoapFormatter序列化对象
    byte[] data1 = SoapFormatterSerialize(obj);
    Console.WriteLine("SoapFormatter序列化后:");
    Console.WriteLine(Encoding.UTF8.GetString(data1));
    Console.WriteLine();
    // 使用XmlSerializer序列化对象
    byte[] data2 = XmlSerializerSerialize(obj);
    Console.WriteLine("XmlSerializer序列化后:");
    Console.WriteLine(Encoding.UTF8.GetString(data2));


    Console.ReadKey();
}

示例运行结果如下图所示:

5 如何自定义序列化和反序列化?

对于某些类型,序列化和反序列化往往有一些特殊的操作或逻辑检查需求,这时就需要我们能够主动地控制序列化和反序列化的过程。.NET中提供的Serializable特性帮助我们非常快捷地申明了一个可序列化的类型(因此也就缺乏了灵活性),但很多时候由于业务逻辑的要求,我们需要主动地控制序列化和反序列化的过程。因此,.NET提供了ISerializable接口来满足自定义序列化需求。

下面的代码展示了自定义序列化和反序列化的类型模板:

[Serializable]
public class MyObject : ISerializable
{
    protected MyObject(SerializationInfo info, StreamingContext context)
    {
        // 在此构造方法中实现反序列化
    }


    public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        // 在此方法中实现序列化
    }
}

如上代码所示,GetObjectData和特殊构造方法都接收两个参数:SerializationInfo 类型参数的作用类似于一个哈希表,通过key/value对来存储整个对象的内容,而StreamingContext 类型参数则包含了流的当前状态,我们可以根据此参数来判断是否需要序列化和反序列化类型独享。

如果基类实现了ISerializable接口,则派生类需要针对自己的成员实现反序列化构造方法,并且重写基类中的GetObjectData方法。

下面通过一个具体的代码示例,来了解如何在.NET程序中自定义序列化和反序列化的过程:

① 首先我们需要一个需要被序列化和反序列化的类型,该类型有可能被其他类型继承

[Serializable]
public class MyObject : ISerializable
{
    private int _number;
    [NonSerialized]
    private string _name;


    public MyObject(int num, string name)
    {
        this._number = num;
        this._name = name;
    }


    public override string ToString()
    {
        return string.Format("整数是:{0}\r\n字符串是:{1}", _number, _name);
    }


    // 实现自定义的序列化
    protected MyObject(SerializationInfo info, StreamingContext context)
    {
        // 从SerializationInfo对象(类似于一个HashTable)中读取内容
        this._number = info.GetInt32("MyObjectInt");
        this._name = info.GetString("MyObjectString");
    }


    // 实现自定义的反序列化
    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        // 将成员对象写入SerializationInfo对象中
        info.AddValue("MyObjectInt", this._number);
        info.AddValue("MyObjectString", this._name);
    }
}

② 随后编写一个继承自MyObject的子类,并添加一个私有的成员变量。需要注意的是:子类必须负责序列化和反序列化自己添加的成员变量。

[Serializable]
public class MyObjectSon : MyObject
{
    // 自己添加的成员
    private string _sonName;


    public MyObjectSon(int num, string name)
        : base(num, name)
    {
        this._sonName = name;
    }


    public override string ToString()
    {
        return string.Format("{0}\r\n之类字符串是:{1}", base.ToString(), this._sonName);
    }


    // 实现自定义反序列化,只负责子类添加的成员
    protected MyObjectSon(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
        this._sonName = info.GetString("MyObjectSonString");
    }


    // 实现自定义序列化,只负责子类添加的成员
    public override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        base.GetObjectData(info, context);
        info.AddValue("MyObjectSonString", this._sonName);
    }
}

③ 最后编写Main方法,测试自定义的序列化和反序列化

public class Program
{
    public static void Main(string[] args)
    {
        MyObjectSon obj = new MyObjectSon(10086, "Edison Chou");
        Console.WriteLine("初始对象为:");
        Console.WriteLine(obj.ToString());
        // 序列化
        byte[] data = Serialize(obj);
        Console.WriteLine("经过序列化与反序列化之后:");
        Console.WriteLine(DeSerialize(data));


        Console.ReadKey();
    }


    // 序列化对象-BinaryFormatter
    static byte[] Serialize(MyObject p)
    {
        // 使用二进制序列化
        IFormatter formatter = new BinaryFormatter();
        using (MemoryStream ms = new MemoryStream())
        {
            formatter.Serialize(ms, p);
            return ms.ToArray();
        }
    }


    // 反序列化对象-BinaryFormatter
    static MyObject DeSerialize(byte[] data)
    {
        // 使用二进制反序列化
        IFormatter formatter = new BinaryFormatter();
        using (MemoryStream ms = new MemoryStream(data))
        {
            MyObject p = formatter.Deserialize(ms) as MyObject;
            return p;
        }
    }
}

上述代码的运行结果如下图所示:

从结果图中可以看出,由于实现了自定义的序列化和反序列化,从而原先使用Serializable特性的默认序列化和反序列化算法没有起作用,MyObject类型的所有成员经过序列化和反序列化之后均被完整地还原了,包括申明了NonSerialized特性的成员。

总结

本文总结复习了.NET的流与序列化处理相关的重要知识点,下一篇会总结.NET中委托相关的重要知识点,欢迎继续关注!


参考资料(全是经典)

朱毅,《进入IT企业必读的200个.NET面试题》

张子阳,《.NET之美:.NET关键技术深入解析》

王涛,《你必须知道的.NET(第二版)》

相关推荐

悠悠万事,吃饭为大(悠悠万事吃饭为大,什么意思)

新媒体编辑:杜岷赵蕾初审:程秀娟审核:汤小俊审签:周星...

高铁扒门事件升级版!婚宴上‘冲喜’老人团:我们抢的是社会资源

凌晨两点改方案时,突然收到婚庆团队发来的视频——胶东某酒店宴会厅,三个穿大红棉袄的中年妇女跟敢死队似的往前冲,眼瞅着就要扑到新娘的高额钻石项链上。要不是门口小伙及时阻拦,这婚礼造型团队熬了三个月的方案...

微服务架构实战:商家管理后台与sso设计,SSO客户端设计

SSO客户端设计下面通过模块merchant-security对SSO客户端安全认证部分的实现进行封装,以便各个接入SSO的客户端应用进行引用。安全认证的项目管理配置SSO客户端安全认证的项目管理使...

还在为 Spring Boot 配置类加载机制困惑?一文为你彻底解惑

在当今微服务架构盛行、项目复杂度不断攀升的开发环境下,SpringBoot作为Java后端开发的主流框架,无疑是我们手中的得力武器。然而,当我们在享受其自动配置带来的便捷时,是否曾被配置类加载...

Seata源码—6.Seata AT模式的数据源代理二

大纲1.Seata的Resource资源接口源码2.Seata数据源连接池代理的实现源码3.Client向Server发起注册RM的源码4.Client向Server注册RM时的交互源码5.数据源连接...

30分钟了解K8S(30分钟了解微积分)

微服务演进方向o面向分布式设计(Distribution):容器、微服务、API驱动的开发;o面向配置设计(Configuration):一个镜像,多个环境配置;o面向韧性设计(Resista...

SpringBoot条件化配置(@Conditional)全面解析与实战指南

一、条件化配置基础概念1.1什么是条件化配置条件化配置是Spring框架提供的一种基于特定条件来决定是否注册Bean或加载配置的机制。在SpringBoot中,这一机制通过@Conditional...

一招解决所有依赖冲突(克服依赖)

背景介绍最近遇到了这样一个问题,我们有一个jar包common-tool,作为基础工具包,被各个项目在引用。突然某一天发现日志很多报错。一看是NoSuchMethodError,意思是Dis...

你读过Mybatis的源码?说说它用到了几种设计模式

学习设计模式时,很多人都有类似的困扰——明明概念背得滚瓜烂熟,一到写代码就完全想不起来怎么用。就像学了一堆游泳技巧,却从没下过水实践,很难真正掌握。其实理解一个知识点,就像看立体模型,单角度观察总...

golang对接阿里云私有Bucket上传图片、授权访问图片

1、为什么要设置私有bucket公共读写:互联网上任何用户都可以对该Bucket内的文件进行访问,并且向该Bucket写入数据。这有可能造成您数据的外泄以及费用激增,若被人恶意写入违法信息还可...

spring中的资源的加载(spring加载原理)

最近在网上看到有人问@ContextConfiguration("classpath:/bean.xml")中除了classpath这种还有其他的写法么,看他的意思是想从本地文件...

Android资源使用(android资源文件)

Android资源管理机制在Android的开发中,需要使用到各式各样的资源,这些资源往往是一些静态资源,比如位图,颜色,布局定义,用户界面使用到的字符串,动画等。这些资源统统放在项目的res/独立子...

如何深度理解mybatis?(如何深度理解康乐服务质量管理的5个维度)

深度自定义mybatis回顾mybatis的操作的核心步骤编写核心类SqlSessionFacotryBuild进行解析配置文件深度分析解析SqlSessionFacotryBuild干的核心工作编写...

@Autowired与@Resource原理知识点详解

springIOCAOP的不多做赘述了,说下IOC:SpringIOC解决的是对象管理和对象依赖的问题,IOC容器可以理解为一个对象工厂,我们都把该对象交给工厂,工厂管理这些对象的创建以及依赖关系...

java的redis连接工具篇(java redis client)

在Java里,有不少用于连接Redis的工具,下面为你介绍一些主流的工具及其特点:JedisJedis是Redis官方推荐的Java连接工具,它提供了全面的Redis命令支持,且...