Java面试基础知识点简要总结

目录

  • Java基础
    • 1. 为什么Java代码可以一次编译,到处运行
    • 2. Java的基本数据类型以及它们的范围
    • 3. 自动装箱和自动拆箱
    • 4. Object类中的方法
    • 5. 说一说hashcode()和equals()的关系
    • 6. 为什么要重写hashCode()和equals()
    • 7. == 和equals()的区别
    • 8. String,StringBuffer,StringBuilder的区别
    • 9. 使用字符串时,new 和 ""如何选择
    • 10. 说说字符串拼接
    • 11. String a ="123",这个过程是怎么样的
    • 12. new String("123")的执行过程
    • 13. Java异常处理
    • 14.在finally中return会发生什么
    • 15. 说说static关键字
    • 16. 堆区、栈区、方法区
    • 17. 说一说你对Java反射机制的理解
    • 18. Java反射在实际项目中有哪些应用场景
    • 19. Java的四种引用方式
    • 20. 接口和抽象类的区别
    • 21. 说一说你对泛型的理解
    • 22. 介绍一下泛型擦除
    • 23. `List`和`List`有什么区别
    • 24. 说一说Java的访问权限
  • Java容器
    • 1. 描述一下Map的put过程
    • 2. HashMap的特点
    • 3. JDK7和JDK8中的HashMap有什么区别
    • 4. HashMap的实现原理
    • 5. 介绍一下HashMap的扩容机制
    • 6. HashMap中的循环链表是如何产生的
    • 7. HashMap和Hashtable的区别
    • 8. HashMap和ConcurrentHashMap有什么区别
    • 9. 简要介绍一下ConcurrentHashMap的实现
    • 10. ConcurrentHashMap是怎么分段分组的
    • 11. 说一说LinkedHashMap
    • 12. LinkedHashMap的底层原理
    • 13. 介绍TreeMap的底层原理
    • 14. ArrayList和LinkedList有什么区别
    • 15. 有哪些线程安全的List
    • 16. 介绍一下ArrayList的数据结构
    • 17. 谈谈CopyOnWriteArrayList的实现原理
    • 18 TreeSet和HashSet的区别
  • IO
    • 1. 介绍一下Java的序列化和反序列化
    • 2. 同步和异步
    • 3. BIO、NIO、AIO概念
  • 多线程
    • 1. 创建线程的方式
    • 2. run()和start()有什么区别
    • 3. 介绍一下线程的生命周期
  • JVM
    • 1. 堆和栈的区别
  • 补充
    • 1. JDK和JRE的区别
    • 2. 赋值、深拷贝和浅拷贝
    • 3. Java静态方法能不能调用非静态方法?为什么
    • 4. Java中静态变量存放位置和初始化时间
    • 5. 类的生命周期
    • 6. JWT加密原理
    • 7. final、finally、finalize的区别
    • 8. Java的初始化块
    • 9. 运行时异常和检查性异常
    • 10. this和super的区别
    • 11. 谈谈GC垃圾回收
      • 11.1 GC的区域
    • 12. synchronize和lock的区别
    • 13. 谈谈ReentrantLock的实现原理
    • 14. default位置和执行的关系
    • 15. 基本数据类型和包装类的区别

我们先把必须要会的知识点捋一遍

Java基础

1. 为什么Java代码可以一次编译,到处运行

  • 主要是因为有JVM,也就是Java虚拟机,程序运行前,Java源代码(.java)经过编译器编译为字节码(.class),在程序运行时,JVM负责把字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装JVM,就可以运行字节码文件

2. Java的基本数据类型以及它们的范围

  • 具体范围就不写了,根据计算机组成原理的知识,对于一个整数,最高位是符号位,所以假设说一共有 n n n位,那范围就是 [ − 2 n − 1 , 2 n − 1 − 1 ] [-2^{n-1},2^{n-1}-1] [2n1,2n11],右端点显然,这个左端点是这么得到的,负数中最小的应该是减1减不下去了,也就是发生了向高位的借位,那么32位数,哪个数是这样的呢?只有符号位为为1,其余均为0满足,这是补码,那么原码是所有位取反,再+1,还是这个数 : − 2 n − 1 :-2^{n-1} :2n1
  • f l o a t float float范围大概是 [ − 3.4 × 1 0 38 , 1.8 × 1 0 38 ] [-3.4\times10^{38},1.8\times10^{38}] [3.4×1038,1.8×1038]
  • d o u b l e double double范围大概是 [ − 1.8 × 1 0 308 , 1.8 × 1 0 308 ] [-1.8\times10^{308},1.8\times10^{308}] [1.8×10308,1.8×10308]
类型 位数
i n t int int 32 32 32
b y t e byte byte 8 8 8
s h o r t short short 16 16 16
l o n g long long 64 64 64
f l o a t float float 32 32 32
d o u b l e double double 64 64 64
c h a r char char 16 16 16
b o o l e a n boolean boolean
  • b o o l e a n boolean boolean不同的JVM有不同的实现机制

3. 自动装箱和自动拆箱

  • 就是 i n t int int I n t e g e r Integer Integer这种,自动装箱就是可以把一个基本类型直接赋值给对应的包装类型;自动拆箱就是把一个包装类型的对象直接赋给对应的基本类型
  • 好处就是方便

