图灵300题
图灵面试题视频:https://www.bilibili.com/video/BV17z421B7rB?spm_id_from=333.788.videopod.episodes&vd_source=be7914db0accdc2315623a7ad0709b85&p=20。
本文是学习笔记,如果需要面试没有时间阅读原博文,可以快速浏览笔记。
推荐深度阅读对应书籍或者知识点原文,避免碎片化学习。
1. 面向对象概念与Java示例
在面向对象编程中,有几个关键概念,如封装、继承和多态。
封装
封装是指将对象的内部细节对外部调用透明化,外部调用者无需修改或关心对象的内部实现。封装主要通过将类的属性设置为私有(private
),并提供公共的访问方法(getter
和 setter
)来实现,以保证数据的安全性和一致性。以下是具体示例:
- JavaBean 示例:
在 JavaBean 中,属性通常是私有的,通过getter
和setter
方法对外提供访问。例如:
private String name;
public void setName(String name){
this.name ="tuling_"+name;
}
在这个例子中,name
属性有自己的命名规则,不能由外部直接赋值,只能通过 setName
方法进行操作,从而保证了数据的安全性和一致性。
- ORM 框架示例:
ORM(Object-Relational Mapping)框架是一种将对象模型映射到关系型数据库的技术。使用 ORM 框架可以简化数据库操作,开发者不需要关心底层的 SQL 语句,只需要引入相应的库即可。以 MyBatis 为例,使用时无需关心数据库连接建立和 SQL 执行过程,通过调用方法即可操作数据库。
继承
继承是指子类可以继承基类的方法,并做出自己的改变和/或扩展。子类共性的方法或属性直接使用父类的,无需重新定义,只需扩展个性化的部分。
多态
多态是基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。实现多态的条件包括继承、方法重写以及父类引用指向子类对象。例如:
父类类型 变量名=new子类对象;
变量名.方法名();
2. Java虚拟机(JVM)相关知识
JDK、JRE与Java工具:JDK(Java Development Kit)是Java开发工具包,包含JRE(Java Runtime Environment)和一系列开发工具,如
javac
(用于编译Java文件)、java
(用于运行Java程序)、jconsole
等。JRE是Java运行时环境,提供了运行Java程序所需的所有组件,包括Java虚拟机、类库等。JVM内存结构:JVM内存主要包括程序计数器、虚拟机栈、本地方法栈、堆、方法区等。堆又分为年轻代(包含Eden区、Survivor区)和老年代。不同区域有不同的作用,例如程序计数器用于记录当前线程执行的字节码指令地址,堆用于存储对象实例等。
3. ==
和equals
比较
==
对比的是栈中的值,对于基本数据类型是变量值,对于引用类型是堆中内存对象的地址。equals
方法在Object
类中默认采用==
比较,通常会被重写。以String
类为例,其重写后的equals
方法如下:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n--!= 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
该方法不仅比较地址,还会比较字符串的内容是否相等。
4. hashCode
与equals
hashCode()
的作用是获取哈希码,用于确定对象在哈希表中的索引位置。它定义在JDK的Object.java
中,任何类都包含该函数。- 在哈希表存储键值对时,会利用哈希码快速检索对应的值。以
HashSet
检查重复为例,对象加入HashSet
时,会先计算对象的hashcode
值判断加入位置,若该位置已有值,再调用equals()
方法检查两个对象是否相同。若相同则不允许加入,不同则重新散列。 - 关于
hashCode
和equals
的关系:如果两个对象相等,则hashcode
一定相同;两个对象有相同的hashcode
值,它们不一定相等。因此,当equals
方法被覆盖时,hashCode
方法也必须被覆盖。
5. final
关键字
- 修饰类:表示类不可被继承。
- 修饰方法:表示方法不可被子类覆盖,但可以重载。
- 修饰变量:表示变量一旦被赋值就不可更改。
- 修饰成员变量时,如果是类变量,只能在静态初始化块中指定初始值或声明时指定;如果是成员变量,可以在非静态初始化块、声明变量或构造器中执行初始值。
- 修饰局部变量时,系统不会为其初始化,需程序员显式初始化。使用
final
修饰局部变量时,可在定义时指定默认值(后续代码不能再赋值),也可在后面代码中仅一次赋初值。
6. 内部类与局部变量
内部类和外部类处于同一级别,内部类不会因定义在方法中而随方法执行完毕被销毁。当外部类方法结束,局部变量被销毁,但内部类对象可能仍存在,这可能导致内部类访问不存在的变量。为解决此问题,将局部变量复制一份作为内部类成员变量,且通常将局部变量设置为final
,以保证内部类成员变量和方法局部变量的一致性。
7. String
、StringBuffer
、StringBuilder
的区别
String
是不可变的,尝试修改会新生成一个字符串对象;StringBuffer
和StringBuilder
是可变的。StringBuffer
是线程安全的,StringBuilder
是线程不安全的。在单线程环境下,StringBuilder
效率更高。
8. 重载和重写的区别
- 重载:发生在同一个类中,方法名必须相同,参数类型、个数、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
- 重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;若父类方法访问修饰符为
private
,子类不能重写该方法。
9.接口和抽象类的区别
语法层面区别
- 方法定义:抽象类可以存在普通成员函数,而接口中只能存在
public abstract
方法。 - 成员变量类型:抽象类中的成员变量可以是各种类型,而接口中的成员变量只能是
public static final
类型的。
设计目的区别
- 接口:
接口的设计目的,是对类的行为进行约束(更准确地说是一种“有约束”,因为接口不能规定类不可以有什么行为) ,也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。
例如:Bird
类和Aircraft
(像飞行器一样可以飞)类,都可以实现Flyable
接口,至于实现主体是谁、是如何实现的,接口并不关心。 - 抽象类:
抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(行为为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B) ,可以让这些类都派生于一个抽象类。在这个抽象类中实现B,避免让所有的子类来实现,这就达到了代码复用的目的。而A类剔除B的部分,留给各个子类自己实现。正是因为A - B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A - B时,无法执行)。
比如:BMW
是a Car
,抽象类是对类本质的抽象,表达的是is a
的关系,抽象类包含并实现了子类的通用特性,将子类存在差异化的特性进行抽象,交由子类去实现。
使用场景区别
- 当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。
- 抽象类的功能要远超过接口, 但是,定义抽象类的代价高。因为高级语言来说(从实际设计上来说也是)每个类只能继承一个类。在这个类中,你必须继承或编写出其所有子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述,相比抽象类的全面性,接口更容易被使用 。
10. List
和Set
的区别
List
:有序,按对象进入的顺序保存对象,可重复,允许多个Null
元素对象。可使用Iterator
取出所有元素并遍历,也可使用get(int index)
获取指定下标的元素。Set
:无序,不可重复,最多允许有一个Null
元素对象。取元素时只能用Iterator
接口取得所有元素并遍历。
11. ArrayList
和LinkedList
区别
- 底层数据结构:
ArrayList
底层基于数组实现,LinkedList
底层基于链表实现。 - 适用场景:由于底层结构不同,
ArrayList
更适合随机查找,LinkedList
更适合删除和添加操作,它们的查询、添加、删除时间复杂度不同。 - 接口实现:
ArrayList
和LinkedList
都实现了List
接口,LinkedList
还额外实现了Deque
接口,因此LinkedList
还可当作队列使用。
12. HashMap
和HashTable
的区别及底层实现
- 区别:
HashMap
方法没有synchronized
修饰,线程非安全;HashTable
线程安全。HashMap
允许key
和value
为null
,HashTable
不允许。 - 底层实现:两者底层都采用数组 + 链表实现(JDK8开始,链表高度到8、数组长度超过64时,链表转变为红黑树)。通过计算
key
的hash
值,二次hash
后对数组长度取模确定对应数组下标。若未产生hash
冲突,直接创建Node
存入数组;若产生冲突,先进行equals
比较,相同则取代该元素,不同则判断链表高度插入链表,链表高度达到8且数组长度到64时转变为红黑树,长度低于6时将红黑树转回链表。key
为null
时,在HashMap
中存在下标0的位置。
13. ConcurrentHashMap
的扩容机制
- 1.7版本:基于
Segment
分段实现,每个Segment
相当于一个小型的HashMap
。每个Segment
内部会进行扩容,扩容逻辑和HashMap
类似,先生成新数组,再转移元素,扩容判断是每个Segment
内部单独进行的。 - 1.8版本:不再基于
Segment
实现。当某个线程进行put
操作时,若发现ConcurrentHashMap
正在扩容,该线程一起进行扩容;若未发现扩容,则添加key - value
,然后判断是否超过阈值,超过则进行扩容。ConcurrentHashMap
支持多个线程同时扩容,扩容前先生成新数组,转移元素时先将原数组分组,由不同线程负责每组元素的转移。
14. JDK1.7到JDK1.8 HashMap
的变化(底层)
- 数据结构:JDK1.7中底层是数组 + 链表,JDK1.8中是数组 + 链表 + 红黑树,添加红黑树目的是提高插入和查询整体效率。
- 链表插入方式:JDK1.7中链表插入使用头插法,JDK1.8中使用尾插法。因为JDK1.8插入
key
和value
时需判断链表元素个数,遍历链表统计个数时适合直接使用尾插法。 - 哈希算法:JDK1.7中哈希算法复杂,存在右移与异或运算;JDK1.8中进行了简化,新增红黑树后可适当简化哈希算法,节省CPU资源。
15. HashMap
的put
方法流程
- 根据
Key
通过哈希算法与与运算得出数组下标。 - 如果数组下标位置元素为空,则将
key
和value
封装为Entry
对象(JDK1.7中是Entry
对象,JDK1.8中是Node
对象)并放入该位置。 - 如果数组下标位置元素不为空,则分情况讨论:
- JDK1.7中,先判断是否需要扩容,若要扩容则进行扩容,否则生成
Entry
对象,使用头插法添加到当前位置链表中。 - JDK1.8中,先判断当前位置上的
Node
类型,若是红黑树Node
,则将key
和value
封装为红黑树节点添加到红黑树中,过程中判断红黑树是否存在当前key
,存在则更新value
;若是链表Node
,则将key
和value
封装为链表Node
通过尾插法插入链表最后位置,遍历链表时判断是否存在当前key
,存在则更新value
,插入链表后判断链表节点个数,若超过8则将链表转成红黑树。最后判断是否需要扩容。
- JDK1.7中,先判断是否需要扩容,若要扩容则进行扩容,否则生成
16. 泛型中extends
和super
的区别
<? extends T>
表示包括T
在内的任何T
的子类。<? super T>
表示包括T
在内的任何T
的父类。
17. 深拷贝和浅拷贝
深拷贝和浅拷贝针对对象拷贝,对象属性包括基本数据类型和实例对象的引用。
- 浅拷贝:只会拷贝基本数据类型的值和实例对象的引用地址,不会复制引用地址指向的对象,即浅拷贝出来的对象,内部类属性指向同一个对象。
- 深拷贝:既会拷贝基本数据类型的值,也会复制实例对象引用地址指向的对象,深拷贝出来的对象,内部类执行指向的不是同一个对象。
18. HashMap
的扩容机制原理
- 1.7版本:先生成新数组,遍历老数组中每个位置链表上的每个元素,取每个元素的
key
,基于新数组长度计算其在新数组中的下标,将元素添加到新数组,所有元素转移完后,将新数组赋值给HashMap
对象的table
属性。 - 1.8版本:先生成新数组,遍历老数组中每个位置的链表或红黑树。若是链表,直接将链表中每个元素重新计算下标并添加到新数组;若是红黑树,先遍历红黑树计算每个元素在新数组中的下标位置,统计每个下标位置的元素个数,若超过8则生成新的红黑树并将根节点添加到新数组对应位置,若未超过8则生成链表并将头节点添加到新数组对应位置,最后将新数组赋值给
HashMap
对象的table
属性。
19. CopyOnWriteArrayList
的底层原理
CopyOnWriteArrayList
内部通过数组实现,向其添加元素时,会复制一个新数组,写操作在新数组上进行,读操作在原数组上进行。- 写操作会加锁,防止并发写入丢失数据。写操作结束后,原数组指向新数组。
- 该集合允许写操作时读取数据,提高了读性能,适合读多写少的场景,但比较占内存,且可能读到的数据不是实时最新的,不适合实时性要求高的场景。
20.什么是编译器和字节码?采用字节码的好处是什么?
编译器和字节码的概念
Java 引入了虚拟机的概念,即在源码和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟机在任何平台上都能提供编译程序一个共同的接口。
编译程序只需面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在 Java 中,这种供虚拟机理解的代码叫做字节码(其扩展名为 .class
文件),它不面向任何特定的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java 源程序经过编译器编译后会生成字节码,字节码由虚拟机解释执行。虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。这也就是 Java 的编译与解释并存的特点。
简单来说,编译程序把 Java 源码生成字节码,字节码再经过解释器,变成机器可执行的二进制机器码,进而运行程序。
采用字节码的好处
Java 通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序的运行比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同的计算机上运行。