Java基础面试题

69 阅读21分钟

1.JDK与JRE区别?

JDK提供了java的开发环境和运行环境,而JRE提供了java的运行环境。具体来说就是JDK包含了JRE,还包含了编译java源码的编译器javac,如果你要运行java程序就安装JRE就行了,如果你要编写java程序就需要安装jdk

2.== 和 equals 的区别是什么?

==在基本类型和引用类型中的效果是不同的,在基本类型中比较的是值是否相同,而在引用类型中比较的是引用是否相同。equals 默认情况下是引用比较,只是很多类重写了 equals 方法,比如 StringInteger 等、把它从引用比较变成了值比较,所以一般情况下 equals 比较的是值是否相等。

3.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?

不对,两个对象的 hashCode()相同,equals()不一定 true

4.为什么需要重写equalshashCode方法?

重写 equals 和 hashCode 方法是因为它们是成对出现的,用于判断对象内容是否相等,它们需要协同工作,尤其是在使用哈希集合(如 HashSet 或 HashMap)时。如果重写了 equals 但没有重写 hashCode,就会导致同一个相等对象可能拥有不同的哈希码值,从而在哈希集合中被当作不同的元素存储,造成程序行为异常和效率低下。 

  • equals方法:用于判断两个对象的内容是否相等,而不是仅仅比较它们的内存地址。默认情况下,Object 类中的 equals 方法是比较对象的引用是否相同(即内存地址是否一样)。重写 equals 方法可以让开发者根据业务需求来定义对象的相等性,比如两个 Person 对象的 id 相同就认为它们相等。
  • hashCode方法:用于计算对象的哈希值,这个值可以用来快速定位对象在哈希表(如 HashMap 的桶)中的位置。
  • 成对重写的原则:根据 Java 的约定,如果两个对象通过 equals 方法比较是相等的(返回 true),那么它们必须返回相同的 hashCode 值。
  • 违反原则的后果:
    • equals 为 true,hashCode 不同:当你在HashMap 中使用一个对象作为键,如果它与一个已存在的键通过 equals 判定相等,但它们的 hashCode 不同,HashMap 会认为它们是两个不同的键,可能会创建新的条目,导致数据不一致或重复。
    • 影响集合性能:HashSet 等集合会先根据 hashCode 找到可能的存储位置,然后再用 equals 进行确认。如果 hashCode 不一致,即使 equals 相等,也会导致对象没有被正确地添加到集合中,或者查询效率低下。 

因此,在重写 equals 方法时,必须同时重写 hashCode 方法,以确保对象的正确性和一致性。

5.final, finally, finalize的区别?

final 是 Java 语言中的一个关键字,使用 final 修饰的对象不允许修改或替换其原始值或定义。
final 可以用来修饰:类、方法、变量和参数,其中可以用来修饰“参数”这一项,容易被人遗忘,这是 final 的 4 种用法。

final 用法说明

  • 当 final 修饰时,此类不允许被继承,表示此类设计的很完美,不需要被修改和扩展。
  • 当 final 修饰方法时,此方法不允许任何从此类继承的类来重写此方法,表示此方法提供的功能已经满足当前要求,不需要进行扩展。
  • 当 final 修饰变量时,表示该变量一旦被初始化便不可以被修改。
  • 当 final 修饰参数时,表示此参数在整个方法内不允许被修改。

finally 则是 Java 中保证重点代码一定要被执行的一种机制。
我们可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC 连接、保证释放锁等动作

finalizeObject 类中的一个基础方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收,但在 JDK 9 中已经被标记为弃用的方法(deprecated)。

在实际开发中不推荐使用 finalize 方法,它虽然被创造出来,但无法保证 finalize 方法一定会被执行,所以不要依赖它释放任何资源,因为它的执行极不“稳定”。在 JDK 9 中将它废弃,也很好的证明了此观点。

6.String是基础数据类型吗?

String不属于基本数据类型,基本数据类型有8种,分别是byte short int long float double boolean char,String属于引用数据类型

7.java 中操作字符串都有哪些类?它们之间有什么区别?