4. Object类中的方法

  1. Class getClass()返回该对象的运行时类
  2. boolean equals(Object obj)判断指定对象与该对象是否相等
  3. int hashcode()返回该对象的 h a s h C o d e hashCode hashCode值,默认的计算方法如下
    public int hashCode() {// 实际上就是我们熟悉的进制哈希
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
  • String toString()返回该对象的字符串表示
    此外, O b j e c t Object Object类还提供了wait(),notify(),notifyAll()这几个方法,通过这几个方法可以控制线程的暂停和运行

5. 说一说hashcode()和equals()的关系

  • 这个好像也挺爱考的
  • 前面说过了hashcode是计算一个哈希码,那么如果两个对象相等,显然哈希码相同,但是如果哈希码相同,两个对象不一定相等(哈希冲突);equals用于判断两个对象是否相等,注意这里比较的是地址是否相等,因为源码如下
    public boolean equals(Object obj) {
        return (this == obj);
    }
  • 等号是判断地址是否相同

6. 为什么要重写hashCode()和equals()

  • 前面说过了,Object的equals默认是比较地址的,所以要重写,而hashCode()和equals()是联动关系,所以也得重写

7. == 和equals()的区别

  • ==作用于基本数据类型时,是比较两个数值是否相等,作用于引用数据类型时,是比较两个内存地址是否相同,也就是判断它们是不是同一个对象
  • equals()没重写时就是==,重写之后一般就按照对象的内容来进行比较了

8. String,StringBuffer,StringBuilder的区别

  • String由final修饰,不可继承,它是一个不可变类,这主要是出于安全和性能的考虑,比如说它会存储账号密码等信息,如果可变可能引起SQL注入;它是线程安全的;同时对于散列集合如hashmap等,需要用这个hashcode来确定位置,如果String总变,就没办法高效实现这一缓存功能,所以也提升了性能
  • StringBuffer和StringBuilder都是可变的字符串对象,它们有共同的父类AbstractStringBuilder,但是StringBuffer是线程安全的,而Stringbuilder非线程安全,所以后者性能略高,如果创建一个可变字符串优先选择后者。它们都提供了append()、insert()、reverse()、setCharAt()、setLength()等方法,可以进行变换,变换好了以后再转换回String这种不可变类型

9. 使用字符串时,new 和 ""如何选择

  • 如果我们这样写s="123",JVM会使用常量池来管理这个字符串,而如果使用s=new("123")这样的语句的话,首先JVM会使用常量池来管理"123"这个字符串,在调用String类的构造器来创建一个新的String对象,新创建的String对象会被存储在堆内存中
  • 显然,采用new的方式会多创建一个对象出来,占用更多的内存,所以一般使用直接量的形式创建字符串

10. 说说字符串拼接

  • 运算符+,如果拼接的都是字符串常量,那么编译器会直接优化到一起,效率很高;如果包含变量,编译器会采用StringBuilder对其进行优化,也就是调用append()方法,效率也很高, 但是如果是在循环中调用,那么每次都会创建一个StringBuilder实例,再拼接,这样效率就很低了
  • StringBuilder,不要求线程安全,包含变量,用这个
  • StringBuffer,要求线程安全,包含变量,用这个,此外它和StringBuilder都有字符串缓冲区,缓冲区的容量在创建字符串的时候确定,默认为16,当拼接的字符串的数量超过缓冲区的容量的时候,会触发缓冲区的扩容机制,也就是加倍;频繁扩容会降低拼接性能,所以如果能提前预估字符串的长度,可以指定缓冲区的容量为预估的字符串长度
  • String类的concat方法,如果只是对两个字符串进行拼接,且包含变量,适合使用concat方法。它的逻辑是先创建一个足以容纳两个字符串的字节数组,然后拼进去再转换回字符串,在拼接大量字符串的时候,concat效率低于StringBuffer,但是只拼两个的时候concat要好,且代码简洁

11. String a =“123”,这个过程是怎么样的

  • JVM会使用常量池来管理字符串直接量,在执行这句话时,JVM会检查常量池中是否已经存有"123",如果没有就存进去,否则就将其引用赋值给变量a

12. new String(“123”)的执行过程

  • JVM首先使用常量池管理"123"字符串,也就是存进去,然后创建一个新的String对象,这个对象会保存在堆内存中,并且,堆中对象的数据会指向常量池中的直接量

13. Java异常处理

  • 捕获异常,也就是try和catch
  • 处理异常,catch内部处理,先记录日志,便于以后追溯,然后根据异常类型,结合当前的业务情况,进行相应的处理
  • 回收资源,写在finally块内,尝试关闭资源,也就是说无论是否发生异常,finally内部的代码总会被执行

14.在finally中return会发生什么

  • 会使try、catch块内部的return、throw语句失效

15. 说说static关键字

  • static修饰的成员是类成员,类成员属于整个类,而不属于单个对象。类成员(成员变量、方法、初始化块、内部类和内部枚举)不能访问实例成员(成员变量、方法、初始化块、内部类和内部枚举),因为类成员的作用域比实例成员的作用域更大,完全可能出现类成员已经初始化完成而实例成员还没有初始化的情况,所以不能让类成员访问实例成员
  • static修饰的类可以继承

16. 堆区、栈区、方法区

  • 堆区:存线程操纵的数据(以对象形式存放),存储的全是对象,每个对象包含一个与之对应的class信息,JVM只有一个堆区被所有线程共享,堆区不存放基本类型和对象引用,只存放对象本身
  • 栈区:每个线程包含一个栈区,栈中只存放基础数据类型的对象和自定义对象的引用(不是对象);每个栈中的数据(基础数据对象和引用),都是私有的,其他栈不能访问;栈分为三个部分:基本类型变量区,执行环境上下文,操作指令区(存放操作指令)
  • 方法区:存放线程所执行的字节码指令,又叫静态区,和堆一样,被所有线程共享,方法区包含所有的class和static变量;方法区中包含的是在整个程序中唯一的元素,如class static变量

17. 说一说你对Java反射机制的理解

  • 能够分析类能力的程序称为反射
  • Java程序中的对象在运行时表现为两种类型,即编译时类型和运行时类型,例如Person p = new Student();这行代码将会生成一个p变量,该变量的编译时类型为Person,运行时类型为Student
  • 如果程序在运行的时候收到一个编译时类型为Object类型的对象,我现在不知道这个到底是个什么类型,那我怎么调用这个对象的运行时类型呢?
  • 第一种做法时是假设在编译时和运行时都完全知道类型的具体信息,在这种情况下,可以先使用instanceof运算符(一个二元运算符,例如:Integer a = 1; assert(a instanceof Integer == true);)进行判断,再利用强制类型转换将其转换成运行时类型的变量即可
  • 第二种做法时编译时根本无法预知该对象和类可能属于哪些类,程序只依靠信息来发现该对象和类的真实信息,这就必须使用反射
  • 利用反射机制,当程序运行时,我们可以通过反射获得任何一个类的class对象,并通过这个对象查看类的信息;程序运行时,可以通过反射创建任意一个类的实例,并访问该实例的成员;程序运行时,可以通过反射机制生成一个类的动态代理类或动态代理对象

18. Java反射在实际项目中有哪些应用场景

  • 使用jdbc时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序
  • xml注解配置。从配置中解析出的类是字符串,需要通过反射机制实例化
  • 面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现

19. Java的四种引用方式

  • 强引用:这是Java中最常见的引用方式,即程序创建一个对象,并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象。当一个对象被一个或一个以上的变量引用时,它处于可达状态,不可能被系统垃圾回收机制回收
  • 软引用:当一个对象只有软引用时,它有可能被垃圾回收机制回收,对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象。当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中
  • 弱引用:和软引用很像,但是弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,等到系统垃圾回收机制运行时,总会回收该对象所占用的内存。
  • 虚引用:完全类似于没有引用,它主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,必须和引用队列联合使用

20. 接口和抽象类的区别

  • 接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务;对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务。当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间调用接口时,接口是多个程序之间的通信标准
  • 抽象类体现的是一种模板式设计。抽象类作为多个子类的抽象父类,当然可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能,但这个产品仍然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同的方式
  • 使用模式上的区别如下
  • 接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽象类则完全可以包含普通方法
  • 接口里只能定义静态常量,不能定义普通成员变量;抽象类里既可以定义普通成员变量,也可以定义静态常量
  • 接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作
  • 接口里不能包含初始化块;但抽象类则完全可以包含初始化块
  • 一个类最多只能有一个直接父类,包含抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足
  • 相同点
  • 都不能被实例化,都位于继承树的顶端,用于被其他类实现和继承
  • 都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法

21. 说一说你对泛型的理解

  • Java集合有个缺点,当把一个对象扔进集合里面之后,集合就会忘记这个元素的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。这样设计的原因是Java的设计者们不知道我们会用集合来保存什么样的对象,所以他们把集合设计成能保存任何类型的对象,只求有更好的通用性,这样的问题在于可能把一个对象误扔进另一个对象的集合里面,或者拿出元素的时候要进行强制类型转换,可能会出错
  • 从Java5开始,Java引入了参数化类型的概念,允许在创建集合的时候指定集合元素的类型,Java的参数化类型被称为泛型,例如List表示它只能存储String类型的对象,这样就使得程序更加的简洁,且不用进行强制类型转换

22. 介绍一下泛型擦除

  • 在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用泛型声明的类时不指定实际的类型。如果没有为这个泛型类指定实际的类型,此时被称为raw type(原始类型),默认是声明该泛型形参时指定的第一个上限类型。
  • 当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将会被扔掉。比如一个List的类型被转换成List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)

