Java高级特性编程及实战
上QQ阅读APP看书,第一时间看更新

任务1 查询标题

关键步骤如下。

  • 创建集合对象,并添加数据。
  • 统计新闻标题总数量。
  • 输出新闻标题名称。

1.1.1 认识集合

开发应用程序时,如果想存储多个同类型的数据,可以使用数组来实现;但是使用数组存在如下一些明显缺陷:

  • 数组长度固定不变,不能很好地适应元素数量动态变化的情况。
  • 可通过数组名.length获取数组的长度,却无法直接获取数组中实际存储的元素个数。
  • 数组采用在内存中分配连续空间的存储方式存储,根据元素信息查找时效率比较低,需要多次比较。

从以上分析可以看出数组在处理一些问题时存在明显的缺陷,针对数组的缺陷,Java提供了比数组更灵活、更实用的集合框架,可大大提高软件的开发效率,并且不同的集合可适用于不同应用场合。

Java集合框架提供了一套性能优良、使用方便的接口和类,它们都位于java.util包中,其主要内容及彼此之间的关系如图1.1所示。

0

图1.1 Java集合框架图

从图1.1中可以看出,Java的集合类主要由Map接口和Collection接口派生而来,其中Collection接口有两个常用的子接口,即List接口和Set接口,所以通常说Java集合框架由3大类接口构成(Map接口、List接口和Set接口)。本章讲解的主要内容就是围绕这3大类接口进行的。

注意

虚线框表示接口或者抽象类,实线框表示开发中常用的实现类。

1.1.2 List接口

Collection接口是最基本的集合接口,可以存储一组不唯一、无序的对象。List接口继承自Collection接口,是有序集合。用户可使用索引访问List接口中的元素,类似于数组。List接口中允许存放重复元素,也就是说List可以存储一组不唯一、有序的对象。

List接口常用的实现类有ArrayList和LinkedList。

1. 使用ArrayList类动态存储数据

针对数组的一些缺陷,Java集合框架提供了ArrayList集合类,对数组进行了封装,实现了长度可变的数组,而且和数组采用相同的存储方式,在内存中分配连续的空间,如图1.2所示,所以,经常称ArrayList为动态数组。但是它不等同于数组,ArrayList集合中可以添加任何类型的数据,并且添加的数据都将转换成Object类型,而在数组中只能添加同一数据类型的数据。

0

图1.2 ArrayList存储方式示意图

ArrayList类提供了很多方法用于操作数据,如表1-1中列出的是ArrayList类的常用方法。

表1-1 ArrayList类的常用方法

0

示例1

使用ArrayList常用方法动态操作数据。

实现步骤如下。

(1)导入ArrayList类。

(2)创建ArrayList对象,并添加数据。

(3)判断集合中是否包含某元素。

(4)移除索引为0的元素。

(5)把索引为1的元素替换为其他元素。

(6)输出某个元素所在的索引位置。

(7)清空ArrayList集合中的数据。

(8)判断ArrayList集合中是否包含数据。

关键代码:

0

在示例1中,① 的代码调用ArrayList的无参构造方法,创建集合对象。常用的ArrayList类的构造方法还有一个带参数的重载版本,即ArrayList(int initialCapacity),它构造一个具有指定初始容量的空列表。

② 的代码将list集合中索引为0的元素删除,list集合的下标是从0开始,也就是删除了“张三丰”,集合中现有元素为“郭靖”和“杨过”。

③ 的代码将list集合中索引为1的元素替换为“黄蓉”,即将“杨过”替换为“黄蓉”,集合中现有元素为“郭靖”和“黄蓉”。

④ 的代码是使用for循环遍历集合,输出集合中的所有元素。list.get(i)取出集合中索引为i的元素,并强制转换为String类型。

⑤ 的代码为输出元素“小龙女”所在的索引位置,因集合中没有该元素,所以输出结果为-1。

⑥ 的代码是使用增强for循环遍历集合,输出集合中的所有元素。增强for循环的语法在Java基础课程中讲过,这里不再赘述。可以看出,遍历集合时使用增强for循环比普通for循环在写法上更加简单方便,而且不用考虑下标越界的问题。

⑦ 的代码用来判断list集合是否为空,因为前面执行了list.clear()操作,所以集合已经为空,输出为true。

注意

① 调用ArrayList类的add(Object obj)方法时,添加到集合当中的数据将被转换为Object类型。

② 使用ArrayList类之前,需要导入相应的接口和类,代码如下:

import java.util.ArrayList;
import java.util.List;

示例2

使用ArrayList集合存储新闻标题信息(包含ID、名称、创建者),输出新闻标题的总数量及每条新闻标题的名称。

实现步骤如下。

(1)创建ArrayList对象,并添加数据。

(2)获取新闻标题的总数。

(3)遍历集合对象,输出新闻标题名称。

关键代码:

//创建新闻标题对象,NewTitle为新闻标题类
NewTitle car=new NewTitle(1, "汽车", "管理员");
NewTitle test=new NewTitle(2, "高考", "管理员");
//创建存储新闻标题的集合对象
List newsTitleList=new ArrayList();
//按照顺序依次添加新闻标题
newsTitleList.add(car);
newsTitleList.add(test);
//获取新闻标题的总数
System.out.println("新闻标题数目为:"+newsTitleList.size()+"条");
//遍历集合对象
System.out.println("新闻标题名称为:");
for(Object obj:newsTitleList){
      NewTitle title=(NewTitle)obj;
      System.out.println(title.getTitleName());
} 

输出结果如图1.3所示。

0

图1.3 输出新闻标题信息

在示例2中,ArrayList集合中存储的是新闻标题对象。在ArrayList集合中可以存储任何类型的对象。其中,代码List newsTitleList=new ArrayList();是将接口List的引用指向实现类ArrayList的对象。在编程中将接口的引用指向实现类的对象是Java实现多态的一种形式,也是软件开发中实现低耦合的方式之一,这样的用法可以大大提高程序的灵活性。随着编程经验的积累,开发者对这个用法的理解会逐步加深。

ArrayList集合因为可以使用索引来直接获取元素,所以其优点是遍历元素和随机访问元素的效率比较高。但是由于ArrayList集合采用了和数组相同的存储方式,在内存中分配连续的空间,因此在添加和删除非尾部元素时会导致后面所有元素的移动,这就造成在插入、删除等操作频繁的应用场景下使用ArrayList会导致性能低下。所以数据操作频繁时,最好使用LinkedList存储数据。

2. 使用LinkedList类动态存储数据

LinkedList类是List接口的链接列表实现类。它支持实现所有List接口可选的列表的操作,并且允许元素值是任何数据,包括null。

LinkedList类采用链表存储方式存储数据,如图1.4所示,优点在于插入、删除元素时效率比较高,但是LinkedList类的查找效率很低。

0

图1.4 LinkedList类存储示意图

它除了包含ArrayList类所包含的方法外,还提供了表1-2所示的一些方法,可以在LinkedList类的首部或尾部进行插入、删除操作。

表1-2 LinkedList类的常用方法

0

示例3

使用LinkedList集合存储新闻标题(包含ID、名称、创建者),实现获取、添加及删除头条和末条新闻标题信息功能,并遍历集合。

实现步骤如下。

(1)创建LinkedList对象,并添加数据。

(2)添加头条和末条新闻标题。

(3)获取头条和末条新闻标题信息。

(4)删除头条和末条新闻标题。

关键代码:

//创建多个新闻标题对象
NewTitle car=new NewTitle(1, "汽车", "管理员");
NewTitle medical=new NewTitle(2, "医学", "管理员"); 
NewTitle fun=new NewTitle(3, "娱乐", "管理员");
NewTitle gym=new NewTitle(4, "体育", "管理员");
//创建存储新闻标题的集合对象并添加数据
LinkedList newsTitleList=new LinkedList(); 
newsTitleList.add(car);
newsTitleList.add(medical);
//添加头条新闻标题和末条新闻标题
newsTitleList.addFirst(fun);
newsTitleList.addLast(gym);
System.out.println("头条和末条新闻已添加");
//获取头条以及最末条新闻标题
NewTitle first=(NewTitle) newsTitleList.getFirst();
System.out.println("头条的新闻标题为:"+first.getTitleName());
NewTitle last=(NewTitle) newsTitleList.getLast();
System.out.println("排在最后的新闻标题为:"+last.getTitleName());
//删除头条和末条新闻标题
newsTitleList.removeFirst();
newsTitleList.removeLast();
System.out.println("头条和末条新闻已删除");
System.out.println("遍历所有新闻标题:");
for(Object obj:newsTitleList){
     NewTitle newTitle=(NewTitle)obj;
     System.out.println("新闻标题名称:"+newTitle.getTitleName());
}

输出结果如图1.5所示。

0

图1.5 使用LinkedList存储并操作新闻标题信息

除了表1-2中列出的LinkedList类提供的方法外,LinkedList类和ArrayList类所包含的大部分方法是完全一样的,这主要是因为它们都是List接口的实现类。由于ArrayList采用和数组一样的连续的顺序存储方式,当对数据频繁检索时效率较高,而LinkedList类采用链表存储方式,当对数据添加、删除或修改比较多时,建议选择LinkedList类存储数据。