String、StringBuffer、StringBuilder
String和StringBuffer、StringBuilder的区别在于String声明的是不可变的字符序列,每次操作都会生成新的String对象,然后将指针指向新的String对象,而StringBuffer、StringBuilder可以在原有的对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用String
StringBuffer和StringBuilder的最大区别在于,StringBuffer是线程安全的,StringBuilder是线程不安全的,但StringBuilder的性能却高于StringBuffer,所以在单线程环境下推荐使用StringBuilder,多线程环境下使用StringBuffer

8. String str="i"与 String str=new String(“i”)一样吗?

不一样,内存分配方式不一样,Stirng str="i"的方式,java虚拟机会将其分配到字符串常量池中,而String str = new String("i")则会分配到堆内存中。类似于String str=new String(“i”)会创建两个对象:一个是在堆上创建的新的 String 对象,另一个是在字符串常量池中创建的字符串 "i"

9.如何将字符串反转?

使用StringBuilder 或者 StringBuffer的reverse() 方法

10.抽象类一定需要抽象方法吗?

不一定,抽象类不一定需要抽象方法

11.抽象类与普通类的区别?

普通类不能包含抽象方法,抽象类可以包含抽象方法。抽象类不能直接实例化new Xxx(),普通类可以直接实例化new Xxx()

12.抽象类与接口的区别?

修饰符不同,一个是abstract,一个是interface。抽象类中可以有不是抽象的方法,接口当中必须全是抽象方法(jdk1.8之前成立),jdk1.8还可以定义静态方法和默认方法。接口中变量全部默认使用public static final修饰的常量,抽象类中不限制 。抽象类只能够被单继承,接口可以有多实现。抽象类可以有构造函数;接口不能有。接口中的方法默认使用 public 修饰,抽象类中的方法可以是任意访问修饰符。

13.Java中IO分类?

按功能来分: 输入流、输出流 按类型来分: 字节流和字符流

字节流和字符流的区别是: 字节流按8位传输以字节为单位输入输出数据, 字符流按16位传输以字符为单位输入输出数据

14.BIO,NIO,AIO的区别?

BIO(Blocking I/O):采用阻塞式 I/O 模型,线程在执行 I/O 操作时被阻塞,无法处理其他任务,适用于连接数较少且稳定的场景。

NIO(New I/O 或 Non-blocking I/O):使用非阻塞 I/O 模型,线程在等待 I/O 时可执行其他任务,通过 Selector 监控多个 Channel 上的事件,提高性能和可伸缩性,适用于高并发场景。

AIO(Asynchronous I/O):采用异步 I/O 模型,线程发起 I/O 请求后立即返回,当 I/O 操作完成时通过回调函数通知线程,进一步提高了并发处理能力,适用于高吞吐量场景。

15.Files的常用方法都有哪些?

Files.exists(): 检测文件路径是否存在

Files.createFile(): 创建文件

Files.createDirectory(): 创建文件夹

Files.delete() : 删除一个文件或目录

Files.read(): 读取文件

Files.write(): 写入文件

15.Map与Collection?

Collection 和 Map 是 Java 集合框架的两个根接口,主要区别在于存储方式:Collection 存储一组对象(单列集合),而 Map 存储键值对(双列集合)。Collection 的子接口包括 List(有序、可重复)和 Set(无序、不可重复)等,而 Map 的键(key)是唯一的。 

Collection

  • 作用: 存储一组对象。
  • 特性:
    • 单列集合: 存储的是单个元素。
    • 根接口: 是 ListSetQueue 等集合接口的父接口。
    • 子接口:
      • List: 存储的元素有序且允许重复。
      • Set: 存储的元素不重复。 

Map

  • 作用: 存储键值对(key-value)的映射关系。
  • 特性:
    • 双列集合: 存储的是键值对。
    • 根接口: 与 Collection 接口同级,不继承自 Collection。
    • 键(key)的唯一性: 每个键在 Map 中是唯一的。
    • 值(value)的重复性: 允许重复的值。

16.ArrayList与LinkedList的区别?

ArrayList: ArrayList是Java集合框架中的一个类,它实现了List接口,底层基于数组实现。ArrayList的特点是支持动态数组,可以自动扩容,适合顺序访问和随机访问。

LinkedList: LinkedList也是Java集合框架中的一个类,同样实现了List接口,但底层基于链表实现。LinkedList的特点是支持高效的插入和删除操作,但随机访问的性能相对较差。

