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的区别
区别:
-
HashTable方法用synchronized修饰,线程安全,而HashMap线程不安全
-
hash函数不同,HashMap使用了扰乱函数,让hashCode的前16位异或后16位,而HashTable直接使用的是hashCode
-
底层实现不同,一个是数组+链表+红黑树,一个是数组加链表
-
插入链表方式不同,1.8以后HashMap改成了尾插法,而HashTable是头插法
-
HashTable的数组是在构造时创建,而HashMap是插入第一个元素时,在resize中创建
-
扩容不同
初始容量不同
HashTable为11,HashMap为16,且HashTable可以指定初始容量,而HashMap需要将指定容量扩大成2的幂
扩容时机不同
HashTable是每次容量超过过阈值时才扩容,而HashMap是第一次插入元素,以及容量超过阈值时扩容
扩容机制不同
HashMap扩容大小为2n,HashTable扩容大小为2n+1 -
HashMap的键值可以为null,而HashTable和ConcurrentHashMap的键值都不能为null
原因
1.因为Hashtable在我们put 空值的时候会直接抛空指针异常,但是HashMap却做了特殊处理。 2.因为Hashtable使用的是安全失败机制(fail-safe),fail-safe 允许在遍历的过程中对容器中的数据进行修改,所以这种机制会使你此次读到的数据不一定是最新的数据。 3.如果你使用 null 值,就会使得其无法判断对应的 key 是不存在还是为空,因为你无法再调用一次contain(key)来对 key 是否存在进行判断,ConcurrentHashMap 同理.
-
迭代器不同
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、正向代理和反向代理
- 代理其实就是一个中介,A和B本来可以直连,中间插入一个C,C就是中介。
- 刚开始的时候,代理多数是帮助内网client访问外网server用的
- 后来出现了反向代理,"反向"这个词在这儿的意思其实是指方向相反,即代理将来自外网客户端的请求转发到内网服务器,从外到内。
5、如何实现一个IOC容器
- 配置文件配置包扫描路径
- 递归包扫描获取.class文件
- 反射、将需要交给IOC管理的类,加入容器中
- 对需要注入的类进行依赖注入
- 在配置文件中指点需要扫描的包路径
- 定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入注解、获取配置文件注解
- 从配置文件中获取需要扫描的包路径,获取到当前路径下的文件信息以及文件夹信息,将所有路径下所有.class结尾的文件添加到一个Set集合中存储
- 遍历Set集合,获取在类上有指定注解的类,并将其交给IOC容器,定义一个线程安全的Map用来存储这些对象
- 遍历这个IOC容器,获取到每个类的实例,判断里面是否有依赖于其它类的实例,然后进行对注入
6、什么是字节码?采用字节码的好处是什么
- 字节码是由Java源文件经编译器而编译生成的class文件,JVM能够理解的代码叫做字节码,是一种中间的代码
- 首先因为我们的硬件操作复杂而繁琐,所以我们用操作系统封装了硬件的复杂调度,而向用户暴露了操作的接口,而对于不同的操作系统,JVM提供给了编译程序一个公共的接口,编译程序只需要面向虚拟机,生成字节码这样一个JVM能够理解的中间代码,然后由解释器转换成不同系统的机器码,从而使得Java达到了一个平台无关性。
- Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
7、Java类加载器有哪些
- JDK自带的三个类加器:Bootstrap ClassLoader、Extent ClassLoader、App ClassLoader
- Bootstrap ClassLoader 默认负责加载 %JAVA_HOME%/lib 目录下的jar包和类文件(rt.jar)
- Extent ClassLoader 默认负责加载 %JAVA_HOME%/lib/ext 目录下的jar包和类文件
- App ClassLoader 是自定义类加载器的父类,负责加载classpath下的类文件(默认的系统类加载器,自己写的代码,和引入的jar包)
- 如何要自定义类加载器可以实现ClassLoader,然后实现App ClassLoader
8、双亲委派模型
- 在JDK中有三个默认的类加载器,它们各自有自己的加载路径
- 当类加载器加载一个类时,它实际上不会直接去加载这个类,会进行一个向上委派的操作
- 向上委派实际上就是查找缓存,每一个类加载器都会将自己加载过的类放到自己的一个缓存中,然后从App ClassLoader开始,首先会找自己缓存中是否存在,如果有就无需加载了,否则会向上重复这个操作,直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载(通过加载路径向下查找),会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。
好处:
- 主要是为了安全性,避免用户自己编写的类动态替换Java的一些核心类,比如String
- 同时避免类的重复加载,因为JVM中区分不同的类,不仅仅是根据类名,相同的class文件被不同的ClassLoader加载就是两个不同的类
9、Java中的异常体系
- Java中所有异常都来自顶级父类Throwable
- Throwable下有两个子类Exception和Error
- Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行,比如虚拟机的错误和OOM
- Exception不会导致程序停止,又分为RunTimeException运行时异常和CheckedException检查异常
- RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。(空指针,数组下标越界,并发修改异常)
- CheckedException常常发生在程序编译过程中,会导致程序编译不通过(类型不匹配,语法问题)
10、GC如何判断对象可以被回收的
- 引用计数法:每个对象都有一个引用计数属性,新增一个引用是计数加1,引用释放时计数减1,计数为0使表示可以回收
- 可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GCRoot没有任何引用链相连时,则证明此对象时不可用的,那么虚拟机就判断是可回收对象
但是引用计数法,可能出现循环引用,这个时候计数器不可能为0,对象永远无法被回收
GC Roots的对象有:
- 虚拟机栈(栈帧的局部变量表)中引用的对象
- 方法区中类静态变量引用的对象
- 方法区常量引用的对象
- 本地方法栈Native方法引用的对象
11、线程的生命周期,线程有哪些状态
在操作系统中,线程有五种状态:
- 新建状态(New):新创建了一个线程对象
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法
- 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码
- 阻塞状态:阻塞状态时线程由于某些原因,放弃CPU使用权,暂停运行,直至被激活后进入就绪状态
- 死亡状态(Dead):线程执行完了,或因为异常退出了run方法
但在Java中,只有:
- 新建状态(NEW)
- 运行状态(RUNNABLE)是操作形同的就绪状态加运行状态
- 阻塞状态(BLOCKED)
- 无限等待状态(WAITING)
- 有限等待状态(TIMED_WAITING)
- 死亡状态(TERMINATED)
12、sleep()、wait()、join()、yield()的区别
- sleep是Thread类的静态方法,wait则是Object类的方法
- sleep方法不会释放锁,但是wait会释放锁,而且会加入到等待队列进行阻塞
- sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
- sleep不需要被唤醒,但是wait需要(不指定时间需要被别人唤醒)
- sleep一般用于当前线程休眠,或者轮循暂停操作,wait则多用于多线程之间的通信
- yield()执行之后,线程直接进入就绪状态,马上释放了CPU的执行权,但依然保留了CPU的执行资格,所以可能马上又会执行这个线程
- join()执行后线程进入阻塞状态,例如在main线程调用thread1.join(),此时main线程会被阻塞,直至thread结束或中断线程
以上是关于Java基础面试题-第二集的主要内容,如果未能解决你的问题,请参考以下文章