精华内容
下载资源
问答
  • set 原理题
    千次阅读 多人点赞
    2019-05-22 20:36:34

    HashSet 的实现原理?

    首先,我们需要知道它是Set的一个实现,所以保证了当中没有重复的元素
    一方面Set中最重要的一个操作就是查找。而且通常我们会选择

    HashSet来实现,因为它专门对快速查找进行了优化。

    HashSet使用的是散列函数,那么它当中的元素也就无序可寻。当中是允许元素为null的。

    先对实现原理进行一个总结:

    (1)基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。

    (2)当我们试图把某个类的对象当成 HashMap的 key,或试图将这个类的对象放入 HashSet 中保存时,重写该类的equals(Object obj)方法和 hashCode() 方法很重要,而且这两个方法的返回值必须保持一致:当该类的两个的 hashCode() 返回值相同时,它们通过 equals() 方法比较也应该返回 true。通常来说,所有参与计算 hashCode() 返回值的关键属性,都应该用于作为 equals() 比较的标准。

    (3)HashSet的其他操作都是基于HashMap的。

    在这,为大家讲解HashSet 的实现原理:

    它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成, HashSet的源代码如下:

    import java.util.AbstractSet;
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.LinkedHashMap;
    import java.util.Set;
    
    import javax.swing.text.html.HTMLDocument.Iterator;
    
    public class HashSet<E>  
              extends AbstractSet<E>  
              implements Set<E>, Cloneable, java.io.Serializable  
    {  
    static final long serialVersionUID = -5024744406713321676L;  
    
    // 底层使用HashMap来保存HashSet中所有元素。  
    private transient HashMap<E,Object> map;  
      
    // 定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。  
    private static final Object PRESENT = new Object();  
    
     
    //  默认的无参构造器,构造一个空的HashSet。 
    //   
    //  实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。 
      
    public HashSet() {  
    map = new HashMap<E,Object>();  
    }  
    
     
    //  构造一个包含指定collection中的元素的新set。 
    //  
    //  实际底层使用默认的加载因子0.75和足以包含指定 
    //  collection中所有元素的初始容量来创建一个HashMap。 
    //  @param c 其中的元素将存放在此set中的collection。 
     
    public HashSet(Collection<? extends E> c) {  
    map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));  
    addAll(c);  
    }  
    
     
    //  以指定的initialCapacity和loadFactor构造一个空的HashSet。 
    //  
    //  实际底层以相应的参数构造一个空的HashMap。 
    // @param initialCapacity 初始容量。 
    //  @param loadFactor 加载因子。 
       
    public HashSet(int initialCapacity, float loadFactor) {  
    map = new HashMap<E,Object>(initialCapacity, loadFactor);  
    }  
    
     
    //  以指定的initialCapacity构造一个空的HashSet。 
    //  
    //  实际底层以相应的参数及加载因子loadFactor为0.75构造一个空的HashMap。 
    //  @param initialCapacity 初始容量。 
     
    public HashSet(int initialCapacity) {  
    map = new HashMap<E,Object>(initialCapacity);  
    }  
    
    
    //  以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。 
    //  此构造函数为包访问权限,不对外公开,实际只是是对LinkedHashSet的支持。 
    //  
    //  实际底层会以指定的参数构造一个空LinkedHashMap实例来实现。 
    //  @param initialCapacity 初始容量。 
    //  @param loadFactor 加载因子。 
    //  @param dummy 标记。 
       
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {  
    map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);  
    }  
    
     
    //  返回对此set中元素进行迭代的迭代器。返回元素的顺序并不是特定的。 
    //  
    // 底层实际调用底层HashMap的keySet来返回所有的key。 
    //  可见HashSet中的元素,只是存放在了底层HashMap的key上, 
    //  value使用一个static final的Object对象标识。 
    //  @return 对此set中元素进行迭代的Iterator。 
    
     public Iterator<E> iterator() {  
         return map.keySet().iterator();  
    
    }  
     
    // 返回此set中的元素的数量(set的容量)。 
    //  
    //  底层实际调用HashMap的size()方法返回Entry的数量,就得到该Set中元素的个数。 
    //  @return 此set中的元素的数量(set的容量)。 
      
    public int size() {  
    return map.size();  
    }  
    
     
    //  如果此set不包含任何元素,则返回true。 
    //  
    //  底层实际调用HashMap的isEmpty()判断该HashSet是否为空。 
    //  @return 如果此set不包含任何元素,则返回true。 
     
    public boolean isEmpty() {  
    return map.isEmpty();  
    }  
    
     
    //  如果此set包含指定元素,则返回true。 
    //  更确切地讲,当且仅当此set包含一个满足(o==null ? e==null : o.equals(e)) 
    //  的e元素时,返回true。 
    // 
    //  底层实际调用HashMap的containsKey判断是否包含指定key。 
    // @param o 在此set中的存在已得到测试的元素。 
    // @return 如果此set包含指定元素,则返回true。 
      
    public boolean contains(Object o) {  
    return map.containsKey(o);  
    }  
    
    
    // 如果此set中尚未包含指定元素,则添加指定元素。 
    // 更确切地讲,如果此 set 没有包含满足(e==null ? e2==null : e.equals(e2)) 
    // 的元素e2,则向此set 添加指定的元素e。 
    // 如果此set已包含该元素,则该调用不更改set并返回false。 
    // 
    // 底层实际将将该元素作为key放入HashMap。 
    // 由于HashMap的put()方法添加key-value对时,当新放入HashMap的Entry中key 
    //与集合中原有Entry的key相同(hashCode()返回值相等,通过equals比较也返回true), 
    //新添加的Entry的value会将覆盖原来Entry的value,但key不会有任何改变, 
    //  因此如果向HashSet中添加一个已经存在的元素时,新添加的集合元素将不会被放入HashMap中, 
    //  原来的元素也不会有任何改变,这也就满足了Set中元素不重复的特性。 
    //  @param e 将添加到此set中的元素。 
    //  @return 如果此set尚未包含指定元素,则返回true。 
     
    public boolean add(E e) {  
           return map.put(e, PRESENT)==null;  
    }  
    
    //  如果指定元素存在于此set中,则将其移除。 
    //  更确切地讲,如果此set包含一个满足(o==null ? e==null : o.equals(e))的元素e, 
    //  则将其移除。如果此set已包含该元素,则返回true 
    // (或者:如果此set因调用而发生更改,则返回true)。(一旦调用返回,则此set不再包含该元素)。 
    //  
    //  底层实际调用HashMap的remove方法删除指定Entry。 
    //  @param o 如果存在于此set中则需要将其移除的对象。 
    //  @return 如果set包含指定元素,则返回true。 
     
    public boolean remove(Object o) {  
    return map.remove(o)==PRESENT;  
    }  
    
     
    //  从此set中移除所有元素。此调用返回后,该set将为空。 
    //  
    //  底层实际调用HashMap的clear方法清空Entry中所有元素。 
     
    public void clear() {  
    map.clear();  
    }  
     
    //  返回此HashSet实例的浅表副本:并没有复制这些元素本身。 
    
    //  底层实际调用HashMap的clone()方法,获取HashMap的浅表副本,并设置到HashSet中。 
     
    public Object clone() {  
        try {  
            HashSet<E> newSet = (HashSet<E>) super.clone();  
            newSet.map = (HashMap<E, Object>) map.clone();  
            return newSet;  
        } catch (CloneNotSupportedException e) {  
            throw new InternalError();  
        }  
    }  
    }
    

    谢谢,请大家批评指正!

    更多相关内容
  • Vue 中 $set() 与 Vue.set() 原理及使用

    万次阅读 多人点赞 2020-06-14 20:29:12
    可以发现 Vue.set() 和 this.$set() 这两个 api 的实现原理基本一模一样,都是使用了set函数。 set 函数是从 …/observer/index 文件中导出的。 区别: Vue.set( ) 是将 set 函数绑定在 Vue 构造函数上,this.$set...

    1. 前言

    问题: 在使用 vue 进行开发的过程中,可能会遇到一种情况:当生成vue实例后,再次给数据赋值时,有时候并不会自动更新到视图上去。也就是 如果在实例创建之后添加新的属性到实例上,它不会触发视图更新。

    案例:

    <template>
      <div class="home">
        <div>{{student}}</div>
        <div v-for="(item,index) in items" :key="index">{{item}}</div>
    	  <button @click="btn()">修改</button>
      </div>
    </template>
    
    <script>
    export default {
      name: 'Home',
      data(){
        return{
          student:{
            name:'张三',
          },
    	  items:[1, 2, 3],
        }
      },
      methods:{
    	btn(){
          this.student.age = 18;
          this.items[1] = 'two';
    	  console.log(this.student,this.items);
    	}
      }
    }
    </script>
    

    当点击按钮后页面:
    在这里插入图片描述

    当点击按钮后控制台:
    在这里插入图片描述

    原因:
    受 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以 属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的。

    因此:
    Vue 不能检测以下变动的数组:
    当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
    当你修改数组的长度时,例如:vm.items.length = newLength
    eg:
    使用 this.arr[0] 去更新 array 的内容,视图没有刷新
    使用 Vue.set(this.arr, 0, !this.arr[0]) 去更新 array 的内容,视图被刷新
    使用 this.arr[0] = !this.arr[0] 和 this.obj.a = !this.obj.a 同时更新,视图被刷新

    结论:
    如果方法里面单纯的更新数组 Array 的话,要使用 Vue.set();
    如果方法里面同时有数组和对象的更新,直接操作 data 即可;

    2. 原理

    每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
    在这里插入图片描述
    受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 不能检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。

    3. $set() 与 Vue.set() 的使用

    3.1 通过 Vue.set() 改写
    语法:

    Vue.set( target, propertyName/index, value )
    参数:
    {Object | Array} target
    {string | number} propertyName/index
    {any} value
    返回值:设置的值。
    用法:
    向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。
    它必须用于向响应式对象上添加新 property,因为 Vue 无法探测普通的新增 property (比如 this.myObject.newProperty = 'hi')
    注意:
    对象不能是 Vue 实例,或者 Vue 实例的根数据对象。
    
    <template>
      <div class="home">
        <div>{{student}}</div>
        <div v-for="(item,index) in items" :key="index">{{item}}</div>
    	<button @click="btn()">修改</button>
      </div>
    </template>
    
    <script>
    import Vue from 'vue' // 别忘了引入
    export default {
      name: 'Home',
      data(){
        return{
          student:{
            name:'张三',
          },
    	  items:[1, 2, 3],
        }
      },
      methods:{
    	btn(){
          Vue.set(this.student, 'age', 18);
          Vue.set(this.items, 1, 'two');
    	  console.log(this.student,this.items);
    	}
      }
    }
    </script>
    

    当点击按钮后页面:
    在这里插入图片描述
    当点击按钮后控制台:
    在这里插入图片描述

    3.2 通过 $set() 改写
    语法:

    vm.$set( target, propertyName/index, value )
    参数:
    {Object | Array} target
    {string | number} propertyName/index
    {any} value
    返回值:设置的值。
    用法:
    这是全局 Vue.set 的别名。
    参考:Vue.set
    
    <template>
      <div class="home">
        <div>{{student}}</div>
        <div v-for="(item,index) in items" :key="index">
          <span>年龄:{{item.age}}</span>
          <span>地点:{{item.address}}</span>
        </div>
    	<button @click="btn()">修改</button>
      </div>
    </template>
    
    <script>
    export default {
      name: 'Home',
      data(){
        return{
          student:{
            name:'张三',
          },
          items:[
            {
              age:12,
              address:'南京',
            },{
              age:15,
              address:'深圳',
            },{
              age:20,
              address:'上海',
            }
          ]
        }
      },
      methods:{
        btn(){
          this.$set(this.student, 'age', 18);
          this.$set(this.items, 1, {age:22, address:'武汉'});
          console.log(this.student,this.items);
        }
      }
    }
    </script>
    

    当点击按钮后页面:
    在这里插入图片描述

    当点击按钮后控制台:

    在这里插入图片描述

    3.3 Vue.set() 和 this.$set() 的区别

    Vue.set() 源码:

    import { set } from '../observer/index'
    ...
    Vue.set = set
    ...
    
    

    this.$set() 源码

    import { set } from '../observer/index'
    ...
    Vue.prototype.$set = set
    ...
    
    
    可以发现 Vue.set() 和 this.$set() 这两个 api 的实现原理基本一模一样,都是使用了set函数。
    set 函数是从 …/observer/index 文件中导出的。
    区别: Vue.set( ) 是将 set 函数绑定在 Vue 构造函数上,this.$set() 是将 set 函数绑定在 Vue原型上。
    
    展开全文
  • 主要介绍了Java面试之HashSet的实现原理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
  • Vue原理面试

    千次阅读 2021-09-29 15:27:34
    一、大厂必考原理 1.组件化和MVVM 2.响应式原理 3.vdom和diff算法 4.模板编译 5.组件渲染过程 6.前端路由 1.组件化基础=>(MVVM模型) 传统组件,知识静态渲染,更新依赖于操作DOM 数据驱动视图 - Vue ...

    一、大厂必考原理

    1.组件化和MVVM

    2.响应式原理

    3.vdom和diff算法

    4.模板编译

    5.组件渲染过程

    6.前端路由

    1.组件化基础=>(MVVM模型)

     传统组件,知识静态渲染,更新依赖于操作DOM

    Vue的核心理念是数据驱动的理念,所谓的数据驱动的理念:当数据发生变化的时候,用户界面也会发生相应的变化,开发者并不需要手动的去修改dom.

    这样做给我们带来的好处就是,我们不需要在代码中去频繁的操作dom了,这样提高了开发的效率,同时也避免了在操作Dom的时候出现的错误。

    Vue.js的数据驱动是通过MVVM这种框架来实现的,MVVM 框架主要包含三部分:Model,View,ViewMode

    数据驱动视图 - Vue MVVM

    MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到ViewModel层并自动将数据渲染到页面中,视图变化的时候通知viewModel层更新数据


    2.Vue响应式原理的实现

    组件data的数据一旦改变,立马触发视图的更新。

    核心API -- Object.defineProperty

    Object.defineProperty有缺点(Vue3启用Proxy)

    Proxy的兼容性不太好,且无法使用polyfill

    Object.defineProperty基本用法

     Object.defineProperty实现响应式

    • 监听对象,监听数组
    • 复杂对象,深度监听

     Object.defineProperty的缺点

    • 深度监听需要递归到底,一次性计算量大
    • 无法监听新增属性、删除属性(要使用Vue.set  Vue.delete)
    • 无法原生监听数组,需要特殊处理

    总结;

    1.核心API -- Object.defineProperty

    2.如何监听对象(深度监听),如何监听数组

    3.缺点

    // 触发更新视图
    function updateView() {
        console.log('视图更新')
    }
    
    // 重新定义数组原型
    const oldArrayProperty = Array.prototype
    // 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
    const arrProto = Object.create(oldArrayProperty);
    ['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
        arrProto[methodName] = function () {
            updateView() // 触发视图更新
            oldArrayProperty[methodName].call(this, ...arguments)
            // Array.prototype.push.call(this, ...arguments)
        }
    })
    
    // 重新定义属性,监听起来
    function defineReactive(target, key, value) {
        // 深度监听
        observer(value)
    
        // 核心 API
        Object.defineProperty(target, key, {
            get() {
                return value
            },
            set(newValue) {
                if (newValue !== value) {
                    // 深度监听
                    observer(newValue)
    
                    // 设置新值
                    // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
                    value = newValue
    
                    // 触发更新视图
                    updateView()
                }
            }
        })
    }
    
    // 监听对象属性
    function observer(target) {
        if (typeof target !== 'object' || target === null) {
            // 不是对象或数组
            return target
        }
    
        // 污染全局的 Array 原型
        // Array.prototype.push = function () {
        //     updateView()
        //     ...
        // }
    
        if (Array.isArray(target)) {
            target.__proto__ = arrProto
        }
    
        // 重新定义各个属性(for in 也可以遍历数组)
        for (let key in target) {
            defineReactive(target, key, target[key])
        }
    }
    
    // 准备数据
    const data = {
        name: 'zhangsan',
        age: 20,
        info: {
            address: '北京' // 需要深度监听
        },
        nums: [10, 20, 30]
    }
    
    // 监听数据
    observer(data)
    
    // 测试
    // data.name = 'lisi'
    // data.age = 21
    // // console.log('age', data.age)
    // data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
    // delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
    // data.info.address = '上海' // 深度监听
    data.nums.push(4) // 监听数组
    

    3.虚拟DOM(vdom)和diff算法

    • DOM操作非常耗费性能
    • 以前用jQuery,可以自行控制DOM操作时机,手动调整
    • vue和react都是数据驱动试图,如何有效控制DOM操作?

    解决方案——vdom

    • 有一定的复杂度,想减少计算次数比较难
    • 难不能把计算,更多的转移为JS计算?因为JS执行比较快
    • vdom——用JS模拟DOM结构,计算出最小的变更,操作DOM

    面试题:用JS模拟DOM元素

    包含三部分:标签tag,附着在标签上的属性、样式、事件props,子元素children

    通过snabbdom 学习vdom

    • vue3重写了vdom的代码,优化了性能
    • 但vdom的理念不变,面试考点不变

    h函数、vnode数据结构、patch函数

    vdom总结

    • 用js模拟DOM结构(vnode)
    • 新旧vnode对比,得出最小的更新范围,最后更新DOM
    • 数据驱动视图的模式下,有效控制DOM操作

    diff算法

    两个数做diff,如这里的vdom diff

     vnode   ->patch ->new vnode

    树diff的时间复杂度O(n^3)

    • 第一,遍历tree1;第二,遍历tree2
    • 第三,排序
    • 1000个节点,要计算1亿次,算法不可用

    优化时间复杂度到O(n)

    • 只比较同一层级,不跨级比较
    • tag不相同,直接删掉重建,不再深度比较
    • tag和key,两者都相同,则认为是相同的节点,不再深度比较

    diff算法总结

    • patchVnode
    • addVnodes removeVnodes
    • updateChildren(key的重要性)

    vdom和diff总结

    • 细节不重要,updateChildren更新过程也不重要,不要深究
    • vnode核心概念很重要:h vnode patch diff key 等
    • vnode的存在价值更重要:数据驱动试图,控制DOM操作

    4.模板编译

     

    with语法 

     

    模板编译

    总结

    vue中使用render代替template 

    总结

     5.组件渲染更新过程

    vue原理的三大知识点

    组件渲染/更新过程 

    初次渲染过程

    第二步是因为,执行render函数会触发getter操作 

    更新过程

    触发setter,看是修改的data是否在getter中已经被监听,如果是,就执行render函数

    patch的diff算法,会计算出最小差异,更新在DOM上 

    完整流程图

    模板编译完,生成render函数,执行render函数生成vnode (虚拟DOM的树)

    执行render函数的时候会touch getter,即执行函数的时候回触发Data里的getter

    触发的时候就会收集依赖,即在模板中出发了哪个变量的getter就会把哪个给观察起来(watcher)

    在修改Data的时候,看这个Data是否是之前作为依赖被观察起来的

    如果是,就重新出发re-render,重新渲染,重新生成vdom tree,重新touch

    异步渲染

    1.$nextTick:

    vue是异步渲染,$nextTick会待Dom渲染完之后调用

    页面渲染时会将data的修改做整合,多次data修改只会渲染一次

    2.汇总data的修改,一次性更新试图

    3.减少DOM操作次数,提高性能

    6.前端路由原理

    网页url组成部分

     hash的特点

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>hash test</title>
    </head>
    <body>
        <p>hash test</p>
        <button id="btn1">修改 hash</button>
    
        <script>
            // hash 变化,包括:
            // a. JS 修改 url
            // b. 手动修改 url 的 hash
            // c. 浏览器前进、后退
            window.onhashchange = (event) => {
                console.log('old url', event.oldURL)
                console.log('new url', event.newURL)
    
                console.log('hash:', location.hash)
            }
    
            // 页面初次加载,获取 hash
            document.addEventListener('DOMContentLoaded', () => {
                console.log('hash:', location.hash)
            })
    
            // JS 修改 url
            document.getElementById('btn1').addEventListener('click', () => {
                location.href = '#/user'
            })
        </script>
    </body>
    </html>

    H5 history

    • 用url规范的路由,但跳转时不刷新页面
    • history.pushState
    • window.onpopstate

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>history API test</title>
    </head>
    <body>
        <p>history API test</p>
        <button id="btn1">修改 url</button>
    
        <script>
            // 页面初次加载,获取 path
            document.addEventListener('DOMContentLoaded', () => {
                console.log('load', location.pathname)
            })
    
            // 打开一个新的路由
            // 【注意】用 pushState 方式,浏览器不会刷新页面
            document.getElementById('btn1').addEventListener('click', () => {
                const state = { name: 'page1' }
                console.log('切换路由到', 'page1')
                history.pushState(state, '', 'page1') // 重要!!
            })
    
            // 监听浏览器前进、后退
            window.onpopstate = (event) => { // 重要!!
                console.log('onpopstate', event.state, location.pathname)
            }
    
            // 需要 server 端配合,可参考
            // https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90
        </script>
    </body>
    </html>

     总结

     两者选择

    to c的要是不需要管seo、搜索引擎也不需要用H5 history 简单一点就好

    展开全文
  • 主要介绍了Java面试 从源码角度分析HashSet实现原理?,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
  • 前面已经讲了面试必问 Redis 数据结构底层原理String、List篇; 链接如下: https://blog.csdn.net/abu935009066/article/details/110549742 redis版本:6.0.6 Hash 字典 是什么 听名称就知道很像Java中的HashMap,...

    在这里插入图片描述

    前言

    1. 一文干翻Integer、int等基础数据类型和包装类型相关问题
    2. 面试必问 容器 ArrayList
    3. 面试必问 Redis 持久化
    4. 面试必问 Redis 数据结构底层原理一
    5. 面试必问 Redis 数据结构底层原理二
    前面已经讲了面试必问 Redis 数据结构底层原理String、List篇;
    链接如下:
    https://blog.csdn.net/abu935009066/article/details/110549742

    redis版本:6.0.6

    Hash 字典

    是什么

    听名称就知道很像Java中的HashMap,原理也有很多相似之处,但是Reids做了很多优化,Redis的hash的底层存储有两种数据编码,一种是ziplist,另外一种是hashtableziplist之前已经讲过了。

    在这里插入图片描述

    我们来用redis自带的debug命令来看下hash类型的encoding编码字段:

    ziplist

    > hset key4 name Nick
    1
    > debug object key4
    Value at:0x7f21f2eadde0 refcount:1 encoding:ziplist serializedlength:24 lru:13214504 lru_seconds_idle:21
    

    hashtable

    > hset key5 name 01234567890123456789012345678901234567890123456789012345678901234
    1
    > debug object key5
    Value at:0x7f21f2eade00 refcount:1 encoding:hashtable serializedlength:28 lru:13214661 lru_seconds_idle:3
    

    hash类型,会优先使用ziplist编码,当下面任一条件被破坏了才会采用hashtable编码

    • hash对象保存的键和值字符串长度都小于64字节
    • hash对象保存的键值对数量小于512 ziplist

    这两个条件是可以修改的,在redis.conf

    hash-max-ziplist-value 64     #字符串长度都小于64字节
    hash-max-ziplist-entries 512  #元素数量小于512
    

    为什么

    为什么要优先使用ziplist压缩列表?

    • 压缩列表是非常紧凑的数据结构,占用的内存已经压缩的非常小了,可以提高内存的利用率。
    • 压缩列表底层是连续的内存空间,对CPU高速缓存支持更友好,提高查询效率。

    如何实现

    下面来剖析hashtable原理;

    是什么

    其实就是上一篇中的dict,内部有2个hashtable,通车情况下只有一个hashtable是有值的,但是在字典扩容、缩容的时候,需要用的另一个hashtable过渡,进行渐进式搬迁,搬迁完成后,旧的hashtable删除,新的hashtable取而代之。

    typedef struct dict {
        // 包含2个hashtable
        dictht ht[2];
        // ...
    } 
    
    typedef struct dictht {
        // 哈希表数组
        dictEntry **table;
        // ...
    } 
    
    typedef struct dictEntry {
        // 键
        void *key;
        // 值
        void *val;
        // 指向下个哈希表节点,形成链表
        dictEntry *next;
    
    }
    

    结构图如下:

    在这里插入图片描述

    为什么

    为什么内部要用2个hashtable

    核心原理基本上跟Java中HashMap一样,随着对哈希表的操作,键会逐渐增多或减少,也会存在hash冲突,解决版本也是链表法。为了保证查询时间复杂度不会退化为链表,所以也会进行rehash操作。

    与HashMap最大的不同在rehash的方式,Java中的HashMap一次性把所有数据rehash完成,由于rehash是个很耗时的过程,而redis核心处理线程模型又是单线程的,唯恐阻塞其他指令,所以不能像HashMap的一次性处理方式,而选择了渐进式rehash方式。

    如何实现

    渐进式rehash

    为了使rehash操作更高效,Redis默认使用了两个全局哈希表:哈希表1和哈希表2,起始时哈希表2没有分配空间,但是随着数据增多,Redis执行分三步执行rehash;

    • 给哈希表2分配更大的内存空间,如是哈希表1的两倍

    • 把哈希表1中的数据重新映射并拷贝到哈希表2中

    • 释放hash1的空间

      由于步骤2重新映射非常耗时,会阻塞redis,于是把集中迁移数据,改成每处理一个请求时,就从哈希表1中的第一个索引位置, 顺带将这个索引位置上的所有entries拷贝到哈希表2中。

    触发前提

    数据已经达到rehash条件,hash表中的元素等于数组长度时。

    • 被动式:每次客户端发起命令时,做rehash(如hadd、hdel等);

    • 主动式:服务器定时rehash;

    扩容倍数

    这块倒是跟HashMap一样是2倍扩容

    缩容

    当hash表中元素被删除到元素个数小于数组长度的10%,就会去缩容。

    Set 集合

    是什么

    类似于Java中的HashSet,内部实现是一个特殊的字典,字典中所有的value都是null,常被用于去重功能。

    底层采用了intset整数集合和hashtable字典两种方式来实现的,当满足如下两个条件的时候,采用整数集合实现,否则用哈希表。

    • 集合中的所有元素都为整数

    • 集合中的元素个数不大于 512

      可以通过修改 set-max-intset-entries调整集合大小,默认512

        set-max-inset-entries 512
      

      其中hashtablekeyset中元素的值,而valuenullinset为可以理解为数组。

    intset

    > sadd key6 1 2 3
    3
    > debug object key6
    Value at:0x7f21f2eaddd0 refcount:1 encoding:intset serializedlength:15 lru:13223640 lru_seconds_idle:7
    

    hashtable

    > sadd key6 a b c
    3
    > debug object key6
    Value at:0x7f21f2eaddd0 refcount:1 encoding:hashtable serializedlength:13 lru:13223654 lru_seconds_idle:5
    

    为什么

    为什么要优先使用intset整数集合?

    • intset是非常紧凑的数据结构,占用的内存已经压缩的非常小了,可以提高内存的利用率。
    • 查询方式一般采用二分查找法,实际查询复杂度也就在log(n).
    • intset底层是连续的内存空间,对CPU高速缓存支持更友好,提高查询效率。

    如何实现

    typedef struct intset {
        // 编码方式
        uint32_t encoding;
        // 集合包含的元素数量
        uint32_t length;
        // 保存元素的数组
        int8_t contents[];
    } 
    

    在这里插入图片描述

    intset底层实现为有序无重复数组保存集合元素。 intset这个结构里的整数数组的类型可以是16位的,32位的,64位的。如果数组里所有的整数都是16位长度的,如果新加入一个32位的整数,那么整个16的数组将升级成一个32位的数组。升级可以提升intset的灵活性,又可以节约内存,但不可逆。

    ZSet 有序集合

    是什么

    类似Java中的TreeMap,用set保证value一致性,给每个value一个score属性代表排序权重。
    在这里插入图片描述

    zset有序,自动去重的集合数据类型,其底层实现为 ziplist + skiplist跳跃表,当数据比较少的时候用ziplist编码结构存储,否则改为跳跃表。

    默认按照score排序,score相同则按照元素字典序

    同时满足以下两个条件采用ziplist存储:

    • 有序集合保存的元素数量小于默认值128个
    • 有序集合保存的所有元素的长度小于默认值64字节

    可以通过修改redis.conf修改默认值

    zset-max-ziplist-entries 128 #配置元素个数最多512个
    zset-max-ziplist-value 64 #配置value最大为64字节
    

    ziplist

    > zadd key7 1 a 2 b
    2
    > debug object key7
    Value at:0x7f21f2eade10 refcount:1 encoding:ziplist serializedlength:22 lru:13224623 lru_seconds_idle:6
    

    skiplist

    > zadd key7 1 a 2 b67777777777777777777777777777777777777777777777777777777777777777777777777777777
    1
    > debug object key7
    Value at:0x7f21f2eade10 refcount:1 encoding:skiplist serializedlength:43 lru:13224690 lru_seconds_idle:2
    

    怎么实现

    ziplist,之前讲不过,zskiplist编码其实就是zset

    typedef struct zset {    
        // 字典,键为成员,值为分值    
        // 用于支持 O(1) 复杂度的按成员取分值操作    
        dict *dict;    
        // 跳跃表,按分值排序成员    
        // 用于支持平均复杂度为 O(log N) 的按分值定位成员操作    
        // 以及范围操作    
        zskiplist *zsl;
    }
    
    typedef struct zskiplist {
         // 表头节点和表尾节点
        struct zskiplistNode *header, *tail;
        // 表中节点的数量
        unsigned long length;
        // 表中层数最大的节点的层数
        int level;
     } zskiplist;
    typedef struct zskiplistNode {
        // 成员对象
        robj *obj;
        // 分值
        double score;
         // 后退指针
        struct zskiplistNode *backward;
        // 层
        struct zskiplistLevel {
            // 前进指针
            struct zskiplistNode *forward;
             // 跨度---前进指针所指向节点与当前节点的距离
            unsigned int span;
        } level[];
    } zskiplistNode;
    

    zskiplist编码分为两部分,dict+ zskiplistdict跳跃表都存储数据,实际上 dict 和跳跃表最终使用指针都指向了同一份数据,即数据是被两部分共享的,dict结构,主要key是其集合元素,而value就是对应分值,而zkiplist作为跳跃表,按照分值排序,方便定位成员。

    盗个图:

    在这里插入图片描述

    来源:https://juejin.cn/post/6863258283483807752

    skiplist的查找时间复杂度是 O(log N),可以和平衡二叉树相当,但实现起来又比它简单。

    为什么

    这里redis为什么用跳表而不像Java的TreeMap一样用红黑树呢?

    对于这个问题,Redis的作者 @antirez 是怎么说的:

    There are a few reasons:

    1) They are not very memory intensive. It’s up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.

    2) A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.

    3) They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.

    简单概括下

    • 内存占用相比于树更少,跳跃表使用的指针比树更少,64位操作系统每个指针占用8字节
    • 跳跃表范围查询支持更友好
    • 算法实现难度上相比于树实现起来更简单

    总结

    redis涉及的常用基本数据结构和相关底层实现数据编码全部介绍完了,下面总结一下:

    数据结构底层实现(数据编码)
    stringint、raw、embstr
    listquicklist
    setintset/hashtable
    hashziplist/hashtable
    zsetziplist/skiplist

    底层实现(数据编码介绍):

    • int:其实就是long型
    • ziplist(压缩列表):内存地址连续,元素之间紧凑存储,功能类似链表的一种数据结构。
    • quicklist(快速列表):是ziplist和linkedlist的结合。
    • intset(整数集合):用有序无重复数组保存集合元素。
    • hashtable(字典或者也叫哈希表):就是dict,里面有个hashtable数组ht[0]、ht[1],用于渐进式rehash,可以看做升级版的HashMap
    • skiplist(跳跃表):查找时间复杂度是 O(log N),可以和平衡二叉树相当,但实现起来又比它简单。

    参考:

    • https://juejin.cn/post/6863258283483807752
    • 《Redis深度历险 核心原理与应用实践》

    QQ群【837324215
    关注我的公众号【Java大厂面试官】,回复:架构资源等关键词(更多关键词,关注后注意提示信息)获取更多免费资料。

    公众号也会持续输出高质量文章,和大家共同进步。

    展开全文
  • 简答 1.数据库管理系统的主要功能有哪些 ? 答:数据库定义、操纵、保护、存储、维护和数据字典。 2.数据库系统中的常见故障有哪些 ? 答: .事务故障,系统故障、介质故障。 3.简述 SQL 语言的组成。 答:分为四个...
  • Set不重复是基于什么原理? ....... 2. 题目剖析 如果你看过 壹哥 之前关于List的面试文章,当看到今天的题目时,你起码可以准确说出部分答案。我前面跟大家特别强调过,我们出去面试时,Java中的集合部分,是...
  • ES6——之数据存储结构 Set、Map基本使用及实现原理 使用时必须要new: 1、Set:是ES6提供给我们的构造函数,能构造出一种新的储存数据的结构; 特点:只有属性值,成员值唯一(不重复) 用途:可以转换成数组,进行...
  • ArrayList实现原理要点概括参考文献: http://zhangshixi.iteye.com/blog/674856l https://www.cnblogs.com/leesf456/p/5308358.html ArrayList是List接口的可变数组非同步实现,并允许包括null在内的所有元素。...
  • 计算机组成原理复习 --- 全面版

    万次阅读 多人点赞 2018-01-08 16:14:36
    设Cache中共有m个块,在采用组相联映像方式时,将m个Cache块分成u组(set),每组k个块(即m=u×k),组间直接映像,而组内全相联映像。所谓组间直接映像,是指某组中的Cache块只能与固定的一些主存块建立映像关系。 ...
  • Vue面试-Vue响应式原理

    千次阅读 2021-08-15 18:47:18
    coderwhy Vue面试-vue响应式原理笔记
  • 当然,上面说的情况也可
