Java基础面试题-第二集

Posted 唐僧洗澡不秃头

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java基础面试题-第二集相关的知识,希望对你有一定的参考价值。

1、ArrayList和LinkedList区别

ArrayList:
1. 基于动态数组,会占据一片连续内存空间,适合下标访问(随机访问)
2. 扩容机制:因为数组长度固定,超出长度存数据是需要新建数组,然后将老数组数据移过去
3. 如果不是在尾部插入数据,会涉及元素的移动(底层是数组的拷贝),所以如果使用尾插性能极佳
4. 容量**elementData.length**不等于数据个数**size**,会有一部分闲置,但是在序列化时只会序列化有数据的部分
LinkedList:
1. 基于链表,可以存储在分散的内存中,适合数据的插入和删除,不适合查询
2. 需要逐一遍历时必须使用Iterator(for-each底层用的迭代器),不能使用for-i,因为每次for-i循环体内通过get(i)获得某一元素时都需要对list重新遍历,性能消耗极大。
3. LinkedList是由一个个的Node节点组成,对象创建开销大

2、HashMap和HashTable的区别

区别:
  1. HashTable方法用synchronized修饰,线程安全,而HashMap线程不安全

  2. hash函数不同,HashMap使用了扰乱函数,让hashCode的前16位异或后16位,而HashTable直接使用的是hashCode

  3. 底层实现不同,一个是数组+链表+红黑树,一个是数组加链表

  4. 插入链表方式不同,1.8以后HashMap改成了尾插法,而HashTable是头插法

  5. HashTable的数组是在构造时创建,而HashMap是插入第一个元素时,在resize中创建

  6. 扩容不同
    初始容量不同
    HashTable为11,HashMap为16,且HashTable可以指定初始容量,而HashMap需要将指定容量扩大成2的幂
    扩容时机不同
    HashTable是每次容量超过过阈值时才扩容,而HashMap是第一次插入元素,以及容量超过阈值时扩容
    扩容机制不同
    HashMap扩容大小为2n,HashTable扩容大小为2n+1

  7. HashMap的键值可以为null,而HashTable和ConcurrentHashMap的键值都不能为null

    ​ 原因

    1.因为Hashtable在我们put 空值的时候会直接抛空指针异常,但是HashMap却做了特殊处理。
    2.因为Hashtable使用的是安全失败机制(fail-safe),fail-safe 允许在遍历的过程中对容器中的数据进行修改,所以这种机制会使你此次读到的数据不一定是最新的数据。
    3.如果你使用 null 值,就会使得其无法判断对应的 key 是不存在还是为空,因为你无法再调用一次contain(key)来对 key 是否存在进行判断,ConcurrentHashMap 同理.
    
  8. 迭代器不同
    HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的。所以,当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛出 ConcurrentModificationException 异常,而 Hashtable 则不会

3、ConcurrentHashMap原理,jdk7和jdk8版本的区别

jdk7:
	1. 数据结构:ReentrantLock+Segment+HashEntry,一个Segment包含一个HashEntry数组,每个HashEntry又是一个链表结构
	2. 元素查询:二次hash,第一次hash定位到Segment,第二次hash定位到元素所在链表的头结点
	3. 锁:Segment分段锁,Segment继承了ReentrantLock,锁定操作的Segment,其它Segment不受影响,并发度为Segment的个数,可以通过构造函数指定,数组扩容是以Segment为单位
	4. get方法无需加锁,使用volatile保证
jdk8:
1. 数据结构:synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性
2. 查找、替换、赋值操作都使用CAS
3. 锁:锁链表的头结点,不影响其它元素的读写,锁粒度更细,效率更高,扩容时阻塞所有的读写、并发扩容,每个线程分配一部分任务
4. 读操作无锁:Node的val和next都用volatile修饰,数组用volatile修饰保证扩容时被其它线程可见

4、正向代理和反向代理

  1. 代理其实就是一个中介,A和B本来可以直连,中间插入一个C,C就是中介。
  2. 刚开始的时候,代理多数是帮助内网client访问外网server用的
  3. 后来出现了反向代理,"反向"这个词在这儿的意思其实是指方向相反,即代理将来自外网客户端的请求转发到内网服务器,从外到内。

5、如何实现一个IOC容器

  1. 配置文件配置包扫描路径
  2. 递归包扫描获取.class文件
  3. 反射、将需要交给IOC管理的类,加入容器中
  4. 对需要注入的类进行依赖注入
  • 在配置文件中指点需要扫描的包路径
  • 定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入注解、获取配置文件注解
  • 从配置文件中获取需要扫描的包路径,获取到当前路径下的文件信息以及文件夹信息,将所有路径下所有.class结尾的文件添加到一个Set集合中存储
  • 遍历Set集合,获取在类上有指定注解的类,并将其交给IOC容器,定义一个线程安全的Map用来存储这些对象
  • 遍历这个IOC容器,获取到每个类的实例,判断里面是否有依赖于其它类的实例,然后进行对注入

