Java 基础之类集

来源:互联网 发布:网络购物市场交易规模 编辑:程序博客网 时间:2024/06/07 23:11

平时我们用 List 、 Map 用得挺 Happy 的,只知道可以用它来保存数据,最近面试的时候问到这个 List 和 那个 List 的区别是什么呀, Vector 和 ArrayList 的区别是什么呀,我连 Vector 是什么都不知道,我咋知道它俩有什么区别。。。唉,说来说去还是基础的锅,这不,连视频带看书,重新了解了一下 Java 的类集。


1、基本概念

Java 类集就是一组 Java 实现的数据结构,它是一个特别有用的工具类,它就像一个容器,把多个对象(其实是对象的引用)存到这个容器中。

一般来说,如果想要保存多个对象,可以用到对象数组,但是如果使用对象数组的话,长度是固定的,所以一般不用数组。也可以用链表来实现一个动态的对象数组,但是链表的开发很麻烦,链表的操作性能又比较差,而且链表还会使用 Object 类来保存对象,会涉及到向上转型和向下强制转型。于是乎,类集的优势就体现出来了。

Java 设计之初就提供了类似链表的工具类—— Vector ,但是后来发现这个类并不能很好的描述数据结构,所以从 Java 2 开始,提供了类集框架来实现数据结构,并在 Java 5 引入了泛型的概念,于是可以在类集框架中避免向下强制转型。在 Java 8 后还提供了数据流的操作。


2、Collection 接口

Collection 接口是整个类集之中单值保存的最大父接口。单值保存即每一次向集合中保存一个对象,Collection 接口下的常用方法如下:
public boolean add(E e):将指定元素添加到集合中,添加成功则返回 true。
public boolean addAll(Collection<? extends E> c):将集合 c 中的所有元素添加到集合中,添加成功则返回 true。
public void clear():清空集合中的所有元素。
public boolean contains(Object o):判断集合中是否包含指定元素,如果包含则返回 true,否则返回 false。
public boolean containsAll(Collection<?> c):判断集合中是否包含集合 c 中的所有元素,如果包含则返回 true,否则返回 false。
public boolean isEmpty():判断集合是否为空,如果集合长度为 0 则返回true,否则返回 false。
public boolean remove(Object o):从集合中删除指定元素,如果集合中包含多个该元素时,只会删除第一个,删除成功则返回 true。
public boolean removeAll(Collection<?> c):从集合中删除集合 c 中的所有元素,删除成功则返回 true。
public int size():返回集合的长度,即元素个数。
public Object[] toArray():将集合转换成一个 Object 数组,所有的集合元素则变为对应的数组元素。
public <T> T[] toArray(T[] a):将集合转换为指定类型的数组。
public Iterator<E> iterator():返回 Iterator 对象,该对象用于遍历集合里的元素。
注意 contains() 和 remove() 方法是依靠 equal() 方法支持的。
一般来说,要使用接口中的方法,应该创建一个子类然后实现这个接口,但是由于开发的严格性,所以我们现在不会直接使用 Collection 接口,而是使用它的两个子接口 List 和 Set。这两个子接口的区别在于 List 允许元素重复而 Set 不允许重复。


2.1 List 子接口

一般来说,80% 的情况都会使用 List 子接口,它对 Collection 接口进行了一些功能的扩展,下面是一个常用方法,主要是利用索引的一些方法:
public void add(int index,E element):添加元素到指定索引处。
public E get(int index):返回指定索引的元素。
public int indexOf(Object o):返回指定元素第一次出现的索引。
public int lastIndexOf(Object o):返回指定元素最后一次出现的索引。
public ListIterator<E> listIterator():返回一个 ListIterator 对象。
public E remove(int index):删除并返回指定索引的元素。
public E set(int index,E element):设置(修改)指定索引的元素。
public List<E> subList(int fromIndex,int toIndex):返回从 fromIndex(包含)到 toIndex(不包含)的元素的子集合。
default void sort(Comparator<? super E> c):使用 Comparator 对集合进行排序。这个方法后面不做记录,以后有时间专门记录下各个排序方式。
List 也是一个接口,所以要想使用这个接口,就必须使用它的子类,它的子类很多,ArrayList 、 LinkedList 、 Stack 、 Vector等等,最为常用的就是 ArrayList。

2.1.1 ArrayList

对于 ArrayList 不需要做过多的介绍,只需要知道 90% 的情况下都使用这个子类即可,下面通过 ArrayList 这个最为典型的子类来介绍下 Collection 接口和 List 接口常用方法的基本用法,然后等四个子类都介绍完了再将四个子类进行一下对比。
public class ListDemo {public static void main(String[] args) {List<String> list = new ArrayList<String>();System.out.println(list);list.add("迈克尔乔丹");list.add("科比布莱恩特");list.add("特雷西麦克格雷迪");list.add("阿伦艾弗森");System.out.println(list);System.out.println("list中是否包含'迈克尔乔丹':" + list.contains("迈克尔乔丹"));System.out.println("list中是否包含'勒布朗詹姆斯':" + list.contains("勒布朗詹姆斯"));list.remove("迈克尔乔丹");System.out.println(list);list.add(1, "凯文杜兰特");list.add(3, "凯文杜兰特");System.out.println(list);System.out.println("凯文杜兰特第一次出现的索引:" + list.indexOf("凯文杜兰特"));System.out.println("凯文杜兰特最后一次出现的索引:" + list.lastIndexOf("凯文杜兰特"));list.set(2, "雷阿伦");System.out.println(list);System.out.println(list.get(2));System.out.println("截取第二个元素到第四个元素的子集合:" + list.subList(2, 4));list.clear();System.out.println("list是否为空:" + list.isEmpty());}}
运行程序,打印结果如下:


注意 List 是成功保存了重复元素的,这和 Set 是有区别的。因为 List 接口是 Collection 接口的子接口,所以我们也可以用 ArrayList 来实例化 Collection 接口,但是就不能用 List 接口中的方法了:


这些方法的基本使用看着都没问题,但是如果我们现在来保存对象的话,会有一个问题产生:
class Person {private String name;private int age;public Person(String name, int age) {super();this.name = name;this.age = age;}@Overridepublic String toString() {return "[姓名:" + name + ",年龄:" + age + "]";}}public class ListDemo {public static void main(String[] args) {List<Person> list = new ArrayList<Person>();list.add(new Person("李一", 22));list.add(new Person("张三", 23));list.add(new Person("王八", 24));System.out.println(list);list.remove(new Person("王八", 24));System.out.println(list);}}
这个程序看起来应该是添加了三个对象又删除了一个,所以最后应该还剩两个对象才对,但是实际运行结果却是:
这说明 list 集合中没有我们要删除的元素,在介绍 Collection 接口的方法的时候有提到 contains() 和 remove() 方法都依靠 equals() 方法,所以我们应该覆写 equals() 方法,如果不覆写的话则默认依靠地址来比较,覆写 equals() 方法和 hashCode() 方法的话 Eclipse 都有快捷键,也不用我们手动去写,覆写后重新运行,发现相同的对象被删除了:

2.1.2 Vector

Vector 是一个古老的类,在 Java 1 的时候就有了,因为那个年代 Java 还没有提供系统的集合框架,所以 Vector 中的方法名都比较长,比如 addElement(E obj),实际上它和

add(E e) 并没有什么区别。为了方便编程而简短了方法名,应运而生了类集框架,所以对集合的操作就有了新的标准,为了保留下 Vector 类,所以 Vector 类多实现了 List 接口,所以 Vector 中会有一些功能重复的方法,比如 addElement(E obj) 和 add(E e),在 Vector 中这两个方法都有。Vector 的使用跟 ArrayList 一样,因为它们依赖的是接口而不是具体类,所以只是具体子类不一样,用法完全一样:

public class ListDemo {public static void main(String[] args) {List<Person> list = new Vector<Person>();list.add(new Person("李一", 22));list.add(new Person("张三", 23));list.add(new Person("王八", 24));System.out.println(list);}}
运行结果如下:


2.1.3 Stack

Stack 表示的是栈操作,栈是一种先进后出的数据结构。Stack 是 Vector 的子类,但是它不会使用 Vector 的方法,它的常用方法并不多,事实上 Stack 这种数据结构使用得也并不多,主要也就是有“入栈”和“出栈”操作。
public E push(E item):将一个元素压入栈,这个元素处于栈顶。
public E peek():返回栈顶第一个元素,但并不会让元素出栈。
public E pop():返回栈顶第一个元素,并让这个元素出栈。
public boolean empty():判断该栈是否为空。
public int search(Object o):返回指定元素的索引。
public class ListDemo {public static void main(String[] args) {Stack<Person> stack = new Stack<Person>();stack.push(new Person("李一", 22));stack.push(new Person("张三", 23));stack.push(new Person("王八", 24));System.out.println("栈是否为空:" + stack.empty());System.out.println(stack.peek());System.out.println(stack.pop());System.out.println(stack.pop());System.out.println(stack.pop());System.out.println(stack.pop());}}
运行程序,打印结果如下:

可以看到调用 peek() 方法只会返回栈顶元素,并不会将元素出栈,这个栈里面只有三个元素,当我们出栈三次后再调用 pop() 方法就发现程序报错了:java.util.EmptyStackException,说明当前栈已经为空了,不允许再执行出栈操作了。
栈这种结构在 Android 上体现得比较典型,因为 Back 键实际上体现的就是栈操作,每次按 Back 键后显示的总是处于栈顶的 Activity。


2.1.4 LinkedList

LinkedList 不仅实现了 List 接口,还实现了 Deque 接口,Deque 代表一个“双段队列”,可以同时从两端来添加和删除元素,Deque 的方法不过多描述,需要的时候查一下 API 即可,用一个简单例子示范一下 LinkedList 的用法:
public class ListDemo {public static void main(String[] args) {LinkedList<Person> list = new LinkedList<Person>();list.offer(new Person("李一", 22));list.push(new Person("张三", 23));list.push(new Person("王八", 24));list.offer(new Person("赵四", 25));// 顺序应该是王八,张三,李一,赵四System.out.println(list);// 返回第一个元素但不删除该元素System.out.println(list.peekFirst());// 返回最后一个元素但不删除该元素System.out.println(list.peekLast());// 返回栈顶元素并将该元素出栈System.out.println(list.pop());// 返回最后一个元素并删除该元素System.out.println(list.pollLast());System.out.println(list);}}
运行程序,打印结果如下:


2.1.5 List 接口小结

先上一张表:
 ArrayListVectorStackLinkedList出现时间Java 2Java 1Java 1Java 2性能较优同步处理,较差同步处理,较差随机访问元素时较差,新增和删除元素时占优实现方式数组数组数组双向链表容量扩展每次至少扩展至(原长度*3)/2+1每次至少扩展至原长度的2倍同Vector 数据安全线程不安全线程安全线程安全线程不安全
总的来说,Vector 现在基本不怎么使用了,它的效率相对于 ArrayList 也要稍差一点,LinkedList  相对于 ArrayList 的话, 它除了 List 的功能还有双端队列和栈的功能,一般对性能要求不高的话,只需要知道它们之间这点差别就行了,但是由于它们俩的内部实现方式不同,数组结构在随机访问元素的时候效率较高,而链表结构在插入、删除操作时效率较高。但是总体来说还是 ArrayList 效率较高。在我们的开发中,还是优先考虑 ArrayList,只有涉及到大量插入、删除操作时才考虑 LinkedList。


2.2 Set 接口

Set 接口并没有对 Collection 接口进行功能的扩展,只是简单的继承了 Collection 接口,所以它没有 get() 等 List 接口特有的方法。

2.2.1 HashSet

HashSet 是 Set 接口的常用实现类,HashSet 就是按 Hash 算法来存储集合中的元素的,它的核心价值在于速度,因为它可以直接根据该元素的 hashCode 来计算元素的存储位置,所以它具有比较好的存取和查找性能。HashSet 的存储是无序的,而且不允许有重复元素,这是它和 List 的最大区别。
public class SetDemo {public static void main(String[] args) {Set<String> set = new HashSet<String>();set.add("李一");set.add("张三");set.add("王八");set.add("王八");set.add("王八");System.out.println(set);}}
运行程序,打印结果如下:


可以看到 set 集合存储元素跟添加先后无关,是无序的,而且并没有保存重复元素。然后再保存对象试试:
创建一个 Person 类:
class Person {private String name;private int age;public Person(String name, int age) {super();this.name = name;this.age = age;}@Overridepublic String toString() {return "[姓名:" + name + ",年龄:" + age + "]";}}

利用 HashSet 保存对象:
public class SetDemo {public static void main(String[] args) {Set<Person> set = new HashSet<Person>();set.add(new Person("李一", 21));set.add(new Person("张三", 22));set.add(new Person("王八", 23));set.add(new Person("王八", 23));System.out.println(set);}}
运行程序,打印结果如下:

奇怪的事情发生了,不能保存重复元素的 HashSet 集合却保存了两个“重复”的对象。这是因为 HashSet 判断两个元素相等的标准是通过 equals() 和 hashCode() 方法,只有两个方法都比较相等时才会认为是同一个对象。所以我们需要重写这两个方法(必须两个都重写),利用编译器自动生成即可:
class Person {private String name;private int age;public Person(String name, int age) {super();this.name = name;this.age = age;}@Overridepublic int hashCode() {final int prime = 31;int result = 1;result = prime * result + age;result = prime * result + ((name == null) ? 0 : name.hashCode());return result;}@Overridepublic boolean equals(Object obj) {if (this == obj)return true;if (obj == null)return false;if (getClass() != obj.getClass())return false;Person other = (Person) 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;}@Overridepublic String toString() {return "[姓名:" + name + ",年龄:" + age + "]";}}
再次运行,发现重复元素没有了:


我们不用手动去重写 hashCode() 方法,但也可以了解一下它的基本规则:
1).程序运行过程中,同一个对象多次调用 hashCode() 方法返回的值应该相同。
2).当两个对象通过 equal() 方法比较返回 true 时,这两个对象的 hashCode() 应该返回相同的值。
3).对象中用作 equals() 方法比较标准的实例变量,都应该用于计算 hashCode 值。 

2.2.1 TreeSet

HashSet 是无序的,而 TreeSet 是有序的,如何排序的,先不要方,先来看看能不能像使用 HashSet 一样使用 TreeSet,先保存字符串:

public class SetDemo {public static void main(String[] args) {Set<String> set = new TreeSet<String>();set.add("李一");set.add("张三");set.add("王八");set.add("王八");System.out.println(set);}}
打印如下:

这没有问题,一旦保存对象:

public class SetDemo {public static void main(String[] args) {Set<Person> set = new TreeSet<Person>();set.add(new Person("李一", 21));set.add(new Person("张三", 22));set.add(new Person("王八", 23));set.add(new Person("赵四", 23));System.out.println(set);}}

一运行,程序就报错了:

Exception in thread "main" java.lang.ClassCastException: com.qinshou.collectiondemo.Person cannot be cast to java.lang.Comparable

类型转换错误,Person类 不能转为 Comparable 类,这个 Comparable 类是什么,之前说 TreeSet 有序的,这个 Comparable 就是帮助 TreeSet 排序的比较器,String 类是实现了这个接口的,所以我们保存字符串才会没问题,现在我们要利用 TreeSet 来保存自定义的类,那么这个自定义的类也得实现 Comparable 接口并且覆写接口中的方法,先简单比较一下年龄:

@Overridepublic int compareTo(Person o) {// TODO Auto-generated method stubif (this.age > o.age) {return 1;} else if (this.age < o.age) {return -1;}return 0;}

重新运行程序,结果如下:


我们发现确实按照年龄,排序了,但是却少了“赵四”这个对象,为什么?原因是我们只比较了年龄,年龄相等的时候返回 0 ,所以我们知道了,当 compareTo() 方法返回 0 时, TreeSet 则认为是同一个对象,所以不会保存“重复”元素。所以还需要修改 compareTo() 方法:

@Overridepublic int compareTo(Person o) {// TODO Auto-generated method stubif (this.age > o.age) {return 1;} else if (this.age < o.age) {return -1;}return this.name.compareTo(o.name);}

运行程序,结果如下:


由此我们也发现了 TreeSet 的问题,类中的成员变量我们都需要去比较,当类中成员变量很多的时候,我们覆写 compareTo() 方法就特别麻烦,所以我们一般也不会使用 TreeSet。


2.2.3 Set接口小结

 HashSetTreeSet性能总是优于TreeSet总是低于HashSet线程安全线程不安全线程不安全Comparable不需要实现必须实现该接口,并重写该接口中的方法进行排序相同元素比较利用 hashCode() 和 equals() 方法利用 compareTo() 即可实现重复元素的判断一般来说,我们为了省事都不愿意使用 TreeSet ,因为成员变量过多会导致覆写比较方法的麻烦,只有当我们需要实现特定的排序方式时,才会使用 TreeSet。需要注意的是,
compareTo() 方法判断重复元素并不是真正意义上的重复判断,在非排序的情况下,判断重复元素都是依靠 hashCode() 和 equals() 方法。
Set 接口使用得也比较少,如果真要使用的时候一定要记住一个原则就是:数据不重复。

3、Map 接口

Collection 接口是单值保存,即每次保存的是一个对象,而 Map 接口则是保存一对对象,即 key-value 对。value 可以重复,但是 key 不允许重复,因为每一个 key 只能对应一个 value,是一对一的关系。通过指定 key 可以拿到唯一确定的 value。 Map 接口常用方法如下:

public void clear():删除该 Map 中的所有 key-value 对。
public boolean containsKey(Object key):查询该 Map 中是否包含指定 key ,如果有则返回 ture ,否则返回 false 。
public boolean containsValue(Object value):查询该 Map 中是否包含指定 value (至少一个),如果有则返回 ture ,否则返回 false 。
public Set<Map.Entry<K,V>> entrySet():返回 Map 中包含 key-value 对组成的 Set 集合,每个元素都为 Map.Entry(Map 的内部类)。
public V get(Object key):根据指定 key 返回对应 value,如果该 Map 中不包含指定 key ,则返回 null。
public Set<K> keySet():返回该 Map 中所有 key 组成的 Set 集合。
public put(K key, V value):添加一个 key-value 对,如果该 key-value 对已存在,则覆盖原来的数据。
public V remove(Object key):删除指定的 key 对应的 key-value 对,如果该 key 不存在,则返回 null。
public Collection<V> values():返回该 Map 中所有 value 组成的 Collection。

Map 接口并不像 Collection 接口那样还有更具体的子接口,它的实现类也很多,比较常用的 HashMap 和 Hashtable。


3.1 HashMap

Map 存放数据的最终目的是为了数据的查找,就像字典一样,而 Collection 的目的是为了存放数据和输出数据。

还是利用 HashMap 子类来介绍一下 Map 的基本使用:

public class MapDemo {public static void main(String[] args) {Map<String, Integer> map = new HashMap<String, Integer>();map.put("一", 1);map.put("二", 2);map.put("三", 3);System.out.println(map);System.out.println("是否包含key '一':" + map.containsKey("一"));System.out.println("是否包含key '四':" + map.containsKey("四"));System.out.println("是否包含value '1':" + map.containsValue(1));System.out.println("是否包含value '4':" + map.containsValue(4));System.out.println(map.get("二"));for (String string : map.keySet()) {System.out.println("每一个key:" + string);}for (int i : map.values()) {System.out.println("每一个value:" + i);}map.remove("二");System.out.println(map);map.clear();System.out.println(map);}}
运行程序,打印结果如下:

通过打印我们可以看出,HashMap  的存放都是无序的,只要带有 hash,都是无序的。

HashMap 的 key 和 value 都是可以为 null 的。

public class MapDemo {public static void main(String[] args) {Map<String, Integer> map = new HashMap<String, Integer>();map.put(null, 0);map.put("空", null);System.out.println(map);}}

运行程序,打印如下:


一般情况下,我们是使用 Integer 或者 String 类型的数据作为 key ,那么如果我们使用自定义类来作为 key 会发生什么情况呢?先创建一个 Person 类:

class Person {private String name;private int age;public Person(String name, int age) {super();this.name = name;this.age = age;}@Overridepublic String toString() {return "[姓名:" + name + ",年龄:" + age + "]";}}

我们来看看分别使用 String 和 自定义 Person 类作为 key 时的不同情况:

public class MapDemo {public static void main(String[] args) {Map<String, Person> map = new HashMap<String, Person>();map.put("李一", new Person("李一", 21));System.out.println(map.get("李一"));Map<Person, String> map2 = new HashMap<Person, String>();map2.put(new Person("李一", 21), "李一");System.out.println(map2.get(new Person("李一", 21)));}}
运行程序,打印结果如下:


可以看到使用 Person 类作为 key 的时候,取出时为 null 。Map 中有一个方法 keySet() 是获得所有 key 的,返回的是一个 Set 集合,就是说 key 是存放在 Set 集合中的,前面提到 Set 集合判断重复元素依靠的是 hashCode() 和 equals() 方法,所以我们如果要使用自定义类作为 key 的话,必须要覆写这两个方法。一般情况下,首选都是使用 String 类作为 key。


3.2 Hashtable

Hashtable 是一个古老的实现类,从 Java 1 就出现了,所以它跟 Vector 类似,里面也有几个方法名比较长的方法,它的使用跟 HashMap 完全一样,因为之前说过,它们依赖的是接口。但是它和 HashMap 不同的是它不允许 key 或 value 为null。

public class MapDemo {public static void main(String[] args) {Map<String, Integer> map = new Hashtable<String, Integer>();map.put("一", 1);map.put("二", 2);map.put("三", 3);System.out.println(map);map.put(null, 0);map.put("空", null);}}
运行程序,打印结果如下:


当保存 null 的时候抛出了 NullPointerException ,这就是 Hashtable 和 HashMap 的不同。


3.3 Map 接口小结

由于 Hashtable 和 Vector 一样都是一个古老的类,所以 HashMap 与 Hashtable 的区别就和 ArrayList 与 Vector 的区别类似。
 HashMapHashtable出现时间Java 2Java 1性能异步处理同步处理数据安全线程不安全线程安全设置nullkey和value 都允许为nullkey和value都不允许为null

4、总结

Java 的类集框架其实还有很多接口和子类,比如 Deque 接口,EnumSet 类等,但是最常用的基本就是我总结的几个,这几个类集框架基本也就能应对平时的开发了。类集框架中也还有很多东西没细细总结,比如每个集合的遍历方式,一般最经典的方式就是使用 Iterator 来遍历,这也是 Java 内部实现的迭代器模式,还有我以前老用的 foreach 方式,for 循环方式,以及 Enumeration 接口遍历,List 特有的 listIterator 遍历方式,这些方式实现遍历都不难,只是性能会有些许差异。还有一个操作集合的工具类 Collections 可以帮助我们操作集合,但是里面只是些方法,就不过多总结了。
看这一章节的东西花了三天,其实这些东西在平时开发中也经常用到,只是面试时忽然一问到就有点懵逼,基础不能丢,虽然看着枯燥,但终归还是要弄清楚的。
原创粉丝点击