【Java核心知识】容器类(一)
容器类又称为集合类,但我个人更偏向于容器类这个叫法,容器,盛放物体的东西,在《Java编程思想》的英文版中,给这一章起的标题是:holding your objects,容器类很形象的描述了这个类的目的就是从来盛放你的对象的。
数组同样具有存储对象(引用)的功能,更多的时候,数组用来存储基本数据类型,但是数组一经创建以后,大小就已经固定,不能更改,但我们在程序中,对象的数量往往是不确定的,因此数组就不能满足我们的需求。容器类不仅可以达到存储对象的目的,而且动态改变其容量的大小,并且在JDK中,针对不同的数据结构,设计了多样的容器类来提高存取元素的效率(需要注意的是,容器类只能存储引用数据类型,不能存储基本数据类型,对基本数据类型的存储是通过自动装箱机制来完成的,并且容器类中存储的是对象的引用)。
01
容器类概述
依照存入容器类中对象的形式来分,容器类可以分为单列容器,双列容器,单列容器内直接存储对象,双列集合以key-value的形式存储对象。单列容器的顶层接口为Collection,双列容器的顶层接口为Map。下面看JDK中设计的容器类的继承体系:
上面是集合体系完整的、标准的继承习题,但是其中涉及的很多接口是我们用不到,因此我们给出一些常用的容器类中的继承体系,该图并非实际的继承体系,只是方便记忆的简化版继承体系:
先看单列容器类:
双列容器:
以上两个继承体系图中涉及的类及其特点请面试的朋友务必记忆。
02
单列容器类顶层接口Collection
单列容器类的顶层接口为Collection,其源码中常用的方法如下:
public interface Collection<E> extends Iterable<E> { //返回容器中对象个数。 int size(); //判断容器是否为空 boolean isEmpty(); //判断容器中是否包含某个对象 boolean contains(Object o); //返回容器的迭代器对象 Iteratoriterator(); //将容器中的对象转为数组 Object[] toArray(); //将容器转为所需要类型的数组 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); //清空容器 void clear(); //获取Stream流 default Streamstream() { return StreamSupport.stream(spliterator(), false); }}
下面给出一个例子,以ArrayList为例:
public class CollectionTest { public static void main(String[] args) { System.out.println("==========Collection中元素的添加==========="); Collection collectionChar=new ArrayList(); collectionChar.add("a"); collectionChar.add("b"); collectionChar.add("c"); System.out.println(collectionChar);//输出[a, b, c] Collection collectionString=new ArrayList(); collectionString.add("hello"); collectionString.add("world"); Collection collectionInteger=new ArrayList(); collectionInteger.add("1"); collectionInteger.add("2"); //将collectionChar中的元素全部添加到collectionString collectionString.addAll(collectionChar); System.out.println(collectionString);//输出[hello, world, a, b, c] System.out.println("==========Collection中元素查询==========="); //判断当前容器中元素个数 System.out.println(collectionString.size());//输出5 //判断容器中是否包含某个指定元素 System.out.println(collectionString.contains("hello"));//输出true //判断元素中是否包含另一个容器中的所有元素 System.out.println(collectionString.containsAll(collectionChar));//输出true System.out.println(collectionString.containsAll(collectionInteger));//输出false //从当前容器中移除其子集元素,也就是求某一子集的补集 boolean all = collectionString.removeAll(collectionChar); System.out.println(collectionString);//[hello, world] //清空元素 collectionString.clear(); System.out.println(collectionString.size());//0 }}
以上代码给出了一些基本方法的使用,这些方法通过方法名称和参数类型就可以知道其作用。下面详述几个重要的方法。
toCharArray方法
下面详述以下toArray,toArray(T[] a)两个方法的用法:
toArray:将Collection容器类中的对象转为数组,注意这个结果是Object[]类型的数组,需要逐个向下转型,转变为我们需要的数据类型。示例如下:
public static void main(String[] args) { Set hashSet=new HashSet(); hashSet.add("a"); hashSet.add("b"); hashSet.add("c"); //错误示范,会报错java.lang.ClassCastException //String[] Strings = (String[])hashSet.toArray(); Object[] objects = hashSet.toArray(); for (Object object : objects) { System.out.println((String)object); } }
注意上述代码中报错的地方,Java中的转型只适用于单个元素的,如果你想集体把Object[]转为String[],这是不允许的。
toArray(T[] a):把当前容器中的元素,转为指定的数组类型进行存储。示例如下:
public class SetTest { public static void main(String[] args) { Set<String> hashSet=new HashSet<String>(); hashSet.add("a"); hashSet.add("b"); hashSet.add("c"); String[] strings=new String[hashSet.size()]; String[] stringsResult = hashSet.toArray(strings); for (String s : stringsResult) { System.out.println(s); } }}
contains方法
先看这样一段代码:
public class ContainsMethod { public static void main(String[] args) { //新建一个字符串 String s1="hello"; //开辟内存再创建一个hello String s2=new String("hello"); Collection strings = new ArrayList<>(); //将s1放入容器 strings.add(s1); //判断s2是否在容器中 boolean contains = strings.contains(s2); System.out.println(contains); }}
contains是true还是false呢?
在本文的开篇词中,我们说过,容器中存储的是对象的引用,在上述代码中,我们分别创建了两个字符串的引用s1和s2,分别指向了不同堆内存但是堆内存存储了相同内容字符串的元素。把s1存入了容器,并未将s2存储到容器,但我们用contains方法判断容器中是否包含s2的时候却返回了true。这说明容器内是已经存在s2的。这就表明contains方法判断容器中是否包含某个元素是通过判断元素的内容的。当然,这样说还不太准确。contains方法是Collection接口中的方法,不同的实现类contains方法的实现可能不同,对我们这里看ArrayList中contains的源码:
public boolean contains(Object o) { return indexOf(o) >= 0; } public int indexOf(Object o) { if (o == null) { for (int i = 0; i < size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i < size; i++) if (o.equals(elementData[i])) return i; } return -1; }
重点显而易见,equals方法。也就是说,contains方法判断某个元素是否包含在容器中,是通过调用传入元素的equals方法的结果来判断的。因为String类中重写了equals方法,用来比较字符串的内容,所以contains判断容器中是否存在比较的是对象的内容。所以引例中输出的是true。如果集合类中的类没有重写equals方法,那么调用的Object 的equals方法,那么比较的就是内存地址了。所以一般每个实现了Collection 的类都会重写contains方法。
iterator方法
Collection 接口继承了Iterable接口,iterator方法是Iterable接口中的方法,用来返回一个Iterator对象,Iterator主要用来实现容器中元素的遍历。我们先看一下Iterator类中的方法:
public interface Iterator<E> { //判断当前游标的下一个位置是否有元素 boolean hasNext(); //返回当前游标指向的元素,并将游标指向下一个位置。 E next(); //删除游上次调用next方法h获得的元素 default void remove() { throw new UnsupportedOperationException("remove"); }}
下面给出一个例子展示其用法:请注意例子注释中的错误示例。
public class IteratorTest { public static void main(String[] args) { Collection collectionChar=new ArrayList(); collectionChar.add("a"); collectionChar.add("b"); collectionChar.add("c"); Iterator iterator = collectionChar.iterator(); //利用iterator遍历容器中的元素 while (iterator.hasNext()){ Object next = iterator.next(); System.out.println(next); } Iterator iteratorNew = collectionChar.iterator(); //删除容器中的元素 while (iteratorNew.hasNext()){ iteratorNew.next(); iteratorNew.remove(); System.out.println(collectionChar); } // 错误示范2:不可以用collection的remove// while (iterator.hasNext()){// String next =(String) iterator.next();// collectionChar.remove(next);//不可以用collection的remove// }// 错误示范2:remove()将会删除上次调用next()时返回的元素,也就是说先调用next()方法,再调用remove方法才会删除元素// while (iterator.hasNext()){// iterator.remove();//// } }}
以上就是单列集合类Collection中涉及的方法。
2、1
Collection的子接口List
现在我们把目光转向容器类继承体系图中的Collection接口的左侧来看一下list接口,list接口用来描述具有如下特点的单列容器:
1、有序,有序指的是存储与读取的顺序是一样的(并非大小排序)
2、有整数的索引
3、可以存储重复元素
因为list是Collection的子接口,所以它拥有Collection接口中描述的所以方法,除此之外,还有其特殊的方法。
//在指定索引的下标处插入一个元素,其余元素后移void add(int index, E element);//获取指定索引处的元素E get(int index);//获取指定元素第一次出现位置的索引int indexOf(Object o); //获取指定元素最后一次出现位置的索引int lastIndexOf(Object o);//将指定索引处的元素替换为目标元素E set(int index, E element);
下面给出一个示例:
public static void main(String[] args) { LinkedList strings = new LinkedList<>(); strings.add("a"); strings.add("c"); //在第一个位置添加一个元素b strings.add(1,"b");//输出[a, b, c]\ strings.get(2);//输出c int b = strings.indexOf("b");//输出1 int a = strings.lastIndexOf("a");//输出0 strings.set(2, "d");//输出[a, b, d] }
2、1、1
List中的ArrayList
ArrayList的基础用法我们在前面已经有了基础的使用,现在我们说一些ArrayList底层的知识。
1、ArrayList是实现了基于动态数组的数据结构,其增删数据较慢,但是查询数据较快。
2、ArrayList初始化的数组容量大小是10,每次扩容为当前容量的1.5倍,源码如下:
/** * 初始化容量.*/private static final int DEFAULT_CAPACITY = 10;
//扩容函数private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
3、ArrayList是线程不安全的
2、1、2
List中的Vector
Vector是一个线程安全版的ArrayList,在Vector类内部的、涉及对元素的更改的方法都是有关键synchronized字修饰的,因此它是在多线程环境下使用的。与ArrayList不同的是,Vector的扩容是按照当前容量的2倍扩容的。
2、1、3
List中的LinkedList
ArrayList是基于数组实现的,其查找元素的时间复杂度为O(1),但是在数组中间插入元素或者删除元素,会移动大量的元素,所以对于涉及插入和删除的操作,效率较低。但是LinkedList是基于双向链表实现的,因此其增加和删除元素的效率高,但是查询元素的效率低。
2、2
Collection的子接口Set
在Collection接口中,子接口list中存储的元素是存取有序并且可以重复的,而Set接口存储的元素特点如下:
1、元素存取无序(存入的顺序和读取的顺序不一样)。因为Set元素的存入是根据元素的哈希值计算的。
2、元素不可重复
HashSet小例如下
public static void main(String[] args) { Set sets=new HashSet(); sets.add("c"); sets.add("b"); sets.add("a"); sets.add("a"); System.out.println(sets);//输出[a, b, c] }
我们给出的HashSet源码如下:
public HashSet() { map = new HashMap<>(); }
可以看出HashSet底层是通过双列容器中的HashMap实现的,其实HashSet存储的就是HashMap中的key类型。具体原理我们学完HashMap就可以明朗。
2、2
Collection中的子接口SortedSet
SortedSet接口表示的是有排序功能的接口,实现了SortedSet的类具有自动排序的功能。TreeSet就是SortedSet的一个实现类,存入中TreeSet的元素会自动排序。小例如下:
public static void main(String[] args) { Set sets=new TreeSet<>(); sets.add(10); sets.add(9); sets.add(20); sets.add(25); System.out.println(sets);//[9, 10, 20, 25] }
TreeSet底层是通过TreeMap实现的,等学完TreeMap我们就会对TreeSet更加明朗。
以上就是我们这篇文章的内容,在下一篇中我们会讲双列容器。