JavaSE 学习笔记05丨泛型、集合

Chapter. 10 泛型

10.1 泛型程序设计

泛型,指可以在类或方法中预支地使用未知的类型。泛型程序设计(Generic programming),意味着编写的代码可被很多不同类型的对象所重用。使用泛型机制编写的程序代码,要比那些杂乱使用Object变量然后再进行强制类型转换的代码具有更好的安全性可读性。泛型对于集合类尤其有用,如集合类ArrayList

10.1.1 引入

在Java SE5.0之前,Java泛型程序设计是用继承实现的。例如ArrayList类只维护一个Object引用的数组:

public class JustTest{ //before Java SE 5.0
    public Object get(int i){...}
    public void add(Object o){...}
    ...
    private Object[] elementData;
}

如此实现有两个问题,当获取一个值时必须将返回的Object类进行强制类型转换。

ArrayList files = new ArrayList();
...
String filename = (String) names.get(0);

此外,下面这里不存在错误检查,可向数组列表中添加任何类的对象

files.add(new File("..."));

10.1.2 使用泛型的好处

泛型为上面所产生的问题提供了一个更好的解决方案——类型参数type parameters)。其带来的好处:

  1. 代码具有更好的可读性。

    ArrayList<String> files = new ArrayList<String>(); //一看便知道该数组列表中包含的是String对象
    
  2. 编译器能够很好地利用信息,无需进行强制类型转换。

    String filename = files.get(0); //当调用get()无需强转,编译器便知道返回值类型为String,而不是Object
    String str = it.next(); //使用迭代器无需强转
    

    一般在创建对象时,将未知的类型确定具体类型。若没有指定泛型,则默认类型为Object类。

  3. 编译器可以进行检查,避免插入错误类型的对象。将运行时的ClassCastException转移到了编译时异常。

    files.add("abcd"); //Correct
    //files.add(5); 当集合明确类型后存放类型不一致就会编译报错! 
    

    泛型,是数据类型的一部分。我们将类名与泛型合并一起看做数据类型。

10.2 泛型的定义与使用

TIPS:类型变量 使用大写形式且比较短。在Java库中,使用变量E表示集合的元素类型KV分别表示表的关键字与值的类型T表示任意类型

10.2.1 含有泛型的类

定义格式为:修饰符 class 类名<代表泛型的变量>{ }

class ArrayList<E>{ //API中的ArrayList集合
    public boolean add(E e){ ... }
    public E get(int index){ ... }
    ...
}
public class MyGenericClass<MVP>{ //自定义的泛型类
    private MVP mvp;
    public void setMVP(MVP mvp){
        this.mvp = mvp;
    }
    public MVP getMVP(){
        return mvp;
    }
}
public class Pair<T, U> { ... } //泛型类可以有多个类型变量,此处的Pair类第一个域和第二个域使用不同的类型

创建对象时,就需要将泛型确定下来。即用具体的类型替换类型变量就可以实例化泛型类型,例如:

ArrayList<Integer> list = new ArrayList<Integer>(); //在对象list中,所有的E类型都替换为String类型
MyGenericClass<String> a = new MyGenericClass<String>(); 
String mvp = a.getMVP();
MyGenericClass<Integer> b = new MyGenericClass<Integer>();
Integer mvp2 = b.getMVP();

10.2.2 含有泛型的方法

泛型方法可以定义在普通类中,也可以定义在泛型类中。

定义格式为:修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ }

public class MyGenericMethod{
    public <T> void show(T mvp){
        System.out.println(mvp.getClass());
    }
    public <T> T show2(T mvp){
        return mvp;
    }
}

调用泛型方法时,大多情况下方法调用可以省略<…>具体的类型参数,编译器有足够的信息能够推断出所调用的方法。

String middle = Array.show2(names); 

10.2.3 含有泛型的接口

定义格式为:修饰符 interface 接口名<代表泛型的变量> { }

public interface MyGenericInterface<E>{
    public abstract void add(E e);
    public abstract E getE();
}

