附:关于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. 当传入一个源生数据类型数组时,asList真正得到的参数就不是数组的元素了,而是数组对象本身
- 使用集合的修改方法: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数组都无所谓~
注意: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
从这个例子可以看出:
- 表面结论:泛型类型的每一个类型参数都被保留了,而且在运行期可以通过反射机制获取到
- 深层结论:泛型的擦除机制实际上擦除的是除结构化信息外的所有东西(结构化信息指与类结构相关的信息,而不是与程序执行流程有关的,即与类及其字段和方法的类型参数相关的元数据都会被保留下来通过反射获取到)
总结
Java的泛型一直是被吐槽的对象,它确实很渣不好用,容易出错。但是存在即合理,你不能改变它那请好好接受它。
编码的时候遵循一个原则:该写泛型的地方务必写上泛型,能使得你对泛型的理解更加的深刻。这是一个非常良好的编码习惯~