第十四章 集合框架、语法糖和泛型
一、集合框架
1.概念
Java 集合框架(Java Collections Framework,JCF)是为表示和操作集合而规定的一种统一的标准的体系结构
Java 集合框架包含如下内容:
- 接口:是代表集合的抽象数据类型,例如 Collection、List、Set、Map等。之所以定义多个接口,是为了以不同的方式操作集合对象
- 实现(类):是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:ArrayList、LinkedList、HashSet、HashMap
- 算法:是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序。这些算法被称为多态,因为相同的方法可以在相似的接口上有不同的实现
从上面的集合框架图可以看到,Java 集合框架主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合,另一种是图(Map),存储键/值对映射
- Collection 中可以容纳一组集合元素(Element)
- Map 没有继承 Collection 接口,与 Collection 是并列关系。Map 提供键(key)到值的映射。一个 Map
中不能包含相同的键,每个键只能映射一个值
2.接口
集合接口
接口 | 描述 |
---|---|
Iterable | 实现这个接口允许对象能进行迭代(按顺序访问线性结构中的每一项) |
Collection | Collection 接口存储一组不唯一,无序的对象 |
List | List 接口存储一组不唯一,有序(按照插入顺序存储数据)的对象 |
Queue | Queue 在 Collection 的基础上添加了增删改查操作,并且队列默认使用FIFO(先进先出)规则 |
Deque | Deque 是一种线性集合,可以从两端操作的队列 |
Set | Set 接口存储一组唯一,无序的对象 |
SortedSet | SortedSet 接口存储有序的集合(数学概念上的) |
NavigableSet | NavigableSet 接口具有为指定搜索目标报告最接近匹配项的导航方法 |
集合类
类 | 描述 |
---|---|
ArrayList | ArrayList 是一个动态数组,也是我们最常用的集合。它允许任何符合规则的元素插入甚至包括 null。每一个 ArrayLis t都有一个初始容量(10),该容量代表了数组的大小。随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。 ArrayList 擅长于随机访问,同时 ArrayList 是非同步的 |
Vector | 与 ArrayList 相似,但是 Vector 是同步的。所以说 Vector 是线程安全的动态数组。它的操作与 ArrayList 几乎一样 |
Stack | Stack 继承自 Vector,实现一个后进先出的堆栈。Stack 提供 5 个额外的方法使得 Vector 得以被当作堆栈使用。基本的 push 和 pop 方法,还有 peek 方法得到栈顶的元素,empty 方法测试堆栈是否为空,search 方法检测一个元素在堆栈中的位置。Stack 刚创建后是空栈 |
LinkedList | LinkedList 是一个双向链表,所以它除了有 ArrayList 的基本操作方法外还额外提供了 get,remove,insert 方法在 LinkedList 的首部或尾部。由于实现的方式不同,LinkedList 不能随机访问,它所有的操作都是要按照双重链表的需要执行。在列表中索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。这样做的好处就是可以通过较低的代价在 List 中进行插入和删除操作。与 ArrayList 一样,LinkedList 也是非同步的 |
ArrayDeque | ArrayDeque 实现了 Deque 接口,使用了可变数组,所以没有容量上的限制。需要注意的是,ArrayDeque不支持 null 值,而且 ArrayDeque 是非同步的的 |
HashSet | HashSet 底层通过包装 HashMap 来实现,HashSet 在添加一个值的时候,实际上是将此值作为HashMap 中的 key 来进行保存。HashSet 不保证元素的顺序(这里所说的没有顺序是指元素插入的顺序与输出的顺序不一致)。HashSet 按 Hash 算法来存储集合的元素,因此具有很好的存取和查找性能。HashSet 允许使用 null 元素,且 HashSet 是非同步的 |
LinkedHashSet | LinkedHashSet 具有可预测的迭代顺序,也就是我们插入的顺序。LinkedHashSet 是非同步的 |
EnumSet | EnumSet 是一个专为枚举设计的集合类,EnumSet 中的所有元素都必须是指定枚举类型的枚举值,EnumSet 的集合元素也是有序的。EnumSet 集合不允许加入 null 元素 |
TreeSet | TreeSet 是基于 TreeMap 实现的。TreeSet 中的元素支持 2 种排序方式:自然排序或者根据创建 TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。TreeSet 是非同步的 |
图接口
接口 | 描述 |
---|---|
Map | Map 是由一系列键值对组成的集合,提供了 key 到 Value 的映射 |
SortedMap | SortedMap 接口使 Key 保持在升序排列 |
NavigableMap | NavigableMap 接口使导航存储在映射中的键和值成为可能 |
图类
类 | 描述 |
---|---|
Dictionary | Dictionary 类是一个抽象类,用来存储键/值对,作用和Map类相似 |
Hashtable | HashTable 是较为远古的使用 Hash 算法的容器结构了,现在基本已被淘汰,单线程转为使用 HashMap ,多线程使用ConcurrentHashMap。HashTable 的操作几乎和 HashMap 一致,主要的区别在于 HashTable 为了实现多线程安全,在几乎所有的方法上都加上了 synchronized 锁,而加锁的结果就是 HashTable 操作的效率十分低下。Hashtable可以插入 null |
HashMap | HashMap 是一个散列表,它存储的内容是键值对 (key-value) 映射。该类实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步 |
LinkedHashMap | LinkedHashMap 保留插入的顺序,具有可预知的迭代顺序,允许使用null值和null键 |
WeakHashMap | 使用弱密钥的哈希表 |
EnumMap | EnumMap 是保存枚举类型的Map,它要求 map 的 key 是枚举类型 |
AbstractMap | AbstractMap 是一个抽象类,它实现了Map接口的绝大部分API函数 |
TreeMap | TreeMap 实现 SortMap 接口,内部实现是红黑树。能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用 Iterator 遍历 TreeMap 时,得到的记录是排过序的。TreeMap 不允许 key 的值为 null,且是非同步的 |
二、语法糖
1.概念
语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机专家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性
Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等
2.解语法糖
语法糖的存在主要是方便开发人员使用,但其实,Java 虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖
在 com.sun.tools.javac.main.JavaCompiler 的源码中,你会发现在 compile() 中有一个步骤调用 desugar(),这个方法就是用于解语法糖
三、泛型
1.概念
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?
顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法
2.泛型类
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map
泛型类在实例化类的时候指明泛型的具体类型
泛型类的基本格式:
class 类名称 <泛型标识>{
private 泛型标识 变量类型 var;
.....
}
}
实例:
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
Box<String> stringBox = new Box<String>();
integerBox.add(new Integer(10));
stringBox.add(new String("Sisyphus"));
System.out.printf("整型值为 :%d\n\n", integerBox.get());
System.out.printf("字符串为 :%s\n", stringBox.get());
}
}
运行结果:
整型值为:10
字符串为:Sisyphus
3.泛型接口
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
实现泛型接口的类(未传入泛型实参):
/**
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
* 即:class FruitGenerator<T> implements Generator<T>{
* 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
*/
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
实现泛型接口的类(传入泛型实参):
/**
* 传入泛型实参时:
* 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口 Generator<T>
* 但是我们可以为T传入无数个实参,形成无数种类型的 Generator 接口。
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
* 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
*/
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
4.泛型方法
你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用
定义泛型方法的规则:
- 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前
- 每个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开
- 类型参数只能代表引用型类型,不能是基本类型
实例:
public class GenericMethodTest
{
// 泛型方法 printArray
public static < E > void printArray( E[] inputArray )
{
// 输出数组元素
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
public static void main( String args[] )
{
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3, 4, 5 };
Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
System.out.println( "整型数组元素为:" );
printArray( intArray ); // 传递一个整型数组
System.out.println( "\n双精度型数组元素为:" );
printArray( doubleArray ); // 传递一个双精度型数组
System.out.println( "\n字符型数组元素为:" );
printArray( charArray ); // 传递一个字符型数组
}
}
运行结果:
整型数组元素为:
1 2 3 4 5双精度型数组元素为:
1.1 2.2 3.3 4.4字符型数组元素为:
H E L L O
5.类型通配符
类型通配符一般是使用 ? 代替具体的类型参数。例如 List<?> 在逻辑上是List,List 等所有List<具体类型实参>的父类
import java.util.*;
public class GenericTest {
public static void main(String[] args) {
List<String> name = new ArrayList<String>();
List<Integer> age = new ArrayList<Integer>();
List<Number> number = new ArrayList<Number>();
name.add("icon");
age.add(18);
number.add(314);
getData(name);
getData(age);
getData(number);
}
public static void getData(List<?> data) {
System.out.println("data :" + data.get(0));
}
}
运行结果:
data :icon
data :18
data :314
解析:因为getData()方法的参数是List类型的,所以name,age,number都可以作为这个方法的实参,这就是通配符的作用
类型通配符上限通过形如List来定义,如此定义就是通配符泛型值接受Number及其下层子类类型
import java.util.*;
public class GenericTest {
public static void main(String[] args) {
List<String> name = new ArrayList<String>();
List<Integer> age = new ArrayList<Integer>();
List<Number> number = new ArrayList<Number>();
name.add("icon");
age.add(18);
number.add(314);
//getUperNumber(name);//1
getUperNumber(age);//2
getUperNumber(number);//3
}
public static void getData(List<?> data) {
System.out.println("data :" + data.get(0));
}
public static void getUperNumber(List<? extends Number> data) {
System.out.println("data :" + data.get(0));
}
}
运行结果:
data :18
data :314
解析:getUperNumber(name);会报错,因为 getUperNumber() 方法中的参数已经限定了参数泛型上限为 Number,所以泛型为 String 的参数是不在这个范围之内的
特别地,类型通配符下限通过形如 List<? super Number> 来定义,表示类型只能接受 Number 及其三层父类类型
6.应用场景
帮助数据类型的检查
String[] a = new String[5];
a[0] = 1;
a[1] = 1.6;
a[2] = 'c';
我们在使用数组时,如果像上面那样赋值,那么还未编译就会报错,方便程序员检查
List list = new ArrayList();
list.add("Sisyphus");
list.add(1);
list.add(1.6);
list.add('c')
我们在使用集合时,无论我们赋什么类型的值,集合都不会进行约束,那如果我们想像数组那样约束该怎么做呢?
我们可以引入泛型
List<String> list = new ArrayList<>();
list.add("Sisyphus");
list.add(1);
list.add(1.6);
list.add('c');
这样我们赋值的时候,只要是非字符串类型都会报错
泛型可以写出更加通用的代码`
如果我们需要遍历打印 3 个数据类型均不同的数组,我们可以通过重载的方式,写 3 个方法
Integer[] a = {1,2,3};
Double[] b = {1.1,2.2,3.3};
String[] c = {"Amy","Bod","Chris"};
public void print(Integer[] arr){
for(Integer i : arr){
System.out.println(i);
}
}
public void print(Double[] arr){
for(Double i : arr){
System.out.println(i);
}
}
public void print(String[] arr){
for(String i : arr){
System.out.println(i);
}
}
但这样代码太冗余了,我们完全可以采用泛型方法
Integer[] a = {1,2,3};
Double[] b = {1.1,2.2,3.3};
String[] c = {"Amy","Bod","Chris"};
public <E>void print(E[] arr){
for(<E> i : arr){
System.out.println(i);
}
}