Java基础面试 --序列化,反射,拷贝

在之前的文章中,已经发布了常见的面试题,这里我了点时间整理了一下对应的解答,由于个人能力有限,不一定完全到位,如果有解答的不合理的地方还请指点,在此谢过。

 

本文主要描述的是java基础内容序列化,反射和拷贝。这些在面试中会经常问到,尤其是反射,功能强大到基本框架都利用了该知识点。在实际项目开发中,序列化,反射,拷贝都是常用的知识点,所以重点掌握这些内容是很有必要的。

 

java创建一个类的方法有几种?

这是字节跳动的一道面试题,原来的题目意思是除了new一个对象,java还可以通过哪些方法来构建一个对象?这是一个java基本语法的题目,涉及到反序列化,拷贝,反射等基础知识。那这几个知识点也是java面试中常见的面试题。如果在回答完上面的方法适合,我们也可以适当的展开说序列化,拷贝和反射的原理。这样会显得对某些知识点掌握相对扎实,留下一些好的印象。

序列化

Java的序列化的作用是什么?

其底层实现的原理是如何的?

你了解到哪些常见的序列化框架,其优缺点是?

Java 序列化类中有一个final的Long型id,这个序列化id的作用是什么?如果没有的话,是否可以序列化成功?

如果不想序列化某个字段,应该怎么办?

上面描述的问题是序列化中常见的面试题,这里也说明一下。

序列化的作用

我们都知道,java是以对象为基本核心理念的,在java中,万物皆为对象。在某些场景下,我们需要将对象传递给其他机器或者写到磁盘保存。这个时候就需要将对象转换成二进制的方式,它是一个对象转换为二进制流的过程,而序列化过程就是完成这项工作的,与之对应的是将二进制转换为对象的过程,我们称之为反序列化过程。

序列化的步骤

Java实现序列化的过程由以下几个流程:

  1. 在类对象中实现Serializable接口
  2. 在类中定义序列化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

使用方便,可序列化所有类

速度慢,占空间

 

拷贝

  1. 聊聊你对java拷贝的认识?什么是深拷贝,什么是浅拷贝?
  2. 怎么实现一个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一个对象也可以实现深拷贝,或者通过反序列化的方式来说实现。

 

反射

  1. 介绍一下你了解的反射,反射的原理是什么?
  2. 怎么实现一个反射?反射的几种调用方法?区别是什么?

反射原理:

反射是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中,我们可以通过以下几种方式动态获取一个类的信息:

  1. Class.forName()
  2. Object.class
  3. 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内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈

                                                                   

           Java基础面试 --序列化,反射,拷贝

上一篇:Linux学习-第六周


下一篇:nmcli管理网络 RHEL8和CentOS8怎么重启网络