面试题

一、Java 基础相关面试

Java 基础相关

1.1、面向对象的特征

结合下面的文字形成自己的理解每天都重复一遍,方便记忆。

  • 封装

利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。

  • 继承

在一个类的基础上创建一个新的类,提高了代码的复用性,继承实现了 IS-A 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法。继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 向上转型 。

  • 多态

多态分为编译时多态和运行时多态,编译时多态主要指方法的重载,运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定。

满足运行时多态的三个条件:继承或接口实现、子类对象重写父类方法、父类引用指向子类对象。

以下代码,乐器类(Instrument)有两个子类: Wind 和 Percussion,它们都覆盖了父类的 play() 方法,并且在main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。

  1. public class Instrument {
  2. public void play() {
  3. System.out.println("Instument is playing...");
  4. }
  5. }
  6. public class Wind extends Instrument {
  7. public void play() {
  8. System.out.println("Wind is playing...");
  9. }
  10. }
  11. public class Percussion extends Instrument {
  12. public void play() {
  13. System.out.println("Percussion is playing...");
  14. }
  15. }
  16. public class Music {
  17. public static void main(String[] args) {
  18. List<Instrument> instruments = new ArrayList<>();
  19. instruments.add(new Wind());
  20. instruments.add(new Percussion());
  21. for(Instrument instrument : instruments) {
  22. instrument.play();
  23. }
  24. }
  25. }

1.2、Java 语言有哪些特点

  • 简单易学。
  • 面向对象(封装,继承,多态)。
  • 平台无关性( Java 虚拟机实现平台无关性)。
  • 支持多线程。
  • 可靠性。
  • 安全性。
  • 支持网络编程并且很方便。
  • 编译与解释并存。

    1.4、为什么说 Java 语言“编译与解释并存”?

    这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class 文件),这种字节码必须由 Java 解释器来解释执行。

    1.5、字符型常量和字符串常量的区别 ?

    形式方面:字符常量是单引号引起的一个字符,字符串常量是双引号引起的若干个字符。
    含义方面:字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。
    内存占用方面:字符常量只占 2 个字节; 字符串常量占若干个字节。

    1.6、Java中有哪几种注释方式,你如何理解代码的注释?

    Java 有三种注释方式,分别是单行注释、多行注释、文档注释。
    代码的注释并不是越详细越好。好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。写注释时采用换位思考的方式,假设这个代码是别人写的你希望别人怎么描述呢?按照这个思路编写注释效果会事半功倍😉

    1.7、标识符和关键字的区别是什么?

    在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了标识符,简单来说,标识符就是一个名字。但是有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这种特殊的标识符就是关键字。因此,关键字是被赋予特殊含义的标识符。比如,在我们的日常生活中 ,“警察局”这个名字已经被赋予了特殊的含义,所以如果你开一家店,店的名字不能叫“警察局”,“警察局”就是我们日常生活中的关键字。

    1.3、 a = a + b 与 a += b 的区别

    += 隐式的将加操作的结果类型强制转换为持有结果的类型。如果两这个整型相加,如 byte、short 或者 int,首先会将它们提升到 int 类型,然后在执行加法操作。
    1. byte a = 127;
    2. byte b = 127;
    3. b = a + b; // error : cannot convert from int to byte
    4. b += a; // ok
    以上代码因为 a+b 操作会将 a、b 提升为 int 类型,计算结果是 int 类型,所以将 int 类型赋值给 byte 就会编译出错。

    1.4、 3 * 0.1 == 0.3 将会返回什么? true 还是 false?

    false,因为有些浮点数不能完全精确的表示出来。
    那么如何比较两个浮点数是否相等呢?我们可以设置一个相对比例阈值,两个数相减,差值小于阈值,认为这两个浮点数相等。

    1.6、能在 Switch 中使用 String 吗?

    从 Java 7 开始,我们可以在 switch case 中使用字符串,但这仅仅是一个语法糖。内部实现在 switch 中使用字符串的 hash code。

    1.7、 == 和 equals() 的区别

    Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。

