Java集合

集合


集合简介

集合(Collection):是由若干个确定元素构成的整体的一种数据结构。

集合与数组非常相似,既然有了数组这种数据结构,为什么还需要集合呢?因为数组有许多限制:

  • 数组初始化后大小不可变;
  • 数组只能按照索引顺序存取数据。

所以,我们需要不同类型的集合来处理不同的数据。例如:

  • 可变大小的顺序链表;
  • 保证五重复元素的集合;

Collection

Java 标准库自带的 java.util 包提供了了集合类: Collection,它是除了 Map 外所有其他集合类的根接口。Java 的 java.util 包主要提供了以下三种类型的集合:

  • List:一种有序列的集合;
  • Set:一种无重复元素的集合;
  • Map:一同通过键值(key-value) 查找的映射表集合。

Java 集合有几个特点:实现接口和实现类相分离,比如,有序表的接口是 List,具体实现类有 ArrayList,LinkList 等;其次是支持泛型,可以限制在一个集合中只能放入同一种数据类型的元素。

Java 访问集合总是通过迭代器(Iterator)来实现,好处是无需知道集合内部的元素按照什么方式存储。

List

在集合类中,List 是一种最基本的集合:它是一种有序列表。

List 几乎和数组完全一样,List 内部按照放入元素的先后顺序存放,每个元素都可以通过索引确定自己的位置,List 的索引和数组一样都是从 0 开始。

数组也是有序结构,但是我们在添加和删除元素的时候非常不方便,因为它是把一部分元素依次向后或向前移动来实现元素的添加和删除,效率非常低下。

实际上在使用需要增删的有序列表时,我们一般选择 ArrayList。ArrayList 在内部也是使用数组来存储所有的元素。但是ArrayList 在第一次添加元素时会直接创建一个大小为10的数组,当需要添加元素时,ArrayList 自动移动元素,当数组已满,没有空闲位置时,ArrayList 会重新创建一个更大的数组(默认按照1.5倍扩容)然后把原来的数组复制到新数组,然后用新数组取代原来的数组。

查看 List 接口,可以看到以下几个主要的接口方法:

  • boolean add(E e):在末尾添加一个元素;
  • boolean add(int index, E e):在指定索引处添加一个元素;
  • int remove(int index):删除指定索引的元素;
  • int remove(Object o):删除某个元素;
  • E get(int index):获取指定索引的元素;
  • int size():获取列表大小。

LinkedList 也是 List 接口的一种实现,linkedList 是一种链表,它的内部每个元素都指向下一个元素。

ArrayList 与 LinkedList 区别:

AraryListLinkedList
获取指定元素通过下标,速度快需要从头查找元素,速度慢
在指定位置添加/删除需要移动元素,速度慢无需移动元素,速度快
占用内存

通常情况下优先使用 ArrayList。

List 的特点:List 内部元素可以重复,还允许添加 null。

List 的创建:除了使用 ArrayList 和 LinkedList,我们还可以通过 List 接口提供的 of() 方法,根据给定的元素快速创建 List:

List<Integer> list = List.of(1.2.5);

但是 List.of() 不接受 null 值,如果传入null, 会抛出 NullPointerException 异常。

遍历 List

和数组一样,我们也可以使用 for 循环根据索引配合 get(i) 方法遍历,但是这种方法不推荐,一是代码复杂,二是因为 get(i) 方法只有对 ArrayList 是高效的,换成 LinkedList 后,索引越大,访问速度越慢。所以我们更推荐使用迭代器 Iterator 来方位 List。Iterator 本身也是一个对象,但它是由 List 的实例调用 iterator() 方法的时候创建的。Iterator 对象知道如何遍历一个 list 类型,并且不同的 List 类型,返回的 Iterator 对象实现也是不同的,但总是具有最高的访问效率。

Iterator 对象有两个方法:boolean hasNext() 判断是否有下一个元素,E next() 返回下一个元素。因此 Iterator 遍历 List 代码如下:

public static void main(String[] args) {
    List<String> list = List.of("A", "b", "c");
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        System.out.println(iterator.next());
    }
}