区别与优缺点对比 存储结构:ArrayList使用数组作为底层数据结构,数据在内存中是连续存储的,因此支持随机访问非常快速。LinkedList则使用链表作为底层数据结构,每个元素都包含指向前后元素的指针,插入和删除操作非常高效。

插入与删除操作:在ArrayList中,如果插入或删除元素,可能会导致数组元素的移动,从而影响性能。而LinkedList在插入和删除操作上具有明显优势,因为只需修改指针的指向,不需要移动大量元素。

随机访问性能:由于ArrayList的数组连续存储特性,它在随机访问上具有很好的性能。通过索引即可直接访问元素。而LinkedList需要从头或尾开始遍历链表,随机访问性能较差。

内存占用:由于LinkedList每个元素都需要存储前后指针,相对于ArrayList会占用更多的内存空间。如果需要存储大量数据,考虑内存占用也是一个重要因素。

迭代性能:在迭代(遍历)操作上,ArrayList由于连续存储的特性,性能通常较好。而LinkedList在迭代操作上由于需要通过指针跳转,性能相对较差。

如何选择?

  • 使用ArrayList的场景: 需要频繁进行随机访问,例如根据索引获取元素。 数据集合相对固定,不需要频繁的插入和删除操作。 内存占用相对较少,不会造成严重的资源浪费。
  • 使用LinkedList的场景: 需要频繁进行插入和删除操作,尤其是在中间位置。 不关心随机访问性能,而更关注插入和删除的效率。 可能需要更少的内存占用,尤其是在元素数量较少的情况下。

17.ArrayList与Vector的区别?

ArrayList和Vector的主要区别在于线程安全性性能:Vector是线程安全的(同步的Vector的线程安全是通过在每个方法上都使用synchronized关键字来实现的),因此在多线程环境下使用;而ArrayList不是线程安全的(非同步的),性能更好,适合单线程环境。此外,在容量不足时,Vector默认将容量翻倍,而ArrayList默认增加容量的50%。

18.ArrayList如何转成成线程安全的List?

可以通过 Collections.synchronizedList()方法将 ArrayList 转换成线程安全的集合,或者使用线程不安全的 CopyOnWriteArrayList,Collections.synchronizedList() 方法会返回一个线程安全的 List,该列表中的所有方法都会进行同步,以保证在多线程环境下操作的安全性。

19.HashMap与HashTable的区别?

  • HashMap不是线程安全的,HashTable是线程安全。
  • HashMap允许空(null)的键和值(key),HashTable则不允许。
  • HashMap性能优于Hashtable(Hashtable的线程安全是通过在每个方法上都使用synchronized关键字来实现的)

20.synchronized锁的升级过程?

  • 无锁 -> 偏向锁: 当一个线程首次执行同步代码块时,如果对象未上锁,synchronized 会将其偏向于该线程,此时升级为偏向锁。
  • 偏向锁 -> 轻量级锁: 当有其他线程尝试竞争已经偏向某个线程的锁时,偏向锁会升级为轻量级锁。
  • 轻量级锁 -> 重量级锁: 当轻量级锁发生竞争时,会通过 CAS 操作和自旋(自旋锁)来尝试获取锁。如果自旋一定次数后仍未获取到锁,则升级为重量级锁。

21.重载和重写的区别?

重载是发生在同一个类中,方法名相同,参数的类型、个数不同。重写发生在子类中,方法的名称、类型、返回值全部相同

22.Java 为什么要有包装类? 基本数据类型和对应包装类的区别?

因为当向ArrayList、HashMap中放东西时,int、double这种基本类型是放不进去的,因为容器都是装object的,这就需要有包装类了。基本数据类型不使用new关键字,而包装类需要使用new关键字在堆中分配存储空间;基本类型是将值存储在栈中,包装类型是将值放在堆中;

23.双亲委派机制可以打破吗?

可以打破。第一种是自定义类加载器去继承ClassLoader类重写loadClass()方法,第二种是线程上下文类加载器破坏双亲委派模型。

24.双亲委派机制是什么?

当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。 Java中提供的这四种类型的加载器,是有各自的职责的:

  • Bootstrap ClassLoader ,主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
  • Extention ClassLoader,主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
  • Application ClassLoader ,主要负责加载当前应用的classpath下的所有类
  • User ClassLoader , 用户自定义的类加载器,可加载指定路径的class文件 一个用户自定义的类,如com.hollis.ClassHollis 是无论如何也不会被ClassLoader和Extention ClassLoader加载的。

