黑马程序员---Java数据结构

本文详细介绍了Java中的集合框架,包括集合、列表、队列、映射表和集等数据结构。讲解了它们的特点、常用方法以及如何进行操作。特别提到了ArrayList和LinkedList的性能差异,以及HashMap、TreeMap和LinkedHashMap的选择建议。还讨论了Map接口的实现,如HashMap、TreeMap和ConcurrentHashMap,并强调了在多线程环境下的选择。最后总结了不同数据结构的适用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

——Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ——-
2015.10.8
2016.3.11

目录:

1.集合
2.列表
3.队列
4.映射表
5.集
6.遗留的集合实现
7.总结


1.集合

集合(Collection)是Java数据结构中的列表(List)、集(Set)、队列(Queue)等的超级接口。其中定义了公共的API。
除了特殊实现外,集合与数组的区别在于集合是无界的,可以保存任意多个元素。

(1)常见的方法:

  • 添加元素

    pubiic boolean add(E e)//添加单个元素
    public boolean addAll(Collection c)//添加整个Collection对象中的元素

    这两个方法的返回值表明当前调用者由于调用而发生更改,那么返回true。

    示例:

    // 新建一个Collection
    Collection c = new ArrayList();
    //添加单个元素
    boolean flag1 = c.add("Java");
    boolean flag2 = c.add("Android");
    
    //新建一个Collection
    Collection c2 = new ArrayList();
    //添加一整个Collecion对象的元素
    boolean flag3 = c2.addAll(c);
    
    //打印三次的添加结果
    System.out.println(flag1);//true
    System.out.println(flag2);//true
    System.out.println(flag3);//true    
    
  • 查询Collection

    public boolean contains(E e)//查询单个元素
    public boolean containsAll(Collection c)//查询整个Collecion
    public int size()//返回当前Collecion包含的元素

    示例:

    Collection c = new ArrayList();
    Collection c2 = new ArrayList();
    c.add("Java");
    c.add("Android");
    c2.addAll(c);
    
    // 使用contain方法查询元素是否存在
    boolean flag1 = c.contains("Java");
    // 使用containAll方法查询整个Collecion
    boolean flag2 = c2.containsAll(c);
    // 使用size方法查询集合的元素个数
    int size=c.size();
    
    // 打印结果
    System.out.println(flag1);// true
    System.out.println(flag2);// true
    System.out.println(size);// 3
    
  • 删除元素

    public void clear()//清除所有元素
    public boolean remove(Object obj)//从此collection中移除指定元素
    public boolean removeAll(Collection c)//移除整个c对象中包含的元素

    示例:

    Collection c1 = new ArrayList();
    Collection c2 = new ArrayList();
    c1.add("Java");
    c1.add("Android");
    c2.addAll(c1);
    
    // 清除c中的所有元素
    c1.clear();
    // 使用isEmpty方法判断c中是否为空
    boolean flag1 = c1.isEmpty();
    System.out.println(flag1);
    
    String target = "Java";
    // 清除c2中的"Java"元素
    c2.remove(target);
    // 使用contain方法查询元素是否被删除
    boolean flag2 = c2.contains(target);// true
    
    // 使用removeAll方法将清除c2与c1同时包含的元素
    c2.removeAll(c1);//由于c1为空,所以当前步骤将不删除任何元素
    
  • 遍历Collecion

    Collecion实现了Iterable接口,意味者它可以用迭代器迭代。迭代器是一种设计模式,Collecion中有数量巨大的实现类,每一个实现类的数据结构和实现细节都可能不同,但是迭代器没有依赖这些,这正是他的强大之处。

    下面的方法将返回当前Collecion的迭代器:
    public Iterator iterator()

    示例:

    Collection c = new ArrayList();
    c.add("Java");
    c.add("and");
    c.add("Android");
    
    // 获取c的迭代器
    Iterator iterator = c.iterator();
    
    while (iterator.hasNext()) {
        // 获取每一个元素
        String element = (String) iterator.next();
        // 下面的条件语句将删除"and"元素
        if (element.equals("and"))
            iterator.remove();
        // 打印元素到控制台
        System.out.println(element);
    }
    

    使用迭代器时要注意:

    • Java迭代器与位置紧密相连,只有调用了next方法才能查找某个元素。
    • 可以认为Java迭代器位于两个元素之间。当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素。
    • 同理,只有调用了next方法才能使用remove方法。
    • 迭代器进行迭代的时候,只能使用迭代器对Collecion进行修改,比如对Collecion元素的删除,如果迭代器发现它的集合被另一迭代器或者集合自身的方法修改了,就会抛出并发修改异常。例如下面的示例将是错误的:

      while (iterator.hasNext()) {
          // 获取每一个元素
          String element = (String) iterator.next();
          //此处使用c对象进行修改,将出现异常。迭代器依赖于集合存在,当集合元素被修改而迭代器不知道时将出现未知状况,所以迭代器检测到异常的修改后将抛出异常
          //c.remove("and");Error,ConcurrentModificationException
      }
      
  • 集合与数组的相互转换

    Collecion与数组的转换非常有用,可以把Collecion转化成一个数组,此时将大大增强数据的随机访问性能;同样使用数组初始化Collecion将可以使用很多集合中便利的功能。

    数组转换为集合:
    Arrays.asList(T…a);
    返回一个受指定数组支持的固定大小的列表。(对返回列表的更改会“直接写”到数组。)

    注意:a为数组,同时该方法还可以创建固定长度的列表的便捷方法,该列表被初始化为包含多个元素:

    List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");
    

    集合转换成数组:
    Collection.toArray();
    Collection.toArray(T[] a);
    注意:
    两者的功能相同,区别如下

    • 第一个方法不能使用强制转换转换Object[]数组。
    • 两个方法都返回迭代器顺序相同的数组(如果对迭代顺序做了保证)
      • Object[] toArray()
        返回一个原Collection的副本,可以在该副本上进行任何操作而不会影响原来的集合。
      • T[] toArray(T[] t)
        返回一个与t运行时类型相同的数组,如果t的大小小于集合的大小,那么新建一个运行时类型相同的新数组把元素复制过去,如果t的长度大于集合的长度,那么多余的空间将被初始化为null。

    集合转为数组的示例:

    Collection<String> c = new ArrayList<String>();
    c.add("Java");
    c.add("and");
    c.add("Android");
    
    // 把Collecion转换为数组
    String[] strs = c.toArray(new String[3]);
    
    // 遍历数组
    for (String str : strs)
        System.out.println(str);
    

