那些年我们在Java泛型上躺过的枪---万恶的泛型擦除【享学Java】(下)

简介: 那些年我们在Java泛型上躺过的枪---万恶的泛型擦除【享学Java】(下)

附:关于Arrays.asList()使用陷阱、指南


Arrays.asList()在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个List集合。但是很多小伙伴对它有点滥用,它的使用还是存在一些坑的,这里借助泛型,稍微总结一下:


// @since  1.2
public class Arrays {
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }
    private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable {
      ...
      // 来自父类  它自己并没有复写
      public void add(int index, E element) {
          throw new UnsupportedOperationException();
      }
      public E remove(int index) {
          throw new UnsupportedOperationException();
      }
      ...
    }
}


这是asList()方法的一个申明,可以看到它接收的也是一个可变参数,这么看来它和我们上面定义的doSomething(T... values)方法何其相似,因此此处我就不再用代码示例了,总结出下面几条结论即可,具体缘由我建议小伙伴一定要做到心知肚明:


  1. 传递的数组必须是对象数组,而不是基本类型。1. 当传入一个源生数据类型数组时,asList真正得到的参数就不是数组的元素了,而是数组对象本身
  2. 使用集合的修改方法:add()、remove()、clear()会抛出异常(因为它本质上还是个数组,不可变)


那么问题来了,如何正确的把数组Array转换为List呢???下面介绍几种常用方案:

方案一:自己for循环实现


代码略


方案二:最简便的方法(推荐)

List list = new ArrayList<>(Arrays.asList("a", "b", "c"))

方案三:使用 Java8 的Stream(推荐)


    public static void main(String[] args) {
        Integer[] myArray = {1, 2, 3};
        List<Integer> myList = Arrays.stream(myArray).collect(Collectors.toList());
        //基本类型也可以实现转换(依赖boxed的装箱操作) //boxed()是IntStream类的方法
        int[] myArray2 = {1, 2, 3};
        myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
        System.out.println(myList); //[1, 2, 3]
    }


方案四:使用 Apache Commons Collections


    public static void main(String[] args) {
        Integer[] myArray = {1, 2, 3};
        List<Integer> list = new ArrayList<>();
        CollectionUtils.addAll(list, myArray);
        System.out.println(list); // 
    }


它的addAll()是个重载方法,数组List都行。它内部其实就是把myArray遍历了一遍而已,因此int或者Integer数组都无所谓~


image.png



注意:java.util.Collections可以没有这个能力把Array转为List,但是它的singleton(T o)等方法也是非常好用的,但也是只读视图哦~


Java泛型经典面试题


下面两个代码片段有问题吗,为什么?
public static void main(String[] args) {
    //Part 1
    List<Object> obj = new ArrayList<Long>();
    obj.add("I love Android!");
    // Part 2
    Object[] objArray = new Long[1];
    objArray[0] = "I love Android!"; //编译通过,运行出错
}


答:上面 Part 1 编译出错,Part 2 编译 OK,运行出错。

原因:因为 List 和 ArrayList 没有继承关系,而 Java 的数组是在运行时类型检查的(这就是为何推荐使用集合,少用数组的原因之一)。


如何把 int 值放入 ArrayList list = new ArrayList(); 的 list 列表中?


本题的考点是泛型擦除。下面介绍两种方案:

// 通过泛型擦除兼容性实现如下:
public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    List list1 = list; // 使用无泛型的List重新引用(擦除了泛型)
    list1.add(12);
    System.out.println(list1.get(0));
}
// 通过反射实现如下:
public static void main(String[] args) throws Exception {
    List<String> list = new ArrayList<>();
    Class clazz = list.getClass();
    Method m = clazz.getMethod("add", Object.class);
    m.invoke(list, 100);
}


泛型擦除到底擦除了哪些信息?


这个答案上面已经说过了


既然泛型类型在编译时就被擦除了,那类似 Gson 这种 json 解析框架是如何解析数据到泛型类型 Bean 结构的呢?


本题其实一箭双雕,即考察了对于 Gson 框架是否熟悉又考察了 Java 泛型与反射的关系及泛型的实质。

由于在运行期间无法通过 getClass() 得知 T 的具体类型,所以 Gson 通过借助 TypeToken 类来解决这个问题,使用样例如下:

public static void main(String[] args) throws Exception {
    List<String> list = new ArrayList<String>();
    list.add("java");
    // 序列化
    Type type = new TypeToken<ArrayList<String>>() {}.getType();
    String gStr = new Gson().toJson(list, type);
    System.out.println(gStr); // ["java"]
    // 反序列化
    List<String> gList = new Gson().fromJson(gStr, type);
    System.out.println(gList); // [java]
}