首先,通过委派的方式,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。 另外,通过双亲委派的方式,还保证了安全性。因为Bootstrap ClassLoader在加载的时候,只会加载JAVA_HOME中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的JDK。

双亲委派模型中,类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码的

25.throws与throw的区别?

throw是抛出一个异常,throws用来声明一个方法可能抛出的异常

26.Java中的Exception与Error是什么?

在 Java 中,Exception 和 Error 都继承自 Throwable 类。Exception 是程序在运行中可能遇到的可预料的意外情况,可以被捕获和处理。Error 指的是系统级或严重问题,通常是程序无法处理的,比如 JVM 内存溢出,需要恢复的严重错误。- Exception分类: - 编译时异常(Checked Exception) : 在源代码中必须显式处理,否则程序无法编译通过。例如,文件未找到 (FileNotFoundException)。 - 运行时异常(Unchecked Exception) : 继承自 RuntimeException。它们在编译时不会强制检查,可以不捕获或抛出。通常是由于程序逻辑错误引起,如 NullPointerExceptionArrayIndexOutOfBoundsException 等。常见的RuntimeException NullPointerException (空指针异常) NumberFormatException (数字格式化异常) ArrayIndexOutOfBoundsException (数组越界异常) StringIndexOutOfBoundsException (字符串越界异常) ClassCastException (类型转换异常) ArithmeticException (算术异常) IllegalArgumentException (非法参数异常)

27.JDK动态代理实现方法与实现步骤是怎样的?

实现方法:

  • 在java的动态代理机制中,有两个重要的类或接口,一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class),这一个接口和类是实现我们动态代理所必须用到的

实现步骤:

  • step 1:拿到被代理对象的引用,并且通过反射获取到它的所有的接口。
  • step 2:通过JDK Proxy类重新生成一个新的类,同时新的类要实现被代理类所实现的所有的接口。
  • step 3:动态生成 Java 代码,把新加的业务逻辑方法由一定的逻辑代码去调用。
  • step 4:编译新生成的 Java 代码.class。
  • step 5:将新生成的Class文件重新加载到 JVM 中运行。

28.JDK动态代理与CGLIB代理的区别?

JDK动态代理是通过重写被代理对象实现的接口中的方法来实现,而CGLIB是通过继承被代理对象来实现,和JDK动态代理需要实现指定接口一样,CGLIB也要求代理对象必须要实现MethodInterceptor接口,并重写其唯一的方法intercept。 CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。(利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理)

注意:因为CGLIB是通过继承目标类来重写其方法来实现的,故而如果是final和private方法则无法被重写,也就是无法被代理。

29.Java中创建对象的几种方式?

  • new关键字
  • Class.newInstance
  • Constructor.newInstance
  • Clone方法
  • 反序列化
30.深度拷贝与浅度拷贝区别,如何实现深度拷贝?

区别:

浅度拷贝只复制对象的引用,而深度拷贝会为对象及其所有子对象在内存中创建独立的副本

实现方法:

  • 实现 Cloneable 接口并重写 clone() 方法
  • 使用序列化和反序列化,这种方法通过将对象转换为字节流,然后从字节流中恢复为新的对象来创建深拷贝副本。

