Java常见面试题及解答

Java常见面试题及解答

  • 1 面向对象的三个特征
  • 2 this,super关键字
  • 3 基础数据类型
  • 4 public、protected、default、private
  • 5 接口
  • 6 抽象类
    • 6.1 抽象类和接口的区别
  • 7 重载(overload)、重写(override)
  • 8 final、finalize、finally
  • 9 final 用法
  • 10 深拷贝和浅拷贝的区别
  • 11 static用法
  • 12 int和Integer的区别
  • 13 String、StringBuffer、StringBuilder
    • 13.1 String和StringBuffer
    • 13.2 StringBuffer和StringBuilder
  • 14 垃圾回收机制
    • 14.1 垃圾回收机制的意义
    • 14.2 触发垃圾回收的条件
    • 14.3 主动请求JVM运行垃圾回收的方式
    • 14.4 垃圾回收的两个阶段:标记阶段、回收阶段
    • 14.5 标记阶段
      • 14.5.1 引用计数法(Reference Counting Collector)
      • 14.5.2 根搜索算法(可达性检测算法)
    • 14.6 回收阶段
      • 14.6.1 标记-清除(Mark-Sweep)算法
      • 14.6.2 复制(Copying)算法
      • 14.6.3 标记-压缩(Mark-Compact)算法
      • 14.6.4 标记-清除算法、复制算法、标记-压缩算法的比较
      • ※14.6.5 分代收集(Generational Collection)算法
        • 14.6.5.1 区域划分
        • 14.6.5.2 分代收集算法GC 类型
        • 14.6.5.3 分代收集算法的垃圾收集器(GC)
        • 14.6.5.4 对象的四种引用状态
  • 15 类加载机制
    • 15.1 类加载的生命周期
    • 15.2 类加载的时机
    • 15.3 类加载过程(具体的生命周期)
    • 15.4 类加载器的类型
      • 15.4.1 系统提供的3种类加载器
      • 15.4.2 双亲委派模型
      • 15.4.3 自定义类加载器
  • 16 异常处理
    • 16.1 异常的介绍
    • 16.2 异常处理的机制
    • 16.3 异常处理机制中的 finally
    • 16.4 抛出异常的方法:throw 和 throws
    • 16.5 自定义异常
  • 17 泛型
    • 17.1 泛型本质
    • 17.2 为什么使用泛型
    • 17.3 如何使用泛型
    • 17.4 泛型通配符
    • 17.5 泛型中KTVE的含义
    • 17.6 泛型的实现原理
  • 18 反射
    • 18.1 反射的用法
  • 19 网络IO
    • 19.1 网络IO的分类
    • 19.2 同步和异步、阻塞和非阻塞的概念
      • 19.2.1 同步和异步
      • 19.2.2 阻塞和非阻塞
    • 19.3 同步阻塞(BIO)
      • 19.3.1 面试题:介绍一下Java中的IO流
    • 19.4 同步非阻塞(NIO)
      • 19.4.1 NIO和BIO的区别
    • 19.5 异步非阻塞(AIO)
    • 19.6 BIO、NIO、AIO适用场景
  • 20 多线程
  • 21 集合框架
  • ~~22 网络~~
  • ~~23 高并发~~
  • ~~24 高负载~~
  • ~~25 高可用性~~
  • 26 JVM原理
  • 27 注解
  • 28 线程池
  • 29 锁
  • 30 设计模式
  • 31 栈和堆
  • 能否创建一个包含可变对象的不可变对象
  • 创建对象的几种方式
  • Object中有哪些公共方法
  • java当中的四种引用
  • 为什么要有不同的引用类型
  • ==和eqauls()的区别
  • equals()和hashCode()的联系
  • a.hashCode()有什么用?与a.equals(b)有什么关系
  • 有没有可能两个不相等的对象有相同的hashcode
  • a==b 与a.equals(b)有什么区别
  • a=a+b与a+=b有什么区别
  • short s1= 1; s1 = s1 + 1; 该段代码是否有错,有的话怎么改?
  • short s1= 1; s1 += 1; 该段代码是否有错,有的话怎么改?
  • & 和 &&的区别
  • 内部类的作用
  • 如何将byte转为String
  • 可以将int强转为byte类型么?会产生什么问题?
  • 进程、线程之间的区别
  • 循环依赖

1 面向对象的三个特征

参考1:java之封装,继承,多态
推荐看参考1。

  1. 封装:是把对象的属性和操作(或服务)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。通过提供的方法来给外部调用。比如:类的属性使用private修饰符,表示最小的访问权限。对成员变量的访问,统一提供setXXXgetXXX方法。

封装的特点

  • 将类的某些信息隐藏在类的内部,不允许外部程序进行直接的访问调用。
  • 通过该类提供的方法来实现对隐藏信息的操作和访问。
  • 隐藏对象的信息。
  • 留出访问的对外接口。
  1. 继承: 是子类继承父类的特征行为,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为。但是如果在父类中拥有私有属性(private修饰),则子类是不能被继承的。

继承的特点

  • 只支持单继承,即一个子类只允许有一个父类,但是可以实现多级继承,即子类拥有唯一的父类,而父类还可以再继承父类。
  • 子类可以拥有父类的属性和方法。
  • 子类可以拥有自己的属性和方法。
  • 子类可以重写覆盖父类的方法。
  1. 多态:是同一个行为具有多个不同表现形式或形态的能力。

多态的特点

  • 消除类型之间的耦合关系,实现低耦合。
  • 灵活性。
  • 可扩充性。
  • 可替换性。

多态的体现形式

  • 继承
  • 父类引用指向子类。
  • 重写

2 this,super关键字

参考1:java之封装,继承,多态

this关键字用法:
​1. 本类成员方法中,访问本类的成员变量。
​2. 本类成员方法中,访问本类的另一个成员方法。
3. 本类的构造方法中,访问本类的另一个构造方法。

super()关键字的用法
​1. 子类的成员方法中,访问父类的成员变量。
​2. 子类的成员方法中,访问父类的成员方法。
3. 子类的构造方法中,访问父类的构造方法。

注意:
this关键字同super一样,必须在构造方法的第一个语句,且是唯一的。
thissuper不能同时存在。

3 基础数据类型

参考1:java基本数据类型

java语言中有8种基本数据类型,分类四大类型:

  1. 逻辑类型:boolean
  2. 整数类型:byte、short、int、long
  3. 浮点类型 :float、double
  4. 字符类型:char