使用方式:

  1. 定义实现类时即确定泛型的类型。

    public class MyImp1 implements MyGenericInterface<String>{
        @Override
        public void add(String e){ //方法参数从类型参数改为String
            //...
        } 
        public String getE(){ //返回类型从类型参数改为String
            //...
        }
    }
    
  2. 始终不确定泛型的类型,直至创建对象时才确定泛型的类型。

    public class MyImp2 implements MyGenericInterface<E>{
        @Override
        public void add(E e){
            //...
        }
        public E getE(){
            //...
        }
    }
    

    确定泛型:

    MyImp2<String> my = new MyImp2<String>();
    my.add("2333");
    

10.3 泛型通配符

10.3.1 通配符的基本使用

当使用泛型类或者接口时,传递的数据中泛型类型不确定,可以通过<?>表示,其中?表示未知通配符。但一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用;只能接受数据,而不能往该集合中存储数据。

public class JustTest {
    public static void main(String[] args) {
        Collection<Integer> list1 = new ArrayList<Integer>();
        getElement(list1);
        Collection<String> list2 = new ArrayList<String>();
        getElement(list2);
    }
    public static void getElement(Collection<?> coll){
        //....
    }
}

TIPS:泛型不存在继承关系,如:Collection<Object> list = new ArrayList<String>(); 这种是错误写法!

10.3.2 通配符限定

Java的泛型可以指定一个泛型的上限或下限。

  • 泛型的上限

    格式:类型名称 <? extends 类> 对象名称

    意义:只能接收指定类型或其子类。

    public static void getElements1(Collection<? extends Number> coll){ ... }
    //此时的泛型 ? ,必须是Number类型或者Number类型的子类
    
  • 泛型的下限

    格式:类型名称 <? super 类> 对象名称

    意义:只能接收指定类型或其父类。

10.4 泛型的约束与局限性

  1. 不能用基本类型实例化类型参数。

    比如说,没有Pair<double>,而只有Pair<Double>。其原因在于类型擦除,Pair类含有Object类型的域,而Object不能存储double

  2. 运行时类型查询只适用于原始类型

    if(a instanceof Pair<String>){ /*...*/ } //true,实际上仅仅测试a是否为任意类型的Pair
    if(a instanceof Pair<T>) { /*...*/ } //true
    
    Pair<String> a = ...;
    Pair<Employee> b = ...;
    if(a.getClass() == b.getClass()){ /*...*/ } //比较结果为true,因为两次调用getClass都将返回Pair.class
    
    Pair<String> p = (Pair<String>) a; //WARNING---can only test that a is a Pair
    
  3. 不能抛出也不能捕获泛型类实例

    事实上,泛型类拓展Throwable都不合法。

    public class MyProblem<T> extends Exception{ /*...*/ } //不会通过编译,Error!
    

    不能再catch子句中使用类型变量,如下不能通过编译。

    public static <T extends Throwable> void doWork(Class<T> t){
        try {
            /*do work*/
        }
        catch (T e){ //ERROR--can't catch type variable!!! 
            //...
        }
    }
    

    但在异常声明中可以使用类型变量,如下是合法的

    public static <T extends Throwable> void doWork(Class<T> t) throws T{
        try {
            /*do work*/
        }
        catch (T e){ //ERROR--can't catch type variable!!!
            //...
        }
    }
    
  4. 参数化类型的数组不合法。

    Pair<String> table = new Pair<String>[10]; //ERROR
    //若需要收集参数化类型的对象,最好使用ArrayList
    ArrayList<Pair<String>> table = ....
    
  5. 不能实例化类型变量。

    不能使用像new T(...)new T[...]或者T.class这样的表达式中的类型变量。

  6. 泛型类的静态上下文中类型变量无效。

  7. 注意擦除后的冲突。

Chapter 11. 集合

11.1 集合概述

从Java SE 5.0 开始,集合类是带有类型参数的泛型类。Java集合类库将接口实现分离。其集合按照其存储结构可分为两大类:单列集合java.util.Collection、双列集合Java.util.Map

实现 Collection 接口的类