Iterator 遍历 List 是最高效的方式。并且,由于 Iterator 遍历时如此常用,所以,java 的 for each 循环本身就可以帮我们使用 Iterator 遍历。

public static void main(String[] args) {
    List<String> list = List.of("A", "b", "c");
    list.forEach(s -> {
        System.out.println(s);
    });
}

List 和 Array 转换

把 List 变为 Array 有三种方法,第一种是调用 toArray() 方法直接返回一个 Object[] 数组。这种方法会丢失类型信息,所以实际用的少。

public static void main(String[] args) {
    List<String> list = List.of("A", "b", "c");
    Object[] arr = list.toArray();
    for (Object o : arr) {
        System.out.println(o);
    }
}

第二种是给 toArray(E []) 传入一个类型相同的 Array, List 内部会自动把元素复制到传入的 Array 中。

public static void main(String[] args) {
    List<String> list = List.of("A", "b", "c");
    String[] strings = list.toArray(new String[3]);
    for (Object o : strings) {
        System.out.println(o);
    }
}

第三种是通过 List 接口定义的 T[] toArray(IntFunction<T[]> generator) 方法

public static void main(String[] args) {
    List<String> list = List.of("A", "b", "c");
    String[] strings = list.toArray(String[]::new);
    for (Object o : strings) {
        System.out.println(o);
    }
}

Array 转 List

Array 转 List 就简单的多,通过 List.of(T…) 方法最简单。要注意的是,返回的 List 不一定就是 ArrayList 或者 LinkedList,因为 LIst 只是一个接口,如果我们调用 List.of() 它返回的是一个只读 List:

public static void main(String[] args) {
    String[] strArr = {"a", "b","c"};
    List<String> strArr1 = List.of(strArr);
    for (String s : strArr) {
        System.out.println(s);
    }
}

使用Map

Map<k, v> 是一种键-值映射表,当我们调用 put(k key, v value) 方法的时候,就把 key 和 value 做了映射并放入 Map。当我们调用V get(k key) 时,就可以通过 key 获取对应的value。如果 key 不存在,则返回 null。和 List 类似,Map 也是一个接口,最常用的实现类是 HashMap。

如果只想查询某个 key 是否存在,可以调用 boolean containsKey(k key) 方法。

一个 key 只能关联一个 value,如果我们用一个 key 调用两次 put() 方法,分别放入两个不同的 value,那么后一次 put 的值会将第一次 put 的值覆盖掉。注意 put 方法是有返回值的,源码是 public V put(K key, V value) 这样定义的,返回值是value。当我们第一次调用 put 方法返回的是 null,第二次 put 同一个key,返回的是第一次 put 的 value。因为 put 同一个 key 时,会把第一次 put 的值删掉并返回,在 put 新值。

遍历Map

对 Map 来说,要遍历 key 可以使用 for each 循环遍历 Map 实例的 keySet() 方法返回 Set 集合,它包含不重复的 key 的集合。

public static void main(String[] args) {
    HashMap<String, String> map = new HashMap<>();
    map.put("a", "1");
    map.put("b", "2");
    map.put("c", "3");
    for (String s : map.keySet()) {
        System.out.println("key: " + s);
    }
}

如果要同时遍历 key 和 value 可以使用 for each() 循环遍历 Map 实例的 entrySet() 集合,它包含每一个 key-value 映射:

public static void main(String[] args) {
    HashMap<String, String> map = new HashMap<>();
    map.put("a", "1");
    map.put("b", "2");
    map.put("c", "3");
    for (Map.Entry<String, String> entry : map.entrySet()) {
        System.out.println("key: " + entry.getKey() + " + value: " + entry.getValue());
    }
}

Map 和 List 不同的是,Map 存储的是 key-value 映射关系,并且不保证顺序。在遍历的时候,顺序是随机的。所以在使用 Map 的时候依赖顺序的逻辑都是不可靠的。

Map 内部实现

我们知道 Map 是一种键值映射表,可以通过 key 快速的找到 value,它的内部是怎么实现的呢?

其实它是用一个数组存储所有的 value,根据 key 的值计算出的值作为索引,将 value 存储在此索引下。

