JavaSE学习总结(五)

聊一聊你对泛型的理解

泛型就是将类型参数化,在编译时才确定具体的参数,泛型可以用于类、接口和方法,即泛型类、泛型接口和泛型方法。使用泛型主要有两个好处,第一是提高Java程序的类型安全,这也是泛型的主要目的,通过使用泛型,在编译时就可以检查出因参数不正确而导致的错误,符合越早发现问题代价越小的原则;第二就是消除类型的强制转换,在没有使用泛型之前,比如使用ArrayList存储多个数据,但是使用get()方法获取数据时需要手动强转成我们需要的类型,但是在编译时并不知道这个强转有没有错误,只有在运行时才能确定。

Java泛型的原理是什么 ? 什么是类型擦除 ?

泛型是一种语法糖,泛型这种语法糖的基本原理是类型擦除。Java中的泛型基本上都是在编译器这个层次来实现的,也就是说:**泛型只存在于编译阶段,而不存在于运行阶段。**在编译后的 class 文件中,是没有泛型这个概念的。

类型擦除:使用泛型的时候加上的类型参数,编译器在编译的时候去掉类型参数。

例如:

public class Caculate<T> {
    private T num;
}

我们定义了一个泛型类,定义了一个属性成员,该成员的类型是一个泛型类型,这个 T 具体是什么类型,我们也不知道,它只是用于限定类型的。反编译一下这个 Caculate 类:

public class Caculate{
    public Caculate(){}
    private Object num;
}

发现编译器擦除 Caculate 类后面的两个尖括号,并且将 num 的类型定义为 Object 类型。

那么是不是所有的泛型类型都以 Object 进行擦除呢?大部分情况下,泛型类型都会以 Object 进行替换,而有一种情况则不是。那就是使用到了 extends 和 super 语法的有界类型,如:

public class Caculate<T extends String> {
    private T num;
}

这种情况的泛型类型,num 会被替换为 String 而不再是 Object。这是一个类型限定的语法,它限定 T 是 String 或者 String 的子类,也就是你构建 Caculate 实例的时候只能限定 T 为 String 或者 String 的子类,所以无论你限定 T 为什么类型,String 都是父类,不会出现类型不匹配的问题,于是可以使用 String 进行类型擦除。

实际上编译器会正常的将使用泛型的地方编译并进行类型擦除,然后返回实例。但是除此之外的是,如果构建泛型实例时使用了泛型语法,那么编译器将标记该实例并关注该实例后续所有方法的调用,每次调用前都进行安全检查,非指定类型的方法都不能调用成功。

实际上编译器不仅关注一个泛型方法的调用,它还会为某些返回值为限定的泛型类型的方法进行强制类型转换,由于类型擦除,返回值为泛型类型的方法都会擦除成 Object 类型,当这些方法被调用后,编译器会额外插入一行 checkcast 指令用于强制类型转换。这一个过程就叫做『泛型翻译』。

我在这里想要详细解释一下类型擦除这个概念,上面的大多数都是废话,网上也能搜到,之所以还要再次罗列不过是因为更多的例子能让大家更好理解,有时候多看一些比较松散的例子也能有意想不到的收获。

所谓类型擦除,就是指我们在编写代码时设置泛型,类似这样 List<String> list = new ArrayList<>();,这样做了之后有什么好处呢?在之后我们通过list.add()添加数据时,编译器会帮我们自动检测,如果类型不符合就会报错。OK,添加完数据,做完相应工作后,进行编译,Java程序变为字节码,这个时候,所有的泛型信息都会被擦除,就像上面的例子 变为 Object。既然我在编译之后泛型被擦除了,那我调用list.get()方法时如何保证我得到结果是泛型类型呢?答案是在调用get()方法时编译器会自动帮我们进行强转。

通过使用泛型,我们可以让编译器在编译期间检查变量的数据类型是否正确,从而减少运行期错误的概率,增加程序的健壮性。而在运行期间,由于泛型类型信息被擦除了,所以我们无法直接得到元素的实际类型参数。不过,编译期间所生成的字节码文件中会保存一部分泛型类型信息,这些信息可以在运行期间反射获取到。

当你调用 list.get() 方法时,如果在这段代码的上下文中已经确定了元素的类型,那么编译器会自动进行类型转换,保证返回值的类型与指定的泛型类型相同。例如:

