首页 > 代码库 > 牛刀小试 - 浅析Java集合框架的使用
牛刀小试 - 浅析Java集合框架的使用
基本概述
Java中的集合框架与数组类似,都是用于存储多个同一类型数据的容器。
但是对于数组的使用,会因为数组本身的特性会导致一些使用限制,例如:
数组要求在构造时,就必须确定数组的长度。所以如果想要存放的数据个数不确定,数组就无法使用。
于是促使了集合框架的诞生,与数组相比,集合框架最大特点在于:
- 集合框架下的容器类只能存放对象类型数据;而数组支持对基本类型数据的存放。
- 任何集合框架下的容器类,其长度都是可变的。所以不必担心其长度的指定问题。
- 集合框架下的不同容器了底层采用了不同的数据结构实现,而因不同的数据结构也有自身各自的特性与优劣。
常用容器类关系图
网上找的一张图,基本描述了Java集合框架中最常用部分的构成关系:
集合框架的分类
1、Collection体系:以单值的形式存储对象数据的容器体系。
1.1、List容器:允许重复元素的存储。(最常用的为ArrayList以及LinkedList;另外在JDK1.2集合框架出现之前,该类型使用的容器为Vector)
- ArrayList :线程不同步、底层的数据结构是数组结构、元素存储顺序有序、访问元素速度相对较快,存取或删除元素等操作速度较慢
- LinkedList :线程不同步、底层的数据结构是链表结构、元素存储顺序有序、访问元素速度相对较慢,存取或删除元素等操作速度快
- Vector :线程同步、 底层的数据结构是数组结构、元素存储顺序有序、访问,存取,删除元素的操作都很慢。
1.2、Set容器:不允许重复元素的存储,该容器中存储的对象都是唯一的。(最常用的为HashSet与TreeSet)
- HashSet :线程不同步、底层的数据结构为哈希表结构、无序
- TreeSet :线程不同步、底层的数据结构为二叉树结构、会按自然排序方式或指定排序方式对元素进行排序
2、Map体系:以“键值对”的形式存储数据的容器体系。即是将键映射到值的对象;一个映射不能包含重复的键;每个键最多只能映射到一个值。
- HashMap :线程不同步、底层数据结构是哈希表结构、无序、允许NUll作为键或值
- Hashtable :线程同步 、底层数据结构是哈希表结构、无序、不允许NUll作为键或值
- TreeMap :线程不同步、底层数据结构是二叉树结构、可以按照自然或指定排序方式对映射中的键进行排序
面试中常常会问到HashMap和HashTable的不同,由此就可以做出回答:
1、HashTable与Vector一样,在JDK1.2之前就存在,是基于陈旧的Dictionary类实现的。
HashMap是JDK1.2之后,经集合框架出现之后,基于Map接口实现的。
2、HashTable是线程同步的,而HashMap默认并没有实现线程同步。
也就是说,如果存在多线程并发操作同一个HashMap容器对象时,需要自己加上同步。
3、HashTable中存放的数据允许将null作为键或值,而HashMap则不支持。
List接口的常用操作
List接口是Collection的子接口,这就意味着:除开Collection接口提供的方法列表之外,它又根据自身特性新增了一些额外的方法。
所谓数据存放的容器,所涉及的操作自然离不开“增删改查”。那就不妨来看一下关于List体系下,常用的操作方法。
List接口的常用方法列表
1.添加操作
- void add(index,element); //在列表的指定位置插入元素
- void add(index,collection); //将指定collection中的所有元素都插入到列表中的指定位置
2.删除操作
- Object remove(index): //移除列表中指定位置的元素
3.修改操作
- Object set(index,element); //修改(替换)列表指定位置的元素的值
4.获取操作
- Object get(index); //获取列表指定位置的元素的值
- int indexOf(object); //获取对象在列表中的位置,如果不包含该对象,则返回-1
- int lastIndexOf(object); //获取对象在列表中最后一次出现的位置,如果不包含该对象,则返回-1
- List subList(from,to); //从指定起始位置from(包括)到指定结束位置to(不包括)截取列表
可以看到与其父接口Collection提供的方法列表相比,List接口新增的方法有一个显著的特性:可以操作角标。
ArrayList的基本应用
package com.tsr.j2seoverstudy.collection; import java.util.ArrayList; import java.util.List; public class ArrayListTest { public static void main(String[] args) { //ArrayList容器对象的创建 ArrayList<Integer> arrayList = new ArrayList<Integer>(); List<Integer> listTemp = new ArrayList<Integer>(); //添加数据的操作 //添加单个元素 arrayList.add(1); arrayList.add(2); listTemp.add(3); listTemp.add(4); //一次性添加另一个容器内的所有元素 arrayList.addAll(listTemp); //在指定位置添加元素 arrayList.add(0, 5); //打印一次数据 printList(arrayList); System.out.println(); //删除数据 System.out.println("删除的元素为:"+arrayList.remove(2)); System.out.println("删除元素值为5的元素是否成功:"+arrayList.remove(new Integer(5))); printList(arrayList); System.out.println(); //修改数据 System.out.println("修改下标为1的元素值为6,修改前的值是:"+arrayList.set(1, 6)); printList(arrayList); System.out.println(); //获取元素总个数 System.out.println("size:"+arrayList.size()); //元素截取 listTemp = arrayList.subList(0, 2); System.out.print("listTemp:"); printList(listTemp); } static void printList(List<Integer> list){ for (Integer integer : list) { System.out.print(integer + "\t"); } } } /* 输出结果为: 5 1 2 3 4 删除的元素为:2 删除元素值为5的元素是否成功:true 1 3 4 修改下标为1的元素值为6,修改前的值是:3 1 6 4 size:3 listTemp:1 6 */
LinkedList的基本使用
与ArrayList不同,LinkedList内部采用链表结构所实现,它每一个节点(Node)都包含两方面的内容:
节点本身的数据(data)以及下一个节点的信息(nextNode)。
由于ArrayList内部采用数组结构,所以一旦你在某个位置添加或删除一个数据,之后的所有元素都不得不跟着进行一次位移。
而对于LinkedList则只需要更改nextNode的相关信息就可以实现了,这是LinkedList的优势。
而也正是基于链表结构的特性,LinkedList还新增了一些自身特有的方法。
其中最常用的是:addFirst(E e),addLast(E e),getFirst(),getLast(),removeFirst(),removeLast()等方法。
对于LinkedList中元素的基本操作方法,与ArrayList并无多大区别。
所以这里主要看一下对于LinkedList的链表特性和新增方法的使用方式。
根据这些特性,我们可以通过封装LinkedList来实现我们自己的容器类。
并且,可以完成队列,或是栈的不同存储方式的实现。
而在面试中,也可能遇到类似问题:请分别自定义实现一个采用队列和栈存储方式的容器类。
那么,队列意味着数据先进先出,而栈则意味着数据先进后出的特性,正好可以通过LinkedList来实现:
package com.tsr.j2seoverstudy.collection; import java.util.LinkedList; /* * 输出结果为: * 队列: s1 s2 s3 s4 * 栈: s4 s3 s2 s1 */ public class QueueAndStack { public static void main(String[] args) { MyQueue q = new MyQueue(); q.add("s1"); q.add("s2"); q.add("s3"); q.add("s4"); System.out.print("队列:"+"\t"); while (!q.isEmpty()) { System.out.print(q.get()+"\t"); } System.out.println(); System.out.print("栈:"+"\t"); MyStack s = new MyStack(); s.push("s1"); s.push("s2"); s.push("s3"); s.push("s4"); while (!s.isEmpty()) { System.out.print(s.pop()+"\t"); } } } //我的队列 class MyQueue { private LinkedList linklist; MyQueue() { this.linklist = new LinkedList(); } public void add(Object obj) { linklist.addFirst(obj); } public Object get() { return linklist.removeLast(); } public boolean isEmpty() { return linklist.isEmpty(); } } //我的栈 class MyStack { private LinkedList linklist; MyStack() { this.linklist = new LinkedList(); } public void push(Object obj) { linklist.addFirst(obj); } public Object pop() { return linklist.removeFirst(); } public boolean isEmpty() { return linklist.isEmpty(); } }简而言之,如果不涉及到底层实现的“数据结构”的具体研究,而只是针对于容器自身的使用。
那么了解了不同容器之间的特点就应放在首位。因为了解了不同容器之间的差别,基本上也就对集合框架有一个不错的掌握了。
正如ArrayList和LinkedList,它们对于操作数据提供的方法使用都是没有太大区别的。因为它们都是基于List接口而实现的。
而针对于它们的使用,则是根据实际需求,来选择更适合的容器类。例如说:可以考虑你的数据是否需要频繁的增删操作?
如果需要,则选择LinkedList。否则就可以选择ArrayList,因为其访问元素的速度更快。
Set接口的常用操作
与同样隶属于Colleciton体系下的List接口不同的是,实现Set接口的容器类有着保证存放元素唯一性的特性。
Set接口的实现子类当中,最为常用的容器类分别是HashSet和TreeSet。
HashSet的基本使用
顾名思义,HashSet内部采用的是哈希表结构。而该容器也正是基于此特性来保证元素唯一的。
HashSet容器会通过存放的元素的类型中的hashCode方法和equals方法判断元素是否相同。
首先经hashCode方法判断,如果通过hashCode()方法返回的哈希值不同,则证明不是重复元素,直接存储到哈希表中。
而如果判断得到的元素的hashCode值相同,则接着调用对象的equal方法判断元素是否重复。
来看一个例子:
package com.tsr.j2seoverstudy.collection; import java.util.HashSet; import java.util.Set; /* * 输出结果为: * name:张三..age:19 * name:张三..age:19 * name:李四..age:20 */ public class HashSetDemo { public static void main(String[] args) { HashSet<Student> set = new HashSet<Student>(); set.add(new Student("张三", 19)); set.add(new Student("李四", 20)); set.add(new Student("张三", 19)); printSet(set); } static void printSet(Set<Student> set) { for (Student student : set) { System.out.println(student); } } } class Student { private String name; private int age; Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return new StringBuilder().append("name:").append(name) .append("..age:").append(age).toString(); } }从程序的输出结果中注意到Set中存放了两个姓名同为“张三”,年龄同为19岁的学生。
这可能与我们最初的设想偏离甚远,不是说Set容器类保证元素唯一性吗?为何出现重复。
这正是因为自定义的Student中并没有对hashCode于equals方法进行覆写。
所以HashSet容器在通过equals方法判断元素是否重复的时候,采用的原本Object当中“obj1==obj2”的方式。
==用于比较对象的时候,是比较内存中的地址,所以自然得到的结果是两个对象不是相同的。
所以说,在使用HashSet容器存储自定义对象的时候。请一定记得按照自己的需求,重新覆写hashCode与equals方法。
修改后的Student类如下:
class Student { private String name; private int age; Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return new StringBuilder().append("name:").append(name) .append("..age:").append(age).toString(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + age; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Student other = (Student) obj; if (age != other.age) return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } }
TreeSet的基本使用
与HashSet一样,从命名我们就可以猜到:TreeSet内部的结构采用的是二叉树结构。
所谓的二叉树数据结构是指:每个结点最多有两个子树的有序树,子树有左右之分,次序不能颠倒。
TreeSet容器通过底层的二叉树保证数据唯一性的原理基本可以分析为:
当向TreeSet容器内添加进第一个元素,二叉树就在最顶端有了一个结点。
当插入第二个元素时,TreeSet就会用这第二个元素与该结点的元素进行比较。
假定该次插入的元素比上一个元素的结点小,就会被放在顶端节点的左子树上。而如果较大,则放在右子树上。
以此类推,当所有的元素完成存储,所有左边子树上的元素值都较小,右边则较大。
而比较结果为相同的元素则无法进入到该数据结构中。从而保证了数据的唯一性。
那么,同样以上面的需求为例,我们这次利用TreeSet写一个测试类:
package com.tsr.j2seoverstudy.collection; import java.util.Set; import java.util.TreeSet; public class TreeSetDemo { public static void main(String[] args) { TreeSet<Student> set = new TreeSet<Student>(); set.add(new Student("张三", 19)); set.add(new Student("李四", 20)); set.add(new Student("张三", 19)); printSet(set); } static void printSet(Set<Student> set) { for (Student student : set) { System.out.println(student); } } }当我们再次运行程序,得到的信息是这样的:
没错,通过该异常打印信息我们能够读到的是:自定义的Student类不能转换为Comparable。
也就是说我们定义的该类不具备比较性。我们已经说过了,二叉树结果会针对元素进行比较。
根据结果决定其是否进入该结构,或者应当排在左边子树还是右边子树。那么,自然就应该有一个比较的方式。
而TreeSet容器对元素进行比较的方式有两种:
- 让存储的元素所属类实现Comparable接口,并覆写compareTo()方法。
- 新建比较器类,实现Comparatorj接口,并覆写compare()方法。将比较器对象作为TreeSet的构造器的参数传递给TreeSet对象
假设我们选用第一种方式对Student类进行修改:
package com.tsr.j2seoverstudy.collection; import java.util.TreeSet; import java.util.Set; /* * 输出结果为: * name:张三..age:19 * name:张三..age:19 * name:李四..age:20 */ public class HashSetDemo { public static void main(String[] args) { TreeSet<Student> set = new TreeSet<Student>(); set.add(new Student("张三", 19)); set.add(new Student("李四", 20)); set.add(new Student("张三", 19)); printSet(set); } static void printSet(Set<Student> set) { for (Student student : set) { System.out.println(student); } } } class Student implements Comparable<Student>{ private String name; private int age; Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return new StringBuilder().append("name:").append(name) .append("..age:").append(age).toString(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + age; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Student other = (Student) obj; if (age != other.age) return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } @Override public int compareTo(Student s) { int temp = this.age - s.age; return temp == 0 ? this.name.compareTo(s.name) : temp; } }我们实现的compareTo方法的含义在于:先通过年龄进行比较,如果两个对象的年龄相同,则再通过姓名进行比较。
注:compareTo方法返回的是一个int型的值,该值代表的含义是:负数代表小于,0代表对象相等,正数代表大于。
而此时运行main方法得到的输出结果为:
name:张三..age:19
name:李四..age:20
可以发现相对于HashSet来说,TreeSet除了保持了元素的唯一性之外。
还可以按照我们定义的比较方法,来保证元素的顺序。
例如上面例子中实现的compareTo方法正是:让元素按照年龄从小到大的顺序排序;
如果年龄相同,则按照姓名字母从小到大的顺序排序。
Map接口的常用操作
我们已经说过,Map体系与Collection体系最大的不同之处就在于:不再采用单值,而是采用键值对的方式存储元素。
也就是说,除开存储对象本身之外,还给每个对象关联了一张“身份证”,这张“身份证”就是所谓的“键”。
那么,对应的其对于元素的操作方法,自然也就有了不同。Map接口常用的方法为:
添加:
- V put(K key,V value); //将指定的值与此映射中的指定键关联,如果此映射以前包含一个该键的映射关系,则用该指定值覆盖旧值。返回前一个和key关联的值,如果没有返则回null.
- void putAll(Map<? extends K,? extend V> m)//从指定映射中将所有映射关系复制到此映射中.
删除:
- V remove(Object key); //如果存在一个键的映射关系,则将其从此映射中移除
- void clear(); //从此映射中移除所有映射关系
判断:
- boolean containsKey(Object key); //判断是否存在对应的键的映射关系。如果包含,则返回 true
- boolean containsValue(Object value); //判断是否该map内是否有一个或多个键映射到该指定值,如果有,则返回true.
- boolean isEmpty(); //如果此映射未包含任何键-值映射关系,则返回 true
获取:
- V get(Obejct key);//获取该指定键所映射的值,如果此映射不包含该键的映射关系,则返回
null
- Set<K> keySet(); //获取该map内包含的所有键的set视图
- Set<Map.Entry<K,V>> entrySet(); //获取该map内包含的所有映射关系的set视图
- Collection<V> values(); //获取该map内包含的所有值的collection视图。
- int size(); //获取该map内所包含的映射关系的总数,如果该映射包含的元素大于 Integer.MAX_VALUE,则返回 Integer.MAX_VALUE
可以通过一个例子,来简单的看一下对于Map容器的常用操作:
public class MapDemo { public static void main(String[] args) { Map<String, Integer> map = new HashMap<String, Integer>(); //存储 map.put("张三", 19); map.put("李四", 18); System.out.println(map); //获取 System.out.println("size:"+map.size()); Set<String> nameSet = map.keySet(); for (Iterator<String> it = nameSet.iterator(); it.hasNext(); ) { System<span style="font-size:12px;">.</span>out.println(map.get(it.next())); } Set<Map.Entry<String, Integer>> set = map.entrySet(); for (Iterator<Map.Entry<String, Integer>> it = set.iterator(); it.hasNext(); ) { Entry<String, Integer> entry = it.next(); String name = entry.getKey(); Integer age = entry.getValue(); System.out.println("name:"+name+"..age:"+age); } Collection<Integer> values = map.values(); for (Iterator<Integer> it = values.iterator(); it.hasNext(); ) { System.out.println(it.next()); } //删除 System.out.println("..."+map.remove("张三")); } }
关于Map容器的使用,可能值得注意的就是:
1.取出数据时,是根据键获取该键映射的值。
2.Map容器本身无法进行迭代,但可以:
通过keySet()方法获取该容器内所有键的Set视图进行迭代;
通过entrySet()方法获取该容器内所有映射关系的Set视图进行迭代;
通过values()方法获取该容器内所有值的Collection视图进行迭代;
3.通过命名,就可以猜想到。与HashSet与TreeSet的使用相同。
在通过该HashMap与TreeMap的时候,请不要忘记覆写hashcode、equals方法和添加实现比较器接口.
总结
所谓的集合框架,究其根本其实都是对于对象数据的一个存储容器。
之所分离出如此之多的不同容器,原因也是因为根据实际需求提供不同特性的容器。
而之所各个容器类之间具备不同的特性,也正是因为它们底层采用了不同的数据结构实现。
正如前面谈到过的一样,如果你暂时不想或者没有经理去更深一步的了解不同数据结构的具体实现。
那么针对于集合框架中不同容器类的使用,应该掌握的重点就是,它们各自不同的特性。
因为只有掌握了其各自的优劣点,就可以根据实际的需求选择最合适的容器来使用。
1、思考你是只需要保存单个的值,还是希望以键值对(字典)的形式保存数据。
如果存储单值,则选用Collection;如果存储字典,则选用Map。
2、选用Collection体系时,思考你存储的数据是否需要保证唯一性?
如果需要,则选用Set。如果不需要,则选用List。
2.1、如果选用的是List,则考虑元素是否涉及到频繁的增删操作。
如果是,则选用LinkedList;如果不是,则选用ArrayList。
2.2、如果选用的是Set,则考虑是否需要指定存储顺序。
如果是,则选用TreeSet;如果不是,则选用HashSet。
另外一种情况,如果不需要自定义顺序,而希望让元素按存储的先后顺序排序,则可以直接选用LinkedHashSet。
3、当选用Map体系时,与Set的选择一样。
如果需要按指定存储顺序,则选用TreeMap。如果不需要则选用HashMap。
如果想让元素按照存储的先后顺序进行排列,则选用LinkedHashMap。
牛刀小试 - 浅析Java集合框架的使用