23. ListList有什么区别

  • ?是类型通配符,List可以表示各种泛型的父类,意思是元素类型未知的List
  • List用于设定类型通配符的下限,此处?代表一个未知的类型,但它必须是T的父类型
  • List用于设定类型通配符的上限,此处?代表一个未知的类型,但它必须是T的子类型

24. 说一说Java的访问权限

  • Java语言为我们提供了三种访问修饰符,private,protected,public,在使用这些修饰符修饰目标时,一共可以形成四种访问权限,即private,default,protected,public,如果不加任何修饰符的话默认是default访问权限

修饰成员变量/成员方法时,该成员的四种访问权限的含义如下:

  • private:该成员可以被该类内部成员访问
  • default:该成员可以被该类内部成员访问,也可以被同一包下其他的类访问
  • protected:该成员可以被该类内部成员访问,也可以被同一包下其他的类访问,还可以被它的子类访问
  • public:该成员可以被任意包下,任意类的成员访问

在修饰类时,该类只有两种访问权限,对应的访问权限的含义如下

  • default:该类可以被同一包下的其他的类访问
  • public:该类可以被任意包下,任意的类所访问

Java容器

  • Java中的集合类主要由Collection和Map(具有映射关系的集合)这两个接口派生而出,其中Collection又派生出三个子接口,分别是Set(无序的,元素不可重复的集合),List(有序的,元素可以重复的集合),Queue

