Java面试题

本文详细解析了Java面试中的重点知识点,包括JavaSE的集合、线程、基础概念,Spring框架基础,HTTP/网络协议,数据库相关以及面试技巧。强调了HashMap与HashTable的区别,线程安全问题,Spring的使用,以及数据库连接池的作用和原理。同时涵盖了面试注意事项,如自我介绍和回答问题的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章目录

面试技巧

1. 自我介绍

  • 基本信息(姓名,年龄,学历,学校,专业)
  • 工作经验(工作年限,项目经验)
  • 技术栈(结合做过的项目)
  • 其他(特长,爱好)

2. 面试技巧

  • 不能一问一答,主动找话题,将话题引向自己擅长的领域,技术。
  • 如果答不上来问题,组织一下语言,重新说,实在想不起来就说自己忘了,不能支支吾吾。
  • 听完问题再回答,如果没有听清楚,适当反问题目意思。
  • 声音洪亮,自信。
  • 回答问题:what是什么,how怎么用,why原理。

3. 注意事项

  • 刷题,写简历,投简历,面试。
  • 面试前,查询好去公司的路线,安排好时间。
  • 认真阅读公司招聘需求,恶补需求技术点,不会的技术也要去了解。

1.JavaSE

1.1 集合(Collection,Set,List,Map)

1.1.1 HashMap和HashTable的区别

  • 共同点:
    1. HashMap和HashTable都实现了Map接口,并且都是key-value的数据结构。
  • 不同点
    1. HashMap是java1.2引进的Map接口的实现类,而HashTable是java1.1的继承自Dictionary的一个类
    2. HashMap是线程不安全的,HashTable是线程安全的(也称作线程同步)
    3. HashMap允许将null值作为key或value,而HashTable不允许,如果存储null的话会报空指针异常
  • 使用ConcurrentMap兼顾线程安全和效率

1.1.2 HashMap的put()和get()方法是怎么实现的

  • HashMap的put()方法实现:
    2. 通过传递key-value数据时调用put方法的时候,HashMap使用key的hashcode()方法(使用了hashcode()方法此方法增加了高位运算防止hash冲突用来重新计算hash值)和哈希算法来找出存储key-value对的索引,如果索引处为空,则直接将数据插入到对应的数组中,否则判断是否是红黑树。若是,则以红黑树插入,否则遍历链表。若长度不小于8,则将链表转换为红黑树,转换成功后再插入。新加入的数据放到链表头,最先加入的数据放到链表尾。
  • HashMap的get()方法的实现:
    1. 根据keyhashcode算出元素在数组中的下标,之后遍历Entry对象链表,直到找到元素为止。
    2. 如果使用对象作为key,需要重写key的hashcode()和equals()方法
    3. 如果出现了大量hash冲突,那么遍历链表的时候,会比较慢。JDK 1.8里面,当链表的长度大于阀值(默认为8)的时候,会使用红黑树来存储数据,以便加快key的查询速度。

1.1.3 HashMap与HashSet的区别

HashMap不允许重复的键,HashSet不允许存储重复的值(对象)

  1. HashSet底层调用的是HashMap
  2. HashMap实现Map接口,HashSet实现Set接口
  3. HashMap存储key-value格式的数据,HashSet只能存储对象
  4. HashMap使用put()方法将元素存入map,HashSet使用add()方法将数据存入set
  5. HashMap中使用key来计算hashcode值,HashSet使用成员对象来计算hashcode值,使用equals()方法判断对象的相等性,如果两个对象不同返回false
  6. HashMap速度快因为其使用的是唯一的key来获取对象,HashSet较慢

1.1.4 HashMap的数据结构,线程是否安全,为什么?

  • HashMap数据结构

    1. JDK1.8以前是数组+链表
    2. JDK1.8以后是数组+链表+红黑树
  • 线程不安全,可能造成死循环,具体表现链表的循环指向,应该使用ConcurrentHashMap,它是线程安全的。

1.1.5 HashMap的实现原理

  1. HashMap的主干是一个Entry数组,Entry数组时HashMap的基本组成单元。每一个Entry包含一个key-value键值对。
  2. HashMap由数组+链表+红黑树组成,数组是HashMap的主体,链表是为了解决hash冲突而存在的。
  3. 如果定位到的数组位置不含链表(当前entry的next指向null)那么对于查找,添加操作很快,仅需一次寻址即可;
  4. 如果定位的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals()方法注意比对查找。所以,出于对性能的考虑,HashMap中的链表出现的越少,性能越好。