集合类型 描述
\(ArrayList\) 动态增长和缩减的索引序列
\(LinkedList\) 可在任何位置进行高效插入和删除有序序列
\(ArrayDeque\) 循环数组实现的双端队列
\(HashSet\) 无重复元素且无序 的散列集
\(TreeSet\) 有序
\(LinkedHashSet\) 可记住元素插入次序的 集(链表+散列表实现)
\(PriorityQueue\) 高效删除最小元素的机会
\(BitSet\)(遗留) 存放序列的位集(功能同C++的位集模板)

实现 Map 接口的类

集合类型 描述
\(HashMap\) 存储键-值关联的数据结构
\(TreeMap\) 键值有序排列的映射
\(LinkedHashMap\) 可记住键-值添加次序映射

11.2 Collection 接口 与 Collections 工具类

11.2.1 Collection 继承结构

Collection接口,为单列集合类的根接口,用于存储一系列符合某种规则的元素。

它有两个重要子接口:java.util.Listjava.util.Set

  • List 特点为元素是有序的(指存储和取出元素的顺序是一致的)、且允许存储重复元素、有索引(故可用普通for循环进行遍历),它的主要实现类为java.util.ArrayListjava.util.LinkedList

  • Set 特点为元素不一定有序、不允许存储重复元素、没有索引(故不能用普通的for循环进行遍历),它的主要实现类为java.util.HashSetjava.util.TreeSet

11.2.2 Collection 常用方法

Collection中定义了单列集合(ListSet)通用的方法:

  • public boolean add(E element):将对象element添加至当前集合中。

    Collection<String> coll = new ArrayList<String>();
    coll.add("Luffy");
    
  • public void clear():清空集合中所有的元素。

  • public boolean remove(E element):在当前集合中删除对象element

  • public boolean contains(E element):判断当前集合中是否包含对象element

  • public boolean isEmpty():判断当前集合是否为空。

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

  • public Object[] toArray():将集合中元素,存储至数组中。

    Object[] arr = coll.toArray();
    for(int i = 0; i < arr.length(); i++){
        System.out.println(arr[i] + " ");
    }
    

11.2.3 Collections 工具类

java.utils.Collections是集合工具类,用于对集合进行相关操作。常用方法有:

  • public static <T> boolean addAll(Collection<T> c, T... elements)

    往集合c中添加多个相同类型的元素elements

    ArrayList<Integer> mylist = new ArrayList<Integer>();
    Collections.addAll(mylist, 233, 666, 1, 2);
    /* 上语句等价于下面写法
        mylist.add(233);
        mylist.add(666);
        mylist.add(1);
        mylist.add(2);
    */
    
  • public static void shuffle(List<?> list): 使用默认随机源对列表进行置换,所有置换发生的可能性都是大致相等的。(简而言之,打乱集合顺序)

    List<Integer> mylist = new ArrayList<Integer>();
    for(int i = 1; i <= 10; i++) mylist.add(i);
    for(int i = 1; i <= 6; i++){
        System.out.println("第" + i + "次洗牌:");
        Collections.shuffle(mylist);
        System.out.println(mylist);
    }
    
  • public static<T> void sort(List<T> list):将集合中元素按照默认规则排序。

    public static<T> void sort(List<T> list, Comparator<? super T>):将集合中元素按照指定规则排序。

11.2.4 Comparator 比较器

Java中提供两种对象间比较大小的实现方式:

  • 采用java.lang.Comparable接口实现——强行对实现它的每个类的对象进行整体排序。(类的自然排序)

    有些标准的Java平台类,实现了Comparable接口,而该接口定义了compareTo方法。

    public interface Comparable<T>{
        int compareTo(T others);
    }
    

    而对于自定义的对象,必须通过实现Comparable接口自定义排列顺序,因为在Object类中没有提供任何compareTo接口的默认实现。如下展示了如何用“部件编号”为Item对象进行排序。实现Comparable接口的对象列表(和数组)可以通过Collections.sortArrays.sort进行排序。

    class Item implements Comparable<Item>{
        public int compareTo(Item other){
            return partNum - other.partNum; //若第一项位于第二项前面,则返回负值;编号相同返回0;否则返回正值。
        }
    }
    

    显然对于上面的Comparable接口定义排序有其局限性,对于一个给定的类,只能够实现这个接口一次(也就说只能在类中实现compareTo()一次)。如上例子,如果我希望在一个集合中按“部件编号”排序,在另一集合按描述信息排序,同时这个类创建者又没有实现Comparable接口,此时就需要下面的Comparator接口。

  • java.util.Comparator接口实现——强行对某个对象进行整体排序。

    Comparator接口声明了一个带两个显式参数compare方法。

    public interface Comparator<T>{
        int compare(T a, T b);
    }
    

    该接口代表一个比较器。这个比较器没有任何数据,只是比较方法的持有器,可以将Comparator传递给sort方法(如Collections.sortArrays.sort),或者传递给某些数据结构(如TreeSet)。

    SortedSet<Item> sortByDescription = new TreeSet<Item>(new Comparator<Item>(){
        @Override
        public int compare(Item a, Item b){
            String descrA = a.getDescription();                   									String descrB = a.getDescription();
            return descrA.compareTo(descrB);
        }
    }); //将Comparator对象传递给TreeSet构造器来告诉树集使用不同的比较方法
    Collections.sort(list, new Comparator<Student>(){
        @Override
        public int compare(Student a, Student b){
            return a.getAge() - b.getAge();
        }
    });
    

11.3 Iterator 接口与迭代器

11.3.1 概述

JDK专门提供一个接口java.util.Iterator,用于遍历集合中的元素。Iterator接口属于Java集合,但与CollectionMap这两个专门用于存储元素的接口不同,Iterator 主要用于迭代访问(即遍历)元素,因而,Iterator对象被称为迭代器。

使用迭代器时,元素被访问的顺序取决于集合类型。若对ArrayList进行迭代,迭代器会从索引0开始每迭代一次便+1;若访问HashSet中元素,每个元素将按照某种随机的次序出现(尽管该迭代过程一定能遍历到集合所有元素,但却无法预知元素被访问的次序)。

11.3.2 相关方法

Collection 接口拓展了Iterable接口,获取迭代器的方法为:

  • public Iterator<E> iterator():返回一个实现了Iterator接口的对象,可以使用该迭代器对象依次访问集合中的元素。

Iterator接口包含以下常用方法:

  • public E next():调用next时,迭代器会越过下一个元素,并返回刚刚越过的那个元素的的引用。当到达集合末尾时next方法会抛出NoSuchElementException

  • public boolean hasNext() :如果迭代器对象还有多个供访问的元素,返回true。反之返回false

    public class JustTest {
        public static void main(String[] args) {
            Collection<String> arr = new ArrayList<String>();
            arr.add("Luffy");
            arr.add("Nami");
            arr.add("Zoro");
            Iterator<String> it = arr.iterator();
            while(it.hasNext()){
                String tmp = it.next();
                System.out.println(tmp);
            }
        }
    }
    
  • public void remove() :删除上次调用next方法时返回的元素。(也就说,想要删除指定位置上的元素,仍然需要越过该元素)。当调用remove之前没有调用next是不合法的,抛出IllegalStateException异常

    Iterator<String> it = c.iterator();
    it.next(); //越过第一个元素
    it.remove(); //将刚越过的元素删去
    

应该将Java迭代器认为是位于两个元素之间的。C++标准模板库中迭代器是根据数组索引建模的,不需查找元素即可将迭代器向前移动一个位置。而Java不同,查找操作与位置变更紧密相连,查找一个元素的唯一方法是调用next,而在执行查找操作同时迭代器位置随之向前移动。

11.3.3 for each 循环

编译器将for each循环翻译为带有迭代器的循环,故使用for each循环遍历时,不能对集合元素进行增删操作。

for(元素的数据类型 变量名 : Collection集合或者数组){
    //do something
}
public class JustTest {
    public static void main(String[] args) {
        Collection<String> arr = new ArrayList<String>();
        arr.add("Luffy");
        arr.add("Nami");
        arr.add("Zoro");
        for(String tmp: arr){
            System.out.println(tmp);
        }
    }
}

11.4 List 接口与其子类

List 作为 Collection 集合的子接口之一,它用于描述一个有序集合,并且集合中每个元素的位置十分重要。它有两种访问元素的协议:使用迭代器,或用 getset 方法随机地访问每个元素(不适用链表,适用数组)。

