2023最新---java面试题大全

Posted 一篇博文

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2023最新---java面试题大全相关的知识,希望对你有一定的参考价值。

java面试大全

自己辛苦整理,相对简化,适用于面试突击。
希望对初中级java开发的面试有所帮助。
毕竟现在的就业环境太差了。
有问题、有补充欢迎评论指出,虚心求教,有错麻溜改。
对你有帮助的话,记得点赞收藏。
朋友要找工作的话,记得转发给他哦~

文章目录

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死循环。

怎么解决: