【读书笔记】【Java基础】
Java核心技术卷一
- 第10,11,12,13章所讲Java图形界面相关,当今这方面几乎很少使用,选择忽略。
https://www.zhihu.com/question/269505829/answer/933582331
2022/10/21
第九章 集合
数据结构会导致其实现风格以及性能,是需要大数据的搜索,有序队列的快速删除和插入,是否需要建立键值关联
9.1 Java集合框架
随着Java SE 1.2的问世,设计人员感到是推出一组功能完善的数据结构的时机了。希望让类库规模小且易于学习,而不希望像C++的“标准模版库”(即STL)那样复杂,但却又希望能够得到STL率先推出的“泛型算法”所具有的优点。
9.1.1 将集合的接口与实现分离
尾部添加元素,头部删除元素,并且可以查找队列中元素的个数。
“先进先出”是采用的数据结构。
实现
- 循环数组
- 链表
循环数组队列,就可以使用ArrayDeque类。如果需要一个链表队列,就直接使用LinkedList类
一旦构建了集合就不需要知道究竟使用了哪种实现。因此,只有在构建集合对象时,使用具体的类才有意义。可以使用接口类型存放集合的引用。
一旦改变了想法,可以轻松地使用另外一种不同的实现。只需要对程序的一个地方做出修改,即调用构造器的地方。如果觉得LinkedListQueue是个更好的选择,就将代码修改为:
循环数组还是链表?
循环数组要比链表更高效,因此多数人优先选择循环数组。然而,通常这样做也需要付出一定的代价。循环数组是一个有界集合,即容量有限。如果程序中要收集的对象数量没有上限,就最好使用链表来实现。
会发现另外一组名字以Abstract开头的类,例如,AbstractQueue。这些类是为类库实现者而设计的。如果想要实现自己的队列类(也许不太可能),会发现扩展AbstractQueue类要比实现Queue接口中的所有方法轻松得多。
9.1.2 Collection接口
add方法:
用于向集合中添加元素。如果添加元素确实改变了集合就返回true,如果集合没有发生变化就返回false。例如,如果试图向集中添加一个对象,而这个对象在集(set)中已经存在,这个添加请求就没有实效,因为集中不允许有重复的对象。
iterator方法:
用于返回一个实现了Iterator接口的对象。可以使用这个迭代器对象依次访问集合中的元素。下一节讨论迭代器。
9.1.3 迭代器
通过反复调用next方法,可以逐个访问集合中的每个元素。但是,如果到达了集合的末尾,next方法将抛出一个NoSuchElementException。因此,需要在调用next之前调用hasNext方法。如果想要查看集合中的所有元素,就请求一个迭代器,并在hasNext返回true时反复地调用next方法。例如:
编译器简单地将“for each”循环翻译为带有迭代器的循环。
for each底层实现就是带有迭代器的循环
“for each”循环可以与任何实现了Iterable接口的对象一起工作,这个接口只包含一个抽象方法:
Collection接口扩展了Iterable接口。因此,对于标准类库中的任何集合都可以使用“for each”循环。
public interface Collection<E> extends Iterable<E>
类详解
元素被访问的顺序取决于集合类型。如果对ArrayList进行迭代,迭代器将从索引0开始,每迭代一次,索引值加1。然而,如果访问HashSet中的元素,每个元素将会按照某种随机的次序出现。
在传统的集合类库中,例如,C++的标准模版库,迭代器是根据数组索引建模的。如果给定这样一个迭代器,就可以查看指定位置上的元素,就像知道数组索引i就可以查看数组元素a[i]一样。查找一个元素的唯一方法是调用next,而在执行查找操作的同时,迭代器的位置随之向前移动。
应该将Java迭代器认为是位于两个元素之间。当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用(见下图)。
Iterator接口的remove方法将会删除上次调用next方法时返回的元素。在大多数情况下,在决定删除某个元素之前应该先看一下这个元素是很具有实际意义的。更重要的是,对next方法和remove方法的调用具有互相依赖性。如果调用remove之前没有调用next将是不合法的。如果这样做,将会抛出一个IllegalStateException异常。
必须先调用next越过将要删除的元素,才能将返回的元素删除。
可以认为next让迭代器去执行查找元素的地址操作,找到后迭代器持有一个元素,迭代器一次也只能持有一个,这时候持有的是有元素的时候才能调用remove去删除。
9.1.4 泛型实用方法
由于Collection与Iterator都是泛型接口,可以编写操作任何集合类型的实用方法。例如,下面是一个检测任意集合是否包含指定元素的泛型方法:
如果该集合包含指定的元素,则返回true。更正式地说,当且仅当这个集合包含至少一个元素e (o==null ?e = = null: o.equals (e))。
contains方法
参数:
在这个集合中被测试的元素
返回值: 如果此集合包含指定的元素,则为 true
抛出:
ClassCastException——如果指定元素的类型与此集合不兼容(可选)
NullPointerException——如果指定的元素是空的,并且这个集合不允许空元素(可选)
boolean contains(Object o);
Java类库的设计者认为:这些实用方法中的某些方法非常有用,应该将它们提供给用户使用。这样,类库的使用者就不必自己重新构建这些方法了。contains就是这样一个实用方法。
事实上,Collection接口声明了很多有用的方法,所有的实现类都必须提供这些方法。下面列举了其中的一部分:
为了能够让实现者更容易地实现这个接口,Java类库提供了一个类AbstractCollection,它将基础方法size和iterator抽象化了,但是在此提供了例行方法。例如:
具体的集合类可以扩展AbstractCollection类了。现在要由具体的集合类提供iterator方法,而contains方法已由AbstractCollection超类提供了。然而,如果子类有更加有效的方式实现contains方法,也可以由子类提供,就这点而言,没有什么限制。
对于Java SE 8,这种方法有些过时了。如果这些方法是Collection接口的默认方法会更好。但实际上并不是这样。不过,确实已经增加了很多默认方法。其中大部分方法都与流的处理有关(有关内容将在卷Ⅱ中讨论)。另外,还有一个很有用的方法:
删除此集合中满足给定谓词的所有元素。迭代期间或谓词抛出的错误或运行时异常被转发给调用者。
removeIf方法
参数:
Filter—对于要删除的元素返回true的谓词
返回值: 如果删除了任何元素,则为 true
抛出:
NullPointerException -如果指定的过滤器为空
UnsupportedOperationException——如果元素不能从这个集合中移除。如果不能删除匹配的元素,或者通常不支持删除,则实现可能抛出此异常。
实现要求:
默认实现使用迭代器遍历集合的所有元素。
使用Iterator.remove()删除每个匹配元素。
如果集合的迭代器不支持删除,则会在第一个匹配的元素上抛出UnsupportedOperationException异常。 自从: 1.8
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
java.util.Collection 1.2
Iterator<E> iterator()
返回一个用于访问集合中每个元素的迭代器。
int size()
返回当前存储在集合中的元素个数。
boolean isEmpty()
如果集合中没有元素,返回true。
boolean contains(Object obj)
如果集合中包含了一个与obj相等的对象,返回true。
boolean containsAll(Collection<? > other)
如果这个集合包含other集合中的所有元素,返回true。
boolean add(Object element)
将一个元素添加到集合中。如果由于这个调用改变了集合,返回true。
boolean addAll(Collection<? extends E> other)
将other集合中的所有元素添加到这个集合。如果由于这个调用改变了集合,返回true。
boolean remove(Object obj)
从这个集合中删除等于obj的对象。如果有匹配的对象被删除,返回true。
boolean removeAll(Collection<? > other)
从这个集合中删除所有与other集合中的元素不同的元素。如果由于这个调用改变了集合,返回true。
Object[] toArray()返回这个集合的对象数组。
<T> T[] toArray(T[] arrayToFill)返回这个集合的对象数组。如果arrayToFill足够大,就将集合中的元素填入这个数组中。剩余空间填补null;否则,分配一个新数组,其成员类型与arrayToFill的成员类型相同,其长度等于集合的大小,并填充集合元素。
java.util.Iterator 1.2
boolean hasNext()如果存在可访问的元素,返回true。
E next()返回将要访问的下一个对象。如果已经到达了集合的尾部,将抛出一个NoSuchElement Exception。
void remove()删除上次访问的对象。这个方法必须紧跟在访问一个元素之后执行。如果上次访问之后,集合已经发生了变化,这个方法将抛出一个IllegalStateException。
9.1.5 集合框架中的接口
集合有两个基本接口:Collection和Map。
map插入元素和获取不同于Collection
V put(K key, V value);
V get(Object key);
List是一个有序集合(ordered collection)。
元素会增加到容器中的特定位置。可以采用两种方式访问元素:使用迭代器访问,或者使用一个整数索引来访问。后一种方法称为随机访问(random access),因为这样可以按任意顺序访问元素。
与之不同,使用迭代器访问时,必须顺序地访问元素。List接口定义了多个用于随机访问的方法
- void add(int index, E element);
将指定的元素插入到列表中的指定位置(可选操作)。
将当前位于该位置的元素(如果有的话)和所有后续元素向右移动(向它们的索引添加1)。
参数: 索引-指定元素要插入的索引 要插入的元素
抛出:
UnsupportedOperationException -如果此列表不支持添加操作 如果指定元素的类阻止将其添加到此列表中
NullPointerException -如果指定的元素是空的,并且这个列表不允许空元素
IllegalArgumentException -如果指定元素的某些属性阻止它被添加到这个列表
IndexOutOfBoundsException -如果索引超出范围(index < 0 || index > size())
void add(int index, E element);
index 0-size()
demo1:如果插入的索引位置有元素
@Test
public void testListIndex(){
ArrayList<Integer> integers = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
System.out.println(integers);
integers.add(1,9);
System.out.println(integers);
}
demo2: 如果插入的索引位置没有元素即size()
@Test
public void testListIndex2(){
ArrayList<Integer> integers = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
System.out.println(integers);
integers.add(4,9);
System.out.println(integers);
}
ListIterator接口是Iterator的一个子接口。它定义了一个方法用于在迭代器位置前面增加一个元素:
集合框架的这个方面设计得很不好。实际中有两种有序集合,其性能开销有很大差异。由数组支持的有序集合可以快速地随机访问,因此适合使用List方法并提供一个整数索引来访问。与之不同,链表尽管也是有序的,但是随机访问很慢,所以最好使用迭代器来遍历。如果原先提供两个接口就会容易一些了
RandomAccess
List实现使用的标记接口,表示它们支持快速(通常是恒定时间)随机访问。该接口的主要目的是允许泛型算法改变其行为,以便在应用于随机或顺序访问列表时提供良好的性能。
操作随机访问列表(如ArrayList)的最佳算法在应用于顺序访问列表(如LinkedList)时可以产生二次行为。
操作随机访问列表可以用类似数组的思想,get(index)一步就能找到元素。但是对于顺序访问列表来说就是链表结构,访问索引为index的元素就要在这之前要访问索引为0-index-1一共index个元素。
我们鼓励泛型列表算法在应用算法之前检查给定的列表是否是该接口的一个实例,如果该算法应用于顺序访问列表将会提供较差的性能,并且在必要时改变它们的行为以保证可接受的性能。 人们认识到,随机访问和顺序访问之间的区别通常是模糊的。例如,如果某些List实现非常庞大,则提供渐近线性的访问时间,但实际上提供恒定的访问时间。这样的List实现通常应该实现这个接口。
根据经验,如果对于类的典型实例,下面的循环,List实现应该实现这个接口:
for (int i=0, n=list.size(); i < n; i++)
list.get(i);
比下面循环快:
for (Iterator i=list.iterator(); i.hasNext(); )
i.next();
自从: 1.4
public interface RandomAccess {
}
Set接口
等同于Collection接口,不过其方法的行为有更严谨的定义。要适当地定义集的equals方法:只要两个集包含同样的元素就认为是相等的,而不要求这些元素有同样的顺序。hashCode方法的定义要保证包含相同元素的两个集会得到相同的散列码。从概念上讲,并不是所有集合都是集。建立一个Set接口可以让程序员编写只接受集的方法。
SortedSet和SortedMap接口会提供用于排序的比较器对象,这两个接口定义了可以得到集合子集视图的方法。有关内容将在9.4节讨论。最后,Java SE 6引入了接口NavigableSet和NavigableMap,其中包含一些用于搜索和遍历有序集和映射的方法。(理想情况下,这些方法本应当直接包含在SortedSet和SortedMap接口中。)TreeSet和TreeMap类实现了这些接口。
9.2 具体的集合
为简单起见,省略了将在第14章中介绍的线程安全集合
9.2.1 链表
在本书中,有很多示例已经使用了数组以及动态的ArrayList类。然而,数组和数组列表都有一个重大的缺陷。这就是从数组的中间位置删除一个元素要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动(见图9-6)。在数组中间的位置上插入一个元素也是如此。
另外一个大家非常熟悉的数据结构——链表(linked list)解决了这个问题。尽管数组在连续的存储位置上存放对象引用,但链表却将每个对象存放在独立的结点中。每个结点还存放着序列中下一个结点的引用。在Java程序设计语言中,所有链表实际上都是双向链接的(doubly linked)——即每个结点还存放着指向前驱结点的引用(见图
从链表中间删除一个元素是一个很轻松的操作,即需要更新被删除元素附近的链接(见图9-8)。
链表与泛型集合之间有一个重要的区别。链表是一个有序集合(ordered collection),每个对象的位置十分重要。LinkedList.add方法将对象添加到链表的尾部。但是,常常需要将元素添加到链表的中间。由于迭代器是描述集合中位置的,所以这种依赖于位置的add方法将由迭代器负责。只有对自然有序的集合使用迭代器添加元素才有实际意义。例如,下一节将要讨论的集(set)类型,其中的元素完全无序。因此,在Iterator接口中就没有add方法。相反地,集合类库提供了子接口ListIterator,其中包含add方法:
ListIterator类
- 博客
add方法
将指定的元素插入到列表中(可选操作)。
该元素插入到next将返回的元素之前(如果有的话),插入到previous将返回的元素之后(如果有的话)。
(如果列表中不包含任何元素,则新元素将成为列表中的唯一元素。)
新元素插入到隐式游标之前:后续调用next将不受影响,后续调用previous将返回新元素。(该调用将调用nextIndex或previousIndex返回的值增加1。
) 参数: E -要插入的元素 抛出: 如果这个列表迭代器不支持add方法 如果指定元素的类阻止将其添加到此列表中 如果这个元素的某些方面阻止它被添加到这个列表中
void add(E e);
Demo如下:
Test
public void testListterator(){
LinkedList<Integer> integers = new LinkedList<>(Arrays.asList(1, 2, 3, 4));
ListIterator<Integer> listIterator = integers.listIterator();
//在元素开头位置提添加
System.out.println("next的后续返回元素的索引"+listIterator.nextIndex());
listIterator.add(5);
//此时元素的索引位置为
System.out.println("next的后续返回元素的索引"+listIterator.nextIndex());
System.out.println(listIterator.nextIndex());
listIterator.next();
System.out.println("next的后续返回元素的索引"+listIterator.nextIndex());
//在索引为2的元素前面添加一个
listIterator.add(5);
while (listIterator.hasNext()){
listIterator.next();
}
//此处迭代器在在最后一个元素后,所以表示在最后一个元素后添加一个
listIterator.add(5);
ListIterator<Integer> integerListIterator = integers.listIterator();
while (integerListIterator.hasNext()){
System.out.println("元素索引"+integerListIterator.nextIndex()+"元素值"+integerListIterator.next());
}
nextIndex方法
返回将由next的后续调用返回的元素的索引。(如果列表迭代器位于列表的末尾,则返回列表大小。) 返回值: 后续调用next将返回的元素的索引,如果列表迭代器位于列表的末尾,则返回列表大小
int nextIndex();