equals()方法存在于Object类中,而Object类是所有类的直接或间接父类。equals()不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。

对于基本数据类型来说,== 比较的是值。
对于引用数据类型来说,== 比较的是对象的内存地址。

equals()方法存在两种使用情况:

  • 类没有重写equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法,具体代码如下。

    1. public boolean equals(Object obj) {
    2. return (this == obj);
    3. }
  • 类重写了equals()方法 :一般我们都覆盖equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则认为这两个对象相等返回 true。

    1.7、为什么在重写 equals 方法时需要重写 hashCode 方法?

    因为有规范指定需要同时重写 hashCode()equals(),许多容器类,如 HashMap、HashSet 都依赖于此规范。如果我们没有按照规范去重写方法那么将会导致意外的结果。

以 HashSet 添加元素去重为例,当把一个对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置。如果计算出的位置没有其他对象 HashSet 则会将其加入。如果计算出的位置有其他对象,这时会调用equals()方法来检查新加入的对象和当前位置的对象是否相等。如果两者相等则,则表示重复 HashSet 就不会让其加入。可以结合下图方便你的理解。
💕 Java 相关面试题整理 - 图1

1.7、final、finalize 和 finally 的不同之处?

  • final 是一个修饰符,可以修饰变量、方法和类。
  • Java 允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个方法调用的,但是什么时候调用 finalize 没有保证。
  • finally 是一个关键字,与 try 和 catch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。

    1.8、接口与抽象类的区别?

    这个答案从小到大来方便我们的记忆。

  • 从变量角度来说,接口中变量都是被public static final修饰的,抽象类中的变量与普通类一样,可以定义任何类型的变量,无特殊限制。

  • 从构造方法角度来说,抽象类可以有构造方法,接口没有构造方法。
  • 从方法角度来说,接口中的方法都是抽象方法,抽象类中的方法除了抽象方法之外还可以有普通方法。
  • 从使用上来说,抽象类配合 extends 关键字使用,接口配合 implements 关键字使用,除此之外二者均不可以实例化。一个类可以实现多个接口但只能继承一个抽象类。

    1.2、this()super()在构造方法中使用时的区别?

  • super()从子类调用父类构造, this()在同一类中调用其他构造均需要放在第一行。

  • 尽管可以用this()调用一个构造器, 却不能调用2个
  • this()super()不能出现在同一个构造器中, 否则编译不通过。

    1.3、Java 移位运算符

  • <<左移运算符,x << 1,相当于x乘以2(不溢出的情况下),低位补0。

  • >>带符号右移,x >> 1,相当于x除以2,正数高位补0,负数高位补1。
  • >>>无符号右移,忽略符号位,空位都以0补齐。

    二、Java 泛型相关面试题

    泛型,非常常见的技术栈

2.1、为什么使用泛型 ?

第一方面:适用于多种数据类型执行相同的代码
如果不使用泛型效果是下面这个样子的。

  1. private static int add(int a, int b) {
  2. System.out.println(a + "+" + b + "=" + (a + b));
  3. return a + b;
  4. }
  5. private static float add(float a, float b) {
  6. System.out.println(a + "+" + b + "=" + (a + b));
  7. return a + b;
  8. }
  9. private static double add(double a, double b) {
  10. System.out.println(a + "+" + b + "=" + (a + b));
  11. return a + b;
  12. }

使用泛型后效果是这样的

  1. private static <T extends Number> double add(T a, T b) {
  2. System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
  3. return a.doubleValue() + b.doubleValue();
  4. }

第二方面: 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)
先看一下未使用泛型的效果如下

  1. List list = new ArrayList();
  2. list.add("xxString");
  3. list.add(100d);
  4. list.add(new Person());

