• Java基础


    1. Java基础

    Java 基础

    《Java编程思想》、《疯狂Java:突破程序员基本功的16课.修订版》,这里省略了一些非常基础的知识点

    请你解释为什么会出现 4.0 - 3.6=0.40000001 这种现象

    二进制小数无法精确表达十进制小数,计算机在计算十进制小数的过程中要先转换为二进制进行计算,这个过程中出现了误差

    请你讲讲一个十进制的数在内存中是怎么存的?

    以二进制补码形式存储,最高位是符号位,正数的补码是它的原码,负数的补码是它的反码加1,在求反码时符号位不变,其他取反,1表示负数,0正数

    接口和抽象类的区别是什么 ?

    接口中的所有方法隐含的都是抽象的,而抽象类则可以同时包含抽象和非抽象方法

    类可以实现很多个接口,但只能继承一个抽象类

    类可以不实现抽象类和接口声明的所有方法,但这种情况下,该类必须声明成抽象的

    接口中的成员函数默认是public的。抽象类的成员函数可以是private,protected或者是public。

    JDK8后,接口中可以包含default方法,抽象类中不可以

    面向对象开发的六个基本原则,在项目中用过哪些原则

    • 六个基本原则
      • 单一原则

        一个类只做它该做的事情(高内聚)。在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一原则
      • 开闭原则

        软件实体应当对扩展开发,对修改关闭,要做到开闭有两个要点:
        1. 抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点
        2. 封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变的复杂而繁乱
      • 里氏替换原则

        任何时候都可以用子类型替换掉父类型,子类一定是增加父类的能力而不是减少父类的能力
      • 依赖倒置原则

        面向接口编程。高层模块不应该依赖底层模块,两者都应该依赖其抽象,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代。
      • 接口隔离原则

        类间的依赖关系应该建立在最小的接口上,不能大而全,接口表示能力,一个接口只应该描述一种能力,接口也应该高度内聚
      • 迪米特法则

        由叫最少知识原则,一个对象应该对其他对象有尽可能少的了解
    • 根据自己的项目来说

      详细的可以看这里:https://www.cnblogs.com/qifengshi/p/5709594.html

    关于网络方面的问题,面试官应该只会问一题,不会多问

    HTTP请求的GET与POST方式的区别

    1. GET在浏览器回退是无害的,而POST会再次提交请求
    2. GET请求会被浏览器主动cache,而POST不会,除非手动设置
    3. GET请求只能进行URL编码,而POST支持多种编码
    4. GET请求参数会被完整保留在浏览器历史记录中,而POST中的参数不会被保留
    5. GET请求在URL中传送参数是有大小限制的,不能大于2KB,而POST可以说没有
    6. GET只接受ASCII字符,而POST没有限制
    7. GET参数直接暴露在URL上,而POST将数据放在request body中

    TCP 三次握手,四次挥手

    可见该文:https://blog.csdn.net/qq_38950316/article/details/81087809

    TCP 和 UDP区别

    • 区别:

      UDP是无连接的,即发送数据之前不需要建立连接

      UDP使用尽最大努力交付,即不保证可靠交付,同时也不使用拥塞控制

      UDP是面向报文的,没有拥塞控制,适合多媒体通信要求

      UDP支持一对一,一对多,多对一和多对多的交互通信

      UDP首部开销小,只有8个字节

      TCP是面向连接的运输层协议

      TCP只能一对一连接

      TCP提供可靠的交付服务,提供全双工通信

      TCP 面向字节流,头部最低20个字节

    从输入网址到获取页面的过程

    • 查询DNS, 获取域名对应的IP地址
      • 浏览器搜索自身的DNS缓存
      • 搜索操作系统的DNS缓存
      • 读取本地的HOST文件
      • 发起一个DNS系统调用(宽带运营服务器查看本身缓存,运营服务器发起一个迭代DNS解析请求)
    • 浏览器获得域名对应的IP地址后,发起HTTP三次握手
    • TCP/IP建立连接后,浏览器可以向服务器发送HTTP请求了
    • 服务器接收到请求后,根据路径参数,经过后端处理将页面返回给浏览器
    • 浏览器渲染页面,和外部资源,最终将完整的页面呈现给用户

    Session与Cookie区别

    Session:

    服务器端会为每个访问服务端的请求分配一个会话Session,其数据存储在服务器端,不依赖浏览器端环境,因此高效安全

    Cookie:

    数据已文件形式存在用户浏览器端,用户可以通过浏览器禁用Cookie,用户可以对Cookie进行查看,修改,和删除

    列出自己常用的JDK包

    常用的包:

    java.lang 包装类,线程等都在该包

    java.match 有BigDecimal 精确数字类型

    java.util 并发,集合等都在该包内

    equals与==的区别

    1. equals 比较两个实体值是否相同,可以被覆盖,但需要遵循几个约定:

      自反性:对于任何非null的引用值x, x.equals(x)必须返回true

      对称性:对于任何非null的引用值x和y,当y.equals(x)返回true时,x.equlas(y)必须返回true

      传递性:对于任何非null的引用值x、y、z,如果x.equals(y)返回true,并且y.equals(x)也返回true,那么x.equals(z)也必须返回true

      一致性:对于任何非null的引用值x和y,只要比较对象中的所有信息没有被修改,多次调用equals一致返回true,或者false
    2. == 比较两个实体的引用地址是否相等,不能覆盖,如果引用地址相等,那认为两个实体为同一个实体

    hashCode和equals方法的区别与联系

    对于覆盖了equals方法的类中,同样也要覆盖hashCode方法,如果不这样该类在散列集合中会有问题,比如HashMap中,计算key的索引位置,会调用到key.hashCode

    什么是Java序列化和反序列化,如何实现Java序列化?或者请解释Serializable 接口的作用

    序列化是一种用来处理对象流的机制,也就是将对象的内容转化成二进制流,可以将对象持久化或者网络传输

    反序列化是将二进制流还原为对象的过程

    实现Java序列化,通过实现Serializable即可

    Object类中常见的方法,为什么wait notify会放在Object里边?

    因为Java提供的锁是对象级的,每个对象都有对象头,用来存储锁

    解释一下extends 和super泛型限定符

    称为 上界限定符,list只能get,不能add(除了add null值),通常用于读 称为 下界限定符,list只能add,不能get(只能用Object接收),通过用于写 ### 请列举你所知道的Object类的方法并简要说明 Object()默认构造方法 clone()创建并返回此对象的一个副本 equals(Object obj) 当前对象是否与obj对象相同 finalize()当垃圾收集器确定该对象可以回收时,由垃圾收集器调用此方法 getClass返回一个对象的运行时类 hashCode()返回该对象的哈希码值 notify()唤醒此对象监视器上等待的单个线程 notifyAll()唤醒在此对象监视器上等待的所有线程 toString()返回该对象的字符串表示 wait()使当前线程等待,直到其他线程调用此对象的notify()或者notifyAll()方法 wait(long timeout)导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。wait(long timeout, int nanos) 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。 ### 创建线程的方式 1. 继承Thread类创建线程,并重写run方法,调用实例对象的start()方法启动线程 2. 实现Runnable接口,并实现run方法,将实现Runnable的类传入Thread构造函数中,并调用Thread实例对象的start方法启动线程 3. 实现Callable接口,并实现call方法,创建Callable实现类的实例,使用FutureTask包装Callable对象,使用FutureTask对象传入Thread中,调用start方法启动线程,使用FutureTask对象的get方法获取线程的返回值 ### ArrayList 与 LinkedList 区别 ArrayList 是一种顺序存储的线性表,底层使用数组实现 LinkedList是一种链式存储的线性表,本质是一个双向链表,实现了List、Deque接口,可以当成双向链表、队列、栈使用 ### 自定义注解 1. 声明注解的保留期限类型
    @Retention(RetentionPolicy.RUNTIME)表示该注解可以在运行期保留
    保留期限类型:java.lang.annotation.Retention
    SOURCE: 注解信息仅保留在目标类源代码文件中,对应的字节码文件不会保留
    CLASS: 注解信息存在于源代码、字节码文件中,但运行期JVM不能获得该注解信息
    RUNTIME: 注解信息存在于源代码、字节码文件、运行期JVM中,能够通过反射机制获取注解类信息 2. 声明注解的可以使用的目标类型
    [@Target(ElementType.METHOD) ](/ElementType.METHOD) 表示这个注解只能在方法上使用
    目标类型:java.lang.annotation.ElementType
    TYPE: 类、接口、注解类、Enum
    FIELD: 类成员变量或常量
    METHOD: 方法
    PARAMETER: 参数
    CONSTRUCTOR: 构造器
    LOCAL_VARIABLE: 局部变量
    ANNOTATION_TYPE: 注解
    PACKAGE: 包 3. 使用[@interface ]() 修饰类 4. 声明注解成员
    成员无入参、不能抛出异常;
    可以通过default成员指定默认值
    成员类型只能使用基本数据类型、String、Class、enums、注解类型,及上述类型的数组类型。如ForumService value()是非法的
    如果注解只有一个成员,则成员名必须取名为value(),再使用时可以忽略成员名和赋值号,如果注解类拥有多个成员时,
    对value成员赋值,可以省略value和赋值号,如果是多个成员赋值,必须使用赋值号 ### ArrayList扩容机制是怎么样的? 详细说一下。 在往ArrayList add元素的时候,如果ArrayList 已有元素数量+1 大于 ArrayList 存储元素的总长度,就会触发扩容。 首先ArrayList会计算新数组的长度,长度为老数组的0.5倍,如果新数组长度小于插入元素需要的最小长度,那么新数组长度赋值为最小长度,如果超过ArrayList允许的最大长度Integer.MAX_VALUE(![](https://g.yuque.com/gr/latex?2%5E31%5E%20-%201#card=math&code=2%5E31%5E%20-%201&height=19&width=43)),那么新数组长度为Integer.MAX_VALUE,否则为Integer.MAX_VALUE - 8 最后将原数组元素拷贝到新数组进行扩容 ### HashMap 1.7 和 1.8 的区别 - 1.7,在发生hash冲突的时候,数据结构只有链表; - 1.8,数据结构有链表和红黑树,使用红黑树是为了能够提高查询效率。在链表长度达到7时(bingCount >= TREEIFY_THRESHOLD - 1),并且hash tab[]数组长度大于等于64时,将链表转换成红黑树,如果数组长度小于64,只是对数组进行扩容
    [https://blog.csdn.net/qq_21251983/article/details/90056067](https://blog.csdn.net/qq_21251983/article/details/90056067) ### HashMap中的key可以是任何对象或数据类型吗 - 可以是null,但不能是可变对象,如果是可变对象,对象中的属性改变,则对象的HashCode也相应改变,导致下次无法查找到已存在Map中的数据 - 如果要可变对象当着键,必须保证其HashCode在成员属性改变的时候保持不变 ### HashMap 初始容量 计算方法 如果在new HashMap的时候,没有指定初始initialCapacity,则初始initialCapacity为16,负载因子为0.75,下次扩容阈值为 16*0.75=12 `这个` `这个初始容量 不一定等于初始化完成后底层数组实际的容量,因为存在阈值的计算,方法如下;也不是初始容量是多少开始就能存多少个元素,因为存在负载因子,在底层数组还没满的时候就会进行扩容` 阈值计算方法为: ```java static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; } ``` 该方法计算大于等于输入参数并最接近参数的2的整数次幂的数,如10,返回16 cap -1 ,-1是为了在计算的时候能得到大于等于输入参数的值 在HashMap 进行put方法的时候,如果判断已有的元素大于 阈值就会触发扩容计算,扩容步骤 ```java final Node[] resize() { //原table数组赋值 Node[] oldTab = table; //如果原数组为null,那么原数组长度为0 int oldCap = (oldTab == null) ? 0 : oldTab.length; //赋值阈值 int oldThr = threshold; //newCap 新数组长度 //newThr 下次扩容的阈值 int newCap, newThr = 0; // 1. 如果原数组长度大于0 if (oldCap > 0) { //如果大于最大长度1 << 30 = 1073741824,那么阈值赋值为Integer.MAX_VALUE后直接返回 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 2. 如果原数组长度的2倍小于最大长度,并且原数组长度大于默认长度16,那么新阈值为原阈值的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // 3. 如果原数组长度等于0,但原阈值大于0,那么新的数组长度赋值为原阈值大小 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults // 4. 如果原数组长度为0,阈值为0,那么新数组长度,新阈值都初始化为默认值 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 5.如果新的阈值等于0 if (newThr == 0) { //计算临时阈值 float ft = (float)newCap * loadFactor; //新数组长度小于最大长度,临时阈值也小于最大长度,新阈值为临时阈值,否则是Integer.MAX_VALUE newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //计算出来的新阈值赋值给对象的阈值 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) //用新计算的数组长度新建一个Node数组,并赋值给对象的table Node[] newTab = (Node[])new Node[newCap]; table = newTab; //后面是copy数组和链表数据逻辑 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode)e).split(this, newTab, j, oldCap); else { // preserve order Node loHead = null, loTail = null; Node hiHead = null, hiTail = null; Node next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } ``` 总结: 如下3种情况,例子需要自己调式,主要关注数组长度(OldCap,newCap)新老变化,扩容阈值(oldThr,newThr)新老变化 ```java //① Map map = new HashMap<>(); map.put("1", "1"); //② Map map1 = new HashMap<>(2); map1.put("2", "2"); //③ Map map2 = new HashMap<>(2, 0.5f); map2.put("3", "3"); ``` ① 没有设置initialCapacity,也没有设置负载因子,第一次put的时候会触发扩容 ```java if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; ``` 第一次的时候,数组长度为默认值16,阈值为16*0.75=12,走的`代码4`逻辑,等到数组长度超过阈值12后,触发第二次扩容,此时table数组,和threshold都不为0,即oldTab、oldCap、oldThr都不为0,先走`代码1`,如果oldCap长度的2倍没有超过最大容量,并且oldCap 长度大于等于 默认容量16,那么下次扩容的阈值 变为oldThr大小的两倍即 12 * 2 = 24,newThr = 24,newCap=32 ② 设置了initialCapacity,没有设置负载因子,此时hashMap使用默认负载因子0.75,本实例设置的初始容量为2,通过计算阈值为2,第一次put的时候由于还没初始化table数组,因此触发第一次扩容。此时oldCap为0,oldThr为2,走`代码3`,确定这次扩容的新数组大小为2,此时还没有确定newThr 下次扩容的大小,于是进入`代码5` 确定newThr为 2 _ 0.75 = 1.5 取整 1 ,及下次扩容阈值为1。当数组已有元素大于阈值及1时,触发第二次扩容,此时oldCap为1,oldThr为1,走`代码1`newCap = oldCap << 1 结果为 4 小于最大容量, 但oldCap 小于hashMap默认大小16,结果为false,跳出判断,此时由于newThr等于0,进入`代码5`,确定newThr为 4 _ 0.75 = 3,下次扩容阈值为3 ③ 设置了initialCapacity=2,负载因子为0.5,通过tableSizeFor计算阈值为2,第一次put的时候,进行扩容,此时oldCap为2,oldThr为2,进入`代码1`,同实例②,newCap = oldCap << 1 结果为 4 小于最大容量, 但oldCap 小于hashMap默认大小16,结果为false,跳出判断,进入`代码5`,确定newThr为 4 * 0.5 = 2,下次扩容阈值为2 **面试回答要素** 1. 回答什么情况下会第一扩容,举例说明,新数组大小,阈值大小 2. 以后什么情况下会再次扩容,这次是怎么计算新数组大小,及阈值大小的 ### HashMap、ConcurrentHashMap初始化阈值为什么要是8,才转为红黑树? 1. 当初始阈值为8时,链表的长度达到8的概率变的很小,如果再大概率减小的并不明显 2. 树结构查找的时间复杂度是O(log(n)),而链表的时间复杂度是O(n),当阈值为8时,long8 = 3,相比链表更快,但树结构比链表占用的空间更多,所以这是一种时间和空间的平衡 ### 手写HashMap 要点: 1. 底层是数组结构 2. hash冲突的时候,转换为链表 3. 考虑扩容处理 ### HashMap在并发下会产生什么问题?有什么替代方案?(HashTable, ConcurrentHashMap)。它们两者的实现原理。 - HashMap并发下产生问题:由于在发生hash冲突,插入链表的时候,多线程会造成环链,再get的时候变成死循环,Map.size()不准确,数据丢失
    [https://www.iteye.com/blog/hwl-sz-1897468](https://www.iteye.com/blog/hwl-sz-1897468) - HashTable: 通过synchronized来修饰,效率低,多线程put的时候,只能有一个线程成功,其他线程都处于阻塞状态 - ConcurrentHashMap:
    1.7 采用锁分段技术提高并发访问率
    1.8 数据依旧是分段存储,但锁采用了synchronized,内部采用Node数组+链表+红黑树的结构存储,当单个链表存储数量达到红黑树阈值8时(此时链表已有元素7),并且数组长度大于64时,存储结构转换为红黑树来存储,否则只进行数组的扩容
    [https://www.cnblogs.com/banjinbaijiu/p/9147434.html](https://www.cnblogs.com/banjinbaijiu/p/9147434.html) ### HashMap 为什么不用平衡树,而用红黑树 红黑树也是一种平衡树,但不是严格平衡,平衡树是左右子树高度差不超过1,红黑树可以是2倍 红黑树在插入、删除的时候旋转的概率比平衡树低很多,效率比平衡树高 查找时间复杂度都维持在O(logN) > 关于HashMap的其他文章:[https://blog.csdn.net/login_sonata/article/details/76598675](https://blog.csdn.net/login_sonata/article/details/76598675) ### 常见异常分为哪两种(Exception,Error),区别是什么,了解受检异常和非受检异常吗 Exception,Error有共同的父类Throwable Error: 表示程序发生错误,是程序无法处理的,不可恢复的,如OutOfMemoryError Exception: 表示程序可处理的异常,又分为CheckedException(受检异常)、UncheckedException(非受检异常),受检异常发生在编译期,必须要使用try...catch 或者 throws捕获或者抛出异常,否则编译不通过;非受检异常发生在运行期,具有不确定性,主要由程序的逻辑问题引起的,在程序设计的时候要认真考虑,尽量处理异常 ### 实现一个LRU 可以直接使用LinkedHashMap实现 [《LinkedHashMap源码分析》](https://github.com/jujunchen/Java-interview-question/blob/master/%E8%AF%A6%E7%BB%86%E5%88%86%E6%9E%90/LinkedHashMap%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md) ```java public class LRUCache extends LinkedHashMap { private int initialCapacity; public LRUCache(int initialCapacity) { //true表示按照访问顺序排序 super(initialCapacity, 0.75f, true); this.initialCapacity = initialCapacity; } @Override protected boolean removeEldestEntry(Map.Entry eldest) { if (size() > initialCapacity) { return true; } return false; } } ```
  • 相关阅读:
    ReentrantLock的实现原理及AQS和CAS
    lock、tryLock和lockInterruptibly的差別
    Sleep和Wait的区别
    什么场景使用多线程
    多生产者多消费者,生产一个数据,消费一个数据
    线程间通讯
    如何退出线程
    线程的状态
    判断二叉树是不是完全二叉树
    js2048小游戏
  • 原文地址:https://www.cnblogs.com/sweetorangezzz/p/13195464.html
Copyright © 2020-2023  润新知