6、什么是字节码?采用字节码的好处是什么

  1. 字节码是由Java源文件经编译器而编译生成的class文件,JVM能够理解的代码叫做字节码,是一种中间的代码
  2. 首先因为我们的硬件操作复杂而繁琐,所以我们用操作系统封装了硬件的复杂调度,而向用户暴露了操作的接口,而对于不同的操作系统,JVM提供给了编译程序一个公共的接口,编译程序只需要面向虚拟机,生成字节码这样一个JVM能够理解的中间代码,然后由解释器转换成不同系统的机器码,从而使得Java达到了一个平台无关性。
  3. Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。

7、Java类加载器有哪些

  1. JDK自带的三个类加器:Bootstrap ClassLoader、Extent ClassLoader、App ClassLoader
  2. Bootstrap ClassLoader 默认负责加载 %JAVA_HOME%/lib 目录下的jar包和类文件(rt.jar)
  3. Extent ClassLoader 默认负责加载 %JAVA_HOME%/lib/ext 目录下的jar包和类文件
  4. App ClassLoader 是自定义类加载器的父类,负责加载classpath下的类文件(默认的系统类加载器,自己写的代码,和引入的jar包)
  5. 如何要自定义类加载器可以实现ClassLoader,然后实现App ClassLoader

8、双亲委派模型

  1. 在JDK中有三个默认的类加载器,它们各自有自己的加载路径
  2. 当类加载器加载一个类时,它实际上不会直接去加载这个类,会进行一个向上委派的操作
  3. 向上委派实际上就是查找缓存,每一个类加载器都会将自己加载过的类放到自己的一个缓存中,然后从App ClassLoader开始,首先会找自己缓存中是否存在,如果有就无需加载了,否则会向上重复这个操作,直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载(通过加载路径向下查找),会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。

好处

  1. 主要是为了安全性,避免用户自己编写的类动态替换Java的一些核心类,比如String
  2. 同时避免类的重复加载,因为JVM中区分不同的类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是两个不同的类

9、Java中的异常体系

  1. Java中所有异常都来自顶级父类Throwable
  2. Throwable下有两个子类Exception和Error
  3. Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行,比如虚拟机的错误和OOM
  4. Exception不会导致程序停止,又分为RunTimeException运行时异常和CheckedException检查异常
  5. RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。(空指针,数组下标越界,并发修改异常)
  6. CheckedException常常发生在程序编译过程中,会导致程序编译不通过(类型不匹配,语法问题)

10、GC如何判断对象可以被回收的

  1. 引用计数法:每个对象都有一个引用计数属性,新增一个引用是计数加1,引用释放时计数减1,计数为0使表示可以回收
  2. 可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GCRoot没有任何引用链相连时,则证明此对象时不可用的,那么虚拟机就判断是可回收对象

但是引用计数法,可能出现循环引用,这个时候计数器不可能为0,对象永远无法被回收

GC Roots的对象有:

  1. 虚拟机栈(栈帧的局部变量表)中引用的对象
  2. 方法区中类静态变量引用的对象
  3. 方法区常量引用的对象
  4. 本地方法栈Native方法引用的对象

11、线程的生命周期,线程有哪些状态

在操作系统中,线程有五种状态:

  1. 新建状态(New):新创建了一个线程对象
  2. 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法
  3. 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码
  4. 阻塞状态:阻塞状态时线程由于某些原因,放弃CPU使用权,暂停运行,直至被激活后进入就绪状态
  5. 死亡状态(Dead):线程执行完了,或因为异常退出了run方法

但在Java中,只有:

  1. 新建状态(NEW)
  2. 运行状态(RUNNABLE)是操作形同的就绪状态加运行状态
  3. 阻塞状态(BLOCKED)
  4. 无限等待状态(WAITING)
  5. 有限等待状态(TIMED_WAITING)
  6. 死亡状态(TERMINATED)

12、sleep()、wait()、join()、yield()的区别

  1. sleep是Thread类的静态方法,wait则是Object类的方法
  2. sleep方法不会释放锁,但是wait会释放锁,而且会加入到等待队列进行阻塞
  3. sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
  4. sleep不需要被唤醒,但是wait需要(不指定时间需要被别人唤醒)
  5. sleep一般用于当前线程休眠,或者轮循暂停操作,wait则多用于多线程之间的通信
  6. yield()执行之后,线程直接进入就绪状态,马上释放了CPU的执行权,但依然保留了CPU的执行资格,所以可能马上又会执行这个线程
  7. join()执行后线程进入阻塞状态,例如在main线程调用thread1.join(),此时main线程会被阻塞,直至thread结束或中断线程

以上是关于Java基础面试题-第二集的主要内容,如果未能解决你的问题,请参考以下文章

Java 基础面试题 第二题

Java工程师面试题,二级java刷题软件

Java基础 面试题

java基础入门第二版第二章课后答案,知识点总结+面试题解析

面试系列-JAVA面试题

Java进阶之光!2021必看-Java高级面试题总结