C# Serializable到底有啥用

有时想起 2014-10-23 03:02:37
为啥有些类要加[Serializable],不加会怎样 加了又怎样,什么时候加。网上看到的回答都太官方了,理解力有限还不能理解,希望得到点好的回答
...全文
9749 30 打赏 收藏 转发到动态 举报
写回复
用AI写文章
30 条回复
切换为时间正序
请发表友善的回复…
发表回复
我是三峡移民 2017-04-21
  • 打赏
  • 举报
回复 2
引用 14 楼 ycg_893 的回复:
首先要理解序列化和反序列化,概念性的就不说了,自己可以查查; 例:你开发一个程序,服务器用.NET开发,有一个返回的有员工的方法Emps(),你的客户端可能是用.NET开发的WinFrom、WinWeb、或者手机(Android、IOS、Windows Mobile)、或者Linux等,你如何定义Emps()方法返回的数据格式才能确保任何客户端都能解析并展示数据呢?因为Emps()的调用过程可能是一个List<Emp>对象(如下)

    Public string Emps()
    {
        EmpManager emp = new EmpManager();
        List<Emp> empInfos = emp.GetInfos();
        //仅是例子,.NET 没有对应的类或方法
       return Json.Serialized(empInfos);
    }
,但对于客户端是不认识这个对象的也是无法转输时,因此我们可以将List<Emp>转换为JSON或XML格式,这就是一种序列化,当客户端收到这个string时,又将转换为对象集合时,叫反序列化。 所以我们给Emp类加[Serializable]特性时,就表示Emp类可以被序列化,只有可以被序列化的类才能可以被反序列化,但使用[Serializable]特性进行序列化,也只有.NET 才能反序列化,当然也可以根据.NET 的序列化算法,用Java编写一个反序列化方法,但这种方法就不合适了,因此跨平台的时候,我们通常用通用的序列化方法如Json或Xml等,也可能用开源跨平台的序列化工具如 Google Protobuf。 如有不对请大家指正。
看了一大堆,觉得你讲得最好
kisslqq 2017-02-14
  • 打赏
  • 举报
回复
8楼感觉你好牛逼,叫人家去调试就调试下呗,何必要加上前面那句话,我相信你碰到问题也会去百度goole查答案。
  • 打赏
  • 举报
回复
引用 12 楼 moonwrite 的回复:
http://www.cnblogs.com/epjnpe/archive/2007/12/05/984053.html 博客中的一句话 程序的Session就会存储到数据库中了,有一点需要注意的是,这样配置以后,存到Session中的对象必须是可序列化的,如果是自定义的类,那么就要加上[Seriablizable]属性。
其实这种“含糊其辞”的话,非常容易让人担忧,非常影响 lz 这样的喜欢了解事情真伪的人的心情。这种话也是官话,更多是被动抄袭而来的,自己不明说。 说明白一点,其实真正的意思应该反过来翻译才准确: 当你把数据放入Session集合(并且使用状态服务器或者SQL Server状态数据机制的时候),因为很多对象(类型)其实不能自动地正确反射、序列化,因此希望程序员有一种办法标记出“哪些不能序列化”,好让程序及时崩溃,造成程序崩溃看起来总比带着错误的数据而执行下去要好。但是,实在是有太多的类型不能序列化了,所以“退而求其次”,你只要给类型做一个标记“哪些能够序列化”,也能让asp.net帮你提前崩溃一下程序。 这是不得已而为之的,而且不过是帮助程序员“随随便便地”给类型加了一个标注,其准确性肯定有争议的,因为其实这个属性丝毫不能保证对象真的可以序列化或者不能序列化。
卧_槽 2014-10-24
  • 打赏
  • 举报
