首页 > 学院 > 开发设计 > 正文

7.集合1

2019-11-08 01:37:20
字体:
来源:转载
供稿:网友

1. 集合

1.1 什么是集合

  集合是存储对象的容器。集合中可以存储任意类型的变量。java提供了多种集合类,不同集合类的数据结构不同,分别适合在不同的场景使用。

1.2 集合和数组

  数组也用来存储数据,如果定义一个对象数组,例如Object[] objArr;那么对象数组也能存储对象。但是集合和数组有很大区别:

  (1)数组长度是固定的,集合的长度是可变的。如果无法预先知道需要多少存储多少个数据,那么很难使用数组。

  (2)数组一般只能存储同一类型的元素,集合可存储不同类型的元素。

  (3)集合只能存储引用类型的元素。JDK 5实现了自动装箱功能,所以集合形式上也能存储基本数据类型的元素。

1.3 Collection接口

  集合有很多共性,因此集合抽象出了一个Collection接口,下面讲的单例集合都是Collection接口的实现类。下面通过一个实现类来讲Collection接口中的常用方法。

2. Collection接口

  为了了解Collection接口中方法,首先要创建一个集合对象,那么需要用多态方式来创建一个Collection接口的实现类对象。

  先找到一个实现Collection接口的类。查看API,有如下说明:

  “JDK 不提供此接口的任何直接实现:它提供更具体的子接口(如 Set 和 List)的实现。”

  也就是说Collection没有直接的实现类,我们可以找Collection子接口Set或者List接口的实现类。其中,ArrayList是List接口的一个实现类,我们就用它来创建集合对象,以此介绍Collection接口中的方法。【提示,目前看API时会遇到、等字样,而且在IDE中会有警告,这涉及到泛型,暂时不用管它】

2.1 添加和删除对象方法

  (1)public boolean add(Object o):添加一个元素到集合中。此方法总是返回true,因此返回值不重要。

  (2)public boolean addAll(Collection c):将一个集合的元素全部添加到该集合中。

  (3)public boolean remove(Object o):将元素从集合中删除(如果有重复,只会删除一个)。如果集合中没有该元素,则返回false(不会出现异常)。

  (4)public boolean removeAll(Collection c):移除集合中存在于集合c中的元素,包括重复的元素。只要有一个元素被移除了就返回true。

  (5)public void clear():移除集合中所有的元素,集合变为一个空集合。

2.2 其他方法

  (1)public boolean contains(Object o):判断集合中是否包含指定的元素。对于引用类型,contains方法中是调用对象的equals方法来判断的,因此当集合存储自定义对象时,注意重写好自定义类的equals方法。

  (2)public boolean isEmpty():判断集合是否为空。

  (3)public int size():返回集合中元素个数。

  (4)public boolean containsAll(Collection c):判断集合中是否包含了集合c的所有元素。必须全部包含才返回true。

  (5)public boolean retainAll(Collection c):将该集合与集合c进行交集,结果保存在本集合中,如果本集合改变了就返回true,否则返回false。

  (6)public Object[] toArray():将集合转成Object数组。

  (7)public String toString():Collection重写了toString()方法,可以直接将集合以类似数组的方式输出。

  下面是将自定义对象存储再集合中的例子。

  (1)Student类

package com.zhang.test;public class Student { PRivate String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } // 构造方法 public Student(){} public Student(String name, int age) { this.name = name; this.age = age; } // 下面重写equals、hashCode和toString方法 public boolean equals(Object obj) { if(!(obj instanceof Student)) { return false; } if(obj == this) { return true; } Student stu = (Student) obj; if(stu.getName().equals(this.getName()) && stu.getAge() == this.getAge()) { // 若姓名和年龄相同,就假定他们是同一对象 return true; } else { return false; } } public int hashCode() { return this.getName().hashCode() + this.getAge(); } public String toString() { return this.getName() + ":" + this.getAge(); }}

  (2)主类