(2)集合的使用

使用集合特别注意面向接口编程,集合接口和实现分离的主要目的就是为了更容易的改变实现从而适用新情况,类似于策略模式。
例如这样创建一个集合:

Collection collection=new ArrayList();

而不是

ArrayList arrayList=new ArrayList();

或者方法的参数类型定义为Collecion而不是ArrayList将具有更好的可扩展性和灵活性。

2.列表

列表是某种特定类型的值的有序集合。列表中增加了用索引来操作元素的一些方法

(1)新方法

public void add(int index,E element)//在列表的指定位置插入指定元素
public E get(int index)//获取列表指定索引位置的元素
public E set(int index,E element)//用指定元素替换列表中指定位置的元素

示例:

    //新建一个列表
    List<String> list = new ArrayList<>();
    // 使用带索引的add方法向列表指定索引位置添加元素
    list.add(0, "Java");

    //使用get方法获取指定索引位置的元素 
    String element = list.get(0);
    System.out.println(element);

    // 使用"android"替换0索引位置的元素
    list.set(0, "android");

(2)ListIterator

列表迭代器(ListIterator)相比Collection的迭代器,增加了向前迭代的功能,即增加了hasPrevious方法和previous方法。hasPrevious方法判断是否还有可迭代元素,previous则返回迭代器刚刚略过的元素。

示例:

// 创建一个包含三个元素的列表
List<String> list = new ArrayList<String>() {
    {
        add("java");
        add("and");
        add("android");
    }
};
// 获取列表迭代器ListIterator,新建的列表迭代器起始位置在列表的末尾处
ListIterator<String> listIterator = list.listIterator(2);
// 使用listIterator进行迭代
while (listIterator.hasPrevious()) {
    // 返回迭代的元素
    String element = listIterator.previous();
    // 打印到控制台
    System.out.println(element);
}   

由于可以使用get方法获取指定索引位置的元素,所以可以使用for循环语句对列表进行迭代:

for(int i=0;i<list.size();i++){
    E element=list.get(i);
    //对元素进行操作
}

List接口包含了不同的实现类ArrayList和LinkedList以及古老的Vector类。
有时候ArrayList比LinkedList更适合,而有时候则相反。两种列表的选择要根据具体情况来判断,同时不同的列表将会对应用程序的性能或内存产生严重的影响。

2.1 ArrayList

ArrayList是一个底层数据结构为数组的列表。在创建之初可以指定初始数组的大小。当存储的元素超过数组的长度时,ArrayList会自动创建一个长度更大的数组,然后将原来的内容全部复制过去。
这个过程是占用较大的时间的,所以在使用ArrayList之初,可以指定一个更大的初始大小。

在ArrayList中间或头部插入值时,底层数组的整个或部分的值将要全部向后移动以腾出空间给新元素,这个重新分配空间的过程特别是在元素数量很大的时候将耗费更多的资源。

ArrayList的内存分配是单向的,比如从ArrayList中删除一个元素,那么ArrayList占用的内存并不会减小,因为底层数组的长度并没有改变。

2.2 LinkedList

LinkedList底层为链表,在其实例内部使用对象保存元素。例如简化版本的LinkedList示例:

class SimpleLinkedList<E> {
    private static class Element<E> {
        E value;
        Element next;
    }

    private Element<E> head;
}

LinkedList实例包含了一个指向列表头元素的引用,类型为Element。内部类Element是一个递归的数据结构,next字段指向下一个元素。

在LinkedList中根据索引访问元素的话,需要遍历这个链表,同时进行计数,直到索引等于计数值。相对于ArrayList来说,它的随机访问是慢的。

链表在头部或中间增删元素非常简单,只需要把相应元素的引用重新赋值给下一个元素即可。相对于ArrayList的重分配操作来说,它的速度是快的。

2.3 列表的选择

根据实际需求来考虑选择:
如果需要随机访问,特别是列表规模较大时,那么使用ArrayList。
如果需要对列表进行大量插入删除操作,特别是在列表头部或中间操作,那么使用LinkedList。并且LinkedList的内存使用量会随着列表的收缩而减小。

3.队列

Queue是(FIFO)先进先出的Java数据结构接口,它包含的add方法用于向队列添加元素,remove方法删除最老的元素。其中还添加了用于向有界队列中操作元素的方法,这些方法在失败后只是返回false而不是抛出异常,下面就介绍新增的方法:

boolean offer(E e)//将指定的元素插入此队列(如果立即可行且不会违反容量限制),当使用有容量限制的队列时,此方法通常要优于 add(E),后者可能无法插入元素,而只是抛出一个异常。
E poll()//获取并移除此队列的头,如果此队列为空,则返回 null。
E peek()获取但不移除此队列的头;如果此队列为空,则返回 null。

示例:

    // Queue的一个实现ArrayBlockingQueue是一个由数组支撑的有界队列,使用它的实例来演示上面的三个方法
    // 创建一个容量为2的队列
    ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(2) {
        {
            add("java");
            add("android");
        }
    };
    // 使用offer插入元素,此时队列是满的,所以插入元素失败,返回false
    boolean flag1 = queue.offer("heima");
    String element;

    // 使用peek获取但不移除次队列的头,element=java
    element = queue.peek();

    // 使用poll获取并移除此队列的头,element=java
    element = queue.poll();

Deque是双端队列的接口,此类实现允许在队列的两端进行元素的插入和删除。

3.1 LinkedList

LinkedList实现了队列和双端队列的接口,所以它拥有队列和双端队列的操作方法。