Collection体系的继承树如下
Java面试基础知识点简要总结_第1张图片
Map体系的继承树如下 Java面试基础知识点简要总结_第2张图片

1. 描述一下Map的put过程

  • 从HashMap的视角看put的过程
  1. 首次扩容:先判断数组是否为空,若数组为空进行第一次扩容(resize)
  2. 计算索引:通过hash算法,计算键值对在数组中的索引
  3. 插入数据:如果当前位置元素为空, 则直接插入数据;如果K非空,V存在则覆盖V;如果K非空,V不存在则将数据接到链表末端;若链表长度达到8,则将链表转化为红黑树,并将数据插入树中
  4. 再次扩容:如果数组中元素个数超过阈值 ( t h r e s h o l d ) (threshold) (threshold),则再次进行扩容操作

2. HashMap的特点

  1. HashMap是线程不安全的实现
  2. HashMap可以使用null作为key或value

3. JDK7和JDK8中的HashMap有什么区别

  • JDK7中的HashMap是基于数组+链表实现的,它的底层维护一个Entry数组,它会根据计算的hashCode将对应的KV键值对存储到该数组中,一旦发生hashCode冲突,那么就会将该KV键值对放到对应的已有元素的后面,此时便形成了一个链表式的存储结构
  • 显然这个链表会越来越长,最后效率就很差了,所以JDK8做了一个处理,就是当链表长度大于等于8的时候,不再采用链表存储而采用红黑树,这样查询一直维护在 O ( l o g n ) O(logn) O(logn)

4. HashMap的实现原理

  • 它基于hash算法,通过put方法和get方法存储和获取对象
  • 存储对象时,我们将K/V传给put方法时,它调用K的hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过LoadFactor就扩容到原来的2倍),获取对象时,我们把K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对
  • 如果发生碰撞则通过链表组织数据,当链表长度过长,则转化为红黑树,这个在前面说过了

5. 介绍一下HashMap的扩容机制

  • 数组的初始容量是16,而容量是以2的次方扩充的,一是为了提高性能,二是为了能使用位运算代替取模运算(因为容量是一个2的倍数)
  • 数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。这个0.75就是默认的负载因子,可由构造器传入。我们也可以设置大于1的负载因子,这样数组就不会扩充,牺牲性能,节省内存。
  • 为了解决碰撞,数组中的元素是单向链表类型,当链表长度到达一个阈值(8),会将链表转换成红黑树提高性能,降到6的时候又会将红黑树转换回单向链表以提高性能,这里有一个缓冲的7就是为了防止在两种数据结构之间反复转化导致性能退化
  • 扩容的时候通过检测当前哈希值最高位是0还是1来判断要不要往前移动,因为我们之前是取了模的,有可能某些高位被忽略掉了,现在又拿了出来,如果是1则往前移动 2 l e n − 1 2^{len-1} 2len1个单位,如果是0不移动

6. HashMap中的循环链表是如何产生的

  • 在多线程并发条件下,如果两个线程同时扩容,存储在链表中的元素会反过来,因为移动到新的bucket位置时,HashMap不会把元素放在链表的尾部,而是放在头部,这是为了避免尾部便利,如果产生了条件竞争,就会产生死循环

7. HashMap和Hashtable的区别

  1. Hashtable是一个线程安全的Map实现,但HashMap是线程不安全的,所以HashMap性能更高
  2. Hashtable不允许null作为K或V,但HashMap可以

8. HashMap和ConcurrentHashMap有什么区别

  • HashMap是非线程安全的,这意味着不能在多线程中对这些Map进行修改操作,否则会导致数据不一致甚至产生循环链表
  • Collection工具类可以将一个Map转换成线程安全的实现,也就是通过一个包装类把所有功能都传入给Map,而包装类是基于synchronized关键字来保证线程安全的(Hashtable也是基于synchronized,竞争激烈所以效率很低)
  • ConcurrentHashMap没有使用一个全局锁,而是采用了减小锁的粒度的方法,使用锁分段技术,每一段数据一把锁,尽量减少因为竞争锁而导致的阻塞与冲突,检索操作不需要锁

9. 简要介绍一下ConcurrentHashMap的实现

  • JDK1.7:由Segment数据结构和HashEntry数据结构构成,采取分段锁来保证安全性。Segment是ReentrantLock重入锁,在ConcurrentHashMap中扮演锁的角色,HashEntry则用于存储键值对数据,类似HashMap是一个数组+链表结构
  • JDK1.8:已经摒弃了Segment的概念,而是用Node数组+链表+红黑树的数据结构来实现,并发控制使用sgnchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap

10. ConcurrentHashMap是怎么分段分组的

  • get操作:先经过一次散列,然后使用这个散列值通过散列运算定位到Segment,再通过散列算法定位到元素。get操作的高效之处在于整个get过程不需要加锁,除非读到空值才会加锁重读。原因是将使用的共享变量定义成volatile类型
  • put操作:首先判断是否需要扩容;然后定位元素位置,将其放到HashEntry数组中。插入过程会经过一次K的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS进行赋值,然后进行第二次hash操作,找到相应的HashEntry位置,然后利用继承ReentrantLocktryLock()方法尝试获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那么当前线程会以自旋的方式继续调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒

讲一下自旋锁和互斥锁,它们是类似的,都是为了解决对某项资源的互斥使用,无论是自旋锁还是互斥锁,任何时候最多都只能有一个保持者,也就是说,在任何时候都最多只能有一个执行单元获得锁。但对于互斥锁,如果资源已经被占用,资源申请者只能进入到睡眠状态;但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋“一词就是这么得名

11. 说一说LinkedHashMap

  • 采用双向链表维护K的顺序,该链表负责维护Map的迭代顺序,该顺序与K-V对的插入顺序一致
  • LinkedHashMap可以避免对HashMap、Hashtable里的K-V对进行排序(只要插入K-V对保持顺序即可),同时可避免使用TreeMap所增加的成本
  • LinkedHashMap由于需要维护插入的顺序,因此性能略低于HashMap的性能,但因为它以链表来维护内部顺序,所以在迭代访问Map里的所有元素时将有较好的性能

12. LinkedHashMap的底层原理

  • 继承于HashMap,它维护一条双向链表,实现上,仅为维护双向链表重写了部分方法

13. 介绍TreeMap的底层原理

  • TreeMap基于红黑树实现,映射根据其键的自然顺序进行排序,或者根据创建映射时提供的Comparator进行排序,具体取决于采用的构造方法。TreeMap基本操作复杂度都是 O ( l o g n ) O(logn) O(logn)
  • TreeMap包含几个重要的成员变量:root、size、comparator。其中root是红黑树的根节点,是一个Entry类型,Entry是红黑树的节点,它包含了红黑树的6个基本组成:key、value、left、right、parent和color;Entry节点根据Key排序,包含的内容是value;Entry中Key比较大小是根据比较器comparator来进行判断的;size是红黑树的节点个数

14. ArrayList和LinkedList有什么区别

  1. ArrayList基于数组;LinkedList基于双向链表
  2. 随机访问前者好, O ( 1 ) O(1) O(1)的;后者 O ( n ) O(n) O(n)
  3. 插入和删除后者好
  4. 后者更占内存,因为除了数据他还存储了两个引用,一个指向前面元素,另一个指向后面元素

15. 有哪些线程安全的List

  1. Vector,古老,线程安全,效率低,不推荐
  2. Collections.SynchronizedList,比上一个扩展性和兼容性好,但所有的方法都带有同步锁,性能不是最优
  3. CopyOnWriteArrayList,Java1.5在java.util.concurrent下增加的类,它采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时,线程将会直接读取集合本身,无需加锁与阻塞。当线程对此类集合执行写入操作时,集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对集合的写入操作都是对数组的副本执行操作,因此它是线程安全的,且是目前最优方案

16. 介绍一下ArrayList的数据结构

  • 底层数组,默认第一次插入元素时创建大小为10的数组,扩容时增加50%的容量,数据以System.arraycopy()复制到新的数组

17. 谈谈CopyOnWriteArrayList的实现原理

  • 线程安全,读操作无锁;写操作时会复制一份新的List,在新的List上执行操作,然后把原引用指向新的List,这样保证了写操作的线程安全,当然此时写操作要上锁的,如果这时候读,读到的是原来的容器,因此上锁不影响并发的读
  • 优点:适用于读多写少的并发场景,读写分离,读操作性能很高;缺点:写操作每次都要拷贝,内存占用大,读和写无法保持实时性,和Vector不同,Vector对于读写全加锁,强一致

18 TreeSet和HashSet的区别

  • 二者都是线程不安全的,元素都不能重复,但是
  1. TreeSet中元素不能是null,HashSet元素可以是null
  2. TreeSet支持定制排序和自然排序两种方式,HashSet不能保证元素的排列顺序
  3. TreeSet底层红黑树,HashSet底层哈希表(基于HashMap实现的)

IO

1. 介绍一下Java的序列化和反序列化

  • 序列化机制可以将对象转换成字节序列,这些字节序列可以保存在磁盘上,也可以在网络中传输,并允许程序将这些字节序列再次恢复成原来的对象。其中对象的序列化,是指将一个Java对象写入IO流中,对象的反序列化,是指从IO流中恢复该Java对象。若对象要支持序列化机制,则它的类要实现Serializable接口,该接口是一个标记接口,它没有提供任何方法,只是标明该类是可以序列化的
  • 若要实现序列化,则需要使用对象流ObjectInputStreamObjectOutputStream,其中,在序列化时需要调用ObjectOutputStream对象的writeObject()方法,以输出对象序列。在反序列化时需要调用ObjectInputStream对象的readObject()方法,将对象序列恢复为对象

