本篇文章是 Stack Overflow 周报的第二周,共收集了 4 道高关注的问题和对应的高赞回答。公众号「渡码」为日更,欢迎关注。
DAY1. serialVersionUID 的重要性
关注: 2820,最高赞: 2152
这篇文章介绍一下 Java 中 serialVersionUID 属性的含义以及重要性。从属性可以看出它与序列化有关系,所以在 java.io.Serializable 接口的注释中对它有详细的介绍,下面我们对照文档注释来学习一下。Java 中每个可序列化的类都有一个版本号与之关联,这个版本号就是 serialVersionUID。它在对象反序列化时使用, 用于判断该类的发送方和接收方的 serialVersionUID 是否一致,如果接收方装载的类的 serialVersionUID 与发送方不一致,则抛出 InvalidClassException 异常。一个可序列化的类可以显示地声明 serialVersionUID 属性,但必须是 static,final,long 修饰的,如:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;
下面结合实际的例子来看看 serialVersionUID 的用法以及作用,上面说了 serialVersionUID 属性是定义在可序列化的类中,所以我们的类需要实现 java.io.Serializable 接口。因此,我们定义的 Person 类如下:
package com.cnblogs.duma.week2; import java.io.Serializable; public class Person implements Serializable { private static final long serialVersionUID = 42L; public int age; public String name; public Person(int age, String name){ this.age = age; this.name = name; } @Override public String toString() { return age + "," + name; } }
接下来,我们在定义两个方法分别用来序列化和反序列化,序列化方法如下:
// 将 Person 对象序列化后存到文件 public static void ser() { Person p = new Person(28, "duma"); System.out.println("person Seria:" + p); try { FileOutputStream fos = new FileOutputStream("person.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(p); oos.flush(); oos.close(); } catch (IOException e) { e.printStackTrace(); } }
序列化方法如下:
// 从文件中反序列化出 Person 对象 public static void deser() { Person p; try { FileInputStream fis = new FileInputStream("person.txt"); ObjectInputStream ois = new ObjectInputStream(fis); p = (Person) ois.readObject(); ois.close(); System.out.println(p.toString()); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } }
先调用 ser 方法,成功后可以看到项目根目录下生成了 person.txt 文件,然后在调用 deser 方法可以成功反序列化 Person 对象并输出结果。这波操作就是正常的序列化和反序列化操作。回到今天的主题,为了验证 serialVersionUID 属性的作用,我们可以在调用完 ser 方法后,先修改 serialVersionUID 值,然后再调用 deser 方法,这时就会抛出 java.io.InvalidClassException 异常。
明白了 serialVersionUID 属性的含义和作用,接下来我们再来看看它的重要性。在我们的例子中我们显式地定义了 serialVersionUID 属性,如果没有显式地指定 serialVersionUID,序列化运行时会根据类的信息计算一个默认值,《Effective Java》一书中提到这些信息包括类名、实现的接口、public和protected的成员。虽然有默认值,但 Java 官方文档强烈建议我们显式地定义 serialVersionUID 属性,因为默认的 serialVersionUID 依赖类的信息,而类的信息可能在不同编译器下会不同。因此,如果发送方和接收方使用的编译器不同,有可能导致默认的 serialVersionUID 不一致从而导致接收方无法正常反序列化,同时 Java 官方也建议使用 private 修饰 serialVersionUID,这样可以防止子类继承这个属性。
对于上面提到的《Effective Java》一书中的内容,我们可以做个简单的验证。因为生成默认的 serialVersionUID 会用到 public 成员信息,那我们改变成员变量就会导致 serialVersionUID 值改变。首先我们将 Person 类中的 serialVersionUID 属性删掉,调用 ser 方法序列化。然后在 Person 中加一个成员,比如:public String nickname = "zhangsan"; ,然后调用 deser 方法,可以看到程序抛出 java.io.InvalidClassException 异常。这同时也警示我们尽量显示地定义 serialVersionUID 属性。
DAY2. 创建线程到底用哪种方式
关注: 1972,最高赞: 1583
我们知道 Java 实现线程的方式有两种, 一种是继承 Thread 类,另一种是实现 Runnable 接口。那么问题来了,这两种方式有什么区别呢?我们应该用哪种方式更好呢?下面先简单看下这两种方式的代码。
1. 继承 Thread 类
static class MyThread extends Thread { @Override public void run() { super.run(); } }
2. 实现 Runnable 接口
static class Workder implements Runnable { @Override public void run() { } }
对于这两种方式的选择,Stack Overflow 的回答者普遍认为优先选择实现 Runnable 接口的方式,理由如下:
- 在面向对象中,继承意味着添加新功能、修改或者改进父类的行为,如果我们没有这方面的改动,那就尽量避免使用继承,因为我们的代码只是单纯需要执行一些任务,而不需要改造 Thread 的行为,所以继承 Runnable 接口更合理,所以上面代码中继承的方式类名是 Thread1 是一种(is a)Thread,而实现 Runnable 接口的类名是 Worker,一个 Worker 对象(工人)的“工作”逻辑可以放在 run 方法中,然而这并不意味这这个工人 7*24 小时一直工作。
- 由于 Java 中不支持多继承,因此继承 Thread 类意味着无法在继承其他的类,影响代码的扩展性。
- 继承 Thread 意味着每个线程都有一个唯一的 Thread 的对象与之对应,而实现 Runnable 接口可以让多个线程共享同一个对象。
总之,如果我们的类定位在单纯地执行任务,并不需要改造 Thread 类,那我们就应该实现 Runnable 接口。反之,如果我们需要改造 Thread 类,或者它是一种(is a)线程,那我们就继承 Thread 类。我目前正在写的一本关于 RPC 的书中,创建线程就是以继承 Thread 为主。
DAY3. 反射非用不可吗
关注: 1960,最高赞: 1611
相信有 Java 基础的朋友都知道反射的概念。然而如果你仅仅了解概念,而在工程实践中没有应用的话,那可能总是感觉有层窗户纸模模糊糊的。我之前学习反射就有这种感觉。那么今天这篇文章我就完整地梳理一下反射的概念和作用,结合 Hadoop RPC 框架聊聊反射为什么非用不可或者说用了反射是不是给程序带来了非常大的便利性。
首先,我们看定义:反射是语言提供了一种在运行时检查和动态调用类、方法和属性的能力。基于这个能力,反射一般大量应用在框架中,如:Spring,Hadoop。从反射的定义我们可能会问一个问题,为什么要在运行时动态地调用?既然 Java 是静态语言,任何需要调用的东西为什么不在编译时就确定好呢?这个问题也就是在问反射的作用是什么以及是不是非用不可。
我们可以简单猜想一下,以类的反射为例,当我们使用第三方框架时,框架并不知道用户定义了什么类,因此框架想要使用用户的类,只能在运行时动态地检查类是否存在,再进行调用。下面以 Hadoop 的 MapReduce 框架为例,看一下它使用反射的一个例子。
用户自定义的类如下:
// 需要继承 Mapper 基类,Hadoop 框架才能正常使用它 public class WordCountMapper extends Mapper<Object, Text, Text, IntWritable> { // Hadoop 框架会在创建 WordCountMapper 对象后调用 map 方法 @Override protected void map(Object key, Text value, Context context) throws IOException, InterruptedException { // 数据处理逻辑 } }
Hadoop 框架通过反射调用 WordCountMapper 的代码如下:
// taskContext.getMapperClass() 为运行时用户传入的类,WordCountMapper org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE> mapper = (org.apache.hadoop.mapreduce.Mapper<INKEY,INVALUE,OUTKEY,OUTVALUE>) ReflectionUtils.newInstance(taskContext.getMapperClass(), job);
// run 方法中循环调用 map 方法处理数据 mapper.run(mapperContext);
上面是 Hadoop 代码的一部分,它将类的反射代码封装在 ReflectionUtils.newInstance 方法中,该方法用类的默认构造方法动态地创建一个对象。上述代码中该方法创建的对象被强转为 Mapper 类,这就是为什么因为我们的 WordCounterMapper 类要继承 Mapper。
因此,可以看到 Hadoop 框架在编译的时候并不知道用户定义了WordCountMapper 类,只能在运行时根据配置动态地检查、调用。当然为了框架能够正常使用我们定义的类,就需要定义类时符合框架定义的规范,在我们的例子中需要遵循的规范是实现一个 Mapper 基类,并且需要有默认构造函数。如果我们在代码中修改 WordCountMapper 的构造函数,那就不符合框架的规范,反射就会报错,如下:
public class WordCountMapper extends Mapper<Object, Text, Text, IntWritable> { // 有参构造函数覆盖默认构造函数 public WordCountMapper(int a) { a = 100; } }
再次运行,当 Hadoop 调用 ReflectionUtils.newInstance 时找不默认构造函数便会以下报错:
Error: java.lang.RuntimeException: java.lang.NoSuchMethodException: com.cnblogs.duma.mapreduce.WordCountMapper.<init>() at org.apache.hadoop.util.ReflectionUtils.newInstance(ReflectionUtils.java:135) at org.apache.hadoop.mapred.MapTask.runNewMapper(MapTask.java:751)
接下来,再举一个动态调用方法的例子,假设我们要在运行时才能检查并调用某方法,写法如下:
Method method = foo.getClass().getMethod("doSomething", null); method.invoke(foo, null);
这种场景也是框架比较喜欢用的,比如:Java 的单元测试框架 Junit4,通过反射检查类中带有 @Test 注解的方法,然后调用他们运行单元测试。
从上面两个例子可以看到为什么框架对反射如此钟情。框架不需要关心用户定义了什么类,只要用户的代码符合框架定义的规范,框架就会在运行时进行检查,并按照自己定义的规范调用代码即可。因此反射可以让框架和用户的应用解耦,使得开发更方便。
DAY4. 一行代码搞定数组的初始化、搜索、打印
我们平时遇到的好多问题可能一行代码就搞定了。平时遇到问题可以多想想是不是已经有工具已经实现了, 如果有的话可以直接拿来用,避免重复造轮子。这篇文章今天发在公众号上,算是关注公众号读者的一个福利吧。后续再发博客。
以上便是 Stack Overflow 的第二周周报,希望对你有用,后续会继续更新,如果想看日更内容欢迎关注公众号。
公众号「渡码」,分享更多高质量内容