Java 面试宝典系列之 Java 基础

Posted Spring-_-Bear

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 面试宝典系列之 Java 基础相关的知识,希望对你有一定的参考价值。

文章目录

1、为什么 Java 代码可以实现一次编写、到处运行?

JVM(Java 虚拟机)是 Java 跨平台的关键。Java 源代码(.java)经过编译器编译成字节码(.class),JVM 负责将字节码翻译成特定平台下的机器码并运行,从而实现了平台无关性

2、一个 Java 文件里可以有多个类吗(不含内部类)?

一个 .java 结尾的文件中可以有多个类但有且仅有一个使用 public 修饰符修饰的类,且源文件名必须与 public 修饰的类名相同并以 .java 作为文件后缀名

3、说一说你对 Java 访问权限的了解

修饰符说明
private仅可被本类的成员访问
default可以被本类的成员、同一个包下的其他类访问
protected可以被本类的成员、同一个包下的其他类、本类的子类访问
public访问不受限

4、介绍一下 Java 的数据类型

Java 的数据类型分为八大基本数据类型和引用类型

  1. 八大基本数据类型:整数类型(byte/short/int/long)、浮点类型(float/double)、字符类型(char)、布尔类型(boolean,除 boolean 类型外其它类型都可以看作数字类型)

    类型字节数
    boolean不确定,因 JVM 具体实现而异
    byte1
    char2(\\u0000 ~ \\uffff)
    short2
    int4
    float4
    long8
    double8
  2. 引用类型:对一个对象的引用,可以将引用类型分为三类,即数组、类和接口

5、int 类型的数据范围是多少?

Java 中的 int 数据类型占用 4 个字节(byte),一个字节占用 8 位(bit),因而 int 的数据范围是 232 - 232 - 1

6、请介绍全局变量和局部变量的区别

  1. 全局变量(Java 中没有真正意义上的全局变量的概念,代指类的成员变量)

    1. 定义位置:成员变量是在类的范围里定义的变量
    2. 默认值:成员变量有默认初始值
    3. 实例变量:未被 static 修饰的成员变量也叫实例变量,它存储于对象所在的堆内存中,生命周期与对象相同
    4. 类变量:被 static 修饰的成员变量也叫类变量,它存储于方法区中,生命周期与当前类相同
  2. 局部变量

    1. 定义位置:局部变量是在方法或代码块里定义的变量

    2. 默认值:局部变量没有默认初始值(数组除外)

      public static void main(String[] args) 
          int[] arr = new int[3];
          // Output: [0, 0, 0]
          System.out.println(Arrays.toString(arr));
      
      
    3. 作用域:局部变量存储于栈内存中,方法执行结束时变量空间会自动释放

7、请介绍一下实例变量的默认值

所谓实例变量也即未被 static 关键字修饰的成员变量(参考 6.1.3),当实例变量未在定义时,或未在构造器中,或未在代码块中赋初值时将拥有默认值。引用类型默认初始化为 null,八大基本数据类型的默认初始值如下表

类型默认值
booleanfalse
byte0
char‘\\u0000’
short0
int0
float0.0F
long0L
double0.0

8、为啥要有包装类?

在万物皆对象的 Java 编程世界里,八大基本数据类型在实际使用过程中存在着一些约束,比如传参、赋值传入的值可能是 null,而基本数据类型不允许为 null,这时就会抛出隐式的异常而导致程序中断运行,因而为每个基本数据类型都引入对应的包装类以更加高效地使用 Java 进行编程

9、说一说自动装箱、自动拆箱的应用场景

  • 什么是自动装箱和自动拆箱?自动装、拆箱是 JDK1.5 提供的功能,通过自动装箱、自动拆箱可以大大简化基本类型和包装类对象之间的转换

  • 自动装箱与自动拆箱

    1. 自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型
    2. 自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型
  • 手动装箱与手动拆箱

    // 手动装箱
    Integer integer = Integer.valueOf(i);
    // 手动拆箱
    int ii = integer.intValue();
    