记忆规律:byte、short、char、int、long、float、double(占用内存分别是:1、2、2、4、8、4、8)

4 public、protected、default、private

参考1:Java中public、private、default和protected详解
Java常见面试题及解答_第1张图片

  1. publicpublic修饰的变量,当前类、当前包、子类和其他类均可访问。
  2. protectedprotected修饰的变量,可在当前类访问,也可在当前包和子类(继承父类的子类)中访问。
  3. default:默认情况下的变量(也就是没有修饰的变量),既能在当前类内访问,又能在当前包(package)访问。
  4. privateprivate修饰词修饰的变量,该变量仅能在当前类内访问,其他地方(如:当前包、子类、其他类)均无法访问。

5 接口

参考1:Java 接口
Java的接口是一个抽象类型,是抽象方法的集合,用interface声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。

接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性方法。接口则包含类要实现的方法

接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。

接口的特点:

  • 接口不能用于实例化对象。
  • 接口没有构造方法。
  • 接口中所有的方法必须是抽象方法,Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法。
  • 接口不能包含成员变量,除了 staticfinal 变量。
  • 接口不是被类继承了,而是要被类实现。
  • 接口支持多继承。

6 抽象类

参考1:Java 抽象类

抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量成员方法构造方法的访问方式和普通类一样。

抽象类的特点:

  • 抽象类不能被实例化,如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。
  • 由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。
  • 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
  • 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。
  • 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。
  • 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。

6.1 抽象类和接口的区别

参考1:Java 接口

  1. 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行,接口中的都是抽象方法。
  2. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
  3. 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
  4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

7 重载(overload)、重写(override)

参考1:java之封装,继承,多态

重载(overload)
是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的地方就是构造器的重载。

重载规则:

  1. 被重载的方法必须改变参数列表(参数个数或者类型不一样)。
  2. 被重载的方法可以改变返回类型。
  3. 被重载的方法可以改变访问修饰符。

重写(override)
是子类对父类允许访问的方法的实现过程进行重新实现, 返回值和形参都不能改变。

重写规则:

  1. 参数列表必须与被重写方法相同。
  2. 访问权限不能比父类中被重写的方法的访问权限更低(public>protected>(default)>private)。
  3. 父类成员的方法只能被它的子类重写。
  4. final修饰的方法不能被重写。
  5. 构造方法不能重写。

8 final、finalize、finally

final 是一个修饰符,可以修饰变量、方法和类。如果 final修饰变量,意味着该变量的值在初始化后不能被改变。

finalize 是方法,它是在对象被回收之前调用的方法,给对象自己最后一个复活的机会,但是什么时候调用 finalize 没有保证。

finally 是一个关键字,与 trycatch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。

9 final 用法

  1. final修饰的类不可以被继承。
  2. final修饰的方法不可以被重写。
  3. final修饰的变量不可以被改变。如果修饰引用,那么表示引用不可变,引用指向的内容可变。
  4. final修饰的方法,JVM会尝试将其内联,以提高运行效率。
  5. final修饰的常量,在编译阶段会存入常量池中。

10 深拷贝和浅拷贝的区别

浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。

深拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。

11 static用法

static关键字这两个基本的用法:静态变量静态方法。也就是被static所修饰的变量/方法都属于类的静态资源,类实例所共享。

除了静态变量和静态方法之外,static也用于静态块,多用于初始化操作。

此外static也多用于修饰内部类,此时称之为静态内部类。

注意:

  1. static修饰的静态方法不能直接调用非静态的方法,需要先创建类的实例再调用。
  2. 非静态的方法可以直接调用static修饰的静态方法。

12 int和Integer的区别

Integerint的包装类型,在拆箱和装箱中,二者自动转换。

int是基本类型,直接存数值,而Integer是对象,用一个引用指向这个对象。

13 String、StringBuffer、StringBuilder

String:是字符串常量,不可变,原因是有final修饰;
StringBuffer:字符串变量(线程安全),StringBuffer类里很多方法都有 synchronized(同步锁)实现线程安全;
StringBuilder:字符串变量(线程不安全)。

13.1 String和StringBuffer

StringStringBuffer主要区别是性能:String是不可变对象,每次对String类型进行操作都等同于产生了一个新的String对象,然后指向新的String对象。所以在使用时尽量不要对String进行大量的拼接操作,否则会产生很多临时对象,导致GC开始工作,影响系统性能。

StringBuffer是对对象本身操作,而不是产生新的对象,因此在有大量拼接的情况下,建议使用StringBuffer

13.2 StringBuffer和StringBuilder

StringBufferStringBuilder 都是 extends AbstractStringBuilder implements java.io.Serializable, CharSequence

StringBuffer是线程安全的可变字符串,其内部实现是可变数组。

StringBuilderjdk 1.5新增的,其功能和StringBuffer类似,但是非线程安全。因此,在没有多线程问题的前提下,使用StringBuilder会取得更好的性能。

14 垃圾回收机制

参考1:Java 垃圾回收机制整理
参考2:Java超详细分析垃圾回收机制
参考3:深入理解 Java 垃圾回收机制

14.1 垃圾回收机制的意义

垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存;

内存泄露:指该内存空间使用完毕后未回收,在不涉及复杂数据结构的一般情况下,java的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有是也将其称为 “对象游离”;

14.2 触发垃圾回收的条件

参考1:JAVA垃圾回收机制

  1. 当没有线程在运行时,垃圾回收会被调用。因为垃圾回收在优先级最低的线程中进行,当应用忙时,垃圾回收不被调用(不是由程序员自己调用的),但除堆内存不足外。

  2. 堆内存不足时会触发垃圾回收机制。

14.3 主动请求JVM运行垃圾回收的方式

参考1:面试必问:Java 垃圾回收机制

  1. 使用System.gc() 方法:系统类包含静态方法gc() 用于请求 JVM 运行垃圾收集器。
  2. 使用Runtime.getRuntime().gc() 方法:运行时类允许应用程序与运行应用程序的 JVM 交互。因此,通过使用其 gc() 方法,我们可以请求 JVM 运行垃圾收集器。

注意:以上两种方法中的任何一种都不能保证一定会运行垃圾收集器,也就是系统是否进行垃圾回收依旧不确定,因为这不是程序员控制的,而是系统决定的。

