Java笔记(集合)

集合
在这里插入图片描述

集合接口与实现分离:
与现代的数据结构类库的常见做法一样,Java集合类库也将接口(interface)与实现(implementation)分离。
队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中元素的个数。

每一个实现都可以用一个实现了Queue接口的类表示:
public class CircularArrayQueue<E> implements Queue<E>//not an actual library class
{
  private int head;
  private int tail;
  CircularArrayQueue(int capacity) {...}
  public void add(E element) {...}
  public E remove() {...}
  public int size() {...}
  private E[] elements;
}
public class LinkedListQueue<E> implements Queue<E>//not an actual library class
{
  private Link head;
  private Link tail;

  LinkedListQueue() {...}
  public void add(E element) {...}
  public E remove() {...}
  public int size() {...}
}

因此,只有在构造集合对象时,才会使用具体的类,可以使用接口类型存放集合引用。

Queue<Customer>expressLane = new CircularArrayQueue<>(100);
expressLane.add(new Customer("Harry"));

Collection接口
在Java类库中,集合类的基本接口是Collection接口。这个接口有两个基本方法:

public interface Collection<E>
{
  boolean add(E element);//add方法用于向集合中添加元素。
  Iterator<E>iterator();//iterator方法用于返回一个实现了Iterator接口的对象。
  ...
}

迭代器
Iterator接口包含4个方法:

public interface Iterator<E>
{
   E next();
   boolean hasNext();
   void remove();
   default void forEachReamining(Consumer<? super E> action);
}
通过反复调用next方法,可以逐个访问集合中的元素。(若达到集合的末尾,next方法将抛出一个NoSuchElementException。因此需要在调用next之前调用hasNext方法。

用“for each”循环可以更加简练地表示同样的循环操作:

for (String element : c)
{
  do something with element
}

Iterator接口的remove方法将会删除上次调用next方法时返回的元素。

例如,可以如下删除一个字符串集合中的第一个元素:
Iterator<Stirng> it = c.iterator():
it.next();//skip over the first element
it.remove();//now remove it
next方法和remove方法调用之间存在依赖性,若调用remove之前没有调用next,将是不合法的。
实际上,必须先调用next越过将要删除的元素:
it.remove();
it.next();
it.remove();//ok

泛型实用方法
由于Collection与Iterator都是泛型接口,可以编写处理任何集合类型的实用方法。

例如,下面是一个检测任意集合是否包含指定元素的泛型方法:
public static <E> boolean contains(Collection<E> c, Object obj)
{
  for(E element : c)
    if(element.equals(obj))
      return true;
      return false;
}

数组列表
List接口用于描述一个有序集合,并且集合中每个元素的位置很重要。
散列集
链表和数组允许你根据意愿指定元素的次序。
有一种众所周知的数据结构,可以用于快速的查找对象,这就是散列表。散列表为每个对象计算一个整数,称为散列码(hash code )。
数集
TreeSet类与散列集十分类似,不过它比散列集有所改进。数集是一个有序集合(sorted collection)。

假设插入3个字符串,然后访问添加的所有元素。
var sorter = new TreeSet<String>();
sorter.add("Bob");
sorter.add("Amy");
sorter.add("Carl");
for (String s: sorter) System.out.println(s);

队列 与双端队列
队列允许你高效地在尾部添加元素,并在头部删除元素。双端队列(deuqe)允许在头部和尾部都高效地添加或删除元素。不支持在队列中间添加元素。
优先队列(priority queue)
优先队列中的元素可以按照任意的顺序插入,但会按照有序的顺序进行检索。即,无论何时调用remove方法,总会获得当前优先队列中最小的元素。但,优先队列并没有对所有元素进行排序。

映射
Java类库为映射提供了两个通用的实现:HashMap和TreeMap。
散列映射对键进行散列,数映射根据键的顺序将元素组织为一个搜索树。散列或比较函数只应用于键。

以下代码建立一个散列映射来存储员工信息:
var staff = new HashMap<String,Employee>();
var harry = new Employee("Harry Hacker");
staff.put("987-98-9996",harry);
...

更新映射条目
正常情况下,可以得到与一个键关联的原值,完成更新,再返回更新后的值。
映射试图
集合框架不认为映射本身是一个集合。不过·,可以得到映射的视图(view)——这是实现了Collection接口或某个子接口的对象。
有三种视图:键集、值集合(不是一个集)以及键/值对集。键和键/值对可以构成一个集,因为映射中一个键只能有一个副本。

下面的方法:
Set<K> keySet()
Collection<V> values()
Set<Map.Entry<K,V>> entrySet()
会分别返回这3个视图。

枚举集与映射
EnumSet是一个枚举类型元素集的高效实现。
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.class);
EnumSet<Weekday> mwf = EnumSet.of(Weekday.MONDAY,Weekday.WEDNESDAY,Weekday.FRIDAY);
可以使用Set接口的常用方法来修改EnumSet。