2. 同步和异步

  • 同步:发起一个请求,调用者一直等待请求结果返回,也就是当前进程会被挂起,无法从事其他任务,只有当条件就绪才能继续
  • 异步:发起一个调用后,立即得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件、回调等机制来通知调用者其返回结果
  • 二者最大区别在于异步调用者不需要等待其结果,被调用者会通过回调等机制来通知调用者其返回结果

3. BIO、NIO、AIO概念

  • 它们是Java语言对操作系统的各种IO模型的封装
  • BIO(Blocking I/O),同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成
  • NIO(Non-Blocking I/O),是一种同步非阻塞I/O模型
  • AIO(Asynchronous I/O),它是一个异步非阻塞的IO模型

多线程

1. 创建线程的方式

  • 继承Thread类,重写run()方法,创建Thread类的实例,即创建了线程对象,调用线程对象的start()方法来启动该线程
  • 实现Runnable接口来创建并启动线程,定义接口实现类,实现run()方法,该run()方法作为线程执行体,创建Runnable()实现类的实例,并将其作为Threadtarget来创建Thread对象,Thread对象为线程对象,调用线程对象的start()方法来启动对象
  • 也可通过实现Callable接口来创建并启动线程

2. run()和start()有什么区别

  • run()方法被称为线程执行体,它的方法体代表了线程需要完成的任务,而start()方法用来启动线程,调用start()方法启动线程时,系统会把该run()方法当成线程执行体来处理,但如果直接调用线程对象的run()方法,则run()方法立即就会被执行,而且在run()方法返回之前其他线程无法并发执行。也就是说,如果直接调用线程对象的run()方法,系统把线程对象当成一个普通对象,而run()方法也是一个普通方法,而不是方法执行体

3. 介绍一下线程的生命周期

  • 线程的生命周期分为新建、就绪、运行、阻塞和死亡五种状态
  • 新建:当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时它和其他的Java对象一样
  • 就绪:当线程对象调用了start()方法之后,该线程就处于就绪状态,Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了,至于该线程何时开始运行,取决于JVM里线程调度器的调度
  • 运行:如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态,如果计算机只有一个CPU,那么在任何时刻只有一个线程处于运行状态。当然,在一个多处理机的机器上,将会有多个线程并行执行,当线程个数大于处理器数时,依然会有多个线程在一个CPU上轮换的现象
  • 阻塞:当发生如下情况时,线程会进入阻塞状态
  1. 线程调用sleep()方法主动放弃所占用的处理器资源(到时间解除阻塞)
  2. 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞(方法返回解除阻塞)
  3. 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有(成功获得监视器时解除阻塞)
  4. 线程在等待某个通知(notify)(在等待时,其他线程发出了一个通知,解除阻塞)
  5. 程序调用了线程的suspend()方法将该线程挂起,但这个方法容易导致死锁,所以应该尽量避免使用该方法(处于挂起状态的线程被调用了resume()恢复方法解除阻塞)
  • 死亡:线程会以如下三种方式结束,结束之后就处于死亡状态
  1. run()或call()方法执行完成,线程正常结束
  2. 线程抛出一个未捕获的Exception或Error
  3. 直接调用该线程的stop()方法来结束该线程,该方法容易导致死锁, 通常不建议使用

JVM

1. 堆和栈的区别

  • 堆:主要用于存储实例化的对象,数组,由JVM动态分配内存空间。一个JVM只有一个堆内存,线程可以共享数据
  • 栈:主要用于存储局部变量和对象的引用变量,每个线程都会有一个独立的栈空间,所以线程是不共享数据

补充

1. JDK和JRE的区别

Java面试基础知识点简要总结_第3张图片

2. 赋值、深拷贝和浅拷贝

  • 浅拷贝只复制某个对象的引用,而不复制对象本身,新旧对象还是共享一块内存;如果对象是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。由于两个对象所指内存相同,析构时会发生内存泄漏问题
  • 深拷贝是不断对指针进行拷贝,而且对指针指向的内容进行拷贝,会创造一个一模一样的对象,新对象和原对象不共享内存。析构时不会发生内存泄漏

3. Java静态方法能不能调用非静态方法?为什么

  • 如果是静态方法调用非静态方法:调用静态方法时,是没有传入this指针的,所以在静态方法中调用非静态方法,非静态方法的第一个参数是隐含的,无法传值,所以无法调用。
  • 如果是非静态方法调用静态方法;可以调用,因为调用非静态方法,不需要this指针

4. Java中静态变量存放位置和初始化时间

  • 因为Java中的静态变量相当于全局变量,可以被所有的线程共享,所以存放在方法区中(方法区中存储的是已经被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据)
  • Java中的静态变量是在经过类加载中的加载=>验证两个阶段之后的准备阶段
  • 准备阶段:是正式为类变量分配内存并设置类变量初始值(0)的阶段,public static int i = 2;会先赋值为0,然后在解析阶段再赋值为2;但是如果该静态变量为final类型,那么在通过javac编译器编译时,就会自动为该字段属性中生成一个ConstantValue属性,那么在准备阶段就会根据ConstantValue属性直接设置为2