这时我们就会发现一个问题,假若我们的 key 是一个对象,我们获取 value 的 key 对象不是我们放入 value 的那个 key对象。也就是说 key 对象的内容相同,但不是一个对象,这个时候还能取到 value 吗?

public static void main(String[] args) {
    HashMap<String, String> map = new HashMap<>();
    String key1 = new String("a");
    map.put(key1, "1");

    String key2 = new String("a");
    System.out.println(map.get(key2));
}

答案是可以取到的,因为在 Map 内部,对 key 做比较是通过 equals() 实现的,相同的 key 对象(使用 equals 判断时返回 true)必须要计算出相同的索引,否则,相同的 key 每次计算的索引不一样,就会取出不同的 value。因此,正确的使用 Map 必须保证:

1.作为 key 的对象必须正确覆写 equals() 方法,相等的两个 key 实例调用 equals() 方法必须返回 true。

2.作为 key 对象还必须正确覆写 hashCode() 方法,且 hashCode() 方法要严格遵循以下规范

  • 如果两个对象相等,则两个对象的 hashCode 必须相等;
  • 如果两个对象不相等,则两个对象的 hashCode 尽量不要相等。

对应实例 a 和 b :

  • 如果 a 和 b 相等,那么 a.equals(b) 一定为 true,则 a.hashCode() 必须等于 b.hashCode();
  • 如果 a 和 b 不相等,那么 a.equals(b) 一定为 false,则 a.hashCode() 尽量不要与 b.hashCode() 相等。

如上,如果 a.equals(b) 为 false,但是 a.hashCode() 和 b.hashCode() 相等了,怎么办呢?a 和 b 计算的索引相等,那么后放入的 value 会不会把先放入的 value 覆盖呢?实际上是不会覆盖的,虽然索引一样,但是key不相等,HashMap 内部会把这两个键值对放入到 List 中,然后在把 List 映射到索引上。在取值时,需要先找到索引,根据索引取出 List,在遍历 list 取出对应的值。

HashMap内部使用的是数组,那么 HashMap 内部使用的数组是多大呢?实际上 HashMap 初始化时默认数组大小只有16,当添加的键值对超过初始大小时,HashMap 会在内部自动扩容,每次扩容一倍,扩容后会重新确定键值对的索引,如果 HashMap 需要频繁的扩容,则性能就会大打折扣。如果我们能确定容量的大小,可以在初始化时,指定大小:

Map map = new HashMap<>(10000);

EnumMap

HashMap 是通过 key 计算 hashCode() ,通过空间换时间的方式,直接定位到对应的索引,因此查找效率非常高。

如果 key 的对象是 Enum 类,那么还可以使用 java 集合库中的 EnumMap,根据 enum 类型的 key 直接定位到索引,不需要计算 hashCode,不但效率高,还不浪费空间。

public static void main(String[] args) {
    EnumMap<DayOfWeek, Object> map = new EnumMap<>(DayOfWeek.class);
    map.put(DayOfWeek.MONDAY, "星期一");
    map.put(DayOfWeek.TUESDAY, "星期二");
    map.put(DayOfWeek.WEDNESDAY, "星期三");
    map.put(DayOfWeek.THURSDAY, "星期四");
    map.put(DayOfWeek.FRIDAY, "星期五");
    map.put(DayOfWeek.SATURDAY, "星期六");
    map.put(DayOfWeek.SUNDAY, "星期天");
    System.out.println(map);
    System.out.println("map.get(DayOfWeek.MONDAY) = " + map.get(DayOfWeek.MONDAY));
}

如果 Map 的 key 是 enum 类型的,推荐使用 EnumMap,既保证速度,还不浪费空间。

TreeMap

我们知道HashMap内部的key是无序的,还有一种Map,它内部会对key进行排序,这种Map就是SortedMap。SortedMap是接口,它的实现类是TreeMap。

SortedMap保证遍历的时候以key的顺序来排序。

public static void main(String[] args) {
    Map map = new TreeMap();
    map.put("zip", "z");
    map.put("get", "g");
    map.put("man", "m");
    map.put("app", "a");
    map.put("boy", "b");
    System.out.println(map);
    // {app=a, boy=b, get=g, man=m, zip=z}
}

