Java 常问知识总结

本文旨在总结 Java 相关的面试常见问题。

Java 基础

面向对象编程的特征

面向对象编程主要具有以下四大特征:

  • 抽象:对同一个目标的共有属性和方法进行抽取、归纳、总结
  • 封装:隐藏对象的属性和实现细节,控制成员属性的访问和修改权限
  • 继承:子类继承父类的成员和方法(类单继承,接口多继承)
  • 多态:同一个行为具有多个不同的表现形式或形态(方法多态 & 对象多态)

Java 程序的执行

执行环境

Java 程序是在 JRE(Java 运行环境)中运行的,包括了 JVM、Java 核心类库等;JDK(Java 开发工具包)则包含了 Java 运行环境和一系列 Java 开发工具的完整包。一句话总结如下:

JRE 是 JDK 的子集,只能用来运行 Java 应用程序,不能用于编译开发

执行顺序

Java 程序需要先编译完成后再运行(与 Python 等脚本语言区分),具体的命令如下:

  • 使用 javac 命令来编译 .java 文件
  • 生成 class 文件后,使用 java 命令来运行(不用带后缀)

main 方法

main 方法是 Java 程序的入口方法,执行 Java 程序时会首先查找 main 方法。可以通过一个 String 数组向 main 方法传递参数:

public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println("args[" + i + "]=" + args[i]);
}
}

Java 命名规范

标识符命名规则

Java 中的所有标识符(包括类、方法、变量、常量)的命名需要遵循如下规则:

  • 标识符只能由字母、数字、下划线和美元符号组成
  • 标识符不能以数字开头
  • 标识符不能使用 Java 关键字
  • 标识符区分大小写

类命名规范

类命名一般遵循大驼峰式风格:首字母大写,后面每个单词首字母大写。

PS:部分众所周知的缩写例外,如 DO、BO、DTO、VO 等。

方法命名规范

方法命名一般应符合小驼峰式风格:首字母小写,后面每个单词首字母大写。

变量命名规范

变量量包括方法参数名、成员变量、局部变量,一般应符合小驼峰式风格。

常量命名规范

与变量相比,常量赋值后不可改变,使用 final 关键字定义,可分为三种类型:

  • 静态常量(类中)
  • 成员常量(类中)
  • 局部常量(类方法中)

静态常量名一般全部大写,单词间用下划线隔开。其他常量一般遵循小驼峰式风格。

Java 注释

Java 主要有三种注释:单行注释、块注释和文档注释:

private int id; // 这是 ID

private int id; /* 这是 ID */

/**
* 这是 ID
*/
private int id;

Java 数据类型

Java 基本数据类型

Java 总共有 4 类 8 种基本数据类型,具体如下:

  • 整型:byte、short、int、long
  • 浮点型:float、double
  • 字符型:char
  • 布尔型:boolean

Java 枚举类

Java 中的枚举是一种特殊的数据类型,用于定义一组常量。枚举类使用 enum 定义,可以包含零个或多个枚举常量,命名规范建议与静态常量保持一致,全大写。

枚举常量实际上是枚举类的静态实例,常量本身不可以修改,常量的字段值可以修改但不建议改。

Java 数组

数组可以通过两种方式创建:

  • 通过 new 创建:int[] arr = new int[5]
  • 直接创建:int[] arr = {1, 2, 3, 4, 5}

与 String 类型相比,数组没有 length 方法,但是有 length 属性。

值传递和引用传递

两者的主要区别如下:

  • 值传递是基本类型参数的字面量值的拷贝,方法对参数的修改不会影响之前参数的值
  • 引用传递是所引用的对象在堆中地址值的拷贝,方法对参数的修改直接影响之前的值

Java 严格来说都是值传递,看传递的对象是基本类型还是引用类型:

  • 对于基本类型,传递的是实际值的副本
  • 对于引用类型,传递的是对象的引用的值的副本,即内存地址

包装类型

Java 提供了 8 种基本数据类型及对应的包装类型,以解决基本数据类型无法面向对象编程的问题:

基本数据类型 包装类型
byte Byte
boolean Boolean
short Short
char Character
int Integer
long Long
float Float
double Double

包装类的应用场景包括:

  1. 集合类泛型只能是包装类
  2. 成员变量不能有默认值(基本数据类型都有默认值)
  3. 方法参数允许定义空值

Java 5 增加了自动拆箱 & 装箱机制:

  • 自动装箱即自动将基本类型转换为包装类型(调用 valuesOf 方法)
  • 自动拆箱即自动将包装类型拆解为基本类型(调用 xxValue 方法)

类型转换与提升

Java 的类型转换主要有两种:

  • 强制类型转换:强制显示地把一个数据类型转换为另一个数据类型,可能存在精度丢失和数据溢出问题
  • 自动类型转换:数字表示范围小的数据类型可以自动转换成范围大的数据类型,同样存在数据丢失和数据溢出问题,且可以直接将 int 常量字面量赋值给 byte、short、char

此外,在表达式中,Java 还会提供类型提升机制(与自动类型转换本质相同):

在多种不同数据类型的表达式中,类型会自动向范围表示大的值的数据类型提升。

泛型

泛型的本质就是参数化类型,可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法,其提供了编译时类型安全检测机制,可以在编译时检测到非法的类型。常用的泛型含义如下表所示:

泛型 说明
T Type(类型)
R Result(结果)
K Key(键)
V Value(值)
E Element(元素)
N Number(数字)
? 通配符,不确定类型

Java 修饰符

访问修饰符的作用范围

Java 主要有四类访问修饰符,其作用范围从大到小依次如下:

  • public:所有范围均可访问
  • protected :其他包不能访问,其他均可
  • 默认:仅当前类和当前包可访问,子孙类和其他包不能访问
  • private: 仅当前类可访问

static 关键字

static 代表静态,不依赖实例化对象(局部变量、this 或 super 关键字不适用),可以用来修饰内部类、方法、变量和代码块,其中静态代码块只会在类被加载时执行且执行一次。

和普通变量相比,static 变量的区别在于:

  • 所属目标:静态类属于类的变量,普通变量属于对象的变量
  • 存储区域:静态变量存储在方法区的静态区(JDK7 以上存储在 Class 对象对应的堆中),普通变量存储在堆区
  • 加载时间:静态变量随着类的加载而加载,随着类的消失而消失;普通对象则随着对象的加载而加载,随着对象的消失而消失
  • 调用方式:静态变量只能通过类名、对象调用,普通变量只能通过对象调用

final 关键字

final 关键字主要有三种用法:

  • 修饰类:表示该类不能被继承
  • 修饰方法:表示该方法不能被重写
  • 修饰变量:表示常量,只能赋值一次,不能被修改

注意与 finallyfinalize 区分:

  • finally 是 try-catch-finally 的最后一部分,表示不论发生任何情况都会执行的部分,可以省略
  • finalize 是 Object 类的一个方法,在垃圾收集器执行的时候会自动调用被回收对象的此方法,一般不建议主动使用

Java 运算符

== 和 equals 的区别

两种比较符的主要区别如下:

  • == 对于基本数据类型比较值是否相同,对于引用数据类型,比较对象地址是否相同
  • equals 是 Objects 类提供,默认比较对象地址,不能用于比较基本数据类型

PS:包装数据类型 String、Date、Integer 等类重写了 equals 和 hashCode 方法,使其比较的是存储对象的内容是否相等

s1 = s1 + 1 和 s1 += 1 的区别

如果是 int 类型,两者没有任何区别;如果数据类型小于 int,前者不支持强制转换(大转小,属于向下转型),会发生编译异常,需要强制转换:

public static void main(String[] args) throws Exception {
short s1 = 1;
s1 = (short) (s1 + 1);
System.out.println(s1);
}

+= 则自动包含了隐式强制类型转换。

i++ 和 ++i 的区别

一道经典的问题,比较简单:

  • i++ 是先取值再自增:表达式返回 i 的值
  • ++i 是先自增再取值:表达式返回 i+1 的值

& 和 && 的区别

两个运算符的区别如下:

  • 两者都可以用于逻辑与判断,推荐使用 &&,其具有短路功能(第一个表达式为 false 则跳过第二个)
  • 位运算需要使用 &,当两边的表达式不是 boolean 类型时,表示按位与操作

Java 条件和循环

while 和 do while 的区别

两者的区别如下:

  • while 是先判断条件再循环
  • do while 是先执行循环再判断条件,会多执行一次

循环的跳出

单个循环的跳出方式如下:

  • 使用 continue 跳出当前本次循环
  • 使用 break 跳出整个循环
  • 使用 return 跳出整个循环及当前方法

