Java基础 - 集合


集合的概念

集合可以简单理解为一个长度可以改变,可以保存任意数据类型的动态数组。

在 Java中,集合不是由一个类来完成的,而是由一组接口和类构成了一个框架体系。大致可分为3层,最上层是一组接口,继而是接口的实现类,接下来是对集合进行各种操作的工具类

接口 描述
Collection 集合框架最基本的接口,一个 Collection存储一组无序、不唯一的对象,一般不直接使用该接口
List Collection的子接口,存储一组有序、不唯一的对象,常用的接口之一
Set Collection的子接口,存储一组无序、唯一的对象
Map 独立于Collection的另外一个接口,存储一组键值对象,提供键到值的映射
Iterator 输出集合元素的接口,一般适用于无序集合,从前到后单向输出
ListIterator Iterator的子接口,可以双向输出集合中的元素
Enumeration 传统的输出接口,已被 Iterator所取代
SortedSet Set的子接口,可对集合中的元素进行排序
SorteMap Map的子接口,可对集合中的键值元素进行排序
Queue 队列接口,此接口的子类可实现队列操作
Map.Entry Map的内部接口,描述Map中的一个键值对元素

Collection 接口

Collection是集合框架中最基础的父接口,可以存储一组无序,不唯一的对象。一般不直接使用该接口,也不能被实例化,只是用来提供规范定义,Collection 接口的定义如下所示。

public interface Collection<E> extends Iterable<E>{}

可以看到 Collection 是 Iterable 的子接口,Collection 和 Iterable 后面的<E>表示它们都使用了泛型的定义,泛型是指在操作集合时需要指定具体的数据类型,这样可以保证数据的安全性。Collection接口常用方法的描述如下:

方法 描述
int size() 获取集合长度
boolean isEmpty() 判断集合是否为空
boolean contains(Object o) 判断集合中是否存在某个对象
Iterator<E> iterator() 实例化Iterator接口,遍历集合
Object[] toArray() 将集合转换为一个object类型的对象数组
T[] toArray(T[] a) 将集合转换为一个指定数据类型的对象数组
boolean add(E e) 向集合中添加元素
boolean remove(Object o) 从集合中移除元素
boolean containsAll(Collection<?> c) 判断集合中是否存在某个集合的所有元素
boolean addAll(Collection<? extends E> c) 向集合中添加某个集合的所有元素
boolean removeAll(Collection<?> c) 从集合中移除某个集合的所有元素
default boolean removeIf(Predicate<? super E> filter) 从集合中移除满足给定条件的集合的所有元素
boolean retainAll(Collection<?> c) 对集合进行操作,只保留包含在目标集合中的元素
void clear 清除集合中的所有元素
boolean equals(Object o) 比较两个集合是否相等
int hashCode() 获取集合的散列值
default Spliterator<E> spliterator() 将集合转化为一个指定数据类型的并行迭代器
default Stream<E> stream() 将集合转换为一个流
default Stream<E> parallelStream() 将集合转换为一个可并行的流

Collection 的子接口

Collection 作为集合的基本接口,在实际开发中一般不直接使用,而是使用其子接口进行开发,Collection主要的子接口如下:

  • List:存放有序,不唯一的元素。
  • Set:存放无序,唯一的元素。
  • Queue:队列接口

List接口

List 是 Collection 的常用的子接口,可以存储一组有序、不唯一的对象,List接口的定义如下

public interface List<E> extends Collection<E>{ }

List 接口在继承Collection接口的基础上进行了扩展,常用的扩展方法如下

方法 描述
E get(int index) 通过下标获取集合中指定位置的元素
E set(int index, E element) 替换集合中指定位置的元素
void add(int index, E element) 向集合中的指定位置添加元素
E remove(int index) 通过下标删除集合中指定位置的元素
int indexOf(Object o) 查找某个对象在集合中的位置
int lastIndexOf(Object o) 从后向前查找某个对象在集合中的位置
ListIterator<E> listIterator() 实例化ListIterator接口
List<E> subList(int fromIndex, int toIndex) 获取集合中的子集合