使用TreeMap的时候,放入的key必须实现Comparable接口。String、Integer这些类已经实现了Comparable接口,因此可以直接作为key使用。Value对象则没有任何要求。

Properties

在编写程序时,有时候需要读写配置文件,此时,可以通过配置 .properties文件的方式实现。

username=admin
password=123

配置文件的特点是,它的key-value一般都是String-String类型的,因此我们完全可以用Map<String, String>来表示它。

因为配置文件非常常用,Java集合库使用 Properties来表示一组配置。Properties内部本质时一个HashTable。

当需要从文件里读取到这个 .properties 文件时:

// 第一步:创建 Properties 实例
Properties prop = new Properties();

// 第二部:调用 load() 读取文件
String f = "setting.properties";
prop.load(new FileInputStream(f));

// 第三步:调用 getProperty() 获取配置
prop.getProperty("username");
prop.getProperty("password");

有多个 .properties 文件时,可以反复 load() 读取,后读取的key-value会覆盖已读取的key-value。

SET

我们知道,Map 用于存储key-value的映射,对于充当key的对象,是不能重复的,并且要重写equals()和hashCode()方法。如果我们只需要存储不重复的key,并不需要存储映射的value,那么就可以使用Set。

Set用于存储不重复的元素集合,它主要提供以下几个方法:

  • 将元素添加进Set:boolean add(E e)
  • 将元素从Set删除:boolean remove(Object e)
  • 判断是否包含元素:boolean contains(Object e)
public static void main(String[] args) {
    HashSet<String> set = new HashSet<>();
    System.out.println(set.add("aa")); // true
    System.out.println(set.add("bb")); // true
    System.out.println(set.add("bb")); // false

    System.out.println(set.contains("aa")); // true
    System.out.println(set.contains("cc")); // false

    System.out.println(set.remove("bb"));// true

    System.out.println(set.size()); // 1
}

Set实际上是只存储key,不存储value的Map。Set最常被用来去除重复元素。Set接口并不保证有序,如果需要有序的Set,就要使用SortedSet接口:

  • HashSet是无序的,因为它实现了Set接口,并没有实现SortedSet接口;
  • TreeSet是有序的,因为它实现了SortedSet接口。

使用TreeSet和使用TreeMap一样,添加原色必须正确的实现Comparable接口,如果没有实现Comparable接口,那么创建TreeSet时必须传入一个Comparator对象。

Queue

队列是一种经常使用的集合。Queue实际上实现了一个先进先出的有序表。它和List的区别在于,List可以在任意位置添加和删除元素,而Queue只有两个操作:

  • 把元素添加到对立末尾;
  • 从队列头部取出元素。

队列接口Queue定义了以下几个方法:

  • int size():获取队列的长度;
  • boolean add(E) / boolean offer(E):天界元素到队尾;
  • E remove() / E pool():获取队首元素并将元素删除
  • E element() / E peek():获取队首元素并不从队列中删除。

对应到具体的实现类,有的Queue有最大队列长度,有的Queue没有。注意到添加和删除都有两个方法,这是因为在添加和获取元素失败时,这两个方法的行为是不同的:

throw Exception返回false或null
添加元素到队尾add(E e)boolean offer(E e)
获取队首元素并删除E remove()E pooll()
获取队首元素但不删除E element()E peek()

PriorityQueue

PriorityQueue和Queue的区别在于,它的出队顺序于元素的优先级有关,对PriorityQueue调用remove()或poll()方法,返回的总是优先级高的元素。

要使用PriorityQueue,我们就必须给每个元素定义优先级,

public static void main(String[] args) {
    Queue<Object> o = new PriorityQueue<>();
    o.offer("a");
    o.offer("c");
    o.offer("b");
    System.out.println(o.poll()); // a
    System.out.println(o.poll()); // b
    System.out.println(o.poll()); // c
    System.out.println(o.poll()); // null,因为队列为空
}

我们放入的顺序是a、c、b,但取出的顺序确实a、b、c,这是因为PriorityQueue按照字符串排序了。因此放入的元素必须要实现Comparable接口,PriorityQueue会根据元素的排序顺序决定出队的优先级。