List<String> list = new ArrayList<>();
list.add("hello");
String str = list.get(0); // 这里编译器会自动将 Object 类型转换为 String 类型

如果在上下文中无法确定元素的类型,那么返回值类型将会是 Object。例如:

List list = new ArrayList();
list.add("hello");
Object obj = list.get(0); // 返回值类型为 Object 类型

需要注意的是,使用时如果强制转换得到的结果类型错误,可能会导致运行时异常,比如 ClassCastException。因此在转换类型时需要进行类型检查,避免出现类型错误的情况。

如果想要详细理解泛型相关的知识,可以参考这篇文章——ava泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题

强烈推荐大家看这篇文章,绝对精品!!!

Java序列化与反序列化是什么?

Java序列化是指把Java对象转换为字节流的过程,而Java反序列化是指把字节流恢复为Java对象的过程。

序列化: 序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。核心作用是对象状态的保存与重建。我们都知道,Java对象是保存在JVM的堆内存中的,也就是说,如果JVM堆不存在了,那么对象也就跟着消失了。

而序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。就像我们平时用的U盘一样。把Java对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。这样,当再次需要这个对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。

反序列化: 客户端从文件中或网络上获得序列化后的对象字节流,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。

序列化和反序列化常见应用场景

  • 对象在进行网络传输(比如远程过程调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;

  • 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;

  • 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;

  • 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。

需要注意的是,虽然将对象序列化到内存中可以减少与磁盘或者网络的交互,但是仍然需要占用内存空间。如果需要处理大量的对象,可能会导致内存问题,因此应该根据实际情况来选择是否将对象序列化到内存中。

JDK 自带的序列化方式

实现 Serializable 接口或者 Externalizable 接口。

Serializable接口

类通过实现 java.io.Serializable 接口以启用其序列化功能。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。

如以下例子:

import java.io.Serializable;

public class User implements Serializable {
   private static final long serialVersionUID = 1905122041950251207L;
   private String name;
   private int age;
   public String getName() {
       return name;
   }
   public void setName(String name) {
       this.name = name;
   }

   @Override
   public String toString() {
       return "User{" +
               "name='" + name +
               '}';
   }
}

通过下面的代码进行序列化及反序列化:

public class SerializableDemo {

   public static void main(String[] args) {
       //Initializes The Object
       User user = new User();
       user.setName("cosen");
       System.out.println(user);

       //Write Obj to File
       try (FileOutputStream fos = new FileOutputStream("tempFile"); ObjectOutputStream oos = new ObjectOutputStream(
           fos)) {
           oos.writeObject(user);
       } catch (IOException e) {
           e.printStackTrace();
       }

       //Read Obj from File
       File file = new File("tempFile");
       try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
           User newUser = (User)ois.readObject();
           System.out.println(newUser);
       } catch (IOException | ClassNotFoundException e) {
           e.printStackTrace();
       }
   }
}

//OutPut:
//User{name='cosen'}
//User{name='cosen'}

Externalizable接口

Externalizable继承自Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。

当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。否则所有变量的值都会变成默认值。

public class User implements Externalizable {
   private static final long serialVersionUID = 1905122041950251207L;
   private String name;
   private int age;

   public String getName() {
       return name;
   }
   public void setName(String name) {
       this.name = name;
   }
   public void writeExternal(ObjectOutput out) throws IOException {
       out.writeObject(name);
   }
   public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
       name = (String) in.readObject();
   }

   @Override
   public String toString() {
       return "User{" +
               "name='" + name +
               '}';
   }
}

通过下面的代码进行序列化及反序列化:

public class ExternalizableDemo1 {

  public static void main(String[] args) {
      //Write Obj to file
      User user = new User();
      user.setName("cosen");
      try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"))){
          oos.writeObject(user);
      } catch (IOException e) {
          e.printStackTrace();
      }

      //Read Obj from file
      File file = new File("tempFile");
      try(ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file))){
          User newInstance = (User) ois.readObject();
          //output
          System.out.println(newInstance);
      } catch (IOException | ClassNotFoundException e ) {
          e.printStackTrace();
      }
  }
}

//OutPut:
//User{name='cosen'}

两种序列化的对比

实现Serializable接口实现Externalizable接口
系统自动存储必要的信息程序员决定存储哪些信息
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持必须实现接口内的两个方法
性能略差性能略好
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

路上阡陌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值