List接口的实现类

ArrayList 是开发中经常使用到的实现类,实现了长度可变的数组。可以在内存中分配连续的空间,底层是基于索引的数据结构,所以访问元素效率较高。缺点是若添加或者删除元素,需要移动兄弟元素的位置,效率较低。

public class Main {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add("hello");
        list.add("world");
        list.add("java");
        list.add("c++");
        list.add("python");
        System.out.println("打印list:" + list);
        System.out.println("list的长度:" + list.size());
        System.out.println("list是否包含java:" + list.contains("java"));
        System.out.println("------- 打印list ------");
        Iterator iterator = list.iterator();
        while (iterator.hasNext()){
            System.out.print(iterator.next() + ", ");
        }
        System.out.println("");
        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i) + ", ");
        }
        System.out.println("");
        System.out.println("------- 移除list元素 ------");
        list.remove("hello");
        list.remove(0);
        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i) + ", ");
        }
        System.out.println("");
        System.out.println("------- list添加元素 ------");
        list.add(0, "time");
        list.add(1, "sec");
        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i) + ", ");
        }
        System.out.println("");
        System.out.println("------- list替换元素 ------");
        list.set(0, "one");
        list.set(1, "two");
        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i) + ", ");
        }
        System.out.println("");
        System.out.println("c++ 在集合中的下标:" + list.indexOf("c++"));
        System.out.println("--------------------");
        List list1 = list.subList(2,5);
        System.out.println(list1);
    }
}
// 输出
打印list:[hello, world, java, c++, python]
list的长度:5
list是否包含java:true
------- 打印list ------
hello, world, java, c++, python, 
hello, world, java, c++, python, 
------- 移除list元素 ------
java, c++, python, 
------- list添加元素 ------
time, sec, java, c++, python, 
------- list替换元素 ------
one, two, java, c++, python, 
c++ 在集合中的下标:3
--------------------
[java, c++, python]

Vector 是一个早期的 List 实现类,用法基本与ArrayList一致。Stack 是 Vector的子类,实现了一个 “后进先出”的栈

public class Main {
    public static void main(String[] args) {
        Stack stack = new Stack();
        stack.push("one");
        stack.push("two");
        stack.push("three");
        for (int i = 0; i < stack.size(); i++) {
            System.out.print(stack.get(i) + " ");
        }
        System.out.println("");
        System.out.println("栈顶元素:" + stack.peek());
        for (int i = 0; i < stack.size(); i++) {
            System.out.print(stack.get(i) + " ");
        }
        System.out.println("");
        System.out.println("栈顶元素:" + stack.pop());
        for (int i = 0; i < stack.size(); i++) {
            System.out.print(stack.get(i) + " ");
        }
        System.out.println("");
    }
}
// 输出
one two three 
栈顶元素:three
one two three 
栈底元素:three
one two 

ListedList 实现了一个“先进先出”的队列,采用链表的形式存储分散的内存空间。元素和元素之间通过存储彼此的位置信息来形成连接关系,通过位置信息找到前后节点的关系,所以添加和删除元素的效率高;ListedList不但要保存节点数据,还需要保存前后节点的位置信息,所以需要更多的内存空间,查询元素的效率也低。

public class Main {
    public static void main(String[] args) {
        LinkedList linkedList = new LinkedList();
        linkedList.add("Tom");
        linkedList.add("Alice");
        linkedList.add("Bob");
        linkedList.add("Tim");
        for (int i = 0; i < linkedList.size(); i++) {
            System.out.print(linkedList.get(i) + " ");
        }
        System.out.println("");
        linkedList.offer("hong");
        linkedList.addLast("ming");
        System.out.println(linkedList);
        linkedList.push("wang");
        linkedList.addFirst("zhang");
        System.out.println(linkedList);
        System.out.println("第一个元素:" + linkedList.peekFirst());
        System.out.println(linkedList);
        System.out.println("最后一个元素:" + linkedList.peekLast());
        System.out.println(linkedList);
        System.out.println("取出第一个元素:" + linkedList.pop());
        System.out.println(linkedList);
        System.out.println("取出最后一个元素:" + linkedList.pollLast());
        System.out.println(linkedList);
    }
}

