2.基础加强版面试题

1.异常

  这里引用JavaGuide的一张异常类层次结构图

 1.1 Exception和Error的区别

  在Java中,所有的异常类有一个共同的父类Throwable存在于java.lang包中。Throwable类有两个子类:

  • Exception:程序中可以处理的异常,可通过catch进行捕获;Exception可分为Checked Exception(受检查异常,必须处理)(RuntimeException及其子类)Unchecked Exception(不受检查异常,可不处理)
  • Error:程序本身无法处理的异常;如虚拟机运行异常(Virtual MachineError)、内存溢出异常(OutOfMemory)、类定义错误(NoClassDefFoundError)。

  FileNotFoundException是一种受检查异常,是Exception异常类的子类

1.2 Throwable类常见的方法

  1. String getMessage():返回异常发生时的简要描述;
  2. String toString:返回异常时的详细信息;
  3. String getLocalizedMessage():返回异常对象的本地化信息。如果Throwable的子类覆盖该方法,可生成本地化信息。若没有则返回getMessage()方法信息;
  4. void printStackTrace():控制台打印Throwable对象封装的异常信息

1.3 try-catch-finally 如何使用

try块:捕获异常。可接0个或多个catch块,如果没有catch块,一定要有finally块

catch块:处理try捕获到的异常;

finally块:无论是否捕获或处理异常,finally块的语句都会被执行;当遇到try或catch中存在return时,会在其之前执行

代码实例

注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值

 1.4 finally一定会执行吗

  不一定,在某些情况下,finally不会执行,如终止正在运行的虚拟机(System.exit(1))关闭CPU

代码实例和展示

 1.5 异常使用需要注意的地方

  • 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。
  • 抛出的异常信息一定要有意义
  • 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出NumberFormatException而不是其父类IllegalArgumentException
  • 使用日志打印异常之后就不要再抛出异常了(两者不要同时存在一段代码逻辑中)。

1.6 实际应用拓展(记录自己后续项目中对异常的应用)

2.泛型

2.1 什么是泛型?有什么作用?

  泛型(Generics)是JDK5引入的特性,它是一种实现参数化类型的机制。即在定义接口方法时使用参数表示类型,在实际使用时指定具体的类型。使用泛型参数可增强代码的可读性及稳定性

编译器对泛型参数进行检测,并通过泛型参数指定传入的对象类型实现自动转换类型

代码实例

public class Generics<T>{
    private T value;
    private Generics(T value){
        this.value = value;
    }
    public void setValue(T Value){
        this.value = value;
    }
    public T getVallue(){
        return value;
    }
}
泛型类

 2.2 泛型有哪几种?

  泛型有泛型类泛型接口泛型方法三种

2.2.1 泛型类

public class Generics<T>{
    private T value;
    private Generics(T value){
        this.value = value;
    }
    public void setValue(T Value){
        this.value = value;
    }
    public T getVallue(){
        return value;
    }
}    

实例化

Generics<String> generics = new Generics<>("123456");

2.2.2 泛型接口

public interface Generics<T>{
public T method(); }

实现泛型接口,不指定类型,如果不是null类型,那么会报编译错误。当返回值为null时,表示空引用,因此不会报错。

//T即不指定类型
public class GenericsClass<T> implements Generics<T>( @Override public T method(){ retutn 5; } )

   可以通过强制转换将其转换为T类型避免报错

代码实例

public class GenericsClass<T> implements Generics<T>(
    @Override
    public T method(){
        retutn (T)Integer.valueOf(5);
    }
)

实现泛型接口,指定类型

//T即不指定类型
public class GenericsClass<T> implements Generics<String>(
    @Override
    public String method(){
        retutn "6";
    }
)

2.2.3 泛型方法

public static <E> void printArray(E[] array){
for (E element:array) {
System.out.println(element);
}
}

注意:public static <E> coid printArray(E[] array)一般被称为静态泛型方法;在Java中,泛型是一个占位符,必须在传递类型后才能使用类在实例化时才能真正的传递参数,静态方法加载与编译器,先于运行期加载的类实例化对象。即类中的泛型还没有真正传递类型参数时,静态方法已加载完。因此静态方法无法使用类上的泛型只能使用自己的泛型<E>

2.3 项目中泛型的用处

  • 自定义接口通用返回结果 CommonResult<T>通过参数 T 可根据具体的返回类型动态指定结果的数据类型
  • 定义 Excel 处理类 ExcelUtil<T> 用于动态指定 Excel 导出的数据类型
  • 构建集合工具类(参考 Collections 中的 sortbinarySearch 方法)。

2.4 实际应用拓展(记录自己后续项目中对泛型的应用)

3. 反射

3.1 概念

   反射运行时分析类以及执行类中方法的机制。通过反射可以获取任意一个类的所有属性和方法,还可以调用这些属性和方法

3.2 优缺点

  • 优点:灵活,为Spring/Spring Boot、Mybatis等框架提供开箱即用的功能提供了便利;
  • 缺点:其在运行时分析类以及执行类中方法能力的同时,增加了安全性问题;如无视泛型参数的安全检查(泛型参数安全检查发生在编译时)。反射性能稍差。

3.3 new一个对象和反射生成一个对象的差异

  反射是在运行时动态操作、监测修改类、对象、方法和属性的机制。使用“new”关键字创建对象是在编译器确定的静态操作

  • 创建对象成本:反射创建对象需要动态解析构造函数、检查访问权限,这些操作在性能上需要额外开销。使用“new”关键字可直接调用构造函数进行对象实例化,成本较低。

3.4 反射应用场景

  在Spring/SpringBoot、Mybatis框架中大量使用了反射机制。这些框架大量使用了动态代理模式,而动态代理模式依赖于反射

  JDK实现动态代理,其中使用反射类的Method方法调用指定的方法

public class DebugInvocationHandler implements InvocationHandler{
        private final Object target ;//final修饰的变量必须在声明时或构造函数中进行初始化赋值,不然会编译报错

        private DebugInvocationHandler(Object target){
            this.target = target;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("before method" + method.getName());
            Object result = method.invoke(target, args);
            System.out.println("after method" + method.getName());
            return result;
        }
    }
JDK实现动态代理

注意:为什么使用 Spring 的时候 ,一个@Component注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 @Value注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?

  因为可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理

3.5 反射实战

3.5.1 获取Class对象的四种方式

  反射动态获取类信息,需要使用Class对象Class类对象将一个类的方法、属性等信息告诉运行的程序。Java提供四种方式获取Class对象。

1.知道具体类情况下使用:

@Data
    static class User{
        private String name;

    }
    public static void main(String[] args) {
        Class<User> userClass = User.class;
        System.out.println(userClass);
    }

  我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化

由于我创建的User是一个静态内部类,所以加载的结尾为$User

2.通过Class.forName()传入类的全路径获取

Class<?>aClass = Class.forName("com.ku.test.basic.TestReflect");
System.out.println(aClass);

3.通过instance.getClass()获取

User user = new User();
Class userClass2 = user.getClass();

4.通过类加载器XXXClassLoader传入类路径获取

Class<?> aClass = ClassLoader.getSystemClassLoader("com.ku.test.basic.TestReflect");

 3.5.2 反射的一些基本操作(重点)

public class TargetObject{
    private String value;
    private TragetObjext(){
        value = "小库"
    }  
    public void publicMethod(String s){
        System.out.println("I love"+s);
    }

    public void privateMethod(){
        System.out.println("love id "+value);
    }
}
TargetObject类
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        //反射基本操作
        //获取TargetClass的Class对象,并创建TargetObject类实例
       Class<?>targetClass = Class.forName("com.ku.test.basic.TargetObject");
        //通过Class.newInstance创建一个对象强转为TargetObject类型
        TargetObject targetObject = targetClass.newInstance();
        //获取TargetObject中定义的所有方法
        Method[] mathods = targetClass.getDeclareMethods();
        for(Method method : methods){
            System.out.println(method.getName());
        }
        //获取指定方法并调用
        //传入String.class为参数类型
       Method publicMethod = targetClass.getDeclareMethod("publicMethod", String.class);
        //动态调用
        publicMethod.invoke(targetObject, "xiaoku");
        //获取指定参数,并对参数进行修改
        Field filed = Class.getDeclareField("value");
        field.setAccess(true);
        field.set(tagetObject, "xiaoku");
        //执行private方法
        Method privateMethod = targetClass.getDeclareMethod("privateMethod");
        privateMethod.setAccess(true);
        privateMethod.invoke(targetObject, "xiaoku");
基本操作类

注意:反射操作涉及多个受检测异常,我这里统一在方法名后抛出。

3.6 代理模式

  代理模式是一种设计模式,它通过代理对象对真实对象进行功能扩增。在不修改原目标对象的情况下,扩展原目标对象的功能。代理模式有动态代理和静态代理两种。

  代理模式主要作用:扩展目标对象的功能。如在目标对象的某个方法前后增加一些自定义操作

3.6.1 静态代理

   静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法目标对象和代理对象都要进行修改,因为目标和代理都需要实现相同的接口)且麻烦(需要对每个目标类都单独写一个代理类)。

  从JVM层面分析,静态代理是在编译时就将接口实现类代理类编译为一个.class文件

静态代理实现步骤:

  1. 定义一个接口及真实实现类;
  2. 定义一个代理类实现与真实实现类相同的接口;
  3. 将目标对象注入代理类,再通过代理类的方法调用目标对象的方法(通过代理类屏蔽对目标对象的访问,还可以在目标方法执行前后自定义)

代码实例

public interface SmsService {
    String smsSend(String message);
}

public class SmsServiceImpl implements SmsService {
    @Override
    public String smsSend(String message) {
        //实现类对接口方法进行重写
        System.out.println("send message:"+message);
        return message;
    }
}

public class SmsProxy implements SmsService {

    //当变量被final修饰,必须在声明时复制或者在构造方法中初始化
    private final SmsService smsService;

    public SmsProxy(SmsService smsService) {
        this.smsService = smsService;
    }

    @Override
    public String smsSend(String message) {
        System.out.println("before send Message");
        smsService.smsSend(message);
        System.out.println("after send Message");
        return null;
    }
}
发送消息接口、实现类、代理类

结果

   从上述结可以看出,接口与实现类以及代理类之间的耦合度高,当接口增加新方法时,接口实现类和代理类都需要进行代码修改,且每一个单独功能的实现类都需要一个代理类,如我需要在接口上面新增一个接受消息的方法,那么我需要分别在实现类和代理类中重写接受消息的方法。如果有多个目标类,则需要为每个目标类增加一个代理类因为多个目标类中的功能可能有所不同,因此需要创建多个代理类扩展每个目标对象各自的功能

代码实例

public interface SmsService {
    String smsSend(String message);

    String rcvMessage(String message);
}

public class SmsServiceImpl1 implements SmsService{
    @Override
    public String smsSend(String message) {
        //实现类对接口方法进行重写
        System.out.println("send message:"+message);
        return message;
    }

    @Override
    public String rcvMessage(String message) {
        System.out.println("receive:"+message);
        System.out.println("我是第二个目标类,有别于第一个目标类");
        return message;
    }
}

public class SmsProxy1 implements SmsService {

    private final SmsService smsService;

    public SmsProxy1(SmsService smsService) {
        this.smsService = smsService;
    }

    @Override
    public String smsSend(String message) {
        System.out.println("我是第二个代理类");
        System.out.println("before message");
        smsService.smsSend(message);
        System.out.println("after message");
        return null;
    }

    @Override
    public String rcvMessage(String message) {
        System.out.println("我是第二个代理类");
        System.out.println("before message");
        smsService.rcvMessage(message);
        System.out.println("after message");
        return null;
    }
}

public static void main(String[] args) {
        SmsService smsService = new SmsServiceImpl();//实例化一个接口实现类对象
        //将实例化的接口实现类对象注入到代理类中,作为接口的实现类
        SmsService smsProxy = new SmsProxy(smsService);
        smsService.smsSend("proxy");
        System.out.println("--------------------");
        smsProxy.smsSend("proxy");

        smsService.rcvMessage("message");
        System.out.println("--------------------");
        smsProxy.rcvMessage("proxy");

        //创建另一个实现类和代理类,这里使用第一个代理类反向测试,再创建属于第二个目标对象的代理类测试
        SmsService smsService1 = new SmsServiceImpl1();
        SmsService smsProxy1 = new SmsProxy(smsService1);
        SmsService smsProxy2 = new SmsProxy1(smsService1);

        smsService1.smsSend("proxy");
        System.out.println("--------------------");
        smsProxy1.smsSend("proxy");

        System.out.println("--------------------");
        smsProxy2.rcvMessage("proxy");
    }
新增的接收消息、实现类和代理类

运行结果

