《Java面试题集中营》- Java基础

2024-07-14 08:32:25 浏览数 (1)

1.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.math 有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. == 比较两个实体的引用地址是否相等,不能覆盖,如果引用地址相等,那认为两个实体为同一个实体
代码语言:javascript复制
int a = 1;
Integer b = new Integer(1);
Integer c = new Integer(1);
Integer d = Integer.valueOf(1);
Long e = new Long(1);
Long f = Long.valueOf(1);

assert a == b;
assert b != c;
assert b != d;
assert a == d;
assert a == e;
assert a == f;
assert b.equals(c);
assert !b.equals(e);
hashCode和equals方法的区别与联系

对于覆盖了equals方法的类中,同样也要覆盖hashCode方法。这是JDK规定的结果。

比较两个对象是否相同,hashCode比equals效率更高,所以优先会根据hashCode来比较,但如果不重写hashCode,原本两个对象可以认为是相等,但由于hashCode默认返回表示对象地址的整数,必然不相等,所以需要重写hashCode。

由于hashCode有个问题,可能两个不同的对象会有相同的hashCode,这样还需要通过equals来比较

比如HashMap中,计算key的索引位置,会用到key.hashCode,在确定是否为同一个元素时通过equals比较

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

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

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

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

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

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

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

<? extends Fruit> 称为 上界限定符,list只能get,不能add(除了add null值),通常用于读

<? super Apple>称为 下界限定符,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) 表示这个注解只能在方法上使用 目标类型: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会计算新数组的长度,长度为老数组的1.5倍,如果新数组长度还是小于插入元素需要的最小长度,那么新数组长度赋值为最小长度,如果超过ArrayList允许的最大长度Integer.MAX_VALUE(

2^{31} - 1

),那么新数组长度为Integer.MAX_VALUE,否则为Integer.MAX_VALUE - 8(为什么要-8?Why the maximum array size of ArrayList is 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
HashMap中的key可以是任何对象或数据类型吗
  • 可以是null,但不能是可变对象,如果是可变对象,对象中的属性改变,则对象的HashCode也相应改变,导致下次无法查找到已存在Map中的数据
  • 如果要可变对象当着键,必须保证其HashCode在成员属性改变的时候保持不变
HashMap 初始容量 计算方法

如果在new HashMap的时候,没有指定初始initialCapacity,则初始initialCapacity为16,负载因子为0.75,下次扩容阈值为 16*0.75=12

这个初始容量 不一定等于初始化完成后底层数组实际的容量,因为存在阈值的计算,方法如下;也不是初始容量是多少开始就能存多少个元素,因为存在负载因子,在底层数组还没满的时候就会进行扩容

阈值计算方法为:

代码语言:javascript复制
 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方法的时候,如果判断已有的元素大于 阈值就会触发扩容计算,扩容步骤

代码语言:javascript复制
final Node<K,V>[] resize() {
    	//原table数组赋值
        Node<K,V>[] 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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
    	//后面是copy数组和链表数据逻辑
        if (oldTab != null) {
            for (int j = 0; j < oldCap;   j) {
                Node<K,V> 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<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> 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)新老变化

代码语言:javascript复制
//①
Map<String, String> map = new HashMap<>();
map.put("1", "1");
//②
Map<String, String> map1 = new HashMap<>(2);
map1.put("2", "2");
//③
Map<String, String> map2 = new HashMap<>(2, 0.5f);
map2.put("3", "3");

① 没有设置initialCapacity,也没有设置负载因子,第一次put的时候会触发扩容

代码语言:javascript复制
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,走代码1newCap = 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
  • HashTable: 通过synchronized来修饰,效率低,多线程put的时候,只能有一个线程成功,其他线程都处于阻塞状态
  • ConcurrentHashMap: 1.7 采用锁分段技术提高并发访问率 1.8 数据依旧是分段存储,但锁采用了synchronized,内部采用Node数组 链表 红黑树的结构存储,当单个链表存储数量达到红黑树阈值8时(此时链表已有元素7),并且数组长度大于64时,存储结构转换为红黑树来存储,否则只进行数组的扩容 https://www.cnblogs.com/banjinbaijiu/p/9147434.html
HashMap 为什么不用平衡树,而用红黑树

红黑树也是一种平衡树,但不是严格平衡,平衡树是左右子树高度差不超过1,红黑树可以是2倍

红黑树在插入、删除的时候旋转的概率比平衡树低很多,效率比平衡树高

查找时间复杂度都维持在O(logN)

关于HashMap的其他文章:https://blog.csdn.net/login_sonata/article/details/76598675 源码解析:https://www.jianshu.com/p/0a70ce2d3b67

常见异常分为哪两种(Exception,Error),区别是什么,了解受检异常和非受检异常吗

Exception,Error有共同的父类Throwable

Error: 表示程序发生错误,是程序无法处理的,不可恢复的,如OutOfMemoryError

Exception: 表示程序可处理的异常,又分为CheckedException(受检异常)、UncheckedException(非受检异常),受检异常发生在编译期,必须要使用try…catch 或者 throws捕获或者抛出异常,否则编译不通过;非受检异常发生在运行期,具有不确定性,主要由程序的逻辑问题引起的,在程序设计的时候要认真考虑,尽量处理异常

实现一个LRU

可以直接使用LinkedHashMap实现

《LinkedHashMap源码分析》

代码语言:javascript复制
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private int initialCapacity;
    public LRUCache(int initialCapacity) {
        //true表示按照访问顺序排序
        super(initialCapacity, 0.75f, true);
        this.initialCapacity = initialCapacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        if (size() > initialCapacity) {
            return true;
        }
        return false;
    }
}
用过流没有,流怎么实现

Stream流是Java8中引入的新特性,Stream有几个特点:

不存数据,都是通过管道将源数据元素传递给操作;

对Stream的任何修改都不会修改数据源,都是新产生一个流

流的很多操作如filter、map都是延迟执行的,只有到终点才会将操作顺序执行

对于无限流可以通过“短路”操作访问到有限元素后就返回

流的元素只访问一次,如果需要重新访问,需要重新生成一个新的流

Stream中BaseStream规定了流的基本接口,在Stream中使用Stage来描述一个完整的操作,将具有先后顺序的各个Stage连一起,就构成了整个流水线。

AbstractPipeline是流水线的核心,定义了三个AbstractPipeline类型的变量:sourceStage(源阶段)、previousStage(上游pipeline,上一阶段),nexStage(下一阶段)

ReferencePipeline 继承了AbstractPipeline

Head、StatefulOp、StatelessOp继承了ReferencePipeline,分别表示源,无状态操作,有状态操作

比如Collection.stream()方法得到Head也就是stage0,紧接着调用一系列的中间操作,不断产生新的stage。这些stage对象以双向链表的形式组织在一起,构成整个流水线。 由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就建立起对数据源的所有操作。

0 人点赞