// 输出
Tom Alice Bob Tim 
[Tom, Alice, Bob, Tim, hong, ming]
[zhang, wang, Tom, Alice, Bob, Tim, hong, ming]
第一个元素:zhang
[zhang, wang, Tom, Alice, Bob, Tim, hong, ming]
最后一个元素:ming
[zhang, wang, Tom, Alice, Bob, Tim, hong, ming]
取出第一个元素:zhang
[wang, Tom, Alice, Bob, Tim, hong, ming]
取出最后一个元素:ming
[wang, Tom, Alice, Bob, Tim, hong]

LinkedList 和 stack 都有 pop()方法,都是取出集合的第一个元素,但是可以看到两者的顺序是相反的,Stack采用的是“后进先出”的方法,是栈的形式;LinkedList 采用的是“先进先出”的方式,是队列的形式

Set接口

Set是Collection的子接口,Set接口以散列的形式存储数据,所以元素没有顺序,可以存储一组无序且唯一的对象。

实际开发中不能直接实例化Set,需要对其实现类进行实例化再完成业务操作。Set的常用实现类主要有 HashSet、LinkedHashSet、TreeSet。

HashSet

HashSet是经常使用的实现类,存储一组无序且唯一的对象。无序是指元素的存储顺序和遍历顺序不一致。

public class Main {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add("hello");
        hashSet.add("world");
        hashSet.add("java");
        hashSet.add("c++");
        System.out.println(hashSet);
        System.out.println("hashSet的长度" + hashSet.size());
        Iterator iterator = hashSet.iterator();
        while (iterator.hasNext()){
            System.out.print(iterator.next() + ", ");
        }
        System.out.println("");
        hashSet.remove("c++");
        System.out.println(hashSet);
    }
}

// 输出
[c++, world, java, hello]
hashSet的长度4
c++, world, java, hello, 
[world, java, hello]

LinkedHashSet

LinkedHashSet 是Set的另外一个子接口,可以存储一组有序且唯一的元素,有序是指元素的存储顺序和遍历顺序是一致的。其用法跟Hashset类似。

对LinkedHashSet集合添加两个“hello”元素,但是只会保存一个,这是因为LinkedHashSet集合的元素是唯一的,即不能出现两个相等的元素。字符串和对象都必须满足这条。

LinkedHashSet 判断两个对象是否相等的过程:首先会比较对象的hashCode,如果不相等,则认为不是同一个对象,可以添加。如果hashCode相等,还不能认为两个对象就是相等的,需要通过equals()方法进一步判断。如果equals()方法为true,则不会重新添加,如果equals()方法为false,则正常添加。先判断hashCode是否相等可以减少equals()方法的调用,提高效率。

TreeSet

TreeSet中保存的元素也是有序的且唯一,但是TreeSet的有序和LinkedHashSet的有序并不一样。

LinkedHashSet的有序是指元素的存储顺序和遍历顺序一样。TreeSet的有序是指集合内部会自动给所有的元素安装升序进行排列,即无论存入元素的顺序是什么,遍历时会按照升序进行输出。

public class Main {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet();
        treeSet.add("1");
        treeSet.add("3");
        treeSet.add("5");
        treeSet.add("4");
        treeSet.add("2");
        System.out.println(treeSet);
    }
}

// 输出
[1, 2, 3, 4, 5]

存入对象并进行排序

public class Num implements Comparable
{
    private int num;
    public Num(int num){
        this.num = num;
    }
}
public class Main {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet();
        treeSet.add(new Num(1));
        treeSet.add(new Num(3));
        treeSet.add(new Num(4));
        treeSet.add(new Num(2));
        treeSet.add(new Num(5));
        System.out.println(treeSet);
    }
}

// 输出
Exception in thread "main" java.lang.ClassCastException: com.test.a.Num cannot be cast to java.lang.Comparable

报错原因是Num对象不具备排序功能,解决方法是实现Comparable接口。

