Bootstrap

Java有关面试题

1、redis相关面试题。

(1)redis数据类型用过哪些?

String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(sort set有序集合)。
String(字符串)

  • 常用命令: set,get,decr,incr,mget 等。
  • String类型时二进制安全的,也就是说String包含了任何类型,如int、序列化对象、jpg等。最大存储为512MB。

Hash(哈希)

  • 常用命令:hget,hset,hgetall 等。
  • Hash是键值对(key-value)集合,特别适合存储对象。

List(列表)

  • 常用命令:lpush(添加头部),rpush(添加尾部),lpop,rpop,lrange key start stop(获取指定范围的元素)等。
  • List是简单的字符串列表,按照插入的顺序排序,可以添加一个元素到列表头部(lpush)或尾部(rpush)。

Set(集合)

  • 常用命令:sadd,spop,smembers,sunion 等。
  • Set是String类型的无序集合,通过哈希表实现,其添加、删除、查找复杂度都是O(1)。

Zset(sort set有序集合)

  • 常用命令:zadd,zrange,zrem,zcard等。
  • Zset是String类型有序集合,允许键key重复,但不允许成员value重复(后面添加的成员元素与集合中的相同,则替换集合中的元素)。每个元素都会关联一个double类型的score,通过score来为成员的元素从小到大排序。

(2)追问1:

  • 缓存时缓存的是什么东西?
    答 主要是缓存从数去库中查询的数据,第二次查询时直接获取redis缓存的数据。
  • 怎么保证缓存与数据库的一致性?
    答:1.读写分离,即读只访问缓存,写只访问数据库,若更新数据库成功,写入缓存失败,则进行数据回滚。2.队列存储请求,将读或写操作放入队列中,直到完成该操作才执行下一个读写操作。3.设置定时缓存同步。

(3)追问2:

Hash这个数据类型有用过吗?它是怎么扩容的?

(4)追问3:

  • Hash冲突是怎么解决的?
    答:通过链表解决hash冲突,数组类型为链表,即相同的Hash码存入到相同数组下标中链表的尾部。
    在这里插入图片描述
  • 哈希码是如何实现的?
    哈希码是哈希表的下标,通过算法计算出字符串的哈希码(HashCode),该算法不唯一。
    如:算出字符串每个字符的ASCII码,然后进行相加再取模,得出哈希表的下标(Hashcode)。在这里插入图片描述
    通过获取key的hashCode,并对其取模,发现其恒定不变:
public class Main extends HashMap {

    public static void main(String args[]) {
        Main map = new Main();
        map.put("zs", "zs1");
        map.put("ls", "ls1");
        map.put("ww", "ww1");
        map.put("abcd", "abcd1");
    }

    static void put(String key, String val) {
        System.out.println(key + "-" + val + "(哈希码:" + key.hashCode() 
        + ",数组下标:" + Math.abs(key.hashCode() % 15) + ")");
    }
}

以上代码输出如下:
在这里插入图片描述

  • put操作最终保存的是什么值?
    当进行put操作时,会将要put的值放入对应下标数组的链表中,存放的值有:key、value、hash、next,如下所示:
    在这里插入图片描述
  • get操作:
    先是计算出key的哈希值,然后在再通过哈希值算出数组下标,然后在该数组下标遍历查找链表中的元素,比较hash和key是否相等。

2、HashMap相关面试题:

(1)模拟源码实现HashMap。

Map接口代码如下:

public interface Map<K, V> {
    // 存
    V put(K key, V value);
    // 取
    V get(K key);
    // 长度
    int size();

    interface Node<K, V> {
        K getKey();
        V getValue();
    }
}

HashMap接口代码如下:

public class HashMap<K, V> implements Map<K, V> {

    private Node<K, V>[] hashTable = null;
    private int size;

    public HashMap() {
        hashTable = new Node[16];
    }

    /**
     * 1.通过key进行hs算法得出hashCode
     * 2.通过hashCode算出数组下标
     * 3.判断该数组下标元素是否为空,不为空直接存储
     * 4.若不为空进行链表存储,返回
     * @param key
     * @param value
     * @return
     */
    @Override
    public V put(K key, V value) {
        int hashCode = key.hashCode();
        // 获取hash下标
        int index = getHash(hashCode);
        // 获取该下标的链表
        Node<K, V> linked = hashTable[index];
        if (linked == null) {
            // 直接存储
            hashTable[index] = new Node<>(key, value, hashCode, null);
            size++;
        } else {
            // 头插法,将新元素插入到原来链表的头部
            hashTable[index] = new Node<>(key, value, hashCode, linked);
        }
        return hashTable[index].getValue();
    }

    /**
     * 通过key获取hashCode,进行取模获取数组下标
     * @param hashCode
     * @return
     */
    private int getHash(int hashCode) {
        int index = hashCode % 16;
        return index >= 0 ? index : -index;
    }

    /**
     * 1.获取key的hashCode,并通过hashCode获取数组下标
     * 2.通过下标获取对应的链表
     * 3.循环遍历链表,若key和hashCode相等,返回value,否则返回null
     * @param key
     * @return
     */
    @Override
    public V get(K key) {
        if (size() == 0)
            return null;
        int hashCode = key.hashCode();
        // 获取hash下标
        int index = getHash(hashCode);
        // 获取该下标的链表,并查找链表中对应的value
        return getValue(hashTable[index], key, hashCode);
    }

    private V getValue(Node<K, V> linked, K key, int hashCode) {
        // 若key相等且hashCode相等
        if (key.equals(linked.getKey()) && linked.getHash() == hashCode) {
            return linked.getValue();
        }
        // 若链表的下一个节点不为null,递归遍历
        if (linked.next != null)
            return getValue(linked.next, key, hashCode);
        return null;
    }

    @Override
    public int size() {
        return size;
    }

    class Node<K, V> implements Map.Node<K, V> {

        K key;
        V value;
        int hash;
        Node<K, V> next;

        public Node(K key, V value, int hash, Node<K, V> next) {
            this.key = key;
            this.value = value;
            this.hash = hash;
            this.next = next;
        }

        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return value;
        }

        public int getHash() {
            return hash;
        }
    }
}

代码测试:

public class Test {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        map.put("zs", "18");
        map.put("ls", "19");
        map.put("ww", "20");
        map.put("zl", "21");
        System.out.println(map.get("zs"));
        System.out.println(map.get("ls"));
        System.out.println(map.get("ww"));
        System.out.println(map.get("zl"));
    }
}

输出结果如下:
在这里插入图片描述

(2)为什么HashMap要用红黑树

因为链表插入删除效率高,查找元素慢,所以需要将链表构建成红黑树来提高查询效率。
当阈值(TREEIFY_THRESHOLD)值大于8时,采用红黑树,否则采用普通方法(数组和链表方式)。

红黑树定义:

  1. 节点是红色或者黑色。
  2. 根节点是黑色。
  3. 每个叶子节点都是黑色的空节点。
  4. 每个红色节点的两个子节点都是黑色(不能有两个连续的红色节点)。
  5. 任意每个节点到叶子节点路径所包含相同数量的黑色节点。

除此之外,最长路径不能超过最短路径2倍。
在这里插入图片描述
在每次插入或删除的时候可能会打乱原来红黑树的结构,所以需要进行调整,调整的方法有:左旋转、右旋转、节点变色。这个过程中比较消耗性能的,所以需要结合链表结构进行操作。
如图为左旋转:
在这里插入图片描述
如图为右旋转:
在这里插入图片描述

(3)说说HashMap、TreeMap和LinkedHashMap的区别。

HashMap:数组 + 链表(单向) + 红黑树。实现了Map接口。非线程安全,且不保持元素顺序。
TreeMap:数组 + 红黑树。非线程安全,所有元素都保持着固定的顺序。
LinkedHashMap:数组 + 链表(双向) + 红黑树。非线程安全,实现了Map接口并继承了HashMap,保证了插入元素的有序。