我们在使用上述 list 中,list 中的元素都是 Object 类型(无法约束其中的类型),所以在取出集合元素时需要人为的强制类型转化到具体的目标类型,如果我们选择了错误的类型进行转换,那么很容易出现java.lang.ClassCastException异常。

使用泛型之后的效果是这样的。

  1. // list中只能放String, 不能放其它类型的元素
  2. List<String> list = new ArrayList<String>();
  3. list.add("Hello World!");
  4. //在使用的时候也不用考虑类型转换的事情
  5. System.out.println("Result is " + list.get(0));

2.2、泛型的上限及下限

在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
泛型的上限

  1. class Info<T extends Number>{ // 此处泛型只能是数字类型
  2. private T var ; // 定义泛型变量
  3. public void setVar(T var){
  4. this.var = var ;
  5. }
  6. public T getVar(){
  7. return this.var ;
  8. }
  9. public String toString(){ // 直接打印
  10. return this.var.toString() ;
  11. }
  12. }
  13. public class demo1{
  14. public static void main(String args[]){
  15. Info<Integer> i1 = new Info<Integer>() ; // 声明Integer的泛型对象
  16. }
  17. }

泛型的下限

  1. class Info<T>{
  2. private T var ; // 定义泛型变量
  3. public void setVar(T var){
  4. this.var = var ;
  5. }
  6. public T getVar(){
  7. return this.var ;
  8. }
  9. public String toString(){ // 直接打印
  10. return this.var.toString() ;
  11. }
  12. }
  13. public class GenericsDemo21{
  14. public static void main(String args[]){
  15. Info<String> i1 = new Info<String>() ; // 声明String的泛型对象
  16. Info<Object> i2 = new Info<Object>() ; // 声明Object的泛型对象
  17. i1.setVar("hello") ;
  18. i2.setVar(new Object()) ;
  19. fun(i1) ;
  20. fun(i2) ;
  21. }
  22. public static void fun(Info<? super String> temp){ // 只能接收String或Object类型的泛型,String类的父类只有Object类
  23. System.out.print(temp + ", ") ;
  24. }
  25. }

2.3、如何理解 Java 中的泛型是伪泛型

泛型中类型擦除 Java泛型这个特性是从 JDK 1.5才加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
那么如何验证呢?我们可以通过反射的方式向一个定义了泛型的集合中添加任意类型的元素都可以添加成功。

三、Java 注解相关面试题

Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。

3.1、注解的作用

注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。它主要的作用有以下四方面:

  • 生成文档,通过代码里标识的元数据生成javadoc文档。
  • 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。
  • 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。
  • 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。

    3.2、注解的分类

  • Java自带的标准注解,包括@Override、@Deprecated和@SuppressWarnings,分别用于标明重写某个方法、标明某个类或方法过时、标明要忽略的警告,用这些注解标明后编译器就会进行检查。

  • 元注解,元注解是用于定义注解的注解,包括@Retention、@Target、@Inherited、@Documented
    • @Retention用于标明注解被保留的阶段
    • @Target用于标明注解使用的范围
    • @Inherited用于标明注解可继承
    • @Documented用于标明是否生成javadoc文档
  • 自定义注解,可以根据自己的需求定义注解,并可用元注解对自定义注解进行注解。

    四、Java 异常相关面试题

    整理了一些常见的面试题,系统性的知识参看上文。