11.4.1 数组列表(ArrayList )

java.util.ArrayList 封装了一个动态再分配的对象数组。元素增删慢,查找快。

ArrayList 方法不是同步的,故建议在不需要同步时使用ArrayList,比使用Vector更高效。

11.4.2 链表(LinkedList)

链表将每个对象存放在独立的节点中,每个节点还存放着序列中下一个节点的引用。而在Java中,所有链表实际上都是双向链表doubly linked)的。而java.util.LinkedList集合数据存储的结构即为双向链表,元素增删块,查找慢。

以下为LinkedList常用方法的举例使用:

import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

public class LinkedListTest {
    public static void main(String[] args) {
        List<String> staff = new LinkedList<String>();
        //调用LinkedList的add方法:将对象添加至链表尾部
        staff.add("Luffy");
        staff.add("Zoro");
        staff.add("Sanji");

        //遍历LinkedList
        ListIterator<String> iter_1 = staff.listIterator();
        while(iter_1.hasNext()){
            String tmp = iter_1.next();
            System.out.println(tmp);
        }

        ListIterator<String> iter_2 = staff.listIterator();
        //越过第一个元素,并返回该元素至oldName
        String oldName = iter_2.next();
        //调用ListIterator的Set方法,用一新元素取代调用next或previous方法返回的上一个元素
        iter_2.set("Monkey.D.Luffy");
        //调用子接口ListIterator的previous方法,返回越过的对象

        if(iter_2.hasPrevious()) iter_2.previous();
        //调用子接口ListIterator的add方法(注意区分上面的Collection的add方法),将对象添加至迭代器位置之前
        iter_2.add("Nami");
    }
}

由于迭代器是描述集合中位置的,故这种依赖位置的add方法将由迭代器负责,只有对自然有序的集合使用迭代器添加元素才有实际意义。

11.5 Set 接口与其子类

Set 作为 Collection 集合的子接口之一,它由散列表所实现,由于散列将元素分散在表的各个位置上,故其Set接口中元素是无序的(TreeSet 除外),并以某种规则保证存入元素是不重复的。

11.5.1 基于散列表的集(HashSet)

如果想要查看某个指定的元素,但不在意元素的顺序,散列表(hash Table)正能够满足于此。在散列表中,具有不同数据域的对象产生了不同的散列码hash code)。

HashSet类,正实现了基于散列表的集。它根据对象的散列码来确定元素在集合中的存储位置,它保证元素唯一性的方式依赖于hashCode方法与equals方法。由此,在用HashSet存放自定义类型元素时,需要覆写对象中的hashCode方法(没覆写的话每个新对象的hashCode值均不同)和equals方法(没覆写的话就是比较地址值),建立自己的比较方式方能保证HashSet集合中的对象唯一。

hashCode值是由操作系统产生的逻辑地址值,而地址值才是真实的物理地址。

/* Student.java文件中 */
public class Student{
    private String name;
    private int age;
    
    public Student(){
    }
    public Student(String name, int age){
        this.name = name; this.age = 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; }
    
