[1]. 介绍面向对象的特征
面向对象编程(Object-Oriented Programming, OOP),主要基于“对象”的概念,将现实世界的事物抽象为对象,每个对象都有自己的属性和行为。
面向对象的三个重要的特征分别是:封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。
封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
继承:可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
多态:允许不同类的对象对同一消息做出响应的能力,即同一个接口可以被不同的实例以不同的方式实现。
[2]. Java方法重写
方法重写是指当子类觉得父类中的某个方法无法满足自己的需求时,子类可以重写一个与父类中的方法名称、参数列表一样的方法。
子类重写父类方法时,访问权限大于等于父类中该方法的权限。
返回值类型的范围小于等于被重写方法。
私有方法、静态方法不能被重写。
[3]. 介绍类加载机制
类加载分为三个步骤:加载、连接、初始化。
加载:将类的class文件读入到内存,并将这些静态数据转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象。
连接:把类的二进制数据合并到JRE中。
连接又可分为三个阶段:
- (1) 验证:主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理;
- (2) 准备:给类的静态变量分配内存,并设置初始值;
- (3) 解析:将类的二进制数据中的符号引用替换成直接引用;
初始化:对类的静态变量,静态代码块执行初始化操作。
[4]. 泛型解决了什么问题,上下界了解吗?擦除呢?
①解决元素存储的安全性问题。
②解决获取数据元素时需要类型强制转换的问题。
上界 <? extends T>,限定了参数化类型为T和T的子类,针对于上界来说,只能取,不能存,取出来的东西只能放到T或T的父类中。
下界 <? super T>,限定了参数化类型为T和T的父类的所有类型,可以往里存,但往外取只能放在Object对象里。
Java中的类型擦除是指:在编译期,编译器会将泛型的类型参数都擦除成它指定的原始限定类型,如果没有指定的原始限定类型则擦除为object类型,之后在获取的时候再强制类型转换为对应的类型,因此生成的Java字节码中是不包含泛型中的类型信息的。
[5]. ArrayList和LinkedList的区别
ArrayList的底层数据结构是动态数组,而LinkedList是双向链表。
ArrayList随机访问的时间复杂度为O(1),而LinkedList为O(n)。
对于插入和删除操作,LinkedList 要比 ArrayList 效率要高,因为ArrayList 增删操作要影响数组内的其他元素的下标。
LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了指向其前后两个元素的引用。
[6]. HashMap的底层实现,为什么要用红黑树,如何解决哈希冲突
在JDK 1.7版本之前, HashMap的底层数据结构是数组和链表,将链表和数组相结合,即创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可,这种方式存在的问题是:当Hash冲突严重时,链表会变得越来越长,导致查询的效率降低。
在JDK1.8版本之后,HashMap是基于数组+链表/红黑树来实现的,当链表的存储的元素个数大于等于8的时候,不再采用链表,而是采用红黑树存储结构,将查询的时间复杂度由链表的O(n),优化为红黑树的O(logn)。
[7]. LinkedHashMap的底层实现,为什么要使用双向链表
LinkedHashMap是HashMap的子类,在 HashMap 存储结构的基础上额外维护了一个双向链表,用于记录元素的插入顺序。
通过双向链表将元素连接起来,可以使得元素按照插入的顺序进行迭代。
[8]. Hashtable了解吗?和ConcurrentHashMap有什么区别?
Hashtable和ConcurrentHashMap两者的区别主要在于底层的数据结构和线程安全的实现方式:
Hashtable底层是基于数组+链表,数组是主体,链表是为了解决hash冲突而存在的。
Hashtable使用synchronized来保证线程安全,效率非常低。比如当一个线程访问同步方法另一个线程也访问的时候,就会陷入阻塞或者轮询状态。
JDK1.7之前的ConcurrentHashMap底层采用的是分段的数组+链表,JDK1.8之后采用的是数组+链表/红黑树;
JDK1.7以前,ConcurrentHashMap采用分段锁,对整个数组进行了分段分割,由于每一把锁只锁容器里的一部分数据,多线程访问不同数据段里的数据时不会存在锁竞争,因此提高了并发访问效率;
JDK1.8以后,直接采用数组+链表/红黑树,并发控制使用CAS和synchronized操作,锁的粒度更小,并且引入红黑树查询效率更高。
[9]. 为什么要用线程池了解吗?有哪些参数?
使用线程池的三大好处:
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的构造函数有7个参数,分别是corePoolSize(线程池核心线程大小)、maximumPoolSize(线程池最大线程数量)、keepAliveTime(空闲线程存活时间)、unit(空闲线程存活时间单位)、workQueue(工作队列)、threadFactory(线程工厂)、handler(拒绝策略)。
[10]. Synchronized锁了解吗?它是可重入的吗?有哪些应用方式?锁的升级了解吗?
synchronized 关键字解决的是多个线程之间访问资源的同步性,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。
synchronized锁是可重入的。这意味着同一个线程可以多次获取同一个对象锁而不会发生死锁。具体来说,当一个线程首次获取该锁时,锁对象内部的计数器会加1。如果同一个线程再次获取该锁,计数器会再次加1,实现所谓的“重入”。
synchronized有三种应用方式:
作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁;
作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁;
作用于代码块,对括号里配置的对象加锁。
synchronized锁有四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态随着竞争情况逐渐升级。
- 只有一个线程执行的场景,使用偏向锁;
- 发生多个线程执行的场景,偏向锁升级为轻量级锁;
- 发生多个线程并发执行的场景,轻量级锁升级为重量级锁;
[11]. 介绍JVM的内存结构
JVM主要由四部分组成:类加载器(ClassLoader),运行时数据区(Runtime Data Area),本地库接口(Native Interface),执行引擎(Execution Engine)。
其中运行时数据区即为JVM的内存结构,主要分为堆(Heap)、虚拟机栈(VM Stack)、本地方法栈(Native Method Stack)、方法区(Method Area)、程序计数器(Program Counter Register)。
虚拟机栈、本地方法栈、程序计数器为线程私有,方法区和堆为线程共享区。
[12]. GC了解吗?为什么要从引用计数法演变为使用可达性分析算法?
GC是一种自动化的内存管理技术,用于检测和释放不再使用的对象所占用的内存空间。
对于嵌入式系统这种对垃圾回收实时性要求高,且内存资源有限的场景,引用计数法非常适用。
但是,当两个或多个对象相互引用时,即使它们已经不再被其他对象引用,引用计数也不会归零,导致内存无法被正确回收。而可达性分析法在处理复杂对象关系上能力更强,因此,在存在循环引用的情况下,可达性分析法更加适用。
[13]. 老年代里采用什么垃圾回收算法?标记清除算法和复制算法的优缺点
老年代中的对象存活时间比较久,每次垃圾回收之后,存活对象比较多,而需要回收的垃圾对象比较少,所以适合采用标记-清除(CMS(Concurrent Mark-Sweep)算法)、标记-整理算法。
- 标记清除算法速度快,但是会产生大量内存碎片;
- 复制算法不会产生内存碎片,但是会占用双倍的内存空间;