send message:proxy
--------------------
我是第一个代理类
before send Message
send message:proxy
after send Message
receive:message
我是第一个目标类,有别于第二个目标类
--------------------
我是第一个代理类
before send Message
receive:proxy
我是第一个目标类,有别于第二个目标类
after send Message
send message:proxy
--------------------
我是第一个代理类
before send Message
send message:proxy
after send Message
--------------------
我是第二个代理类
before message
receive:proxy
我是第二个目标类,有别于第一个目标类
after message
测试结果

3.6.2 动态代理

   相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类CGLIB 动态代理机制)。

  从JVM层面讲,动态代理时在运行时动态生成类字节码文件,并加载到JVM中。AOPRPC框架都依赖动态代理

  在Java中动态代理分为JDK动态代理CGLIB动态代理

 3.6.2.1 JDK动态代理机制

  在Java动态代理机制中InvocationHandler接口Proxy类是核心。

  Proxy类中使用频率最高的方法newProxyInstance()方法,它主要用来生成一个代理对象

    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        ......
    }

该方法一共有三个参数:

  1. loader:加载代理类的类加载器;
  2. interfaces:被代理类实现的一些接口
  3. h:实现了InvocationHandler接口的对象;

  实现动态代理需要Proxy的newProxyInstance()方法生成一个代理对象,还必须实现InvocationHandler接口来自定义处理逻辑。当我们的动态代理对象调用一个方法时,该方法的调用会被转发到实现了InvocationHandler接口的实现类的invoke()方法进行调用

public interface InvocationHandler{
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;  
}

invoke方法中的三个参数:

  1. proxy:动态生成的代理类
  2. method:与代理类的调用方法相对应
  3. args:当前method方法参数

3.6.2.2 JDK动态代理类使用步骤

  1. 定义一个一个接口及其实现类;
  2. 实现InvocationHandler接口并重写invoke()方法,在invoke()方法中调用原生方法(被代理的方法)并自定义一些处理逻辑;
  3. 通过Proxy类的newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)创建代理对象;

3.6.2.3 代码示例

1.发送短信接口

public interface SmsService{
    String send(String msg);  
}

2.发送短信接口实现类

public class SmsServiceImpl implements SmsService {
    public String send(String msg) {
        System.out.println("send message:" + msg);
        return msg;
    }
}
实现类

3.实现InvocationHandler接口,重写invoke()方法

public class DebugInvocationHandler implements InvocationHandler {

    //代理类中真实对象
    private final Object target;

    public DebugInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before method:"+method.getName());
        Object res = method.invoke(target, args);
        System.out.println("before method:"+method.getName());
        return res;
    }
}
InvocationHandler实现类

4.获取代理对象的工厂类

public class JDKProxyFactory {

    public static Object getProxy(Object target){
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),//目标类的类加载器
                                      target.getClass().getInterfaces(),//代理需要实现的接口,可多个
                                      new DebugInvocationHandler(target)//代理对象对应的自定义InvocationHandler
        );
    }
}
代理对象工厂类

getProxy():获取某个类的代理对象

测试

 3.6.3 CGLIB动态代理机制

3.6.3.1 简介

  JDK动态代理致命的缺点只能代理实现了接口的类。为解决这个问题,可使用CGLIB动态代理。

  CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成CGLIB 通过继承方式实现代理。很多知名的开源框架都使用到了CGLIB, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

3.6.4 动态代理实现拓展

 

3.7 实际应用拓展(记录自己后续项目中对反射的应用)

 

4.注解

4.1 简介

   Annotation(注解)是Java5引入的新特性,它是一种特殊的注释。主要用于修饰类方法变量提供某些信息供程序编译或运行时使用

@Target(ElementType.METHOD)//可以标注在方法上
@Retention(RetentionPolicy.SOURCE)//在编译器后丢弃,只存在于源文件和编译期
public @interface Override {
}

注解中的元注解

@Docimented:会被javadoc工具提取为文档;

@Target:声明在哪些目标元素之前,即注释类型的程序元素的种类

@RetentionPolicy:表示注解的生命周期;

@Inherit:表示子类可以继承该类的注解;

