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) { |
Java 命名规范
标识符命名规则
Java 中的所有标识符(包括类、方法、变量、常量)的命名需要遵循如下规则:
- 标识符只能由字母、数字、下划线和美元符号组成
- 标识符不能以数字开头
- 标识符不能使用 Java 关键字
- 标识符区分大小写
类命名规范
类命名一般遵循大驼峰式风格:首字母大写,后面每个单词首字母大写。
PS:部分众所周知的缩写例外,如 DO、BO、DTO、VO 等。
方法命名规范
方法命名一般应符合小驼峰式风格:首字母小写,后面每个单词首字母大写。
变量命名规范
变量量包括方法参数名、成员变量、局部变量,一般应符合小驼峰式风格。
常量命名规范
与变量相比,常量赋值后不可改变,使用 final
关键字定义,可分为三种类型:
- 静态常量(类中)
- 成员常量(类中)
- 局部常量(类方法中)
静态常量名一般全部大写,单词间用下划线隔开。其他常量一般遵循小驼峰式风格。
Java 注释
Java 主要有三种注释:单行注释、块注释和文档注释:
private int id; // 这是 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 |
包装类的应用场景包括:
- 集合类泛型只能是包装类
- 成员变量不能有默认值(基本数据类型都有默认值)
- 方法参数允许定义空值
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
关键字主要有三种用法:
- 修饰类:表示该类不能被继承
- 修饰方法:表示该方法不能被重写
- 修饰变量:表示常量,只能赋值一次,不能被修改
注意与 finally
和 finalize
区分:
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 { |
而 +=
则自动包含了隐式强制类型转换。
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) { |
switch case 支持的数据类型
switch case 的语法格式如下:
switch(expression) { |
其中 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"; |
字符串编码转换操作
通过 getBytes
方法获取字节数组,再使用带编码的 String 构造器:
String text1 = "测试"; |
字符串分割操作
字符串分割的方式有很多,以下列举几种:
- 使用自身的
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 异常的顶级类,Error
和 Exception
是异常类的两个大分类
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 面试库,仅做个人学习记录使用。