1.1.6 JDK1.8的HashMap有哪些优化

  1. JDK1.8之前的HashMap的数据结构是数组+链表,JDK1.8之后HashMap的数据结构是数组+链表+红黑树
  2. 当一个链表太长的时候(链表长度大于8时),HashMap会动态的将它替换成一个红黑树,这话的话会将时间复杂度从O(n)降为O(logn)。
  3. 利用红黑树快速增删改查的特点提高了HashMap的性能。

1.1.7 ArrayList和LinkedList有什么区别

  1. ArrayList的底层是数组,查询速度快,而LinkedList底层是一个双向链表,增删速度快
  2. ArrayList允许重复的元素存储,而LinkedList不允许存储重复的元素。

1.1.8 ArrayList去重

  1. 利用HashSet去重(不保证去重后顺序一定)
  2. 利用LinkedList去重(去重后顺序一定)

1.1.9 Set集合去重后会存在哪些问题?

  1. 经过Set去重后,元素列表原始顺序被打乱,需要经过排序后才能与原来的元素列表比对。
  2. LinkedHashSet是有序的HashSet(按照存入集合的顺序打印)

1.1.10 HashMap和HashTable与ConcurrentMap的区别

  1. HashMap是线程不安全的,HashTable,ConcurrentMap是线程安全的
  2. HashMap的键和值都允许有null值都存在,而HashTable不允许
  3. HashMap的效率相对HashTable和ConcurrentMap较高
  4. HashMap根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历的顺序是不确定的
  5. HashMap最多只允许存储一条键为null的记录,多条值为null
  6. HashMap是非线程安全,如果为了满足线程安全,可以使用Collections的synchronizedMap,或者使用ConcurrentMap

1.1.11 String,StringBuilder,StringBuffer的区别

  • 线程安全相关:
  1. StringBuilder线程不安全,StringBuffer线程安全。因为StringBuffer的所有公开方法都是synchronized修饰的,而StringBuilder并没有synchronized修饰。String也是线程安全的,因为它的底层是使用final关键字修饰的char[]数组。
  2. 因为StringBuilder是线程不安全的,速度较String,StringBuffer快
  • 使用场景:
  1. String类适用于操作较少字符串。
  2. StringBuilder类适用于操作单线程下在字符缓冲区进行大量字符串操作。
  3. StringBufffer类适用于操作多线程下在字符缓冲区进行大量字符串操作。
  • String为什么要使用final来修饰(为什么底层是不可变)
  1. 主要是为了“效率” 和 “安全性” 的缘故。若 String允许被继承, 由于它的高度被使用率, 可能会降低程序的性能,所以String被定义成final。
  2. StringBuilder和StringBuffer继承自AbstractStringBuilder,而它没有使用final修饰,所以是可变的

1.1.12 JAVA集合框架中的常用集合及其特点、适用场景、实现原理简介

Collectin接口

定义了包含了一批对象的集合,存储数据的格式是value格式

List接口

继承自Collection,用于定义以列表形式存储的集合。

List接口常用类:

  1. ArrayList:ArrayList的底层是数组,查询速度快,允许重复的元素存储。
  2. LinkedList:LinkedList底层是一个双向链表,增删速度快,不允许存储重复的元素。可以用来进行去重操作,去重之后的list不保证元素的存放顺序。

Map接口

Map接口在Collection的基础上,为每个value对象指定了一个key,并使用Entry保存每个Key-Value键值对。通过key快速定位到对象(value)

1.1.13 Java8的新特性

  1. Lambda表达式
  2. 方法引用
  3. 函数式接口
  4. 默认方法
  5. 新的编译工具
  6. Steam API
  7. Option类
  8. Date Time API
  9. Nashorn JavaScript引擎
  10. 内置Base64编码

1.1.14 解决hash碰撞

  1. 开放地址法
  2. 再hash法
  3. 链地址法(拉链法)
  4. 建立一个公共溢出区

1.1.15 TreeMap和TreeSet在排序时如何比较元素,Collections工具类中的sort()方法如何比较元素?

TreeMap

TreeMap要求存放的键值对的键的类必须实现Comparable接口,重写compareTo()方法从而根据键对元素进行排序。

TreeSet

  1. 自然排序TreeSet要求存放的对象的类必须实现Comparable接口,重写compareTo(T o)方法进行排序。
  2. 比较器Comparator根据TreeSet构造器传入的比较容器中重写的compare(T o1,T o2)方法