10、 如何对 Integer 和 Double 类型判断相等?

  • 错误方法:不能使用 == 、转为字符串、包装类的 compareTo 方法进行比较,因为这三种比较方式比较前提是二者属于同一数据类型

  • 正确方法:将 IntegerDouble 先转为转换为相同的基本数据类型(double),然后再二者作差进行比较

    Integer i = 100;
    Double d = 100.00;
    System.out.println(i.doubleValue() - d.doubleValue() == 0 ? "equal" : "not equal");
    

11、 int 和 Integer 有什么区别,二者在做 == 运算时会得到什么结果?

int 是基本数据类型,Integer 是 int 的包装类。二者在做 == 运算时,Integer 会自动拆箱为 int 类型,然后再进行比较

12、说一说你对面向对象的理解

面向对象是比面向过程编程更为优秀的一种程序设计思想,其基本思想是使用类对客观世界中存在的事物进行模拟,形成一种映射的关系,使得系统设计符合人类的自然思维方式,开发更加便捷,设计更加直观,程序更加健壮

13、面向对象的三大特征是什么?

  1. 继承:是面向对象实现代码复用的重要手段,当子类继承父类后获得父类的方法和属性
  2. 封装:将对象的实现细节隐藏,对外暴露接口
  3. 多态:子类的对象可以赋值给父类的引用,做到运行时动态绑定,意味着执行同一对象的同一方法时,可表现出多种行为特征

14、 封装的目的是什么,为什么要有封装?

封装的目的是隐藏对象内部的实现细节,外部无法直接操作和修改(只能通过提供的接口方法)。使用封装具有以下好处:

  1. 隐藏了类的实现细节
  2. 限制对成员变量的非法访问
  3. 进行数据检查,保证了对象信息的完整性
  4. 利于修改,提高了代码的可维护性

15、说一说你对多态的理解

子类的对象可以赋值给父类的引用,做到运行时动态绑定,意味着执行同一对象的同一方法时,可表现出多种行为特征。提高了程序的可扩展性,让代码更加简洁优雅

16、Java 中的多态是怎么实现的?

多态的实现依赖于继承。程序设计时,将方法的参数设置为父类型,传参时传入该父类型的子类型即可做到运行时动态绑定,此时执行父类的同一方法时可以表现出多种行为特征

17、Java 为什么是单继承,为什么不能多继承?

Java 业内也称为 “C++--”,即 Java 语言设计时借鉴了 C++ 的语法,而 C++ 的多继承机制繁琐,容易产生误解,代码可读性不高,Java 语言设计时摒弃了这一实现,只允许单继承,但一个子类有父类,父类仍可有父类,变相地实现了多继承

18、说一说重写与重载的区别

  • 重载:重载发生在一个类中,多个方法之间方法名相同、参数列表不同时构成重载(不能根据方法的访问修饰符和返回值判断是否构成重载,因为他们不属于方法签名的一部分)
  • 重写:重写发生在父子类或实现接口时,重写要求返回值类型小于等于父类、抛出的异常小于等于父类、访问权限修饰符要大于等于父类即不能缩小父类方法的访问权限

19、构造方法能不能重写?

构造方法可以重载但不能重写。因为构造方法需要和类保持同名,而重写的要求是子类方法要和父类方法保持同名。如果允许重写构造方法的话,那么子类中将会存在与类名不同的构造方法,这与构造方法的要求是矛盾的

20、介绍一下 Object 类中的方法

方法功能
Class<?> getClass()返回该对象的运行时类
boolean equals(Object obj)判断传入的对象与当前对象引用是否相等
int hashCode()返回该对象的 hashCode 值。在默认情况下,Object 类的 hashCode() 方法根据该对象的地址来计算
String toString()Object 类的 toString() 方法返回【运行时类名@十六进制 hashCode 值】格式的字符串
clone()克隆对象
finalize()主动调用垃圾回收器回收内存,JDK9 及之后版本不推荐使用
wait()让当前线程进入等待状态。直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法
notify()唤醒在该对象上等待的某个线程
notifyAll()唤醒在该对象上等待的所有线程