EnumMap是一个键类型为枚举类型的映射。它可以直接且高效地实现为一个值数组。需要在构造器中指定键类型:

var personInCharge = new EnumMap<Weekday,Employee>(Weekday.class);

子范围
可以为很多集合建立子范围(subrange)视图。假如有一个列表staff,想从中取出第10个~第19个元素。可以使用subList方法来获得这个列表子范围的视图。

List<Employee> group2 = staff.subList(10,20);
可对子范围应用任何操作,且操作会自动反映到整个列表。例如,可删除整个子范围:
group2.clear();//staff reduction
对于有序集和映射,可使用排序顺序而不是元素位置建立子范围。SortedSet接口声明了3个方法:
SortedSet<E> subSet(E from,E to)
SortedSet<E> headSet(E to)
SortedSet<E> tailSet(E from)

检查型视图
“检查型”视图用来对泛型类型可能出现的问题提供调试支持。
实际上将错误类型的元素混入泛型集合中的情况极有可能发生。例如:

var strings = new ArrayList<String>();
ArrayList rawList = strings;//warning only, not an error
                            //for compatibility with legacy code
rawList.add(new Date());//now strings contains a Date object!
这个错误的add命令在运行时检测不到。实际上,只有当另一部分调用get方法,并将结果强制转换为String时,才会出现一个类强制转换异常。
检查型视图可以探测这类问题。下面定义一个安全列表:
List<String> safeStrings = Collection.checkedList(strings,String.class);
这个视图的add方法将检查插入的对象是否属于给定的类。如果不属于给定的类,就立即抛出一个ClassCastException。这样做的好处是会在正确的位置报告错误:
ArrayList rawList = safeStrings;
rawList.add(new Date());//checked list throws a ClassCastException

算法
为什么使用泛型算法
泛型集合接口有一个很大的优点,即算法只需要实现一次。
例如考虑一下计算集合中最大元素的简单算法。使用传统方式,程序设计人员可能会用循环实现这个算法。可以如下找出数组中最大的元素。

if(a.length == 0) throw new NoSuchElementException();
T largest = a[0];
for (int i = 1; i<a.length;i++)
   if (largest.compareTo(a[i]) <0)
      largest = a[i];

或者可以将max方法实现为能够接收任何实现了Collection接口的对象。
public static <T extends Comparable> T max(Collection<T> c)
{
  if (c.isEmpty()) throw new NoSuchElementException();
  Iterator<T> iter = c.iterator();
  T largest = iter.next();
  while (iter.hasNext())
  {
    T next = iter.next();
    if(largest.compareTo(next) < 0)
      largest = next;
  }
  return largest;
}
现在就可以使用一个方法计算链表、数组列表或数组中的最大元素了。

排序与混排
Collections类中的sort方法可以对实现了List接口的集合进行排序。

var staff = new LinkedList<String>();
fill collection
collections.sort(staff);
也可以使用List接口的sort方法并传入一个Comparator对象。可以如下按工资对一个员工列表排序:
staff.sort(Comparator.comparingDouble(Employee::getSalary));
如果想按照降序对列表进行排序,可以使用静态的便利方法Collectinons.reverseOrder()。这个方法将返回一个比较器,比较器则返回b.compareTo(a)。例如:
staff.sort(Comparator.reverseOrder())
同理:
staff.sort(Comparator.comparingDouble(Employee::getSalary).reversed())//将按工资逆序排序

