本篇开始主要就是总结一下,在秋招和春招期间Java常见的面试题,也欢迎大家在评论区进行面试题的补充。
1. 面向对象OOP的理解,并说明其3大特性
面向对象是一种编程思想, 它是将现实世界中的所有事物都抽象成类,类有属性和方法,可以将类实例化后使用这个对象的属性和方法。
面向对象有3个特点:封装、继承、多态
- 封装:隐藏内容的实现细节,而提供一些公共的接口、方法,来直接使用功能,也就是把该隐藏的隐藏起来,该展示的展示出来,它是通过访问修饰符来控制。
- 继承:比如动物这个概念是父类,猫、狗是子类,那么子类继承父类,子类就可以拿到父类中的一些特性,并且子类也有自己的特点。所以继承就是子类继承父类,子类就可以使用父类的属性和方法(共性抽取,实现代码的复用)。
- 多态:同一个对象,在不同时刻体现出来的不同状态,比如说,买东西你使用二维码付款,可以使用微信,也可以使用支付宝,也可以使用银联来支付,这种就是同一个行为可以有不同的表现形式,那么多态也就是同一个接口,使用不同的实例就可以执行不同的操作,但这个必须是在继承的体系下,并且子类要重写父类的方法,然后通过父类的引用调用重写的方法(向上转型 Fu f = new Zi() ),好处,可替换性,可扩充性,灵活性,简化性。
2. 访问修饰符都有什么
访问修饰符作用:通过控制类和访问权限来实现封装,类可以将数据和封装数据的方法结合在一起,而访问权限用来控制方法或者字段能否直接在类外使用。
3. == 和 equals 的区别
对于 Object 来说,equals 是用 == 来实现的,所以 == 和 equals 是相同的,都是去比较对象的引用是否相同。但 Java 中其它的类比如 Interger、String 都是使用重写后的 equals,都是去比较的值。
4. 什么是方法的重载,返回值不同还是不是重载
方法重载就是指在同一个类中定义多个方法,它们方法名相同但参数列表不同。
那么如果方法名相同参数列表不同,但返回值不同还算不算是重载?
java
public String myMethod(int arg1) {
// 方法体
}
public int myMethod(int arg1) {
// 方法体
}
JVM 调用方法是通过方法签名来判断到底要选择调用哪个方法,而方法签名 = 方法名称 + 参数类型 + 参数个数 这样组成的一个唯一值,这个唯一值就是方法签名。所以可以看出返回类型不是方法签名的组成部分。
5. 重载和重写的区别
相同点:方法名一样;
不同点:
重载:保证两点:
-
方法名相同;
-
参数列表不同;
其他都是无关项(比如访问修饰符、返回值等)
重写:保证三点:
-
在继承的体系下;
-
核心重写,外壳不变,即就是方法内部代码的实现要改变,而方法的名称、参数列表、返回类型这些都不变。
-
访问权限修饰符要大于等于父类;抛出的异常类型要小于等于父类,也就是要抛出父类的子类异常;返回值类型要小于等于父类。
6. 重载的底层逻辑理解
首先明确,重载是 Javac 编译器在编译阶段根据静态类型,去选择对应的重载版本。
其次对于重载版本优先级的选取过程:先会匹配参数的个数,然后再去匹配参数类型的所属类;如果找不到直接的所属类,那会向上转型(包装类->接口->父类)就是在继承的关系中从低到高搜索看有没有实现;如果向上转型找不到,那再查找可变的参数列表,如果都找不到就方法报错。
7. 常见的集合类都有哪些
Java 集合类主要由Collection和map这两个接口派生出来的,Collection有3个子接口分别是:List、Set、Queue。
- List 代表有序可重复的集合,可以直接根据元素的索引来访问;
- Set 代表无序不可重复的集合,只能根据元素本身来访问;
- Queue 是队列集合。
- Map 代表的是存储 key-vaule 的键值对集合,可以根据元素的key来访问value。
集合中场景的实现类有 ArrayList、LinkedList、HashSet、TreeSet、PriorityQueue(优先级队列)、HashMap、TreeMap、ConcurrentHashMap等。
8. ArrayList 和 LinkedList 的区别
- 底层实现不同:ArrayList 是基于动态数组的数据结构,而 LinkedList 是基于双向链表的数据结构。
- 随机访问性能不同:ArrayList 优于 LinkedList,因为 ArrayList 可以根据下标以 O(1) 时间复杂度对元素进行随机访问。而 LinkedList 的访问时间复杂度为 O(n),因为它需要遍历整个链表才能找到指定元素。
- 插入和删除的性能不同:LinkedList 优于 ArrayList,因为 LinkedList 的插入和删除操作时间复杂度为 O(1),而 ArrayList 的时间复杂度为 O(n)。
所以基于这种特点,如果随机访问比较多的业务场景可以选择使用 ArrayList,如果添加和删除比较多的业务场景可以选择使用LinkedList。
补充:动态扩容
ArrayList 当没插入元素时,不分配内存空间,当第一次插入元素时,先分配 10 个对象空间,然后后面扩容是按 1.5 倍来扩容。这个扩容本质上是新建一个更大的数组,然后把旧数组中的内容复制过去
所以可以分析一下,ArrayList 插入数据,时间主要是花费在了复制数组上,而 LinkedList 插入元素,时间主要是花费在了构建结点上了。
当元素少的时候,插入元素 LinkedList 比 ArrayList 快,因为构建的结点花费时间少,而数组复制时间长
当元素量很大的时候,插入元素 ArrayList 比 LinkedList 快,因为此时的这个 ArrayList 数组已经扩容到足够大了,可以直接添加元素,而 LinkedList 此时还是要不断构建结点进行插入,所以比较慢。
9. String、StringBuffer、StringBuilder区别
String 底层是字符数组,并且数组被 final 修饰,所以一旦指向某个对象后,就不能再指向其它对象了,所以 String 是不可变的。
StringBuffer 和 StringBuilder 它底层还是一个字符数组,不过它没有被 final 修饰,所以是可变的;StringBuffer 线程安全,但效率低,StringBuilder 线程不安全,但效率高;两个类都提供了非常方便操作字符串的方法,比如:append(拼接)、insert(插入)、reverse(反转)....;
StringBuffer和StringBuilder的扩容机制是一样的,都是从当前容量开始扩容,它默认的初始容量为16
- 一次追加长度超过当前容量,则会按照 当前容量*2+2 扩容一次
- 一次追加长度不仅超过初始容量,而且按照 当前容量*2+2 扩容一次也不够,其容量会直接扩容到与所添加的字符串长度相等的长度。之后再追加的话,还会按照 当前容量*2+2进行扩容
10. String a = "abc" 和 new String("abc") 区别
-
**String a = "abc':**JVM 会使用常量池来管理这个字符串,如果常量池中已经有这个字符串了,那就将引用直接赋值给变量 a,如果常量池中没有这个字符串,那就将这个字符串存入常量池中
-
**new String("abc"):**JVM 会先在常量池中存入这个 "abc" 字符串,然后再创建一个新的 String 对象,这个对象会被保存到堆内存中,堆中对象的数据会指向常量中的字符串
对比下来,new 会消耗更多内存,所以建议使用第一种
**11.**hashcode 和 equals 关系
重写 equals 方法,也需要对 HashCode 进行重写
总结:
-
首先明确:一般使用的是重写后的equals,只会比较内容,而Hashcode是根据哈希函数去计算哈希地址,也就是从地址的角度去考虑,equals代表内容,而Hashcode代表地址;
-
然后就是比如给Hashset 中插入一个元素,hashset 特点是无序和不可重复的,所以给Hashset中插入元素,会先根据哈希函数去计算哈希值获取存储对象的地址,然后在散列表中去找这个地址,然后通过equals 去对比这个地址上对象的内容是否一致,如果一致就不插入,如果不一致就插入到链表后;
-
所以基于2中的这个过程来看,重写equals也要重写hashcode来保证内容和地址上的一致性;
-
最后就是两个对象相同,那么哈希地址一定会相同
-
如果哈希地址相同,那么两个对象并不一定相同
12. ArrayList 和 Vector 区别
**相同点:**如图可以看出,ArrayList 和 Vector 都实现了 List 这个接口,它们都是动态数组的实现,也拥有相同的方法,可以对元素进行添加、删除、查找等操作。
异同点:
- 线程安全:Vector 是线程安全的,而 ArrayList 不是。所以在多线程的环境下,应该使用 Vector。
- 扩容方式:当数组容量不足时,ArrayList 默认是按照 1.5 倍来扩容的,而 Vector 是按照 2 倍来扩容的。这就说明在添加元素时,ArrayList 相比与 Vector 需要更频繁的进行扩容操作。
- 性能选择:Vector 是线程安全的,所以它的性能通常会比 ArrayList 差。
基于上述内容,如果不需要考虑线程安全问题,并且需要快速的存取操作,使用 ArrayList 更优。如果需要考虑线程安全问题以及更好的数据存储能力,则使用 Vector 更优。
13. HashMap 和 HashSet 区别
- 存储方式不同: HashMap 存储的是键值对,将键映射到值,可通过键来访问值;而HashSet 存储的是唯一值的集合,它底层就是只使用 HashMap 键值对里面的 key。
- 实现方式不同:HashMap 内部采用的是哈希表数据结构来存储键值对,而 HashSet 采用的是哈希表或者二叉树数据结构来存储唯一值的集合。
- 存储特点不同:HashSet 存储的是无序不可重复的元素集合;而 HashMap 存储的是键值对,键是唯一的,而值可以重复。
- 数据访问方式不同:HashMap 可以通过键来访问对应的值,通过 get() 方法;而 HashSet 只能通过迭代器(Iterator)或 forEach() 来遍历元素,没有直接获取单个元素的方法。
- 扩容方式不同:HashMap 扩容的时候会重新调整内部存储结构,将所有键值对重新散列到新的存储区域中;而 HashSet 扩容则仅仅只是增加了哈希桶的数量,然后将原有的元素重新分配到新的桶中。
14. HashTable、HashMap、ConcurrentHashMap 区别
总体来看,HashTable、HashMap、ConcurrentHashMap 都是 Map 接口的实现类,都是以 key-value 的形式来存储数据。
下面对这三个分别进行阐述对比
HashMap
- HashMap 的键值可以为null (当key为空时,哈希会被赋值为0)
- HashMap 的默认初始容量是16, 最大容量是2^30;
- HashMap 使用的数据结构是 数组 + 链表 + 红黑树。当链表长度 > 8,并且桶的数量必须大于64时,链表才会转化为红黑树(桶的数量小于64时只会扩容);如果链表长度 < 6 那么红黑树又会转化为链表。
- HashMap 效率非常高,但线程不安全;
HashTable
- HashTable 的键值不能为 null;
- HashTable 虽然线程安全,但只是简单得用 Synchronized 给所有方法加锁,相当于是对 this 加锁,也就是对整个 HashTable 对象进行加锁(非常无脑的加锁方式)。这就会导致一个 HashTable 对象只有一把锁,如果两个线程同时访问一个对象时,就会发生锁冲突;
- HashTable 效率非常低,因为是无脑加锁,比如一些读操作不存在线程不安全问题,还是加锁了,导致效率非常低;
- HashTable 底层的数据结构是 数组 + 链表;
ConcurrentHashMap
- ConcurrentHashMap 键值不可以为 null;
- ConcurrentHashMap 使用的数据结构为 数组 + 链表 + 红黑树
- ConcurrentHashMap 最大的特点就是线程安全,ConcurrentHashMap 相比较与 HashTable 做了很多的优化。最核心的思路就是:降低锁冲突的概率;
主要的做法就是:
(1)锁粒度的控制
ConcurrentHashMap 不是锁整个对象,而是使用多把锁,对每个哈希桶(链表)都进行加锁,只有当两个线程同时访问同一个哈希桶时,才会产生锁冲突,这样也就降低了锁冲突的概率,性能也就提高了
(2)ConcurrentHashMap 只给写操作加锁,读操作没加锁
如果两个线程同时修改,才会有锁冲突
如果两个线程同时读,就不会有锁冲突
如果一个线程读,一个线程写,也是不会有锁冲突的
(这个操作也是可能会锁冲突的,因为有可能,读的结果是一个修改了一半的数据
不过ConcurrentHashMap在设计时,就考虑到这一点,就能够保证读出来的一定时一个"完整的数据",要么是旧版本数据,要么是新版本数据,不会是读到改了一半的数据;而且读操作中也使用到了volatile保证读到的数据是最新的)
(3)充分利用到了CAS的特性
比如更新元素个数,都是通过CAS来实现的,而不是加锁
(4)ConcurrentHashMap 对于扩容操作,进行了特殊优化
ConcurrentHashMap 在扩容时,不再是直接一次性完成搬运数据,而是搬运一点,具体是这样的
扩容过程中,旧的和新的会同时存在一段时间,每次进行哈希表的操作,都会把旧的内存上的元素搬运一部分到新的空间上,直到最终搬运完成,就释放旧的空间
在这个过程中如果要查询元素,旧的和新的一起查询;如果要插入元素,直接在新的上插入
;如果是要删除元素,那就直接删就可以了;
15. 哈希冲突的解决方案有哪些
哈希冲突是指在哈希表中,两个或多个元素被映射到了同一个位置的情况。
常见的解决哈希冲突的常用方法有以下三种:链地址法、开放地址法和再哈希法。
- 开放地址法: 当发生哈希冲突时,通过一定的探测方法(比如线性探测、二次探测等)在哈希表中找到下一个可用的位置。这种方法优点是不需要额外的存储空间,适用于元素较少的情况下;缺点是容易产生聚集现象,也就是某些桶中的元素过多,而其他的桶中元素很少。
- 链地址法:将哈希表中的每个桶都设置为一个链表,当发生哈希冲突时,将新的元素插入到链表的末尾。这种方法优点是适用于元素较多的情况;缺点是当链表过长时,查询效率也会变低。
- 再哈希法:当发生哈希冲突时,使用另一个哈希函数计算出一个新的哈希值,然后将元素插入到对应的桶中。优点是适用于元素数量较少的情况;缺点是需要额外的哈希函数,且当哈希函数不够随机时,容易产生聚集现象。
线性探测&二次探测:
- 线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
- 二次探测:线性探测找位置是一个一个往后找,缺点是将冲突的元素都放在一起,并且删除也不方便。那么二次探测为了解决这个问题,找"下一个"位置有了不同的方法,找下一个空位置的方法为Hi = (H0 + i^2) % m。二次探测的优点是相对于线性探测来说,它更加均匀地分布元素,但缺点是容易产生二次探测聚集现象,即某些桶中的元素过多,而其他桶中的元素很少。
HashMap 如何解决哈希冲突?
在 Java 中,HashMap 使用的是链地址法解决哈希冲突的,对于存在冲突的 key,HashMap 会把这些 key 组成一个单向链表,之后使用尾插法把这个 key 保存到链表尾部。
16. 什么是负载因子,为什么是0.75
HashMap 的负载因子是 HashMap 在扩容时的第一个阈值,当 HashMap 中的元素个数超过了容量乘以负载因子时,就会扩容。默认的负载因子是 0.75,也就是说当 HashMap 中的元素个数超过了容量的 75% 时,就会进行扩容。当然,我们也可以通过构造函数来指定负载因子。
HashMap 扩容的目的是为了减少哈希冲突,提高 HashMap 性能的。
至于为什么说负载因子是0.75,官方给出来的理由是默认负载因子为 0.75,是因为它提供了空间和时间复杂度之间的良好平衡。 负载因子太低会导致大量的空桶浪费空间,负载因子太高会导致大量的碰撞,降低性能。0.75 的负载因子在这两个因素之间取得了良好的平衡
17. 抽象类是什么,它和接口有什么区别
抽象类就是一个类中没有足够的信息去描绘出一个具体的对象,所以它是不能实例化的 ,**所以抽象类的作用就是为了被继承的,**但是其他都是存在的,成员方法,成员变量,构造方法都是有的。抽象类中普通方法可以有具体的实现,抽象方法是不能有具体的实现的,并且当抽象类被继承的时候,这个抽象方法要被重写。并且他是不能被private static final修饰的。
接口是用来实现的,是用来解决多继承的手段,在 jdk1.7中方法是被 public abstract 修饰的,普通的成员方法是不能有具体的实现的,在 jdk 1.8中如果要有具体的实现必须给方法前加 default,并且方法也可以被 static 修饰
比较:
-
目的不同:抽象类是给其他类提供了一个通用的模板和基类,实现代码的复用和统一,并且保证子类实现了父类中的抽象方法;接口是提供了某个具体的行为或动作,来让其他的类来使用,可以理解为接口提供了某个和实现类不相关的行为,作为实现类的补充,并且接口也是解决多继承的手段;
-
抽象类的成员方法和普通的类相同;接口的成员方法是被 public statac final 修饰的;
-
抽象类是用来被继承的;接口是用来被实现的;
-
关键字不同:抽象类是abstract,然后被继承是extends;接口是 interface,实现接口是 implements
-
抽象类中有普通方法,抽象方法,构造方法,成员变量等;接口是没有构造方法的,而普通方法在接口中也是不能有具体的实现的,在 jdk1.8后可以加default然后实现了;
既然已经有子类来继承父类这个概念,为什么还要有子类继承抽象类的概念
目的不同:子类继承父类,是为了实现共性抽取,代码复用,子类继承抽象类,是为了使用抽象类这个通用的模板,以实现代码复用和统一,并且也为了保证必须实现抽象类中的抽象方法;
18. 为什么 HashMap 会死循环
总结:HashMap 死循环发生在 JDK 1.7 版本中,形成死循环的原因是 HashMap 在 JDK 1.7 使用的是头插法,头插法 + 多线程并发操作 + HashMap 扩容,这几个点加在一起就形成了 HashMap 的死循环,解决死循环可以采用线程安全容器 ConcurrentHashMap 替代。
HashMap 导致死循环的原因是由以下条件共同导致的:
- HashMap 使用头插法进行数据插入(JDK 1.8 之前);
- 多线程同时添加;
- 触发了 HashMap 扩容。
常见的解决方法
- 升级到高版本 JDK(JDK 1.8 以上),高版本 JDK 使用的是尾插法插入新元素的,所以不会产生死循环的问题;
- 使用线程安全容器 ConcurrentHashMap 替代(推荐使用此方案);
- 使用 synchronized 或 Lock 加锁 HashMap 之后,再进行操作,相当于多线程排队执行(比较麻烦,也不建议使用)
19. 深克隆和浅克隆的区别
深克隆和浅克隆最大的区别就在于克隆出来的新对象是否与原始对象共享引用类型的属性。
- 浅克隆:克隆出来的新对象与原始对象共享引用类型的属性。也就是说,新对象引用指向的就是原始对象,所以才可以共享引用类型属性。如果修改了新对象中的引用类型属性,原始对象中的相应属性也会被改变。在 Java 中,可以通过实现 Cloneable 接口和重写 clone() 方法来实现浅克隆。(可以理解为只是名义上克隆了,本质还是指向的是同一个对象)
- 深克隆:克隆出来的新对象与原始对象不共享引用类型属性。也就是说,新对象引用指向的就是新对象,而不是指向原始对象。如果修改了新对象中的引用类型属性,原始对象中的相应属性不会被改变(可以理解为真的克隆了,所以指向的不是同一个)
深克隆的实现方法:
- 所有引用属性都实现克隆,整个对象就变成了深克隆。
- 使用 JSON 工具,如 GSON、FastJSON、Jackson 序列化和反序列化对象实现深克隆。
在 Java 中,序列化是指将对象转换为字节流的过程,以便可以将其存储在文件中、通过网络发送或在进程之间传递。反序列化是指将字节流转换回对象的过程。
20. 说一下 Java 内存模型(Java Memory Model,JMM)
当问到 Java 内存模型的时候,需要注意,Java 内存模型(Java Memory Model,JMM)它和 JVM 内存布局(JVM 运行时数据区域)是不一样的,它们是两个完全不同的概念。
首先要知道为什么要有 Java 内存模型
Java 内存模型存在的原因在于解决多线程环境下并发执行时的内存可见性和一致性问题。
Java 内存模型(Java Memory Model,简称 JMM)是一种规范,它定义了 Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式,即规范了 Java 虚拟机与计算机内存之间是如何协同工作的。具体来说,它规定了一个线程如何和何时可以看到其他线程修改过的共享变量的值,以及在必须时如何同步地访问共享变量。
Java 内存模型主要包括以下内容:
- 主内存(Main Memory):所有线程共享的内存区域,包含了对象的字段、方法和运行时常量池等数据。
- 工作内存(Working Memory):每个线程拥有自己的工作内存,用于存储主内存中的数据的副本,线程只能直接操作工作内存中的数据。
- 内存间交互操作:线程通过读取和写入操作与主内存进行交互。读操作将数据从主内存复制到工作内存,写操作将修改后的数据刷新到主内存。
- 原子性(Atomicity):JMM 保证基本数据类型(如 int、long)的读写操作具有原子性,即不会被其他线程干扰,保证操作的完整性。
- 可见性(Visibility):JMM 确保一个线程对共享变量的修改对其他线程可见。这意味着一个线程在工作内存中修改了数据后,必须将最新的数据刷新到主内存,以便其他线程可以读取到更新后的数据。
- 有序性(Ordering):JMM 保证程序的执行顺序按照一定的规则进行,不会出现随机的重排序现象。这包括了编译器重排序、处理器重排序和内存重排序等。
Java 内存模型通过以上规则和语义,提供了一种统一的内存访问方式,使得多线程程序的行为可预测、可理解,并帮助开发者编写正确和高效的多线程代码。开发者可以利用 JMM 提供的同步机制(如关键字 volatile、synchronized、Lock 等)来实现线程之间的同步和通信,以确保线程安全和数据一致性。
21. 异常如何处理,finally 中的代码一定会执行吗
-
捕获异常:将业务代码放在 try 内部,当业务代码发生异常时,系统都会自动创建一个异常对象,然后 JVM 就会在 catch 内,找这个异常对应的处理模块
-
处理异常:在 catch 中处理异常,记录日志,然后根据对应的异常类型,和当前业务场景进行处理
-
回收资源:将业务代码中打开的某个资源,执行完毕后进行关闭,不论是否发生异常都要使用 finally 进行处理
谈一谈抛出异常
当程序出现错误时,系统会自动抛出异常,也可以由程序主动抛出异常,通过 throw 关键字
谈一谈 Java 异常接口
Throwable 是异常的最顶层父类,它有两个直接子类,分别为 Error、Exception
Error是 错误,一般是和虚拟机相关的(比如栈溢出也是),一般这种错误都会使程序中断,也不应使用 catch 来捕获 error 对象
Exception 是异常,其子类分为 编译时异常 和 运行时异常两大类(RuntimeException)
finally 中的代码不一定会执行
正常运行的情况下,finally 中的代码是一定会执行的,但是,如果遇到 System.exit() 方法或 Runtime.getRuntime().halt() 方法,或者是 try 中发生了死循环、死锁,遇到了掉电、JVM 崩溃等问题,finally 中的代码是不会执行的。而 exit() 方法会执行 JVM 关闭钩子方法或终结器,但 halt() 方法并不会执行钩子方法或终结器。
22. this 和super的区别
相同点:
-
在构造方法中调用,必须是在构造方法的第一行,并且super和this不能同时存在;
-
super()和this()都是需要对象的,所以不能在static环境中使用;
-
都是关键字;
不同点:
-
this是对当前对象属性和方法的引用,super是子类对象调用父类方法和属性的引用,两个不能同时存在;
-
在构造方法中,一定会存在super 的调用,不论写不写都有,而this 是不写就不调用;
23. 说一说对泛型的理解
普通的类和方法,只能使用具体的类型,如果要应用多种类型的代码,就非常的不方便,而从 JDK1.5 之后,就引入了泛型这个概念,可以类比泛型和函数, 函数传参传入的是值,而泛型传的是类型,这样泛型就可以用于多种类型,它是将类型当做参数了
泛型存在的意义,就是指定当前的容器,想要什么类型的对象,就让编译器去检查,然后把想要的类型,当做参数去传递
泛型擦除机制的理解
擦除机制就是,在编译的过程将,将泛型 T 替换成 Object,并且擦除机制是编译时期的一种机制,运行期间是没有这个概念的
通配符的理解
通配符的作用是用来解决泛型无法协变问题的,这个协变的意思就是,比如 孩子类 child 是 父类 parent 的子类,那么 List<Child> 也应该是 List<Parent> 的子类,但是泛型是不支持这样的父子类关系的
通配符 ? ,表示可以接收任何类型,List<?> 可以表示各种泛型 List 的父类,意思是元素类型未知的 List
-
通配符上界 List<? extends T> ?表示未知类型,它必须是 T 的子类型
-
通配符下界 List<? super T> ?表示未知类型,它必须是 T 的父类型
24. 什么是反射,使用场景有哪些
在 Java 中,反射是指在运行时检查和操作类、接口、字段、方法等程序结构的能力。通过反射,可以在运行时获取类的信息,创建类的实例,调用类的方法,访问和修改类的字段等。通过反射可以提高程序的灵活性和可扩展性,可以实现更多的功能。但在使用反射时需要考虑性能问题以及安全等问题。
使用场景
反射的使用场景有很多,以下是比较常见的几种反射的使用场景:
- 编程开发工具的代码提示,如 IDEA 或 Eclipse 等,在写代码时会有代码(属性或方法名)提示,这就是通过反射实现的。
- 很多知名的框架如 Spring,为了让程序更简洁、更优雅,以及功能更丰富,也会使用到反射,比如 Spring 中的依赖注入就是通过反射实现的。
- 数据库连接框架也会使用反射来实现调用不同类型的数据库(驱动)。
优缺点:
反射的优点如下:
- 灵活性:使用反射可以在运行时动态加载类,而不需要在编译时就将类加载到程序中。这对于需要动态扩展程序功能的情况非常有用。
- 可扩展性:使用反射可以使程序更加灵活和可扩展,同时也可以提高程序的可维护性和可测试性。
- 实现更多功能:许多框架都使用反射来实现自动化配置和依赖注入等功能。例如,Spring 框架就使用反射来实现依赖注入。
反射的缺点如下:
- 性能问题:使用反射会带来一定的性能问题,因为反射需要在运行时动态获取类的信息,这比在编译时就获取信息要慢。
- 安全问题:使用反射可以访问和修改类的字段和方法,这可能会导致安全问题。因此,在使用反射时需要格外小心,确保不会对程序的安全性造成影响。
25. 说一下C语言的编译和Java中的编译
C语言编译过程:
-
编写代码:编写代码将代码放在 .c的扩展名文件中
-
编译器:使用C编译器,将C语言代码编译成机器可以执行的目标代码文件。编译器会进行词法分析、语法分析、语义分析和代码生成等步骤,最终生成目标代码文件,通常以 .o 或 .obj 为文件扩展名
-
链接器:在有多个源文件的情况下,编译器会生成多个目标代码文件。链接器负责将这些目标代码文件以及所需的库文件链接在一起,生成可执行的程序。链接器解决符合引用、地址重定向和符号合并的问题。
-
生成可执行的文件:最终生成的可执行文件包含了程序的机器代码以及用于加载和执行程序的元数据,用户可以运行可执行的程序。
Java 的编译过程:
-
编写代码:编写代码将代码放在 .java 的扩展名文件中
-
编译器:使用 Java 编译器(如javac命令),将 java源代码编译成字节码文件。字节码是一种与特定平台无关的中间代码,通常为 .class文件,编译器会进行词法分析、语法分析和生成字节码等步骤。
-
字节码执行:字节码通过 Java虚拟机(JVM)在运行时执行。JVM负责将字节码转换为本地机器代码,并执行程序。
-
类加载和动态链接:Java程序的类是在运行时动态加载的,而不是在编译时静态链接的。JVM在需要时将类加载到内存中,并进行动态链接。(这意味着Java 程序可以在任何具有兼容JVM的平台上运行,而无需重写编译)。
-
跨平台性:由于Java程序编译为字节码,它们可以在任何支持Java的平台上运行,而不需要对源代码进行修改或重新编译。
总结来说,C语言的编译生成本地机器代码,而Java的编译生成字节码,由JVM在运行时将其转换为机器代码。这使得Java具有跨平台性和动态加载的特性。
26. 基本类型和包装类型有什么区别?
-
声明方式不同:基本类型不用new,而包装类型需要使用new关键字在堆中分配存储空间
-
存储方式及位置不同:基本类型直接将变量值存储在栈中,而包装类型是将对象放在堆中,然后通过引用来使用
-
初始化值不同:基本类型初始值有默认值,而包装类型初始化为 null
-
包装类型可以用于泛型,而基本类型不可以
-
在使用 == 判断时不同,基本类型直接比较大小,而包装类型比较地址
-
把基本类型转化为包装类型叫装箱,把包装类型转化为基本类型叫拆箱
27. sort排序,快速排序的思路。基准值可以随机选取吗?
思路:从待排序的数组中,选取一个数作为基准。然后遍历待排序的数组,将比基准小的数放在左边,将比基准值大的数放在右边。然后采用分治的思想,对左右两个区间重复这样的一个操作。
基准值的选取可以随机,这种方法称 "随机化快速排序"。在数组中随机选取一个数作为基准,虽然最坏情况仍然是 O(n^2),但是最快情况不再依赖于输入数据,而是由于随机基准的取值不佳引起的。所以实际上,随机化快速排序得到的最坏情况理论上位 1/(2n),所以随机选取基准,可以对绝大多数输入数据达到 O(nlgn)的期望复杂度。
快排其他的选取基准值的方法有:
-
Hoare法:一般先把第一个元素当做基准,然后从前往后找比基准值小的,从后往前找比基准值大的,然后交换,直到两个位置相遇,交换基准值和相遇位置的元素
-
挖坑法:先拿出第一个元素当做基准,此时第一个位置空出来了(挖坑),然后先从后往前找第一个小于基准值的数据,放入之前的坑中,此时又有了新的坑,然后从前往后找大于基准值的数字放进坑中...直到前后遍历相遇,然后把基准值放入相遇的坑中
-
前后指针法:去第一个元素为基准,然后定义两个指针 pre是 cur ,cur如果比基准小,就++,如果比基准大就停下来和pre交换。
28. 说一下你知道的排序的时间复杂度和空间复杂度还有稳定性 ,说一下归并排序的思路 ,冒泡和快速排序的区别,哪些是稳定的
排序 | 时间复杂度(最优) | 时间复杂度(最坏) | 空间复杂度 | 稳定性 |
---|---|---|---|---|
冒泡排序 | O(n) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n) | O(n^2) | O(1) | |
插入排序 | O(n) | O(n^2) | O(1) | 稳定 |
快速排序 | O(nlgn) | O(n^2) | O(nlgn)~O(n) | |
堆排序 | O(nlgn) | O(nlgn) | O(1) | |
希尔排序 | O(n^1.3) | O(n^1.5) | O(1) | |
归并排序 | O(nlgn) | O(nlgn) | O(n) | 稳定 |
归并排序的思路:归并排序是一种分而治之的排序算法,主要的思路就是将大问题分解为小问题,最后将所有小问题的解合并起来,最终得到整个问题的解
冒泡排序和快速排序的区别:
冒泡和快排都是基于比较并交换的思想,区别是
冒泡排序是稳定的,快排不是稳定的;冒泡排序适用于数据量不大的情况下,快排适用于数据量比较大的情况下。
本篇文章主要参考: