Java基础面试题总结,可供复习

一.Java基础

1. Java 基础知识

1.1 重载和重写的区别

重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以
不同,发生在编译时。


重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,
访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。

1.2 String 和 StringBuffffer、StringBuilder 的区别是什么?String 为什么是不可变的?

可变性
简单的来说:String 类中使用 fifinal 关键字字符数组保存字符串, private final char value[] ,所以 String对象是不可变的。而StringBuilder 与 StringBuffffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder中也是使用字符数组保存字符串 char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。

StringBuilder 与 StringBuffffer 的构造方法都是调用父类构造方法也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码.

在这里插入图片描述线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共 方法。StringBuffffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对
方法进行加同步锁,所以是非线程安全的。

性能

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    
     
char[] value; 
int count; 
AbstractStringBuilder() {
    
     
}
AbstractStringBuilder(int capacity) {
    
     
value = new char[capacity]; 
}

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。

StringBuffffer 每次都会对 StringBuffffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StirngBuilder 相比使用 StringBuffffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

对于三者使用的总结:

  1. 操作少量的数据 = String
  2. 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据 = StringBuffffer
1.3 自动装箱与拆箱

装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;

1.4 == 与 equals

== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型= =比较的是值,引用数据类型= =比较的是内存地址)

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。

情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。


public class test1 {
    
     
public static void main(String[] args) {
    
     
String a = new String("ab"); // a 为一个引用 
String b = new String("ab"); // b为另一个引用,对象的内容一样 
String aa = "ab"; // 放在常量池中 
String bb = "ab"; // 从常量池中查找 
if (aa == bb) // true 
System.out.println("aa==bb"); 
if (a == b) // false,非同一对象 
System.out.println("a==b"); 
if (a.equals(b)) // true 
System.out.println("aEQb"); 
if (42 == 42.0) {
    
     // true 
System.out.println("true"); 
} 
} 
}

说明:
String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的equals 方法比较的是对象的值。

当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。

1.5 关于 final 关键字的一些总结

final关键字主要用在三个地方:变量、方法、类。

  1. 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
  2. 当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。
  3. 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。

在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为fianl。

1.6 Object类的常见方法总结

Object类是一个特殊的类,是所有类的父类。它主要提供了以下11个方法:

//native方法,用于返回当前运行时对象的Class对象,使用了 final关键字修饰,故不允许子类重写。 
public final native Class<?> getClass()

//native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。 
public native int hashCode() 

//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户 比较字符串的值是否相等。 
public boolean equals(Object obj)

//naitive方法,用于创建并返回 当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() 
//== x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生 CloneNotSupportedException异常。 
protected native Object clone() throws CloneNotSupportedException

//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方 法。
public String toString()

//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视 器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。 
public final native void notify()

//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒 在此对象监视器上等待的所有线程,而不是一个线程。
public final native void notifyAll() 

//native方法,并且不能 重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。
public final native void wait(long timeout) throws InterruptedException 

//多了nanos参数, 这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。 
public final void wait(long timeout, int nanos) throws InterruptedException

//跟之前的2个wait方法一样,只不过该方法一直等 待,没有超时时间这个概念 
public final void wait() throws InterruptedException

//实例被垃圾回收器回收的时候触发的操作
protected void finalize() throws Throwable {
    
     }

1.7 Java 中的异常处理

在这里插入图片描述在 Java 中,所有的异常都有一个共同的祖先java.lang包中的 Throwable类。Throwable: 有两个重要的子类:

Exception(异常) 和 Error(错误) ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。

  1. Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。
    这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错
    误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。

  2. Exception(异常):是程序本身可以处理的异常。 Exception 类有一个重要的子类 RuntimeException。
    RuntimeException 异常由Java虚拟机抛出。NullPointerException(要访问的变量没有引用任何对象时,抛出该 异常)、ArithmeticException(算术运算异常,一个整数除以0时,抛出该异常)和
    ArrayIndexOutOfBoundsException (下标越界异常)。

注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。

Throwable类常用方法 :

  1. public string getMessage():返回异常发生时的详细信息
  2. public string toString():返回异常发生时的简要描述
  3. public string getLocalizedMessage():返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以声称本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同
  4. public void printStackTrace():在控制台上打印Throwable对象封装的异常信息

异常处理总结

try 块:用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个fifinally块。

catch 块:用于处理try捕获到的异常。

fifinally 块:无论是否捕获或处理异常,fifinally块里的语句都会被执行。当在try块或catch块中遇到return语句 时,fifinally语句块将在方法返回之前被执行。

在以下4种特殊情况下,fifinally块不会被执行:

  1. 在fifinally语句块中发生了异常。
  2. 在前面的代码中用了System.exit()退出程序。
  3. 程序所在的线程死亡。
  4. 关闭CPU。
1.8 获取用键盘输入常用的的两种方法

**方法1:通过 Scanner **

Scanner input = new Scanner(System.in); 
String s = input.nextLine(); 
input.close(); 

**方法2:通过 BufffferedReader **

BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); 
String s = input.readLine(); 
1.9 接口和抽象类的区别是什么
  1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),抽象类可以有非抽象的方法
  2. 接口中的实例变量默认是 fifinal 类型的,而抽象类中则不一定
  3. 一个类可以实现多个接口,但最多只能实现一个抽象类
  4. 一个类实现接口的话要实现接口的所有方法,而抽象类不一定
  5. 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
    备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,必须重写,不然会报错。
1.10JVM调优及内存泄漏
JVM调优工具

Jconsole,jProfile,VisualVM

Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。

**JProfiler:**商业软件,需要付费。功能强大

**VisualVM:**JDK自带,功能强大,与JProfiler类似。推荐。

如何调优

观察内存释放情况、集合类检查、对象树
上面这些调优工具都提供了强大的功能,但是总的来说一般分为以下几类功能

堆信息查看
在这里插入图片描述可查看堆空间大小分配(年轻代、年老代、持久代分配)
提供即时的垃圾回收功能
垃圾监控(长时间监控回收情况)
在这里插入图片描述查看堆内类、对象信息查看:数量、类型等

在这里插入图片描述对象引用情况查看
有了堆信息查看方面的功能,我们一般可以顺利解决以下问题:
– 年老代年轻代大小划分是否合理
– 内存泄漏
– 垃圾回收算法设置是否合理

线程监控

在这里插入图片描述线程信息监控:系统线程数量。
线程状态监控:各个线程都处在什么样的状态下
在这里插入图片描述Dump线程详细信息:查看线程内部运行情况
死锁检查

热点分析
在这里插入图片描述 CPU热点 检查系统哪些方法占用的大量CPU时间

内存热点 检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对象一起统计)

这两个东西对于系统优化很有帮助。我们可以根据找到的热点,有针对性的进行系统的瓶颈查找和进行系统优化,而不是漫无目的的进行所有代码的优化。

快照

快照是系统运行到某一时刻的一个定格。在我们进行调优的时候,不可能用眼睛去跟踪所有系统变化,依赖快照功能,我们就可以进行系统两个不同运行时刻,对象(或类、线程等)的不同,以便快速找到问题

举例说,我要检查系统进行垃圾回收以后,是否还有该收回的对象被遗漏下来的了。那么,我可以在进行垃圾回收前后,分别进行一次堆情况的快照,然后对比两次快照的对象情况。

内存泄漏检查

内存泄漏是比较常见的问题,而且解决方法也比较通用,这里可以重点说一下,而线程、热点方面的问题则是具体问题具体分析了。

内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。

内存泄漏对系统危害比较大,因为他可以直接导致系统的崩溃。

需要区别一下,内存泄漏和系统超负荷两者是有区别的,虽然可能导致的最终结果是一样的。内存泄漏是用完的资源没有回收引起错误,而系统超负荷则是系统确实没有那么多资源可以分配了(其他的资源都在使用)。

年老代堆空间被占满

异常: java.lang.OutOfMemoryError: Java heap space

在这里插入图片描述这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分配新空间。

如上图所示,这是非常典型的内存泄漏的垃圾回收情况图。所有峰值部分都是一次垃圾回收点,所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有谷底的点,可以发现一条由底到高的线,这说明,随时间的推移,系统的堆空间被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄漏。(上面的图仅供示例,在实际情况下收集数据的时间需要更长,比如几个小时或者几天)

解决:

这种方式解决起来也比较容易,一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。

持久代被占满

异常:java.lang.OutOfMemoryError: PermGen space

说明:
Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。

更可怕的是,不同的classLoader即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有N个classLoader那么他将会被加载N次。因此,某些情况下,这个问题基本视为无解。当然,存在大量classLoader和大量反射类的情况其实也不多。

解决

  1. -XX:MaxPermSize=16m
  2. 换用JDK。比如JRocket。