21、说一说 hashCode() 和 equals() 的关系

hashCode() 用于获取对象的哈希码(散列码),eauqls() 用于比较两个对象引用是否相等

  1. 如果两个对象相等,则它们必然有相同的 hashCode
  2. 虽然两个对象有相同的 hashCode,但它们未必相等

22、为什么要重写 hashCode() 和 equals()?

如果重写 equals() 时不重写 hashCode(),那么导致该类的实例添加到散列集合(Set、Map)中时不同的对象具有不同的 hashCode(对象间两两比较始终不会相等),从而致使通过 equals() 方法比较内容判断对象是否相同时就失去了意义。

重写 equals() 的同时重写 hashCode() 的意义在于当两个对象 equals() 比较相等时,他们就有相同的 hashCode 即可判断元素重复

23、== 和 equals() 有什么区别?

基本类型引用类型
==比较数值是否相等比较引用是否相同
未重写重写后
equals()比较引用是否相同比较对象内容是否相同

注:重写类的 equals() 方法的同时一般也会重写其 hashCode() 方法,原因分析参考第 22 点

24、 String 类有哪些常用方法?

方法功能
char charAt(int index)返回指定索引处的字符
String substring(int beginIndex, int endIndex)从字符串中截取 [begin, end) 子串
String[] split(String regex)以指定的规则将此字符串分割成数组
String trim()删除字符串前导和后置的空格
int indexOf(String str)返回子串在此字符串首次出现的索引
int lastIndexOf(String str)返回子串在此字符串最后出现的索引
boolean startsWith(String prefix)判断此字符串是否以指定的前缀开头
boolean endsWith(String suffix)判断此字符串是否以指定的后缀结尾
String toUpperCase()将此字符串中所有的字符大写
String toLowerCase()将此字符串中所有的字符小写
String replaceFirst(String regex, String replacement)用指定字符串替换第一个匹配的子串
String replaceAll(String regex, String replacement)用指定字符串替换所有的匹配的子串

25、 String 可以被继承吗?

String 类不可以被继承因为其是一个不可变类即使用了 fianl 关键字进行修饰。Java9 之前使用 char[] 数组来存储字符,Java9 及之后使用 byte[] 来保存字符。将 String 设计为不可变的类提供了极大的方便:

  1. 便于存储敏感信息如账号、密码、网络路径
  2. 不可变的数据可以由多线程共享而不用考虑数据安全问题
  3. 不变的字符串使得字符串常量池有了意义,节省了堆空间
  4. 使用不变的 String 字符串来存储对象的 hashCode 值

26、说一说 String 和 StringBuffer 有什么区别

  • String 创建后其内容不可变,线程安全且效率高
  • StringBuffer 创建后可以通过其 append()、insert()、reverse()、setCharAt()、setLength() 等方法进行修改,线程安全但效率低

27、说一说 StringBuffer 和 StringBuilder 有什么区别

两个类都是 final 类型的类,构造方法和成员方法基本相同,构建的字符串序列可变,不同的是 StringBuffer 较 StringBuilder 线程安全,导致效率较低。单线程情况下通常使用 StringBuilder

28、使用字符串时,new 和 “” 推荐使用哪种方式?

  • "hello" 的方式直接将栈中的变量引用到常量池的 “hello” 数据空间
  • new String("hello") 将栈中的变量引用到堆中的 String 对象的 value 数组,而 value 最终引用到常量池的 “hello” 数据空间

new String() 比 “” 的方式占用更多的空间,推荐直接使用 “” 方式创建字符串

29、说一说你对字符串拼接的理解

  1. +:常量字符串直接相加
  2. String 类的 concat(str1,str2) 方法:对两个包含变量的字符串进行拼接
  3. StringBuilder:包含变量且不要求线程安全
  4. StringBuffer:包含变量且要求线程安全

30、两个字符串相加的底层是如何实现的?

常量相加看池,变量相加看堆

// 池中只创建了一个字符串常量 "hello123"
String a = "hello" + "123";
// b -> 常量池的 "hello"
String b = "hello";
// c -> 常量池的 "123"
String c = "123";
// d -> 堆中 value,value 指向常量池中的 "hello123"
// b + c 的底层实现:调用 StringBuilder 的 append 方法连接两次,然后再 new String() 返回
String d = b + c;

31、String a = “abc”; 说一下这个过程会创建什么,放在哪里?

JVM 会先检查常量池中是否已经存有 “abc”,若没有则将 “abc” 存入常量池,已存在则直接将其引用赋值给栈变量 a

32、new String(“abc”) 是去了哪里,仅仅是在堆里面吗?

“abc” 字符串存放于常量池中,使用堆中的 String 对象的 private final char value[]; 属性引用到 “abc”

33、接口和抽象类有什么区别?

  • 接口体现的是一种规范和标准,对外暴露的接口体现了实现者所能提供的服务
  • 抽象类体现的是一种模板式设计的理念,定义了多个子类共有的属性和方法
接口抽象类
所有内部类所有内部类
方法默认方法、静态方法、抽象方法所有类型的方法、抽象方法
变量成员变量默认都是 public static final 类型所有类型的变量
构造器有构造器,但不用于创建对象而用于对抽象类的初始化
代码块可以包含

34、接口中可以有构造函数吗?

接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。但接口里可以包含成员变量(默认都是 public static final 类型)、方法(只能是抽象方法、静态方法、默认方法)、内部类(包括内部接口、枚举等)

35、谈谈你对面向接口编程的理解

接口体现的是一种规范和实现分离的设计哲学,充分利用接口可以极好地降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。基于这种原则,很多软件架构设计理论都倡导 “面向接口” 编程,而不是面向实现类编程,使用接口编程也能更好地使用多态

36、遇到过异常吗,如何处理?

  • try 捕获异常

  • catch 处理异常

  • finally 无条件回收资源

37、说一说 Java 的异常机制

异常逐层抛出,依次寻找处理者,若没有异常的处理者则由 JVM 打印异常信息后退出系统

  • try 块用于包裹业务代码
  • catch 块用于捕获并处理某个类型的异常
  • finally 块则用于回收资源

38、请介绍 Java 的异常接口

  • Error 是无法处理的错误:一般是与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败

  • Exception 分为运行时异常(Runtime,无须显式声明抛出)和编译时异常(Checked,编译时必须处理)

39、finally 是无条件执行的吗?

正常情况下 finally 块中的语句是无条件执行的。如果在 try 块或 catch 块中使用 System.exit(1); 退出虚拟机,则 finally 块将失去执行的机会

try 
    throw new RuntimeException("Runtime exception was thrown");
 catch (Exception e) 
    System.out.println(e);
    System.exit(1);
 finally 
    // 此时 finally 将不再输出
    System.out.println("finally");

40、在 finally 中 return 会发生什么?

一旦在 finally 块中使用了 return、throw 语句,将会导致 try、catch 块中的 return、throw 动作不会执行

public int method() 
    int i = 1;
    try 
        i++;
        String[] names = new String[3];
        if (names[i].equals("lcx")) 
        
        return i;
     catch (NullPointerException e) 
        return ++i;
     finally 
        // return result: 4
        return ++i;
    

41、说一说你对 static 关键字的理解

在 Java 类里只能包含成员变量、方法、构造器、初始化块、内部类(包括接口、枚举)5 种成员,除构造器外全都可以用 static 关键字进行修饰。以 static 修饰的成员就是类成员,类成员属于整个类而不属于单个对象

