Java基础面试题总结

1.Java集合对象

java中集合类都是由Collection和Map接口派生出来的,其中Set和List接口是Collection接口派生出来的子接口,他们分别代表了无序集合和有序集合,Map接口的所有实现类用于保存具有映射关系的数据(关联数组)。

ArrayList:实现了长度可变的数组,在内存中分配连续空间;遍历元素和随机访问元素的效率比较高,线程不安全。初始容量10,负载因子0.5,扩容增量0.5倍。
LinkedList:采用链表存储方式;插入、删除元素时效率比较高,线程不安全。
Vector:通过数组实现的,它支持线程的同步,访问比ArrayList慢,线程安全。初始容量10,负载因子1,扩容增量1倍。

HashSet:线程不安全,存取速度快。
HashMap:轻量级,线程不安全,效率较高,允许出现null。初始容量16,负载因子0.75,扩容增量1倍。 1.8之后底层红黑树实现
HashTable:重量级,线程安全,效率较低,不允许出现null。
ConcurrentHashMap:线程安全,分段加锁ReentrantLock(重入锁);
1.8之后synchronized+CAS+Node+红黑树,Node的val和next用volatile修饰

使用HashMap时,重写equals方法时,必须重写hashCode方法
hashCode()用来计算该对象放入数组中的哪个位置,因为是两个都是new的对象,所以即使里面的值一样,但是对象所处的地址却不同,所以使用默认的hashCode也就不同,当然在hashMap中就不会认为两个是一个对象。

2.通过Class类获取Class对象的方法

1)通过Object类的getClass()方法

Person p = new Person();
 Class c = p.getClass();

2) 通过 类名.class 获取到字节码文件对象(任意数据类型都具备一个class静态属性,看上去要比第一种方式简单)。

Class c2 = Person.class;

3)通过Class类中的方法(将类名作为字符串传递给Class类中的静态方法forName即可)

Class c3 = Class.forName("Person");

3.抽象类和接口

相同点:
(1)都不能被实例化
(2)接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。
不同点:
(1)接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
(2)实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
(3)接口强调特定功能的实现,而抽象类强调所属关系。
(4)接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public、abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。
(5)接口被用于常用的功能,便于日后维护和添加删除,而抽象类更倾向于充当公共类的角色,不适用于日后重新对立面的代码修改。功能需要累积时用抽象类,不需要累积时用接口。

4.面向对象三大特征

继承:代码复用、传递性。子类继承父类的特征和行为。子类可以有父类的方法,属性(非private)。子类也可以对父类进行扩展,也可以重写父类的方法。缺点就是提高代码之间的耦合度。
super:通过super实现对父类成员的访问。用来引用当前对象的父类。通过super显示的调用父类的有参构造,无参构造可以隐式调用;
this:用来引用当前对象,指向自己;
final:可以修饰类,方法,属性;修饰的类不能继承,修饰的方法不能重写,修饰的属性不能修改;

封装:隐藏内部实现,只暴露公共行为;把一堆数据属性与方法数据放在一个容器中,这个容器就是对象。让对象可以通过 "." 来调用对象中的数据属性与方法属性。
类:封装的是对象的属性和行为;
方法:封装一段特定的业务逻辑功能;
访问控制修饰符:封装的是具体的访问权限

多态:指的是同一个方法调用,由于对象不同可能会有不同的行为。现实生活中,同一个方法,具体实现会完全不同。多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象
对象多态(所有对象)体现:
向上造型:向上转型是自动的 Father f = new Children(); 不需要强转
向下造型:向下转型需要强转 Children c = (Children)new Father() 需要强转。让父类知道具体转成哪个子类

4.重写和重载

重写(Override)是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写。子类可以根据需要,定义特定于自己的行为, 也就是说子类能够根据需要实现父类的方法。重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。声明为 final 的方法不能被重写;声明为 static 的方法不能被重写,但是能够被再次声明;构造方法不能被重写。

重载(overloading)是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的地方就是构造器的重载。被重载的方法可以改变返回类型;被重载的方法可以改变访问修饰符;方法能够在同一个类中或者在一个子类中被重载;无法以返回值类型作为重载函数的区分标准。

4.final、finally和finalize

final:可以修饰类,方法,属性;修饰的类不能继承,修饰的方法不能重写,修饰的属性不能修改;
若父类中final方法的访问权限为private,将导致子类中不能直接继承该方法,因此,此时可以在子类中定义相同方法名的函数,此时不会与重写final的矛盾,而是在子类中重新地定义了新方法。
finally:finally作为异常处理的一部分,它只能用在try/catch语句中,并且附带一个语句块,表示这段语句最终一定会被执行(不管有没有抛出异常),经常被用在需要释放资源的情况下。只有与finally对应的try语句块得到执行的情况下,finally语句块才会执行。
finalize:finalize()是在java.lang.Object里定义的,也就是说每一个对象都有这么个方法。这个方法在gc启动,该对象被回收的时候被调用。

