持有对象(1):泛型和类型安全的容器、基本概念、添加一组元素

    如果一个程序只包含固定数量的且其生命周期都是已知的对象,那么这是一个非常简单的程序。通常,程序总是根据运行时才知道某些条件去创建新对象。在此之前,不会知道所需对象的数量,甚至不知道确切的类型,为解决这个普遍的编程问题,需要在任意时刻和任意位置创建任意数量的对象。所以,就不能依靠创建命名的引用来持有每一个对象:

MyType aReference;

    因为你不知道实际上会需要多少这样的引用。

    大多数语言都提供某种方法来解决这个基本问题。java有多种方式保存对象(应该说是对象的引用)。例如前面曾经学习过的数组,它是编译器支持的类型。数组是保存一组对象的最有效的方式,如果你想保存一组基本类型数据,也推荐使用这种方式。但是数据具有固定的尺寸,而在更一般的情况下,你在写程序时并不知道将需要多少个对象,或者是否需要更复杂的方式来存储对象,因此数组尺寸固定这一限制显得过于受限了。

    java实用类库还提供了一套相当完整的容器类来解决这个问题,其中基本的类型是List、Set、Queue和Map。这些对象类型也称为集合类,但由于java的类库中使用了Collection这个名字来指代该类库的一个特殊子集,所以我使用了范围更广的术语“容器”称呼它们。容器提供了完善的方法来保存对象,你可以使用这些工具来解决数量惊人的问题。

    容器还有其他一些特性。例如,Set对于每个值都只保存一个对象,Map是允许你将某些对象与其他一些对象关联起来的关联数组,java容器类都可以自动地调整自己的尺寸。因此,与数组不同,在编程时,你可以将任意数量的对象放置到容器中,并且不需要担心容器应该设置为多大。

    即使在java中没有直接的关键字支持,容器类仍旧是可以显著增强你的编程能力的基本工具。接下来,你将了解有关java容器类库的基本知识,以及对典型用法的重点介绍。

一、泛型和类型安全的容器

    使用java SE5之前的容器的一个主要问题就是编译器允许你向容器中插入不正确的类型。例如,考虑一个Apple对象的容器,我们使用最基本最可靠的容器ArrayList。现在,你可以把ArrayList当作“可以自动扩充自身尺寸的数组”来看待。使用ArrayList相当简单:创建一个实例,用add()插入对象;然后使用get()访问这些对象,此时需要使用索引,就像数组一样,但是不需要方括号。ArrayList还有一个size()方法,使你可以知道已经有多少元素添加了进来,从而不会不小心因索引越界而引发错误(通过抛出运行时异常)。

    在本例中,Apple和Orange都放置在了容器中,然后将它们取出。正常情况下,java编译器会报出警告信息,因为这个示例没有使用泛型。在这里,我们使用java SE5所特有的注解来抑制了警告信息。注解以“@”符号开头,可以接受参数,这里的@SuppressWarnings注解及其参数表示“抑制没有进行类型检查操作的警告”和“使用generics时忽略没有指定相应的类型”:

import java.util.ArrayList;

class Apple {
	private static long counter;
	private final long id = counter++;

	public long id() {
		return id;
	}
}

class Orange {
}

public class ApplesAndOrangesWithoutGenerics {
	@SuppressWarnings({ "unchecked", "rawtypes" })
	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());
		for (int i = 0; i < apples.size(); i++)
			((Apple) apples.get(i)).id();
	}
}

    Apple和Orange类是有区别的,它们除了都是Object之外没有任何共性(记住,如果一个类没有显示地声明继承自哪个类,那么它自动地继承自Object)。因为ArrayList保存的是Object,因此你不仅可以通过ArrayList的add()方法将Apple对象放进这个容器,还可以添加Orange对象,而且无论在编译器还是运行时都不会有问题。当你在使用ArrayList的get()方法来取出你认为是Apple的对象时,你得到的只是Object引用,必须将其转型为Apple,因此,需要将整个表达式括起来,在调用Apple的id()方法之前,强制执行转型。否则,你就会得到语法错误。在运行时,当你试图将Orange对象转型为Apple时,你就会以前面提及的异常的形式得到一个错误。

     你将会了解到,使用java泛型来创建类会非常复杂。但是,应用预定义的泛型通常会很简单。例如,要想定义用来保存Apple对象的ArrayList,你可以声明ArrayList<Apple>,而不仅仅只是ArrayLIst,其中尖括号括起来的是类型参数(可以有多个),它指定了这个容器实例可以保存的类型。通过使用泛型,就可以在编译器防止将错误类型的对象放置到容器中。下面还是这个示例,但是使用了泛型:

public class ApplesAndOrangesWithoutGenerics {
	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 (int i = 0; i < apples.size(); i++)
			System.out.println(apples.get(i).id());
		for (Apple apple : apples) {
			System.out.println(apple.id());
		}
	}
}

    现在,编译器可以阻止你将Orange放置到apples中,因此它变成了一个编译期错误,而不再是运行时错误。

    你还应该注意到,在将元素从List中取出时,类型转换也不再是必需的了。因为List知道它保存的是什么类型,因此它会在调用get()时替你执行转型。这样,通过使用泛型,你不仅知道编译器将会检查你放置到容器中的对象类型,而且在使用容器中的对象时,可以使用更加清晰的语法。

    这个实例还表明,如果不需要使用每个元素的索引,你可以使用foreach语法来选择List中的每个元素。

    当你指定了某个类型作为泛型参数时,你并不仅限于只能将该确切类型的对象放置到容器中。向上转型也可以像作用于其他类型一样作用于泛型:

import java.util.ArrayList;

/**
 * 绿苹果
 */
class GrannySmith extends Apple {
}

class Gala extends Apple {
}

class Fuji extends Apple {
}

class Braeburn extends Apple {
}

public class GenericsAndUpcasting {
	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对象的容器中。

    程序的输出是从Object默认的方法toString()方法产生的,该方法将打印类名,后面跟随该对象的散列码的无符号十六进制表示(这个散列码是通过hashCode()方法产生的)。

二、基本概念

    java容器类类库的用途是“保存对象”,并将其划分为两个不同的概念:

  1. Collection。一个独立元素的序列,这些元素都服从一条或多条规则。List必须按照插入的顺序保存元素,而Set不能有重复元素。Queue按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。
  2. Map。一组成对的“键值对”对象,允许你使用键来查找值。ArrayList允许你使用数字来查找值,因此在某种意义上讲,它将数字与对象关联在了一起。映射表允许我们使用另一个对象来查找某个对象,它也被称为“关联数组”,因为它将某些对象与另外一些对象关联在了一起;或者被称为“字典”,因为你可以使用键对象来查找值对象,就像在字典中使用单词来定义一样。Map是强大的编程工具。

    尽管并非总是这样,但是在理想情况下,你编写的大部分代码都是在与这些接口打交道,并且你唯一需要指定所使用的精确类型的地方就是在创建的时候。因此,你可以像下面这样创建一个List:

List<Apple> apples = new ArrayList<>();

    注意,ArrayList已经被向上转型为List,这与前一个示例中的处理方式正好相反。使用接口的目的在于如果你决定去修改你的实现,你所需的只是在创建出修改它,就像下面这样:

List<Apple> apples = new LinkedList<>();

    因此,你应该创建一个具体类的对象,将其转型为对应的接口,然后在其余的代码中都使用这个接口。

    这种方式并非总能奏效,因为某些类具有额外的功能,例如,LinkedList具有在List接口中未包含的额外方法,而TreeMap也具有在Map接口中未包含的方法。如果你需要使用这些方法,就不能将它们向上转型为更通用的接口。

    Collection接口概括了序列的概念--一种存放一组对象的方式。下面这个简单的示例用Integer对象填充了一个Collection(这里用ArrayList表示),然后打印所产生的容器中的所有元素:

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);
		for (Integer integer : c) {
			System.out.print(integer + ", ");
		}
	}
}

    因为这个示例只是使用了Collection方法,因此任何继承自Collection的类的对象都可以正常工作,但是ArrayList是最基本的序列类型。

    add()方法的名称就表明它是要将一个新元素放置到Collection中。但是,文档中非常仔细地叙述到:“要确保这个Collection包含指定的元素。”这是因为考虑到了Set含义,因为在Set中只有元素不存在的情况下才会添加。在使用ArrayList,或者任何种类的List时,add()总是表示“把它放进去”,因为List不关心是否存在重复。

    所有的Collection都可以用foreach语法遍历,就像这里所展示的。

三、添加一组元素

    在java.util包中的Arrays和Collections类中都有很多实用方法,可以在一个Collection中添加一组元素。Arrays.asList()方法接受一个数组或是一个用逗号分隔的元素列表(使用可变参数),并将其转换为一个List对象。Collections.addAll()方法接受一个Collection对象,以及一个数组或是一个用逗号分割的列表,将元素添加到Collection中。下面的示例展示了这两个方法,以及更加传统addAll()方法,所有Collection类型都包含该方法:

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));
		Integer[] moreInts = { 6, 7, 8, 9, 10 };
		collection.addAll(Arrays.asList(moreInts));
		Collections.addAll(collection, 11, 12, 13, 14, 15);
		Collections.addAll(collection, moreInts);
		List<Integer> list = Arrays.asList(16, 17, 18, 19, 20);
		list.set(1, 99);
		// list.add(21);
	}
}

    Collection的构造器可以接受另一个Collection,用它将自身初始化,因此你可以使用Arrays.aslist()来为这个构造器产生输入。但是,Collection.addAll()方法运行起来要快得多,而且构建一个不包含元素的Collection,然后调用Collections.addAll()这种方式很方便,因此它是首选方式。

    Collection.addAll()成员方法只能接受另一个Collection对象作为参数,因此它不如Arrays.asList()或Collections.addAll()灵活,这两个方法使用的都是可变参数列表。

    你也可以直接使用Arrays.asList()的输出,将其当作list,但是在这种情况下,其底层表示的是数组,因此不能调整尺寸。如果你试图用add()或delete()方法在这种列表中添加或删除元素,就有可能会引发去改变数组尺寸的尝试,因此你将在运行时获得“UnsupportedOperationException(不支持的操作)”错误。

   Map更加复杂,并且除了用另一个Map之外,java标准类库没有提供其他任何自动初始化它们的方式。

如果本文对您有很大的帮助,还请点赞关注一下。

发布了100 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_40298351/article/details/104338907