以通过再开一个数组进行下标和元素的对应来解决,但是set提供了更为直观的接口,并且加
入set之后可以实现自动排序,因此熟练使用set可以在做某些时减少思维量。 如果要使用set,需要添加...
  • Mybatis工作原理,常见面试解析

    千次阅读 2020-09-02 14:39:35
    号,调用 PreparedStatement 的 set 方法来赋值; Mybatis 在处理 时,就是把{}时,就是把时,就是把{}替换成变量的值。 使用#{}可以有效的防止 SQL 注入,提高系统安全性 7、当实体类中的属性名和表中的字段名不...
  • 计算机组成原理常考大合集

    千次阅读 多人点赞 2020-08-15 14:22:21
    计算机组成原理常考大合集1.在“Cache-主存-辅存”三级存储体系中,“Cache-主存”结构与“主存-辅存”结构的引入为了解决什么问题?答案扩展2.在CPU中,那些寄存器属于控制用指令部件,它们各起什么作用?答案...
  • 一、谈一下你对MVVM原理的理解 1.MVC是Model-View- Controller的简写。即模型-视图-控制器。是最早的架构模型,是从前台到后台统一称之为MVC,比如前端交视图层(View),后端的数据库(Model),用户操作界面想...
  • 常见的Java笔试 JUC、JMM核心知识点笔记 尚硅谷周阳老师课程——笔记。 / / JUC知识点 JMM volatile关键字 可见性 原子性 有序性 哪些地方用到过volatile? 单例模式的安全问题 CAS CAS底层原理 CAS缺点 ABA问题 ...
  • Vue双向数据绑定的原理 1.Vue双向数据绑定是通过 数据劫持 结合 发布订阅者 的方式来实现的 也就是说数据和视图一起更新 数据发生变化 视图也发生变化 视图发生变化 数据也发生变化 2.关于双向数据绑定 其核心是...
  • map和set的使用和原理

    千次阅读 2017-02-16 13:21:46
    在《C++Primer》中列举了标准库中的8个关联容器,如下:关联容器支持高效的关键字查找和访问,我们在这里介绍两个主要的关联容器set和map。mapmap里面存的是一些key-value对,其中key起到索引的作用, 而
  • 数据库原理习题

    万次阅读 多人点赞 2020-09-07 18:39:47
    数据库原理是每个计算机专业的学生必须掌握的课程之一,所以学好数据库原理对日后实际工作和项目十分重要。这篇博客通过总结广州大学数据库原理课程和教材中的例题,希望能够对数据库原理加深理解。
  • Set Sorted set 二、各种数据类型应用和实现方式 1、String String 数据结构是简单的key-value类型,value其实不仅是String,也可以是数字。 常用命令:get、set、incr、decr、mget等。 应用场景:String是最常用的...
  • 【最全】《数据库原理及应用》知识点整理+习题

    万次阅读 多人点赞 2020-05-26 10:57:22
    第11章 并发控制概述 封锁 活锁和死锁 并发调度的可串行性 两段锁协议 习题 选择 简答 应用 第1章 绪论 1.1术语 1.数据(Data) 数据是数据库中存储的基本对象。定义为描述事务的符号记录。数据的含义称为语义...
  • 交通大学数据库原理答案,第三章参考答案,一块学习,一块进步
  • 数据库原理及应用教程 第4版|微课版答案 陈志泊主编第一章一、选择二、填空第二章一、选择二、填空第三章一、选择二、填空第四章一、选择二、填空第五章一、选择二、填空第六章一、选择二、...
  • 数据库系统原理简答

    万次阅读 多人点赞 2020-08-11 17:27:11
    1、 2、 3、 4、 5、 6、 7、 8、 9、 10、 11、 12、 13、 14、 15、 16、 17、 18、 19、 20、 21、 22、 23、 24、 25、 26、 27、 28、 29、 30、 31、 32、 33、 34、 35、 36、 37、 38、 ...6
  • 帆软报表学习习题+经典案例 1.基本原理理解2.多源报表3.动态隔间运算4.图表5,参数联动,6超级链接,8填报报表9.填报报表检验10.移动端预览分页、表头固定 还有一些常用美化报表、排序,占比,校验等报表的小案例
  • 数据库原理及应用教程课后习题参考答案

    万次阅读 多人点赞 2019-08-22 15:03:14
    第1章习题参考答案 一、选择 C 2. B 3. D 4. C 5. D B 7. A 8. B 9. D 10. B C 12. D 13. D 14. D 15. B C 17. D 18. A 19. D 20. A D 22. D 23. C 24. A ...
  • 面试:vue双向绑定原理

    千次阅读 2020-04-01 20:05:44
    引言:vue的双向绑定原理简单来说就是:vue内部使用object.defineProperty方法给所有数据加上getter和setter方法,在数据发生改变时发布消息给订阅者Watcher,触发响应的监听回调。 object .defineProperty是JS里一...
  • Redis分布式锁原理 分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的...
  • 数据库原理及应用教程 第4版 微课版 陈志泊主编 习题参考答案

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 102,356
精华内容 40,942
关键字:

set 原理题