二分查找
在数组中查找一个对象,可以就在数组的前半部分查找;或者在数组的后半部分查找。
Collection类的binarySearch方法实现了这个算法。(集合必须是有序的,要想查找某个元素,必须提供集合)

i = Collections.binarySearch(c,element);
i = Collections.binarySearch(c,element,comparator);
如果binarySearch方法返回一个非负的值,这表示匹配对象的索引。
可以利用返回值来计算应该将element插入到集合的哪个位置,以保持集合的有序性。插入的位置是
insertionPoint = -i - 1;
这并不是简单的-i,因为0值是不确定的。以下:
if(i<0)
   c.add(-i-1,element);
将把元素插入到正确的位置上。

批操作
很多操作会“成批”复制或删除元素。以下调用:

colll.removeAll(coll2);
将从colll中删除coll2中出现的所有元素。与之相反,
colll.retainAll(coll2);
会从colll中删除所有未在coll2中出现的元素。

假设希望找出两个集的交集(intersection),也就是两个集中共有的元素。首先,建立一个新集来存放结果:

var result = new HashSet<String>(firstSet);
每一个集合都有这样一个构造器,其参数是包含初始值的另一个集合。
现在来使用retainAll方法:
result.retainAll(secondSet);
例如,假设有一个映射,将员工ID映射到员工对象,另外有一个不再聘用的所有员工的ID集。
Map<String,Employee> staffMap = ...;
Set<String>terminatedIDs = ...;
只需要建立一个键集,并删除终止聘用关系的所有员工的ID。
staffMap.keySet().removeAll(terminatedIDs);
由于键集是映射的一个视图,所以键和相关联的员工名会自动从映射中删除。

集合与数组的转换
如果需要把一个数组转换为集合,List.of包装器可实现例如:

String[] values = ...;
var staff = new HashSet<>(List.of(values));
从集合得到数组会更困难一些。当然,可以使用toArray方法:
Object[] values = staff.toArray();

遗留的集合
Hashtable类:经典的Hashtable类与HashMap类的作用一样,实际上,接口也基本相同。与Vector类的方法一样,Hashtable方法也是同步的。

枚举
遗留的集合使用Enumeration接口遍历元素序列。Enumeration接口有两个方法,即hasMoreElements和nextElement。这两个方法完全类似于Iterator接口的hasNext方法next方法。
属性映射
有三个特性:
1.键与值都是字符串
2.这个映射可以很容易地保存到文件以及从文件加载
3.有一个二级表存放默认值
实现属性映射的Java平台类名为Properties。属性映射对于指定程序的配置选项很有用。
例如:

var settings = new Properties();
settings.setProperty("width","600.0");
settings.setProperty("filename","/home/cay/books/cj11/code/v1ch11/raven.html");

Properties类有两种提供默认值的机制,第一种是只要查找一个字符串的值,可以指定一个默认值,这样当键不存在时就会自动使用这个默认值。

String filename = settings.getProperty("filename","");
如果属性映射中有一个“filename”属性,filename就会设置为相应的字符串。否则,filename会设置为空串。
如果觉得在每个getProperty调用中指定默认值太过麻烦,可以把所有默认值都放在一个二级属性映射中,并在主属性映射的构造器中提供这个二级映射。
var defaultSettings = new  Properties();
defaultSettings.setProperty("width","600");
defaultSettings.setProperty("height","400");
defaultSettings.setProperty("filename","");
...
var settings = new Properties(defaultSettings);


位集:由于位集将位包装在字节里,所以使用位集要比使用Boolean对象的ArrayList高效得多。
BitSet类提供了一个便于读取、设置或重置各个位的接口。使用这个接口可以避免掩码和其他调整位的操作,如果将位存储在int或long变量中就必须做这些烦琐的操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值