1.1.3 Set接口

1. Set接口概述

Set接口是Collection接口的另外一个常用子接口,Set接口描述的是一种比较简单的集合。集合中的对象并不按特定的方式排序,并且不能保存重复的对象,也就是说Set接口可以存储一组唯一、无序的对象。

Set接口常用的实现类有HashSet。

2. 使用HashSet类动态存储数据

假如现在需要在很多数据中查找某个数据,LinkedList类就无需考虑了,它的数据结构决定了它的查找效率低下。如果使用ArrayList类,在不知道数据的索引且需要全部遍历的情况下,效率一样很低下。为此Java集合框架提供了一个查找效率高的集合类HashSet。HashSet类实现了Set接口,是使用Set集合时最常用的一个实现类。HashSet集合的特点如下。

  • 集合内的元素是无序排列的。
  • HashSet类是非线程安全的。
  • 允许集合元素值为null。

表1-3中列举了HashSet类的常用方法。

表1-3 HashSet类的常用方法

0

示例4

使用HashSet类的常用方法存储并操作新闻标题信息,并遍历集合。

实现步骤如下。

(1)创建HashSet对象,并添加数据。

(2)获取新闻标题的总数。

(3)判断集合中是否包含汽车新闻标题。

(4)移除对象。

(5)判断集合是否为空。

(6)遍历集合。

关键代码:

//创建多个新闻标题对象
NewTitle car=new NewTitle(1, "汽车", "管理员");
NewTitle test=new NewTitle(2, "高考", "管理员");
//创建存储新闻标题的集合对象
Set newsTitleList=new HashSet();
//按照顺序依次添加新闻标题
newsTitleList.add(car);
newsTitleList.add(test);
//获取新闻标题的总数
System.out.println("新闻标题数目为:"+newsTitleList.size()+"条");
//判断集合中是否包含汽车新闻标题
System.out.println("汽车新闻是否存在:"+newsTitleList.contains(car)); //输出true
newsTitleList.remove(test);  //移除对象
System.out.println("汽车对象已删除");
System.out.println("集合是否为空:"+newsTitleList.isEmpty());   //判断是否为空
//遍历所有新闻标题
System.out.println("遍历所有新闻标题:");
for(Object obj:newsTitleList){
     NewTitle title=(NewTitle)obj;
     System.out.println(title.getTitleName());
}

输出结果如图1.6所示。

0

图1.6 使用HashSet类存储并操作新闻标题信息

注意

使用HashSet类之前,需要导入相应的接口和类,代码如下:

import java.util.Set;
import java.util.HashSet;

在示例4中,通过增强for循环遍历HashSet,前面讲过List接口可以使用for循环和增强for循环两种方式遍历。使用for循环遍历时,通过get()方法取出每个对象,但HashSet类不存在get()方法,所以Set接口无法使用普通for循环遍历。其实遍历集合还有一种比较常用的方式,即使用Iterator接口。

1.1.4 Iterator接口

1. Iterator接口概述

Iterator接口表示对集合进行迭代的迭代器。Iterator接口为集合而生,专门实现集合的遍历。此接口主要有如下两个方法:

  • hasNext():判断是否存在下一个可访问的元素,如果仍有元素可以迭代,则返回true。
  • next():返回要访问的下一个元素。

凡是由Collection接口派生而来的接口或者类,都实现了iterate()方法,iterate()方法返回一个Iterator对象。

2. 使用Iterator遍历集合

下面通过示例来学习使用迭代器Iterator遍历Arraylist集合。

示例5

使用Iterator接口遍历ArrayList集合。

实现步骤如下。

(1)导入Iterator接口。

(2)使用集合的iterate()方法返回Iterator对象。

(3)while循环遍历。

(4)使用Iterator的hasNext()方法判断是否存在下一个可访问的元素。

(5)使用Iterator的next()方法返回要访问的下一个元素。

关键代码:

public static void main(String[] args){
     ArrayList list=new ArrayList();
     list.add("张三");
     list.add("李四");
     list.add("王五");
     list.add(2, "杰伦");
     System.out.println("使用Iterator遍历,分别是:");
     Iterator it=list.iterator(); //获取集合迭代器Iterator
     while(it.hasNext()){   //通过迭代器依次输出集合中所有元素的信息
          String name=(String)it.next();
          System.out.println(name);
     }
}

输出结果:

使用Iterator遍历,分别是:
张三
李四
杰伦
王五

