注:本文部分内容源自Java 面试指南 | JavaGuide
一、基础概念与常识
1.JVM、JDK、JRE
JVM:Java Virtual Machine,虚拟机,运行Java字节码,实现Java的平台无关性
JDK:Java Development Kit,Java开发工具包,能够创建和编译 Java 程序。它包含了 JRE,同时还包含了编译 java 源码的编译器 javac 以及一些其他工具比如 javadoc(文档注释工具)、jdb(调试器)、jconsole(基于 JMX 的可视化监控⼯具)、javap(反编译工具)等
JRE:Java Runtime Environment,是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)
二、数据类型
1.基本数据类型
byte、short、int、long、float、double、char(2字节)、boolean
对应的包装类:Byte、Short、Integer、Long、Float、Double、Character、Boolean
2.基本类型和包装类型的区别
用途:除了定义一些常量和局部变量之外,在其他地方比如方法参数、对象属性中很少会使用基本类型。并且包装类型可用于泛型
存储方式:基本数据类型的局部变量存放在栈中的局部变量表中,成员变量(未被 static
修饰 )存放在堆中。包装类型属于对象类型,存放在堆中
占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小
默认值:成员变量包装类型不赋值就是 null
,而基本类型有默认值且不是 null
比较方式:对于基本数据类型来说,==
比较的是值。对于包装数据类型来说,==
比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals()
方法
3.自动装箱与拆箱
装箱其实就是调用了包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法
频繁拆装箱会严重影响系统的性能,应尽量避免不必要的拆装箱操作
private static long sum() {
// 应该使用 long 而不是 Long
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
4.包装类型的缓存机制
Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。
Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False,Float和Double没有实现缓存机制
Interger缓存源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static {
// high value may be configured by property
int h = 127;
}
}
Integer i1 = 40; // 等价于 Integer i1=Integer.valueOf(40)
Integer i2 = new Integer(40);
System.out.println(i1==i2); // 输出false
三、变量
1.为什么成员变量要赋默认值
安全性:如果没有默认值,成员变量将包含内存中的任意随机数据。这会导致程序在读取这些变量时出现不可预知的行为,可能引发难以调试的错误
局部变量未赋值编译报错,而成员变量赋值可能在运行时发生,难以检测
四、方法
1.重载与重写的区别
重载发生在编译期,是同一个类中多个同名方法根据不同参数列表来执行不同的逻辑处理
重写发生在运行期,是子类对父类允许访问的方法的实现过程进行重新编写
重写要求:
- 方法名相同、形参列表相同
- 返回值类型与父类相同或是其子类,方法声明抛出的异常类应比父类更小或相等
- 访问权限应比父类方法的访问权限更大或相等
2.Java有引用传递吗
Java 中将实参传递给方法的方式是 值传递:
- 如果参数是基本类型的话,传递的是基本类型的字面量值的拷贝,会创建副本。
- 如果参数是引用类型,传递的是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本
五、面向对象
1.接口和抽象类
- 设计目的:接口主要用于对类的行为进行约束,抽象类主要用于代码复用,强调所属关系
- 继承和实现:一个类只能继承一个类,可以实现多个接口,一个接口也可以继承多个其他接口
- 成员变量:接口中的成员变量只能是
public static final
类型的,抽象类的成员变量可以有任何修饰符(private
,protected
,public
),可以在子类中被重新定义或赋值 - 方法:
- Java 8 之前,接口中的方法默认是
public abstract
,也就是只能有方法声明。自 Java 8 起,可以在接口中定义default
(默认) 方法和static
(静态)方法。 自 Java 9 起,接口可以包含private
方法 - 抽象类可以包含抽象方法和非抽象方法。抽象方法没有方法体,必须在子类中实现。非抽象方法有具体实现,可以直接在抽象类中使用或在子类中重写
- Java 8 之前,接口中的方法默认是
2.引用拷贝、浅拷贝和深拷贝
// 假设有一个简单的类
class MyObject {
int value;
SubObject subObject;
}
MyObject originalObject = new MyObject();
// 引用拷贝
MyObject copy = originalObject;
// 浅拷贝:只拷贝了顶层对象,内部的 SubObject 仍然是引用拷贝
// clone()是Object类提供的方法
MyObject shallowCopy = originalObject.clone();
// 深拷贝:需要手动实现,递归地拷贝所有引用类型成员
MyObject deepCopy = new MyObject();
deepCopy.value = originalObject.value;
deepCopy.subObject = new SubObject(originalObject.subObject);
3.Object类常见方法
/**
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
*/
public final native Class<?> getClass()
/**
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
*/
public boolean equals(Object obj)
/**
* native 方法,用于创建并返回当前对象的一份拷贝。
*/
protected native Object clone() throws CloneNotSupportedException
/**
* 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
*/
public String toString()
/**
* native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
*/
public final native void notify()
/**
* native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
*/
public final native void notifyAll()
/**
* native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
*/
public final native void wait(long timeout) throws InterruptedException
/**
* 多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。。
*/
public final void wait(long timeout, int nanos) throws InterruptedException
/**
* 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
*/
public final void wait() throws InterruptedException
/**
* 实例被垃圾回收器回收的时候触发的操作
*/
protected void finalize() throws Throwable { }
4.==、equals()、hashCode()
==:比较基本数据类型的值,引用数据类型的内存地址
equals():类没有重写该方法则等价于“==”,一般重写为比较对象内容是否相等
hashCode():如果需要把对象放入哈希表(HashSet、HashMap)中需要重写,否则该方法无用
六、String
1.String、StringBuffer和StringBuilder
可变性:String不可变
线程安全性:String为常量,StringBuffer方法中有同步锁,StringBuilder线程不安全
性能:String改变时会生成新的对象,StringBuffer、StringBuilder修改自身(前者有同步锁性能略低)
使用场景:
- 操作少量的数据: 适用
String
- 单线程操作字符串缓冲区下操作大量数据: 适用
StringBuilder
- 多线程操作字符串缓冲区下操作大量数据: 适用
StringBuffer
2.为什么String是不可变的
- 保存字符串的数组被
final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的方法。 String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变
3.字符串常量池
字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建
4.String s1 = new String("abc");这句话创建了几个字符串对象?
如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中,反之会创建一个字符串对象
5.intern()方法
String.intern()
是一个 native(本地)方法,其作用是将指定的字符串对象的引用保存在字符串常量池中
// 在堆中创建字符串对象”Java“
// 将字符串对象”Java“的引用保存在字符串常量池中
String s1 = "Java";
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s2 = s1.intern();
// 会在堆中在单独创建一个字符串对象,不会放入常量池
String s3 = new String("Java");
// 直接返回字符串常量池中字符串对象”Java“对应的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一个对象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的对象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一个对象
System.out.println(s1 == s4); //true
6.String 类型的变量和常量做“+”运算时发生了什么?
对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存入字符串常量池,这个得益于编译器的优化
字符串不加 final
关键字拼接时,编译器视作变量,实际运算是
String str3 = new StringBuilder().append(str1).append(str2).toString();
字符串不加 final
关键字拼接时,编译器视作常量,直接进行字符串拼接并存入常量池
七、异常
1.异常分类
Exception
:程序本身可以处理的异常,可以通过catch
来进行捕获Error
:Error
属于程序无法处理的错误 ,不建议通过catch
捕获
Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch
或者throws
关键字处理的话,就没办法通过编译
Unchecked Exception 即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译
RuntimeException
及其子类都统称为非受检查异常,常见的有:
NullPointerException
(空指针错误)IllegalArgumentException
(参数错误比如方法入参类型错误)NumberFormatException
(字符串转换为数字格式错误,IllegalArgumentException
的子类)ArrayIndexOutOfBoundsException
(数组越界错误)ClassCastException
(类型转换错误)ArithmeticException
(算术错误)SecurityException
(安全错误比如权限不够)UnsupportedOperationException
(不支持的操作错误比如重复创建同一用户)
2.Throwable类常用方法
String getMessage()
: 返回异常发生时的简要描述String toString()
: 返回异常发生时的详细信息String getLocalizedMessage()
: 返回异常对象的本地化信息。使用Throwable
的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()
返回的结果相同void printStackTrace()
: 在控制台上打印Throwable
对象封装的异常信息
3.try-catch-finally
try
块:用于捕获异常。其后可接零个或多个catch
块,如果没有catch
块,则必须跟一个finally
块catch
块:用于处理 try 捕获到的异常finally
块:无论是否捕获或处理异常,finally
块里的语句都会被执行。当在try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行
面对必须要关闭的资源,应该优先使用 try-with-resources
而不是try-finally
。随之产生的代码更简短,更清晰,产生的异常对我们也更有用
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
int b;
while ((b = bin.read()) != -1) {
bout.write(b);
}
}
catch (IOException e) {
e.printStackTrace();
}
八、泛型
1.泛型介绍
泛型一般有三种:泛型类、泛型接口、泛型方法
// 泛型类
public class Generic<T>{
private T key;
// 泛型方法
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
}
// 泛型接口
public interface Generator<T> {
public T method();
}
注意: public static < E > void printArray( E[] inputArray )
一般被称为静态泛型方法;在 java 中泛型只是一个占位符,必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法无法使用类上声明的泛型
2.项目中哪里用到了泛型
九、反射
1.动态代理的实现
public class DebugInvocationHandler implements InvocationHandler {
/**
* 代理类中的真实对象
*/
private final Object target;
public DebugInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
System.out.println("before method " + method.getName());
Object result = method.invoke(target, args);
System.out.println("after method " + method.getName());
return result;
}
}
2.反射的优缺点
优点:可以动态的创建和使用对象,代码更加灵活、为各种框架提供开箱即用的功能提供了便利
缺点:增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射基本是解释执行,性能稍差,不过对于框架来说影响不大
3.反射相关API
// 获取 Class 对象的四种方法
Class alunbarClass = TargetObject.class;
Class alunbarClass1 = Class.forName("cn.javaguide.TargetObject"); // 全类名
TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();
Class alunbarClass3 = ClassLoader.getSystemClassLoader().loadClass("cn.javaguide.TargetObject");
// 获取类的信息
String packageName = alunbarClass.getPackage().getName(); // 包名
String fullClassPath = alunbarClass.getName(); // 全类名
TargetObject targetObiect = alunbarClass.newInstance(); // 创建对象实例
Field field = alunbarClass.getField(fieldName); // 根据属性名获取public属性
Field[] fields = alunbarClass.getFields(); // 获取所有属性
Field field = alunbarClass.getDeclaredField(fieldName); // 根据属性名获取任意权限的属性
field.setAccessible(true); // 取消安全检查,即使field是私有的也可以访问
field.getName(); // 获取属性名
field.get(targetObject); // 获取targetObject实例对象中field属性的属性值
field.set(targetObject, fieldValue); // 为targetObject实例对象中field属性赋值
// Method、Constructor类与Field类相关操作基本相同
// 执行method方法
// 如果方法有返回值,统一返回Object,但运行类型与方法定义的返回类型一致
method.invoke(targetObject, args1, args2);
十、注解
1.注解如何生效
注解只有被解析之后才会生效,常见的解析方法有两种:
- 编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 - 运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的
@Value
、@Component
)都是通过反射来进行处理的。
十一、序列化和反序列化
- 序列化:将数据结构或对象转换成二进制字节流的过程
- 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中
1.JDK 自带的序列化方式
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@ToString
// 只需实现 Serializable 接口
public class RpcRequest implements Serializable {
private static final long serialVersionUID = 1905122041950251207L;
private String requestId;
private String interfaceName;
private String methodName;
private Object[] parameters;
private Class<?>[] paramTypes;
private RpcMessageTypeEnum rpcMessageTypeEnum;
}
序列化号 serialVersionUID
用于版本控制。反序列化时,会检查 serialVersionUID
是否和当前类的 serialVersionUID
一致。若不一致会抛出 InvalidClassException
异常
强烈推荐每个序列化类都手动指定其 serialVersionUID(
需要使用 static
和 final
关键字)
2.如果有些字段不想序列化怎么办?
使用 transient
关键字修饰,阻止变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。
transient
只能修饰变量,不能修饰类和方法transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值static
变量因为不属于任何对象(Object),所以无论有没有transient
关键字修饰,均不会被序列化
3.为什么不推荐使用 JDK 自带的序列化?
- 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了
- 性能差:主要原因是序列化之后的字节数组体积较大,导致传输成本加大
- 存在安全问题:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码
十二、I/O
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
InputStream
/Reader
: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。OutputStream
/Writer
: 所有输出流的基类,前者是字节输出流,后者是字符输出流
1.I/O流为什么要划分字节流和字符流
问题本质是:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
主要有两点原因:
- 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时
- 如果我们不知道编码类型的话,使用字节流的过程中很容易出现乱码问题
2.Java I/O 中的设计模式有哪些
十三、语法糖
1.常见语法糖
泛型、自动拆装箱、变长参数、枚举、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式