堆栈溢出

异常:java.lang.StackOverflowError
说明:这个就不多说了,一般就是递归没返回,或者循环调用造成

线程堆栈满

异常:Fatal: Stack size too small
说明:java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。
解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。

1.11JVM内存管理

JVM将内存划分为6个部分:PC寄存器(也叫程序计数器)、虚拟机栈、堆、方法区、运行时常量池、本地方法栈
在这里插入图片描述

  • • PC寄存器(程序计数器):用于记录当前线程运行时的位置,每一个线程都有一个独立的程序计数器,线程的阻塞、恢复、挂起等一系列操作都需要程序计数器的参与,因此必须是线程私有的。
  • • java 虚拟机栈:在创建线程时创建的,用来存储栈帧,因此也是线程私有的。java程序中的方法在执行时,会创建一个栈帧,用于存储方法运行时的临时数据和中间结果,包括局部变量表、操作数栈、动态链接、方法出口等信息。这些栈帧就存储在栈中。如果栈深度大于虚拟机允许的最大深度,则抛出StackOverflowError异常。
  • java 堆:java堆被所有线程共享,堆的主要作用就是存储对象。如果堆空间不够,但扩展时又不能申请到足够的内存时,则抛出OutOfMemoryError异常。
    在这里插入图片描述 方法区:方发区被各个线程共享,用于存储静态变量、运行时常量池等信息。

本地方法栈:本地方法栈的主要作用就是支持native方法,比如在java中调用C/C+

1.12GC回收机制
  • 哪些内存需要回收?——who

  • 什么时候回收?——when

  • 怎么回收?——how

  • 哪些内存需要回收?
    在这里插入图片描述什么时候回收?

  • 引用计数法

  • 可达性分析

(1)、引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器加一。反之每当一个引用失效时,计数器减一。当计数器为0时,则表示对象不被引用。

举个例子:

在这里插入图片描述(2)、可达性分析

设立若干根对象(GC Root),每个对象都是一个子节点,当一个对象找不到根时,就认为该对象不可达。
在这里插入图片描述没有一条从根到Object4 和 Object5的路径,说明这两个对象到根是不可达的,可以被回收

补充:java中,可以作为GC Roots的对象包括:

  • java虚拟机栈中引用的对象
  • 方法区中静态变量引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

3、怎么回收?

  • 标记——清除算法
  • 复制算法
  • 分代算法

(1)、标记——清除算法

遍历所有的GC Root,分别标记处可达的对象和不可达的对象,然后将不可达的对象回收。

缺点是:效率低、回收得到的空间不连续

(2)、复制算法
将内存分为两块,每次只使用一块。当这一块内存满了,就将还存活的对象复制到另一块上,并且严格按照内存地址排列,然后把已使用的那块内存统一回收。

优点是:能够得到连续的内存空间

缺点是:浪费了一半内存

在这里插入图片描述(3)、分代算法

在java中,把内存中的对象按生命长短分为:

• 新生代:活不了多久就go die 了,比如局部变量
• 老年代:老不死的,活的久但也会go die,比如一些生命周期长的对象
• 永久代:千年王八万年龟,不死,比如加载的class信息
有一点需要注意:新生代和老年代存储在java虚拟机堆上 ;永久代存储在方法区上
在这里插入图片描述补充:java finalize()方法:

在被GC回收前,可以做一些操作,比如释放资源。有点像析构函数,但是一个对象只能调用一次finalize()方法。

2.Java 集合框架

Java集合之ArrayList源码分析
Java集合之LinkedList源码分析
Java集合HashMap引发的一系列问题
2.1 ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。

底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;

实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

在这里插入图片描述
在这里插入图片描述

2.2 ConcurrentHashMap线程安全的具体实现方式/底层具体实现

JDK1.7(上面有示意图)
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。

Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。

JDK1.8 (上面有示意图)
ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。

synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

2.3 集合框架底层数据结构总结

Collection

  1. List
    Arraylist: Object数组
    Vector: Object数组
    LinkedList: 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环) 详细可阅读JDK1.7-LinkedList循环链表优化

  2. Set
    HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素
    LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类
    似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
    TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)

  3. Map
    HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
    LinkedHashMap: LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:
    HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
    TreeMap: 红黑树(自平衡的排序二叉树)

3.Java多线程

待续。。。。

猜你喜欢

转载自blog.csdn.net/weixin_44078653/article/details/105149663
今日推荐