目录
泛型和类型安全的集合
基本概念
添加一组元素
打印集合
List
Iterator(迭代器)
本笔记参考自: 《On Java 中文版》
在进行程序设计时我们会发现,程序总是会根据某些在运行时才能知道的条件来创建新的对象。这意味着,我们必须能够随时随地创建任意数量的对象。
Java提供了几种方法来持有对象(的引用),其中之一就是数组。数组的优点在于其的高效,但其本身却会受到自身大小固定的制约。实际上,Java.util库提供了一组集合类来解决这一问题,其中包括的基本类List、Set、Queue和Map也被称为容器类。
Java并没有直接为集合提供关键字支持。
泛型和类型安全的集合
在Java 5之前,编译器允许向集合中插入不正确的类型。在举例之前需要先对ArrayList类进行一些必要的说明:
ArrayList是是一种泛型类型,它实现了迭代器(Iterable)、Collection和List等接口。泛型是一种参数化类型的机制,在这种机制下,形参的数据类型也会成为可传输的参数。因为泛型的存在,ArrayList可以存储任何类型的对象,而不需要在使用前进行类型转换。(本段参考林二月er的博客、讯飞星火提供的信息)
ArrayList使用一个数值来查找对应对象。从某种意义上,ArrayList将数值和对象关联起来了。
下例中出现的ArrayList是一个反例,它没有使用泛型:
import java.util.ArrayList;
class Apple {
private static long counter;
private final long id = counter++;
public long id() {
return id;
}
}
class Orange {
}
public class AppleAndOrangesWithoutGenerics {
@SuppressWarnings("unchecked") // 参数unchecked表示只有“unchecked”警告应该被忽略
public static void main(String[] args) {
ArrayList apples = new ArrayList<>();
for (int i = 0; i < 3; i++)
apples.add(new Apple());
apples.add(new Orange()); // 注意:插入的是一个Orange类型,这本来应该是错误的操作
for (Object apple : apples) {
((Apple) apple).id(); // 需要强制类型转换,此时Orange只有在运行时才会被检测出来
}
}
}
上述程序的错误不会在编译时被发现,这个报错会出现在运行时:
Apple和Orange唯一的联系在于它们都继承了Object类。但ArrayList持有的也是Object,所以无论是Apple类还是Orange类都能被加入其中(并且不会引发报错)。此时会从ArrayList中取出Object类型的引用,若想使用引用还必须强制类型转换。也正是在运行时,程序尝试将Orange对象转型为Apple时才会发现错误。
若要定义一个持有Apple对象的ArrayList,应该使用:
ArrayList<Apple>
尖括号包围的是类型参数(可以有多个),它指定了集合实例中可以保存的类型。
下面是修改后的例子:
import java.util.ArrayList;
public class AppleAndOrangesWithGenerics {
public static void main(String[] args) {
ArrayList<Apple> apples = new ArrayList<>();
for (int i = 0; i < 3; i++)
apples.add(new Apple());
// 此时,下方的做法就会引发编译时错误
// apples.add(new Orange());
for (Apple apple : apples)
System.out.println(apple.id());
}
}
程序执行的结果如下:
上述程序中,出现了语句 ArrayList<Apple> apples = new ArrayList<>(); 。这种语句因为<>符号,也被称为“钻石语法”。在Java 7之前,这种语法的使用会更加复杂:
ArrayList<Apple> apples = new ArrayList<Apple>(); // 需要在表达式的两侧重复写出类型声明
随着类型的深入,上述这种语法会变得越发复杂。因此最后被现在这种更为简便的语法代替了。
泛型的存在使得我们从List(接口)中获取对象时,无须进行转型了。因为List知道它所持有的类型。另外,在泛型的情况下,向上转型也可以生效:
import java.util.ArrayList;
class GrannySmith extends Apple {
}
class Gala extends Apple {
}
class Fuji extends Apple {
}
class Braeburn extends Apple {
}
public class GenericeAndUpcasting {
public static void main(String[] args) {
ArrayList<Apple> apples = new ArrayList<>();
apples.add(new GrannySmith());
apples.add(new Gala());
apples.add(new Fuji());
apples.add(new Braeburn());
for (Apple apple : apples)
System.out.println(apple);
}
}
程序执行的结果是:
因为向上转型,所有可以向持有Apple对象的集合中,放入Apple的子类型。
上述输出结果中,子对象名字后面的是由无符号十六进制表示的哈希码。
新特性:类型推断和泛型
“局部变量类型推断”也可用于简化涉及泛型的定义:
import java.util.ArrayList;
public class GenericTypeInference {
void old() {
ArrayList<Apple> apples = new ArrayList<>();
}
void modern() {
var apples = new ArrayList<Apple>();
}
void pitFall() {
var apples = new ArrayList<>();
apples.add(new Apple());
apples.get(0); // 会作为普通的Object类型返回
}
}
在modern()方法中,定义右侧使用的是<Apple>,通过这种方式提供信息,使编译器知道如何进行类型推断。
但替代现有的语法时也可能会产生问题,在pitFall()方法中存在着语句
var apples = new ArrayList<>();
尽管这样也可以正常编译,但<>实际上会变为<Object>,这不是我们需要的。当我们想要从apples中提取元素时,它们会作为普通的Object类型返回,而不是具体的Apple类型。
基本概念
Java的集合类库是用来“持有对象”的。从设计上,可以将这个库分为两种概念,这两种概念分别表示库的两个基本接口:
- Collection:一个由单独元素组成的序列,这些元素要符合一条或多条规则。
- Map:一组键值-对象对,使用键来查找值。Map也被称为关联数组(或是字典),它关联了对象与其他对象。
在理想情况下,我们编写的大部分代码都是在和这些接口打交道,只有在创建的时候才需要指名所使用的确切类型。例如,按照这种思想创建一个List:
List<Apple> apples = new ArrayList<>();
和之前不同,ArrayList在这里被向上转型为了List。这样,在我们决定修改实现时,只需要修改创建的地方即可:
List<Apple> apples = new LinkedList<>(); // 将实现修改为LinkedList
因此,通常会创建一个具体类的对象,并将其向上转型为相应的接口,在其余的代码中使用该接口。
但这种方法不会总是行得通,因为有些类会有额外的功能。当我们需要使用到这些额外的功能时,就无法将对象向上转型为更通用的接口了。
---
序列是一种持有一组对象的方法,而Collection就是序列这一概念的一般化。例如:
import java.util.ArrayList;
import java.util.Collection;
public class SimpleCollection {
public static void main(String[] args) {
Collection<Integer> c = new ArrayList<>();
for (int i = 0; i < 10; i++)
c.add(i); // 发生了自动装箱,向c中添加一个元素
for (Integer i : c)
System.out.print(i + " ");
System.out.println();
}
}
程序执行的结果如下:
这个示例只使用了Collection中定义的方法,所以继承自该接口的类的任何对象都可以正常工作。不过,ArrayList是最基本的序列类型。
添加一组元素
java.util中的Arrays和Collection类都包含了一些工具方法,用于向一个Collection中添加一组元素:
- Arrays.asList():可以接受一个数组,或一个使用逗号分隔的元素列表,将其转换为一个List对象。
- Collection.addAll():接受一个Collection对象、一个数组或一个用都逗号分隔的列表,将其中的所有元素都添加到这个Collection中。
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class AddingGroups {
public static void main(String[] args) {
Collection<Integer> collection = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
System.out.println("line 10: " + collection);
Integer[] moreInts = { 6, 7, 8, 9, 10 };
collection.addAll(Arrays.asList(moreInts)); // 传统的addAll()方法
System.out.println("line 14: " + collection);
// 下方的这种方式运行速度更快,但无法通过这种方式构建Collection。
Collections.addAll(collection, 11, 12, 13, 14, 15);
System.out.println("line 18: " + collection);
Collections.addAll(collection, moreInts);
System.out.println("line 20: " + collection);
// 生成一个底层是数组的列表:
List<Integer> list = Arrays.asList(16, 17, 18, 19, 20);
System.out.println("line 24: " + list);
list.set(1, 99); // 可以进行元素的修改:将下标为1的元素值改为99
// list.add(21); //运行时会发生报错,因为底层数组是不能调整大小的
System.out.println("line 27: " + list);
}
}
运行结果如下(line后跟的是行号):
Collection.addAll()的运行速度会快很多。因此,可以先构建一个没有元素的Collection,之后调用Collection.addAll()进行元素输入。
collection.addAll(Arrays.asList(moreInts)); 表示该方法只能接受另一个Collection对象作为参数,使用限制较大。相比之下,Arrays.asList()或Collection.addAll()方法就更加好用了。
可以直接将Arrays.asList()的输出作为一个List进行使用,但这种方式进行的实现,其底层是数组,大小无法改变。
也可以向Collection中添加子类型:
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
class Snow {}
class Powder extends Snow {}
class Light extends Powder {}
class Heavy extends Powder {}
class Crusty extends Snow {}
class Slush extends Snow {}
public class AsListInferecnce {
public static void main(String[] args) {
List<Snow> snow1 = Arrays.asList(new Crusty(), new Slush(), new Powder());
// snow1.add(new Heavy()); // 发生异常
List<Snow> snow2 = Arrays.asList(new Light(), new Heavy());
// snow2.add(new Slush()); // 发生异常
List<Snow> snow3 = new ArrayList<>();
Collections.addAll(snow3, new Light(), new Heavy(), new Powder()); // 有一个可变参数列表
snow3.add(new Crusty()); // 可以正常操作
// 使用显式类型参数说明作为提示
List<Snow> snow4 = Arrays.<Snow>asList(new Light(), new Heavy(), new Slush());
// snow4.add(new Powder()); // 发生异常
}
}
打印集合
数组的打印往往需要借助于Arrays.toString()等方法,但就像之前的例子演示过的那样,集合类不需要这些:
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
public class PrintingCollections {
static Collection fill(Collection<String> collection) {
collection.add("鼠");
collection.add("猫");
collection.add("狗");
collection.add("狗");
return collection;
}
static Map fill(Map<String, String> map) {
map.put("鼠", "杰瑞");
map.put("猫", "汤姆");
map.put("狗", "斯派克");
map.put("狗", "斯派克");
return map;
}
public static void main(String[] args) {
System.out.println(fill(new ArrayList<>()));
System.out.println(fill(new LinkedList<>()));
System.out.println();
System.out.println(fill(new HashSet<>()));
System.out.println(fill(new TreeSet<>())); // 按键的升序保存对象
System.out.println(fill(new LinkedHashSet<>())); // 按添加顺序保存对象
System.out.println();
System.out.println(fill(new HashMap<>()));
System.out.println(fill(new TreeMap<>())); // 按键的升序进行排序
System.out.println(fill(new LinkedHashMap<>())); // 按插入顺序保存键,同时具有HashMap的查找速度
}
}
程序执行的结果是:
上述例子演示了Java集合类库的两种主要类型。区别在于集合中的每个“槽”(slot)内持有的条目数:
- Collection:每个槽这只保存一个条目。其中,List以指定顺序保存一组条目;Set中,同样的条目只能加入一个;Queue只能一端进,另一端出。
- Map:每个槽内持有两个对象,即键和与之关联的值。
除此之外,上述例子中ArrayList和LinkedList的区别不仅在于进行一些操作时性能上的差异,而且LinkedList包含的操作比ArrayList多。另一个值得注意的是与Hash相关的类,这种类使用的保存方式会不同于同属的其他类。
List
List接口继承了Collection接口,并在此基础之上添加了一些方法,这些方法支持在List的中间进行元素的插入和移除。
List会以特定的顺序维护元素,由其衍生出了许多有用的类:
接下来会介绍其中两种较为常用的List:
- ArrayList:一种基本的List,擅长随机访问元素,但在List中间进行插入和删除速度较慢。
- LinkedList:具有理想的顺序访问性能,插入和删除的成本较低。除此之外,随机访问性能也更差,但与ArrayList相比有更多的功能。
下述程序将会用到的Pet类笔者只进行了简单实现,大致如下:
因此代码未写入本笔记。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
public class ListFeatures {
public static void main(String[] args) {
List<Pet> pets = new ArrayList<>();
Collections.addAll(pets, new Rat(), new Manx(),
new Cymric(), new Pug(), new Cymric(), new Pug()); // 插入一些数据
System.out.println("1. 创建List:" + pets);
System.out.println();
Hamster h = new Hamster();
pets.add(h);
System.out.println("2. 删除元素:" + pets);
System.out.println("3. 确定对象是否存在:" + pets.contains(h)); // 按对象搜索
pets.remove(h); // 按对象删除
System.out.println();
Pet tmpp = pets.get(2); // 按下标获取
System.out.println("4. 获取元素下标:" + pets.indexOf(tmpp));
System.out.println();
Pet cymric = new Cymric();
System.out.println("5. 试图通过同类对象进行索引:" + pets.indexOf(cymric));
System.out.println("6. 试图通过同类对象进行删除:" + pets.remove(cymric));
System.out.println("7. 对象能够精确匹配,正常删除:" + pets.remove(tmpp));
System.out.println();
pets.add(3, new Mouse());
System.out.println("8. 借助索引进行插入:" + pets);
List<Pet> sub = pets.subList(1, 4);
System.out.println("9. 使用区间提取List:" + sub);
System.out.println("10. 判断sub是否存在于pets中:" + pets.containsAll(sub));
System.out.println();
// List<Pet>没有实现Comparator接口。为方便请见,这里进行了现场实现
Collections.sort(sub, new Comparator<Pet>() {
@Override
public int compare(Pet p1, Pet p2) {
return p1.toString().compareTo(p2.toString());
}
});
System.out.println("11. 对sub进行原地排序:" + sub);
System.out.println("12. containsAll()不关心顺序:" + pets.containsAll(sub));
System.out.println();
Random random = new Random(12);
Collections.shuffle(sub, random);
System.out.println("13. 通过随机数使sub进行随机排序:" + sub);
System.out.println();
List<Pet> copy = new ArrayList<>(pets);
sub = Arrays.asList(pets.get(1), pets.get(4));
System.out.println("14. 通过asList获取sub:" + sub);
copy.retainAll(sub);
System.out.println("15. 求交集,保留在copy和sub中都存在的元素:" + copy);
System.out.println();
copy = new ArrayList<>(pets);
System.out.println("16. copy获得了一个pets的副本:" + copy);
System.out.println();
System.out.println("17. 检测pets是否为空:" + pets.isEmpty());
pets.clear();
System.out.println("18. pets被clear()清空:" + pets.isEmpty());
System.out.println();
Object[] o = copy.toArray();
System.out.println("19. toArray()可以将任意Collection转换为一个数组:" + o[3]);
}
}
程序执行的结果是:
如之前所说的,List可以在创建后添加或移除元素,而且可以自己调整大小。
当需要查找某个元素的索引编号,或按照引用从List中删除某个元素时,都会使用到equal()方法(这个方法是Object的组成部分)。另外,每一个Pet都是一个独立的对象,这就是为什么输出5和输出6无法进行匹配的原因。
应该意识到,List的因为会随着equal()行为的改变而改变。
尽管插入和删除的操作需要较高的成本,但这并不意味着我们要在插入或删除时将一个ArrayList转变为LinkedList。这只意味着我们需要注意这个问题,若ArrayList执行多次插入后程序变慢,我们应该看看List的实现(可以使用分析器)。这种优化较为棘手,若不必担心可以暂时放到一边。
Iterator(迭代器)
不管是哪种集合,都会需要通过某种方式完成元素的存取。比如List中的add()和get()。
但当我们开始思考更加复杂的代码时,会发现这样一个缺点:要使用集合,编写程序时就必须考虑集合的确切类型。如果我一开始针对List编写代码,进行优化时却希望将集合修改为Set,这时就麻烦了。
为此,就出现了迭代器(它同时也是一种设计模式)的概念。迭代器是一个对象,可以在序列中移动,并用来选择序列中的每个对象。并且使用它的程序员不需要知道序列的底层结构。
另外,迭代器是一个轻量级对象,创建成本很低。也因此,迭代器往往会具有一些使用限制。
Java中的Iterator只能向一个方向移动,并且只能实现几个功能:
- iterator():让Collection返回一个Iterator。此时,这个迭代器会准备好返回序列中的第一个元素。
- next():获得序列中的下一个对象。
- hasNext():检测序列是否存在更多对象。
- remove():删除迭代器最近返回的元素。
下列例子中的PetCreator().list()能够返回一个装载了随机Pet对象的ArrayList(笔者是使用了switch语句对这个方法进行了实现)。
import java.util.Iterator;
import java.util.List;
public class SimpleIteration {
public static void main(String[] args) {
// PetCreator().list()会返回一个ArrayList,其中包含随机填充的Pet对象
List<Pet> pets = new PetCreator().list(12);
Iterator<Pet> it = pets.iterator();
while (it.hasNext()) {
Pet p = it.next();
System.out.print(p.id() + ":" + p + " ");
}
System.out.println();
// 可以使用一种更简单的方式
for (Pet p : pets)
System.out.print(p.id() + ":" + p + " ");
System.out.println();
// 也可以使用迭代器来删除元素
it = pets.iterator();
for (int i = 0; i < 6; i++) {
it.next();
it.remove();
}
System.out.println(pets);
}
}
程序执行的结果如下:
Iterator的存在使得程序员不再需要操心集合中的元素数量,因为使用hasNext()和next()就能做到不错的处理。
若只需要对List进行遍历,for-in语句显得更加简洁。
这种对集合中的每个对象执行一个操作的想法十分有用,可以在许多地方发现。
上述的例子还无法完全展示Iterator的作用,下面的例子将会创建一个与具体的集合类型无关的display()方法:
注:为了实现这一例子,自制的Pet类需要继承并实现Comparable接口。其中需要被实现的compareTo()方法就是为了进行对象排序而定义的。
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeSet;
public class CrossCollectionIteration {
public static void display(Iterator<Pet> it) {
while (it.hasNext()) {
Pet p = it.next();
System.out.print(p.id() + ":" + p + " ");
}
System.out.println();
}
public static void main(String[] args) {
List<Pet> pets = new PetCreator().list(8);
LinkedList<Pet> petsLL = new LinkedList<>(pets);
HashSet<Pet> petsHS = new HashSet<>(pets);
TreeSet<Pet> petsTS = new TreeSet<>(pets);
display(pets.iterator());
display(petsLL.iterator());
display(petsHS.iterator());
display(petsTS.iterator());
}
}
程序执行的结果如下:
这个例子体现了Iterator真正的作用:能够将序列的遍历操作和序列的底层结构分离。也就是说,迭代器统一了对集合的访问。
Iterable接口描述了“任何可以产生一个迭代器的事物”。
使用这一接口可以将上述例子修改成更加简洁的版本:
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeSet;
public class CrossCollectionIteration2 {
public static void display(Iterable<Pet> ip) {
Iterator<Pet> it = ip.iterator();
while (it.hasNext()) {
Pet p = it.next();
System.out.print(p.id() + ":" + p + " ");
}
System.out.println();
}
public static void main(String[] args) {
List<Pet> pets = new PetCreator().list(8);
LinkedList<Pet> petsLL = new LinkedList<>(pets);
HashSet<Pet> petsHS = new HashSet<>(pets);
TreeSet<Pet> petsTS = new TreeSet<>(pets);
// 可以直接传入数组
display(pets);
display(petsLL);
display(petsHS);
display(petsTS);
}
}
程序执行的结果与上面的例子无异,不再重复展示。在这里,因为ArrayList已经实现了Iterable接口,所以display()方法在调用时更加方便了。
ListIterator是Iterator的一个更强大的子类型,只有List类才会生成。与Iterator不同,ListIterator可以双向移动。除此之外,它还可以生成相对于迭代器在列表中指向的当前位置的下一个和上一个元素的索引。并且可以使用set()方法替换它所访问过的最后一个元素。
import java.util.List;
import java.util.ListIterator;
public class ListIteration {
public static void main(String[] args) {
List<Pet> pets = new PetCreator().list(8);
ListIterator<Pet> it = pets.listIterator();
while (it.hasNext())
System.out.print(it.next() + ", " + it.nextIndex() + ", " + it.previousIndex() + "; ");
System.out.println();
// 也可以进行反向的遍历
while (it.hasPrevious())
System.out.print(it.previous().id() + " ");
System.out.println();
System.out.println(pets);
it = pets.listIterator(3);
while (it.hasNext()) { // 从下标为3的位置开始替换对象
it.next();
it.set(new PetCreator().get());
}
System.out.println(pets);
}
}
程序执行的结果是: