目录
JAVA基础
1. JAVA 中的几种基本数据类型是什么,各自占用多少字节。
数据类型 |
关键字 |
内置类 |
内存占用字节数 |
布尔型 |
boolean |
Boolean |
1字节 |
字符型 |
char |
Character |
2字节 |
字节型 |
byte |
Byte |
1字节 |
短整型 |
short |
Short |
2字节 |
整形 |
int |
Integer |
4字节 |
长整型 |
long |
Long |
8字节 |
单精度型 |
float |
Float |
4字节 |
双精度型 |
double |
Double |
8字节 |
2. String类为什么是final的?
1.final方法比非final快一些
2.final关键字提高了性能。JVM和Java应用都会缓存final变量。
3.final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。
4.使用final关键字,JVM会对方法、变量及类进行优化。
5.字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键
3. String,Stringbuffer,StringBuilder的区别。
1.可变与不可变
String类中使用字符数组保存字符串,如下就是,因为有“final”修饰符,所以可以知道string对象是不可变的。
private final char value[];
StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也
是使用字符数组保存字符串,如下就是,可知这两种对象都是可变的。
char[] value;
2.是否多线程安全
String中的对象是不可变的,也就可以理解为常量,显然线程安全。
AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操
作,如expandCapacity、append、insert、indexOf等公共方法。
StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的
StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。
4. ArrayList 和 LinkedList 有什么区别。
1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
2. 底层数据结构: Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向链表数据结构(注
意双向链表和双向循环链表的区别);
3. 插入和删除是否受元素位置的影响:① ArrayList 采用数组存储,② LinkedList 采用链表存储
4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问
就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
5. 内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而
LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和
直接前驱以及数据)。
补充内容:RandomAccess接口public interface RandomAccess{
}
查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,在我看来 RandomAccess 接口不过
是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。在binarySearch()方法中,它要判断传入的list 是否RamdomAccess的实例,如果是,调用
indexedBinarySearch()方法,如果不是,那么调用iteratorBinarySearch()方法
ArraysList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为什么呢?我觉得还是和底层数据结
构有关!ArraysList 底层是数组,而 LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为
O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),
所以不支持快速随机访问。ArraysList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。
RandomAccess 接口只是标识,并不是说 ArraysList 实现 RandomAccess 接口才具有快速随机访问功能
的!
ArrayList:默认长度是10 每次扩容是原来的1.5倍。如果在添加的时候远数组是空的,就直接给一个10的长度,否则的话就加一,当需要的长度大于原来数组长度的时候就需要扩容了
下面再总结一下 list 的遍历方式选择:
实现了RadmoAcces接口的list,优先选择普通for循环 ,其次foreach,
未实现RadmoAcces接口的ist, 优先选择iterator遍历(foreach遍历底层也是通过iterator实现的),大
size的数据,千万不要使用普通for循环
5. 讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,当 new 的时候,他们的执行顺序。
父类静态数据 > 子类静态数据 > 父构造函数> 父字段 > 子构造函数 > 子字段
测试代码可以见:
6. 用过哪些 Map 类,都有什么区别,HashMap 是线程安全的吗,并发下使用的 Map 是什么,他们内部原理分别是什么,比如存储方式,hashcode,扩容,默认容量等。
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的
hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的时数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的
hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
当 HashMap 中的元素个数超过数组大小 loadFactor时,就会进行数组扩容,loadFactor的默认值为 0.75,这
是一个折中的取值。也就是说,默认情况下,数组大小为 16,那么当 HashMap 中元素个数超过 16*0.75=12 的
时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常
消耗性能的操作,所以如果我们已经预知 HashMap 中元素的个数,那么预设元素的个数能够有效的提高
HashMap 的性能。
HashMap 包含如下几个构造器:
HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。
HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap
7. JAVA8 的 ConcurrentHashMap为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计。
从Java 8开始ConcurrentHashMap放弃分段锁,转而使用CAS操作和优化的synchronized机制,是为了简化
设计、提高并发性能、减少内存开销,并更好地适应现代硬件架构。这种改进使得ConcurrentHashMap在高并发
场景下表现更加优越。
ConcurrentHashMap适用于读者数量超过写者时,当写者数量大于等于读者时,CHM的性能是低于Hashtable
和synchronized Map的。这是因为当锁住了整个Map时,读操作要等待对同一部分执行写操作的线程结束。CHM
适用于做cache,在程序启动时初始化,之后可以被多个请求线程访问。正如Javadoc说明的那样,CHM是
HashTable一个很好的替代,但要记住,CHM的比HashTable的同步性稍弱
8. 有没有有顺序的Map 实现类,如果有,他们是怎么保证有序的。
在JAVA中,LRU的原生实现是JDK中LinkedHashMap。LinkedHashMap继承自HashMap
【实现原理】 简单说就是HashMap的每个节点做一个双向链表。
每次访问这个节点,就把该节点移动到双向链表的头部。满了以后,就从链表的尾部删除。
但是LinkedHashMap并是非线程安全(其实现中,双向链表的操作是没有任何线程安全的措施的)。
对于线程安全的HashMap,在JDK中有ConcurrentHashMap原生支持。
9. 抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口么,类可以实现多个接口么。
- 类不可以继承多个类,Java不支持多重继承。一个类只能继承一个父类(可以是具体类或抽象类)。
- 接口可以继承多个接口,使用extends关键字。
- 类可以实现多个接口,使用implements关键字,用逗号分隔。
10.你能用Java覆盖静态方法吗?如果我在子类中创建相同的方法是否会编译时错误?
不能,但在子类中声明一个完全相同的方法不会编译错误,这称为隐藏在Java中的方法。
11. 讲讲你理解的 nio。他和 bio 的区别是啥,谈谈 reactor 模型。
Reactor模式首先是事件驱动的(基于NIO实现的),有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handle
12. 反射的原理,反射创建类实例的三种方式是什么。
//第一种表示方式--》实际在告诉我们任何一个类都有一个隐含的静态成员变量class
Class class1 = Foo.class;
//第二种表示方式 已经知道该类的对象通过getClass方法
Class class2 = foo1.getClass();
//第三种表达方式
class3 = Class.forName("com.imooc.reflect.Foo");
14. 描述动态代理的几种实现方式,分别说出相应的优缺点。
- Jdk cglib jdk底层是利用反射机制,需要基于接口方式,这是由于 Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
- Cglib则是基于asm框架,实现了无反射机制进行代理,利用空间来换取了时间,代理效率高于jdk
15. jdk代理与cglib 实现的区别。
- JDK的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理
- cglib是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。 采用非常底层的字节码生成技术
16. 为什么CGlib 方式可以对接口实现代理。
可以,效率低
17. final的用途。
在Java中,`final` 关键字可以用在三个地方:类,方法和变量。
- 如果一个类被声明为 `final`,意味着这个类不能被继承。这在你希望类的行为不被修改,或者出于安全考虑不希望类被继承时很有用。
- 如果一个方法被声明为 `final`,意味着这个方法不能被子类覆盖(重写)。如果你希望类的某个行为是固定的,不允许任何修改,就可以使用 `final` 方法。
- 如果一个变量被声明为 `final`,意味着这个变量的值一旦被初始化后,就不能再被改变。对于基本类型,这意味着变量的值不能改变;对于引用类型,这意味着引用不能改变,但引用的对象本身是可以被修改的。
`final` 的主要目的是定义不可改变的事物。这对于提高安全性和改善性能都有好处。
18. 写出三种单例模式实现。
饿汉单例
public class EagerSingleton {
//饿汉单例模式
//在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
private static EagerSingleton instance = new EagerSingleton();//静态私有成员,已初始化
private EagerSingleton()
{
//私有构造函数
}
public static EagerSingleton getInstance() //静态,不用同步(类加载时已初始化,不会有多线程的问题)
{
return instance;
}
}
懒汉单例
public class LazySingleton {
private LazySingleton() {
System.out.println("LazySingleton is create");
}
private static LazySingleton instance = null;
public static synchronized LazySingleton getInstance() {
if (instance == null)
instance = new LazySingleton();
return instance;
}
}
双检锁单例
/**
不可否认,synchronized关键字是可以保证单例,但是程序的性能却不容乐观,
原因在于getInstance()整个方法体都是同步的,这就限定了访问速度。
其实我们需要的仅仅是在首次初始化对象的时候需要同步,
对于之后的获取不需要同步锁。因此,可以做进一步的改进
**/
public class DoubleCheckedLock {
private static volatile DoubleCheckedLock instance;
public static DoubleCheckedLock getInstance() {
if (instance == null) { //step1
synchronized (DoubleCheckedLock.class) { //step2
if (instance == null) { //step3 是不是多余?
System.out.println("new DoubleCheckedLock");
instance=new DoubleCheckedLock(); //step4
}
}
}
return instance;
}
}
静态内部类模式
/**
* INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一
* 性,同时也延迟了单例的实例化。
*
**/
public class SingleTon{
private SingleTon(){}
private static class SingleTonHoler{
private static SingleTon INSTANCE = new SingleTon();
}
public static SingleTon getInstance(){
return SingleTonHoler.INSTANCE;
}
}
19. 如何在父类中为子类自动完成所有的 hashcode 和 equals 实现?这么做有何优劣。
反射:可以使用反射API在父类中访问子类的所有字段,然后基于这些字段来实现
hashCode
和equals
。这种方法可以自动处理任何子类的字段,但有一些缺点。尽管在父类中为所有子类实现
hashCode
和equals
可以提高代码的重用性和一致性,但由于性能问题、正确性和维护难度等因素,这通常不是推荐的做法。在大多数情况下,最好在每个子类中单独实现这些方法。
20. 请结合 OO 设计理念,谈谈访问修饰符 public、private、protected、default 在应用设计中的作用。
21. 深拷贝和浅拷贝区别。
浅拷贝:①对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新
的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的
数据。②对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会
进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成
员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
深拷贝:不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,
并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整
个对象图进行拷贝!
如果某个属性被transient修饰,那么该属性就无法被拷贝了
22.拦截器与过滤器的区别
- 拦截器是基于java的反射机制的,而过滤器是基本函数回调。
- 拦截器不依赖于servlet容器,过滤器依赖于servlet容器
- 拦截器只能对action(action 通常是MVC框架中的控制器组件)请求起作用,过滤器可以对几乎所有的请求起作用
- 拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问
- 在action的生命周期中,拦截器可以被多次调用,而过滤器只能在容器初始化时被调用过一次。
23. error 和 exception 的区别,CheckedException,RuntimeException 的区别。
1.Error(错误)是系统中的错误,程序员是不能改变的和处理的,是在程序编译时出现的错误,只能通过修改程序
才能修正。一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类
错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。
2.Exception(异常)表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程
序恢复运行,而不应该随意终止异常。
Exception又分为两类
CheckedException:(编译时异常) 需要用try——catch显示的捕获,对于可恢复的异常使用CheckedException。
UnCheckedException(RuntimeException):(运行时异常)不需要捕获,对于程序错误(不可恢
复)的异常使用RuntimeException。
24. 请列出 5几个编译时异常,运行时异常。