4.2 @Target

  1. ElementType.PACKAGE:该注解只能声明在一个包名前。
  2. ElementType.ANNOTATION_TYPE:该注解只能声明在一个注解类型前。
  3. ElementType.TYPE:该注解只能声明在一个类前
  4. ElementType.CONSTRUCTOR:该注解只能声明在一个类的构造方法前。
  5. ElementType.LOCAL_VARIABLE:该注解只能声明在一个局部变量前。
  6. ElementType.METHOD:该注解只能声明在一个类的方法前
  7. ElementType.PARAMETER:该注解只能声明在一个方法参数前。
  8. ElementType.FIELD:该注解只能声明在一个类的字段前

4.3 RetentionPolicy

  生命周期对应:Java源文件(.java文件)--->。class文件--->内存中字节码

  RetentionPolicy有RetentionPolicy.SOURCE、RetentionPolicy.CLASS、RetentionPolicy.RUNTIME三种。

  1. RetentionPolicy.SOURCE表示注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃,编译期后被丢弃
  2. RetentionPolicy.CLASS表示注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期
  3. RetentionPolicy.RUNTIME表示注解不仅被保存到class文件中jvm加载class文件之后仍然存在

4.4 注解解析方式

  注解只有被解析后才生效,常见解析方式:

  1. 编译期直接扫描:编译期在编译Java代码得时候扫描对应的注解并处理,如某方法使用@Override注解,编译期在编译的时候就会检测当前方法是否重写父类对应的方法。
  2. 运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的 @Value@Component)都是通过反射来进行处理的。

4.5 实际应用拓展

4.5.1 注解如何发挥作用

注解是干啥的?

  注解本身不提供作用,注解只能是被看作元数据,它不包含任何业务逻辑。注解更像是一个标签,一个声明,表面被注释的地方,将具有某种特定的逻辑

注解常见种类

  常见的注解有三大类:JDK的,自定义的,第三方的(比如框架)

注解三板斧

定义、使用、读取

定义:包括名字能用到哪些地方有效期是否可以被继承

使用:定义好之后在允许的地方使用标注即可

光有前两步,没什么用,如最熟悉的@Override注解,为什么能验证重写是否有效,怎么不是验证重载?spring的@Autowired为什么是注入作用,而不是输出一句话?显然,他们在程序中做了实现,使得其注解具有各自的作用,也具有了意义,而赋予灵魂的一步就是读取

读取让注解发挥作用给注解注入灵魂

代码实例

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyBefore {
    //注解参数定义
    String value() default "";
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyCore {
    String value() default "";
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAfter {
    String value() default "";
}
三个注解
public class TestAnnotations {
    //注解不能标注在启动类方法的方法中
    @MyBefore()
    public void init(){
        System.out.println("初始化");
    }

    @MyAfter
    public void destroy(){
        System.out.println("销毁");
    }

    @MyCore
    public void core(){
        System.out.println("核心代码");
    }

    public static void main(String[] args) throws IllegalAccessException, InstantiationException, InvocationTargetException {
        Class<TestAnnotations> testClass = TestAnnotations.class;
        TestAnnotations test = testClass.newInstance();
        //获取Test类的所有方法,并将其添加到对应集合
        ArrayList<Method> myBeforeList = new ArrayList<>();
        ArrayList<Method> myTestList = new ArrayList<>();
        ArrayList<Method> myAfterList = new ArrayList<>();
        Method[] methods = testClass.getDeclaredMethods();
        for (Method method : methods) {
            //isAnnotationPresent()指定类型的注解是否存在此元素上
            if (method.isAnnotationPresent(MyBefore.class)){
                myBeforeList.add(method);
                continue;
            }
            if (method.isAnnotationPresent(MyCore.class)){
                myTestList.add(method);
                continue;
            }
            if (method.isAnnotationPresent(MyAfter.class)){
                myAfterList.add(method);
                continue;
            }
        }

        for (Method testMethod : myTestList) {
            //测试方法钱先执行标有MyBefore注解的方法
            for (Method method : myBeforeList) {
                method.invoke(test);
            }
            //调用test方法
            testMethod.invoke(test);
            for (Method method : myAfterList) {
                method.invoke(test);
            }
        }

    }
}
使用、注入以及测试

运行结果

初始化
核心代码
销毁

  任何注解都是上述的三板斧,后面再结合Spring的注解分析。

参考链接

Java中的注解是如何发挥作用的? - 知乎 (zhihu.com)

5.序列化和反序列化

5.1 什么是序列化?什么是反序列化?

  如果我们需要持久化对象即将Java对象保存在文件中,在网络中传输Java对象,这些场景都需要序列化。

  • 序列化:将数据结构或对象转换为二进制字节流过程
  • 反序列化:将序列化过程中所产生的二进制字节流转换为数据结果或对象过程

5.1.1 为什么要序列化?

  当两个进程进行远程通信时可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。而Java是面向对象开发,一切皆对象如果要实现网络传输、文件存储、数据库存储、内存存储,就需要将对象转换成二进制字节流,因此需要序列化和反序列化。

5.1.2 序列化和反序列化的应用场景

  • 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
  • 对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
  • 对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
  • 对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。

序列化主要目的:通过网络传输对象,或将对象存储在文件、数据库、内存中

摘自JavaGuide的一张序列化和反序列化转换的图片如下:

 5.1.3 序列化协议对象TCP/IP协议哪一层?

TCP/IP四层协议:

  1. 应用层
  2. 传输层
  3. 网络层
  4. 网络接口层

   在OSI七层协议中,表示层是对应用层的用户数据进行处理转换成二进制字节流。反过来就是将二进制的字节流转换成应用层的用户数据。这就是序列化和反序列化。

  OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层,所以序列化协议属于 TCP/IP 协议应用层的一部分。

5.2 有些字段不想序列化怎么办?

  对于不想序列化的字段使用transient关键字修饰即可

transient 关键字的作用是:阻止实例中transient修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复

注意:

  • transient 只能修饰变量不能修饰类和方法
  • transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0
  • static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰均不会被序列化

5.3 常见的序列化协议有哪些? 

  JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 HessianKryo(高性能推荐使用)ProtobufProtoStuff,这些都是基于二进制的序列化协议。像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性较好,但是性能较差,一般不会选择

JDK自带序列化

  JDK序列化,只需实现Java.io.Serialiable接口即可

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@ToString
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;
}
RpcRequest

seriaVersionUid作用
  seriaVersionUid作用是版本控制反序列化时,会检查seriaVersionUid是否与当前类的seriaVersionUid一致。若不一致,则会抛出InvalidClassException异常。强烈建议每个序列化类自定义seriaVersionUid。若不手动指定,那么会在编译期动态生成默认的seriaVersionUid

上述RpcRequest代码中,seriaVersionUid不是被static修饰,为什么还是被“序列化”?

  static修饰的变量是静态变量,位于方法区,本身不能被序列化。但seriaVersionUid的序列化被特殊处理,序列化时会将其序列化为二进制字节流,反序列化,解析它并做一致性判断。真实原因是它只被JVM识别,并不做序列化处理

6.I/O流

InputStream 常用方法:

  • read()返回输入流中下一个字节的数据返回的值介于 0 到 255 之间。如果未读取任何字节,则代码返回 -1表示文件结束
  • read(byte b[ ]) : 从输入流中读取一些字节存储到数组 b 中。如果数组 b 的长度为零,则不读取。如果没有可用字节读取,返回 -1。如果有可用字节读取,则最多读取的字节数最多等于 b.length , 返回读取的字节数。这个方法等价于 read(b, 0, b.length)
  • read(byte b[], int off, int len):在read(byte b[ ]) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字节数)
  • skip(long n)忽略输入流中的 n 个字节 ,返回实际忽略的字节数。
  • available()返回输入流中可以读取的字节数
  • close()关闭输入流释放相关的系统资源

Java 9 开始,InputStream 新增多个实用的方法

  • readAllBytes()读取输入流中的所有字节返回字节数组
  • readNBytes(byte[] b, int off, int len)阻塞直到读取 len 个字节
  • transferTo(OutputStream out)将所有字节从一个输入流传递到一个输出流

OutputStream 常用方法

  • write(int b):将特定字节写入输出流
  • write(byte b[ ]) : 将数组b 写入到输出流,等价于 write(b, 0, b.length)
  • write(byte[] b, int off, int len) : 在write(byte b[ ]) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字节数)
  • flush()刷新此输出流并强制写出所有缓冲的输出字节
  • close():关闭输出流释放相关的系统资源。

  FileOutputStream最常用的字节输出流对象,可直接指定文件路径,可以直接输出单字节数据,也可以输出指定的字节数组

