《码出高效》系列笔记(四):数据结构与集合的数组和泛型

本贴最后更新于 1721 天前,其中的信息可能已经时移俗易

EasyCoding.jpg

良好的编码风格和完善统一的规约是最高效的方式。


前言

本篇汲取了本书中较为精华的知识要点和实践经验加上读者整理,作为本系列里的第四篇章第二节:数据结构与集合的数组和泛型篇。

本系列目录

数组与集合

数组是一种顺序表,可以使用索引下标进行快速定位并获取指定位置的元素。

为什么下标从 0 开始?

因为这样需要计算偏移量需要从当前下标减 1 的操作,加减法运算对 CPU 是一种双数运算,在数组下标使用频率很高的场景下,该运算方式十分耗时。在 Java 的体系中,数组一旦分配内存后无法扩容。

String[] args1 = {"a", "b"};
String[] args2 = new String[2];
args2[0] = "a";
args2[1] = "b";

以上代码一般是数组的两种初始化方式,第一种是静态初始化,第二种是动态初始化。数组的容量大小随着数组对象的创建就固定了。

数组的遍历优先推荐 JDK5 引进的 foreach 方式,即 for(e : obj); JDK8 以上可以使用 stream 操作

Arrays.stream(args1).forEach(str -> System.out.println(str));
Arrays.stream(args1).forEach(System.out::println);

数组转集合

将数组转集合后,不能使用集合的某些方法,以 Arrays.asList() 为例,不能使用其修改集合 addremoveclear 方法,但是可以使用 set 方法。

String[] args1 = {"a", "b"};
List<String> asList = Arrays.asList(args1);
asList.set(1, "c");
System.out.println(asList);
asList.add("c");
asList.remove(0);
asList.clear();
System.out.println(asList);

后面会输出 UnsupportedOperationException 异常。

Arrays.asList() 体现的是适配器模式,其实是 Arrays 的一个名为 ArrayList 的内部类(阉割版),继承自 AbstractList 类,实现了 setget 方法。但是其他部分方法未实现所以会抛出该父类 AbstractList 的异常。

String[] args1 = {"a", "b"};
List<String> e1 = Arrays.asList(args1);
List<String> e2 = new ArrayList<>(2);
e2.add("a");
e2.add("b");
// 第一处
System.out.println(e1.getClass().getName());
// 第二处
System.out.println(e2.getClass().getName());

实际控制台打印情况:

  1. java.util.Arrays$ArrayList
  2. java.util.ArrayList

数组转集合在需要添加元素的情况下,利用 java.util.ArrayList 创建一个新集合。

String[] args = {"a", "b"};
List<String> list = new ArrayList<>(Arrays.asList(args));

集合转数组

集合转数组更加的可控。

List<String> e1 = new ArrayList<>(2);
e1.add("c");
e1.add("d");
String[] args = new String[1];
String[] args2 = new String[2];
e1.toArray(args);
// 第一处
System.out.println(Arrays.asList(args));
e1.toArray(args2);
// 第二处
System.out.println(Arrays.asList(args2));

实际控制台打印情况:

  1. [null]
  2. [c, d]

不同的区别在于即将复制进去的数组容量是否足够,如果容量不等,则弃用该数组,另起炉灶。

集合与泛型

泛型与集合的联合使用,可以把泛型的功能发挥到极致。

List list1 = new ArrayList(3);
list1.add(new Integer(666));
list1.add(new Object());
list1.add("666");

List<Object> list2 = new ArrayList<>(3);
list2.add(new Integer(666));
list2.add(new Object());
list2.add("666");

List<Integer> list3 = new ArrayList<>(3);
list3.add(new Integer(666));
// 以下都是编译出错
list3.add(new Object());
list3.add("666");

List<?> list4 = new ArrayList<>(3);
list4.remove(0);
list4.clear();
// 以下都是编译出错
list4.add(new Integer(666));
list4.add(new Object());
list4.add("666");