31.Java中的封装,继承,多态如何理解?

  1. 封装 (Encapsulation)
  • 概念:将数据(属性)和操作数据的方法(函数)捆绑在一起,形成一个独立的单元——类。

  • 作用

    • 信息隐藏:隐藏对象的内部状态和实现细节,外部只能通过公共接口来访问和修改数据。
    • 提高安全性:防止外部代码不小心或恶意地修改对象内部数据,提高代码的健壮性。
    • 提高可维护性:当内部实现发生变化时,只要公共接口不变,就不会影响到使用该类的其他代码。 
  1. 继承 (Inheritance)
  • 概念:子类(派生类)可以继承父类(基类)的属性和方法,从而获得父类的特性。

  • 作用

    • 代码复用:避免重复编写相同的代码,子类可以直接复用父类的代码。
    • 建立层次结构:形成类之间的“is-a”关系(例如,“狗”is-a“动物”),使代码结构更清晰。
  • Java 特性:Java是单继承语言,一个类只能直接继承一个父类。 

  1. 多态 (Polymorphism)
  • 概念:多态意味着同一个方法调用,在不同的对象上会有不同的行为表现。

  • 实现方式

    • 父类引用指向子类对象:父类引用变量可以指向其子类的对象。
    • 方法重写 (Overriding) :当子类和父类中定义了相同的方法签名时,子类可以重写父类的方法。
  • 作用

    • 灵活性和通用性:允许编写更通用、更灵活的代码。例如,一个处理“动物”的方法,可以通过多态来处理“狗”、“猫”等不同类型的动物对象,而无需知道具体是哪种动物。
    • 多种形态:一个接口或父类可以有多种不同的实现。
  • 编译时多态与运行时多态

    • 编译时多态:通常指方法重载 (Overloading),编译器在编译时根据方法参数的类型和数量来确定调用哪个方法。
    • 运行时多态:通常指方法重写 (Overriding),根据对象的实际类型在运行时决定调用哪个方法。

32.说说HashMap的底层实现

HashMap的底层是一个数组,存储着Node(或TreeNode)对象,当链表过长时会转为红黑树。之所以容量总是2的幂次方,是因为这允许在计算元素位置时使用高效的位运算(hash & (length - 1)),而非缓慢的取模运算。同时,当容量为2的幂次方时,扩容时只需将容量翻倍,并根据新容量计算元素的新位置,过程更简单高效,并能确保哈希值能更均匀地分布,从而减少哈希冲突。 

底层结构

  • 数组HashMap的底层是一个数组,每个元素都是一个链表或红黑树的起始节点。
  • NodeNode是链表的节点,包含键、值、哈希值以及指向下一个节点的引用。
  • TreeNode:当链表中的节点数量达到一定阈值(在JDK 8中是8个)时,该链表会转换为一颗红黑树,以优化查找性能。 

为什么容量为2的幂次方

  1. 效率更高的哈希计算:使用位运算能显著提高查找和插入的效率。

    • 当容量为2的幂次方时,可以用 (capacity - 1) & hash 来代替取模运算 % 来计算索引。
    • 例如,如果 capacity 是16,那么 (capacity - 1) 是15(二进制为1111)。此时,hash & 15 的结果就是取模运算的等价结果,且效率远高于取模运算。
  2. 更均匀的哈希分布:长度为2的幂次方能帮助更均匀地分布哈希值,减少哈希冲突,避免过长的链表。

    • 这使得HashMap在多数情况下都能保持较快的查找速度。
  3. 更高效的扩容:当HashMap需要扩容时,新容量通常是原容量的两倍。

    • 扩容时,原有的链表数据不需要重新计算每一个元素的索引,只需将原链表按照新容量的计算方式分成两部分(奇偶索引),然后直接复制到新数组的对应位置即可,这比重新计算所有元素的索引要高效得多。

33.ConcurrentHashMap是如何实现线程安全的?

  • 分段锁(Java 1.7及之前) :

    • ConcurrentHashMap 内部维护一个或多个 Segment 数组。
    • 每个 Segment 包含一个独立的哈希表,并由一个独立的锁来保护。
    • 当线程需要访问一个段时,只需要获取该段的锁,而不需要锁定整个哈希表。
    • 这允许不同线程同时操作不同的段,显著提高了并发度。
  • 更精细的锁和CAS(Java 1.8及之后) :

    • 放弃分段锁: Java 8 版本不再使用分段锁,而是采用了一种更精细的锁机制。
    • 节点锁: 在写入时,每个哈希桶(通常是链表或红黑树的头节点)会有一个锁。
    • CAS: 对于某些更新操作,会使用 CAS(Compare and Swap)原子操作来尝试更新数据,避免了获取全局锁的开销。
    • volatile: 关键字段使用 volatile 关键字来保证线程之间的可见性,尤其是在读操作时,这允许无锁读取。
  • 内部数据结构:

    • ConcurrentHashMap 使用一个结合了 数组、链表和红黑树 的数据结构来处理哈希冲突。
    • 当链表长度超过一定阈值时,会将其转换为红黑树,以提高查找、插入和删除的效率。