示例:

    // 实例化一个linkedList队列
    Queue<String> queue = new LinkedList<String>();
    // 使用offer方法添加元素
    queue.offer("java");
    queue.offer("android");

    String element;
    // 使用peek方法获取头部元素
    element = queue.peek();
    // 使用poll方法获取并移除头部元素
    element = queue.poll();

3.2 PriorityQueue

优先级队列,此类按照元素的自然顺序进行排序或给定的比较器进行排序。
底层基于二叉堆。一种自平衡的树,值最小的元素在树的根节点。

下面将示例这两种排序方式:

方法一:使用比较器排序的优先级队列

    // 使用匿名内部类创建比较器
    Comparator<Person> comparator = new Comparator<Person>() {

        @Override
        public int compare(Person o1, Person o2) {
            // 比较姓名字符串
            int i1 = o1.name.compareTo(o2.name);
            // 如果姓名相同比较年龄
            int i2 = i1 == 0 ? o1.age - o2.age : i1;
            return i2;
        }
    };

    // 创建一个优先级队列,并使用上面的比较器初始化
    PriorityQueue<Person> queue = new PriorityQueue<Person>(comparator) {
        {
            Person p1 = new Person("Bob", 21);
            Person p2 = new Person("Alex", 18);
            Person p3 = new Person("Raik", 25);

            add(p1);
            add(p2);
            add(p3);
        }
    };
    // 使用lambda experssion遍历优先级队列
    queue.forEach((person) -> System.out.println(person.name + "=" + person.age));

    遍历结果:
        Alex=18
        Bob=21
        Raik=25


    //用于队列的Person类
    class Person {
        String name;
        int age;
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    }

方法二:使用自然排序

    //用于队列的Person类,实现了Comparable接口,提供了用于比较的comparaTo方法
    class Person implements Comparable<Person> {
        String name;
        int age;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
        //重写的compareTo方法
        @Override
        public int compareTo(Person p) {
            int i1 = this.name.compareTo(p.name);
            int i2 = i1 == 0 ? this.age - p.age : i1;
            return i2;
        }
    }


    // 创建一个优先级队列,并使用上面的比较器初始化
    PriorityQueue<Person> queue = new PriorityQueue<Person>() {
        {
            Person p1 = new Person("Bob", 21);
            Person p2 = new Person("Alex", 18);
            Person p3 = new Person("Raik", 25);

            add(p1);
            add(p2);
            add(p3);
        }
    };
    // 使用lambda experssion遍历优先级队列
    queue.forEach((person) -> System.out.println(person.name + "=" + person.age));
    遍历结果:
        Alex=18
        Bob=21
        Raik=25

4.映射