4.ThreadLocal

ThreadLocal 线程局部变量,适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。

get()方法用于获取当前线程的副本变量值。
set()方法用于保存当前线程的副本变量值。
initialValue()为当前线程初始副本变量值。
remove()方法移除当前前程的副本变量值。

4.反射

在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为反射机制。

Class clazz = Class.forName("全类名");
类 object = (类)clazz .newInstance();
//获取构造方法
Constructor constructor = clazz.getDeclaredConstructor(Class... parameterTypes);
类 object = (类)constructor.newInstance(parameter);

Method method = clazz.getDeclaredMethod("方法名");
method.invoke(object);

Field field = clazz.getDeclaredField("属性名");
System.out.println(field.get(object));

5.单例

懒汉式

public class LazySingleton {
          //静态私用成员,没有初始化
      private static LazySingleton intance = null;    
      private LazySingleton() {
            //私有构造函数
        }       
        //静态,同步,公开访问点
        public static synchronized LazySingleton getInstance() {   
            if(intance == null) {
                intance = new LazySingleton();
            }
            return intance;
        }
    }

饿汉式

public class EagerSingleton {
    //饿汉单例模式:在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快    
    private static EagerSingleton instance = new EagerSingleton();//静态私有成员,已初始化  
    private EagerSingleton() {
        //私有构造函数
    }
     //静态,不用同步(类加载时已初始化,不会有多线程的问题)
    public static EagerSingleton getInstance() {     
        return instance;
    }
    public static void main(String[] args) {
        EagerSingleton.getInstance();
        LazySingleton.getInstance();
    }
}

双检查

public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;
    public  static Instance getInstance(){
        if(instance ==null) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (instance == null)
                    instance = new Instance();            
            }
        }
        return instance;
    }
}

枚举:枚举类型是线程安全的,并且只会装载一次,序列化问题

public enum  EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

静态内部类:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存

public class SingleTon{
  private SingleTon(){
  }
 
  private static class SingleTonHoler{
     private static SingleTon INSTANCE = new SingleTon();
 }
 
  public static SingleTon getInstance(){
    return SingleTonHoler.INSTANCE;
  }

6.检查型异常和非检查型异常有什么区别

Java异常类层次结构图.png

检查型异常和非检查型异常的主要区别在于其处理方式。检查型异常都需要使用try,catch 和finally 关键字在编译器进行处理,否则会出现编译器报错。对于非检查型异常则不需要这样做。Java中所有继承 Exception 的类的异常都是检查型异常,所有继承RuntimeException 的异常都被称为非检查型异常。
checkedException:IOException、FileNotFoundException、parseException、自定义Exception
RuntimeException:NullPointerException、ClassNotFoundException 、NumberFormatException 、SQLException 、IndexOutOfBoundsException、ArithmeticException

7.error和exception有什么区别?

error 一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。
exception 表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。

8、throw 和 throws 的区别

throw 用在方法体内,表示抛出异常,由方法体内的语句处理;是具体向外抛出异常的动作,所以它抛出的是一个异常实例,执行 throw 一定是抛出了某种异常。
throws 用在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理。throws 主要是声明这个方法会抛出某种类型的异常,让它的使用者要知道需要捕获的异常的类型。throws 表示出现异常的一种可能性,并不一定会发生这种异常。

9、StackOverFlowError和OutOfMemoryError的区别

StackOverflowError:递归太深。真正的原因是因为Java线程操作是基于栈的,当调用方法内部方法也就是进行一次递归的时候就会把当前方法压入栈直到方法内部的方法执行完全之后,就会返回上一个方法,也就是出栈操作执行上一个方法。
OutOfMemoryError:因为内存溢出,JVM不能分配给对象的创建空间.并且GC也不能够回收足够的空间.当你创建对象的速度快于JVM回收空间的时候就会发生空间不足这个问题。

10、包装类的用途

  1. 作为和基本数据类型对应的类型存在,方便涉及到对象的操作,如Object[]、集合等的操作。
  2. 包含每种基本数据类型的相关属性如最大值、最小值等,以及相关的操作方法(这些操作方法的作用是在基本数据类型、包装类对象、字符串之间提供相互之间的转化)。

11、String、StringBuffer和StringBuilder

String:不可变字符序列。
StringBuffer:可变字符序列,并且线程安全,但是效率低。
StringBuilder:可变字符序列,线程不安全,但是效率高(一般用它)。

12、JVM内存管理

栈区:栈分为java虚拟机栈和本地方法栈重点是Java虚拟机栈,它是线程私有的,生命周期与线程相同。
每个方法执行都会创建一个栈帧,用于存放局部变量表,操作数栈,动态链接,方法出口等。每个方法从被调用,直到被执行完。对应着一个栈帧在虚拟机中从入栈到出栈的过程。通常说的栈就是指局部变量表部分,存放编译期间可知的8种基本数据类型,及对象引用和指令地址。局部变量表是在编译期间完成分配,当进入一个方法时,这个栈中的局部变量分配内存大小是确定的。
会有两种异常StackOverFlowError和 OutOfMemoneyError。当线程请求栈深度大于虚拟机所允许的深度就会抛出StackOverFlowError错误;虚拟机栈动态扩展,当扩展无法申请到足够的内存空间时候,抛出OutOfMemoneyError。本地方法栈为虚拟机使用到本地方法服务(native)

堆区:堆被所有线程共享区域,在虚拟机启动时创建,唯一目的存放对象实例。堆区是gc的主要区域,通常情况下分为两个区块年轻代和年老代。更细一点年轻代又分为Eden区最要放新创建对象,From survivor 和 To survivor 保存gc后幸存下的对象,默认情况下各自占比 8:1:1。

方法区:被所有线程共享区域,用于存放已被虚拟机加载的类信息,常量,静态变量等数据。被Java虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代(permanment generation)。垃圾回收很少光顾这个区域,不过也是需要回收的,主要针对常量池回收,类型卸载。
常量池用于存放编译期生成的各种字节码和符号引用,常量池具有一定的动态性,里面可以存放编译期生成的常量;运行期间的常量也可以添加进入常量池中,比如string的intern()方法。
程序计数器:当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。

Java虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置,每条线程都需要一个独立的程序计数器,所以它是线程私有的。唯一一块Java虚拟机没有规定任何OutofMemoryError的区块。

13.垃圾回收机制

不同的对象的生命周期是不一样的,我们将对象分为三种状态:年轻代、年老代、持久代。JVM将堆内存划分为 Eden、Survivor 和 Tenured/Old 空间。

年轻代:所有新生成的对象首先都是放在Eden区。 年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,对应的是Minor GC,每次 Minor GC 会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费内存空间。当“年轻代”区域存放满对象后,就将对象存放到年老代区域。

年老代:在年轻代中经历了N(默认15)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。年老代对象越来越多,我们就需要启动Major GC和Full GC(全量回收),来一次大扫除,全面清理年轻代区域和年老代区域。-XX:NewRatio来控制新生代和老年代的比例,比如-XX:NewRatio=2代表新生代和老年代的比例为1:2

持久代:用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。

堆内存的划分细节.png

Minor GC:用于清理年轻代区域。Eden区满了就会触发一次Minor GC。清理无用对象,将有用对象复制到“Survivor1”、“Survivor2”区中(这两个区,大小空间也相同,同一时刻Survivor1和Survivor2只有一个在用,一个为空)
Major GC:用于清理老年代区域。
Full GC:用于清理年轻代、年老代区域。 成本较高,会对系统性能产生影响。
老年代空间不足,永久代空间不足,调用System.gc

垃圾回收过程:
1、新创建的对象,绝大多数都会存储在Eden中,
2、当Eden满了(达到一定比例)不能创建新对象,则触发垃圾回收(GC),将无用对象清理掉,然后剩余对象复制到某个Survivor中,如S1,同时清空Eden区
3、当Eden区再次满了,会将S1中的不能清空的对象存到另外一个Survivor中,如S2,同时将Eden区中的不能清空的对象,也复制到S1中,保证Eden和S1,均被清空。
4、重复多次(默认15次)Survivor中没有被清理的对象,则会复制到老年代Old(Tenured)区中,
5、当Old区满了,则会触发一个一次完整地垃圾回收(FullGC),之前新生代的垃圾回收称为(minorGC)

13、GC的两种判定方法:引用计数法与引用可达法

1.引用计数:在JDK1.2之前,使用的是引用计数器算法,即当这个类被加载到内存之后,就会产生方法区,堆栈、程序计数器等一系列信息,当创建对象的时候,为这个对象在堆栈空间中分配对象,同时会产生一个引用计数器,同时引用计数器+1,当有新的引用时,引用计数器继续+1,而当其中一个引用销毁时,引用计数器-1,当引用计数器减为0的时候,标志着这个对象已经没有引用了,可以回收了

2.引用可达法(根搜索算法):根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看做一张图,从一个节点GC Root开始,寻找对应的引用节点,找到这个节点之后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被饮用到的节点,即无用的节点。

目前Java中可作为GC Root的对象有:
1.虚拟机栈中引用的对象(本地变量表)
2.方法区中静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中引用的对象(Native对象)。

JVM垃圾回收算法