回复
引用 20 楼 Z65443344 的回复:
[quote=引用 18 楼 sp1234 的回复:] [quote=引用 12 楼 moonwrite 的回复:] http://www.cnblogs.com/epjnpe/archive/2007/12/05/984053.html 博客中的一句话 程序的Session就会存储到数据库中了,有一点需要注意的是,这样配置以后,存到Session中的对象必须是可序列化的,如果是自定义的类,那么就要加上[Seriablizable]属性。
其实这种“含糊其辞”的话,非常容易让人担忧,非常影响 lz 这样的喜欢了解事情真伪的人的心情。这种话也是官话,更多是被动抄袭而来的,自己不明说。 说明白一点,其实真正的意思应该反过来翻译才准确: 当你把数据放入Session集合(并且使用状态服务器或者SQL Server状态数据机制的时候),因为很多对象(类型)其实不能自动地正确反射、序列化,因此希望程序员有一种办法标记出“哪些不能序列化”,好让程序及时崩溃,造成程序崩溃看起来总比带着错误的数据而执行下去要好。但是,实在是有太多的类型不能序列化了,所以“退而求其次”,你只要给类型做一个标记“哪些能够序列化”,也能让asp.net帮你提前崩溃一下程序。 这是不得已而为之的,而且不过是帮助程序员“随随便便地”给类型加了一个标注,其准确性肯定有争议的,因为其实这个属性丝毫不能保证对象真的可以序列化或者不能序列化。[/quote] 学习了 举一反三一下,看对不对 比如我定义了一个类,标记它可以序列化 但是类里又引用了其他自定义类的对象,而这个被引用的类没有标记可以序列化 这样这个类其实是不能正确被序列化的,是这样吧[/quote] 这样没意义,定义了可序列化的类,只有同样定义了可序列化的对象是会被序列化的,引用了不可序列化的自定义类,对本身没有影响,这需要程序员自己决定何时,如何序列化自己的内容。
失落的神庙 2014-10-24
  • 打赏
  • 举报
回复
[Serializable] class abc { } 表示abc类可序列化。
 [Serializable]
        public class MyDataBase
        {
            public List<SinaLogin.UserInfo> MyUserList = new List<SinaLogin.UserInfo>();
        }

        /**/
        /// <summary>
        /// 将一个object对象序列化,返回一个byte[]
        /// </summary>
        /// <param name="obj">能序列化的对象</param>
        /// <returns></returns>
        public static byte[] ObjectToBytes(object obj)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                IFormatter formatter = new BinaryFormatter();
                formatter.Serialize(ms, obj);
                return ms.GetBuffer();
            }
        }

        /**/
        /// <summary>
        /// 将一个序列化后的byte[]数组还原
        /// </summary>
        /// <param name="Bytes"></param>
        /// <returns></returns>
        public static object BytesToObject(byte[] Bytes)
        {
            using (MemoryStream ms = new MemoryStream(Bytes))
            {
                IFormatter formatter = new BinaryFormatter();
                return formatter.Deserialize(ms);
            }
        }

        public static void save(string Path, object SenderFile)
        {
            byte[] tempdata = ObjectToBytes(SenderFile);
            File.WriteAllBytes(Path, tempdata);
        }

        public static object inputfile(string Path)
        {
            byte[] tempdata = File.ReadAllBytes(Path);
            return BytesToObject(tempdata);
        }
例如MyDataBase类 必须标识可序列化 不然执行save 这个方法会提示该列不可序列化 SinaLogin.UserInfo 这个类也得标识可序列化。
moonwrite 2014-10-24
  • 打赏
  • 举报
回复
引用 18 楼 sp1234 的回复:
[quote=引用 12 楼 moonwrite 的回复:] http://www.cnblogs.com/epjnpe/archive/2007/12/05/984053.html 博客中的一句话 程序的Session就会存储到数据库中了,有一点需要注意的是,这样配置以后,存到Session中的对象必须是可序列化的,如果是自定义的类,那么就要加上[Seriablizable]属性。
其实这种“含糊其辞”的话,非常容易让人担忧,非常影响 lz 这样的喜欢了解事情真伪的人的心情。这种话也是官话,更多是被动抄袭而来的,自己不明说。 说明白一点,其实真正的意思应该反过来翻译才准确: 当你把数据放入Session集合(并且使用状态服务器或者SQL Server状态数据机制的时候),因为很多对象(类型)其实不能自动地正确反射、序列化,因此希望程序员有一种办法标记出“哪些不能序列化”,好让程序及时崩溃,造成程序崩溃看起来总比带着错误的数据而执行下去要好。但是,实在是有太多的类型不能序列化了,所以“退而求其次”,你只要给类型做一个标记“哪些能够序列化”,也能让asp.net帮你提前崩溃一下程序。 这是不得已而为之的,而且不过是帮助程序员“随随便便地”给类型加了一个标注,其准确性肯定有争议的,因为其实这个属性丝毫不能保证对象真的可以序列化或者不能序列化。[/quote] 猜测 → 求证 我说出了我曾经的猜测,又没有说我的猜测是对的~ 不是叫楼主自己去求证了么
海涵德 2014-10-24
  • 打赏
  • 举报
回复
以前在使用.net时,如WebService、WCF等都使用了序列化技术,最近在编写java RMI服务应用后对序列化的理解更深了,Java RMI的含义就是,Remote Method Invoke 远程方法调用。 序列化技术是远程对象访问技术,不是.NET或Java独有的。
海涵德 2014-10-24
  • 打赏
  • 举报
回复
对于远程调用,就是从一台机器访问另一台机器的对象,这个对象一般都需要使用序列化。 在本地调用对象,直接使用二进制代码就可以了,因为都是在内存之间的访问,远程调用访问的是两一台机器的内存对象,对象要通过网络传递,传递要使用通信协议,通过序列化对象可以有效地完成远程对象的访问和传递。
於黾 2014-10-24
  • 打赏
  • 举报
回复
引用 18 楼 sp1234 的回复:
[quote=引用 12 楼 moonwrite 的回复:] http://www.cnblogs.com/epjnpe/archive/2007/12/05/984053.html 博客中的一句话 程序的Session就会存储到数据库中了,有一点需要注意的是,这样配置以后,存到Session中的对象必须是可序列化的,如果是自定义的类,那么就要加上[Seriablizable]属性。
其实这种“含糊其辞”的话,非常容易让人担忧,非常影响 lz 这样的喜欢了解事情真伪的人的心情。这种话也是官话,更多是被动抄袭而来的,自己不明说。 说明白一点,其实真正的意思应该反过来翻译才准确: 当你把数据放入Session集合(并且使用状态服务器或者SQL Server状态数据机制的时候),因为很多对象(类型)其实不能自动地正确反射、序列化,因此希望程序员有一种办法标记出“哪些不能序列化”,好让程序及时崩溃,造成程序崩溃看起来总比带着错误的数据而执行下去要好。但是,实在是有太多的类型不能序列化了,所以“退而求其次”,你只要给类型做一个标记“哪些能够序列化”,也能让asp.net帮你提前崩溃一下程序。 这是不得已而为之的,而且不过是帮助程序员“随随便便地”给类型加了一个标注,其准确性肯定有争议的,因为其实这个属性丝毫不能保证对象真的可以序列化或者不能序列化。[/quote] 学习了 举一反三一下,看对不对 比如我定义了一个类,标记它可以序列化 但是类里又引用了其他自定义类的对象,而这个被引用的类没有标记可以序列化 这样这个类其实是不能正确被序列化的,是这样吧
绿领巾童鞋 2014-10-24
  • 打赏
  • 举报
回复
序列化,我个人印象,就是把你所需的转化的系统类型的或者自定义的对象类型数据转化为字节流,字节流上面的数据排列都是顺序按照类型定义排列的,并且能够反向还原为原类型的数据。
林英南 2014-10-24
  • 打赏
  • 举报
回复
用于显性表示该类可以序列化,.Net里面不同对象/控件对于序列化的方式会有所不同,有些默认是把所有的对象都用.Net本身的机制去做序列化,那么即使你不标示这个特性它也可以运行正常,但是同时也有写方法它有自己部分的实现机制,需要先知道所解析的对象是否是可序列化的,便通过这个特性去处理,所以需要显性的表示这个特性 我觉得你了解下就好了,没有必要为这个特性而有任何困扰,简单的解释就是如果你长得比较漂亮,那么如果穿个中性的衣服的话,可能有些人会认错你的性别,那么就需要你穿个性别特征明显一点的衣服以表示你的性别
u010032648 2014-10-24
  • 打赏
  • 举报
回复
这个主要是对数据的序列化,和反序列化。 比如,有后台数据需要转换成json传到前台,当然数据时对象形式的,就需要在改对象上加上[Serializable],反之,前台到后台一样。再比如,ListView数据储存的是一个集合或对象,那么也需要加上[Serializable],Session一样。把数据存储到磁盘中,而不是直接写成txt文档之类的,也需要这样。目前,我就用到这些,其他的需不需要,我就不知道了。总之,需要把对象存储在ListView,session,json之类的,都需要[Serializable]
xiejin90314 2014-10-24
  • 打赏
  • 举报
回复
不加这个关键字 你的这个对象没放保存到物理存储器上面 嗯 就是你的类没法存入到数据库里面
  • 打赏
  • 举报
回复
Serializable 这个字眼查一下字典,谁都会。超一堆msdn上的“序列化/反序列”解释,谁都会。但是回答你这个问题“哪些类要加,加了又怎样,什么时候加”,你看到了,跟官方说法一样,大家都懒得说啊。
  • 打赏
  • 举报
回复
肯定有不少人抄了“官方”的序列化/反序列化的一堆概念给你。 问题是,大部分程序中,你不写这个也照样能序列化/反序列化。而有些程序中,对于在其它大部分程序中好好地序列化的类型,你不写这个还就是在运行时给你崩溃(抛出异常)。而此时这些”抄官方解释“的无法回答了,因为根本找准这个”点“。 所以我让你自己测试一下,“把这行代码注释掉,然后自己测试,看看都出现哪些bug”。
相思梦 2014-10-23
  • 打赏
  • 举报
回复
可以序列化成影像流,同时也可以将序列化后的流反序列化映射为对应类型
ycg_893 2014-10-23
  • 打赏
  • 举报
回复 4
首先要理解序列化和反序列化,概念性的就不说了,自己可以查查; 例:你开发一个程序,服务器用.NET开发,有一个返回的有员工的方法Emps(),你的客户端可能是用.NET开发的WinFrom、WinWeb、或者手机(Android、IOS、Windows Mobile)、或者Linux等,你如何定义Emps()方法返回的数据格式才能确保任何客户端都能解析并展示数据呢?因为Emps()的调用过程可能是一个List<Emp>对象(如下)

    Public string Emps()
    {
        EmpManager emp = new EmpManager();
        List<Emp> empInfos = emp.GetInfos();
        //仅是例子,.NET 没有对应的类或方法
       return Json.Serialized(empInfos);
    }
,但对于客户端是不认识这个对象的也是无法转输时,因此我们可以将List<Emp>转换为JSON或XML格式,这就是一种序列化,当客户端收到这个string时,又将转换为对象集合时,叫反序列化。 所以我们给Emp类加[Serializable]特性时,就表示Emp类可以被序列化,只有可以被序列化的类才能可以被反序列化,但使用[Serializable]特性进行序列化,也只有.NET 才能反序列化,当然也可以根据.NET 的序列化算法,用Java编写一个反序列化方法,但这种方法就不合适了,因此跨平台的时候,我们通常用通用的序列化方法如Json或Xml等,也可能用开源跨平台的序列化工具如 Google Protobuf。 如有不对请大家指正。
finemi 2014-10-23
  • 打赏
  • 举报
回复
这种的都叫【特性】,又叫属性类,及Attribute ,但是为了和类属性(Property)区分,所以基本都叫做特性,或者特性标记。 从字面上理解,大概就能猜到他是做什么的了。官方的说法是: “特性提供功能强大的方法,用以将元数据或声明信息与代码(程序集、类型、方法、属性等)相关联。 ” 或 “Attribute 类将预定义的系统信息或用户定义的自定义信息与目标元素相关联。 目标元素可以是程序集、类、构造函数、委托、枚举、事件、字段、接口、方法、可移植可执行文件模块、参数、属性、返回值、结构或其他特性。” 我们来直观的看一下特性的用途: 添加一个窗体叫Form1,然后增加如下代码

 [DefaultEvent( " FormClosed " )]
public   partial   class  Form1 : Form{}
然后新建一个窗口Form2继承自Form1
 public   partial   class  Form2 : Form1{}
最后切换到Form2的窗体设计器上,双击窗体自动生成代码:
 private   void  Form2_FormClosed( object  sender, FormClosedEventArgs e){}
看到了吧,这就是改变控件默认事件的特性 注意,这里一直说的是XXX类,没错,他就是一个类。所有特性都以单词“Attribute”结束,但是所有特性都可以省略“Attribute”使用。Serializable原名叫SerializableAttribute。中括号中使用特性其实就是调用它的构造函数,所以有些特性可以加参数。 http://msdn.microsoft.com/zh-cn/library/z0w1kczw.aspx 最后说说你说的[Serializeble],他是给序列化和反序列化用的,标识有这个特性的类才能够被序列化。序列化是一种将一个对象的状态(实例变量)持久化到文件中,反序列化就是从这个格式化的文件中读取对象的状态恢复我们保存的对象。用来比如保存程序的当前状态,下次再启动时恢复到上次的状态。具体你可以搜"C#序列化" 或者看这里:http://www.cnblogs.com/LiZhiW/p/3622365.html
moonwrite 2014-10-23
  • 打赏
  • 举报