跳出多层循环可以在最外面的循环语句前定义一个标号,然后使用 break 标号语句跳出:

public static void main(String[] args) {
javastack:
for (int i = 0; i < 100; i++) {
for (int j = 0; j < 100; j++) {
System.out.println("test");
if (j == 66) {
break javastack;
}
}
}
}

switch case 支持的数据类型

switch case 的语法格式如下:

switch(expression) {
case value: // 常量或字面常量
// 语句
break; // 可选
case value:
// 语句
break; // 可选
// 你可以有任意数量的 case 语句
default: // 可选
// 语句
}

其中 expression 支持的数据类型包括:

  • 基本数据类型:byte、short、char、int
  • 包装数据类型:上面四个对应的
  • 枚举类型:Enum
  • 字符串类型:String(Java 7 开始支持)

Java 对象和类

构造方法

构造方法的定义与特征如下:

  • 构造方法是构造类的主要方法,每个类都必须要有构造方法
  • 构造方法名和类名相同,没有返回类型,new 一个对象的时候会调用指定的构造方法
  • 如果只有默认构造方法,不需要赋值初始化,则可以省略

一个类至少有一个构造方法,也可以有多个构造方法。如果没有显式地创建构造方法,Java 编译器会提供一个默认构造方法

this 与 super

this 和 super 的区别如下:

  • this 代表当前对象本身
  • super 代表当前对象的父类

重载和重写

重载和重写的区别如下:

  • 方法重写是父类与子类之间多态性的一种表现,即子类可以覆盖从父类继承的方法,一般使用 @Override 标识
  • 方法重载是一个类中方法多态性的一种表现,即一个类中可以有多个同名的方法,方法的参数类型不同或参数个数不同,返回类型可以相同也可以不同

基于上述差异,可以形成如下结论:

  • 构造器可以被重载,不能被重写(只属于当前类)
  • 私有方法只可以被重载,不能被子类重写
  • 静态方法可以被继承与重载,但是不可以被重写(子类如果定义相同方法,会替换父类的静态方法)

类的祖先

Java 中所有类的祖先是 java.lang.Object 类,所有类都拓展自该类,继承它的所有方法,具体包括:

普通类和抽象类的区别

两者的区别如下:

  • 抽象类必须用 abstract 关键字标识,普通类则不用
  • 抽象类可以包含 abstract 标识的抽象方法(也可以不包含),无需在抽象类中实现,普通类不能包含
  • 抽象类是设计子类继承用的,不能直接通过 new 实例化,只能通过子类继承或者匿名内部类进行实例化

接口与抽象类的区别

两者的区别如下:

  • 区别 1:抽象类是一个类,接口只是一个接口,概念和应用场景不同
  • 区别 2:接口不能写构造方法,抽象类可以写构造方法
    • Java 8 后接口也可以写实现方法了
    • 抽象类参与类的实例化过程,而接口则不是
  • 区别 3:抽象类可以有自己的各种成员变量,并通过自己的非抽象方法进行改变
    • 接口中的变量默认是 public static final 修饰,无法被自己和外部修改
  • 区别 4:接口可以实现多继承,抽象类只能单继承

类与对象的关系判断

我们可以使用如下方式判断类与对象的关系:

  • 使用 instanceOf 关键字判断一个对象是某类、接口的实例
  • 使用类的 isAssignableFrom 方法判断两个类或者接口之间的派生关系(基类或接口则返回 true)

equals 和 hashCode 的区别和联系

两者的区别与联系如下:

  • 两个对象用 equals() 比较返回 true,那么两个对象的 hashCode() 方法(用于构建 hash tables)必须返回相同的结果
    • 所以重写 equals() 方法,必须重写 hashCode() 方法
  • 两个对象用 equals() 比较返回 false,不要求 hashCode() 方法也一定返回不同的值,但最好返回不同值

对于第一点,如果只重写 equals()方法,就可能造成对象 equals 相等而 hashCode 不相等,最终导致 Hash* 相关的集合不能正常工作(无法正常插入与覆盖相同值的元素)

Java 字符串

String 类型

String 是字符串类,属于 Java 中的类,不属于基础数据类型,其常用的方法包括:

  • equals:比较是否相同
  • indexOf:返回指定字符索引
  • charAt:返回指定索引的字符
  • replace:字符串替换
  • trim:去除两端空白
  • split:分割字符串为数组
  • getBytes:获取 byte 类型数组
  • length:获取长度
  • toLowerCase:转换小写(大写为 Upper)
  • substring:截取字符串

String 常见操作

字符串反转操作

  • 最快方法:借助 StringBuilder 或 StringBuffer(线程安全)中的 reverse 方法
  • 借助字符串的 charAt 方法,从后到前遍历字符串,然后进行填充
  • 借助 Collections.reverse(List ...) 方法,把字符串转为 List 然后再反转

PS:Arrays.asList() 方法只能将对象数组转换为列表,不能将基本类型数组转换为列表

String str = "hello";
List<Character> list = new ArrayList<>();
for (char c : str.toCharArray()) {
list.add(c);
}

Collections.reverse(list);
System.out.println(list);

StringBuilder sb = new StringBuilder();
for (Character c : list) {
sb.append(c);
}
String str_new = sb.toString();
System.out.println(str_new);

字符串编码转换操作

通过 getBytes 方法获取字节数组,再使用带编码的 String 构造器:

String text1 = "测试";
String text2 = new String(text.getBytes(), "UTF-8");

字符串分割操作

字符串分割的方式有很多,以下列举几种:

  • 使用自身的 split 方法
  • 使用 JDK 的 StringTokenizer 工具类
  • 使用 Spring / Apache commons-lang 等工具包中的工具类
  • 自己利用 indexOf 方法写一个分割工具类

字符串工具类

字符串工具类常用的方法之一是判断字符串是否为空,具体有两个方法:

  • isEmpty:只要有一个任意字符(包括空白字符)就不为空
  • isBlank:判断字符串是否为空字符串,全部空白字符也为空

StringBuffer 与 StringBuilder

两者的区别如下:

  • 线程安全:StringBuffer 所有公开方法都是 synchronized 修饰
  • 缓存区:StringBuffer 每次 toString 都会基于缓存区进行构造,StringBuilder 直接复制字符数组构造
  • 性能:StringBuilder 没有对方法加锁同步,性能远大于 StringBuffer

使用结论:单线程选择 StringBuilder,多线程选择 StringBuffer

两者的默认容量大小和扩容机制相同:

  • 初始默认 16 个字符
  • 扩容机制:扩展为原来的 2 倍 + 2 个字符(如果容量不够,则直接扩容到给定长度)

Java 异常处理

Java 异常分类

Throwable 是 Java 异常的顶级类,ErrorException 是异常类的两个大分类

  • Error 是非程序异常,即程序不能捕获的异常,一般是编译或系统性的错误
  • Exception 是程序异常类,由程序内部产生,又分为运行时异常和非运行时异常

运行时异常的特点是 Java 编译器不会检查他,无需手动捕获与抛出,也会编译通过;非运行时异常是程序必须进行处理的异常,捕获或者抛出,不处理就不能编译通过。

Java 常见异常

Java 中经常遇到的异常如下:

  • NullPointerException:空指针异常
  • OutOfMemoryError:内存超出错误(非程序控制)
  • IOException:输入输出异常(受检查异常,需要手工捕获)
  • FileNotFoundException:文件不存在异常(受检查异常,需要手工捕获)
  • ClassNotFoundException:类找不到异常(受检查异常,需要手工捕获)
  • ClassCastException:类转换异常
  • NoSuchMethodException:方法不存在异常(受检查异常,需要手工捕获)
  • IndexOutOfBoundsException:索引越界异常
  • ArithmeticException:算术异常
  • SQLException:SQL 异常(受检查异常,需要手工捕获)

空指针异常

空指针异常是最常见的运行时异常之一,产生原因是调用 null 值对象的方法或变量。一些常见的避免 NPE 的方法如下:

  • 字符串比较,常量放前面或使用工具类
  • 对象初始化时提供默认值或默认构造实现
  • 返回集合时进行额外判断,如果为 null 则返回空集合
  • 使用断言(通常使用 Spring 的 Assert.notNull,一般测试使用)
  • 使用 Optional 对象,不再需要使用 !=null 进行判断,常见用法 Optional.ofNullable(xxx)

throw 和 throws 的区别

两者的区别如下:

  • throw 用在方法中,用来主动抛出一个异常
  • throws 用在方法声明中,声明方法可能会抛出的异常}

Java 集合

Java 多线程

Java IO

Java 虚拟机

本文大部分内容来自小程序 Java 面试库,仅做个人学习记录使用。