14.4 垃圾回收的两个阶段:标记阶段、回收阶段

14.5 标记阶段

标记阶段有:1、引用计数法(Reference Counting Collector);2、根搜索算法(可达性检测算法)。

14.5.1 引用计数法(Reference Counting Collector)

引用计数是垃圾收集器中的早期策略。
这个方法中,堆中的每个对象都会添加一个引用计数器。每当一个地方引用这个对象时,计数器值 +1;当引用失效时,计数器值 -1。任何时刻计数值为 0 的对象就是不可能再被使用的。

优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

缺点:无法解决对象之间相互引用的情况。比如对象有一个对子对象的引用,子对象反过来引用父对象,它们的引用计数永远不可能为 0。

14.5.2 根搜索算法(可达性检测算法)

相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。所以现在一般使用根搜索算法


根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点 GC ROOT 开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
Java常见面试题及解答_第2张图片
如上图中的 ObjFObjDObjE通过 GC Root 是无法找到的,所以它们是无用节点。

java中可作为GC Root的对象:

  1. 虚拟机栈中引用的对象(本地变量表)。
  2. 方法区中静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中引用的对象(Native对象)。

14.6 回收阶段

14.6.1 标记-清除(Mark-Sweep)算法

标记-清除算法分为两个阶段:

  • 标记阶段:标记出需要被回收的对象。
  • 清除阶段:回收被标记的可回收对象的内部空间。
    Java常见面试题及解答_第3张图片
    标记-清除算法实现较容易,不需要移动对象,但是存在较严重的问题:
  1. 算法过程需要暂停整个应用(即GC时存在STW),效率不高。
  2. 标记清除后会产生大量不连续的内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作,此时就需要一个空列表来记录这些地址。

14.6.2 复制(Copying)算法

为了解决标记 - 清除算法在效率方面的缺陷,复制算法采用将内存按容量划分的方式,划分成大小相等的两块,每次只使用其中的一块。算法思想如下 (1或2是一个意思):

  1. 将正在使用的存活对象全部复制到另一块未被使用空间,摆放整齐,然后清空此空间所有对象。

  2. 当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉。

Java常见面试题及解答_第4张图片
优点:实现简单,不易产生内存碎片,每次只需要对半个区进行内存回收。
缺点1:内存空间缩减为原来的一半;算法的效率和存活对象的数目有关,存活对象越多,效率越低。

缺点2 : 需要两倍的内存空间 , 开销较大 , 另外GC如果采用 G1 垃圾回收器的话 , 它将空间拆成了很多份, 如果采用复制算法, 还需要维护各区之间的关系。

14.6.3 标记-压缩(Mark-Compact)算法

为了更充分利用内存空间,提出了标记-压缩算法。此算法结合了“标记-清除”和“复制”两个算法的优点。也是分为两个阶段:

  1. 该算法标记阶段和“标志-清除”算法一样,从根节点开始标记所有被引用对象。
  2. 在完成标记之后,它不是直接清理可回收对象,而是将存活对象压缩到内存的一端,按顺序排放,然后清理掉端边界以外的内存空间。
    Java常见面试题及解答_第5张图片
    标记-压缩算法的最终效果等同于标记-清除算法执行完成后,再进行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法 , 标记- 压缩是移动式的 , 将对象在内存中依次排列比维护一个空列表少了不少开销(如果对象排列整齐,当我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可)。

优点 : 相对于标记 -清除算法避免了内存碎片化,相对于复制算法,避免开辟额外的空间。

缺点 : 从效率上来说是不如复制算法的,移动时,如果存在对象相互引用, 则需要调整引用的位置, 另外移动过程中也会有STW。

14.6.4 标记-清除算法、复制算法、标记-压缩算法的比较

复制算法是效率最高的 , 但是花费空间最大。

标记 - 压缩算法虽然较为兼顾 ,但效率也变低,比标记- 清除多了个整理内存的过程,比复制算法多了标记的过程。
Java常见面试题及解答_第6张图片

※14.6.5 分代收集(Generational Collection)算法

分代收集算法是目前大部分 JVM 的垃圾收集器采用的算法。

分代的垃圾回收策略,是基于:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
Java常见面试题及解答_第7张图片

14.6.5.1 区域划分

年轻代(Young Generation)

  1. 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
  2. 新生代内存按照8:1:1的比例分为一个 eden 区和两个 survivor(survivor0survivor1) 区。一个 Eden 区,两个 Survivor 区(一般而言)。大部分对象在 Eden 区中生成。回收时先将 eden 区存活对象复制到一个 survivor0 区,然后清空 eden 区,当这个 survivor0 区也存放满了时,则将 eden 区和 survivor0 区存活对象复制到另一个 survivor1 区,然后清空 eden 和这个 survivor0 区,此时 survivor0 区是空的,然后将 survivor0 区和 survivor1 区交换,即保持 survivor1 区为空, 如此往复。
  3. survivor1区不足以存放 edensurvivor0 的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次 Full GC,也就是新生代、老年代都进行回收。
  4. 新生代发生的 GC 也叫做 Minor GCMinor GC 发生频率比较高(不一定等 Eden 区满了才触发)。

年老代(Old Generation)

  1. 在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
  2. 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发 Major GCFull GCFull GC 发生频率比较低,老年代对象存活时间比较长,存活率标记高。

持久代(Permanent Generation)
用于存放静态文件,如 Java 类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class ,例如 Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

14.6.5.2 分代收集算法GC 类型

  1. Minor GC(新生代 GC):新生代 GC,指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生熄灭的特点,所以 Minor GC 十分频繁,回收速度也较快。
  2. Major GC(老年代 GC):老年代 GC,指发生在老年代的垃圾收集动作,当出现 Major GC 时,一般也会伴有至少一次的 Minor GC(并非绝对,例如 Parallel Scavenge 收集器会单独直接触发 Major GC 的机制)。 Major GC 的速度一般会比 Minor GC 慢十倍以上。
  3. Full GC:清理整个堆空间—包括年轻代和老年代。Major GC == Full GC。 参考:聊聊JVM(四)深入理解Major GC, Full GC, CMS

产生 Full GC 可能的原因:

  1. 年老代被写满。
  2. 持久代被写满。
  3. System.gc() 被显式调用。
  4. 上一次 GC 之后 Heap(堆) 的各域分配策略动态变化。

14.6.5.3 分代收集算法的垃圾收集器(GC)

不同虚拟机所提供的垃圾收集器可能会有很大差别,下面的例子是 HotSpot

  • 新生代收集器使用的收集器:SerialPraNewParallel Scavenge
  • 老年代收集器使用的收集器:Serial OldParallel OldCMS
    Java常见面试题及解答_第8张图片
  1. Serial 收集器(复制算法):新生代单线程收集器,标记和清理都是单线程,优点是简单高效。
  2. Serial Old收集器(标记-整理算法):老年代单线程收集器,Serial 收集器的老年代版本。
  3. ParNew 收集器(停止-复制算法) :新生代收集器,可以认为是 Serial 收集器的多线程版本,在多核 CPU 环境下有着比 Serial 更好的表现。
  4. Parallel Scavenge 收集器(停止-复制算法):并行收集器,追求高吞吐量,高效利用 CPU。吞吐量一般为 99%, 吞吐量 = 用户线程时间 / (用户线程时间 + GC线程时间)。适合后台应用等对交互相应要求不高的场景。
  5. Parallel Old 收集器(停止-复制算法):Parallel Scavenge 收集器的老年代版本,并行收集器,吞吐量优先。
  6. CMS(Concurrent Mark Sweep) 收集器(标记-清理算法):高并发、低停顿,追求最短 GC 回收停顿时间,cpu 占用比较高,响应时间快,停顿时间短,多核 cpu 追求高响应时间的选择。

总结:
根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;对象存活率高、没有额外空间进行分配担保的(老年代),采用标记-清理算法或者标记-整理算法。

14.6.5.4 对象的四种引用状态

在实际开发中,我们对 new 出来的对象也会根据重要程度,有个等级划分。有些必须用到的对象,我们希望它在其被引用的周期内能一直存在;有些对象可能没那么重要,当内存空间还足够时,可以保留在内存中,如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。

由此,Java 对引用划分为四种:强引用、软引用、弱引用、虚引用,四种引用强度依次减弱。

  1. 强引用:代码中普遍存在的类似"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  2. 软引用:描述有些还有用但并非必需的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。Java 中的类 SoftReference 表示软引用。
  3. 弱引用:描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java 中的类 WeakReference 表示弱引用。
  4. 虚引用:这个引用存在的唯一目的就是在这个对象被收集器回收时收到一个系统通知,被虚引用关联的对象,和其生存时间完全没关系。Java 中的类 PhantomReference 表示虚引用。

15 类加载机制

参考1:java类加载机制,你会了吗?

类加载机制定义:
java虚拟机将编译后的class文件加载到内存中,进行校验、转换、解析和初始化,到最终的使用。这就是java类加载机制;

15.1 类加载的生命周期

加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)等阶段,其中验证、准备、解析3阶段也可以称为连接(Lingking),如下图(类的生命周期):

Java常见面试题及解答_第9张图片

15.2 类加载的时机

在类加载的生命周期中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,其加载过程一定是按照这个顺序执行的。而解析阶段有点特殊,在某些特定的情况下,它是在初始化之后开始的。

Java虚拟机规范中并没有进行强制约束类加载的第一个阶段的时机,而是交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况(类没有初始化)必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  1. 遇到newgetstaticputstaticinvokestaic这四条字节码指令时。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候。
  3. 当初始化一个类的时候,如果发现父类还没有初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。
  5. 当使用JDK1.7的动态语言支持的时候,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,并且这个句柄所对应的类没有进行过初始化,则需要先触发器初始化。

15.3 类加载过程(具体的生命周期)

1 加载:在加载阶段,虚拟机主要执行以下三个操作。

  1. 通过类的全限定名来获取定义这个类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化成方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。

这个阶段相比其他阶段来说,是开发人员可控性最强的阶段。因为这个阶段既能使用系统提供的加载器(这个加载器后面会进行介绍)加载,又能通过开发人员自定义的加载器进行加载。

在加载这个阶段还有一个需要注意的地方,在执行第一个操作时,需要知道可以从哪里获取class文件,例如:

  1. 从压缩文件中读取(JAR,WAR等);
  2. 从本地磁盘中获取;
  3. 从网络上获取(Applet);
  4. 运行过程中动态生成(动态代理);
  5. 其他文件生成(jsp生成对应的class文件);
  6. 从数据库中读取;

2 验证:验证阶段主要有4个阶段的验证:文件格式验证、元数据验证、字节码验证和符号验证。

  1. 文件格式验证:这一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,主要包括魔数、版本号、常量池等验证。
  2. 元数据验证:这个阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求。主要包括是否有父类,类中的字段、方法是否与父类冲突,如果不是抽象类,是否实现了其父类或接口中要求实现的所有方法等;
  3. 字节码验证:这个阶段是在元数据验证之后,对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机的安全事件,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。也是验证过程最复杂的一个阶段。
  4. 符号引用验证:这个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候。是对类自身以外的信息进行匹配性校验。主要目的是确保解析动作能正常执行。

3 准备:准备阶段是为类变量分配内存并设置类变量初始值的阶段,分配这些内存是在方法区里面进行的,这个阶段有两点需要重点注意的:

  1. 只有类变量(被static修饰的变量)会分配内存,不包括实例变量,实例变量是在对象实例化的时候在堆中分配内存的。
  2. 设置类变量的初始值是数量类型对应的默认值,而不是代码中设置的默认值。例如public static int number=111,这类变量number在准备阶段之后的初始值是0而不是111。而给number赋值为111是在初始化阶段。

4 解析:解析阶段是虚拟机将常量池里内的符号引用转换为直接引用。注意2个概念:

  1. 符号引用:以一组符号来描述所有引用的目标,符号可以是任何形式的字面量,只要使用时能正确定义到目标即可。
  2. 直接引用:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

解析操作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等7类符号引用进行的。

5 初始化:这个阶段是类加载过程的最后一步,是代码真正开始执行的时候,在这个阶段,开发人员可以根据自己的需求去给类变量初始化赋值。简单来说就是执行类构造器()方法的过程。

15.4 类加载器的类型

接下来看看是什么是类加载器:

虚拟机设计团队将加载动作放到了Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称之为“类加载器”。

15.4.1 系统提供的3种类加载器

  1. 启动类加载器(Bootstrap ClassLoader):负责将存放在\lib目录中,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。(注:仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)

  2. 扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录中的,或被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

  3. 应用程序类加载器(Application ClassLoader):负责加载用户路径**(ClassPath)**上所指定的类库,开发者可以直接使用这个类加载器,一般情况下该类加载是程序中默认的类加载器。

这三种加载器的加载顺序如下:(系统提供的类加载器的执行顺序)
系统提供的类加载器的执行顺序

15.4.2 双亲委派模型

Java常见面试题及解答_第10张图片
如上图展示的类加载器之间的这种层次关系就是双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应有自己的父类加载器。

双亲委派介绍
如果一个类加载器收到类加载的请求,他首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,每一层次的类加载器都是这样,因此所有的加载请求最终都应该传送到底层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试去加载。

双亲委派原则的好处

  1. 避免重复加载同一个类;
  2. 防止用户任意修改java中的类;

双亲委派原则的加载过程
Java常见面试题及解答_第11张图片

15.4.3 自定义类加载器

上面讲述的是系统提供的类加载器以及它们之间的关系,还有很多情况需要我们自定义类加载器。那该如何定义呢?有以下两种方式:

  1. 如果我们自定义的加载器不想破坏双亲委派,继承 java.lang.ClassLoader 类并重写 findClass 方法。
  2. 如果使用我们自定义的加载器破坏双亲委派,继承 java.lang.ClassLoader 类并重写loadClass(java.lang.String) 方法。

16 异常处理

参考1:Java的异常处理机制
参考2:Java:详解Java中的异常(Error与Exception)

16.1 异常的介绍

  1. java中所有错误的超类为:Throwable。其下有两个子类:ErrorException
  2. Error的子类描述的都是系统错误,比如虚拟机内存溢出。
  3. Exception的子类描述的都是程序,比如空指针,下标越界等。
  4. 通常我们程序中处理的异常都是Exception

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

Exception(异常):是程序本身可以处理的异常。Exception(异常)分两大类:运行时异常非运行时异常(编译异常)。程序中应当尽可能去处理这些异常。

