在之前的文章中,已经发布了常见的面试题,这里我了点时间整理了一下对应的解答,由于个人能力有限,不一定完全到位,如果有解答的不合理的地方还请指点,在此谢过。
本文主要描述的是java基础内容序列化,反射和拷贝。这些在面试中会经常问到,尤其是反射,功能强大到基本框架都利用了该知识点。在实际项目开发中,序列化,反射,拷贝都是常用的知识点,所以重点掌握这些内容是很有必要的。
java创建一个类的方法有几种?
这是字节跳动的一道面试题,原来的题目意思是除了new一个对象,java还可以通过哪些方法来构建一个对象?这是一个java基本语法的题目,涉及到反序列化,拷贝,反射等基础知识。那这几个知识点也是java面试中常见的面试题。如果在回答完上面的方法适合,我们也可以适当的展开说序列化,拷贝和反射的原理。这样会显得对某些知识点掌握相对扎实,留下一些好的印象。
序列化
Java的序列化的作用是什么?
其底层实现的原理是如何的?
你了解到哪些常见的序列化框架,其优缺点是?
Java 序列化类中有一个final的Long型id,这个序列化id的作用是什么?如果没有的话,是否可以序列化成功?
如果不想序列化某个字段,应该怎么办?
上面描述的问题是序列化中常见的面试题,这里也说明一下。
序列化的作用:
我们都知道,java是以对象为基本核心理念的,在java中,万物皆为对象。在某些场景下,我们需要将对象传递给其他机器或者写到磁盘保存。这个时候就需要将对象转换成二进制的方式,它是一个对象转换为二进制流的过程,而序列化过程就是完成这项工作的,与之对应的是将二进制转换为对象的过程,我们称之为反序列化过程。
序列化的步骤:
Java实现序列化的过程由以下几个流程:
- 在类对象中实现Serializable接口
- 在类中定义序列化id
序列化的原理:
在这里,我们介绍java默认的序列化和反序列化的函数。也就是按照上面步骤只实现了序列化的接口。那java底层是通过iO包中的ObjectOutputStream实现将对象转换为二进制流。ObjectOutputStream中的核心方法在于这个writeObject,其实现也是按深度,按照类型写入流中,数组和string,null的写入等等。而writeObject方法核心是writeObject0(看多了源码,你会发现好多源码喜欢使用0代表真正的实现或者do开头的),如果想更多了解源码的话,可以看之前的jdk1.8系列的源码阅读文章。与此相反的是反序列的方法,也就是ObjectInputStream.
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {//如果是允许重写了这个方法,使用该序列化方法
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);//使用默认的序列化方法
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
序列化id:
在序列化中,序列化id其实是一个版本管理的问题,其实在很多时候,我们都会使用到,因为一个对象在使用的过程中,难免出现修改字段,这时候需要将序列化id升级一个版本区分之前的版本。如果没有写序列化id能否序列化成功,当然是可以的,如果我们没有写序列化id 的时候,jvm会默认根据相应的规则生成一个,但这个时候可能会有一个问题是,但我们把jdk升级可能会导致序列化id和之前的不同,这就会导致代码出现无法序列化的问题。所以为了安全起见,最后在代码生成的时候加上一个序列化id。
Transient:
如果不想对某个字段进行序列化,可以使用transient。当然了static也不能被序列化,static是类的概念,而transient是临时的概念。
常见的序列化框架:
这类题目一般出现在后面几面,因为它不在是一个基础题,更多是一个知识体系和知识面的扩展。这种一般就是架构师要了解的,对不同的框架优缺点和选型做一个了解。这种一般回答几个,并且对其优缺点做下介绍。一般序列化框架会从以下几个方面比较,速度,序列化后的大小,是否跨语言。下面摘自博客【java序列化性能对比】的结果(详情:https://www.bbsmax.com/A/8Bz841mVzx/),有几类是我平时用的比较多的,protobuf、fastjosn、jdk自带的方法,这个可以有时间重点了解一下。
|
优点 |
缺点 |
Kryo |
速度快,序列化后体积小 |
跨语言支持较复杂 |
Hessian |
默认支持跨语言 |
较慢 |
Protostuff |
速度快,基于protobuf |
需静态编译 |
Protostuff-Runtime |
无需静态编译,但序列化前需预先传入schema |
不支持无默认构造函数的类,反序列化时需用户自己初始化序列化后的对象,其只负责将该对象进行赋值 |
Java |
使用方便,可序列化所有类 |
速度慢,占空间 |
拷贝
- 聊聊你对java拷贝的认识?什么是深拷贝,什么是浅拷贝?
- 怎么实现一个java 的深拷贝?
深拷贝和浅拷贝:
Java的拷贝技术在面试中的频率相比于序列化会少点,拷贝要实现的内容是将一个对象赋值一份出来。而对于一个基础类型,深拷贝和浅拷贝是一样的,都会复制一份新的,而对于一个对象类型,如果是浅拷贝的话,那其底层访问的是同一份对象,类似于对该对象起了个别名。而深拷贝的话才是我们真正说的复制,它会重新创建一个新的对象出来,和之前的对象的值一模一样,但是底层的地址是完全不同的。
怎么实现一个java 的深拷贝:
在java中,一个最基本的父类Object中有clone方法,而这个方法是实现浅拷贝的,这是一个native方法。如果我们要想实现深拷贝的话,就需要实现Cloneable接口,并且实现clone方法,如果对象中包含其他对象,其他对象也要实现。下面看下通过clone来实现一个类的拷贝。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User implements Cloneable{
private String name;
private LoginInfo loginInfo;
@Override
protected Object clone() throws CloneNotSupportedException {
User clone= (User)super.clone();
//调用底层clone方法重新赋值,不然就是浅拷贝
clone.loginInfo =(LoginInfo) clone.getLoginInfo().clone();
return clone;
}
@NoArgsConstructor
@AllArgsConstructor
@Data
public static class LoginInfo implements Cloneable{
private String id;
private String name;
//也要实现clone接口
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
}
当然除了上面的方式,如果底下对象很多的话,那调用也是相对麻烦过程,我们可以直接new一个对象也可以实现深拷贝,或者通过反序列化的方式来说实现。
反射
- 介绍一下你了解的反射,反射的原理是什么?
- 怎么实现一个反射?反射的几种调用方法?区别是什么?
反射原理:
反射是java程序员必须掌握的技能,其类似于给java程序员开了一个后门,我们可以在运行时获取到任何类的信息,包含这个类的属性和方法,而且可以获取private信息,这在一定程度上破坏了面向对象的封装。那么java反射的原理是怎么样的呢?反射的原理基本分为:加载类到jvm -》根据需求查询类的信息。这里面涉及到jvm 中类加载的过程,这也是面试中常见的面试题型,下面我们通过forName来看下反射的原理。
public static Class<?> forName(String className)
throws ClassNotFoundException {
//获取调用者类主要是为了获取到类加载器
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
forName0是一个native方法,该方法会去加载类,类的加载是回调java的双亲加载模型的代码。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {//加锁
// 检查类是否已经被加载
Class<?> c = findLoadedClass(name);
// 双亲委派加载模型
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);//使用父类加载
} else {
c = findBootstrapClassOrNull(name);//尝试使用Java boot加载器
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 双亲加载不成功,自身加载器加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);//使用自身类加载器
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {//如果是可链接的,链接指定的类
resolveClass(c);
}
return c;
}
}
反射的使用:
在java中,我们可以通过以下几种方式动态获取一个类的信息:
- Class.forName()
- Object.class
- Instance.getClass()
Class c1 = Class.forName("com.java.base.learn.reflect.ReflectTest");
//如果知道类的名字
Class c2= ReflectTest.class;
//如果对象存在
ReflectTest reflectTest = new ReflectTest();
Class c3 =reflectTest.getClass();
对于三者的区别是:
Class.forName会加载类,默认会对静态代码块初始化,当然也可以通过参数设置不初始化静态代码块。
Object.Class只返回类的信息,不会做任何初始化的操作。
Instance.getClass() 是返回对象对应的class对象。
可以构建一个含有静态代码块,动态代码块,和构造函数的进行试验。值得注意的是,无论是谁调用,类一旦被加载过,就会重缓存中获取,并不会每次都加载。如果我们需要将获取到的class进行实例的获取,那么会调用到另一个方法Class.newInstance。而newInstance会将类初始化完成,并调用构造函数初始化一个对象出来。这个在后续的jvm 面试题中会重点描述。
本文的内容就这么多,如果你觉得对你的学习和面试有些帮助,帮忙点个赞。谢谢。
想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