public class Num implements Comparable
{
    private int num;
    public Num(int num){
        this.num = num;
    }

    @Override
    public int compareTo(Object o) {
        Num n = (Num) o;
        if (this.num > n.num){
            return 1;
        }else if (this.num == n.num){
            return 0;
        }else {
            return -1;
        }
    }

    @Override
    public String toString() {
        return "Num{" +"num=" + num +'}';
    }
}
public class Main {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet();
        treeSet.add(new Num(1));
        treeSet.add(new Num(3));
        treeSet.add(new Num(4));
        treeSet.add(new Num(2));
        treeSet.add(new Num(5));
        System.out.println(treeSet);
    }
}

// 输出
[Num{num=1}, Num{num=2}, Num{num=3}, Num{num=4}, Num{num=5}]

Map接口

Map中的元素都是以key-value的键值对映射形式存储的。Map接口定义时使用了泛型,并且定义了两个泛型K和V,K表示key,规定了键元素的数据类型,V表示value,规定了值元素的数据类型。Map接口的常用方法如下:

方法 描述
int size() 获取集合长度
boolean isEmpty() 判断集合是否为空
boolean containsKey(Object key) 判断集合中是否存在某个key值
boolcontainsValue(Object value) 判断集合中是否存在某个value值
V get(Object key) 取出集合中key对应的value值
V put(K key, V value) 向集合中存入一组 key-value的元素
V remove(Object key) 删除集合中key对应的value值
void pullAll(Map<? extends K,? extends V> m) 向集合中添加另外一个map集合
void clear() 清除集合中的所有元素
Set<K> keySet() 取出集合中所有的key,返回一个Set集合
Collection<V> values() 取出集合中的所有value,返回一个Collection集合
Set<Map.Entry<K,V>> entrySet() 将Map对象转换为Set对象
int hashCode() 获取集合的散列值
boolean equals(Object o) 比较两个集合是否相等

Map接口的实现类

Map接口常用的实现类如下:

  • HashMap:存储一组无序,key不可重复,但value可重复的元素
  • Hashtable:存储一组无序,key不可重复,但value可重复的元素
  • TreeMap:存储一组有序,key不可重复,但value可重复的元素,可以按照key来排序。

HashMap 的使用方法

public class Main {
    public static void main(String[] args) {
        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("a","hello");
        hashMap.put("s","world");
        hashMap.put("v","java");
        hashMap.put("w","c++");
        System.out.println(hashMap);
        hashMap.remove("a");
        System.out.println("删除后:" + hashMap);
        hashMap.put("g","google");
        System.out.println("添加后:" + hashMap);
        if (hashMap.containsKey("s")){
            System.out.println("map集合中存在值为s的key");
        }else {
            System.out.println("map集合中不存在值为s的key");
        }
        if (hashMap.containsValue("java1")){
            System.out.println("map集合中存在值为java的value");
        }else {
            System.out.println("map集合中不存在值为java的value");
        }
        Set keys = hashMap.keySet();
        System.out.println(keys);
        Collection<String> values = hashMap.values();
        System.out.println(values);
    }
}

// 输出
{a=hello, s=world, v=java, w=c++}
删除后:{s=world, v=java, w=c++}
添加后:{s=world, v=java, w=c++, g=google}
map集合中存在值为s的key
map集合中不存在值为java的value
[s, v, w, g]
[world, java, c++, google]

Hashtable 和 HashMap 的用法基本一样,与 HashMap相比:Hashtable是线程安全的,但是性能较低。HashMap是非线程安全的,但是性能较高。实际开发中,HashMap的使用频率更高。

TreeMap主要功能就是按照key对集合中的数据进行排序。

public class Main {
    public static void main(String[] args) {
        TreeMap<Integer, String> treeMap = new TreeMap<>();
        treeMap.put(3,"java");
        treeMap.put(5,"css");
        treeMap.put(1,"c++");
        treeMap.put(4,"nodejs");
        treeMap.put(2,"python");
        System.out.println(treeMap);
    }
}