Map是一种键-值存储数据结构。通过查询键,即可获取键关联的值。Map虽然在Collecion API中定义的,但是他没有实现Collecion接口。Map中定义了常见的操作,比如查询、读取、插入、删除键-值有关的方法。

  • 插入元素
    V put(K key,V value)//将指定的值与此映射中的指定键关联,如果当前映射已存在该键的关联值,那么原来的值将被覆盖。
    void putAll(Map< ? extends K,? extends V > m)//从指定映射中将所有映射关系复制到此映射中,这相当于对m映射中的每一个键-值都执行了put方法。

    示例:

    // 创建一个映射,键为Integer类型值,值为String类型值
    Map< Integer, String > map = new HashMap<>();// 使用菱形推断自动推断出键-值的类型
    // 使用put方法放置元素
    map.put(1, "one");
    map.put(2, "two");
    
  • 读取元素
    V get(Object key)//返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。
    boolean containsKey(Object key)//如果此映射包含指定键的映射关系,则返回 true。更确切地讲,当且仅当此映射包含针对满足 (key==null ? k==null : key.equals(k)) 的键 k 的映射关系时,返回 true。(最多只能有一个这样的映射关系)。
    boolean containsValue(Object value)//如果此映射将一个或多个键映射到指定值,则返回 true。
    boolean isEmpty()如果此映射未包含键-值映射关系,则返回 true。
    int size()返回此映射中的键-值映射关系数。

    示例:

    // 创建一个映射,键为Integer类型值,值为String类型值
    Map < Integer, String > map = new HashMap<>();// 使用菱形推断自动推断出键-值的类型
    // 使用put方法放置元素
    map.put(1, "one");
    map.put(2, "two");
    
    // 使用get方法获取映射中键对应的值
    String value1 = map.get(1);//one
    // 使用containsKey方法查询映射中是否包含指定键对应的键值关系
    boolean flag1 = map.containsKey(2);//true
    // 使用containsValue方法查询映射中的键-值是否含有指定的值
    boolean flag2 = map.containsValue("two");//true
    // 使用isEmpty方法查询此映射中是否包含键-值关系
    boolean flag3 = map.isEmpty();//false
    // 使用size方法查询此映射中包含的键-值关系数
    int size = map.size();//2
    
  • 遍历的映射表
    Set keySet()//返回此映射中包含的键的 Set 视图。
    Collection values()//返回此映射中包含的值的 Collection 视图。
    Set< Map.Entry< K,V >>//entrySet()返回此映射中包含的映射关系的 Set 视图。

    下面使用这些方法对映射进行遍历:

    // 创建一个映射,键为Integer类型值,值为String类型值
    Map <Integer, String> map = new HashMap<>();
    // 使用put方法放置元素
    map.put(1, "one");
    map.put(2, "two");
    map.put(2, "three");
    
    // 方法一:根据键的Set进行遍历
    // 获取当前映射的所有键的Set
    Set<Integer> keySet = map.keySet();
    for (Integer key : keySet) {
        // 根据键获取值
        String value = map.get(key);
        System.out.println("" + key + "=" + value);
    }
    
    // 方法二:根据Entry条目遍历
    // 获取当前映射的Entry条目的Set
    Set< Entry< Integer, String > > entry = map.entrySet();
    for (Entry< Integer, String > element : entry) {
        // 根据Entry条目获取键
        Integer key = element.getKey();
        // 根据Entry条目获取值
        String value = element.getValue();
        System.out.println("" + key + "=" + value);
    }
    

4.1 HashMap

HashMap是映射的散列实现。这个类包含了一个内部类Entry,用来表示键-值对,元素保存在Entry对象的数组中。这个数组称为表。

向表中存储键值关系时,首先要计算键实例的哈希值(HashCode方法返回值)。

键值对在数组中存储的位置由键的哈希值决定。由于该值为int类型,所以把该值对表长度取余将获得一个在0和表长度减1之间的值,把该值作为数组下标,那么需要存储的键-值关系将存储在这里。

由于两个不同的实例哈希值可能相等,为了解决这种冲突,在数组的每个位置使用了列表来存储键值关系,这样哈希值冲突的键-值关系将存储在同一列表中。同样,在查询的时候,遍历该列表将获取到查询结果。

HashMap的表长度称为桶数(容量),当前存储的键值关系数称为尺寸,尺寸和桶数的比值的百分比就叫负载因子,0负载因子表示当前Map为空,负载因子为0.5时表半满。在创建HashMap的时候可以自行设定负载因子的大小。当映射的负载因子达到设定值时,底层的表容量将翻倍(重建内部结构)。

表的容量调整后,表中所有的键值关系都要重新分配。如果当前表的规模很大时,那么这个重新分配的过程将很费时。

HashMap是映射中最常用的实现,因为散列带来的速度远快于其他实现。注意此实现允许将null用作键或值。

4.2 TreeMap

TreeMap使用二叉树数据结构来实现Map接口。树中的每一个节点都是一个键值关系。同优先级队列一样,TreeMap可以使用自然顺序或比较器进行排序,这取决于具体使用的构造方法。

TreeMap和HashMap的主要区别是TreeMap中元素是按顺序存储,在遍历整个映射时TreeMap的键的顺序不会变的。而HashMap则没有这个性质,因为键的存储位置由键实例的哈希值决定。

4.3 LinkedHashMap

与HashMap不同的是LinkedHashMap使用双向链表维护键的插入顺序,迭代时候,取得键值对的顺序是插入顺序。除此之外,与HashMap的实现完全相同。

