从零起步,真正理解集合与泛型

零、前言

List<E>的尖括号是什么意思?
什么是泛型?如何去理解它?
泛型有什么优势?面试

以上问题能够在本文中找到答案。算法

1、基础知识

在全部编程语言中,都有这么一种东西,叫作“数组”,好比C++中就有数组编程

可是呢,传统数组最大的缺点就是:它的长度是固定的,若是不知道一个数组须要容纳多大的数据量,编写程序时就会很困难,数组建的太大了,就会浪费内存资源,数组过小了,容量不够就会报错。segmentfault

因此,前人发明了各类各样的大小可变的数组,也就是“集合”,Collection数组

Java集合类型一览(选自《HeadFirstJava》):安全

Scan - 2020-04-11 17_05_04.jpg

正由于集合没有固定的大小,而且有着许多很方便的API,所以集合被普遍使用。(今后之后,原始的数组就只出如今大学的C++课堂之中了)数据结构

众多的集合中,较为常见的就是ArrayList,咱们就拿它来举例。dom

假如,须要一个集合来储存一些字符串,而后遍历输出它们:编程语言

public static void main(String[] args) {
    // new对象,初始化
    ArrayList<String> array = new ArrayList<String>();
    
    // 加入元素
    array.add("ABC");
    array.add("DEF");
    array.add("Test");
    array.add("Debug");
    
    //遍历输出
    for (String i: array) {
        System.out.println(i);
    }
}

那么问题来了,ArrayList<String>中的<String>有什么做用呢?ide

若是把刚才的<String>去掉或者删掉,会发生什么呢?

  • 把尖括号里面改为Double的包装类,但传入的仍是String:
public static void main(String[] args) {
    // 此处的集合是Double类型
    ArrayList<Double> array = new ArrayList<Double>();
    array.add("ABC");
    array.add("DEF");
    array.add("Test");
    array.add("Debug");
    for (String i: array) {
        System.out.println(i);
    }
}

答案是:没法经过编译,传入的类型与规定类型不匹配。

方法 Collection.add(Double)不适用
  (参数不匹配; String没法转换为Double)
  • 把尖括号里面去掉,但传入的仍是String:
public static void main(String[] args) {
    // 此处的集合没有声明类型
    ArrayList array = new ArrayList();
    array.add("ABC");
    array.add("DEF");
    array.add("Test");
    array.add("Debug");
    for (String i: array) {
        System.out.println(i);
    }
}

答案是:仍然没法经过编译,没有进行传入类型的安全检查

咱们能够从中看出一些规律:尖括号<>里面的内容,彷佛是对这个集合的类型作了规定。

那为何没有了这个尖括号,仍是不能经过编译呢?
理论上,没有条件的约束,应该能够储存任何类型的数据啊?

就像这样:

public static void main(String[] args) {
    // 没有类型约束
    ArrayList array = new ArrayList();
    // 字符串
    array.add("ABC");
    // 整数型
    array.add(123);
    // 浮点数
    array.add(123.456);
    // 对象
    array.add(new Object());
    
    for (String i: array) {
        System.out.println(i);
    }
}

固然,上面的代码是错的,100%不能经过编译。

假设能编译,那么问题又出现了:在不知道某个元素的类型的状况下,怎么使用统一的方法去处理它呢?好比Java内置的Print能够输出字符串,但怎么用System.out.println(i)来打印一个对象的内容呢?

因此这种约束条件,最大的好处就是增长了安全性,就比如不一样口径的瓶子,只能装合适的物品,String类型的瓶子只能装String类型的对象

这就是今天的话题,泛型

2、初识泛型

泛型是什么?从字面意思解释:“泛”是普遍的,“型”是一种特定类型,连起来就是“普遍的特定类型的对象”。

说它普遍,不管是什么对象,是只要符合规定的类型,集合(ArrayList)就能够处理它;
说它特定,是由于它必须严格符合约定的类型,不然就会被编译器拦截下来,没法经过编译。

若是说集合是各类瓶子,那么泛型就是对于瓶子的约束。这种约束是为了安全,若是没有了泛型的约束,集合就能够容纳任何类型的对象,啥均可以装进去,就像把绵羊放进老虎的集合中。

那么,怎么使用泛型呢?

首先,不一样于方法的参数,参数是针对于某个方法来讲的,而泛型是针对一个类或对象来讲的。
再解释一下就是:把一个参数传给某个方法,这个方法接收到参数以后,就能够处理它;把一个泛型传给某个类,这个类接收到这个泛型以后,就能够new出一个只能处理这个泛型的对象。

因此泛型参数有类似之处,关键点就是在尖括号<>里加上

泛型能够用在集合中:

  • new一个对象:
new ArrayList<String>
  • new对象并声明变量:
// 声明stringList变量是字符串类型的集合
// 而且连接到一个new出来的对象上
ArrayList<String> stringList = new ArrayList<String>;

泛型还能用在方法的参数中:

// 此方法接收的参数是List集合,
// 但必须是字符串类型的集合才能够
public void printString (List<String> list) {
    ...
}

