JAVA chatper 9 collections

9.1 JAVA collections框架
循环队列 ArrayDeque类
链表队列 LinkedList类

Collections接口

public interface Collection<E>{
  boolean add(E element);
  Iteratora<E> iterator();
}

9.1.3 Iterators
Iterator接口

public interface Iterator<E>{
  E next();
  boolean hasNext();
  void remove();
  default void forEachRemaining(Consumer<? super E> action);
}

使用方法

Collection<String> c=...;
Iterator<String> iter=c.iterator();
while(iter.hasNext()){
  String element=iter.next();
do something...
}

for each循环

for(String element:c){
  do something..
}
public interface Iterable<E>{
  Iterator<E> iterator();
...
}

Collection接口继承于Iterable接口,所以可以用迭代器。

移除第一个元素

Iterator<String> it=c.iterator();
it.next();
it.remove();

删除两个连续1元素,下例是错误的

it.remove();
it.remove();//ERROR

而是

it.remove();
it.next();
it.remove();

库支持AbstractCollection类,其中的size iterator是抽象的,其他常规方法都实现了。
一个具体的collection类现在可以继承AbstractionCollection类。你要实现iterator,但contains已经实现了。当然你可以覆盖该方法。

9.2Colleciton框架的接口

collection的两个基本接口,Collection Map。
插入元素,Collection
boolean add(E element)
插入元素Map
v put(K key,V value)
从Collection中读元素,使用Iterator
Map 使用
V get(K key);
List是有序的Collection,元素可以用iterator,或者下标获得。这叫做随机存取。
List接口定义的方法:
void add(int index,E element)
void remove(int index)
E get(int index)
E set(int index,E element)
ListIterator接口是Iterator的子接口。定义了一个方法添加元素在iterator位置之前。
void add(E element)

RandomAccess接口,用于判断collection是否支持随机存取

if(c instanceof RandomAccess){

}else{

}

Set接口和Collection接口是完全相同的。但是它的方法的行为更加严格。Set中的add不允许重复元素。equals方法两个集合必须包含相同的元素。
但顺序可以不同。hashCode方法则要求相同的两个集合有相同的哈希值。

SortedSet和SortedMap接口用于可比较对象排序。

NavigableSet NavigableMap包含附加的方法用于搜索和遍历有序集合和Map。
TreeSet TreeMap实现了这些接口。

9.3 明确的Collection

9.3.1链表
链表是有序的collection。你要使用链表的add方法,它会添加到链表尾。使用子接口ListIterator,的add方法添加元素。
ListIterator接口还有两个方法倒序遍历。
E previous()
boolean hasPrevious()
LinkedList 有listIterator方法返回一个实现了ListIterator的迭代器
ListIterator iter=staff.listIterator();
add方法会添加新元素在迭代器之前。如果你多次调用add方法,插入的元素按你添加的顺序插入在迭代器当前位置前。
刚获得的ListIterator,插入的话始终插入在表头位置。如果迭代器在表尾位置。即hasNext()为假时,插入的元素会变为表尾。
如果一个链表长为3,它有4个插入位置。用|来示意
|ABC A|BC AB|C ABC|
注意remove操作和退格键并不完全一样,如果你调用了next() remove会删除左侧元素。你调用了previous(),remove则会删除右侧元素。
你在一行不能连续调用remove两次。

set方法替换最后一个元素。下例替换链表第一个元素

ListIterator<String> iter=list.listIterator();
String oldValue =iter.next();
iter.set(newValue);

如果一个迭代器指向的元素被另一个迭代器修改或者删除,迭代器会发现这种情况扔出ConcurrentModificationException。

List<String> list=...;
ListIterator<String> iter1=list.listIterator();
ListIterator<String> iter2=list.listIterator();
iter1.next();
iter1.remove();
iter2.next();//throws ConcurrentModificationException

为了避免这种异常,你可以使用多个迭代器,但他们都是只读的。或者使用一个迭代器,可读可写。