  • 标记-清除(Mark-Sweep)
    jvm会扫描所有的对象实例,通过根搜索算法,将活跃对象进行标记,jvm再一次扫描所有对象,将未标记的对象进行清除,只有清除动作,不作任何的处理,这样导致的结果会存在很多的内存碎片。
  • 复制(copying)
    jvm扫描所有对象,通过根搜索算法标记被引用的对象,之后会申请新的内存空间,将标记的对象复制到新的内存空间里,存活的对象复制完,会清空原来的内存空间,将新的内存最为jvm的对象存储空间。这样虽然解决了内存内存碎片问题,但是如果对象很多,重新申请新的内存空间会很大,在内存不足的场景下,会对jvm运行造成很大的影响
  • 标记-整理(Mark-compact)
    标记整理实际上是在标记清除算法上的优化,执行完标记清除全过程之后,再一次对内存进行整理,将所有存活对象统一向一端移动,这样解决了内存碎片问题。
  • 分代回收
    目前jvm常用回收算法就是分代回收,年轻代以复制算法为主,老年代以标记整理算法为主。原因是年轻代对象比较多,每次垃圾回收都有很多的垃圾对象回收,而且要尽可能快的减少生命周期短的对象,存活的对象较少,这时候复制算法比较适合,只要将有标记的对象复制到另一个内存区域,其余全部清除,并且复制的数量较少,效率较高;而老年代是年轻代筛选出来的对象,被标记比较高,需要删除的对象比较少,显然采用标记整理效率较高。

JVM垃圾回收器
新生代收集器:Serial、ParNew 、Parallel Scavenge
老年代收集器:Serial Old、CMS、Parallel Old
堆内存垃圾收集器:G1

JVM垃圾回收器.png

Stop-The-World机制简称STW,是在执行垃圾收集算法时,应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

Serial 串行
Parallel 并行
CMS 并发

14、容易造成内存泄露的操作

创建大量无用对象、静态集合类的使用、各种连接对象(IO流对象、数据库连接对象、网络连接对象)未关闭、监听器的使用

15、java中存在的四种引用

强引用: 是指创建一个对象并把这个对象赋给一个引用变量,强引用有引用变量指向时永远不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象;
软引用:如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;
弱引用:弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象;
虚引用:虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。主要作用是跟踪对象被垃圾回收的状态。

16、类加载的过程

  1. 加载:根据查找路径找到相应的class文件,然后导入。类的加载方式分为隐式加载和显示加载两种。隐式加载指的是程序在使用new关键词创建对象时,会隐式的调用类的加载器把对应的类加载到jvm中。显示加载指的是通过直接调用class.forName()方法来把所需的类加载到jvm中。
  2. 检查:检查夹加载的class文件的正确性。
  3. 准备:给类中的静态变量分配内存空间。
  4. 解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址。
  5. 初始化:对静态变量和静态代码块执行初始化工作。

17、类加载器

类加载器:根据指定全限定名称将class文件加载到JVM内存,转为Class对象。
启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在\lib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中。
扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

18、双亲委派模型

如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。


双亲委派模型.png

Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类Object,它放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。判断两个类是否相同是通过classloader.class这种方式进行的,所以哪怕是同一个class文件如果被两个classloader加载,那么他们也是不同的类。

实现自己的加载器:只需要继承ClassLoader,并覆盖findClass方法。在调用loadClass方法时,会先根据委派模型在父加载器中加载,如果加载失败,则会调用自己的findClass方法来完成加载。

18、23种设计模式

设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

19、JVM性能监控

top指令:查看当前所有进程的使用情况,CPU占有率,内存使用情况,服务器负载状态等参数。
jps:与linux上的ps类似,用于查看有权访问的虚拟机的进程,可以查看本地运行着几个java程序,并显示他们的进程号。当未指定hostid时,默认查看本机jvm进程。
jinfo:可以输出并修改运行时的java 进程的一些参数。
jstat:可以用来监视jvm内存内的各种堆和非堆的大小及其内存使用量。
jstack:堆栈跟踪工具,一般用于查看某个进程包含线程的情况。
jmap:打印出某个java进程(使用pid)内存内的所有对象的情况。一般用于查看内存占用情况。
jconsole:一个java GUI监视工具,可以以图表化的形式显示各种数据。并可通过远程连接监视远程的服务器的jvm进程。
JVisualVM:是一款免费的性能分析工具,通过 jvmstat、JMX、SA(Serviceability Agent)以及 Attach API 等多种方式从程序运行时获得实时数据,从而进行动态的性能分析。同时,它能自动选择更快更轻量级的技术尽量减少性能分析对应用程序造成的影响,提高性能分析的精度。

你可能感兴趣的:(Java基础面试题总结)