2023最新---java面试题大全
Posted 一篇博文
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2023最新---java面试题大全相关的知识,希望对你有一定的参考价值。
java面试大全
自己辛苦整理,相对简化,适用于面试突击。
希望对初中级java开发的面试有所帮助。
毕竟现在的就业环境太差了。
有问题、有补充欢迎评论指出,虚心求教,有错麻溜改。
对你有帮助的话,记得点赞收藏。
朋友要找工作的话,记得转发给他哦~
文章目录
- java面试大全
- JAVA基础
- 集合
- 异常
- 线程
- 锁
- JVM
- MySQL
- Spring
- SpringMVC
- Mybatis
- springboot
- Linux
- Git
- Redis
- RabbitMQ
- Nginx
- SpringCloud
- Dubbo
- Zookeeper
- Docker
- Vue
JAVA基础
JDK、JRE、JVM之间的区别
JDK:java开发工具;JRE:java运行时环境;JVM:java虚拟机。
面向对象
面向对象相较于面向过程而言是两种不同的处理问题的角度。
面向过程注重步骤,面向对象更注重完成这些任务的参与者(对象)。
面向过程比较直接高效,而面向对象更易于复用、扩展和维护。
- 封装:封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项,内部细节对外部调用透明,外部调用无需修改或者关心内部实现。(私有的属性,共有的get、set方法)
- 继承:继承基类的方法,并做出自己的改变和/或扩展。子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的。
- 多态:基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。(父类引用指向子类对象,相同的方法调用,因为不同的子类对象的实现,执行不同的逻辑)
==和equals区别
- ==比较的是栈中的值,包括基本数据类型的值和引用数据类型的地址。
- equals是顶级父类object类中的方法,在不重写的情况下采用==完成比较,通常会重写,按照重写规则进行内容比较。
- java源码中,equals被String、Integer重写了,所以比较的是对象的内容是否相等。
hashCode与equals
- 如果两个对象的hashCode不相同,那么这两个对象肯定不同的两个对象 。
- 如果两个对象的hashCode相同,不代表这两个对象⼀定是同⼀个对象,也可能是两个对象(equals不一定相等)。
- 如果两个对象相等(equals相等),那么他们的hashCode就⼀定相同。
final
-
修饰的类不可被继承,修饰的方法不能被子类重写,修饰的变量不能被修改(引用类型不可修改地址)。
-
如果final修饰的是类变量(static),只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。
-
如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值。
-
系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时,
即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码
中对final变量赋初值(仅一次)。
-
局部内部类和匿名内部类只能访问局部final变量。(内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着 方法的执行完毕就被销毁。当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解 决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以 访问它,实际访问的是局部变量的"copy"。这样就好像延长了局部变量的生命周期 final变量 )
final、finally、finalize
- Final:用于声明属性(属性不可变),方法(不能被重写),类(被final修饰的类不能被继承)。
- Finally:处理异常时使用,表示总是执行。
- Finalize:0bject类的一个方法,垃圾回收。
String、StringBuffer、StringBuilder
-
String是final修饰的,不可变,底层用char数组实现的。每次操作都会产生新的String对象 。
-
StringBuffer和StringBuilder都是在原对象上操作 。
-
StringBuffer是线程安全的,StringBuilder线程不安全的 。
-
StringBuffer方法都是synchronized修饰的 。
性能:StringBuilder > StringBuffer > String 。
场景:经常需要改变字符串内容时使用后面两个 。
优先使用StringBuilder,多线程使用共享变量时使用StringBuffer。
重载和重写的区别
- 重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问
修饰符可以不同,发生在编译时。
- 重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于
等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。
接口和抽象类的区别
- 抽象类可以存在普通成员函数,而接口中只能存在public abstract 方法(1.8加入了默认方法)。
- 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。
- 抽象类只能继承一个,接口可以实现多个。
接口的设计目的,是对类的行为进行约束;而抽象类的设计目的,是代码复用。
访问修饰符
- Private:私有 出了这个类就不能被访问 当出现集成可继承父类的属性或者方法
- Default:(包访问权限)只能在同一个包下中所有类访问,且必须是同级的包
- Protected:(继承访问权限)只能在同一个包中所有类和不同包的子类访问
- Public:可以再任意位置被访问
Static
静态关键字,用法包括静态变量和静态方法。
- 静态变量:(类变量)被所有的对象所共享。
- 静态方法:静态方法中不能访问类的非静态成员变量和非静态成员方法
String常用API
- length(),返回当前字符串长度
- substring():截取字符串
- equals():比较
- charAt():从字符串中取出指定位置的字符
- tocharArray():将字符串变成字符数组
- trim():去掉空格
- split():分割字符串 数组
- getBytes,字符串转为为byte数组
Object类API
- getClass():返回对象的类
- hashCode():返回对象的哈希值
- equals():比较
- clone():复制
- toString():返回对象字符串
- notify():唤醒等待的单个线程
- notifyAll():唤醒等待的所有线程
- wait():让线程等待
- finalize():垃圾回收
时间常用API
- Date
//创建一个Date日期对象:代表了系统当前此刻日期时间信息
Date d = new Date();
//获取时间毫秒值的形式:从19700101 0:0:0开始走到此刻的总毫秒值
long time = d.getTime(); // long time = System.currentTimeMillis();
time += (60 * 60 + 123) * 1000;
//把时间毫秒值转换成日期对象
Date d2 = new Date(time);
// 与上述代码逻辑一样,只是写法不同
Date d2 = new Date();
d2.setTime(time); // 修改日期对象成为time这个时间
- SimpleDateFormat
//日期对象
Date d = new Date();
//开始格式化:创建一个简单日期格式化对象
// 注意:参数是格式化之后的时间形式,必须申明!
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss EEE a");
//开始格式化日期对象成为字符串形式
String result = sdf.format(d);
//格式化时间毫秒值----------
long time = d.getTime() + 60 * 1000;
sdf.format(time)
//SimpleDateFormat解析字符串时间成为日期对象
String timeStr = "2022年05月27日 12:12:12";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
Date d = sdf.parse(timeStr); // 解析
- Calendar
// 拿到系统此刻日历对象
Calendar rightNow = Calendar.getInstance();
// 获取日历的信息:public int get(int field):取日期中的某个字段信息。
int year = rightNow.get(Calendar.YEAR);
int mm = rightNow.get(Calendar.MONTH);
int days = rightNow.get(Calendar.DAY_OF_YEAR);
//public void add(int field,int amount):为某个字段增加/减少指定的值
// 请问64天后是什么时间
rightNow.add(Calendar.DAY_OF_YEAR , 64);
//拿到此刻时间毫秒值
long time = rightNow.getTimeInMillis();
System.out.println(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(time));
- LocalDate
//获取本地日期对象。
LocalDate nowDate = LocalDate.now();
int year = nowDate.getYear();
int month = nowDate.getMonthValue();
int day = nowDate.getDayOfMonth();
//当年的第几天
int dayOfYear = nowDate.getDayOfYear();
//星期
System.out.println(nowDate.getDayOfWeek());
System.out.println(nowDate.getDayOfWeek().getValue());
//月份
System.out.println(nowDate.getMonth());
System.out.println(nowDate.getMonth().getValue());
//直接传入对应的年月日
LocalDate bt = LocalDate.of(2025, 5, 20);
//相对上面只是把月换成了枚举
System.out.println(LocalDate.of(2025, Month.MAY, 20));
- LocalTime
//获取本地时间对象。
LocalTime nowTime = LocalTime.now();
int hour = nowTime.getHour();//时
int minute = nowTime.getMinute();//分
int second = nowTime.getSecond();//秒
int nano = nowTime.getNano();//纳秒
LocalTime time = LocalTime.of(8, 30);
System.out.println(time);//时分
System.out.println(LocalTime.of(8, 20, 30));//时分秒
- LocalDateTime
// 日期 时间
LocalDateTime nowDateTime = LocalDateTime.now();
//今天是:
System.out.println("今天是:" + nowDateTime);
System.out.println(nowDateTime.getYear());//年
System.out.println(nowDateTime.getMonthValue());//月
System.out.println(nowDateTime.getDayOfMonth());//日
System.out.println(nowDateTime.getHour());//时
System.out.println(nowDateTime.getMinute());//分
System.out.println(nowDateTime.getSecond());//秒
//日:当年的第几天
System.out.println(nowDateTime.getDayOfYear());
//星期
System.out.println(nowDateTime.getDayOfWeek());//枚举
System.out.println(nowDateTime.getDayOfWeek().getValue());//数组
//月份
System.out.println(nowDateTime.getMonth());//枚举
System.out.println(nowDateTime.getMonth().getValue());//数组
//转日期
LocalDate ld = nowDateTime.toLocalDate();
//转时间
LocalTime lt = nowDateTime.toLocalTime();
- DateTimeFormatter
LocalDateTime ldt = LocalDateTime.now();
//格式化器
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String ldtStr1 = dtf.format(ldt);
//解析
DateTimeFormatter dtf1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 解析当前字符串时间成为本地日期时间对象
LocalDateTime ldt1 = LocalDateTime.parse("2022-11-11 11:11:11" , dtf1);
System.out.println(ldt1.getDayOfYear());
冒泡排序
for (int i = 0; i <arr.length-1; i++)
//标志位
boolean flag = true;
for (int j = 0; j <arr.length-1-i ; j++)
if(arr[j] > arr[j+1])
int temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp;
flag = false;
//当不再发生交换时,则结束比较
if(flag)
break;
集合
List和Set的区别
- List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出
所有元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素。
- Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用Iterator接口取得所有元
素,在逐一遍历各个元素。
List
思路:介绍list的特点–>简单介绍Arraylist,LinkedList 的底层实现–>说说Arraylist,LinkedList 的区别–>最后可以说他们不是线程安全的,引入写时复制思想。
-
List是一个有序,可重复的集合。它的实现类包括ArrayList,LinkedList,Vector。
-
ArrayList底层是动态数组实现的。动态数组就是长度不固定,随着数据的增多而变长。实例化Arraylist的时候,如果不指定长度,默认就是10。添加元素时,是按照顺序从头部开始往后添加。
使用无参构造ArrayList()创建ArrayList对象时,不会定义底层数组的长度,当第一次调用add(E e) 方法时,初始化定义底层数组的长度为10,之后调用add(E e)时,如果需要扩容,则调用grow(int minCapacity) 进行扩容,长度为原来的1.5倍。
因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会 涉及到元素的移动,所以增删效率一般。
但是由于每个元素占用的内存相同且是连续排列的,因此在查找的时候,根据元素的下标可以迅速访问数组中的任意元素,查询效率非常高。
-
LinkedList底层是双向链表的数据结构实现,每个节点包括:上一节点和下一节点的引用地址和data用来存储数据,双向链表不是连续排列的,是可以占用一段不连续的内存空间的。
当有新元素插入时,只需要修改所要插入位置的前一个元素的引用和后一个元素的引用。
删除也只需要修改两个引用,当前元素就没有指向,就成了垃圾对象,被回收。效率高。但是查询的时候需要从第一个元素开始查找,直到找到需要的数据,所以查询的效率比较低。
ArrayList和Linkedlist的区别?
-
ArrayList底层是数组实现,LinkedList底层是链表实现
-
Arraylist适合随机查找,LinkedList适合删除和添加
-
都实现了List接口,但是LinkedList同时还实现了Deque接口,还可以作为双端队列。
-
ArrayList通过下标查询快,LinkedList通过下标查询需要遍历所有,但是查第一个和最后一个很快
-
ArrayList添加需要扩容,指定位置添加,需要数组移动元素。LinkedList添加不需要扩容,指定位置添加,需要遍历找到位置
-
ArrayList实现了Random Access接口,LinkedList没有。实现Random Access接口可以使用普通for循环遍历,没有实现的使用foreach和迭代器,ArrayList用for循环快,LinkedList用迭代器快。
-
ArrayList和LinkedList都是线程不安全的。(在添加操作时,可能是分成两步完成的: 1、在items[size]的位置存放此元素,2、增大size的值,这个时候就会引发线程安全问题。)如果想要解决当前的这个问题,可以用写时复制的CopyOnWriteArrayList。
Arraylist如何去除重复元素?
- 可以使用set集合,因为set是不可重复的,可以把数据添加到set集合中,再转为list就可以去重。
- 可以使用stream对象distinct去重关键字进行去重,再收集成新的list。
Arraylist中有很多空值null,如何删除?
- 第一种:list.stream().filter(Objects::nonNull).collect(Collectors.toList());
- 第二种:list.removeIf(Objects::isNull);
Set
无序,元素不能重复。
- HashSet:内部数据结构是哈希表(线程不安全,效率高),元素无序,唯一(存储元素类型是否重写hashCode和equals方法保证),可以存储null元素。
- TreeSet:内部数据结构是二叉树,元素唯一,有序(线程不安全),集合元素唯一。TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素的大小关系,比较是否返回0,如果返回0则相等然后将元素按照升序排列。
Map
键值对 key value的集合,可以使用任何引用类型的数据,key不能重复,通过指定的key就可以获取对应的value。
HashMap 和 HashTable 有什么区别?
- HashMap方法没有synchronized修饰,线程非安全,HashTable 是线程安全的。
- 由于线程安全,所以 HashTable 的效率比不上 HashMap。
- HashMap可以把null作为key或value,而 HashTable不允许。
- HashMap 默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大1倍,后者扩大1倍+1(2n+1)。
- HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode。
Jdk1.7到Jdk1.8 HashMap 发⽣了什么变化?
- 1.7中底层是数组+链表,1.8中底层是数组+链表+红⿊树,加红⿊树的⽬的是提⾼HashMap插⼊和查询整体效率 。
- 1.7中链表插⼊使⽤的是头插法,1.8中链表插⼊使⽤的是尾插法,因为1.8中插⼊key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使⽤尾插法 。
- 1.7中哈希算法⽐较复杂,存在各种右移与异或运算,1.8中进⾏了简化,因为复杂的哈希算法的⽬的就是提⾼散列性,来提供HashMap的整体效率,⽽1.8中新增了红⿊树,所以可以适当的简化哈希算法,节省CPU资源。
说⼀下HashMap的Put⽅法
先说HashMap的Put⽅法的⼤体流程:
- 根据Key通过哈希算法和与运算得出数组下标
- 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中
是Node对象)并放⼊该位置
- 如果数组下标位置元素不为空,则要分情况讨论
- 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对象,并使⽤头插法添加到当前位置的链表中
- 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
- 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过程中会判断红⿊树中是否存在当前key,如果存在则更新value
- 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插 法插⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链表中,插⼊到链表后,会看当前链表的节点个数,如果⼤于等于8,那么则会将该链表转成红⿊树
- 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要就扩容,如果不需要就结束PUT⽅法
HashMap的扩容机制原理
1.7版本
- 先⽣成新数组
- 遍历⽼数组中的每个位置上的链表上的每个元素
- 取每个元素的key,并基于新数组⻓度,计算出每个元素在新数组中的下标
- 将元素添加到新数组中去
- 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
1.8版本
- 先⽣成新数组
- 遍历⽼数组中的每个位置上的链表或红⿊树
- 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
- 如果是红⿊树,则先遍历红⿊树,先计算出红⿊树中每个元素对应在新数组中的下标位置
- 统计每个下标位置的元素个数
- 如果该位置下的元素个数超过了8,则⽣成⼀个新的红⿊树,并将根节点的添加到新数组的对应位置
- 如果该位置下的元素个数没有超过8,那么则⽣成⼀个链表,并将链表的头节点添加到新数组的对应位置
- 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
迭代器在迭代过程中,修改map会出现什么问题?
采用 Fail-Fast 机制,底层通过一个 modCount 值记录修改的次数,对 HashMap 的修改操作都会增加这个值。迭代器在初始过程中会将这个值赋给 exceptedModCount ,在迭代的过程中,如果发现 modCount 和 exceptedModCount 的值不一致,代表有其他线程修改了Map,就立刻抛出异常。
HashMap为什么是线程不安全的?
- 在多线程的情况下,进行put操作的时候,如果插入的元素超过了容量的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容hash到新的扩容数组中,在多线程的环境下,存在同时进行put操作,如果hash值相同,可能出现在同一数组下用链表表示,造成闭环,导致get死循环。
怎么解决:
- 本身hashtable就是线程安全的,因为他的所有涉及多线程的操作都加了synchronized关键字,但是效率太低。所以在多线程的状态下,建议使用ConcurrentHashMap解决多线程不安全问题。ConcurrentHashMap也分为1.7和1.8。ConcurrentHashMap不支持key和value不能为空,如果空就会报空指针异常。
- 1.7使用的是数组+segments分段锁+HashEntry链表的数据结构,锁的实现使用的是Lock+CAS+UNSAFE类,他的扩容方式支持多个segment同时扩容。(实现原理:相当于将一个大的ConcurrentHashMap拆分成16个小的hashtable,每个ha
以上是关于2023最新---java面试题大全的主要内容,如果未能解决你的问题,请参考以下文章