4.1、Java 中异常分为哪几种?

  • 运行时异常
  • 非运行时异常(编译时异常)

    4.2、Java 处理异常有哪些方式?

  • 方式一:使用try catch关键字捕获异常。

  • 方式二:使用throw关键字向方法调用出抛出异常。

    4.4、try catch finally,try 中有 return,finally 还执行么?

    执行,并且 finally 中 return 的执行早于 try 中的。

  • 不管有没有出现异常,finally块中代码都会执行。

  • 当 try 和 catch 中有return时,finally仍然会执行。
  • finally 是在 return 语句执行之后,返回之前执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,不管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在finally执行前就已经确定了。
  • finally 中最好不要包含 return,否则程序会提前退出,返回值不是 try 或 catch 中保存的返回值。

    4.5、throw 与 thorws 区别

  • 位置不同:throws 用在函数上,后面跟的是异常类,而 throw 用在函数内,后面跟的是异常对象。

  • 功能不同:throws 用来声明异常,让调用者知道该功能可能出现的问题,可以给出预先的处理方式。throw 抛出具体的问题对象,执行到 throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。也就是说 throw 语句独立存在时,下面不要定义其他语句,因为执行不到。

    4.6、Error 与 Exception 的区别

    Error 表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出。不可能指望程序能处理这样的情况 Exception 表示一种设计或实现问题,表示如果程序运行正常,从不会发生的情况 。
    Error 和 Exception 都是 Java 错误处理机制的一部分,都继承了 Throwable 类。 Exception 表示的异常,异常可以通过程序来捕捉,或者优化程序来避免。Error表示的是系统错误,不能通过程序来进行错误处理。

    三、Java IO 相关面试题

    因为工作中用的不多,在面试中被问的并不多。

3.1、 Java 中 IO 流分为几种?

  • 按照流的流向可分为输入流和输出流。
  • 按照操作单元可以划分为字节流和字符流。
  • 按照流的角色可分为节点流和处理流。

    3.2、节点流、处理流是什么?

  • 节点流:可以从或向一个特定的地方(节点)读写数据。如FileReader。

  • 处理流:是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。

    二、Java 反射相关面试题

    2 .1、你如何理解反射?

    Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。这种动态获取信息以及动态调用对象的方法的能力大大的提高了Java语言的灵活性。

    2.1、反射能帮助我们做什么?

    反射实际开发用的不多,但是反射时时刻刻陪伴着我们,是开发框架工作的基石。

    2.1、如何使用反射?

    Java 提供了 Reflection API 里边包含一些获取信息和使用等一些列操作,常见类如下。

  • Class 类:反射的核心类,可以获取类的属性,方法等信息。

  • Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
  • Method 类:Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或 者执行方法。
  • Constructor 类:Java.lang.reflec 包中的类,表示类的构造方法。

    2.1、反射的优缺点

  • 优点:可以让代码更加灵活、为各种框架提供开箱即用的功能提供了便利。

  • 缺点 :虽然反射使 Java 在运行时有了分析操作类的能力,但这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,使用反射性能较低,JVM 需要解析字节码,将内存中的对象进行解析。

    2.1、使用反射创建对象的两种方式

  • 方式一:使用 Class 对象的newInstance()方法

    1. //获取 Person 类的 Class 对象
    2. Class clazz=Class.forName("reflection.Person");
    3. //使用.newInstane 方法创建对象
    4. Person p=(Person) clazz.newInstance();
  • 方式二:使用 Constractor 对象的newInstance()方法

    1. //获取构造方法并创建对象
    2. Constructor c=clazz.getDeclaredConstructor(String.class,String.class,int.class);
    3. //创建对象并设置属性13/04/2018
    4. Person p1=(Person) c.newInstance("李四","男",20);

    2.2、Java 反射创建对象效率高还是通过new创建对象的效率高?

    通过new创建对象的效率比较高。使用反射的方式创建对象时,先找查找类资源,进行安全检查,使用类加载器创建,过程比较繁琐,所以效率较低 。

    2.1、 获取 Class 对象的几种方式

    每个类的 Class 对象在内存中仅有一份,所以无论是通过何种方式获取到 Class 对象,都是同一个对象。

  • 方式一:使用 Object 对象的getClass()方法

  • 方式二:使用类名.class
  • 方式三:使用Class.forName("类的全限定名")

    参考

  1. Pdai
  2. JavaGuide
  3. RUNBOO