运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)ArrayIndexOutOfBoundException(数组下标越界异常)ArithmeticException(算术异常)NullPointerException(空指针异常)NumberFormatException(数字格式异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该尽可能避免这类异常的发生。运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。

非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。这类是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOExceptionSQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

Java的异常(Throwable)分为可查的异常(checked exceptions)不可查的异常(unchecked exceptions)

可查异常(编译器要求必须处置的异常):正确的程序在运行中,很容易出现的、情理可容的异常状况。除了Exception中的RuntimeExceptionRuntimeException的子类以外,其他的Exception类及其子类(例如:IOExceptionClassNotFoundException)都属于可查异常。这种异常的特点是Java编译器会检查它,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。

不可查异常(编译器不要求强制处置的异常):包括运行时异常(RuntimeException与其子类)和错误(Error)。RuntimeException表示编译器不会检查程序是否对RuntimeException作了处理,在程序中不必捕获RuntimException类型的异常,也不必在方法体声明抛出RuntimeException类。RuntimeException发生的时候,表示程序中出现了编程错误,所以应该找出错误修改程序,而不是去捕获RuntimeException

RuntimeException 类属于非检测异常,因为普通JVM操作引起的运行时异常随时可能发生,此类异常一般是由特定操作引发。但这些操作在java应用程序中会频繁出现。因此它们不受编译器检查与处理或声明规则的限制。

16.2 异常处理的机制

Java应用程序中,异常处理机制为:异常抛出,异常捕获。
异常抛出:任何Java代码都可以抛出异常,如:自己编写的代码、来自Java开发环境包中代码,或者Java运行时系统。无论是谁,都可以通过Javathrow语句抛出异常。从方法中抛出的任何异常都必须使用throws子句。

异常捕获:捕捉异常通过try-catch语句或者try-catch-finally语句实现。

16.3 异常处理机制中的 finally

  1. finally块定义在异常处理机制中的最后一块。它可以直接跟在try之后,或者最后一个catch之后。
  2. finally可以保证只要程序执行到了try语句块中,无论try语句块中的代码是否出现异常,最终finally都必定执行。
  3. finally通常用来做释放资源这类操作。

16.4 抛出异常的方法:throw 和 throws

参考1:Java中throw和throws有什么区别?

  1. throw作用在方法体内使用,throws 在方法声明上使用;
  2. throw后面接的是异常对象,只能接一个。throws 后面接的是异常类型,可以接多个,多个异常类型用逗号隔开;
  3. throw 是在方法中出现不正确情况时,手动来抛出异常,结束方法的,执行了 throw 语句一定会出现异常。而 throws 是用来声明当前方法有可能会出现某种异常的,如果出现了相应的异常,将由调用者来处理,声明了异常不一定会出现异常。

16.5 自定义异常

当Java内置的异常都不能明确的说明异常情况的时候,需要创建自己的异常。

定义自定义异常需要注意以下问题:

  1. 异常的类名要做到见名知义。
  2. 需要是Exception的子类。
  3. 提供超类异常提供的所有种类构造器。

17 泛型

参考1:Java泛型详解,史上最全图文详解

17.1 泛型本质

Java 泛型(generics)是 JDK5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。

泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类泛型接口泛型方法

17.2 为什么使用泛型

泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

泛型的好处

  1. 保证了类型的安全性:在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,如果不小心插入了错误的类型对象,在运行时的转换处理就会出错。
  2. 消除强制转换:泛型的一个附带好处是,消除源代码中的许多强制类型转换,这使得代码更加可读,并且减少了出错机会。
  3. 避免了不必要的装箱、拆箱操作,提高程序的性能:在非泛型编程中,将筒单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行BoxingUnboxing操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。
  4. 提高了代码的重用性

17.3 如何使用泛型

泛型有三种使用方式,分别为:泛型类、泛型接口和泛型方法。

  1. 泛型类:把泛型定义在类上。
  2. 泛型接口:把泛型定义在接口上。
  3. 泛型方法:把泛型定义在方法上。

17.4 泛型通配符

Java泛型的通配符是用于解决泛型之间引用传递问题的特殊语法, 主要有以下三类:

  1. 无边界的通配符(Unbounded Wildcards),就是,比如List
    无边界的通配符的主要作用就是让泛型能够接受未知类型的数据。

  2. 固定上边界的通配符(Upper Bounded Wildcards),采用的形式
    (1)使用固定上边界的通配符的泛型,就能够接受指定类及其子类类型的数据。
    (2)要声明使用该类通配符,采用的形式,这里的E就是该泛型的上边界。
    注意:这里虽然用的是extends关键字,却不仅限于继承了父类E的子类,也可以代指显现了接口E的类。

  3. 固定下边界的通配符(Lower Bounded Wildcards),采用的形式
    (1)使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据.。
    (2)要声明使用该类通配符,采用的形式,这里的E就是该泛型的下边界。

注意: 你可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界。

17.5 泛型中KTVE的含义

上面这些泛型类定义中的泛型参数E、K和V都是什么意思呢?其实这些参数名称是可以任意指定,就想方法的参数名一样可以任意指定,但是我们通常会起一个有意义的名称,让别人一看就知道是什么意思。泛型参数也一样,E一般是指元素,用来集合类中。

常见泛型参数名称有如下:
E: Element (在集合中使用,因为集合中存放的是元素)
T:Type(Java 类)
K: Key(键)
V: Value(值)
N: Number(数值类型)
?: 表示不确定的java类型

17.6 泛型的实现原理

泛型本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间「擦除」泛型语法并相应的做出一些类型转换动作。

18 反射

参考1:Java反射(通俗易懂)

Reflection(反射) 允许运行中的 Java 程序对自身进行检查。被private封装的资源只能类内部访问,外部是不行的,但反射能直接操作类私有属性。反射可以在运行时获取一个类的所有信息,(包括成员变量,成员方法,构造器等),并且可以操纵类的字段、方法、构造器等部分。

要想查看一个类的具体信息,必须先要获取到该类的字节码文件对象。而查看一个类的具体信息使用的就是Class类中的方法。所以先要获取到每一个字节码文件对应的Class类型的对象。

反射就是把java类中的各种成分映射成一个个的Java对象。例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把一个个组成部分映射成一个个对象。(其实:一个类中这些成员方法、构造方法、在加入类中都有一个类来描述)

加载的时候:Class对象的由来是将 .class 文件读入内存,并为之创建一个Class对象。

Class类
Class 类的实例表示正在运行的 Java 应用程序中的类和接口。也就是jvm中有N多的实例每个类都有该Class对象。(包括基本数据类型)

Class 没有公共构造方法。Class 对象是在加载类时由 Java 虚拟机以及通过调用类加载器中的defineClass 方法自动构造的。也就是这不需要我们自己去处理创建,JVM已经帮我们创建好了。

Bean
Spring框架可以帮我们创建和管理对象。需要对象时,我们无需自己手动new对象,直接从Spring提供的容器中的Beans获取即可。Beans底层其实就是一个Map,最终通过getBean(“user”)来获取。而这其中最核心的实现就是利用反射技术。

  1. Java面向对象,对象有方法和属性,那么就需要对象实例来调用方法和属性(即实例化);
  2. 凡是有方法或属性的类都需要实例化,这样才能具象化去使用这些方法和属性;
  3. 规律:凡是子类及带有方法或属性的类都要加上注册BeanSpring IoC的注解;(@Component@Repository@ Controller@Service@Configration)。
  4. 把Bean理解为类的代理或代言人(实际上确实是通过反射、代理来实现的),这样它就能代表类拥有该拥有的东西了。
  5. 在Spring中,你标识一个@符号,那么Spring就会来看看,并且从这里拿到一个Bean(注册)或者给出一个Bean(使用)。

18.1 反射的用法

看 Java反射(通俗易懂) 3、反射的应用

19 网络IO

参考1:java的几种IO
参考2:网络IO模型(BIO,NIO,AIO)

参考1主要是概念,参考2有更深入的分析及代码示例。

19.1 网络IO的分类

Java IO基于不同的IO模型可以分为三类:

  1. 同步阻塞的BIO(blocking IO),在jdk1.0的时候引入的。
  2. 同步非阻塞的NIO(non-blocking IO),因为是在jdk1.4 引入的,所以又叫(New IO)
  3. 异步非阻塞的AIO(Asynchronous IO),是在jdk1.7 引入的,是对NIO进一步的改进,也被称为NIO2

IO又主要可以分为文件IO网络IO

一般常说的java中的IO流指的就是java中BIO的具体实现

19.2 同步和异步、阻塞和非阻塞的概念

19.2.1 同步和异步

同步:
就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。(当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步)

**异步:**是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。(其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系。)

异步和同步是对于请求结果的获取是客户端主动等待获取,还是由服务端来通知消息结果。

19.2.2 阻塞和非阻塞

阻塞:
阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来线程挂起等待当前一个慢的操作完成 CPU 才接着完成其它的事。阻塞期间无法从事其他任务,只有当慢操作完成条件就绪才能继续。

非阻塞:
非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。

阻塞和非阻塞通常是指客户端在发出请求后,在服务端处理这个请求的过程中,客户端本身是否直接挂起等待结果,还是继续做其他的任务。

19.3 同步阻塞(BIO)

首先,传统的java.io包是 blocking io(BIO),在jdk1.0的时候引入的,它提供了我们最熟知的一些IO功能,比如File抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是在读入输入流或者写入输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。

优点:代码比较简单、直观;
缺点:是IO效率和扩展性存在局限性,容易成为应用性能的瓶颈。

同步阻塞(BIO)的特点:

  1. BIO不仅仅是对文件的操作,网络编程中,比如Socket通信,都是典型的BIO操作目标。
  2. 输入流、输出流(InputStream/OutputStream)是用于读取或写入字节的,例如操作图片文件。
  3. Reader/Writer则是用于操作字符,增加了字符编解码等功能,适用于类似从文件中读取或者写入文本信息。本质上计算机操作的都是字节,不管是网络通信还是文件读取,Reader/Writer相当于构建了应用逻辑和原始数据之间的桥梁。
  4. 处理纯文本数据时使用字符流(xxxReader/xxxWriter),处理非纯文本时使用字节流(xxxStream)。最后其实不管什么类型文件都可以用字节流处理,包括纯文本,但会增加一些额外的工作量。所以还是按原则选择最合适的流来处理。
  5. BuferedOutputStream等带缓冲区的实现,可以避免频繁的磁盘读写,进而提高IO处理效率。这种设计利用了缓冲区,将批量数据进行一次操作,但在使用中千万别忘了结束时调用fush将未满的缓冲区数据进行写入。

19.3.1 面试题:介绍一下Java中的IO流

流是Java对不同输入源输出源的抽象,代表了从起源到接收的有序数据,有了它程序就可以采用统一的方式来访问不同的输入源和输出源了。

  1. 按照数据的流向,可以将流分为输入流和输出流。其中,输入流只能读取数据、不能写入数据,而输出流只能写入数据、不能读取数据。
  2. 按照数据的类型,可以将流分为字节流和字符流。其中,字节流操作的数据单元是byte(8位的字节),而字符流操作的数据单元是char(16位的字符)。
  3. 按照使用的场景,可以将流分为节点流和处理流。其中,节点流可以直接从/向一个特定的IO设备读/写数据,也称为低级流。而处理流则是对节点流的连接或封装,用于简化数据读/写功能或提高效率,也成为高级流。

19.4 同步非阻塞(NIO)

NIOJava 1.4 引入的 java.nio 包,提供了 Channel(通道)、Buffer(缓冲区)、Selector(选择器) 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。

在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO

NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)。