示例5中是以ArrayList为例使用Iterator接口,其他由Collection接口直接或间接派生的集合类,如已经学习的LinkedList、HashSet等,同样可以使用Iterator接口进行遍历,遍历方式与示例5遍历ArrayList集合的方式相同。例如,将示例5改为使用Iterator对象遍历,关键代码如下。

//使用Iterator遍历HashSet集合
while(iterator.hasNext()){
    NewTitle title=(NewTitle) iterator.next();
    System.out.println(title.getTitleName());
}

1.1.5 Map接口

1. Map接口概述

Map接口存储一组成对的键(key)——值(value)对象,提供key到value的映射,通过key来检索。Map接口中的key不要求有序,不允许重复。value同样不要求有序,但允许重复。表1-4中列举了Map接口的常用方法。

表1-4 Map接口的常用方法

0

Map接口中存储的数据都是键——值对,例如,一个身份证号码对应一个人,其中身份证号码就是key,与此号码对应的人就是value。

2. 使用HashMap类动态存储数据

最常用的Map实现类是HashMap,其优点是查询指定元素效率高。

示例6

使用HashMap类存储学生信息,要求可以根据英文名检索学生信息。

实现步骤如下。

(1)导入HashMap类。

(2)创建HashMap对象。

(3)调用HashMap对象的put()方法,向集合中添加数据。

(4)输出学员个数。

(5)输出键集。

(6)判断是否存在“Jack”这个键,如果存在,则根据键获取相应的值。

(7)判断是否存在“Rose”这个键,如果存在,则根据键获取相应的值。

关键代码:

//创建学员对象
Student student1=new Student("李明", "男");
Student student2=new Student("刘丽", "女");
//创建保存“键——值对”的集合对象
Map students=new HashMap();
//把英文名称与学员对象按照“键——值对”的方式存储在HashMap中
students.put("Jack", student1);
students.put("Rose", student2);
//输出学员个数
System.out.println("已添加"+students.size()+"个学员信息");
//输出键集
System.out.println("键集:"+students.keySet());
String key="Jack";
//判断是否存在“Jack”这个键,如果存在,则根据键获取相应的值
if(students.containsKey(key)){
 Student student=(Student)students.get(key);
 System.out.println("英文名为"+key+"的学员姓名:"+student.getName());
}
String key1="Rose";
//判断是否存在“Rose”这个键,如果存在,则删除此键——值对
if(students.containsKey(key1)){
 students.remove(key1);
 System.out.println("学员"+key1+"的信息已删除");
}

输出结果如图1.7所示。

0

图1.7 使用HashMap类存储并检索学生信息

注意

① 数据添加到HashMap集合后,所有数据的数据类型将转换为Object类型,所以从其中获取数据时需要进行强制类型转换。

② HashMap类不保证映射的顺序,特别是不保证顺序恒久不变。

遍历HashMap集合时可以遍历键集和值集。

示例7

改进示例6,遍历所有学员的英文名及学员详细信息。

实现步骤如下。

(1)遍历键集。

(2)遍历值集。

关键代码:

//创建学员对象
Student student1=new Student("李明", "男");
Student student2=new Student("刘丽", "女");
//创建保存“键——值对”的集合对象
Map students=new HashMap();
//把英文名称与学员对象按照“键——值对”的方式存储在HashMap中
students.put("Jack", student1);
students.put("Rose", student2);
//输出英文名
System.out.println("学生英文名:");
for(Object key:students.keySet()){
      System.out.println(key.toString());
}
//输出学生详细信息
System.out.println("学生详细信息:");
for(Object value:students.values()){
      Student student=(Student)value;
      System.out.println("姓名:"+student.getName()+",性别:"+student.getSex());
}

输出结果如图1.8所示。

0

图1.8 使用HashMap集合遍历学生英文名及详细信息

在示例7中,使用增强for循环遍历HashMap集合的键集和值集,当然也可以使用前面的普通for循环或者迭代器Iterator来遍历,视个人习惯而选择。

0

Map补充案例

1.1.6 Collections类

Collections类是Java提供的一个集合操作工具类,它包含了大量的静态方法,用于实现对集合元素的排序、查找和替换等操作。

注意

Collections和Collection是不同的,前者是集合的操作类,后者是集合接口。

1. 对集合元素排序与查找

排序是针对集合的一个常见需求。要排序就要知道两个元素哪个大哪个小。在Java中,如果想实现一个类的对象之间比较大小,那么这个类就要实现Comparable接口。此接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo()方法被称为它的自然比较方法。此方法用于比较此对象与指定对象的顺序,如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。