可以看到 TypeToken 的使用非常简单,只用将需要获取类型的泛型类作为 TypeToken 的泛型参数构造一个匿名的子类就可以通过 getType() 方法获取到我们使用的泛型类的泛型参数类型。


这种做法的本质:利用了类上面的泛型是不会“擦除”的特性来做的,所以是new了一个TypeToken的一个匿名子类嘛~


下面程序的输出是什么?为什么?


public static void main(String[] args) throws Exception {
    ParameterizedType type = (ParameterizedType) Bar.class.getGenericSuperclass();
    System.out.println(type.getActualTypeArguments()[0]);
    ParameterizedType fieldType = (ParameterizedType) Foo.class.getField("children").getGenericType();
    System.out.println(fieldType.getActualTypeArguments()[0]);
    ParameterizedType paramType = (ParameterizedType) Foo.class.getMethod("foo", List.class).getGenericParameterTypes()[0];
    System.out.println(paramType.getActualTypeArguments()[0]);
    System.out.println(Foo.class.getTypeParameters()[0].getBounds()[0]);
}
class Foo<T extends CharSequence> {
    public List<Bar> children = new ArrayList<>();
    public List<StringBuilder> foo(List<String> foo) {
        return null;
    }
    public void bar(List<? extends String> param) {
        //empty
    }
}
class Bar extends Foo<String> {
}


答:其运行结果如下。

class java.lang.String

class Demo$Bar

class java.lang.String

interface java.lang.CharSequence


从这个例子可以看出:


  1. 表面结论:泛型类型的每一个类型参数都被保留了,而且在运行期可以通过反射机制获取到
  2. 深层结论:泛型的擦除机制实际上擦除的是除结构化信息外的所有东西(结构化信息指与类结构相关的信息,而不是与程序执行流程有关的,即与类及其字段和方法的类型参数相关的元数据都会被保留下来通过反射获取到)


总结


Java的泛型一直是被吐槽的对象,它确实很渣不好用,容易出错。但是存在即合理,你不能改变它那请好好接受它。

编码的时候遵循一个原则:该写泛型的地方务必写上泛型,能使得你对泛型的理解更加的深刻。这是一个非常良好的编码习惯~


相关文章
|
4月前
|
存储 Java 编译器
Java泛型类型擦除以及类型擦除带来的问题
泛型擦除是指Java编译器在编译期间会移除所有泛型信息,使所有泛型类型在运行时都变为原始类型。例如,`List&lt;String&gt;` 和 `List&lt;Integer&gt;` 在JVM中都视为 `List`。因此,通过 `getClass()` 比较两个不同泛型类型的 `ArrayList` 实例会返回 `true`。此外,通过反射调用 `add` 方法可以向 `ArrayList&lt;Integer&gt;` 中添加字符串,进一步证明了泛型信息在运行时被擦除。
98 2
|
5月前
|
Java API
[Java]泛型
本文详细介绍了Java泛型的相关概念和使用方法,包括类型判断、继承泛型类或实现泛型接口、泛型通配符、泛型方法、泛型上下边界、静态方法中使用泛型等内容。作者通过多个示例和测试代码,深入浅出地解释了泛型的原理和应用场景,帮助读者更好地理解和掌握Java泛型的使用技巧。文章还探讨了一些常见的疑惑和误区,如泛型擦除和基本数据类型数组的使用限制。最后,作者强调了泛型在实际开发中的重要性和应用价值。
136 0
[Java]泛型
|
5月前
|
存储 安全 Java
🌱Java零基础 - 泛型详解
【10月更文挑战第7天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
46 1
|
5月前
|
Java 语音技术 容器
java数据结构泛型
java数据结构泛型
53 5
|
5月前
|
存储 Java 编译器
Java集合定义其泛型
Java集合定义其泛型
36 1
|
5月前
|
存储 Java 编译器
【用Java学习数据结构系列】初识泛型
【用Java学习数据结构系列】初识泛型
38 2
|
5月前
|
安全 Java 编译器
Java基础-泛型机制
Java基础-泛型机制
48 0
|
5月前
|
Java
【Java】什么是泛型?什么是包装类
【Java】什么是泛型?什么是包装类
49 0
|
22天前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
154 60
【Java并发】【线程池】带你从0-1入门线程池
|
11天前
|
存储 网络协议 安全
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
63 23