传统IO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。

BIO的各种流是阻塞的。所以当一个线程调用read()write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。

NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

NIO的线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)

Channel(通道)

  1. Channel是一个通道,可以通过它读取和写入数据。与流不同的是,流是单向的,而Channel是双向的。数据可以通过Channel读到Buffer里,也可以通过Channel写入到Buffer里。需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。
  2. 为了支持不同的设备,Channel接口有好几种子类,如FileChannel用于访问磁盘文件、SocketChannelServerSocketChannel用于TCP协议的网络通信、DatagramChannel用于UDP协议的网络通信。

Buffer(缓冲区)

  1. NIO是面向缓冲区的,在NIO中所有的数据都是通过缓冲区处理的。Buffer就是缓冲区对象,无论读取还是写入,数据都是先进入Buffer的。
  2. Buffer的本质是一个数组,通常它是一个字节数组,也可以是其他类型的数组。
  3. Buffer是一个接口,它的实现类有:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer,分别对应基本数据类型: byte、char、double、float、int、long、short
  4. Buffer对象包含三个重要的属性,分别是capacitypositionlimit
    capacity:代表Buffer的容量,就是说Buffer中最多只能写入capacity个数据。
    position:代表访问的位置,它的初始值为0,每读取/写入一个数据,它就会向后移动一个位置。
    limit:代表访问限制,就是本次操作最多能读取/写入多少个数据。这三个属性的关系是,position<=limit<=capacityBuffer通过不断调整positionlimit的值,使得自身可以不断复用。

Selector(选择器):
Selector是多路复用器,可以通过它监听网络IO的状态。它可以不断轮询注册的Channel,如果某Channel上有连接、读取、写入事件发生,则这个Channel就处于就绪状态,就会被Selector轮询出来。所有被轮询出来的Channel集合,我们可以通过SelectionKey获取到,然后进行后续的IO操作。

19.4.1 NIO和BIO的区别

  1. NIO是以块的方式处理数据,但是IO是以最基础的字节流的形式去写入和读出的。所以在效率上的话,肯定是NIO效率比IO效率会高出很多。
  2. NIO不在是和IO一样用OutputStreamInputStream输入流的形式来进行处理数据的,但是又是基于这种流的形式,而是采用了通道和缓冲区的形式来进行处理数据的。
  3. NIO的通道是可以双向的,但是IO中的流只能是单向的。
  4. NIO的缓冲区(其实也就是一个字节数组)还可以进行分片,可以建立只读缓冲区、直接缓冲区和间接缓冲区,只读缓冲区很明显就是字面意思,直接缓冲区是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区。
  5. NIOBIO核心区别就是NIO采用的是多路复用的IO模型,普通的IO用的是阻塞的IO模型,两个之间的效率肯定是多路复用效率更高。

19.5 异步非阻塞(AIO)

AIOJava 1.7 之后引入的包,是对NIO进一步的改进,也被称为NIO2,提供了异步非堵塞的 IO 操作方式,所以叫 AIO(Asynchronous IO)。AIO其实是一种在读写操作结束之前允许进行其他操作的I/O处理。

jdk7主要增加了三个新的异步通道:

  1. AsynchronousFileChannel: 用于文件异步读写;
  2. AsynchronousSocketChannel: 客户端异步socket
  3. AsynchronousServerSocketChannel: 服务器异步socket

因为AIO的实施需充分调用操作系统参与,IO需要操作系统支持、并发也同样需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。

19.6 BIO、NIO、AIO适用场景

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。

NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。

AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用操作参与并发操作,编程比较复杂,JDK7开始支持。

20 多线程

参考1:Java多线程(超详细!)
参考2:Java多线程【三种实现方法】
参考3:Java多线程详解——一篇文章搞懂Java多线程
参考4:Java多线程超级详解(看这篇就足够了)
参考5:Java多线程实战精讲-带你一次搞明白Java多线程高并发
参考6:java多线程(详)

21 集合框架

22 网络

23 高并发

24 高负载

25 高可用性

26 JVM原理

27 注解

28 线程池

29 锁

30 设计模式

31 栈和堆

参考1:Java中的栈和堆

Java程序在运行时都会开辟内存空间,而栈和堆就是JVM虚拟机在运行时开辟的内存空间。

:函数中定义的基本数据类型局部变量都是在栈内存中被分配,而用完之后这些变量将会在栈内存中被释放,局部变量存在于方法中,一旦方法被执行,局部变量也将会被执行,而方法执行完后,这个变量将会在栈内存中被释放掉。

:实体对象(被new出来的)在堆内存中被分配,而这些实体封装的数据属性如果在用完后被释放实体对象也不会被释放,但是Java中垃圾回收机制,在对象不被使用后,会被自动当成垃圾不定时的回收。

示例:

int[]  a = new int[2];

这里首先在栈中给a变量分配地址,在堆中给数组分配2个大小的内存空间,并给数组中的每个默认值一个地址(默认为0),这个也就是两个0,之后栈中的a会根据这个地址被赋值,当用完后,a变量会被释放掉,而new对象在堆中存在。


参考1:史上最全Java面试题(带全部答案)
参考2:100+道Java经典面试题(面中率高)
参考3:Java面试题大全带答案
参考4:最全java面试题及答案(208道)
参考5:Java 教程

能否创建一个包含可变对象的不可变对象

可以创建一个包含可变对象的不可变对象的,你只需要谨慎一点,不要共享可变对象的引用就可以了,如果需要变化时,就返回原对象的一个拷贝。最常见的例子就是对象中包含一个日期对象的引用。