Deque

Queue是队列,只能一头进,从另一头出。还有一种队列可以两头进,两头出,这种队列叫做双端队列(Double Ended Queue) Eeque。它即可以添加到队尾,也可以添加到队首;即可以从队尾获取,也可以从队首获取。

我们比较以下Queue和Deque的出队和入队的方法:

QueueDeque
添加元素到队尾add(E e)/offer(E e)addLast(E e)/offerLast(E e)
取队首元素并删除E remove()/E poll()E removeFirst()/E pollFirst()
取队首元素并不删除E element()/E peek()E getFirst()/E peekFirst()
添加元素到队首addFirst(E e)/offerFirst(E e)
取队尾元素并删除E removeLast()/E pollLast()
取队尾元素但不删除E getLast()/E peekLast()

Stack

栈是一种先进后出的数据结构。就是最后进Stack的一定最先出Stack。Stack就像一个桶,只能不断地往里压入(push)元素,最后进去的必须最先弹(pop)出来。

Stack只有入栈和出栈的操作:

  • 把元素压栈:push(E e);
  • 把栈顶元素弹出:pop(E e);
  • 取栈顶元素但不弹出:peek(E e )。

Stack在计算机中使用非常广泛,JVM在处理java方法调用的时候就会通过栈这种数据结构维护方法调用的层次。JVM会创建方法调用栈,每调用一个方法时,先将参数压栈,然后执行对应的方法;当方法返回时,返回值压栈,调用方法通过出栈操作获得返回值。因为方法调用栈有容量限制,嵌套调用过多会造成栈溢出,即引发StackOverflowError;

Collections

Collections是JDK提供的工具类,同样位于java.util包中,它提供一系列静态方法,能够方便的操作各种集合。

我们一般看参数名和参数就可以确认Collections 提供的该方法的功能。比如:

public static boolean addAll(Collection<? super T> c, T... elements){}

addAll()方法可以给一个Collection类型的集合添加若干元素。因为方法签名是Collection,所以我们可以传入List,Set等各种集合类型。

创建空集合:

  • 创建空List:List emptyList();
  • 创建空Map:Map<k, v> emptyMap();
  • 创建空Set:Set emptySet()

要注意返回的空集合是不可变集合,无法向其中添加或删除元素。此外,也可以用各种集合接口提供的of(T…)方法创建单元素集合。例如,以下两种创建空List的方法是等价的:

List<Object> of = List.of();
List<Object> objects = Collections.emptyList();

创建单元素集合:

Collections提供了一系列方法来创建一个单元素集合:

  • 创建一个元素的List:List singletonList(T o)
  • 创建一个元素的Map:Map<k, v> singletonMap(k key, v value)
  • 创建一个元素的Set:Set singleton(T o)

返回的单元素集合也是不可变集合,无法向其中添加或删除元素。也可用各种集合接口提供的of(T…)方法创建单元素集合。例如,以下创建单元素List的两个方法是等价的:

List<String> a = List.of("a");
List<String> a1 = Collections.singletonList("a");

实际上,使用List.of(T…)更方便,因为它既可以创建空集合,也可以创建单元素集合,还可以创建任意个元素集合:

List<String> a1 = List.of();
List<String> a2 = List.of("a");
List<String> a3 = List.of("a", "b");
List<String> a4 = List.of("a", "b", "c");

排序:

Collections可以对List进行排序。因为排序会直接修改List元素的位置,因此必须传入可变List:

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("a");
    list.add("c");
    list.add("b");
    System.out.println(list); // [a, c, b]
    Collections.sort(list);
    System.out.println(list); // [a, b, c]
}

洗牌:

Collections提供了洗牌算法,即传入一个有序的List,可以随机打乱List内部元素的顺序,效果相当于让计算机洗牌:

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("a");
    list.add("b");
    list.add("c");
    System.out.println("洗牌前: " + list); // 洗牌前: [a, b, c]
    Collections.shuffle(list);
    System.out.println("洗牌后: " + list); // 洗牌后: [b, a, c]
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值