📃个人主页:个人主页
🔥系列专栏:Java面试专题
目录
1.接口和抽象类有什么区别?
2.两个对象的 hashCode() 相同,则 equals()也一定为 true,对吗?
3.说一说hashCode()和equals()的关系
4.为什么要重写hashCode()和equals()?
5.==和equals()有什么区别?
6.简单聊聊什么是深拷贝?什么是浅拷贝?
7.简单讲讲你对 Java 中的异常的理解?
8.遇到过异常吗,如何处理?
9.说一说Java的异常机制
10.finally是无条件执行的吗?
11.在finally中return会发生什么?
12.什么是自动拆箱、什么是自动装箱?
13.说一说自动装箱、自动拆箱的应用场景
14.为什么Java代码可以实现一次编写、到处运行?
15.为啥要有包装类?
1.接口和抽象类有什么区别?
- 接口的方法默认是 public,所有的方法在接口中不能有实现(在 JDK8 时接口可以有默认方法和静态方法),抽象方法可以被 public、protected、defalut 修饰,但是不能被 private 和 final 修饰,抽象类可以有非抽象的方法。
- 接口中除了 static 和 final 变量以外不能有其他变量,抽象类中不一定。
- 一个类可以实现多个接口,但是只能实现一个抽象类,因为抽象类首先是一个类。
- 抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。同样,一个实现接口的时候,如不能全部实现接口方法,那么该类也只能为抽象类。
🍉扩展阅读🍉
接口和抽象类都是 Java 语言中用来对架构进行设计的工具。
修饰方式的不同
接口通过 interface 关键字声明,接口通常使用 public 修饰,也允许默认修饰符,只限本包访问。通常情况下接口都会使用 public 修饰。所有的方法在接口中不能有实现(在 JDK8 时接口可以有默认方法和静态方法);
public interface InterfaceTest {
void getUser();
}
抽象方法可以被 public、protected、default 修饰,但是不能被 private 和 final 修饰,抽象类可以有非抽象的方法;
public abstract class AbstractTest {
abstract void doSomething();
}
变量的不同
接口中声明的变量全是静态常量;
抽象类中可以有不同类型的变量。
实现的不同
一个类可以实现多个接口
public class Basic implements Serializable,InterfaceTest {
public void getUser() {
}
}
但是只能继承一个抽象类,因为抽象类首先是一个类。
public class Basic extends AbstractTest {
@Override
void doSomething() {
}
}
方法是否需要实现
抽象类里的抽象方法必须全部被子类所实现,如果子类不能全部实现父类抽象方法,那么该子类只能是抽象类。
接口里的接口方法如果不能被全部实现,那么该类也只能为抽象类。
2.两个对象的 hashCode() 相同,则 equals()也一定为 true,对吗?
不对,hashcode 其实就是对一个对象中的每个元素进行一次运算生成的结果值,两个不同的对象是有可能出现同一个 hash 值的。 Ma 和 NB 两个字符串不同,但是他们有相同的 hashcode 值。
🍉扩展阅读🍉
首先来了解一下 hashCode() 方法是干什么的,hashCode() 的存在主要是用于查找的快捷,比如常用的 HashMap 等集合,hashCode() 用来在散列的存储结构中确定对象的存储地址。
Java 中所有的对象都有一个父类 Object,而 Object 有一个 hashCode 方法,Java 的所有类都有 hashCode() 方法。
来看看 hashcode 的计算方法:
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
hashcode 其实就是对一个对象中的每个元素进行一次运算生成的结果值,两个不同的对象是有可能出现同一个 hash 值的。
比如下面两个字符串:
String s1="Ma";
String s2="NB";
System.out.println(s1.hashCode()==s2.hashCode()); //true
System.out.println(s1.equals(s2)); //false
虽然两个 Ma 和 NB 两个字符串不同,但是他们有相同的 hashcode 值 2484。
创建实体类的时候如果要使用 hashCode() 方法或 equals 方法时需要在实体类中重写:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(name, user.name) &&
Objects.equals(age, user.age);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
同理,实体对象同样存在两个不同对象存在同一个 hash 值的情况。
3.说一说hashCode()和equals()的关系
hashCode()用于获取哈希码(散列码),eauqls()用于比较两个对象是否相等,它们应遵守如下规定:
-
如果两个对象相等,则它们必须有相同的哈希码。
-
如果两个对象有相同的哈希码,则它们未必相等。
🍉扩展阅读🍉
在Java中,Set接口代表无序的、元素不可重复的集合,HashSet则是Set接口的典型实现。
当向HashSet中加入一个元素时,它需要判断集合中是否已经包含了这个元素,从而避免重复存储。由于这个判断十分的频繁,所以要讲求效率,绝不能采用遍历集合逐个元素进行比较的方式。实际上,HashSet是通过获取对象的哈希码,以及调用对象的equals()方法来解决这个判断问题的。
HashSet首先会调用对象的hashCode()方法获取其哈希码,并通过哈希码确定该对象在集合中存放的位置。假设这个位置之前已经存了一个对象,则HashSet会调用equals()对两个对象进行比较。若相等则说明对象重复,此时不会保存新加的对象。若不等说明对象不重复,但是它们存储的位置发生了碰撞,此时HashSet会采用链式结构在同一位置保存多个对象,即将新加对象链接到原来对象的之后。之后,再有新添加对象也映射到这个位置时,就需要与这个位置中所有的对象进行equals()比较,若均不相等则将其链到最后一个对象之后。
4.为什么要重写hashCode()和equals()?
Object类提供的equals()方法默认是用==来进行比较的,也就是说只有两个对象是同一个对象时,才能返回相等的结果。而实际的业务中,我们通常的需求是,若两个不同的对象它们的内容是相同的,就认为它们相等。鉴于这种情况,Object类中equals()方法的默认实现是没有实用价值的,所以通常都要重写。
由于hashCode()与equals()具有联动关系(参考“说一说hashCode()和equals()的关系”一题),所以equals()方法重写时,通常也要将hashCode()进行重写,使得这两个方法始终满足相关的约定。
5.==和equals()有什么区别?
==运算符:
-
作用于基本数据类型时,是比较两个数值是否相等;
-
作用于引用数据类型时,是比较两个对象的内存地址是否相同,即判断它们是否为同一个对象;
equals()方法:
-
没有重写时,Object默认以 == 来实现,即比较两个对象的内存地址是否相同;
-
进行重写后,一般会按照对象的内容来进行比较,若两个对象内容相同则认为对象相等,否则认为对象不等。
6.简单聊聊什么是深拷贝?什么是浅拷贝?
开发过程中,有时会遇到把现有的一个对象的所有成员属性拷贝给另一个对象的需求。如果只是单纯使用 clone 方法进行拷贝,只拷贝引用地址的动作就是浅拷贝。相反,如果拷贝一个对象时不是简单的将地址引用拷贝出来,而是新建了一个对象,这种方式就是深拷贝。
🍉扩展阅读🍉
开发过程中,有时会遇到把现有的一个对象的所有成员属性拷贝给另一个对象的需求。这个时候就会用到拷贝这个概念。把原对象定义成 A,拷贝后的对象定义成 B,如果只是单纯使用 clone 方法进行拷贝,你会发现:
-
对于八个基本类型,会拷贝其值,并且 B 的改变不会影响 A。
-
如果是一个对象,拷贝的是地址引用,也就是说此时新拷贝出的对象与原有对象共享该实例变量,不受访问权限的限制。B 对该值的改变会影响 A。
-
对于 String 字符串,这个比较特殊,虽然拷贝的也是引用,但是在修改的时候,它会从字符串池中重新生成新的字符串,原有的字符串对象保持不变。
这种只单纯拷贝引用地址的动作就是浅拷贝。
相反,如果拷贝一个对象时不是简单的将地址引用拷贝出来,而是新建了一个对象,这种方式就是深拷贝。
浅拷贝代码模拟
通过代码模拟浅拷贝的过程:
首先,新建两个实体类,学生和老师:
public class Teacher {
private int id;
private String name;
public Teacher(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public void setId(int id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Teacher{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
接下来是学生,学生的实体类需要实现 clone
public class Student implements Cloneable {
private int id;
private String name;
private Teacher teacher;
public Student(int id, String name, Teacher teacher) {
this.id = id;
this.name = name;
this.teacher = teacher;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", teacher=" + teacher +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
接下来就来看一下浅拷贝的效果
public static void main(String[] args) throws CloneNotSupportedException {
//新建一个student1
Student student1=new Student(1,"javayz",new Teacher(1,"teacher1"));
//student2从student1中克隆过去
Student student2= (Student) student1.clone();
//修改基本类型 int
student2.setId(2);
//修改String
student2.setName("javayz2");
//修改对象类型teacher
Teacher teacher = student2.getTeacher();
teacher.setName("teacher2");
System.out.println(student1); //{id=1,name='javayz',teacher=Teacher{id=1,name='teacher2'}}
System.out.println(student2); //{id=2,name='javayz2',teacher=Teacher{id=1,name='teacher2'}}
}
通过结果就可以发现,修改被克隆对象的基本类型和 String 类型不会对原来数据造成影响,但是由于用的是同一个引用地址,修改对象时两边都会被修改。
深拷贝代码模拟
深拷贝的其中一个方法是把被拷贝对象中的所有引用类型也都实现深拷贝,最后逐层拷贝实现引用地址是新的而不是用的同一个。
修改上面的 teacher 对象代码,实现 clone 方法
public class Teacher implements Cloneable{
private int id;
private String name;
//省略构造方法、get、set、toString方法
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
修改 student 类的 clone 方法
@Override
protected Object clone() throws CloneNotSupportedException {
Student student = (Student) super.clone();
student.teacher= (Teacher) teacher.clone();
return student;
}
然后执行同样的测试代码后就会发现两个对象已经互相不影响了。
第二个方法是利用 serializable 实现深拷贝,这种方式的原理在于通过 IO 流的方式先将序列化后的对象写进 IO 流中,再取出来实现深拷贝。这种方式下所有涉及到的类都必须实现 Serializable 接口
新建一个 DeepStudent 类,同时需要使得 Teacher 类也实现 Serializable 接口
public class DeepStudent implements Serializable {
private static final long serialVersionUID=1L;
private int id;
private String name;
private Teacher teacher;
//省略构造方法、get、set、toString方法
public Object deepCopy(){
try {
//将对象写到IO流中
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(this);
//再从IO流中获取到对象
ByteArrayInputStream bis=new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois=new ObjectInputStream(bis);
return ois.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
编写一个测试方法:
public static void main(String[] args) throws CloneNotSupportedException {
//新建一个student1
DeepStudent student1=new DeepStudent(1,"javayz",new Teacher(1,"teacher1"));
//student2从student1中克隆过去
DeepStudent student2= (DeepStudent) student1.deepCopy();
//修改基本类型 int
student2.setId(2);
//修改String
student2.setName("javayz2");
//修改对象类型teacher
Teacher teacher = student2.getTeacher();
teacher.setName("teacher2");
System.out.println(student1);
System.out.println(student2);
}
7.简单讲讲你对 Java 中的异常的理解?
Java 中的异常都来自于 java.lang.Throwable 类,从异常类型上分,Java 中的异常可以分为 Exception 和 Error。Exception 异常可以被程序本身处理,Error 无法被程序处理。
Exception 异常又可以分为受检查异常和不受检查异常,所谓受检查异常是指那些在编程期间就需要把异常 try/catch
或 throws
出来的异常,不受检查异常是指在编程期间不需要通过代码来显式地 catch 出来。
不受检查异常往往是程序员的代码逻辑疏忽导致,比如空指针异常,只需要在调用对象之前判断是否是空对象就可避免;下标越界异常只需要保证永远在下标范围内访问即可避免。
受检查异常必须在代码中通过 try/catch 或者 throws 才能通过编译。
🍉扩展阅读🍉
Java 中的异常介绍
Java 中的异常都来自于 java.lang.Throwable 类,从异常类型上分,Java 中的异常可以分为 Exception 和 Error。Exception 异常可以被程序本身处理,Error 无法被程序处理。
Exception 异常又可以分为受检查异常和不受检查异常,所谓受检查异常是指那些在编程期间就需要把异常 try/catch 或 throws 出来的异常,不受检查异常是指在编程期间不需要通过代码来显式地 catch 出来。下图展示异常的类型:
如何处理异常
Error 类型的异常属于尽量去避免而非在代码里去处理的异常,因此我们在处理异常章节主要介绍 Exception 类型的异常。
不受检查异常往往是程序员的代码逻辑疏忽导致,比如空指针异常,只需要在调用对象之前判断是否是空对象就可避免;下标越界异常只需要保证永远在下标范围内访问即可避免。
受检查异常必须在代码中通过 try/catch 或者 throws 才能通过编译,比如下面这段读取文件的代码:
public class ExceptionTest{
public void readFile() throws FileNotFoundException {
FileInputStream fileInputStream=new FileInputStream("xxx.text");
}
}
当使用 new FileInputStream("xxx.text")获取文件时,FileNotFoundException 属于受检查异常,就必须抛出或者被捕获。除了 throws 之外,还可以使用 try/catch
public class ExceptionTest{
public void readFile() {
try {
FileInputStream fileInputStream=new FileInputStream("xxx.text");
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
}
}
}
try/catch 与 throws 的区别
受检查异常的捕获有两种方式:try/catch 和 throws。区别在于,try/catch 捕获到异常后直接在 catch 语句块里进行处理,处理完成之后程序继续往下执行。而 throws 则是将异常抛给它的上一级进行处理,程序就不会再往下执行了。
使用 catch 捕获到异常之后,可以对异常进行处理,常见的方式就是将异常打印出来或者写入到日志里。下面是 Throwable 类中的常用方法。
public String getMessage() //返回异常的简要信息
public String getLocalizedMessage() //返回异常的本地化信息,默认和getMessage是一样的,如果要加入本地化信息往往是子类重写这个方法
public String toString() //返回异常的更详细信息
public void printStackTrace() //在控制台输出异常信息。
try/catch 之后往往还会跟 finally 语句块,finally 语句块中的代码会在程序处理完成最后被执行,不管是否进入异常。如果在代码块中 有 return 语句, 在 return 执行之前,finally 语句块中的代码会被先执行。 finally 语句的返回值将会覆盖原始的返回值。
8.遇到过异常吗,如何处理?
在Java中,可以按照如下三个步骤处理异常:
-
捕获异常
将业务代码包裹在try块内部,当业务代码中发生任何异常时,系统都会为此异常创建一个异常对象。创建异常对象之后,JVM会在try块之后寻找可以处理它的catch块,并将异常对象交给这个catch块处理。
-
处理异常
在catch块中处理异常时,应该先记录日志,便于以后追溯这个异常。然后根据异常的类型、结合当前的业务情况,进行相应的处理。比如,给变量赋予一个默认值、直接返回空值、向外抛出一个新的业务异常交给调用者处理,等等。
-
回收资源
如果业务代码打开了某个资源,比如数据库连接、网络连接、磁盘文件等,则需要在这段业务代码执行完毕后关闭这项资源。并且,无论是否发生异常,都要尝试关闭这项资源。将关闭资源的代码写在finally块内,可以满足这种需求,即无论是否发生异常,finally块内的代码总会被执行。
9.说一说Java的异常机制
关于异常处理:
在Java中,处理异常的语句由try、catch、finally三部分组成。其中,try块用于包裹业务代码,catch块用于捕获并处理某个类型的异常,finally块则用于回收资源。当业务代码发生异常时,系统会创建一个异常对象,然后由JVM寻找可以处理这个异常的catch块,并将异常对象交给这个catch块处理。若业务代码打开了某项资源,则可以在finally块中关闭这项资源,因为无论是否发生异常,finally块一定会执行。
关于抛出异常:
当程序出现错误时,系统会自动抛出异常。除此以外,Java也允许程序主动抛出异常。当业务代码中,判断某项错误的条件成立时,可以使用throw关键字向外抛出异常。在这种情况下,如果当前方法不知道该如何处理这个异常,可以在方法签名上通过throws关键字声明抛出异常,则该异常将交给JVM处理。
关于异常跟踪栈:
程序运行时,经常会发生一系列方法调用,从而形成方法调用栈。异常机制会导致异常在这些方法之间传播,而异常传播的顺序与方法的调用相反。异常从发生异常的方法向外传播,首先传给该方法的调用者,再传给上层调用者,以此类推。最终会传到main方法,若依然没有得到处理,则JVM会终止程序,并打印异常跟踪栈的信息
10.finally是无条件执行的吗?
不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。
注意事项
如果在try块或catch块中使用 System.exit(1); 来退出虚拟机,则finally块将失去执行的机会。但是我们在实际的开发中,重来都不会这样做,所以尽管存在这种导致finally块无法执行的可能,也只是一种可能而已。
11.在finally中return会发生什么?
在通常情况下,不要在finally块中使用return、throw等导致方法终止的语句,一旦在finally块中使用了return、throw语句,将会导致try块、catch块中的return、throw语句失效。
详细解析
当Java程序执行try块、catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含finally块,如果没有finally块,程序立即执行return或throw语句,方法终止;如果有finally块,系统立即开始执行finally块。只有当finally块执行完成后,系统才会再次跳回来执行try块、catch块里的return或throw语句;如果finally块里也使用了return或throw等导致方法终止的语句,finally块已经终止了方法,系统将不会跳回去执行try块、catch块里的任何代码。
12.什么是自动拆箱、什么是自动装箱?
自动装箱是指将基本类型用对应的应用类型包装起来,自动拆箱是指将包装类型转化为对应的引用类型。以 Integer 为例,装箱的时候调用的是 Integer.valueOf(int)方法,拆箱的时候用的是 Integer 的 intValue 方法,算术运算会触发自动拆箱。
13.说一说自动装箱、自动拆箱的应用场景
自动装箱、自动拆箱是JDK1.5提供的功能。
自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;
自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;
通过自动装箱、自动拆箱功能,可以大大简化基本类型变量和包装类对象之间的转换过程。比如,某个方法的参数类型为包装类型,调用时我们所持有的数据却是基本类型的值,则可以不做任何特殊的处理,直接将这个基本类型的值传入给方法即可。
14.为什么Java代码可以实现一次编写、到处运行?
JVM(Java虚拟机)是Java跨平台的关键。
在程序运行前,Java源代码(.java)需要经过编译器编译成字节码(.class)。在程序运行时,JVM负责将字节码翻译成特定平台下的机器码并运行,也就是说,只要在不同的平台上安装对应的JVM,就可以运行字节码文件。
同一份Java源代码在不同的平台上运行,它不需要做任何的改变,并且只需要编译一次。而编译好的字节码,是通过JVM这个中间的“桥梁”实现跨平台的,JVM是与平台相关的软件,它能将统一的字节码翻译成该平台的机器码。
注意事项
-
编译的结果是生成字节码、不是机器码,字节码不能直接运行,必须通过JVM翻译成机器码才能运行;
-
跨平台的是Java程序、而不是JVM,JVM是用C/C++开发的软件,不同平台下需要安装不同版本的JVM。
15.为啥要有包装类?
Java语言是面向对象的语言,其设计理念是“一切皆对象”。但8种基本数据类型却出现了例外,它们不具备对象的特性。正是为了解决这个问题,Java为每个基本数据类型都定义了一个对应的引用类型,这就是包装类。
🍉扩展阅读🍉
Java之所以提供8种基本数据类型,主要是为了照顾程序员的传统习惯。这8种基本数据类型的确带来了一定的方便性,但在某些时候也会受到一些制约。比如,所有的引用类型的变量都继承于Object类,都可以当做Object类型的变量使用,但基本数据类型却不可以。如果某个方法需要Object类型的参数,但实际传入的值却是数字的话,就需要做特殊的处理了。有了包装类,这种问题就可以得以简化。