类成员不能访问实例成员,因为类成员属于类,作用域比实例成员更大,完全可能出现类成员已经初始化完成,但实例成员还未曾初始化的情况,如果允许类成员访问实例成员将会引起大量错误

42、static 修饰的类能不能被继承?

用 static 修饰的内部类(静态内部类)可以被继承,这个内部类属于外部类本身而不属于某个对象。外部类的上一级程序单元是包,所以不可以用 static 修饰

静态内部类需满足如下规则:

  1. 静态内部类可以包含静态成员,也可以包含非静态成员
  2. 静态内部类不能访问外部类的实例成员,只能访问它的静态成员
  3. 外部类的所有方法、初始化块都能访问其内部定义的静态内部类
  4. 在外部类的外部,也可以实例化静态内部类,语法如下:外部类.内部类 变量名 = new 外部类.内部类构造方法();

43、static 和 final 有什么区别?

  • static 关键字可以修饰类的成员变量、成员方法、代码块、内部类,被 static 修饰的成员是类的成员,它属于类而不属于单个对象(类成员属于类,它随类的信息存储在方法区,而并不随对象存储在堆中)

  • final 关键字可以修饰类、变量、方法,被 final 修饰的成员有以下特点:

    1. final 修饰的类不可继承、方法不可重写、变量获得初始值后不可修改

    2. final 修饰类变量的初始化位置:定义时、静态代码块中

      public class Outer 
          private static final int MIN;
          static 
              MIN = -1;
          
      
      
    3. final 修饰成员变量的初始化位置:定义时、代码块中、构造器中

      public class Outer 
          private final int a;
      
          a = 2;
      
          private final int b;
      
          public Outer(int b) 
              this.b = b;
          
      
      
    4. final 修饰局部变量的初始化位置:定义时、后续代码中

      public static void main(String[] args) 
          final int a;
          a = 2;
      
      

44、说一说你对泛型的理解

若没有泛型的支持,则所有放进集合的元素编译类型都为 Object 类型,对放进集合的元素不能进行控制、取出也需要进行向下强制转型,容易造成 ClassCastException

Java5 引入泛型后代码更加简洁、程序更加健壮。Java 泛型的设计原则是只要代码在编译时没有出现警告,就不会在运行时产生 ClassCastException,使用泛型使得代码的可读性更高

45、介绍一下泛型擦除

泛型是 Java 1.5 才引进的概念,在这之前是没有泛型这个概念的。为了与之前的代码更好的兼容,字节码在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除(泛型信息只存在于代码编译阶段)

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
// Output: true	
System.out.println(l1.getClass() == l2.getClass());

// 当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉
List<String> list1 = ...; 
// list2 将元素当做 Object 处理
List list2 = list1; 

// 把一个不具有泛型信息的对象赋值给另一个具有泛型信息的变量时,发生泛型转换,编译器提示未经检查的转换
List list1 = ...; 
// Unchecked assignment: 'java.util.List' to 'java.util.List<java.lang.String>' 
List<String> list2 = list1;

46、List<? super T> 和 List<? extends T> 有什么区别?

  • 其中的 ? 代表通配符,通配符的出现是为了指定泛型中的类型范围。<?>提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。如下代码:

    public void generic(Collection<?> collection) 
        // 编译报错
        collection.add(1);
        // 编译通过
        collection.isEmpty();
        Iterator<?> iterator = collection.iterator();
    
    
  • List<? super T> 用于设定类型通配符 ? 的下限,此处 ? 代表一个未知的类型,但它必须是 T 的父类型

  • List<? extends T> 用于设定类型通配符 ? 的上限,此处 ? 代表一个未知的类型,但它必须是 T 的子类型

泛型和数组有所不同:假设 Foo 是 Bar 的一个子类型(子类或者子接口),那么 Foo[] 依然是 Bar[] 的子类型,但 G<Foo> 不是 G<Bar> 的子类型

47、说一说你对 Java 反射机制的理解

反射机制可以在程序运行时:

  1. 通过反射获得任意一个类的 Class 对象,并通过这个对象查看这个类的信息
  2. 可以通过反射创建任意一个类的实例,并访问该实例的所有成员
  3. 可以通过反射机制生成一个类的动态代理类或动态代理对象

48、Java 反射在实际项目中有哪些应用场景?

  1. JDBC 时使用反射加载数据库的驱动类,获得数据库连接对象
  2. 框架底层解析注解/XML 时根据类全路径,利用反射获取实例
  3. 面向切面编程(AOP)的具体实现是在程序运行时利用反射机制来创建目标对象的代理类

49、说一说 Java 的四种引用方式(强、软、弱、虚)

  1. 强引用:这是 Java 程序中最常见的引用方式,即程序创建一个对象,并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象。当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收

    Object o = new Object();
    
  2. 软引用:当一个对象只有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象。当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中

    SoftReference<String> studentSoftReference = new SoftReference<>("HelloWorld");
    String helloWorld = studentSoftReference.get();
    // HelloWorld
    System.out.println(helloWorld);
    System.gc();
    // Output: HelloWorld(只有当内存不足且垃圾回收机制工作时才回收软引用对象)
    System.out.println(helloWorld);
    
  3. 弱引用:弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。当然,并不是说当一个对象只有弱引用时,它就会立即被回收,正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收

    WeakReference<byte[]> weakReference = new WeakReference<>(new byte[1]);
    // Output: [0]
    System.out.println(Arrays.toString(weakReference.get()));
    // 垃圾回收机制工作时弱引用的对象一定会被回收
    System.gc();
    // Output: null
    System.out.println(Arrays.toString(weakReference.get()));
    
  4. 虚引用:虚引用类似于完全没有引用,对对象本身没有太大影响,对象甚至感觉不到虚引用的存在,虚引用主要用于跟踪对象被垃圾回收的状态。虚引用不能单独使用,必须和引用队列联合使用,它的原理是当一个虚引用指向的对象被回收的时候,它会把相关信息添加到跟这个虚引用相关联的这个队列中。再就是虚引用的 get 方法,返回的永远是 null。用途是可以用来管理堆外内存 Netty NIO

    public class PhantomReference<T> extends Reference<T> 
        public T get() 
            return null;
        
        public PhantomReference(T referent, ReferenceQueue<? super T> q) 
            super(referent, q);
        
    
    
    /**
     * @author Spring-_-Bear
     * @datetime 2022-06-16 06:52
     */
    public class PhantomReferenceTest 
        public static void main(String[] args) 
            // 引用队列,跟虚引用相关联
            ReferenceQueue<Person> referenceQueue = new ReferenceQueue<>();
            // 虚引用
            PhantomReference<Person> personPhantomReference = new PhantomReference<>(new Person(), referenceQueue);
    
            List<Object> list = new LinkedList<>();
            // 开启一个线程,不断地占用内存
            new Thread(() -> 
                while (true) 
                    try 
                        // 不断的给 list 添加数据,使内存被占光,导致虚引用被回收
                        list.add(new byte[1024 * 1024]);
                        Thread.sleep(1000);
                     catch (Exception e) 
                        e.printStackTrace();
                    
                    // Output: null
                    System.out.println(personPhantomReference.get());
                
            ).start();
    
            // 开启另一个线程,不断的从 referenceQueue 引用队列中取数据
            new Thread(() -> [Interview]Java 面试宝典系列之 Java 多线程

    [Interview]Java 面试宝典系列之 Java 集合类

    [Interview]Java 面试宝典系列之 Spring

    [Interview]Java 面试宝典系列之 Java 虚拟机(JVM)

    [Interview]Java 面试宝典系列之 Spring Boot

    [Interview]Java 面试宝典系列之 MyBatis