(4)ConcurrentHashMap和HashTable的区别。

参考链接:HashMap、ConcurrentHashMap和HashTable的区别

  • ConcurrentHashMap:线程安全,采用分段锁的方式,不会造成线程堵塞、效率高,但是不保证操作原子性。
  • HashTable:线程安全,采用syncronized同步锁方式,会造成线程堵塞,效率低

3、TCP/IP传输协议

1.TCP/IP三次握手、四次挥手

描述:

  • TCP/IP是计算机网络需要遵守的网络传输控制协议,使得不同的计算机按照一定的规则进行通信。
  • TCP/IP协议定义了一个在因特网上传输的包,称为IP数据包,它由首部报头和数据两部分组成。

为什么是三次握手?而不是两次?四次?

  • 三次握手是在安全可靠的基础上,握手次数最少的方案。
  • 两次握手并不能保证安全可靠,四次握手会、效率较低,在考虑高安全的情况下,可以有N次握手的协议。
TCP/IP连接的精髓

TCP连接的一方A,由操作系统动态随机选取一个32位长的序列号(InitialSequence Number),假设A的初始序列号为1000,以该序列号为原点,对自己将要发送的每个字节的数据进行编号,1001,1002,1003…,并把自己的初始序列号ISN告诉B,让B有一个思想准备,什么样编号的数据是合法的,什么编号是非法的,比如编号900就是非法的,同时B还可以对A每一个编号的字节数据进行确认。如果A收到B确认编号为2001,则意味着字节编号为1001-2000,共1000个字节已经安全到达。同理B也是类似操作。
参考连接:https://www.zhihu.com/question/24853633

在这里插入图片描述

  • 首部IP报头:包含源IP地址、目的IP地址、数据包长度、IP版本号等。
  • 数据:保存了TCP、UDP、ICMP等传输层报文。

TCP报文:

在这里插入图片描述
TCP报文由首部和数据部分组成,其中首部主要的描述如下:

  • 序号:又称seq序号,占32bit,TCP连接传送的数据流中每一个字节都有一个序号。
  • 确认号:通讯的任何一方在收到对方的报文后,都需要发送一个对应的报文表示确认收到,其中就包含了确认号,表示收到对方的下一个报文段的序号值。如何标识是一个确认报文,需要关注6个标志位,,主要关注3个标志位(ACK、SYN、FIN),次要3个标志位(RST复位标志、URG紧急标志、PSH推送标志)。
    • ACK确认标志(Acknowledgement):当ACK = 1时确认号字段才会有效。当ACK = 0时,确认号无效。
    • SYN同步标志(Synchronize):设置SYN = 1,标识连接请求或连接接受报文。
    • FIN结束标志(Finish):释放一个连接,当FIN = 1时,表示该报文段发送端的数据已发送完毕。

(1)三次报文握手:一次握手,交换三次报文。

  • 第一次握手:客户端向服务端发送一个连接请求,设置SYN = 1(表示连接请求),发送自己的序列号seq = x。
  • 第二次握手:经客户端的发送后,服务端返回发送一个确认报文,表示收到。设置ACK = 1,接着发送一个确认号ACKnum,该确认号表示期望收到对方下一个报文段数据的第一个字节序号,确认号为ACKnum = x + 1,+ 1表示收到FIN的标识。还需要发送SYN = 1表示连接请求,以及自己的序列号seq = y。
  • 第三次握手:向服务端发送一个确认报文ACK = 1,以及确认号ACKnum = y + 1,这样就完成了TCP的三次握手。

总结:

  • 第一次(Client - Server):SYN = 1,seq = x。
  • 第二次(Server - Client):SYN = 1,ACK = 1,ACKnum = x + 1,seq = y。
  • 第三次(Client - Server):ACK = 1,ACKnum = y + 1。

在这里插入图片描述
以上三次握手可以想象为平常生活中家人喊吃饭:

  • 第一次:家人喊,小明啊,小明…
  • 第二次:小明回应,干嘛呢?
  • 第三次:家人说,快点过来吃饭。结束,过去吃饭。

