JAVA笔记-类型转换小记

记录一些java中常会遇到的类型转换“陷阱”

场景一:String 转 Integer or Long

Integer 和 Long 转换的类似,下面就根据 Integer 的转换做示例。

常用的方式有三种

  1. new Integer(s)
  2. Integer.parseInt(s)
  3. Integer.valueOf(s)

如果转换的 String 是一个正常的数字形式字符串,那么三种情况的结果是类似的。

String s = "123";
Integer i1 = new Integer(s);
Integer i2 = Integer.parseInt(s);
Integer i3 = Integer.valueOf(s);

// 123
// 123
// 123

陷阱1:非数字字符串(包括 String 为 null 的情况)的转换会报 NumberFormatException 异常。

String s = "123abc";
Integer i1 = new Integer(s);
Integer i2 = Integer.parseInt(s);
Integer i3 = Integer.valueOf(s);

// NumberFormatException
// NumberFormatException
// NumberFormatException

陷阱2:注意 Integer.valueOf 的使用,可能会产生意外的情况。

从上面的2种情况看,三种方式的表现是一样的,但它们也有区别的地方(不然也不会被重复设计)。

首先看下 new Integer(s) 的源码:

public Integer(String s) throws NumberFormatException {
    this.value = parseInt(s, 10);
}

可以看到实际内部调用了 parseInt ,所以后续我们只讨论 parseInt 和 valueOf。

用下面这个代码案例可以看到两者的区别:

String s = "100000000";
if (Integer.parseInt(s) == Integer.parseInt(s)) { //true
    System.out.println("Integer.parseInt(s):true");
}else {
    System.out.println("Integer.parseInt(s):false");
}
if (Integer.valueOf(s) == Integer.valueOf(s)) { //true
    System.out.println("Integer.valueOf(s):true");
}else {
    System.out.println("Integer.valueOf(s):false");
}
// Integer.parseInt(s):true
// Integer.valueOf(s):false

我们看到结果的不同,其根本原因是 parseInt 返回的是基本类型 int,而 valueOf 返回的是 Integer 对象。我们知道 java 中的  对于基本类型比较的是值是否相等,而对象类型比较的是引用(内存地址)是否相等。另外 valueOf 还有个特殊的地方,当转换的数字在 [-128, 127] 之间时,返回的对象是一致的(== 为true),因为这部分的数字在内存中使用频繁,因此 Integer 内部给这些数字做了对象的缓存,转换后返回的是同一个缓存的地址,所以会出现下面的情况:

String s = "100";
if (Integer.valueOf(s) == Integer.valueOf(s)) { //true
    System.out.println("Integer.valueOf(s):true");
}else {
    System.out.println("Integer.valueOf(s):false");
}
// Integer.valueOf(s):true

场景二:Object 转 String

陷阱1:Object类型的对象在转换成其他对象时语法检查并不会报错,这将可能导致潜在的错误。

Map<String, Object> map = new HashMap<>();
map.put("a", 123);

String a = (String) map.get("a");

// ClassCastException: java.lang.Integer cannot be cast to java.lang.String

以上代码编译可以通过,但在运行时会报错。

陷阱2:因null值可以强制转换为任何java类类型,(String)null也是合法的。

为了避免上面的问题,我们可以使用Object对象的toString方法转换为String。

Map<String, Object> map = new HashMap<>();
map.put("a", 123);

String a = map.get("a").toString();

// "123"

但是当我们map是输入的参数,我们没法确认map中值的具体类型,可能会导致空指针异常,同样这种情况编译时无法发现。

Map<String, Object> map = new HashMap<>();
map.put("a", null);

String a = map.get("a").toString();
// NullPointerException

使用String的valueOf方法可以同时跳过上面2个陷阱。

Map<String, Object> map = new HashMap<>();
map.put("a", null);
map.put("b", 123);

String a = String.valueOf(map.get("a"));
String b = String.valueOf(map.get("b"));
// null
// "123"

场景三:Object 转 List

一个比较常见的场景是我们会接收到一个map类型的参数,然后试图将其某个 key 的 value 转换成 list,这个时候往往会遇到一个熟悉的异常 ClassCastException,例如下面这种情况:

String array = "111";
Map<String, Object> map = new HashMap<>();
map.put("array", array);

List<String> list = (List<String>) map.get("array");

// ClassCastException

在这种场景下如何优雅的进行转换呢,可以使用下面这个函数:

public static <T> List<T> castList(Object obj, Class<T> clazz) {
    List<T> result = new ArrayList<>();
    if (obj instanceof List<?>) {
        for (Object o : (List<?>) obj) {
            result.add(clazz.cast(o));
        }
        return result;
    }
    return null;
}

String array = "123";
Map<String, Object> map = new HashMap<>();
map.put("array", array);

List<String> list = castList(map.get("array"), String.class);

// null

首先我们屏蔽了无法被转换的情况,将抛出 ClassCastException 的情况友好的返回了 null。另外可以指定 List 的模板类型,扩展了更多的转换需求。

场景四:多层结构解析和转换

在这个场景下我们需要从多层的对象模型中获取某个字段的值并结合类型转换。例如有个需求要求我们找到学校里一年级中名叫张三的学生,获取到他的学号,初始化的代码如下:

@Data
@AllArgsConstructor
public static class School {
    private List<Class> classes;
}

@Data
@AllArgsConstructor
public static class Class {
    private List<Student> students;
    private Integer grade;
}

@Data
@AllArgsConstructor
public static class Student {
    private String name;
    private String studentNo;
}

List<Student> students = new ArrayList<>();
students.add(new Student("张三", "101"));
students.add(new Student("李四", "102"));

List<Class> classes = new ArrayList<>();
classes.add(new Class(students, 1));
classes.add(new Class(null, 2));
School school = new School(classes);

一个通常的写法是遍历2层数组,然后对各个对象进行空判断后查询直到找到底层的 Student 然后取出名称做转化。这里面首先循环会带来多层的代码缩进,同时还需要小心的判断对象是否为空(空指针往往容易忽略)。java8 提供了 Optional 的机制,可以让你方便的 stream 式写法,同时自动帮你做空处理。

Integer studentNumber = Optional.of(school)
    .map(School::getClasses)
    .flatMap(classList -> classList.stream()
             .filter(it -> 1 == it.getGrade()).findFirst())
    .map(Class::getStudents)
    .flatMap(studentList -> studentList.stream()
             .filter(it -> "张三".equals(it.getName())).findFirst())
    .map(Student::getStudentNo)
    .map(Integer::parseInt)
    .orElse(null);
;

// 101

当上述过程中遇到了空指针问题,程序会自动进入 orElse,也就是返回我们设置的 null。Optional的更多使用手册可以参考:java-optional

知识点总结

  1. 对待外部参数遵循最坏原则,保持怀疑,需要强转时用最安全的方式(可以参考上面的建议方式)。
  2. Object 可以转万物,一定要注意 null 和不同类型转换的情况。
  3. java8 Optional 可以方便省心的获取多层结构对象的值。