List<?> 是一个泛型,在没有赋值之前,表示它可以接收任何类型的集合赋值,赋值之后就不能随便往里面添加元素了,但可以 remove 和 clear。

List<T> 最大的问题就是只能放置一种类型,如果要实现多种受泛型约束的类型,可以使用 <? extends T><? super T> 两种语法,但是两者的区别非常微妙。

  • <? extends T> 是 Get First,适用于消费集合元素为主的场景;
  • <? super T> 是 Put First,适用于生产集合元素为主的场景。

<? extends T> 可以赋值给任何T 以及 T 子类的集合,上界为 T。取出来的类型带有泛型限制,向上转型为 T。null 可以表示任何类型,所以除了 null 外,任何元素都不得添加进 <? extends T> 集合内。

<? super T> 可以赋值给任何T 以及 T 的父类集合,下界为 T。在生活中,投票选举类似 <? super T> 的操作。选举代表时,你只能往里投票,取数据时,根本不知道是谁的票,相遇泛型丢失。

extends 的场景是 put 功能受限,而 super 的场景是 get 功能受限。

extends 与 super 的差异

假设有一个斗鱼 TV 平台,拥有一个 DOTA2 板块,其下有一个恶心人的 D 能儿主播:谢彬 DD。

那我们从代码里可以这样写:

@Test
public void main() {

    List<DouYu> douYu = new ArrayList<>();
    List<DotA2> dotA2 = new ArrayList<>();
    List<DD> dd = new ArrayList<>();
    douYu.add(new DouYu());
    dotA2.add(new DotA2());
    dd.add(new DD());
    
    // 第一处,编译出错
    List<? extends DotA2> extendsDotA2FromDouYu = douYu;
    List<? super DotA2> superDotA2FromDouYu = douYu;

    List<? extends DotA2> extendsDotA2FromDotA2 = dotA2;
    List<? super DotA2> superDotA2FromDotA2 = dotA2;

    List<? extends DotA2> extendsDotA2FromDD = dd;
    // 第二处,编译出错
    List<? super DotA2> superDotA2FromDD = dd;

}

三个类的继承关系说明 DD < DotA2 < DouYu < Object

第一处编译出错,因为只能赋值给 T 以及 T 的子类,上界是 DotA2 类。DouYu 类明显不符合 extends DotA2 类的情况。不能把 douYu 对象赋值给 <? extends DotA2>,因为 List<DouYu> 不只只有 DotA2 板块,还有吃 ♂ 鸡、颜 ♂ 值区、舞 ♂ 蹈区这些板块。

第二处编译出错,因为只能赋值给 T 以及 T 的父类,DD 类属于 DotA2 的子类,下界只能 DotA2 类的对象。

// 以下<? extends DotA2>类型的对象无法进行add操作,编译出错
extendsDotA2FromDotA2.add(new DD());
extendsDotA2FromDotA2.add(new DotA2());
extendsDotA2FromDotA2.add(new DouYu());

superDotA2FromDotA2.add(new DD());
superDotA2FromDotA2.add(new DotA2());
// 该处编译出错,无法添加
superDotA2FromDotA2.add(new DouYu());

除了 null 以外,任何元素都不能添加进 <? extends T> 集合内。<? super T> 可以放,但是只能放进去自身以及子类

Object obj1 = extendsDotA2FromDotA2.get(0);
DotA2 obj2 = extendsDotA2FromDotA2.get(0);

Object obj3 = extendsDotA2FromDD.get(0);
// 该处编译出错,无法添加
DD obj4 = extendsDotA2FromDD.get(0);

首先 <? super T> 可以进行 Get 操作返回元素,但是类型会丢失。<? extends T> 可以返回带类型的元素,仅限自身及父类,子类会被擦除。

小总结

对于一个笼子,只取不放,属于 Get First,应采用 <? extends T>;只放不取,属于 Put First,应采用 <? super T>

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...