注意:重新插入键不会影响链表。

4.5 ConcurrentHashMap

这个一个HashMap的同步实现,可以在多线程中共享映射实现。注意它不允许将null用作键或值。

4.6 Map的选择

在Map中添加元素的开销随着Map的尺寸增大而急剧增大,但是查询开销没有很大,因为在Map中查询的次数比添加操作多得多。

HashMap和Hashtable性能一致,因为两者的底层查找和存储机制相同,后者被HashMap替代。如果支持,在TreeSet中获取一个键的Set,toArray()转换成数组,用Arrays.binarySearch()在排序数组中快速查询键。

HashMap总体好于TreeMap,它为快速查询进行优化。但是TreeMap的迭代速度比HashMap好一点。如果没有特定排序需求默认使用HashMap。HashMap的插入操作快LinkedHashMap,因为后者要维护一个链表存储元素插入顺序。

5.集

集(Set)不允许插入相同的元素的集合接口。其中的一些实现不能保证保存顺序如HashSet,另一些实现按照某种顺序排序保存如TreeSet。
之前讨论的Map实现,都有一个对应的Set实现,比如HashMap—HashSet、TreeMap—TreeSet、LinkedHashMap—LinkedHashSet。下面的示例中,HashSet底层使用了HashMap,HashSet的值以键的形式保存在HashMap中。

Set中的API与Collecion相同。

例如:

    // 创建一个HashSet
    Set<String> set = new HashSet<String>();

    // 向集中添加元素,相同元素将不能添加
    set.add("java");
    set.add("android");
    set.add("java");// 重复元素

    // 遍历集
    set.forEach(element -> System.out.println(element));

6.遗留的集合实现

(1)Hashtable
与HashMap一样,区别在于它是同步的

(2)Enumeration枚举
包含和迭代器类似的hasMoreElements和nextElement方法

与遗留接口操作时,Collections的静态方法可以返回一个枚举对象。

//static <T> Enumeration<T>  enumeration(Collection<T> c) 
例如:操作合并流时
SequenceInputStream in=new SequenceInputStream(Collections.enumeration(c));

(3)Properties
通常用于程序的配置选项,扩展自Hashtabl
特点:

  • 键和值都是字符串
  • 可以保存到文件中
  • 也可以从文件中加载

(4)栈
从1.0版开始就存在,栈是后进先出(LIFO)的数据结构。扩展自另一个古老的类Vector

包含push和pop方法,但是还有不属于栈的insert和remove方法,即可以在任何地方插入删除操作而不仅仅在栈顶。

示例:

    // 创建一个堆栈
    Stack<String> stack = new Stack<>();

    // 把项压入堆栈顶部
    stack.push("java");
    stack.push("android");

    // 弹出栈顶元素,并把它作为返回值
    String element = stack.pop();
    System.out.println(element);//android

7.总结

(1)数组把对象和数字联系起来。它保存类型明确的对象,查询对象时候,不需要对结果进行强制类型转换。数组的长度一旦创建就不可变。数组可以是多维的。

(2)Collection保存单一的元素而Map存储相关联的键值对。有了泛型,就构建了一个类型安全的容器。在存入和查询都不必进行类型校验和检查。Collection和Map会自动调整长度。容器不能持有基本类型数据。但是有自动包装机制,可以实现基本类型数据存储和获取之间双向切换。

(3)像数组一样,List也把对象和数字关联起来。因此,数组和List都是排好序的容器。List可以自动扩容。

(4)如果要进行大量随机访问,用ArrayList。如果在中间增删多,用LinkedList。

(5)Queue提供了队列及栈的行为。各种实现由LindedList支持。

(6)Map是一种将对象与对象关联起来存储的容器。HashMap快速访问;TreeMap提供针对键的排序存储;LinkedHashMap可以实现快速查询和保存了元素插入的顺序。

(7)Set不接受重复元素。HashSet可以最快查询,TreeSet将元素排序。LinkedHashSet以插入顺序保存元素。

(8)新程序不要使用过时的Vector、Hashtable、Stack。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值