25. 在自己的代码中,如果创建一个 java.lang.String 对象,这个对象是否可以被类加载器加载?为什么。

尝试创建一个属于 java.lang 包的类(如自定义的 java.lang.String)不仅会违反Java的安全机制,而且由于双亲委派模型和类加载器的命名空间规则,这样做在技术上也是不可行的。这是Java设计中的一个重要特征,用于保护核心类库的完整性和安全性。
26. 说一说你对 java.lang.Object 对象中 hashCode 和 equals 方法的理解。在什么场景下需要重新实现这两个方法。
自定义“相等”逻辑:当你想要两个具有相同属性值的不同对象在逻辑上被视为相等时。例如,两个
Person
对象,如果它们的name
和age
属性相同,你可能希望它们被视为相等。使用在哈希表中:如果你打算将对象用作
HashMap
、HashSet
或HashTable
等集合的键,就必须正确实现这些方法。这些集合使用对象的哈希码来快速定位键值对。改善性能:在默认实现中,对象的哈希码是基于它们的地址的。如果你的类有更好的方式来计算哈希码,可以提高基于哈希的集合操作的性能。
27. 这样的 a.hashcode() 有什么用,与 a.equals(b)有什么关系。
将对象放入到集合中时,首先判断要放入对象的hashcode值与集合中的任意一个元素的hashcode值是否相
等,如果不相等直接将该对象放入集合中。
如果hashcode值相等,然后再通过equals方法判断要放入对象与集合中的任意一个对象是否相等,如果equals判
断不相等,直接将该元素放入到集合中,否则不放入。
equals与hashcode的关系
equals相等两个对象,则hashcode一定要相等。但是hashcode相等的两个对象不一定equals相等。
详情原理请看
28. 有没有可能 2 个不相等的对象,有相同的 hashcode。
有
29. Java 中的 HashMap和 HashSet的区别,HashSet内部是如何工作的。
对于 HashSet 而言,它是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,因此
HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,我
们应该为保存到 HashSet 中的对象覆盖 hashCode() 和 equals()
30. 什么是序列化,怎么序列化,为什么序列化,反序列化会遇到什么问题,如何解决。
序列化是将对象的状态转换为可存储或可传输的形式的过程。在Java中,这通常意味着将对象转换为字节流,以便可以将其写入磁盘、存储在数据库中或通过网络发送。
为什么序列化
- 数据持久化:将对象的状态保存到硬盘,以便以后可以重新创建对象。
- 跨时空通信:在网络中发送对象,或在分布式系统间共享。
- 深拷贝:序列化可以用于快速创建对象的深拷贝。
反序列化会遇到的问题
- 类版本不一致:如果序列化的对象的类在反序列化时与原始版本不同(例如字段被添加或删除),会导致
InvalidClassException
。- 安全性问题:反序列化未知或不受信任的数据可能导致安全漏洞,比如执行恶意代码。
- 性能问题:序列化和反序列化过程可能相对耗时。
31.Java中按值传递与按引用传递的区别
值传递:(形式参数类型是基本数据类型):方法调用时,实际参数把它的值传递给对应的形式参数,形式参数只是
用实际参数的值初始化自己的存储单元内容,是两个不同的存储单元,所以方法执行中形式参数值的改变不影响实
际参数的值。
引用传递:(形式参数类型是引用数据类型参数):也称为传地址。方法调用时,实际参数是对象(或数组),这时实
际参数与形式参数指向同一个地址,在方法执行中,对形式参数的操作实际上就是对实际参数的操作,这个结果在
方法结束后被保留了下来,所以方法执行中形式参数的改变将会影响实际参数。
32创建String的特性
字符串类(Java.lang.String)是Java中使用最多的类,也是最为特殊的一个类,很多时候,我们对它既熟悉
又陌生。在很多面试题中经常用String大做文章,只要掌握了String特性,对付它们就不再是困难了。
1、从根本上认识java.lang.String类和String池
首先,我建议先看看String类的源码实现,这是从本质上认识String类的根本出发点。
从源码中可以看到:
String类是final的,不可被继承。public final class String。
String类是的本质是字符数组char[], 并且其值不可改变。private final char value[];
然后打开String类的API文档,从API中可以发现:
String类对象有个特殊的创建的方式,就是直接指定比如String x = "abc","abc"就表示一个字符串对象。
而x是"abc"对象的地址,也叫做"abc"对象的引用。
String对象可以通过“+”串联。串联后会生成新的字符串。也可以通过concat()来串联,这个后面会讲述。
Java运行时会维护一个String Pool(String池),JavaDoc翻译很模糊“字符串缓冲区”。String池用来存放
运行时中产生的各种字符串,并且池中的字符串的内容不重复。而一般对象不存在这个缓冲池,并且创建的对象
仅仅存在于方法的堆栈区。
2、String对象的创建的特性
String对象的创建也很讲究,关键是要明白其原理。
特性1:
当使用任何方式来创建一个字符串对象s时,Java运行时(运行中JVM)会拿着这个字符串的内容在String池中
找是否存在内容相同的字符串对象,如果不存在,则在池中创建一个字符串s,否则,不在池中添加。
特性2:
Java中,只要使用new关键字来创建对象,则一定会(在堆区或栈区)创建一个新的对象。
特性3:
使用直接指定、使用纯字符串串联或者在编译期间可以确定结果的变量表达式来创建String对象,则仅仅会检
查维护String池中的字符串,池中没有就在池中创建一个,有则罢了!但绝不会在堆栈区再去创建该String对
象;
特性4:
使用包含编译期间无法确定结果的变量的表达式来创建String对象,则不仅会检查维护String池,而且还会在堆栈区创建一个String对象
示例:
1、 直接指定,例如:下面代码运行结果为true;
String str1 = "abc";
String str2 = "abc";
System.out.println(str1 == str2);
2、 使用纯字符串串联,例如:下面代码运行结果为true;
String str1 = "abc";
String str2 = "ab" + "c";
System.out.println(str1 =