其他分享
首页 > 其他分享> > 教学心得 2019.4.27

教学心得 2019.4.27

作者:互联网

2019.4.27

今晚下班前一个学生来加我微信说想问我问题,当天对象朋友过生日,我们去吃饭吃到10点半打车回家,回家路上学生给我发来5个面试题。

 1、NIO中的新类有哪两个
 2、hashmap中什么时候覆盖value 什么时候形成链表
 3、请求头请求行请求体  用什么分割
 4、hashmap的dos攻击的应用场景
 5、从http的角度分析一下为什么第一次加载图片慢 而第二次快

我看完,这些都是比较刁钻的题。估计是被面试吓住了,我给他写了个文档,详细的分析了这些题的实现。从源码到本质,大概花了半小时左右,但是我当晚并没有发给他,我说上午给它,上午7点起床我把结果发给他,秒回!真的是秒回,当时我觉得超级欣慰,这就是当一个教育者最大的快乐吧!

 

以下是我对五个题目的回答:


1、NIO中的新类

java.nio.channels包新增了以下4个API来支持异步IO:

NIO学习参考链接:https://blog.csdn.net/youyou1543724847/article/details/52748785

2、hashmap中什么时候覆盖value 什么时候形成链表

hashmap中什么时候覆盖value?

hashmap中什么时候形成链表?

HashMap是有一个一维数组和一个链表组成,从而得知,在解决冲突问题时,hashmap选择的是链地址法。

  1. 为什么用了一维数组:数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难

  2. 为什么用了链表:链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:寻址困难,插入和删除容易

而HashMap是两者的结合,用一维数组存放散列地址,以便更快速的遍历;用链表存放地址值,以便更快的插入和删除!

先看hashmap底层是个数组结构,数组上面存的数据都是 Entry<K,V> 这个类型的数据。

然后看他的主要实现如下:

 static class Entry<K,V> implements Map.Entry<K,V> {
     final K key;
     V value;
     Entry<K,V> next;
     int hash;
 ​
     //Creates new entry.
     Entry(int h, K k, V v, Entry<K,V> n) {
         value = v;
         next = n;
         key = k;
         hash = h;
     }
     //..........
 }

可以看到,四个属性,key,value,hash,和next,重点就是这个next属性,这个是形成链表的关键。

正是因为有了这个存在,hashmap底层才会有数组和链表共存的牛x数据结构出现。

先从往一个hashmap里面put()值说起。

 public V put(K key, V value) {
     if (table == EMPTY_TABLE) {
         inflateTable(threshold);
     }
     //当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap允许为null的原因
     if (key == null)
         return putForNullKey(value);
     //计算key的hash值
     int hash = hash(key);
     //计算key hash 值在 table 数组中的位置
     int i = indexFor(hash, table.length);
     //从i出开始迭代 e,找到 key 保存的位置
     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
         Object k;
         //判断该条链上是否有hash值相同的(key相同)
         //若存在相同,则直接覆盖value,返回旧value
         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
             V oldValue = e.value;//这个时候,这个e对象的value的值还是旧数据
             e.value = value;//更新传进来的value的值到e对象的value属性上
             e.recordAccess(this);
             return oldValue;//返回旧值,就结束了下面的就不执行啦,这是在找到key相同的情况下
         }
     }
     //修改次数增加1
     modCount++;
     //将key、value添加至i位置处
     addEntry(hash, key, value, i);
     return null;
 }

  

其他的都可以先不管,有疑惑的估计,就是那个for循环了吧。 那个呢就是假设在底层数组table的table[i]位置有个链表存在,那么就得循环这个链表的所有值,来判断有没有hash值相同,key也相同的一样的两个map。 如果有的话,那就不新建节点 Entry<K,V> 了,直接修改原来的值,这也就解释了,你在修改map的值的时候,可以直接put相同的key和不同的value就把map修改了。 当然,想理解到这点,你得先知道这个链表是怎么出来的。

假设没有进这个for循环,那么下面就继续执行,添加节点。addEntry(hash, key, value, i);

 /**
      * HashMap 添加节点
      *
      * @param hash        当前key生成的hashcode
      * @param key         要添加到 HashMap 的key
      * @param value       要添加到 HashMap 的value
      * @param bucketIndex 桶,也就是这个要添加 HashMap 里的这个数据对应到数组的位置下标
      */
 void addEntry(int hash, K key, V value, int bucketIndex) {
     //size:The number of key-value mappings contained in this map.
     //threshold:The next size value at which to resize (capacity * load factor)
     //数组扩容条件:1.已经存在的key-value mappings的个数大于等于阈值
     //              2.底层数组的bucketIndex坐标处不等于null
     if ((size >= threshold) && (null != table[bucketIndex])) {
         resize(2 * table.length);//扩容之后,数组长度变了
         hash = (null != key) ? hash(key) : 0;//为什么要再次计算一下hash值呢?
         bucketIndex = indexFor(hash, table.length);//扩容之后,数组长度变了,在数组的下标跟数组长度有关,得重算。
     }
     createEntry(hash, key, value, bucketIndex);
 }
 ​
 /**
      * 这地方就是链表出现的地方,有2种情况
      * 1,原来的桶bucketIndex处是没值的,那么就不会有链表出来啦
      * 2,原来这地方有值,那么根据Entry的构造函数,把新传进来的key-value mapping放在数组上,原来的就挂在这个新来的next属性上了
      */
 void createEntry(int hash, K key, V value, int bucketIndex) {
     HashMap.Entry<K, V> e = table[bucketIndex];
     table[bucketIndex] = new HashMap.Entry<>(hash, key, value, e);
     size++;
 }

添加数据的时候,要是key已经在数组上了,这个“在数组上”,可不仅仅是在数组上,也可能是已经在数组的某个位置的链表上了,因为,上面put方法的时候,他不是循环了那个链表吗?如果在循环链表的过程中找到key相同啦,那就是更新。 还有就是他们虽然以链表的形式在数组的同一个下标位置,学名叫“桶”。在同一个桶里面,但是却不能说他们根据key生成的hash值是相同的。因为是根据hash值去算在数组的下标,那么,就像10%3 ==1,7%3 ==1。根据前面这个算法来算下标的话,那么10和7就可以认为是hash值了,可以是不同的。 第二行,我觉得倒是没问题。新添加的数据就站在数组的位置,原来的就链在刚刚放到数组上的这个Entry对象的next属性上了。链表也就出来咯。

3、请求头请求行请求体 用什么分割

如果是为了解析,直接用字符串操作即可。

如果是要封装建议使用HttpClient就OK!

4、hashmap的dos攻击的应用场景

参考文档:https://www.cnblogs.com/goodbye-lazy/p/10494488.html

随着RESTful风格的接口普及,程序员默认都会使用json作为数据传递的方式。json格式的数据冗余少,兼容性高,从提出到现在已被广泛的使用,可以说成为了Web的一种标准。无论我们服务端使用什么语言,我们拿到json格式的数据之后都需要做jsonDecode(),将json串转换为json对象,而对象默认会存储于Hash Table,而Hash Table很容易被碰撞攻击。我只要将攻击数据放在json中,服务端程序在做jsonDecode()时必定中招,中招后CPU会立刻飙升至100%。16核的CPU,16个请求就能达到DoS的目的。

如何防御

要想防御Hash Collision Dos攻击,行业内已经有很多成熟的方案了,不过都是建议换语言或者重写HashTable。这里只说当前json格式解析的问题。首先我们需要增加权限验证,最大可能的在jsonDecode()之前把非法用户拒绝。其次在jsonDecode()之前做数据大小与参数白名单验证。旧项目的改造与维护成本如果很高,建议自己重写jsonDecode()方法。

5、从http的角度分析一下为什么第一次加载图片慢 而第二次快

参考文档:https://blog.csdn.net/m0_37263637/article/details/80903605

HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议。HTTP协议是无状态的,打开一个服务器上的网页和上一次打开这个服务器上的网页之间没有任何联系,但HTTP是一个无状态的面向连接(传输层为tcp协议)的协议。 HTTP = TCP 握手+ http发送数据

http和https 传输层均为TCP,所以在正式传输数据前,TCP都会三次握手建立链接

在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。每遇到一个Web资源,浏览器就会重新建立一个HTTP会话。 流程:建立连接-传输数据-断开连接 ………. 建立连接-传输数据-断开连接

从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:

 Connection:keep-alive

在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如nginx)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。 流程:建立连接-传输数据- 保持连接 -传输数据- 断开连接

从本质上来讲,HTTP仅仅是应用层的协议,TCP才是真正的传输层协议,因此TCP才有真正的长连接短连接的说法。

 


 

发给他之后,他看完回我消息的时候,我正在骑车过红绿灯!刚好红灯,我就停下来回复他,我们又聊了一会。大致就是了解刚才五道题的细节,以及面试中遇到的坑。

我告诉他,其实技术问题真的不难,平时多分析源码,查看问题本质,所有问题到了底层都好解决,基础扎实一些,即可解决!

最后回答完所有问题刚好上班,写下来记录这些事情,是为了以后能够留作回忆!

 

 

 

标签:27,hash,数组,2019.4,value,链表,key,table,心得
来源: https://www.cnblogs.com/hellokuangshen/p/10782176.html