为一个集合添加数据:

// 添加: add
// 删除: remove
// 其余用法请参考源码,写的很清楚

array.add("ABC");

3、深刻泛型——原理

基本的用法说完了,那么就来看看,泛型的世界里有什么规律。

在以前的《从零起步,真正理解Javascript回调函数》中说到:

程序 = 数据结构 + 算法

若是执行一个写死的程序(好比输出HelloWorld),那么不管怎样运行,结果都是同样的。这样的程序是没有意义的。

那么,若是想让某个方法发挥做用,就要让它是能够“变化”的,若是把输出的数据单独拿出来,做为函数的参数,而方法不变,那么就能够根据不一样的参数,用一样的方法输出不一样的结果。
这就是函数。

反之,若是待处理的数据不变,而处理数据的方法改变,把方法做为参数,就有了回调函数。

以上两种变化都是对于函数的,而泛型是对于来讲的。

咱们看一看ArrayList的源码(部分):

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
    
    public E get(int index) {
        Objects.checkIndex(index, this.size);
        return this.elementData(index);
    }

    public E set(int index, E element) {
        Objects.checkIndex(index, this.size);
        E oldValue = this.elementData(index);
        this.elementData[index] = element;
        return oldValue;
    }

}

能够看到,这个类的许多方法里面,都有一个E,这个E是就是泛型,它不是一个具体的类型,而是在new对象的时候,传入什么泛型,就是用什么泛型。
在类定义的时候,有:

ArrayList<E>

既然定义的时候是E,下面的方法里也是E,那么,在初始化ArrayList的时候,传入什么泛型,下面的E就会变化成什么泛型,好比

ArrayList<String> stringList = new ArrayList<String>;

此时传入的是String,那么上面的代码,等同于发生了以下变化:

// 全部的 E 都被替换为特定的类型

public class ArrayList<String> extends AbstractList<String> implements List<String>, RandomAccess, Cloneable, Serializable {

    public String get(int index) {
        Objects.checkIndex(index, this.size);
        return this.elementData(index);
    }

    public String set(int index, String element) {
        Objects.checkIndex(index, this.size);
        String oldValue = this.elementData(index);
        this.elementData[index] = element;
        return oldValue;
    }

}

此时,这个被new出来的集合对象,就只能处理字符串类型的数据了。

(有没有感受像函数重载?泛型就能够想象成类的重载,并且不用手动写重载代码)

知道了泛型的原理,也就很容易明白一个道理:
泛型并不是只能用于数组和集合中,咱们也能够建立使用泛型的类,只不过因为Java的API很丰富,没有必要再去本身写了。

4、最终篇 集合套娃

已经讲了不少知识,接下来来实践一下,若是能明白这个实例,就真正理解泛型了。

有这么一道面试题:

// 要求:给变量赋值

List<Map<String,List<String>>> list = newArrayList<Map<String,List<String>>>();

要理解这道题,咱们还须要一些知识的补充。

Java集合类型一览(选自《HeadFirstJava》):

Scan - 2020-04-11 17_05_04.jpg

集合有这么多种,可是有些是,有些是接口
众所众知,接口是不能New的,因此题目中的List和Map都是不能new的,但咱们能够new它们的实现类,而后赋值给这个类型的变量。

接着看题,变量名是list,类型是List集合,泛型是<Map<String,List<String>>>

那么怎么理解呢?就像洋葱“同样一层一层的剥开它的心”。

图片.png

  • 最外层,是一个List,它的泛型是<Map>
  • 第二层,是一个Map,它的泛型是<String, List>
  • 最里层,又是一个List,它的泛型是<String>

Map为何有两个泛型呢?
请参考函数的两个参数,Map就至关于两个参数的函数
因为Map具备特殊性,它是“键值对”,Map的每个“”都必须有一个惟一的“”,因此要写成 Map<键,值>

知道了它的组成,就能够去给它赋值了,只不过赋值的过程相反,是从里到外的,先建立里面,再逐层建立外面。

第一步,建立最里面的List:

// new一个ArrayList对象
List inside = new ArrayList<String> ();

// 添加一个String元素
inside.add("This is test String");

第二步,把这个有值的List,放到一个Map中

// new一个HashMap对象
Map middle = new HashMap<String, List<String>> ();

// 添加第一步的List元素
middle.add("key", inside);

这样就获得了一个有值的Map。
第三步,把这个Map赋值给外层的List。

// new一个ArrayList对象,这就是题目中的list变量
List list = new ArrayList< Map< String, List<String> > >();

// 添加第二步的Map元素
outside.add(middle);

至此,一个集合套娃已完成。

总结

见到泛型别烦恼,
这个功能很是好。
泛型若想用得好,
关键在于尖括号。

若是用函数来类比:

  • 函数是使用相同的方法,经过改变数据来产生不一样结果。
  • 回调是使用相同的数据,经过改变方法来产生不一样的结果。
  • 泛型是使用相同的类,经过改变对象的类型来产生不一样的结果。
相关文章
相关标签/搜索