Collections工具类中的sort()方法比较元素

  1. 使用自定义类型的集合进行排序
    1. 使用自然排序compareTo()方法
    2. 比较器Comparator的compare(T o1,T o2)方法。根据传入的比较值判断比较结果。
  2. 使用包装类的compareTo()方法。

1.2 线程

1.2.1 创建线程有几种方式?

  1. 实现Runnable接口来创建线程。多线程
  2. 实现Callable接口来创建线程。
  3. 使用FutureTask类来创建线程,与Callable一样可以实现有返回值的线程创建,并且可以抛出异常。
  4. 通过继承Thread类来创建一个线程。单线程
  5. 创建线程池,使用ThreadPoolExecutorExecutors

1.2.2 Thread中的run()和start()方法有什么区别?

多线程

  • run()方法被调用时,会被当做一个普通的函数调用,程序中只有主线程,它是同步的,只调用run()方法无法达到多线程的目的

  • start()方法被调用时,线程处于就绪状态,可以被JVM来调度执行,在调度过程中,JVM通过调用线程类的run()方法来完成实际的业务逻辑,start()方法能够异步调用run()方法(无需等待run()方法中的代码执行完毕),所以start()方法可以实现多线程。

  • 实现Runnable才是真正的多线程。

1.2.3 synchronized和Lock有什么区别?

类别synchronizedLock
存在层次Java的关键字,基于JVM。一个接口。
锁的释放1,加锁的线程执行完代码后释放锁。2,线程执行中发生异常,JVM会让然线程释放锁。使用Lock锁发生异常时,不会主动释放占有的锁,必须手动调用unlock()方法释放锁,一般都会用try-catch-finally结构,避免死锁的发生。
锁的获取假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待。分情况而定,Lock锁有多个获取锁的方式,获得锁后,线程可以不用一直等待。
锁状态无法判断。可以判断。可以通过tryLock()方法判断是否获取了锁。
锁类型可重入,不可中断,非公平。可重入,可判断,公平。
性能少量同步。1,大量同步。大量资源同时竞争时(大量线程),Lock锁的性能要远远优于synchronized。2,Lock可以提高多个线程进行读操作的效率,可以通过ReadWriteLock接口实现读写分离。
是否响应中断只能等待锁的释放,不能响应中断。Lock等待锁的过程中可以使用interrupt来中断等待
调度机制synchronized使用Object对象本身的wait,notify,notifyAll调度机制。Lock可以使用Condition进行线程之间的调度。

synchronized和Lock用途区别:

synchronized和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,应该使用ReentrantLock

  1. 某个线程在等待一个锁的控制权的这段时间需要中断。
  2. 需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify()具体的线程。
  3. 具有公平锁的功能,每个到来的线程都需要排队等候。

synchronized和Lock的性能区别

  1. synchronized是关键字,基于JVM。Lock锁是使用Java代码编写的控制锁的代码。
  2. 在Java1.5中,synchronized是性能低效的,以为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多,相比之下使用Java提供的Lock对象,性能更高。
  3. 在Java1.6中,synchronized在语义上很清晰,可以进行很多优化,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致Java1.6以上的synchronized的性能并不比Lock差。

Synchronized和Lock机制的区别

  1. synchronized原始采用的是CPU悲观锁,即获得线程的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时,会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。
  2. 而Lock用得是乐观锁的方式。所谓乐观锁就是每次不加锁,而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare And Swap),一个线程的失败或挂起不应该影响其他线程的失败或挂起的算法。

1.2.4 乐观锁和悲观锁

悲观锁

  • 当我们要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据尽心加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称为悲观并发控制(Pessimistic Concurrency Control)缩写PCC,又称悲观锁

  • **悲观锁具有强烈的独占和排他特性。**它指的是对数据被外界修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。

  • 悲观锁的实现,往往依靠数据库提供的锁机制。

  • 悲观锁是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况,每次读取数据的时候默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。悲观锁的实现:

    1. 传统的关系型数据库使用这种锁机制,比如行锁,表锁,写锁,都是在做操作之前先上锁。
    2. Java里面的锁使用synchronized关键字实现。
  • 悲观锁主要分为共享锁和排它锁

    • 共享锁(shared locks)又称为读锁,简称S锁。顾名思义,共享锁就是多个事务对同一数据可以共享同一把锁,都能访问到数据,只能读。
    • 排它锁(exclusive locks)又称为写锁,简称X锁。排它锁就是不能与其它锁并存,如果一个事务获取了一个数据行的排它锁,其他事务就不能再获取该行的其他锁,包括共享锁和排它锁,但是获取排它锁的事务可以对数据进行读和写。
  • 悲观锁说明:

    悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据产生额外的开销,还有增加产生死锁的可能。另外还有降低并行性。一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完成后才可以处理被锁的数据。

乐观锁

  • 乐观锁是相对于悲观锁而言,乐观锁**假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。**乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。

  • 乐观锁避免了数据库幻读,业务处理时间过长等原因引起数据处理错误的一种机制,但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。乐观锁的实现:

    1. CAS实现:Java自带类的原子变量使用了乐观锁的一种CAS实现方式。
    2. 版本号控制:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数。当数据被修改时,version值会+1。当线程A需要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值与当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
  • 乐观锁说明:

    乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接执行下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。

1.2.5 Thread和Runnable的区别

  1. Thread类实现了Runnable接口。
  2. Runnable可以实现多个相同的程序代码的线程共享同一资源,而Thread不适合,因为Thread源码内部向下转型为Runnable,Thread内部资源共享依然是以Runnable的形式去实现资源共享。
  3. Runnable不能直接调用run()方法,调用run()方法并未启动新的线程,而是启动了一个普通函数而已。

Runnable和Thread相比优点有:

  1. 由于Java不允许多继承,因此实现了Runnable接口还可以再继承其他类,但是Thread不行。
  2. Runnable可以实现多个相同的程序代码的线程去共享统一资源,而Thread不适合。

1.2.6 sleep()和wait()的区别

  1. sleep()是Thread类的方法,wait()方法是Object类的方法。
  2. sleep()方法没有释放锁,wait()方法释放了锁。
  3. sleep()可以在任何地方使用,wait()只能在同步控制方法或同步控制代码块中使用(使用synchronized修饰)。
  4. sleep()可以指定时间唤醒线程,如果没到指定时间,可以使用interrupt()强行唤醒线程。wait()会等其他线程调用notify()/notifyAll()唤醒等待池中的所有线程才会进入就绪队列,等待系统分配资源。
  5. 使用sleep()必须捕获异常,使用wait()不用捕获异常。

sleep()wait()的作用

sleep()的作用是让线程休眠指定的时间,在时间到达时恢复,也就是说sleep将在时间到达后恢复线程执行

wait()会将调用者的线程挂起,直到其他线程调用同一个对象的notify()方法才会重新激活调用者。

1.2.7 synchronized加方法上锁对象是什么,加静态方法上锁是什么

synchronized加普通方法上是锁住该类的实例对象(多个),new 创建对象能创建多个。

synchronized加在静态方法上是锁住该类的.class对象(唯一)。

1.2.8 synchronized(this)和synchronized(User.class)的区别

对象锁和类锁

synchronized(this)对象锁

在Java中每个对象都有一个monitor对象,这个对象就是Java对象的锁,通常被称为内置锁或对象锁。类的对象可以有多个,所以每个对象都有其独立的对象锁,互不干扰。

synchronized(*.class)类锁

在Java中,针对每个类也有一个锁,可以称为类锁,类锁实际上是通过对象锁实现的,即类的Class对象锁。每个类只有一个Class对象,所以每个类只有一个类锁。

1.2.9 synchronized和volatitle关键字的区别

Java多线程中的原子性,可见性,有序性

  1. 原子性:是指多线程的多个操作是一个整体,不能被分割,要么就不执行,要么就全部执行完,中间不能被打断。
  2. 可见性:是指线程之间的可见性,就是一个线程修改后的结果,其他的线程能够立马知道。
  3. 有序性:为了提高执行效率,Java中的编译器和处理器可以对指令进行重新排序,重新排序会影响多线程并发的正确性,有序性就是要保证不进行重新排序(保证线程操作的执行顺序)。

synchronized

该关键字提供了一种同步锁,被修饰的代码块可以防止被多个线程同时运行,代码块运行时,相当于单线程操作,故而保证原子性,可见性和有序性。

volatile

volatile只保证可见性和有序性,被volatile修饰的共享变量必须在修改后及时刷新到主存中,并且禁止指令重新排序,故保证可见性和有序性。

synchronized和volatile的区别

  1. synchronized可以用在变量、方法、类、同步代码块中,使用范围广泛。而volatile只能作用于变量。
  2. synchronized可以保证线程的原子性,可见性,有序性。而volatile只能保证可见性和有序性。
  3. synchronized可能会造成线程阻塞,而volatile不会造成线程阻塞。

1.2.10 线程的5种状态

  1. 新建状态(New)创建了一个线程对象

  2. 就绪状态(Runnable)当前线程被其它线程调用start()方法后处于可运行线程池中,等待被线程调度选中,获得cpu的使用权。

  3. 运行状态(Running)就绪状态的线程获得了cpu的时间片timeslice(cpu使用权)执行程序代码。

  4. 阻塞状态(Blocked)阻塞状态是指线程因为某些原因放弃了cpu的使用权,让出了cpu的时间片timeslice,暂时停止运行。直到线程进入就绪状态,才有机会获得timeslice进入运行状态。

    阻塞的三种情况:

    1. 等待阻塞:运行状态的线程执行wait()方法,JVM会把该线程放入等待队列(wait queue)中。
    2. 同步阻塞:运行状态的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(Lock Pool)中。
    3. 其他阻塞:运行的线程执行Thread.sleep()方法时,或使用join()方法,或者发出了I/O请求时,JVM会把该线程设置为阻塞状态。当sleep()状态超时,join()等待线程终止或者超时,或者I/O处理完毕时,线程重新进入就绪状态。
  5. 死亡状态:线程run(),main()方法执行结束,或者因为异常退出了run()方法,则该线程结束生命周期,死亡的线程不能进入就绪状态。

1.3 基础

1.3.1 int,Integer,AtomicInteger的区别

  1. int是基本数据类型,Integer是引用数据类型。
  2. int和Integer之间可以自动拆装箱。
  3. int的默认值是0,Integer的默认值是null。
  4. 使用泛型时只能填入包装类Integer,不能填入基本类型int。
  5. Integer的valueOf()方法默认缓存值是-128~127
  6. AtomicInteger原子操作类适用于高并发场景下,线程安全。

1.3.2 Java中的四个基本流,处理文本用什么流,处理照片用什么流

Java中的四个基本流是

  • InputStream 字节输入流
  • OutputStream 字节输出流
  • Reader 字符输入流
  • Writer 字符输出流

能使用字节流一定能使用字符流,反之字节流能使用字符流处理的文件。

文本用字符流,照片(二进制文件)字节流。

1.3.3 使用反射创建实例的几种方式

  1. 对象调用getClass()方法。
  2. 类名.class()的方法。
  3. 通过class对象的forName()静态方法来获取,使用次数最多。

1.3.4 使用反射调用对象的方法

对象.getClass(),获取Class文件,再调用getMethod()获取Method方法对象,使用方法对象.invoke(obj),调用方法。

2. Spring框架

1.1 Spring

1.1.1 什么是Spring

Spring是一种轻量级框架,旨在提高开发人员的开发效率以及系统的可维护性。我们一般说的Spring框架就是SpringFramework,它是一个模块集合。

1.2 SpringMVC

3. HTTP/网络

3.1 TCP

3.1.1 Http三次握手,四次挥手

三次握手

四次挥手

3.1.2 Session和Cookie的区别

4. 数据库

4.1 MySQL

4.1.1 PrepareStatement和Statement的区别

  1. PrepareStatement是预编译的,批处理效率高。Statement对象只在一次性存取语句时使用。因为使用Statement对象开销比PrepareStatement大。
  2. 使用PrepareStatement可以防止SQL注入,如果使用Statement会有SQL注入的风险。
  3. PrepareStatement为每个参数保留一个?作为占位符。

4.1.2 为什么要用数据库连接池,数据库连接池的原理

  1. 资源重用:

    数据库连接得到重用,避免了频繁创建、释放连接引起的大量的性能开销。减少系统资源的消耗的同时提高了系统运行环境的稳定性。(数据库建立连接的线程开销)。

  2. 更快的系统响应速度:

    对于业务请求,可以直接使用连接池中的连接,不必重新创建连接,避免了连接创建的时间开销,缩短系统整体响应时间。

  3. 新的资源分配手段:

    对于多应用共享同一数据库的系统而言,可以在应用层通过数据库连接池的配置,实现数据库连接技术。

  4. 统一的连接管理:

    避免数据库连接泄漏。

原理

  1. 以空间换时间的思想,系统预先创建多个数据库连接对象,虽然会占用一定的内存空间,但是可以省去后面每次执行SQL的创建和释放连接的时间。
  2. 常用的数据库连接池:
    1. C3P0
    2. DBCP
    3. Druid(阿里)

4.2 Redis

5. MyBatis

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值