Q1:什么是面向对象?
面向对象和面向过程是两种不同的处理问题的角度。
面向对象简称OOP,是一种编程的方法论,通过“对象”来组织代码和数据。OOP的核心思想在于将对象的属性和行为封装在一起,提高代码复用性、灵活性和扩展性。
面向对象主要特点有以下:
1. 封装:通过封装隐藏对象的状态信息,不允许外部直接进行访问,通过类的方法获取该类的私有属性。连接数据库的时候,我们只需要调用方法就可以,不需要关心内部实现细节,这就是封装的好处。
2. 继承:创建一个新类从父类中继承属性和方法,可以直接扩展方法;子类拥有父类的所有属性和方法,还可以添加新的属性和方法,或者重写父类的方法自定义行为。
3. 多态:指同一个接口有不同的实现形式,子类重写父类方法实现多态。增强了代码的灵活性和可扩展性。
多态的必要条件:继承、重写、父类引用指向子类对象。
对于父类引用指向子类对象的目的就是统一接口,处理多种类型。这样使得别的函数只需要接受父类一个参数就可以实现多态。
Q2:什么是Javabean?
JavaBean 是一种符合特定规范的 Java 类。
1. 所有属性均为私有,不能从外部访问。
2. 包含公共的无参构造函数。
3. 包含属性的getter和setter方法。
4. 实现序列化和反序列化接口,保证在网络上进行传输。
Q3:JDK、JRE、JVM三者的联系和区别
JDK(Java Development Kit,Java开发工具包)
定义:JDK是Java开发环境的标准发行版,它不仅包含了JRE中的所有内容,还额外包含了用于开发Java应用程序的各种工具和库。
组成:JRE(包括JVM)
编译器(javac)
调试器(jdb)
文档生成器(javadoc)
归档工具(jar)
等等...
作用:JDK是用来开发Java应用程序的完整工具集。它允许开发者编写、编译和调试Java代码。
JRE(Java Runtime Environment,Java运行时环境)
定义:JRE包含了JVM以及运行Java应用程序所需的所有库文件和其他支持文件。
组成:JVM
核心类库(如java.lang, java.util等)
其他支持文件
作用:JRE提供了运行Java程序所需的全部环境,但不包含开发工具。如果你只是想要运行一个已经编写的Java程序,那么只需要安装JRE即可。
JVM(Java Virtual Machine,Java虚拟机)
定义:JVM是Java运行时环境的核心组成部分,它是一个抽象的计算机,拥有自己的指令集和内存管理机制。JVM负责加载、验证、执行字节码,并提供运行时环境。
作用:解释并执行编译后的Java字节码(.class文件)。
提供垃圾回收机制,自动管理内存。
实现了Java语言的“编写一次,到处运行”的特性,因为不同平台上的JVM可以解释相同的字节码为本地机器指令。
特点:JVM是特定于操作系统的,这意味着每个操作系统都需要一个对应的JVM实现来运行Java程序。
Q4:==和equals
== 比较的是地址(引用)或基本类型的值;
equals() 是方法,默认比较地址(等于==),但很多类(如 String、Integer 等)重写了它,用于比较内容。
Q5:为什么局部内部类和匿名内部类只能访问局部final变量?
final 是 Java 中的一个关键字,它可以用来修饰类、方法和变量,并赋予它们不同的特性。
当一个类被声明为 final 时,意味着这个类不能被继承。
当一个方法被声明为 final 时,意味着这个方法不能被子类重写。但是可以重载(毕竟参数不同不等于一个函数)。
当一个变量被声明为 final 时,意味着它的值一旦初始化后就不能再被修改。
- 对于基本类型的 final 变量,必须在声明时或构造函数中进行初始化,并且其值不可更改。
- 对于引用类型的 final 变量,引用本身不能指向其他对象,但对象的内容是可以修改的。
- 对于局部变量,可以先声明后续初始化,但是使用之前一定要赋值。
答案:局部内部类和匿名内部类只能访问 final 局部变量,是为了确保它们访问的是一个不会改变的值,从而避免因变量生命周期不同步或值变化导致的数据不一致问题。
从 Java 8 开始,如果你没有显式写 final,但变量实际上没有被修改过(称为 “effectively final”),也可以被内部类访问。
Q6:String、StringBuffer、StringBuilder区别和使用场景
String:
不可变性:被final修饰,String 类的对象一旦创建后就不能被修改。任何对 String 的操作(如拼接、替换等)都会生成一个新的 String 对象。
线程安全:由于其不可变性,String 是天然线程安全的。
性能考虑:频繁修改字符串会导致大量的临时对象被创建,从而影响性能。
适用场景:
当你需要一个常量字符串时。
在多线程环境中,如果你只需要读取字符串而不做修改。
StringBuffer:
可变性:与 String 不同,StringBuffer 是可变的。这意味着你可以在同一个对象上进行多次修改而不会创建新的对象。
线程安全:所有公共方法都是同步的,因此它是线程安全的。
性能考虑:虽然避免了创建大量临时对象的问题,由于方法是同步的,在单线程环境下会带来一定的性能开销。(同步的额外操作,比如加锁释放锁)
适用场景:
在多线程环境中需要频繁修改字符串的情况。
多个线程同时访问时保证线程安全。
StringBuilder:
可变性:类似于 StringBuffer,StringBuilder 也是可变的。
非线程安全:与 StringBuffer 不同,StringBuilder 的方法不是同步的,因此它不是线程安全的。
性能考虑:由于没有同步开销,在单线程环境下比 StringBuffer 更高效。
适用场景:
在单线程环境中需要频繁修改字符串的情况。
如果你确定不会在多线程环境下使用该对象,那么 StringBuilder 是更好的选择,因为它提供了更好的性能。
性能
String < StringBuffer < StringBuilder,String性能最差,StringBuilder性能最好(原对象上操作并且没有同步操作)
Q7:重载和重写的区别
重载
重载是指在同一个类中定义多个方法,它们有相同的名字但不同的参数列表(包括参数的数量、类型或顺序不同)。返回类型可以不同,但这不是区分重载的依据。
- 在同一个类中定义多个同名但参数列表不同的方法。
- 主要用于支持不同的输入参数组合。
- 发生在编译时,编译器根据参数列表选择合适的方法版本。
只有返回值不一样,但是参数列表完全一致,编译会报错。
重写
重写是指子类重新定义父类中已有的方法。重写的方法必须具有相同的方法签名(包括方法名、参数列表和返回类型),并且通常会改变其行为。
- 在子类中重新定义父类中的方法,提供特定于子类的行为。
- 主要用于继承层次结构中,允许子类提供自己的实现。
- 发生在运行时,通过父类引用调用方法时,实际执行的是子类中重写后的方法。
访问权限与父类相同或更宽松,保证多态时可以成功调用多态方法。
Q8:接口和抽象类的区别
方法实现
抽象类:本身还是一个类,可以包含具体的方法(有实现的方法)和抽象方法(没有实现的方法)。
接口:所有的方法默认都是抽象的,主要还是用于定义行为规范,Java8以后可以有默认方法。
继承与实现
抽象类:使用extend关键字来继承一个抽象类,并且Java只允许单继承,即一个类只能继承一个抽象类。
接口:使用implements关键字来实现接口,Java允许一个类实现多个接口,从而支持多重继承的行为。
构造器
抽象类:可以拥有构造器。尽管不能直接实例化一个抽象类,但它为子类提供了初始化的方式。
接口:不包含构造器。
字段
抽象类:可以有各种类型的字段,包括私有的、保护的、公共的等。它也允许有状态(成员变量)。
接口:所有字段默认都是public static final的,意味着它们是常量。如果你想在接口中定义非final的字段,你需要提供setter方法通过默认方法来间接修改值。
设计目的
抽象类:实现代码复用,本质上还是父类。
接口:定义规范,应该有什么技能。
Q9:List和Set区别
元素顺序
List:有序集合,它按照插入的顺序维护元素的位置。
Set:不能保证元素的顺序,大多数Set实现都不会保证插入的顺序。
元素唯一性
List:允许重复元素。
Set:不允许重复元素。尝试添加重复的元素不会导致错误,但是该操作不会成功。
常见实现类
List:常见的实现包括ArrayList、LinkedList等。ArrayList提供快速的随机访问,而LinkedList在频繁插入和删除操作时表现更好。
Set:常见的实现包括HashSet、LinkedHashSet和TreeSet。HashSet提供了较快的性能,基于哈希表实现;LinkedHashSet保持了插入顺序;TreeSet实现了SortedSet接口,可以对元素进行排序。
遍历
List:使用增强型for循环,迭代器,通过索引访问(get方法)。
Set:使用增强型for循环,迭代器,不能通过get方法访问。
Q10:hashCode和equals
equals:equals()方法用于比较两个对象是否逻辑上相等。默认情况下,equals()方法比较的是对象的引用(即两个对象是否指向同一内存地址),但很多类会重写这个方法以基于对象的内容进行比较。
ashCode:默认的 hashCode() 是根据对象的内存地址来计算的,输出一个整数,这个整数就是对象的哈希值。
对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有值,如果没有、Hashset会假设对象没有重复出现。但是如果发现有值,这时会调用equals()方法来检查两个对象是否真的相同。如果两者相同,Hashset就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样就大大减少了equals的次数,相应就提高了执行速度。
Q11:ArrayList和LinkedList区别
内部结构
ArrayList:基于动态数组实现,可以对元素进行随机访问,会自动进行扩容(1.5倍)。
LinkedList:基于双向链表实现,不支持高效随机访问,添加或删除元素效率高。
性能对比
操作 | ArrayList | LinkedList |
---|---|---|
访问元素 | 快速,O(1) | 较慢,O(n) |
在末尾添加元素 | 一般情况下快,但如果需要扩容则较慢 | 快,O(1) |
在开头添加元素 | 慢,需要移动元素,O(n) | 快,O(1) |
删除元素 | 如果知道索引,那么删除操作可能较快,但仍然需要移动元素,O(n) | 删除操作可以很快,尤其是当你已经有了指向要删除节点的引用时,O(1) |
迭代 | 效率高 | 效率也高,但在现代JVM上,ArrayList迭代可能会稍微快一些 |
使用场景
ArrayList:主要进行随机访问操作。应用中大多数是对列表末端进行添加或删除操作。
LinkedList:经常需要在列表开头或中间位置插入和删除元素。随机访问需求小。
Q12:HashMap和HashTable的区别?底层实现是什么?
HashMap 和 Hashtable 都是 Java 中用于存储键值对的数据结构,它们提供了一种将键映射到值的方式。
线程安全性
Hashtable:方法都是同步的(即线程安全),这意味着它可以在多线程环境中直接使用而无需额外的同步措施。然而,这也意味着所有操作都会受到锁机制的影响,从而导致性能下降。
HashMap:不是线程安全的。如果要在多线程环境中使用,需要外部同步机制或者使用 Collections.synchronizedMap() 方法来包装 HashMap。此外,在Java并发包中提供了专门的线程安全版本——ConcurrentHashMap。
Null 键和值
Hashtable:不允许键或值为 null。尝试插入 null 键或值会导致 NullPointerException。
HashMap:允许一个 null 键和任意数量的 null 值。
性能
Hashtable:由于保证了线程安全,因此在单线程情境下性能较低。
HashMap:没有同步开销,在单线程环境下性能更好。
底层实现
无论是 HashMap 还是 Hashtable,它们的底层实现都是基于哈希表的原理:
它们都通过计算键的哈希码来确定元素在数组中的位置。
当两个不同的键产生相同的哈希码时(哈希碰撞),这两个键会被存储在一个链表(或在某些条件下转换为红黑树)中。
在 Java 8 及之后版本中,为了优化性能,当链表长度超过一定阈值时,HashMap 会自动将其转换成红黑树,这样查找的时间复杂度可以从 O(n) 提升至 O(log n)。
Q13:dk7和jdk8中ConcurrentHashMap的实现原理
ConcurrentHashMap 是 Java 中用于在多线程环境中高效并发访问的哈希表实现。它提供了比 Hashtable 更好的并发性能,同时保证了线程安全性。
JDK7
实现原理:
分段锁(Segment):JDK 7 的 ConcurrentHashMap 使用了一种叫做“分段锁”的机制来减少锁的竞争。整个哈希表被划分成多个段(segments),每个段实际上是一个小的哈希表,拥有自己的锁。默认情况下,ConcurrentHashMap 被划分为16个段。
并发度:通过这种方式,可以允许不同段的数据同时被修改,从而提高了并发度。理论上,最多可以有 N(默认为16)个线程同时对不同的段进行写操作。
读操作无需加锁:对于读取操作,只要不涉及结构上的修改(如扩容),则不需要加锁,这样可以在高并发环境下提供较好的读性能。
扩容:JDK 7 中的 ConcurrentHashMap 是对单个 Segment 进行扩容的。也就是说,只有当某个 Segment 达到阈值时,才会对该 Segment 进行扩容。在进行扩容时,需要先获取对应 Segment 的独占锁。这意味着在扩容期间,其他试图访问或修改该 Segment 的线程将会被阻塞,直到扩容完成。
JDK8
实现原理:
JDK 8 移除了分段锁的概念,转而采用更细粒度的锁机制。现在每个桶(bucket)都可以独立地加锁,而不是整个段。这使得并发度大大增加。
链表转红黑树:当链表长度超过一定阈值(默认是8)时,会将链表转换为红黑树以加速查找过程。这一特性是从 JDK 8 开始引入的,与 HashMap 类似。
对于一些简单的更新操作(如插入新键值对),使用了非阻塞算法来避免使用锁,从而提高了性能。
扩容机制:在扩容期间,新的写入操作会被分配到新的桶中,旧的桶则继续处理读请求直到迁移完成。这种方法被称为“增量扩容”,它能够有效避免全表锁住的问题。
Q14:如何实现一个IOC容器?
什么是IOC容器
IOC(Inversion of Control,控制反转)容器是Spring框架的核心概念之一。它主要负责管理应用程序中的对象及其依赖关系,从而实现松耦合的设计模式。通过使用IOC容器,对象的创建和装配不再由开发者手动完成,而是交给容器来自动管理。
如何实现
1. 写配置文件,配置包扫描路径
2. 定义注解,选择哪些类需要交给IOC容器
2. 递归获取包路径下的.class文件
3. 反射,确定需要交给IOC管理的类,定义一个安全Map存储这些对象。
4. 遍历IOC容器,获取实例,对需要注入的类进行依赖注入
Q15:什么是字节码,采用字节码的好处是什么?
Java源代码--->编译器--->JVM可运行的字节码.class文件---->JVM中的解释器---->机器可执行的二进制机器码----->运行
输入:源代码 处理:编译器 输出:字节码.class文件
编译器只面向虚拟机,虚拟机中的解释器面向特定机器。
好处
一次编译,到处运行。跨平台性。
JVM在解释字节码时会验证安全性。安全性增强。
一定程度上解决了传统解释型语言执行效率低的问题,同时保留了解释性语言可移植的特点。高效性。
就是,保证可移植的前提下,保证了执行效率。
Q16:Java类加载器有哪些?
什么是类加载器
类加载器是负责加载类到JVM中的机制。它使得Java应用程序能够在运行时动态加载所需的类,而不需要在编译期就确定所有依赖关系。
有哪些?
Java中有三种主要类型的类加载器:
Bootstrap Class Loader:这是最顶层的类加载器,用来加载核心JDK类库(如rt.jar中的类)。负责加载位于$JAVA_HOME/jre/lib目录下的核心类库。
Extension Class Loader:作为Bootstrap Class Loader的子级,用于加载Java的扩展类库。加载位于$JAVA_HOME/jre/lib/ext目录下的jar包,或者由系统属性java.ext.dirs指定的路径下的类。
Application Class Loader:又称为System Class Loader,它是Extension Class Loader的子级。
主要用于加载应用的classpath下的类,即用户自定义的类以及第三方库。
除此之外,开发者还可以通过继承java.lang.ClassLoader来创建自定义类加载器。用于实现热部署:无需重启服务器即可更新应用。加密/解密类文件:保护源码不被轻易反编译。
Q17:双亲委托/委派模型
双亲委派模型规定了类加载器在尝试加载一个类时的查找顺序和策略。
双亲委派模型的基本流程
发起请求:当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
逐级委派:每一层的类加载器都会重复这个动作,从下到上,直到顶层的启动类加载器(Bootstrap ClassLoader)。
实际加载:如果父类加载器可以完成类加载任务,子类加载器就不再进行加载;如果父类加载器无法完成加载任务,则子类加载器才会尝试自己去加载。
注:类加载器在收到类加载请求时,首先会检查自己是否已经加载过这个类(缓存机制),如果没有,则优先委托给父类加载器处理,只有父类加载不了时,才会自己尝试加载。
Q18:Java中的异常体系
Java中的所有异常都来自顶级父类Throwable。
Throwable下有两个子类Exception和Error。
Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行。Exception不会导致程序停止,又分为两个部分RunTimeException运行时异常和CheckedException检查异常。RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException常常发生在程序编译过程中,会导致程序编译不通过。
Q19:GC如何判断对象可以回收
在Java中,垃圾回收(Garbage Collection, GC)主要负责自动管理内存,即自动释放不再使用的对象所占用的内存。以下是几种常见机制:
引用计数法
每个对象都有一个引用计数器,当有地方引用该对象时,计数器加1;当引用失效时,计数器减1。任何引用计数为0的对象表示不再被使用,可以被回收。
缺点:难以处理循环引用的情况。例如,两个对象相互引用,但实际上它们已经不可达,这种情况下引用计数法无法识别这些对象是可以被回收的。
可达性分析算法
通过一系列称为“GC Roots”的对象作为起点,从这些起点开始向下搜索,搜索走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,说明此对象是不可达的,可以被回收。(一系列对象作为起点,不可达即可释放内存)
GC Roots对象包括:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。
在可达性分析算法中,如果第一次发现没有引用链,会被标记,进入一个Finalizer队列,然后判定是否存活。若不存活就直接释放内存。
Q20:说一下JVM中哪些是共享区,哪些可以作为GC Root?
共享区
方法区(Method Area):
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
堆(Heap):
堆是JVM中最大的一块内存区域,几乎所有的对象实例都在这里分配内存。
运行时常量池(Runtime Constant Pool):
运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
非共享区(线程私有)
虚拟机栈(VM Stack):每个方法在执行的时候都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
本地方法栈(Native Method Stacks):类似于虚拟机栈,但是它是为执行本地(Native)方法服务的。
程序计数器(Program Counter Register):每个线程都有它自己的程序计数器,指向当前线程正在执行的字节码指令地址。
GC root
GC Roots对象包括:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中JNI(即一般说的Native方法)引用的对象。