try (FileOutputStream output = new FileOutputStream("output.txt")) {
    byte[] array = "JavaGuide".getBytes();
    output.write(array);
} catch (IOException e) {
    e.printStackTrace();
}

运行结果:

  类似于 FileInputStreamFileOutputStream 通常也会配合 BufferedOutputStream(字节缓冲输出流)来使用。

FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream)

  DataOutputStream 用于写入指定类型数据,不能单独使用,必须结合其它流,比如 FileOutputStream 

// 输出流
FileOutputStream fileOutputStream = new FileOutputStream("out.txt");
DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
// 输出任意数据类型
dataOutputStream.writeBoolean(true);
dataOutputStream.writeByte(1);

  ObjectOutputStream将对象写入到输出流(ObjectOutputStream序列化)。

  为什么IO流区分为字节流和字符流?

  • 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时
  • 如果我们不知道编码类型就很容易出现乱码问题。

  因此,I/O 流提供了一个直接操作字符的接口,方便对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好

字符流默认采用的是 Unicode 编码,我们可以通过构造方法自定义编码。IO笔试题:常用字符编码所占字节数?utf8 :英文占 1 字节,中文占 3 字节unicode:任何字符都占 2 个字节gbk:英文占 1 字节,中文占 2 字节

Reader用于从源头(通常是文件)读取数据(字符信息)到内存中,java.io.Reader抽象类是所有字符输入流的父类

Reader 用于读取文本InputStream 用于读取原始字节

Reader 常用方法

  • read() : 从输入流读取一个字符
  • read(char[] cbuf) : 从输入流中读取一些字符,并将它们存储到字符数组 cbuf,等价于 read(cbuf, 0, cbuf.length)
  • read(char[] cbuf, int off, int len)read(char[] cbuf) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字符数)
  • skip(long n)忽略输入流中的 n 个字符 ,返回实际忽略的字符数。
  • close() : 关闭输入流并释放相关的系统资源。

  InputStreamReader字节流转换为字符流的桥梁,其子类 FileReader 是基于该基础上的封装,可以直接操作字符文件

Writer用于将数据(字符信息)写入到目的地(通常是文件)java.io.Writer抽象类是所有字符输出流的父类

Writer 常用方法

  • write(int c) : 写入单个字符
  • write(char[] cbuf)写入字符数组 cbuf,等价于write(cbuf, 0, cbuf.length)
  • write(char[] cbuf, int off, int len):在write(char[] cbuf) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字符数)。
  • write(String str)写入字符串,等价于 write(str, 0, str.length())
  • write(String str, int off, int len):在write(String str) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字符数)。
  • append(CharSequence csq):将指定的字符序列附加到指定的 Writer 对象并返回该 Writer 对象
  • append(char c)将指定的字符附加到指定的 Writer 对象并返回该 Writer 对象
  • flush()刷新此输出流并强制写出所有缓冲的输出字符
  • close():关闭输出流释放相关的系统资源。

  OutputStreamWriter字符流转换为字节流的桥梁,其子类 FileWriter 是基于该基础上的封装,可以直接将字符写入到文件

try (Writer output = new FileWriter("output.txt")) {
    output.write("你好,我是Guide。");
} catch (IOException e) {
    e.printStackTrace();
}

6.5 字节缓冲流

  IO 操作很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁IO 操作提高流的传输效率

  字节缓冲流这里采用了装饰器模式增强 InputStreamOutputStream子类对象的功能。举个例子,通过 BufferedInputStream(字节缓冲输入流)来增强 FileInputStream 的功能

6.6 字符缓冲流

  BufferedReader (字符缓冲输入流)BufferedWriter(字符缓冲输出流)类似于 BufferedInputStream(字节缓冲输入流)和BufferedOutputStream(字节缓冲输入流)内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息

6.7 打印流

System.out.print("Hello!");
System.out.println("Hello!");

  System.out 实际是用于获取一个 PrintStream 对象print方法实际调用的是 PrintStream 对象的 write 方法PrintStream 属于字节打印流,与之对应的是 PrintWriter (字符打印流)PrintStreamOutputStream 的子类PrintWriterWriter 的子类

参考链接

Java基础常见面试题总结(下) | JavaGuide(Java面试 + 学习指南)

https://blog.csdn.net/Zero_uou/article/details/107819201

posted @ 2023-11-17 09:19  求知律己  阅读(17)  评论(0编辑  收藏  举报