compareTo()方法的定义语法格式如下。

int compareTo(Object obj);

其中:

  • 参数:obj即要比较的对象;
  • 返回值:负整数、零或正整数,根据此对象是小于、等于还是大于指定对象返回不同的值。

实现此接口的对象列表(和数组)可以通过Collections.sort()方法(和Arrays.sort()方法)进行自动排序。示例8通过实现Comparable接口对集合进行排序。

示例8

学生类Student实现了Comparable接口,重写了compareTo()方法,通过比较学号实现对象之间的大小比较。

实现步骤如下。

(1)创建Student类。

(2)添加属性学号number(int)、姓名name(String)和性别gender(String)。

(3)实现Comparable接口、compareTo()方法。

关键代码:

public class Student implements Comparable{
            private int number=0;   //学号
            private String name="";   //姓名
            private String gender="";   //性别
            public int getNumber(){
                  return number;
            }
            public void setNumber(int number){
                  this.number=number;
            }
            public String getName(){
                  return name;
            }
            public void setName(String name){
                  this.name=name;
            }
            public String getGender(){
                  return gender;
            }
            public void setGender(String gender){
                  this.gender=gender;
            }
            public int compareTo(Object obj){
              Student student=(Student)obj;
              //如果学号相同,那么两者就是相等的
              if(this.number==student.number){  
                  return 0; 
              //如果这个学生的学号大于传入学生的学号
              }else if(this.number>student.getNumber()){ 
                  return 1;
              //如果这个学生的学号小于传入学生的学号
              }else{ 
                  return -1;
              }
            }
}

元素之间可以比较大小之后,就可以使用Collections类的sort()方法对元素进行排序操作了。前面介绍过List接口和Map接口,Map接口本身是无序的,所以不能对Map接口做排序操作;但是List接口是有序的,所以可以对List接口进行排序。注意List接口中存放的元素,必须是实现了Comparable接口的元素才可以。

示例9

使用Collections类的静态方法sort()和binarySearch()对List集合进行排序与查找。

实现步骤如下。

(1)导入相关类。

(2)初始化数据。

(3)遍历排序前集合并输出。

(4)使用Collections类的sort()方法排序。

(5)遍历排序后集合并输出。

(6)查找排序后某元素的索引。

关键代码:

//省略声明Student对象代码
public static void main(String[] args){
      Student student1=new Student();
            student1.setNumber(5);
            Student student2=new Student();
            student2.setNumber(2);
            Student student3=new Student();
            student3.setNumber(1);
            Student student4=new Student();
            student4.setNumber(4);
            ArrayList list=new ArrayList();
            list.add(student1);
            list.add(student2);
            list.add(student3);
            list.add(student4);
            System.out.println("-------排序前-------");
            Iterator iterator=list.iterator();
            while(iterator.hasNext()){
                Student stu=(Student)iterator.next();
                System.out.println(stu.getNumber());
            }
            //使用Collections类的sort()方法对List集合进行排序
            System.out.println("-------排序后-------");
            Collections.sort(list); 
            iterator=list.iterator();
            while(iterator.hasNext()){
                Student stu=(Student)iterator.next();
                System.out.println(stu.getNumber());
            }
            //使用Collections类的binarySearch()方法对List集合进行查找
            int index=Collections.binarySearch(list,student3);  //①
            System.out.println("student3的索引是:"+index);
}

输出结果:

-------排序前-------
5
2
1
4
-------排序后-------
1
2
4
5
student3的索引是:0

示例9中,①的代码是使用Collections类的binarySearch()方法对List集合进行查找,因为student3的学号为1,故排序后索引变为0。

2. 替换集合元素

若有一个需求,需要把一个List集合中的所有元素都替换为相同的元素,则可以使用Collections类的静态方法fill()来实现。下面通过一个示例来学习使用fill()方法替换元素。

示例10

使用Collections类的静态方法fill()替换List集合中的所有元素为相同的元素。

实现步骤如下。

(1)导入相关类,初始化数据。

(2)使用Collections类的fill()方法替换集合中的元素。

(3)遍历输出替换后的集合。

关键代码:

public static void main(String[] args){
     ArrayList list=new ArrayList();
     list.add("张三丰");
     list.add("杨过");
     list.add("郭靖");
     Collections.fill(list, "东方不败");  //替换元素
     Iterator iterator=list.iterator();
     while(iterator.hasNext()){
         String name=(String)iterator.next();
         System.out.println(name);
     }
}

输出结果:

东方不败
东方不败
东方不败

至此,任务1已经全部完成。