创建对象的几种方式

  1. 采用new
  2. 通过反射
  3. 采用clone
  4. 通过序列化机制

Object中有哪些公共方法

  1. equals()
  2. clone()
  3. getClass()
  4. notify(),notifyAll(),wait()
  5. toString

java当中的四种引用

强引用,软引用,弱引用,虚引用。不同的引用类型主要体现在GC上:

  1. 强引用:如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。
  2. 软引用:在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。
  3. 弱引用:具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象。
  4. 虚引用:顾名思义,就是形同虚设,如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。

为什么要有不同的引用类型

不像C语言,我们可以控制内存的申请和释放,在Java中有时候我们需要适当的控制对象被回收的时机,因此就诞生了不同的引用类型,可以说不同的引用类型实则是对GC回收时机不可控的妥协。有以下几个使用场景可以充分的说明:

  1. 利用软引用和弱引用解决OOM问题:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题.

  2. 通过软引用实现Java对象的高速缓存:比如我们创建了一Person的类,如果每次需要查询一个人的信息,哪怕是几秒中之前刚刚查询过的,都要重新构建一个实例,这将引起大量Person对象的消耗,并且由于这些对象的生命周期相对较短,会引起多次GC影响性能。此时,通过软引用和 HashMap 的结合可以构建高速缓存,提供性能。

==和eqauls()的区别

== 是运算符,用于比较两个变量是否相等,而equalsObject类的方法,用于比较两个对象是否相等。默认Object类的equals方法是比较两个对象的地址,此时和 == 的结果一样。换句话说:基本类型比较用==,比较的是他们的值。默认下,对象用==比较时,比较的是内存地址,如果需要比较对象内容,需要重写equal方法。

equals()和hashCode()的联系

hashCode()Object类的一个方法,返回一个哈希值。如果两个对象根据equal()方法比较相等,那么调用这两个对象中任意一个对象的hashCode()方法必须产生相同的哈希值。

如果两个对象根据eqaul()方法比较不相等,那么产生的哈希值不一定相等(碰撞的情况下还是会相等的。

a.hashCode()有什么用?与a.equals(b)有什么关系

hashCode() 方法是相应对象整型的 hash 值。它常用于基于 hash 的集合类,如 HashtableHashMapLinkedHashMap等等。它与 equals() 方法关系特别紧密。根据 Java 规范,使用 equal() 方法来判断两个相等的对象,必须具有相同的 hashcode

将对象放入到集合中时,首先判断要放入对象的hashcode是否已经在集合中存在,不存在则直接放入集合。如果hashcode相等,然后通过equal()方法判断要放入对象与集合中的任意对象是否相等:如果equal()判断不相等,直接将该元素放入集合中,否则不放入。

有没有可能两个不相等的对象有相同的hashcode

有可能,两个不相等的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap 中会有冲突。如果两个对象相等,必须有相同的hashcode 值,反之不成立。

a==b 与a.equals(b)有什么区别

如果a 和b 都是对象,则 a==b 是比较两个对象的引用,只有当 a 和 b 指向的是堆中的同一个对象才会返回 true,而 a.equals(b) 是进行逻辑比较,所以通常需要重写该方法来提供逻辑一致性的比较。例如,String 类重写 equals() 方法,所以可以用于两个不同对象,但是包含的字母相同的比较。

a=a+b与a+=b有什么区别

+=操作符会进行隐式自动类型转换,此处a+=b隐式的将加操作的结果类型强制转换为持有结果的类型,而a=a+b则不会自动进行类型转换。

short s1= 1; s1 = s1 + 1; 该段代码是否有错,有的话怎么改?

有错误,short类型在进行运算时会自动提升为int类型,也就是说s1+1的运算结果是int类型。

short s1= 1; s1 += 1; 该段代码是否有错,有的话怎么改?

+=操作符会自动对右边的表达式结果强转匹配左边的数据类型,所以没错。

& 和 &&的区别

&是位操作,而&&是逻辑运算符。另外需要记住逻辑运算符具有短路特性,而&不具备短路特性。

内部类的作用

内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立.在单个外围类当中,可以让多个内部类以不同的方式实现同一接口,或者继承同一个类.创建内部类对象的时刻不依赖于外部类对象的创建。内部类并没有令人疑惑的”is-a”管系,它就像是一个独立的实体。

内部类提供了更好的封装,除了该外围类,其他类都不能访问。

如何将byte转为String

可以使用 String 接收 byte[] 参数的构造器来进行转换,需要注意的点是要使用的正确的编码,否则会使用平台默认编码,这个编码可能跟原来的编码相同,也可能不同。

可以将int强转为byte类型么?会产生什么问题?

我们可以做强制转换,但是Java中int是32位的而byte是8 位的,所以,如果强制转化int类型的高24位将会被丢弃,byte 类型的范围是从-128到128。导致精度损失。

进程、线程之间的区别

参考1:线程和进程的区别

进程:

  1. 进程是资源分配的最小单位。
  2. 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。
  3. 多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。.

线程:

  1. 线程程序执行的最小单位(资源调度的最小单位)。
  2. 线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
  3. 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。

进程和线程的关系:

  1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。
  2. 资源分配给进程,同一进程的所有线程共享该进程的所有资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
  3. 处理机分给线程,即真正在处理机上运行的是线程。
  4. 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

循环依赖

参考1:java 循环依赖_Java详解之Spring Bean的循环依赖解决方案【推荐】
参考2:Java中的Spring循环依赖详情

什么是循环依赖
循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。
Java常见面试题及解答_第12张图片
Spring中循环依赖场景有

  1. 构造器的循环依赖。
  2. field属性的循环依赖。

怎么检测是否存在循环依赖
检测循环依赖相对比较容易,Bean在创建的时候可以给该Bean打标,如果递归调用回来发现正在创建中的话,即说明了循环依赖了。

运行之后,Spring抛出了如下错误信息:

Description:

The dependencies of some of the beans in the application context form a cycle:

解决方案
不使用基于构造函数的依赖注入。可通过下面方式解决。

  1. 在字段上使用@Autowired注解,让Spring决定在合适的时机注入。【推荐】
  2. 用基于setter方法的依赖注射取代基于构造函数的依赖注入来解决循环依赖。

你可能感兴趣的:(Java,java,面试)