package com.zhang.test;import java.util.ArrayList;import java.util.Collection;public class Demo { public static void main(String[] args) { Collection c = new ArrayList(); c.add(new Student("张三", 12)); c.add(new Student("李四", 13)); c.add(5); //可以加任意类型 // 判断,底层调用的是equals方法 System.out.println(c.contains(new Student("张三", 12))); // 得到元素个数 System.out.println(c.size()); // 输出集合,调用的是toString()方法,显示很友好,把其中自定义对象equals方法也调用了 System.out.println(c); }}

2.3 Iterator迭代器

  迭代器用于对集合元素的遍历。迭代器以内部类的形式存在于集合类中,通过迭代器能够拿到集合中元素,也能通过迭代器来操作元素。

  调用集合的iterator()方法能得到此集合的迭代器。Collection接口继承自Iterable接口,正是Iterable接口定义了iterator()方法。iterator()方法返回的是Iterator接口,该接口有如下方法以便遍历和获取元素:

  (1)boolean hasNext():判断是否还有可迭代的元素。

  (2)Object next():获取下一个元素,同时指针再向后移动。如果没有下一个元素,则抛出NoSuchElementException异常。

  小知识点:如果一个方法能返回任意类型或者接受任意类型,那么返回值类型或者参数类型就可设置为Object,因为Object是所有类的基类。

  因此常用上面的方法实现遍历,例子:(Student类还是上面的代码)

package com.zhang.test;import java.util.ArrayList;import java.util.Collection;import java.util.Iterator;public class Demo { public static void main(String[] args) { Collection c = new ArrayList(); c.add(new Student("张三", 12)); c.add(new Student("李四", 13)); // 迭代 Iterator it = c.iterator(); while(it.hasNext()) { // 先拿出对象。由于存储的都是Student,直接转换 Student stu = (Student) it.next(); System.out.println(stu); /* 注意,不能这样写: System.out.println( ((Student) it.next()).getName() ); System.out.println( ((Student) it.next()).getAge() ); 这样每次循环都会执行两次next(),会导致跳过元素和出现 NoSuchElementException的异常 */ } }}

  推荐在for循环中创建迭代器并迭代,这样可以尽早释放迭代器资源。代码如下:

package com.zhang.test;import java.util.ArrayList;import java.util.Collection;import java.util.Iterator;public class Demo { public static void main(String[] args) { Collection c = new ArrayList(); c.add(new Student("张三", 12)); c.add(new Student("李四", 13)); // 迭代 for(Iterator it = c.iterator(); it.hasNext(); ) { Student stu = (Student) it.next(); System.out.println(stu.getName()); System.out.println(stu.getAge()); } }}

  迭代器中还有remove()方法,可在遍历时删除元素,但是使用remove()方法之前必须先调用next()方法,否则会出现IllegalStateException异常。此外,在用迭代器迭代过程中,只能通过迭代器操作元素,不能使用集合本身提供的方法或者其他方法操作集合元素,否则会有安全隐患,会抛出并发修改异常ConcurrentModificationException。这点在List集合中还会强调和加深。

  总结:上述讲的Collection方法和迭代器,在下面讲的单例集合(List和Set集合)中都能使用,因为单例集合就是Collection的实现类。

3. List集合

3.1 简介

  List也是接口,最常用的实现类是ArrayList。List集合有如下特点:

  (1)List集合是有序的。List集合会保留元素的存储顺序,这样取出元素时是顺序取出。

  (2)List集合中元素可以重复,即一个集合能存储多个相同的元素。

3.2 List集合特有方法

  (1)public void add(int index, Object o):在指定索引处添加元素。

  (2)public Object get(int index):获取指定位置的元素。

  (3)public Object remove(int index):删除指定索引处的元素,并返回此元素

  (4)public Object set(int index, Object o):将指定索引处的元素修改为新值o,返回值是原本此位置的元素。若索引超出范围,则抛出IndexOutOfBoundsException。

  (5)public boolean addAll(int index, Collection c):在指定索引处添加集合中所有元素。

  (6)public int indexOf(Object o):返回集合中第一次出现该对象的索引,如果没有,就返回-1。

  (7)public int lastIndexOf(Object o):返回集合中最后一次出现该对象的索引,如果没有,就返回-1。

  (8)public List subList(int fromIndex, int toIndex):获得指定区间的子集合。不包含toIndex。

  说明:上述介绍的很多方法,比如remove()、indexOf()等都要先找到集合中与目标相同的对象,调用的方法都是调用equals()方法。

  List集合有get(int index)方法,这样可以结合Collection的size()方法自行实现遍历,而不需用迭代器。例子:

package com.zhang.test;import java.util.ArrayList;import java.util.List;public class Demo { public static void main(String[] args) { List list = new ArrayList(); list.add(new Student("张三", 12)); list.add(new Student("李四", 13)); // 利用get()和size()遍历List集合 for(int i = 0; i < list.size(); i++) { Student stu = (Student) list.get(i); System.out.println(stu); } }}

3.3 List特有迭代器——ListIterator

  List集合可以直接用Iterator迭代器,还能用List特有的迭代器ListIterator。通过调用List集合对象的listIterator()方法,能得到ListIterator对象。

  ListIterator继承自Iterator,基本的用法和Iterator相同,只是ListIterator中增加了如下方法:

  (1)public boolean hasprevious():判断是否有上一个元素

  (2)public Object previous():获取上一个元素,并将指针再指向前一个元素。

  通过上述方法,能够实现逆向遍历List集合。当然,一般先要使用next()方法,将指针指向后面,才可能逆向遍历,因为一开始的游标(指针)都是在第一个元素的前面。

  另外,ListIterator还有方法:

  public void add(Object o):将指定的元素插入列表(可选操作)。该元素直接插入到 next 返回的下一个元素的前面(如果有),或者 previous 返回的下一个元素之后(如果有);如果列表没有元素,那么新元素就成为列表中的唯一元素。新元素被插入到隐式光标前:不影响对 next 的后续调用,并且对 previous 的后续调用会返回此新元素。

  借此机会着重讲“并发修改异常”。

  前面提到过:在用迭代器过程中,只能通过迭代器操作元素,否则会有安全隐患,产生并发修改异常。例子:集合中存储“hello”,“world”和“java”三个字符串,要求遍历集合时,一旦遇到“world”字符串,就在集合中添加一个“c++”字符串。

  错误示范:在用迭代器过程中,使用集合方法操作集合元素:

package com.zhang.test;import java.util.*;public class Demo { public static void main(String[] args) { List list = new ArrayList(); list.add("hello"); list.add("world"); list.add("java"); // 迭代器遍历 for(Iterator it = list.iterator(); it.hasNext(); ) { String str = (String) it.next(); if(str.equals("world")) { // 用集合的add()方法添加元素 list.add("c++"); } } // 最后输出 System.out.println(list); }}

  运行程序后出现异常:ConcurrentModificationException。为什么出现并发修改异常呢?因为使用迭代器时,如果使用了集合来操作元素,会导致迭代器并不知道集合被修改了,而迭代器的使用是完全依赖于集合的,因此出现并发修改异常。

  解决的办法就是只用迭代器来操作集合(ListIterator中有add方法),或者只用集合的方法来遍历和操作集合。下面是例子。只不过根据他们的实现不用,用ListIterator添加时,元素被添加在当前元素下面,而用集合本身的add()方法添加时,新元素被添加在集合的最后。

  方法1:使用List迭代器的add()方法。

package com.zhang.test;import java.util.*;public class Demo { public static void main(String[] args) { List list = new ArrayList(); list.add("hello"); list.add("world"); list.add("java"); // 迭代器遍历 for(ListIterator it = list.listIterator(); it.hasNext(); ) { String str = (String) it.next(); if(str.equals("world")) { it.add("c++"); } } // 最后输出 System.out.println(list); // [hello, world, c++, java] }}

  方法2:使用List集合的方法来遍历和操作。

package com.zhang.test;import java.util.*;public class Demo { public static void main(String[] args) { List list = new ArrayList(); list.add("hello"); list.add("world"); list.add("java"); // 用集合的size()和get()遍历,并修改 for(int i = 0; i < list.size(); i++) { if(list.get(i).equals("world")) { list.add("c++"); } } // 最后输出 System.out.println(list); // [hello, world, java, c++] }}

3.4 List集合常用子类

  List集合常用的子类有ArrayList(前面一直用的)、LinkedList和Vector。详细解释如下:

  (1)ArrayList:底层的数据结构是动态数组。特点是查询速度快(允许直接用索引来查找对应的元素)、增删慢(插入和删除操作涉及元素移动和内存操作)。ArrayList是线程不安全的类,效率较高。

  (2)LinkedList:底层数据结构是用链表存储数据。链表的特点是查询慢(需要遍历节点查找),增删快(只需记住上个节点和下个节点)。LinkedList也是线程不安全的类,效率较高。LinkedList中一些特有的方法(需要针对头尾的操作):

  addFirst(E e)、addLast(E e)、getFirst()、getLast()、removeFirst()和removeLast()等。

  (3)Vector:是线程安全的ArrayList。用法和ArrayList相似。说明一下,Vector中提供了一个elements()实例方法,返回的是一个Enumeration对象。此对象用法和Iterator一致,只不过方法名不一样,方法原型为:

  public boolean hasMoreElement();和public Object nextElement()。

  除此之外,List的实现类还有Stack,就是堆栈结构。其实List就是数据结构里面的线性表,使用时,结合数据结构的知识,就能很好的选择使用,并能迅速的了解不同结构中提供的方法的含义。因此推荐学习完C语言之后研究下数据结构和算法。

4. 泛型(Generic)

4.1 引入泛型

  集合能存储不同的数据类型,但这个特点可能会出现安全问题。因为操作集合元素时,要先拿到该元素,用集合(或迭代器)的方法只能拿到Object类型元素,因为集合能存储任意类型,集合本身无法知道自己所存储的究竟是什么类型。因此我们拿到Object类型元素时再将元素强制转换成需要的类型,这时可能发生转换错误。

  例子:

package com.zhang.test;import java.util.ArrayList;public class Demo { public static void main(String[] args) { ArrayList list = new ArrayList(); list.add("hello"); list.add("world"); list.add(new Student()); // 该集合中存储了字符串,最后一个元素存储了学生对象 // 遍历取出元素 for(int i = 0; i < list.size(); i++) { // 强制转换为String类型 String str = (String) list.get(i); System.out.println(str); } }}

  运行程序会出现“ClassCastException”,即类型转换异常,这是因为集合中还存储了Student对象。这样取出元素会有安全性问题,这种问题在编译时是无法发现的,因此IDE只能提示警告。虽然能通过类型检查(如instanceof)来判断元素的类型,但是这样效率较低。处理这样的问题,一般是直接限定该集合只能存储一种特定类型的对象。

  而泛型就是用来限定该集合只能存储一种特定数据类型的。

  对集合使用泛型的格式:

  集合类 集合变量 = new 集合类();这里的就是泛型,E就指定该集合能存储何种数据类型。比如:

  ArrayList arrayList = new ArrayList();就表示arrayList集合只能存储String类型的数据。

  需要注意的是,这里的E只能写引用类型,不能写基本数据类型,比如不能写成,而只能写成。以后就常用泛型了,使用泛型后,IDE环境中也不会出现警告了。现在也能知道API中等的含义了。

  下面体会下泛型的使用:

package com.zhang.test;import java.util.ArrayList;public class Demo { public static void main(String[] args) { ArrayList<Student> stuList = new ArrayList<Student>(); stuList.add(new Student("张三", 13)); stuList.add(new Student("李四", 14)); // 遍历 for(int i = 0; i < stuList.size(); i++) { // 使用泛型无需类型转换 Student stu = stuList.get(i); System.out.println(stu); } }}

  上面的例子中体会到,使用泛型后,集合中只能存特定类型的数据,并且取出元素时,会自动根据泛型得到对应的数据类型,无需类型转换,即如果这是用String泛型的集合,那么用get()方法得到的就会是String类型的数据。

  很多地方使用泛型更加友好,比如JDK中提供的一个接口Comparable:

package java.lang;public interface Comparable { public int compareTo(Object o);}

  在JDK 1.5之后,此接口被改成了泛型:

package java.lang;public interface Comparable<T> { public int compareTo(T o);}

  这样的话,这里的泛型类型就能被以后具体的类型所替换,避免了从特定类型转到Object类型,再从Object类型转到具体类型的过程。(学了后面的泛型方法、泛型类等后理解更深刻)

4.2 泛型方法

  泛型方法就是在方法中使用泛型,实际也就是在方法形参和返回值中使用泛型,使形参接受指定类型的参数,然后返回该类型的数据,这样可以使方法适应不同的数据类型,而且不用像使用Object那样,进行强制转换。

  可以只在形参中使用泛型,返回值可以不用泛型,而是返回特定的一种类型,但是一般不只在返回值中使用泛型,因为没有什么意义。

  泛型相当于将类型当作变量处理。规范泛型的定义一般是一个大写的任意字母(实际上任意合法的变量名都是可以的)。在方法中使用泛型的格式:

  public <声明泛型的变量> 返回值类型 方法名(数据类型和形参…)。尖括号中就是声明泛型的变量,声明好了后,就能在方法中使用该变量作为泛型类型了,比如:

  public T getModel(T t) {…}。

  上面的方法就先在尖括号中声明了泛型变量T,这个T就相当于一个类型了,然后在返回值、参数和方法体中就能使用这个T类型了。方法说明此方法接受一个泛型类型T参数,方法返回值是也是类型T。这个T到底是什么类型呢,只有在调用方法时,传递参数时才知道。也就是调用方法时传递什么类型,这个T就是什么类型。

  实际上泛型就是实现参数化类型,把类型当作参数一样的传递。

  例子:

package com.zhang.test;import java.util.Date;public class Demo { public static void main(String[] args) { Student stu = getObj(new Student("张三", 12)); System.out.println(stu); // 传递什么类型进去,就返回什么类型 Date date = getObj(new Date(System.currentTimeMillis())); System.out.println(date); } // 这是一个静态的泛型方法,直接返回传递进来的参数 public static <E> E getObj(E e) { return e; }}

4.3 泛型类

  当一个类中多个方法都用到了泛型方法时,特别是在类中也需要使用泛型时,那么就可将泛型声明到类上,使用泛型类。

  泛型类是在类名后声明泛型,也是在尖括号中。这时,泛型的类型就是在创建此对象时使用的泛型类型。当然,类中的静态成员不能使用类中定义的泛型,如果静态成员要使用泛型,只能独立声明泛型,因为静态成员是在类对象存在之前就存在的,不能根据创建对象时使用的类型来决定静态泛型的类型。

  泛型类的格式:

  public class 类名<声明泛型的变量> {

    …

  }

  例子:

  (1)GenericDemo类(泛型类)

package com.zhang.test;// 类上声明泛型public class GenericDemo <E> { // 类中可以使用泛型类型 private E obj; // 泛型成员 // get和set方法 public void setObj(E obj) { this.obj = obj; } public E getObj() { return this.obj; } // 实例方法,展示其中维护的泛型类型对象 public void showObj() { System.out.println(this.getObj().toString()); } // 如果是静态方法,同样还要定义泛型类型,不能使用实例成员 public static <T> void showArr(T[] arr) { // 用于展示任意类型的数组 for(int i = 0; i < arr.length; i++) { System.out.println(arr[i]); } }}

  (2)主类

package com.zhang.test;import java.util.Date;public class Demo { public static void main(String[] args) { // 使用泛型类。 // 创建对象时,new后面的泛型类型可省略,只要声明时带有泛型类型即可 GenericDemo<Student> stu = new GenericDemo<>(); stu.setObj(new Student("张三", 14)); stu.showObj(); // 使用静态方法 String[] strArr = {"hello", "world", "java"}; GenericDemo.showArr(strArr); }}

  创建对象时,如果不指定泛型的具体类型,那么默认类型是Object(和创建集合对象时一样)。

4.3.1 泛型类的继承

如果一个类继承了泛型类,那么下面的方式:

(1)子类不使用父类泛型,那么直接:

  public class SubClass extends GenericDemo {

    …

  }

  这时在子类中调用父类的泛型方法,其类型是Object。

  或者子类使用自己定义的泛型,那么语法是:

  public class SubClass extends GenericDemo {…}

(2)子类使用和父类一样的泛型:

  public class SubClass extends GenericDemo {…};

(3)子类继承父类的指定类型:

  public class SubClass extends GenericDemo{…}

  当然这时子类也能使用自己的泛型:

  public class SubClass extends GenericDemo{…}

4.4 泛型接口

  接口泛型的声明也是在接口名后面的尖括号中。比如:

package com.zhang.test;public interface GenericInterface <E> { void show(E e);}

  那么泛型接口的实现类,可以还是用泛型,也可以处理具体的类型,比如:

  (1)方法1:还是用泛型,处理任意类:

package com.zhang.test;// 声明泛型类型的名字可以和接口的名字不一样。只要他们一致就表示处理的还是泛型public class ImplDemo1<T> implements GenericInterface<T> { public void show(T t) { System.out.println(t); }}

  然后主类创建实现类对象时指定具体类即可。

  (2)方法2:处理具体的类型:

package com.zhang.test;public class ImplDemo2 implements GenericInterface<String> { @Override public void show(String s) { System.out.println(s); }}

  当然,如果直接implements GenericInterface,那么具体的类就是Object,此外,实现类也能使用自己的泛型,即:

  public class ImplDemo2 implements GenericInterface。但是就是不能同时使用接口的泛型,又使用自己的泛型,泛型类中也是这样。

4.5 泛型通配符

  先看一个需求:写一个方法,此方法能接受任意泛型的集合对象,然后将对象打印。

  由于能接受任意类型,那么方法参数可定义为Collection,即:

public static void print(Collection<Object> c) { for(Iterator<Object> it = c.iterator(); it.hasNext(); ) { Object obj = it.next(); System.out.println(obj); }}

  我们的想法是,传递一个ArrayList对象或者一个ArrayList对象进去,都能打印出集合中的元素,即能:

public static void main(String[] args) { ArrayList<String> arr = new ArrayList<String>(); arr.add("hello"); print(arr);}

  但是print(arr)这行报错,并不能通过编译,原因是print函数已经限定了类型是Object,因此arr是不能作为参数的。

  解决的办法有:1.就传递给print方法Coolection对象,但是这样可能要将原数据进行转换。2.print方法不使用泛型,即形参直接是Collection c。但是这样没有使用泛型会有警告,JDK不推荐。

  而通配符?就是用于解决这个问题的,可以将?作为泛型类型,这个?就代表任意类型。如果传递进来的泛型是String,那么?就代表String,如果传递进来的泛型是Integer,那么?就代表Integer。因此最终可用这样的代码:

package com.zhang.test;import java.util.ArrayList;import java.util.Collection;import java.util.Iterator;public class Demo { public static void main(String[] args) { ArrayList<String> arr = new ArrayList<String>(); arr.add("hello"); ArrayList<Integer> num = new ArrayList<>(); num.add(1); num.add(23); print(arr); print(num); } public static void print(Collection<?> c) { for(Iterator<?> it = c.iterator(); it.hasNext(); ) { Object obj = it.next(); // 这里还是用Object是理所当然的,因为不知道具体类型 // 使用?为了使用泛型并且不用将原来对象进行转换 System.out.println(obj); } }}

  通配符用来限定类型的范围。单单的?没有类型的限定,是没有边界的,可以用下面的方式进行限定:

  

package com.zhang.test;import java.util.ArrayList;import java.util.Collection;public class Demo { public static void main(String[] args) { //?通配符可以是任意类型 Collection<?> e1 = new ArrayList<Animal>(); Collection<?> e2 = new ArrayList<Dog>(); //? extends E,可以是E和E的子类 Collection<? extends Animal> e3 = new ArrayList<Animal>(); Collection<? extends Animal> e4 = new ArrayList<Dog>(); //? super E,可以是E及其父类 Collection<? super Animal> e5 = new ArrayList<Animal>(); Collection<? super Animal> e6 = new ArrayList<Dog>();//错误 }}//先写两个具有继承关系的类class Animal{}class Dog extends Animal{}

5. 补充

  泛型允许程序员在编写代码时,就限制集合的处理类型,从而把原来程序运行时可能发生的问题,转变为编译时问题,以此提高程序的可读性和稳定性。

  注意:泛型是提供给javac编译器使用的,它用于限定集合的输入类型,让编译器在源代码级别上就阻止向集合中插入非法数据。但编译器编译完带有泛形的java程序后,生成的class文件中将不再带有泛形信息,以此使程序运行效率不受影响,这个过程称为“擦除”。

  泛型的基本术语:

  以ArrayList为例:<>念:typeof。

  ArrayList中的E称为类型参数变量

  ArrayList中的Integer称为实际类型参数

  整个称为ArrayList泛型类型

  整个ArrayList称为参数化的类型ParameterizedType


本篇文章已结束,若您喜欢,欢迎支持我的工作。

支付宝

微信


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表