list iterator接口有一个方法告诉你当前位置的索引,nextIndex()返回当前位置调用next()方法后的索引位置。previousIndex()返回当前位置调用previous()的索引位置。这些方法是高效的。因为迭代器会保存当前位置的计数。如果你有个整数索引n,list.listIterator(n)返回迭代器指向n之前的元素。

元素较少时可以使用ArrayList,链表则使用在你需要插入删除中间元素的时候。

建议不要使用随机访问索引的方式使用链表。

9.3.2 Array Lists
Vector是线程同步的。ArrayList则不是。ArrayList效率较高。
9.3.3哈希Sets
如果你不在意元素的排列顺序,想高速获取元素的值,使用哈希集。
自己定义的类中的hashCode方法需要兼容equals方法。a.equals(b),则a,b拥有相同的哈希值。
标准库中的桶数是2的幂。默认为16.(2的4次方)
如果hash表太满,需要重新哈希。重新哈希时,一个含有更多桶的哈希表会被创建。所有元素会被重新插入新的表。原来的哈希表会被弃用。装填因子决定了什么时候哈希表会被重新哈希。如果是0.75,则哈希表元素超过75%时就会重新哈希,桶数量会乘以2。大部分情况,0.75是合理的。

哈希表用来实现很多重要数据结构,比如说Set。Set是不重复元素形成的集合。Set的add方法只会添加集合没有的元素。Java集合中,HashSet类实现了基于哈希表的集合。添加元素使用add方法,使用contains()方法确定是否需要找寻元素。它只会查找一个桶,并不会遍历整个集合。

hash set的Iterator会依次访问所有的桶。由于哈希散列了元素,所以访问是类随机的。当你不在意元素顺序的时候使用哈希表。

9.3.4 Tree Sets
树集是有序的集合。你插入元素到集合中无序,但遍历时则有序。例:

var sorter=new TreeSet<String>();
sorter.add("Bob");
sorter.add("Amy");
sorter.add("Carl");
for(String s: sorter)System.out.println(s);

树集是基于红黑树实现的。
树集添加元素需要对数次比较,比ArrayList LinkList快,慢于哈希表。
使用树集,元素必须实现Comparable接口。或提供Comarator构造树集时。

9.3.5 队列和双端队列queue deque
9.3.6优先级队列
优先级队列是由堆实现的,调用remove方法会获得最小元素。
9.4Maps
使用键值对获得元素
var id="987-98-9996";
Employee e=staff.get(id);//gets harry
有时候返回Null不方便,可以使用默认值

Map<String,Integer> scores=...;
int score=scores.getOrDefault(id,0);//get 0 if id is not present

使用lambda遍历
scores.forEach((k,v)->System.out.println("key="+k+",value="+v));

9.4.4 Weak Hash Maps
这一数据结构和垃圾收集器合作,移除键值对,这个键值对的键只有在哈希表中有引用。其他地方无引用指向它。
若使用Map,键值对会长期存在,你要自行处理没有引用的键值对。

9.4.5链接哈希集合和Maps
LinkedHashSet 和LinkedHashMap记忆了你插入元素的顺序。
Linked hash map可以可选的使用访问顺序,而入不是插入顺序。迭代map entries,你使用get put方法时,受影响的entry会从当前位置移除,放置在链表尾。
位置只影响链表,不影响哈希表的桶。访问顺序当你实现最近使用时很好用。

9.4.6 枚举sets和Maps
EnumSet非常有效的set实现枚举类型。它是通过Bit位实现的。
EnumSet没有公有构造函数,使用静态工厂方法构造集合

enum Weekday{MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY};
EnumSet<Weekday> always=EnumSet.allOf(Weekday.class);
EnumSet<Weekday> never=EnumSet.noneof(Weekday.class);
EnumSet<Weekday> workday=EnumSet.range(Weekday.MONDAY,Weekday.FRIDAY);
EnumSet<weekday> mwk=EnumSet.of(Weekday.MONDAY,Weekday.WEDNESDAY);

可以使用Set接口修改EnumSet
EnumMap 是键属于枚举类型的Map

9.5 Copies 和Views

使用views,你可以获得实现了Collection,Map接口的其他对象。你可以看到一个例子,这个例子是map类伴随一个keySet方法。第一眼看来,看起来像方法创建了一个新的集合,填充所有的键到map中,然后返回。实际并不如此,而是keySet方法,返回了一个实现了Set接口的类的对象。该对象操纵了原始的Map,这样的集合被称为view.

9.5.1 小Collections
Java9介绍了静态方法用给定的元素产生集合或表,或使用键值对产生Map。

List<String> names=List.of("Peter","Paul","Mary");
Set<Integer> numbers=Set.of(2,3,5);
Map<String,Integer> scores=Map.of("Peter",2,"Paul",3,"Mary",5);

这些元素,键,值不能为Null,Set和Map不允许重复元素

numbers=Set.of(13,null);//Error null element
scores=Map.of("Peter",4,"Peter",2);//Error duplicate key

注意:Set 和Map 的迭代顺序是不确定的。

List和Set接口拥有11个of方法,从0到10个参数。一个of方法拥有可变参数。这是为了效率才这样做。

对于Map接口,提供可变参数版本是不可能的,因为参数类型会根据Key value类型而修改。有一个静态方法ofEntries接收一个任意数到Map.Entry<K,V>对象。你可以创建它使用静态entry方法。举例:
Map<String,Integer> scores=ofEntries(entry("Peter",2),entry("Paul",3),entry("Mary",5));

of 和ofEntries方法产生类的对象,他们拥有每个元素的实例,或者是数组实现的

这些collection对象是不可更改的,任何试图更改内容的行为会导致UnsupportedOperationException

如果你想要一个可变的collection,你要传递不可更改的collection到构造函数。

var names=new ArrayList<>(List.of("Peter","Paul","Mary"));
这个方法会调用Collections.nCopies(n,anObject),返回一个不可变的对象,对象实现了List接口,给我们的错觉是拥有n个元素,每个元素是一个anObject
。举例:下面创建了一个List拥有100个字符串,都被设为"DEFAULT":
List settings=Collection.nCopies(100,"DEFAULT");
它只会占用很少的内存,对象只被保存了一次。

9.5.2 不可更改的Copies和Views
要创建不可修改的collections的拷贝,使用Collection类型的copyOf方法。

ArrayList<String> names=...;
Set<String> nameSet=Set.copyOf(names);//The names as an unmodifiable set
List<String> nameList=List.copyOf(names);//The names as an unmodifiable list

每个copyOf方法制作一个collection的拷贝,如果原collection是可修改的,拷贝不回受影响。

如果原collection就是不可更改的而且类型正确。copyOf简单的返回它。

Set<String> names=Set.of("Peter","Paul","Mary");
Set<String> nameSet=Set.copyOf(names);//No need to make a copy

Collections类拥有方法创建不可更改的views。这些views会进行运行时检查,如果试图修改不可改变的collection,会抛出异常。

你可获得不可更改的views通过以下8个方法
Collection.unmodifiableCollection
Collection.unmodifiableList
Collection.unmodifiableSet
Collection.unmodifiableSortedSet
Collection.unmodifiableNavigableSet
Collection.unmodifiableMap
Collection.unmodifiableSortedMap
Collection.unmodifiabbleNavigableMap
每个方法被定义为工作在一个接口。如Collections.unmodifiableList工作在ArrayList,LinkedList或是其他实现了list的接口

比如你想要你代码的一部分查看,但不修改collection的内容,你可以用:

var staff=new LinkedList<String();
...
lookAt(Collections.unmodifiableList(staff));

Collections.unmodifiableList的方法返回一个实现了List接口的对象。它的访问方法从staff中取值,当然,lookAt方法可以调用任何实现了List接口的方法,不仅仅是取得器。但任何可变方法已经被重定义抛出UnsupportedOerationException异常。

不可改变的View不会让collection自身不可变,你可以仍然通过collection的原引用来修改collection,你也可以调用会改变collection的方法。

views包装的接口并不是实际上的collection对象,所以你只能使用定义在接口内的方法,比如LinkedList有addFirst,addLast。但它不是List接口的一部分,这些方法在不可更改的view中不能使用。

9.5.3 Subranges
你可以为一系列的collections构造subrange views。比如你想要从list staff中抽取元素10-19,使用subList方法可以获得view
List group2=staff.subList(10,20);
第一个索引包含在内,最后一个索引不包含在内。
你可以在subrange中进行操作,他们会自动反映在整个list中,比如,你想擦除整个subrange
group2.clear();
staff list中的元素会被清除,group2会变为空。

对于sorted sets 和maps,你使用有序的顺序,而不是元素的位置,对于subranges SortedSet接口定义了三个方法
SortedSet subSet(E from,E to)
SortedSet headSet(E to)
SortedSet tailSet(E from)
这些方法返回一个子集,子集中的元素大于等于from,严格小于to。对于sorted maps 有类似的方法:
SortedMap<K ,V> subMap(K from,K to)
SortedMap<K,V> headMap(K to)
SortedMap<K,V> tailMap(K from)

返回的View中包含所有keys满足范围的entries

NaviagbleSet 接口有更多的subrange操作控制方法,你可以明确指定bounds是否包含在内
NaviGableSet subSet(E from,boolean fromInclusive,E to,boolean toInclusive);
NavigableSet headSet<E to,boolean toInclusive)
NavigableSet tailSet<E from,bolean fromInclusive)

9.5.4 Checked Views
Checked View 目标是提供一个问题的debug支持。该问题会在使用一般类型中出现。实际上,向一般collection里偷运错误类型的元素是可能的。比如:

var strings=new ArrayList<String>();
ArrayList rawList=strings;//warning only ,not an error,for compatibility with legacy code
rawList.add(new Date());//now strings comtains a Date object

错误的add命令在运行时没有被检测出,替代的,当你调用get并转换结果到Stirng时,类型转换异常会晚些时候发生。

checked View会检测到这一问题,下面定义一个安全的列表:
List safeStrings=Collections.checkedList(strings,String.class);

view的add方法UI插入属于给定类的对象,如果不是给定类,会抛出异常。这样的好处是错误会在当前位置就报告。
ArrayList rawList=safeStrings;
rawList.add(new Date());//checked list throws a ClassCastException

9.5.5 同步Views

如果你使用多线程访问collection,你需要确保collection不会被以外破坏。比如,如果一个线程试图向哈希表中添加元素,另一个线程在重新哈希这些元素,那就是灾难性的。

为了替代实现线程安全的collection类。库设计者使用view的机制保证常规的collection是线程安全的。比如,Collection类的静态的synchronizedMap方法,可以转换任何Map到拥有同步访问方法的Map

var map=Collections.synchronizedMap(new HashMap<String,Employee>);
你可以访问map对象使用多线程。get put方法都是同步的,每个方法都必须完全结束后才能被另一个线程调用。

9.5.6 可选操作的注意事项
View通常有一些限制,它也许是只读的,它也许不能够改变大小,也许只支持删除不支持插入。受限制的view会在你执行不合适的操作时抛出异常。

collection 和iterator接口的API文档中,许多操作被描述为可选操作。这也许看起来和接口的概念冲突。毕竟,接口是列出所有类必须实现的方法。确实,从理论上来看不令人满意。一个更好的解决方案也许是为只读views,不能更改collection的views定义分开的接口。但是,这会使接口数目增加三倍,库的设计者认为是不可接受的。

你应该扩展可选方法这一定义技巧到你自身的设计吗?我认为不可以,尽管collections时常被使用,实现他们的模式对于其他问题来说并不是典型的。collection类库的设计者必须解决特殊的有冲突的一系列需求。库的用户希望它学起来很简单,用起来方便,傻瓜式使用。而你遇到的问题不必依赖可选接口来实现。

9.6 算法
排序算法
var staff=new LinkedList();
Collections.sort(staff);

排序算法默认列表中的元素实现了Comparable接口。如果你想要按其他方式排序,你需要传递一个Comparator对象
staff.sort(Comparator.comparingDouble(Employee::getSalary));

如果要降序排列,使用Comparator.reverseOrder()
staff.sort(Comparator.comparingDouble(Employee::getSalary).reversed());

sort算法的参数要求可更改,不要求可改变大小。
定义如下:
支持set方法的列表可更改。
支持add remove方法的列表可改变大小。

Collections类的shuffle算法可以乱序。
ArrayList cards=...;
Collections.shuffle(cards);
如果列表不支持RandomAccess接口,shuffle方法会拷贝其中元素到数组,shuffle数组,重新拷贝回列表。

9.6.3 二分搜索
二分搜索要求执行搜索的Collection是有序的。
i=Collections.binarySearch(c,element);
i=Collections.binarySearch(c,element,comparator);

返回非负值表示搜索成功的位置索引,负值表示没有找到。
你可以利用搜索位置找到插入点
插入点

insertionPoint=-i-1;
if(i<0)
c.add(-i-1,element);

二分搜索要求是随机存储的。不要使用链表。

9.6.5 大批操作

coll1.removeAll(coll2);
移除所有的coll1中的coll2元素
coll1.reatinAll(coll2);
保留所有Coll1中的coll2元素。
利用函数找集合交集。

var result=new HashSet<String>(firstSet);
result.retainAll(secondSet);

如果你有一个Map,有ID 到Employee Object的映射。你想要一个没有被解雇的职员ID的集合。

Map<String,Employee> staffMap=...;
Set<String> terminatedIDs=...;

staffMap.keySet().removeAll(terminatedIDs);

通过使用subarange view,你可以限制sublist subset的大批操作。比如你想要添加一个list的前10个元素到另一个容器中。
relocated.addAll(staff.subList(0,10));
sburange也可以是突变操作的目标
staff.subList(0,10).clear();

9.6.6 Collection和Array的转换
使用List.of()方法将数组转换为collection

String[] names=...;
List<String> staff=List.of(names);

从collection转换为数组
Object[] names=staff.toArray();
这一方法返回的对象数组是Object的,无法强制类型转换
String[] names=(String[])staff.toArray();//error
传递数组构造函数表达式到toArray()获得指定类型
String[] values=staff.toArray(String[]::new);

9.7.1 Hashtable类
它是同步的。使用HashMap类替代它。如果需要同步场景,使用ConcurrentHashMap替代。
9.7.2枚举Enumerations
Enumeration接口拥有两个方法,hasMoreElements() nextElement(),它是Iterator hasNext() next()的模拟。

如果在遗留类中有这样的接口,使用Collections.list将元素收集在ArrayList中。
在Java9中 你可以将枚举转换为iterator
LogManager.getLoggerNames()l.asIterator().forEachRemaing(n->{...});

9.7.3 Property Maps
特殊的map
1.它的键值都是字符串
2.这个map可以飞擦会非常简单的从文件中存储和读取
3.对于默认值有第二个表

java实现了Property Map ,称为Properties。Property maps存储程序配置非常有用。

var settings=new Properties();
settings.setProperty("width","600.0");
setting.ssetProperty("filename","/home/cay/books/cj12/code/html");

使用store方法存储map列表的值到文件.

var out=new FileWriter("program.properties",StandardCharsets.UTF_8);
settings.store(out,"Program Properties");

加载

var in =new FileReader("program.properties",StandardCharsets.UTF_8);
settings.load(in);

String userDir=System.getProperty("user.home");

Properties类有两种机制提供默认值。
如果键没有表示时使用默认值
String filename = settings.getProperty("filename","");
如果property map中有filename属性,filename被设为键指向的值,否则设为空值。

将所有默认值写在第二个properties中,然后用它来给Properties的构造函数来初始化。

var defaultSettings=new Properties();
defaultSettings.setProperty("width","600");
defaultSettings.setProperty("height","400");
defaultSettings.setProperty("filename","");
...
var settings=new Properties(defaultSettings);

9.7.4 栈
push pop peek方法
9.7.5 Bit Sets
使用bit set比ArrayList更加高效,当存储布尔值时。
举例,bucketOfBits是一个BitSet

bucketOfBits.get(i);//on ->true off->false
bucketOfBits.set(i);//turn i bit on
bucketOfBits.clear(i);//turn i bit off
posted @ 2025-03-12 15:48  zhongta  阅读(14)  评论(0)    收藏  举报