// 输出
{1=c++, 2=python, 3=java, 4=nodejs, 5=css}

Collections工具类

集合除了可以存储数据,也提供了很多方法来对数据进行操作,但是这些方法都有局限性,实际操作也不方便。JDK提供了工具类Collections,专门用来操作集合,例如添加元素,对元素进行排序,替换元素等。Collections和Arrays很类似,Arrays是针对数组的工具类,Collections是针对集合的工具类。

public class Main {
    public static void main(String[] args) {
        ArrayList arrayList = new ArrayList();
        arrayList.add("hello");
        arrayList.add("world");
        System.out.println("添加之前的集合:" + arrayList);
        Collections.addAll(arrayList,"java", "python", "php");
        System.out.println("添加之后的集合:" + arrayList);
        Collections.reverse(arrayList);
        System.out.println("反转之后的集合:" + arrayList);
        Collections.swap(arrayList, 0, 2);
        System.out.println("交换之后的集合:"  + arrayList);
        int index = Collections.binarySearch(arrayList, "php");
        System.out.println("php在集合中的小标:" + index);
        Collections.replaceAll(arrayList, "world" , "js");
        System.out.println("替换之后的集合:" + arrayList);
    }
}

// 输出
添加之前的集合:[hello, world]
添加之后的集合:[hello, world, java, python, php]
反转之后的集合:[php, python, java, world, hello]
交换之后的集合:[java, python, php, world, hello]
php在集合中的小标:2
替换之后的集合:[java, python, php, js, hello]

泛型

泛型是指在类定义时不指定类中信息的具体数据类型,而是用一个标识符来代替,当外部实例化对象来指定具体的数据类型。

使用泛型可以避免数据不安全的隐患,接口是支持泛型的,所有在实例化ArrayList对象的时候就指定泛型为Integer,限制存入集合的数据,除了Integer类型以外的数据无法存入集合,当然指定数据类型的子类是可以存入的,这样就保证了集合中数据类型的统一性。在取数据的时候不会抛出数据转换类型失败的异常,同时在指定泛型后,集合内部就会以指定的数据类型来保存所有数据,取数据时就省去了强制类型转换的步骤。

泛型通配符

定义一个参数为ArrayList类型的方法时,希望该方法既可以接收泛型为String的集合参数,也可以接收泛型为Integer的集合参数。解决方法是使用ArrayList<?>,它可以使用任意的泛型类型对象。

public class Main {
    public static void main(String[] args) {
        ArrayList<String> strings = new ArrayList<>();
        ArrayList<Integer> integers = new ArrayList<>();    
        test(strings);
        test(integers);
    }
    public static void test(ArrayList<?> list){
        System.out.println(list);
    }
}

泛型上限和下限

使用泛型时,往往数据类型会有限制,只能使用一种具体的数据类型,如果希望在此基础上进行适量扩容,可以通过泛型上限和下限来完成。

泛型上限表示实例化时的具体数据类型,可以是上限类型的子类或者是上限类型本身,用extends关键字来修饰。

泛型下限表示实例化时的具体数据类型,可以时下限类型的父类或者是下限类型本身,用super关键字修饰

  • 泛型上限:类名<泛型标识 extends 上限类名>
  • 泛型下限:类名<泛型标识 super 下限类名>

泛型接口

在定义类时可以添加泛型,在定义接口时也可以添加泛型。声明泛型接口的语法和声明泛型类很相似,在接口名后加上<T>即可,基本语法:访问权限修饰符 interface 接口名<泛型标识>

实现泛型接口有两种方式,一种是实现类在定义时继续使用泛型标识,另一种是实现类在定义时直接给出具体的数据类型.

两种不同实现类的实例化方式也不同,一种需要在实例化时指定具体的数据类型,另外一种在实例化不需要指定具体的数据类型。

public class MyInterface<T> implements Interface<T>{}
MyInterface<String> myInterface = new MyInterface<String>("接口");

public class MyInterface implements Interface<String>{}
MyInterface myInterface = new MyInterface("接口");

文章作者: zerollone
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 zerollone !
  目录