面试常见问题第三部分
Java 基础
1.Java的异常体系
以 Throwable
为根,分为两大类,分别是Error
和 Exception
。
-
RuntimeException
的名字有点误导,其实其他异常也是运行时产生的,他表示的实际含义是未受检异常,相对而言,其他异常都是受检异常,Error
及其子类也是受检异常。 - 对于受检异常,Java 会强制要求程序员去处理,否则会有编译错误,而对于未受检异常,就没有这个限制
2.什么是异常链
将捕获的异常包装进一个新的异常中并重新抛出的异常处理方式。
- 首先创建一个自己的异常类
public class MyException extends Exception {
public MyException(String message) {
super(message);
}
public MyException(){}
}
- 测试异常链
public class ExceptionChain {
/**
* Test1():抛出 “喝晕了” 异常
* Test2():调用Test1(),捕获 “喝晕了” 异常,并且包装成运行时异常,继续抛出
* main() 方法中,调用 Test2(),捕获 Test2() 方法抛出的异常
*/
public void Test1() throws MyException {
throw new MyException("喝车不开酒!");
}
public void Test2() {
try {
Test1();
} catch (MyException e) {
RuntimeException newException = new RuntimeException("司机一滴酒,亲人两行泪!");
newException.initCause(e);
throw newException;
}
}
public static void main(String[] args) {
ExceptionChain ec = new ExceptionChain();
try {
ec.Test2();
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 运行结果
3.Java 中实现多态的机制是什么
首先回忆一下什么是多态?
子类对象赋值给父类变量,但运行时依然表现出子类的行为特征,意味着同一类型的对象在执行过程中可能表现出不同行为特征。
记住一句话:编译看父类,运行看子类。
实现多态的机制:子类继承父类后,对父类的方法可以有两种行为,一种是重载,一种是重写。对于重写(运行时多态,就是真正的多态),使用父类变量调用该方法时,表现出子类重新定义的行为特征;对于重载(编译时多态,不是真正的多态),因为父类中没有这个方法,所以在编译阶段会出错,父类变量无法调用一个在父类中没有的方法。
如果想要调用子类中特有的方法,需要向下转型。
4.说一下泛型原理,并举例说明
泛型就是类型参数化,处理的数据类型是不固定的,而是可以作为参数传入的。
- 基本原理
Java 有编译器和虚拟机,编译器将 Java 源代码转换成为.class
文件,虚拟机加载并允许.class
文件。对于泛型类,Java 编译器会将泛型代码转换为普通的非泛型代码,将类型参数 T 擦除,替换为 Object,插入必要的强制类型转换。Java 虚拟机并不知道泛型这一回事儿,只知道普通的类及代码。
Java 泛型是通过类型擦除来实现的,类定义中的类型参数 T 会被替换成 Object,在程序运行过程中,不知道泛型的实际类型参数,比如Pair<Integer>
,运行只知道Pair
,不知道Integer
。
如果指定了泛型的边界,那么在进行泛型擦除的时候,就不会转换成Object
,而是转换为边界类型。 - 举例说明
public class Pair<U, V> {
private U name;
private V age;
public Pair(U name, V age) {
this.name = name;
this.age = age;
}
public U getName() {
return name;
}
public V getAge() {
return age;
}
}
在经过 Java 编译之后,所以泛型类型 U,V
都转换成 Object
。在之后需要使用到的地方,都需要进行强制类型转换
Pair<String, Integer> p = new Pair<>("Hello", 20);
String name = (String)p.getName();
Integer age = (Integer)p.getAge();
5.Java 中 String 的了解
- 了解基本用法,基本的函数调用
- String 内部使用一个字符数组表示字符串,实例变量定义为:(JDK 1.8 之前)
private final char value[];
- 从 JDK 1.8 开始,内部使用一个字节数组来表示字符串,实例变量定位为:
private final byte value[];
使用字节数组,如果字符都是 ASCII 字符,他就可以使用一个字节表示一个字符,而不是 UTF-16BE 编码,节省空间; - 不可变性,一旦创建就不可变;
- 常量字符串,可以参考 part2 部分关于
String.intern();
方法的讲解。 - 重写 hashCode 方法,参考 part1 关于 hashCode 的讲解。
- 运用在正则表达式中
6.String 为什么要设计成不可变的?
- 保证安全;
- 保证性能;
- 线程安全
7.序列化的方式
序列化就是将对象转换为字节流,反序列化就是将字节流转换为对象。
- 基本用法
要让一个类支持序列化,要让他实现一个接口java.io.Serializable
。对于一个学生类,我们可以如下操作使其支持序列化public class Student implement Serializable{ // 省略}
。声明之后,保存/读取Student
对象就可以通过使用 ObjectOutputStream/ObjectInputStream
流了。ObjectOutputStream
是 OutputStream
的子类,但是实现了ObjectOutput
接口。ObjectOutput
是 DataOutput
的子接口,增加一个方法public void writeObject(Object obj) throws IOException
。ObjectInputStream
同理,增加一个方法public void readObject() throws ClassNotFoundException, IOException
。
- 保存学生列表的代码
public static void writeStudents(List<Student> students)
throws IOException{
ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("students.dat")));
try {
//out.writeInt(students.size());
//for (Student s : students) {
// out.writeObject(s);
//}
out.writeObject(students);
} finally {
out.close();
}
}
- 从文件中读入学生列表
public static List<Student> readStudents() throws ClassNotFoundException, IOException {
ObjectInputStream in = new ObjectInputStream(
new BufferedInputStream(new FileInputStream("students.dat")));
try {
//int size = in.readInt();
//List<Student> list = new ArrayList<>(size);
//for (int i = 0;i < size;i ++) {
// list.add( (Student) in.readObject());
//}
//return list;
return (List<Student> in.readObject());
} finally {
in.close();
}
}
8.如何格式化日期?
在 Java 8 中,主要的格式化类是java.time.format.DateTimeFormatter
,他是线程安全的。之前用的 java.text.SimpleDateFormat
不是线程安全的。
- 看一段代码,演示基本使用方法
public class DateOperation {
public static void main(String[] args) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime ldt = LocalDateTime.of(2019,12,6,21,29,30);
System.out.println(formatter.format(ldt));
}
}
// 输出
// 2019-12-06 21:29:30
- 也可以将字符串转换为日期和时间对象,可以使用对应类的
parse
方法,如下:
public class DateOperation {
public static void main(String[] args) {
String string = "2019-12-06 21:29:30";
str2DateTime(string);
}
public static void str2DateTime(String string) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime ldt = LocalDateTime.parse(string, formatter);
System.out.println(ldt.toString());
}
}
// 输出
// 2019-12-06T21:29:30
9.静态代理和动态代理的区别,什么场景使用?
参考:JAVA学习篇–静态代理VS动态代理
动态代理: 一种强大的功能,他可以在运行时动态创建一个类,实现一个或多个接口,可以在不修改原有类的基础上动态为通过该类获取的对象添加方法、修改行为。是**面向切面编程(AOP)**的基础。有两种方式,一种是 Java SDK 提供的,还有一种是第三方库如 cglib。
- 静态代理: 由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的
.class
文件就已经存在
- 优点: 代理使客户端不需要知道实现类是什么,怎么做的,只需要知道代理即可。
- 缺点: 代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法,出现大量的重复代码
- 缺点: 代理对象只服务于一种类型的对象,如果要服务多类型的对象,就需要为每一种对象都进行代理
- 静态代理类只能为特定的接口服务 ,如果想要为多个接口服务就要建立很多个代理类
- 动态代理: 在程序员运行时运用反射机制动态创建而成。
- 与静态代理相比,最大的优点就是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理
InvocationHandler.invoke
。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。类职责更加单一,复用性更强。 - 使用静态代理,可以编写通用的代理逻辑,用于各种类型的被代理对象,而不需要为每一个被代理的类型都创建一个静态代理。
10.反射的原理,反射创建类实例的三种方式是什么
1.反射的原理
每个已加载的类在内存中都有一份类信息,每个对象都有指向它所属类信息的引用。类信息对应的类就是
java.lang.Class
。
2.三种方式
- 使用
Object
类中的getClass()
方法;
Student stu = new Student();
Class stuClass = stu.getClass();
- 任何数据类型(包括基本数据类型)都有一个 “静态” 的
class
属性
Student stu = new Student();
Class stuClass2 = Student.class;
- 通过
Class
类的静态方法:forName(String className)
(常用)
try {
Class stuClass3 = Class.forName("包名.Student");
} catch(ClassNotFoundException e) {
e.printStackTree;
}
11.说说对 Java 反射的理解
反射是指在运行时,程序可以动态获取类型的信息,比如接口信息、成员信息、方法信息、构造方法信息等,根据这些动态获取的信息创建对象、访问或修改对象,调用方法。
反射就是把Java类中的各种成分映射成一个个的Java对象
Class 对象的由来是将class文件读入内存,并为之创建一个Class对象
参考如图:Java 基础之-反射
12.说说对 Java 注解的理解
注解就是给程序添加一些信息,用字符 @ 开头,这些信息用于修饰它后面紧挨着的其他代码元素,比如类、接口、字段、方法、方法中的参数、构造方法等。注解可以被编译器、程序运行时和其他工具使用,用于增强或修改程序行为。定制序列化和依赖注入容器。
- 内置注解
@Override
,@Deprecated
,@SuppressWarnings(抑制的警告类型)
。 - 定制序列化和依赖注入容器是两种应用。
- 注解提升了Java语言的表达能力,有效实现了应用功能和底层功能的分离,框架和库的程序员可以专注于底层实现,借助反射实现通用功能,提供注解给程序员使用,应用程序员可以专注于应用功能,通过简单的声明式注解与框架或库进行协作。
- 可以帮助程序员执行基本编译时检查,如
@Override
的使用 - 有四个用于修饰自定义注解的元注解:
@Target
表示注解目标,@Retention
表示注解信息保留到什么时候 ,@Documented
注解信息包含到生成的文档中,@Inherited