(2)四次挥手:TCP连接的释放

  • 第一次挥手:客户端发送一个FIN = 1的包,请求释放报文,seq = x。
  • 第二次挥手:服务端确认客户端的FIN包,表明收到客户端的关闭请求,同时发送一个确认报文,ACK = 1,ACKnum = x + 1。这时还没准备好关闭连接。
  • 第三次挥手:当做好准备关闭连接时,向客户端发送结束连接请求FIN = 1,seq = y。
  • 第四次挥手:客户端收到服务端的关闭请求,发送一个确认包ACK = 1,ACKnum = y + 1。服务器关闭连接,进入CLOSED状态,服务端将不再回应客户端。服务端等待某个固定时间后自行关闭,进入CLOSED状态。
    在这里插入图片描述

2.OSI模型和TCP/IP五(四)层模型

在这里插入图片描述

(1)OSI七层模型

在这里插入图片描述

  1. 应用层:针对你特定的应用协议。
  2. 表示层:设备固定的数据格式和网络标准数据格式之间的转化
  3. 会话层:通信管理,负责建立和单开通信连接,管理传输层 以下分层
  4. 传输层:管理两个节点之间的数据传递。负责可靠传输。
  5. 网络层:地址管理和路由选择。
  6. 数据链接层:互联设备之间传送和识别数据帧。
  7. 物理层:界定连接器和网线之间的规格。

OSI的7层协议体系结构的概念很清楚,理论也比较完整,但是即复杂又不实用。

(2)TCP/IP五(四)层结构

TCP/IP四层结构从实质上来讲,TCP/IP只有上面三层,因为最下面的网络接口层没有什么具体内容。因此通常建议采用TCP/IP五层结构来学习。

  1. 应用层:负责应用进程之间通信。
  2. 传输层:两台主机之间的数据交换服务。
  3. 网络层:负责地址管理和路由选择。
  4. 数据链路层:设备之间数据帧的传输和识别,将网络层交下来的IP数据组装成帧。
  5. 物理层:光电信号传递方式,以比特为单位的数据传输。

4、docker网络模型

参考链接:https://zhuanlan.zhihu.com/p/98788162

(1)Bridge模式

docker默认的网络模式,为容器创建独立的网络命名空间。当docker进程启动时,会在主机上创建一个docker0虚拟网桥,主机上创建的容器就会链接到这个虚拟网桥上。

(2)None模式

为容器创建独立的网络命名空间,但不分配任何网络配置。也就是说容器没有网卡、IP、路由等,需要自己手动添加。

(3)Host模式

容器和宿主机使用同一个网络。

(4)Container模式

指定新创建的容器和已经存在的一个容器共享一个网络。

(5)跨主机通信(Pipework)

Pipework是一个docker容器网络配置工具。使用新建的bri0网桥代替缺省的docker0网桥。bri0网桥与缺省的docker0网桥的区别:bri0和主机eth0之间是weth pair。

5、浅拷贝和深拷贝

参考链接:https://www.jianshu.com/p/94dbef2de298

  • 浅拷贝:
    • 浅拷贝是按位拷贝对象,它会创建一个新对象。
    • 拷贝的对象引用指向原对象的引用,当拷贝对象的值修改时,原对象的值也会被修改。
  • 深拷贝:
    • 不仅会创建对象,还会开辟一个独立的内存空间,实现真正的内容拷贝。

6.布隆过滤器

参考链接:https://zhuanlan.zhihu.com/p/94433082

  • 基于哈希表的实现,传统的哈希表为单函数存储,而布隆过滤器是哈希表的多函数存储,即每次计算哈希值的方式不一样,这样会导致查询时存在一定误区。
  • 由单个m长度的向量表(包含0和1)组成,默认为0。通过k个函数来计算哈希值,将向量表中置为1。
  • 通过函数计算哈希值,只要哈希值中存在0,表示不存在,若全为1,表示存在。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;