5. 类的生命周期

Java面试基础知识点简要总结_第4张图片

  • 加载:找到需要加载的类并把类信息加载到JVM的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类信息的入口
  • 连接:首先验证类是不是合法;然后为类的静态变量分配内存并设为JVM的初值,对于非静态的变量则不会为它们分配内存,注意这里的初值指的是jvm的默认初值而不是我们分配的
  • 解析:把常量池中的符号引用(类似身份证号)转化为直接引用(具体信息)
  • 初始化:如果一个类被直接引用(new、读取或设置类的静态变量、调用类的静态方法;通过反射方式执行以上三种行为;作为程序入口直接运行时),会触发类的初始化,初始化执行顺序:按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句,在类的初始化阶段,只会初始化与类相关的静态赋值语句和静态语句(static),没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行
  • 使用:主动使用和被动使用,引用父类的静态字段,只会引起父类的初始化而不会引起子类的初始化;定义类数组不会引起类的初始化;引用类的常量不会引起类的初始化
  • 卸载:类使用完之后,如果该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例;或者加载该类的ClassLoader已经被回收;或者该类对应的java.lang.Class对象已经没有任何地方被引用,无法在任何地方通过反射访问该类的方法,满足上述情况之后,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程实际上就是在方法区中清空类信息,Java类的整个生命周期就结束了

6. JWT加密原理

  • JWT是JSON Web Token的缩写,是REST接口的一种安全策略,也是一种安全的规范,使用JWT可以让我们在用户端和服务端建立一种可靠的通信保障
  • 优点:在分布式系统中,可以有效的解决单点登录问题以及SESSION共享问题;服务器不保存token或者用户session信息,可以减少服务器压力
  • 缺点:没有失效策略,设置失效时间后,只能等待token过期,无法改变token里面的失效时间
  • 组成头部(header),消息体(payload),签名(sign)
  1. 头部(header){ "typ": "JWT", "alg": "HS256" }  此消息说明类型是JWT,加密解密方式为HS256
  2. 消息体(payload):是实际存放消息的地方,JSON格式如下
{ "iss": "why", "iat": 1416797419, "exp": 1448333419, "aud": "www.example.com", "sub": "taobao.com", }

iss表示该JWT消息的签发者,是否使用是可选的
sub表示该JWT面向的用户,是否使用是可选的
aud表示接收该JWT的一方,是否使用是可选的
exp表示什么时候过期,这里是一个Unix时间戳,是否使用是可选的
iat表示在什么时候签发的,是否使用是可选的

  1. 签名(sign):签名是对头部和消息体的内容进行签名,即使有人获取token内容,改变消息体或者头部的内容,那么生成的签名将和现在的签名不一样;同时如果不知道服务器加密时用的密钥的话,得出来的签名也一定是不一样的
  • 签名的过程(重要):采用header中声明的算法,将base64加密后的头部和消息体以及密钥进行计算得到
  • 解密:后端服务检验jwtToken是否有权访问接口服务,进行解密认证,首先将该字符串按照.切分三段字符串,分别得到header、payload和sign,然后将payload拼装成密钥和HAMC SHA-256算法进行加密然后得到新的字符串和sign进行比对,如果一样就代表数据没有被篡改,然后从头部取出exp对存活期进行判断,如果超过了存活期就返回空字符串,如果在存活期内返回userid的值
    安全性:要使用https进行SSL加密传输,否则不安全,因为header和payload都是采用的base64编码的

7. final、finally、finalize的区别

  • final是Java中的关键字,修饰符,可以修饰变量、类、方法,修饰的类不能被继承,修饰的变量不能被重新赋值,修饰的方法不能被重写。被final修饰的变量必须被初始化,初始化的方式有几种:1. 在定义的时候初始化;2. 在初始化块中初始化,不能在静态初始化块中初始化;3. 静态final变量会在定义时初始化,也可以在静态初始化块中初始化,不可以在初始化块中初始化;4. final变量还可以在类的构造器中初始化,但是静态final变量不可以
  • finally是Java中的一种处理机制,用于抛异常,finally代码块内语句无论是否异常,都会再执行finally,常用于一些流的关闭
  • finalize是Java中的一个方法名,是Object类中的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,供垃圾收集时的其他资源回收,例如关闭文件等

8. Java的初始化块

  • 静态初始化块:使用static定义,当类装载到系统时执行一次,若在静态初始化块中初始化变量,那仅能初始化类变量,即static修饰的数据成员
  • 非静态初始化块:在每个对象生成时都会被执行一次,可以初始化类的实例变量
  • 初始化块会先于构造函数执行,是对构造器的补充,初始化块是不能接受任何参数的,定义的一些所有对象共有的属性、方法等内容时就可以用初始化块来初始化

9. 运行时异常和检查性异常

  • 运行时异常
  1. NULLPointerException:空指针异常
  2. IndexOutOfBoundsException:数组下标越界异常
  3. ClassCastException:类型转换异常
  4. NumberFormatException:数字格式化异常
  5. JSONException:JSON异常,进行JSON格式化操作时出现异常
  • 检查性异常
  1. SQLException:SQL异常
  2. IOException:IO异常,在对流操作时可能会出现的异常
  3. FileNotFoundException:找不到某个文件时,会抛出该异常
  4. ClassNotFoundException:找不到某个类
  5. EOFException:输入过程过程中意外地到达文件尾或流尾,会抛出该异常,常见于对流的操作