    @Override
    public boolean equals(Object o){ //覆写equals方法
        if(this == o) return true;
        if(o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, name);
    }
    @Override
    public int hashCode(){ //覆写hashCode方法
        return Objects.hash(name, age);
    }
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

/* 测试类 HashSetTest.java */
public class HashSetTest{
    public static void main(String[] args){
        HashSet<Student> MySchool = new HashSet<Student>();
        MySchool.add(new Student("Luffy", 18));
        MySchool.add(new Student("Nami", 23));
        MySchool.add(new Student("Luffy", 18));
        for(Student peo : MySchool){
            System.out.println(peo);
        }
    }
}
/* 运行结果 */
Student{name='Nami', age=23}
Student{name='Luffy', age=18}

11.5.2 树集(TreeSet)

TreeSet类与散列集十分类似,但它比`散列集有所改进。树集是一个有序集合(sorted collection),它可以以任意顺序将元素插入到集合中,当在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现,(其中,排序是利用红黑树去完成的,每次将一个元素添加至树中时,都被放置在正确的排序位置上)故迭代器总以排好序的顺序去访问每个元素。

默认情况下,树集假定插入元素实现了Comparable接口(若元素为自定义对象,则需实现Comparable接口自定义排序)。对于基本类型的包装类和String默认为升序。

总而言之,将一个元素添加到TreeSet中要比添加到HashSet中会慢一些,不过TreeSet可自动对元素进行排序。

public class TreeSetTest {
    public static void main(String[] args) {
       Set myset = new TreeSet();
       myset.add(233);
       myset.add(666);
       myset.add(1);
       myset.add(42);
       myset.add(233);
       for(Iterator iter = myset.iterator(); iter.hasNext();){
           System.out.println(iter.next());
       }
    }
}

11.6 Map 接口

集(set)是一个集合,可以快速查找现有元素,但要查看一个元素,需要查找元素的精确副本,但这并不是非常通用的查找方式。

通常,我们知道某些键的信息,并想要查找与之对应的元素,映射表(map)数据结构就是为此实现的。映射表用来存放键/值对,若提供了键,就能查到值。

Java类库为映射表提供两个通用实现:

  • HashMap :散列映射表,对键进行散列。
  • TreeMap :树映射表用键的整体顺序对元素进行排序。

尽管HashTable已被取代,但其子类Properties仍未退出历史舞台,在I/O流章节中会专门讲述。

键必须是唯一的,不能对同一个键存放两个值,每个键只能对应一个值。

注意:

  1. 使用HashMap存放自定义对象时,要保证对象唯一,必须覆写对象的hashCodeequals方法。
  2. 若希望保证map存放的key和取出的顺序一致,可使用java.util.LinkedHashMap集合来存放。

11.6.3 Map常用方法

  • public V put(K key, V value):将指定的键key与值value添加至Map集合中。若指定的键key在该集合中没有,则将指定的键值添加到集合中,并返回null;若指定的键key在集合中存在,则返回值为集合中键对应的值(指替换前的值),并将指定键所对应的值替换为指定的新值value
  • public V remove(Object key):将 指定的键key 所对应的 键值对元素 在Map集合中删除,同时返回被删除元素的 值 。
  • public V get(Object key):在Map集合中获取 指定的键key 所对应的值。
  • public Set<K> keySet():获取Map集合中所有的 键 ,存储到Set集合中。注意,keySet既不是HashSet也不是TreeSet
  • public Set<Map.Entry<K, V>> entrySet():获取Map集合中所有的键值对对象的Set集合。

11.6.2 Map的遍历方式

一、键找值方式

获取Map中所有键,由于键是唯一的,利用keySet方法得到一个存储所有键的Set集合,再遍历该Set集合的每一个键,根据键调用get()方法来获取键所对应的值。

HashMap<Integer, String> mymap = new HashMap<Integer, String>();
mymap.put(20190301, "Luffy");
mymap.put(20190304, "Nami");
mymap.put(20190302, "Zoro");
Set<Integer> mykeys = mymap.keySet();
for(int curkey: mykeys){
    String curname = mymap.get(curkey);
    System.out.println(curkey + "所对应的学生姓名为:" + curname);
}

二、键值对方式

Map中的keyvalue是一一对应的关系,而每一个键值对又称为Map中的一个Entry),Entry 将键值对的对应关系封装为对象,称为键值对对象,或称为 条目

在遍历Map集合时,就可从每一键值对Entry对象中获取对应的键与值。对于Entry,也提供了相应的方法:

  • public K getKey():获取Entry对象中的键。
  • public V getValue():获取Entry对象中的值。
  • public Set<Map.Entry<K, V>> entrySet():获取Map集合中所有的键值对对象的集合。
HashMap<Integer, String> mymap = new HashMap<Integer, String>();
mymap.put(20190301, "Luffy");
mymap.put(20190304, "Nami");
mymap.put(20190302, "Zoro");
Set<Map.Entry<Integer, String>> mySet = mymap.entrySet();
for(Map.Entry<Integer, String> a : mySet){
    System.out.println(a.getValue() + "的学号为:" + a.getKey());
}
posted @ 2020-11-05 00:43  J_StrawHat  阅读(78)  评论(0编辑  收藏  举报