回复
http://www.cnblogs.com/epjnpe/archive/2007/12/05/984053.html 博客中的一句话 程序的Session就会存储到数据库中了,有一点需要注意的是,这样配置以后,存到Session中的对象必须是可序列化的,如果是自定义的类,那么就要加上[Seriablizable]属性。 官方的例子是序列化后存在xml中 http://msdn.microsoft.com/zh-cn/library/system.serializableattribute.aspx 我的个人理解是一个对象 需要保存到其他地方(或者要和其他人交流),有时候又要反序列化回来,那么应该有些规则。 我没有看一个对象序列化后会怎么样~ 你可以按官方的例子 最后看看xml是怎么样的
phommy 2014-10-23
  • 打赏
  • 举报
回复
Serializable不是.net的核心概念 先是开发人员有需求,说我需要序列化反序列化啊,这是个常用功能,于是有人(只不过这个个恰好是微软)做了个工具类 然后需求又说,我这个类里的字段不想全放序列化里啊,这个要序列化那个属性不要。工具类作者说了,那你得传参数啊,直接放方法参数里太麻烦了,每次都要传一堆,干脆用属性标记出来吧,于是有了NonSerialized一干特性,不想序列化的参数你给我标记出来吧 这时这个工具类的作者发现他没法区分了:当他遇到一个类,所有字段都没标记NonSerialized,这个类的意思是所有字段都要序列化呢,还是这个类压根不知道序列化这回事呢?作者只好要求调用方用Serializable把要序列化的类标记出来 大概就是这样
加载更多回复(10)
codeceo 首页问答热门文章RSS订阅 文章首页 Java JavaScript PHP iOS Android HTML5 CSS3 Linux C++ Python C# Node.Js 一文让你彻底理解 Java HashMap 和 ConcurrentHashMap 2018-07-25 分类:JAVA开发、编程开发、首页精华0人评论 来源:crossoverjie.top 分享到:更多0 前言 Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据。 本篇主要想讨论 ConcurrentHashMap 这样一个并发容器,在正式开始之前我觉得有必要谈谈 HashMap,没有它就不会有后面的 ConcurrentHashMap。 HashMap 众所周知 HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。 Base 1.7 1.7 中的数据结构图: 先来看看 1.7 中的实现。 这是 HashMap 中比较核心的几个成员变量;看看分别是什么意思? 初始化桶大小,因为底层是数组,所以这是数组默认的大小。 桶最大值。 默认的负载因子(0.75) table 真正存放数据的数组。 Map 存放数量的大小。 桶大小,可在初始化时显式指定。 负载因子,可在初始化时显式指定。 重点解释下负载因子: 由于给定的 HashMap 的容量大小是固定的,比如默认初始化: public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity; init(); } 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。 因此通常建议能提前预估 HashMap 的大小最好,尽量的减少扩容带来的性能损耗。 根据代码可以看到其实真正存放数据的是 transient Entry[] table = (Entry[]) EMPTY_TABLE; 这个数组,那么它又是如何定义的呢? Entry 是 HashMap 中的一个内部类,从他的成员变量很容易看出: key 就是写入时的键。 value 自然就是值。 开始的时候就提到 HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构。 hash 存放的是当前 key 的 hashcode。 知晓了基本结构,那来看看其中重要的写入、获取函数: put 方法 public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; } 判断当前数组是否需要初始化。 如果 key 为空,则 put 一个空值进去。 根据 key 计算出 hashcode。 根据计算出的 hashcode 定位出所在桶。 如果桶是一个链表则需要遍历判断里面的 hashcode、key 是否和传入 key 相等,如果相等则进行覆盖,并返回原来的值。 如果桶是空的,说明当前位置没有数据存入;新增一个 Entry 对象写入当前位置。 void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } void createEntry(int hash, K key, V value, int bucketIndex) { Entry e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; } 当调用 addEntry 写入 Entry 时需要判断是否需要扩容。 如果需要就进行两倍扩充,并将当前的 key 重新 hash 并定位。 而在 createEntry 中会将当前位置的桶传入到新建的桶中,如果当前桶有值就会在位置形成链表。 get 方法 再来看看 get 函数: public V get(Object key) { if (key == null) return getForNullKey(); Entry entry = getEntry(key); return null == entry ? null : entry.getValue(); } final Entry getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; } 首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。 判断该位置是否为链表。 不是链表就根据 key、key 的 hashcode 是否相等来返回值。 为链表则需要遍历直到 key 及 hashcode 相等时候就返回值。 啥都没取到就直接返回 null 。 Base 1.8 不知道 1.7 的实现大家看出需要优化的点没有? 其实一个很明显的地方就是: 当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。 因此 1.8 中重点优化了这个查询效率。 1.8 HashMap 结构图: 先来看看几个核心的成员变量: static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f; static final int TREEIFY_THRESHOLD = 8; transient Node[] table; /** * Holds cached entrySet(). Note that AbstractMap fields are used * for keySet() and values(). */ transient Set> entrySet; /** * The number of key-value mappings contained in this map. */ transient int size; 和 1.7 大体上都差不多,还是有几个重要的区别: TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。 HashEntry 修改为 Node。 Node 的核心组成其实也是和 1.7 中的 HashEntry 一样,存放的都是 key value hashcode next 等数据。 再来看看核心方法。 put 方法 看似要比 1.7 的复杂,我们一步步拆解: 判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)。 根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。 如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。 如果当前桶为红黑树,那就要按照红黑树的方式写入数据。 如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。 如果在遍历过程中找到 key 相同时直接退出遍历。 如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。 最后判断是否需要进行扩容。 get 方法 public V get(Object key) { Node e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node getNode(int hash, Object key) { Node[] tab; Node first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } get 方法看起来就要简单许多了。 首先将 key hash 之后取得所定位的桶。 如果桶为空则直接返回 null 。 否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。 如果第一个不匹配,则判断它的下一个是红黑树还是链表。 红黑树就按照树的查找方式返回值。 不然就按照链表的方式遍历匹配返回值。 从这两个核心方法(get/put)可以看出 1.8 中对大链表做了优化,修改为红黑树之后查询效率直接提高到了 O(logn)。 但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环。 final HashMap map = new HashMap(); for (int i = 0; i < 1000; i++) { new Thread(new Runnable() { @Override public void run() { map.put(UUID.randomUUID().toString(), ""); } }).start(); } 但是为什么呢?简单分析下。 看过上文的还记得在 HashMap 扩容的时候会调用 resize() 方法,就是这里的并发操作容易在一个桶上形成环形链表;这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环。 如下图: 遍历方式 还有一个值得注意的是 HashMap 的遍历方式,通常有以下几种: Iterator> entryIterator = map.entrySet().iterator(); while (entryIterator.hasNext()) { Map.Entry next = entryIterator.next(); System.out.println("key=" + next.getKey() + " value=" + next.getValue()); } Iterator<String> iterator = map.keySet().iterator(); while (iterator.hasNext()){ String key = iterator.next(); System.out.println("key=" + key + " value=" + map.get(key)); } 强烈建议使用第一种 EntrySet 进行遍历。 第一种可以把 key value 同时取出,第二种还得需要通过 key 取一次 value,效率较低。 简单总结下 HashMap:无论是 1.7 还是 1.8 其实都能看出 JDK 没有对它做任何的同步操作,所以并发会出问题,甚至 1.7 中出现死循环导致系统不可用(1.8 已经修复死循环问题)。 因此 JDK 推出了专项专用的 ConcurrentHashMap ,该类位于 java.util.concurrent 包下,专门用于解决并发问题。 坚持看到这里的朋友算是已经把 ConcurrentHashMap 的基础已经打牢了,下面正式开始分析。 ConcurrentHashMap ConcurrentHashMap 同样也分为 1.7 、1.8 版,两者在实现上略有不同。 Base 1.7 先来看看 1.7 的实现,下面是他的结构图: 如图所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。 它的核心成员变量: /** * Segment 数组,存放数据时首先需要定位到具体的 Segment 中。 */ final Segment[] segments; transient Set keySet; transient Set> entrySet; Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下: static final class Segment extends ReentrantLock implements Serializable { private static final long serialVersionUID = 2249069246763182397L; // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶 transient volatile HashEntry[] table; transient int count; transient int modCount; transient int threshold; final float loadFactor; } 看看其中 HashEntry 的组成: 和 HashMap 非常类似,唯一的区别就是其中的核心数据如 value ,以及链表都是 Volatile 修饰的,保证了获取时的可见性。 原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。 下面也来看看核心的 put get 方法。 put 方法 public V put(K key, V value) { Segment s; if (value == null) throw new NullPointerException(); int hash = hash(key); int j = (hash >>> segmentShift) & segmentMask; if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment s = ensureSegment(j); return s.put(key, hash, value, false); } 首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put。 final V put(K key, int hash, V value, boolean onlyIfAbsent) { HashEntry node = tryLock() ? null : scanAndLockForPut(key, hash, value); V oldValue; try { HashEntry[] tab = table; int index = (tab.length - 1) & hash; HashEntry first = entryAt(tab, index); for (HashEntry e = first;;) { if (e != null) { K k; if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { oldValue = e.value; if (!onlyIfAbsent) { e.value = value; ++modCount; } break; } e = e.next; } else { if (node != null) node.setNext(first); else node = new HashEntry(hash, key, value, first); int c = count + 1; if (c > threshold && tab.length < MAXIMUM_CAPACITY) rehash(node); else setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { unlock(); } return oldValue; } 虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。 首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。 尝试自旋获取锁。 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。 再结合图看看 put 的流程。 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。 最后会解除在 1 中所获取当前 Segment 的锁。 get 方法 public V get(Object key) { Segment s; // manually integrate access methods to reduce overhead HashEntry[] tab; int h = hash(key); long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); e != null; e = e.next) { K k; if ((k = e.key) == key || (e.hash == h && key.equals(k))) return e.value; } } return null; } get 逻辑比较简单: 只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。 由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。 ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。 Base 1.8 1.7 已经解决了并发问题,并且能支持 N 个 Segment 这么多次数的并发,但依然存在 HashMap 在 1.7 版本中的问题。 那就是查询遍历链表效率太低。 因此 1.8 做了一些数据结构上的调整。 首先来看下底层的组成结构: 看起来是不是和 1.8 HashMap 结构类似? 其中抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。 也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。 其中的 val next 都用了 volatile 修饰,保证了可见性。 put 方法 重点来看看 put 函数: 根据 key 计算出 hashcode 。 判断是否需要进行初始化。 f 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。 如果都不满足,则利用 synchronized 锁写入数据。 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。 get 方法 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 如果是红黑树那就按照树的方式获取值。 就不满足那就按照链表的方式遍历获取值。 1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。 总结 看完了整个 HashMap 和 ConcurrentHashMap 在 1.7 和 1.8 中不同的实现方式相信大家对他们的理解应该会更加到位。 其实这块也是面试的重点内容,通常的套路是: 谈谈你理解的 HashMap,讲讲其中的 get put 过程。 1.8 做了什么优化? 是线程安全的嘛? 不安全会导致哪些问题? 如何解决?有没有线程安全的并发容器? ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做? 这一串问题相信大家仔细看完都能怼回面试官。 除了面试会问到之外平时的应用其实也蛮多,像之前谈到的 Guava 中 Cache 的实现就是利用 ConcurrentHashMap 的思想。 同时也能学习 JDK 作者大牛们的优化思路以及并发解决方案。 其实写这篇的前提是源于 GitHub 上的一个 Issues,也希望大家能参与进来,共同维护好这个项目。 分享到:更多0 继续浏览有关ConcurrentHashmapHashMapJAVA开发的文章 发表我的评论 表情插代码发布评论有人回复时邮件通知我 热门文章 成为伟大程序员的 10 个要点 如何成为一名成功的程序员 25个最基本的JavaScript面试问题及答案 程序员最核心的竞争力是什么? Java 内存模型 JMM 浅析 理解 Flexbox:你需要知道的一切 创造型职业程序员的无奈 我(作为一名开发者)所犯过的错误 黑客老王:一个人的黑客史 阿里面试回来,想和Java程序员谈一谈 职场人生 软件工程师生存指南:面试准备、工作经验和实用工具 自由职业的这两年 倾听程序员的心声真的很重要 平庸开发者的生存指南 为什么我从 Google 辞职而为自己工作 我是一名朝九晚五的程序员(你也可以!) 成为伟大程序员的 10 个要点 如何处理前任程序员留下的代码 开发人员爱开发 如何成为一名成功的程序员 版权所有,保留一切权利! © 2016 码农网 浙ICP备14003773号-1 浙公网安备 33010502000955号 商务合作QQ:290074886(请注明来意)

110,536

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