10. this和super的区别

  1. 指代的对象不同,super指代的是父类,是用来访问父类的,而this指代的是当前类
  2. 查找范围不同,super只能查找父类,而this会先从本类中找,如果找不到再去父类中找
  3. 本类属性赋值不同,this可以用来为本类的实例属性赋值,而super不能实现此功能
  4. this可用于sychronized,因为this表示当前对象,所以this可用于sychronized加锁,而super则不能实现此功能

11. 谈谈GC垃圾回收

  • 堆中不再被引用的对象称为垃圾,垃圾回收的基本原理是移除不再被引用的对象,基本假设是大多数对象的生命周期都很短
  • GC的三个方法分别是Mark(从堆的根节点开始遍历整个堆,标记还“活着”的Object),Sweep(删除heap中的Object),Compating(碎片整理)

11.1 GC的区域

  • 堆是Java虚拟机进行垃圾回收的主要场所,其次要场所是方法区

12. synchronize和lock的区别

  • 从功能来看,bock和synchronize和lock都是Java中用于解决安全问题的一个工具
  • 从特性来看,首先synchronize是一个同步关键字,lock是juc包里面提供的一个接口,这个接口有很多的实现类,其中包含Reentrantlock这个重入锁的实现
  • 从锁粒度来看
  • synchronized可以通过修饰在方法层面和修饰在同步代码块上两种方式控制锁的粒度;而且我们可以通过synchronized加锁对象的生命周期,来控制锁的作用范围,比如锁对象是静态对象或者类对象,那么这个锁就属于全局锁,如果锁对象是实例对象,那么这个锁范围取决于这个对象的生命周期
  • lock比synchronized的灵活性更高,lock可以自主决定什么时候加锁,什么时候释放锁,只需要调用lock()和unlock()这两个方法,同时lock还提供了非阻塞的竞争方法trylock()方法,这个方法通过返回true/false来告诉当前线程是否已经有其他线程正在使用锁;由于synchronized是关键字,所以它无法实现非阻塞竞争锁的方法,另外synchronized锁的释放是被动的,就是当synchronized同步代码块执行完以后或者代码出现异常时才释放;lock提供了公平锁和非公平锁的机制,公平锁是指线程竞争资源时,如果有其他线程正在排队等待锁释放,那么当前竞争锁资源的线程无法插队,而非公平锁,就是不管是否有线程正在排队等待锁,他都会尝试去竞争一次锁,synchronized只提供了一种非公平锁的实现
  • 性能方面,二者差距不大,在实现上会有一个区别synchronized引入了偏向锁、轻量级锁、重量级锁以及锁升级的机制去实现锁的优化,而lock则用到了自旋锁的方式实现锁的性能优化

13. 谈谈ReentrantLock的实现原理

  • ReentantLock是基于AQS实现的,AQS即AbstractQueuedSychronizer的缩写,这个是内部实现了两个队列的抽象类,分别是同步队列和条件队列,其中同步队列是一个双向链表,里面储存的是处于等待状态的线程,正在排队等待唤醒去获取锁,而条件队列是一个单向链表,里面储存的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了条件队列的队尾,AQS所做的就是管理这两个队列里面线程之间的等待状态-唤醒的工作
  • ReentrantLock就是通过重写了AQS的tryAcquire和tryRelease方法实现的lock和unlock

14. default位置和执行的关系

  • 笔试的时候发现的一道题,感觉很有意思
  • 下面的代码会输出什么
        int k = 1;
        switch(k){
            default: {
                System.out.println("123");
            }            
            case 0:{
                System.out.println(0);
            }
            case 1:{
                System.out.println(1);
            }
            case 2:{
                System.out.println(2);
            }
        }
  • 可以自己测一测default放在不同位置的情况,结论是在没有break的情况下,如果default之前没有case执行过,且不在最后,那么不执行default,否则就会执行default(如果default放在最后也会执行)
  • 未完待续

15. 基本数据类型和包装类的区别

  • Java基本数据类型有8种,主要分为三类,布尔类型 b o o l e a n boolean boolean,整数类型 b y t e , s h o r t , i n t , l o n g byte,short,int,long byte,short,int,long,浮点数类型 f l o a t , d o u b l e float,double float,double,字符类型 c h a r char char

区别如下

  1. 包装类是对象,拥有方法和字段,对象的调用是通过对象的地址,基本类型不是
  2. 包装类是引用的传递,基本类型是值传递
  3. 声明方式不同,包装类需要 n e w new new声明,在堆内存中分配空间,然后通过对象的引用来调用它们;而基本类型直接将值保存在值栈中。因此包装类的效率要更低
  4. 初始值不同, i n t int int初始值为 0 0 0,而 I n t e g e r Integer Integer初始值为 n u l l null null
  5. 使用方式不同,基本数据类型直接赋值使用,包装数据类型是在集合如Map等中使用

你可能感兴趣的:(面试专题,java,面试,jvm)