【java基础】第七章 对象的容纳

#数组中只有一个整型只读成员length,记录了数组中元素的个数。

动态初始化:int a[]=new int [100];

静态初始化:int a[]{1,2,3,4,5}

如果数组使用时出现下标越界,C和C++会正常执行,从而导致接收到不可预知的数据。

java则会产生一个运行时异常。由于需要检查每个数组的访问,所以导致浪费时间并产生多余的代码。

多维数组静态初始化:int a[][]={{1,2},{1,3},{1,4}};

java实际上没有多维数组,只有一维数组,对于多维数组,其每一个元素实际上都是一个低维数组的引用,直到其指向一维数组为止。如下图:

对象存在内存堆(heap)中

#数组和数组引用示例

扫描二维码关注公众号,回复: 5484410 查看本文章
public class ArrayPara{
    public static void changeArrayValue(int para[]){
        para[0]=2;
}
    public static void changeArrayRef(int para []){
        int temp[]={1,2,3};
        para=temp;
}
public static void main(String []args){
        int a[];
        a=new int []{99,100};
        changeArrayValue(a);
        System.out.println(a[0]);              //2
        changeArrayRef(a);
        System.out.println(a[0]);                //2
}
}

#数组的工具类Array

Java在java.util类库中定义了Arrays工具类,他又一套static方法,提供了操作数组的实用功能;

copyOf方法常用来为数组扩容。

sort()对基本类型使用优化的快速排序方法。

#对象的比较

java提供了两个用于比较大小的接口,凡是实现了某一个接口的对象,都具备比较大小的能力,

一,Comparable接口

java.lang.Comparable接口中只有一个方法:int compareTo(Object o)

对象o是被比较对象,大于,等于,小于o,分别返回正整数,0或负整数。

可以调用sort()方法对实现该接口类的数组排序。

一个实现了Comparable接口的比较类

class Employee implements Comparable{
    int id;
    String name;
    public int compareTo(Object o){
        Employee e=(Employee) o;
        return this.id-e.id;
}
}

程序说明:java眼中,该类可以比较大小,当调用sort方法时,方法自动调用compareTo()方法比较大小并按id值从小到大排序。如果类没有实现compareTo()方法,则该类无法比较大小,此时调用sort()会抛出异常。

二,Comparator接口

和上个接口比,使用方法和领域都不同,

java.util.Comparator接口中只有一个方法:int compare(Object o1,Object o2)

o1和o2比较,大于,等于,小于o,分别返回正整数,0或负整数。

凡是实现了Comparator接口的类就称为了一个比较器,拥有比较两个对象大小的功能。

两者相比,Comparator拥有更优良的模块性,Comparator可以看作时一种算法的实现,将算法和数据分离。Comparator也可以在下面环境中使用:

①类的设计者没有考虑到比较问题而没有实现Comparable,可以通过Comparator来实现排序而不必改变对象本身。

②通过构造不同的比较器,可以实现多种排序标准,比如升序,降序等。

下例完善了上例类中的比较功能:

class DescComparator implements Comparator{           //降序比较器
    public int compare(Object o1,Object o2){
        Comparable c1=(Comparable) o1;
        return 0-c1.compareTo(o2);
}
}

程序说明:若想对上例类按降序排列,可以使用DescComparator类作为比较器,从第四行代码看出他将类中的Comparable接口方法取反,从而使得其排序结果于Comparable排序结果相反。此时可执行下列语句进行降序排序:

Array.sort(employees,new DescComparator());   //employees是一个Employee集合。

Array.sort()方法使用比较器需要两个参数,一个参数为被比较对象,第二个参数为比较器引用。这里的比较器DescComparator可以对所有实现了Comparable接口的类进行其各自的降序排序,而不仅仅局限于Employee类,从此可以看出,Comparator模块化的优点,它在有更强的通用性的同时,也简化了代码。

#枚举

枚举是定义一组值的对象,他可以包括0个或多个成员。枚举对象的值都会自哦对那个获得一个数字值,从0开始一次递增。创建枚举类型用“enum”关键字,隐含了所创建的类型都是java.lang.Enum类的子类(java.lang.Enum是一个抽象类)。

eg定义一个季节枚举类型

public enum Season{SPRING,SUMMER,FALL,WINTER};

然后就可以声明一个Season类型的变量代码如下:

Season s=Season.SPRING;

Season类型变量只能保存此类声明时给定的某个枚举值或null值。

枚举的使用:

public class TestEnum{
    public enum Season{SPRING,SUMMER,FALL,WINTER};
    public static void main(String []args){
        seasonOutput(Season.SPRING);
    }
    private static void seasonOutput(Season s){
        switch(s){
            case SPRING:
                System.out.println("春");break;
             case SUMMER:
                System.out.println("夏");break;
             case FALL:
                System.out.println("秋");break;
             case WINTER:
                System.out.println("冬");break;
            default:
                System.out.println("default");
}
}
}

所有枚举类型都是Enum类的子类,自然继承了此类的一些方法,比较常用的有toString()和values();toString()返回枚举常量名,如Season.SPRING.toString()返回字符串“SPRING".value()方法返回一个包含所有枚举值的数组,如下:

Season []seasons=Season.values();

枚举不是用的越多越好,如果那样不如直接用类,仅当所定义元素有一定实际含义时枚举才有优势。

#容器

容量动态变化,只能保存对象,不能保存基本类型

常用容器类和容器接口示意图:

图中虚线框代表”接口“,实线框代表普通(实现)类。实线箭头代表继承,虚线箭头代表实现接口。实心箭头表示一个类可以生成箭头指向的那个类的对象。例如,任何类集(Collection)都可以生成一个迭代器(Iterator),而一个列表(List)可以生成一个ListIterator(以及原始的Iterator,因为List是从Collection继承的)。

图中主要包括三个接口:Map,List,Set,而且每个接口都有两三个常用的实现类。

#List列表;

List从Collection继承,下面是Collection接口的声明:

public interface Collection<E> extends Iterable<E>

Collection接口从Iterable接口继承,代表所有实现Collection接口以及Collection子接口的类都可以使用迭代器。

Collection(类集)接口的常用方法:

List是Collection的子接口,是有序的Collection。List在Collection基础上新增了大量方法,以便在List中插入和删除元素。如下图:

由上图可以看出,List新增的方法大部分都与”位置“有关,后文将这种位置称为元素的”索引"(index)。

实现List接口的常用类:ArrayList(线性表),LinkedList(链表)等

#ArrayList线性表

ArrayList实现了容量大小可变的数组,具有数组快速存取的优点,同时容量又可以动态变化,因此也称为动态数组。实现了线性表这一数据结构。

注意:所有容器都不能保存基本类型,可以先把基本类型变成封装类,然后用容器保存封装类。

ArrayList的构造方法

为什么构造一个线性表需要指定其容量?因为ArrayList在其内部维护了一个Object类型的数组,任何操作都是对这个数组的操作,新建一个ArrayList的同时就创建并初始化了一个这样的数组。因此就必须在创建一个ArrayList时决定此数组的容量。对不指定参数构造方法的线性表,默认容量为10。若不断向ArrayList中添加元素,当超出容量时,就会申请一个容量为原来容量两倍的新数组,再把原数组的所有数据迁入新数组。所以正确地预估容量可以提高ArrayList的使用效率。

可以使用接口引用ArrayList

List a=new ArrayList();

a.add(,,,);

a.remove(.....);

第一行代码将生成的线性表对象实例赋给一个接口引用变量a是一个向上转型的过程,这样做有一个好处:一开始选择ArrayList这种结构,但随着编程深入可能觉得使用LinkedList更加合适,这时只需要对第一行代码做如下修改:

List a=new LinkedList();

而后面的代码可以完全不用改变,从而增强代码可维护性。这种用法很常见,这是面向对象接口与实现分离的一个典型应用,在后文中,只要不是用到某些类的特殊方法,都会尽量用接口来定义类。

#LinkedList链表

LinkedList在其内部维护了一个带头节点的双向链表,可以方便地进行双向访问,

LinkedList在List接口基础上增加方法:

#泛型

由于对所有容器类来说,插入其中的所有对象都被当做Object类处理,因此可以向一个ArrayList中插入Cat类,Dog类,String类等其他类。因为所有类插入容器前都强制类型转换为Object类,这就产生了一些问题。

①当从一个线性表中取出元素并将其当String使用时,必须将get()到的结果强制转换为String,增加了代码的复杂度和出错几率。

②有可能在以后使用a列表的过程中误将其他类的对象如Cat插入到a列表中,当把Cat对象强制转换为String时会出错。(抛出ClassCastException)

有时需定义一个容器只保存单一类型,为此,java提出泛型机制,java中许多重要的类,如本章的容器类,都是已经泛型化的。泛型的主要目的就是要提高Java程序的类型安全,通过对泛型定义的变量进行类型上的限制,编译器可以在更高层次上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(假设列表a只保存String类型,实际未必)。通过在变量声明中捕获这一个附加的类型信息,泛型允许编译器实施这些附加的类型约束。类型错误就可以在编译时被捕获了,而不是在运行时当做ClassCastException显示出来。将类型检查从运行时挪到编译时有助于找到错误,并可提高程序的可靠性。消除强制类型转换,使代码可读性增强,并减少了出错几率。

一、泛型语法。

泛型(generics)是对java语言的类型系统的一种扩展,以支持创建可以按类型进行参数化的类。他可以消除强制类型转换,同时获得一个附加的类型检查层,该检查层可以防止将错误类型的对象保存到集合中。

声明泛型类的变量时,使用尖括号<>来指定形式类型参数,形式类型参数于实际参数之间的关系类似于方法形式参数(形参)与方法实际参数(实参)之间的关系,只是类型参数表示的是类型。java泛型不支持基本数据类型,这一点和c++不同。

泛型可用于类和接口:

List<String> a=new ArrayList<String>();

这行代码表示a只能保存String或String子类类型的元素,也可以说只能保存可以强制类型转换为String类型的元素。

注:在本例中,定义带泛型的列表a必须指定两次类型参数,一次是在声明变量a的类型为List时,另一次时在选择ArrayList类的参数化以便可以实例化正确类型的一个实例时。

除了异常类型、枚举或匿名内部类以外,任何类都可以使用泛型。

二、泛型方法

方法也可以被泛型化,不管定义他们的类是不是泛型化的。之所以声明泛型方法,一般是因为想要在该方法的多个参数之间声明一个类型约束。

eg下面代码中的 testGen()方法,根据他的第一个参数的bool值,他将返回第二个或第三个参数。

public <T> T testGen(boolean b,T first,T second){
    return b ? first : second;
}

可以调用testGen方法,而不用显示地告诉编译器T代表什么类型;编译器值知道这些值的类型都必须相同。

编译器允许调用下面代码,因为编译器可以使用类型推理来推断,替代T的String或Integer满足所有的类型约束;

String s=testGen(true,"a","b");
Integer i=testGen(flase,new Integer(1),new Integer(2));

但是,编译器不允许调用下面代码,因为没有类型会满足所需的类型约束:

String s=testGen(true,"pi",new Float(3.14));

三、容器的泛型

所有的标准容器接口都是泛型化的,如下图:

类似地,容器接口的实现都是用相同类型参数泛型化的,所以ArrayList<E>实现List<E>等。容器类也使用泛型的许多“技巧”或”方言“。例如,接口Collection<E>中的add()方法的声明如下:

interface Collection<E>{

    boolean add(E e);

}

Collection接口中的E代表类集中元素的类型,Map接口中的K,V代表映射中键与值的类型。这些字母只是代表某个类型的符号,可以任意指定。

Comparable接口也是泛型化的,所以实现Comparable的对象能声明它可以与什么类型进行比较。(通常是对象本身的类型,但有时也可能是其父类)

public interface Comparable <E>{

    public int compareTo(E e);

}

这意味着如果声明一个实现Comparable的类,比如String,就必须不仅声明该类支持比较,还要声明它可与什么类型比较(通常是与其本身类型比较。

public class String implements Comparable<String>{}

四、泛型的使用

eg利用LinkedList类构造泛型化的栈结构

import java.util.LinkedList;
class StackL <T>{
    private LinkedList<T> list=new LinkedList<T>();
    public void push(T t){
        list.addFirst(t);
    }
    public T top(){
        return list.getFirst();
    public T pop(){
        return list.removeFirst();
    }
}
public class StackGen{
    public static void main(String [] args){
        StackL<String> s=new StackL<String>();
        s.push("cat");
        s.push("dog");
        s.push("monkey");
        s.pop();
        System.out.println(s.top());                 //dog
    }
}

#Set 集合

接口声明如下

public interface Set<E> extends Collection <E>

Set是数学上集合的数据结构。不允许保存相同值的元素;也没有顺序的概念。

Set与List不同,没有额外方法,所有方法都是从Collection中继承的。Collection与Set相比只是没有所有元素必须不同的约定。

Set接口的几种常用实现类包括HashSet,TreeSet,LinkedHashSet。

Set的简单使用:

Set<String> set=new HashSet<String>();
set.add("cat");
set.add("dog");
set.add("cat"):
set.remove("cat");
System.out.println(set);   //[dog]

一、Iterator迭代器。

实现集合(Set)的遍历;

public interface Iterator<E>  //E是迭代器遍历的数据类型

迭代器是一种设计模式,用于将集合排成一个序列,遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构。迭代器被看作“轻量级”对象,也就是创建它只需付出极小代价。也正是如此,迭代器存在限制,如,基本迭代器只能单项移动。

每一种集合返回的迭代器Iterator的具体类型可能不同,Array可能返回ArrayIterator,Set可能返回SetIterator,Tree可能返回TreeIterator,但是他们都实现了Iterator接口,因此程序不关心到底是哪种Iterator,它只需实现了这个Iterator接口即可,这就是面向对象接口与实现相分离的特点。

迭代器的常用方法:

1.调用方法iterator()要求容器返回一个Iterator类型的对象。第一次调用Iterator的next()方法时,他返回序列的第一个元素。注意:

Iterator()方法来自java.lang.Iterator接口,被Collection继承。

2.使用next()方法获得序列的下一个元素,没成功调用一次迭代器向后移动一个元素。

3.使用hasNext()方法检查序列中是否还有元素。

4.使用remove()方法将迭代器新返回的元素删除。

Iterator模式总时用同一种逻辑来遍历集合,所有的Collection都可以生成自己类型的迭代器。使用迭代器遍历一个List的方式如下:

List<String> a=new ArrayList<String>();   //Set<String> a=new HashSet<String>():
.....
for(Iterator<String> it=a.iterator();it.hasNext(); ){
    .....
    String s=it.next();   //取出元素
    .....
}

通过上述实例得出迭代器的操作过程可以概况为:通过iteratro()方法获得指向集合开始出的迭代器;用hasNext()方法作为循环条件,直到无元素循环终止;循环时,用next()方法获取每个元素。

使用迭代器遍历数据集有一个好处,对不同的容器,只需改变集合类型声明语句而后面的遍历代码无需更改.(将List改为Set如上面代码)

迭代器把存取逻辑从不同的集合类中抽象出来,从而避免向用户暴露集合内部结构,使得代码复用性增强。用户从不直接和集合类打交道,通过控制Iterator,向他发送“向后”,“取当前元素”的命令,就可以直接遍历整个集合。因此在编写代码时,一旦涉及遍历,就可以用迭代器增强代码的健壮性与复用性。

ListIterator从Iterator继承的接口,可以从两个方向遍历List,也可以在List中插入和删除元素。ListIterator新增方法如下:

ListIterator一般只对LinkedList或其他链表结构使用,因为LinkedList维护的时双向链表,取前驱和后继这样的顺序操作是已经实现的。

ArrayList由于自带随机存取功能,即利用索引存取,无需使用此迭代器。

二、for-each循环

迭代器比较繁琐,JDK1.5之后提供了更简洁遍历数组和Collection的方法:

for(变量类型 变量名:集合名){操作}

Set<String> set=new HashSet<String>();
.....
for(String s:set){
    System.out.println(s);

}

int a[10];
for(int b:a){System.out.println(b);}

for-each实际就是把迭代器包装了一下,使代码更简洁。

只能遍历数组和实现了java.lang.Iterator接口的实例。

Iterator接口中声明的方法只有一个-Iterator()方法,它返回一个实现了java.util.Iterator接口的对象。因此List和Set的实现类都可以使用for-each遍历。

三、散列集

向Set中添加元素要比较与已有元素是否相同,效率低下。散列表(hash table)是一种可以快速查找特定对象的数据结构。散列表可以对每一个对象计算一个整数,称为散列码(hash code)。这个整数可以经过一些变换生成对象保存的地址。也就是说给出一个对象,利用散列算法可以迅速计算出这个对象的位置从而跳过繁杂的循环比较过程达到高速查找的目的。HastSet正是实现了这种数据结构的Set实现类。

HashSet的构造方法:

HashSet实现了Set接口,是最常用的Set接口实现类。因此,HashSet也不允许保存相同元素。HashSet的使用和List无区别。

import java.util.*;
public class HashSetTest {
    public static void main(String []args){
        Set<Integer> set=new HashSet<integer>();  //保存整数的散列集
        for(int i=0;i<100;i++){
            Integer temp=new Integer(i%6);
            set.add(temp);
        }
        for(Iterator <Integer>it=set.iterator();it.hasNext();){
            System.out.print(it.next()+",");
        }
    }
}
//输出结果:0,1,2,3,4,5,

程序说明:该例下散列集中插入0到100所有数除以6的余数,最后只插入了6个数字,其他相同的数字因为散列集中以存在而无法插入。无法插入时不会报错,但此时add()方法返回值为false;

java中散列表使用链表数组实现如下图所示:

下面通过向HashSet添加元素的过程来介绍散列集的实现机制,当向HashSet中添加元素a时,首先调用a.hashCode()方法获得其散列码,从而计算其在数组中的位置,即插入哪个链表。定位到某个链表后,接下来调用a.equals()方法用a的值和此链表中每个元素比较。如果有相同元素,a将被视为以添加过,而不再重复加入链表。否则将a加入链表头。

知道add()机制后,查找的原理是一样的,直接根据数据的散列码和散列表的数组大小计算除余后,就得到了所在数组的位置,然后再查找链表中是否有这个数据即可。查找的代价也主要是在链表中,但是一条链表中的数据很少,有的甚至没有。此时循环的代价很小,所以散列法的查找效率较高。

不同的散列码肯计算相同的散列值,称为冲突。

Java默认的容量大小全部都是2的幂,初始值为16(24),即有16个链表。假如16条链表中的75%有数据,则认为达到默认的加载因子0.75.达到默认加载因子后,HashSet开始重新散列,也就是将原来的散列结构完全抛弃,重新开辟一个散列单元大小为32(25)的散列结构,并重新计算各个数据的存储位置。以此类推。设计合适的容量和加载因子对散列集的效率影响较大。

看下面例子:

import java.util.Iterator;
import java.util.Set;
import java.util.HashSet;
class Employee{
    int id;
    public Employee(int id){
        this.id=id;
    }
}
public class HashSetNoHashCode{
    public static void main(String []args){
        Set <Employee> set=new HashSet<Employee>();
        set.add(new Employee(12));
        set.add(new Employee(1)):
        set.add(new Employee(1));
        for(Iterator<Employee>it=set.iterator();it.hasNext();){
            Employee temp=it.next();
            System.out.println("ID"+temp.id+", hashcode="+temp.hashCode());
        }
    }
}

输出结果:ID=12,hashcode=33263331

ID=1,hashcode=6413875

ID=1,hashcode=21174459

为什么可以存储两个工号为1的员工?
因为在Object中使用hashCode()方法来得到散列码。基本上每一个对象都有一个默认的散列码,其值对应对象的内存地址。一次虽然两个员工id相同,但因为是两个对象,保存在不同位置,有不同散列码。在加入到HashSet的时候就有可能加入到不同的链表中,被当作不同元素看待。即使因为巧合被加入到同一链表,java默认的equals()方法比较的也是两对象的地址,返回的也是false。同样会被当做不同的元素正常添加。但为什么Integer类的HashSet可以区分相同的值呢。这是因为封装类,String类的hashCode()与equals()方法都是重写的。他们的hashCode()方法和equals()方法都是由他们的内容决定的。比如,两个String的字符串字面值相同,他们的hashCode()方法返回结果相同,equals()方法返回true。

根据以上分析,添加到HashSet的对象需要恰当的重写equals()方法和hashCode()方法。

对Employee类改写:

class Employee{
    int id; 
    public Employee(int id){
        this.id=id;
    }
    public int hashCode(){
        return this.id;
    }
    public boolean equals(Object o){
        Employee e=(Employee) o;
        if(this.id==e.id) return true;
        else return false;
    }
}

注:内容相等的两个对象,他们的散列码也一定相同。

两个对象的散列码不同,则两个对象一定不相等。

四、其他集合

1.TreeSet是实现“红黑树”(自平衡排序二叉树)数据结构后得到的顺序Set。红黑树每个结点的值都大于等于左子树,小于等于右子树,这能确保红黑树运行时可以快速地在树中查找和定位到所需结点。

TreeSet添加元素、取出元素的性能都比HashSet低。当TreeSet添删查时,需要通过比较找到新增元素的插入位置,因此比HashSet慢;但TreeSet的优势在于,所有元素总是根据指定排序规则保持有序状态。

一旦涉及排序,说明元素是可比较的。这就要求TreeSet中的对象实现了Comparable接口或者传递给TreeSet一个Comparator比较器。另外,由于TreeSet是一个Set,那么它也应该重写equals()方法以保证元素单一性。那么就要保证当equals()返回true,当且仅当comparaTo()或compare()返回0;

2.LinkedHashSet是HashSet的子类,该类在HashSet的基础上将每一个元素用链表串联起来,可以按元素的插入顺序遍历。LinkedHashSet可以保证确定的顺序,这不但保证了存取性能常量复杂度,同时又保持了顺序的要求,比较适用。

通过下例理解不同Set的实现类的特性:

import java.util.*;
public class OtherSet  {
    public static void main(String []args){
        Set<Integer> hashSet=new HashSet<Integer>();
        Set<Integer> linkedHashSet=new LinkedHashSet<Integer>();
        Set<Integer> treeSet=new TreeSet<Integer>();
        for(int i=0;i<5;i++){
            int s=(int)(Math.random()*100);
            Integer temp=new Integer(s);
            hashSet.add(temp);
            linkedHashSet.add(temp);
            treeSet.add(temp);
            System.out.pringln("第"+i+"次随机数产生为:"+s);
        }
    System.out.println("HashSet:"+hashSet);
    System.out.println("LinkedHashSet:"+linkedHashSet);
    System.out.println("TreeSet:"+treeSet);
    }
}

输出结果:

随机数产生:46,0,27,70,61

Hashset:【0,70,27,61,46】

LinkedHashSet【46,0,27,70,61】

TreeSet【0,27,46,61,70】

注:对于TreeSet,由于Integer已经实现了Comparable接口,因此可以直接向里添加,对Set中元素按值进行排序。如果是自己定义的类,就需要手动实现Comparable或Comparator接口。

一般来说,先把元素添加到HashSet,再把集合转换为TreeSet来进行有序遍历会更快。

Set的实现原理实际上是基于map实现的。许多常用Set(如HashSet,TreeSet),其内部都维护一个相应的Map(如HashMap,TreeMap),Set的所有操作都是通过这些Map完成的,

#Map映射

映射接口实现”映射“数据结构,是维护”键(KEY)-值(value)“关系对结构的无序容器。每个键映射到一个值,可以通过一个键查找相应的值。键与值都视为对象。一个Map中不能包含相同的键,且每个键只能映射一个值。

Map的声明:

public interface Map<K,V>

构建一个键为Integer类型,值为String类型的Map:

Map<Integer,String>map=new HashMap<Integer,String>();

Map接口的方法:

Map使用实例:

Map<String,String> map=new HashMap<String,String>();
map.put("dog","yellow");
map.put("dog","red");
map.put("cat","red");
System.out.println(map);   //{cat=red,dog=red} 这里自动调用map.toString();
map.remove("cat");
System.out.println(map);   //{dog=red}

一、map遍历

Map接口没有继承Iterable接口,也没有能够构造迭代器的方法,同时Map也没有List的位置概念,无法使用索引。

Map提供三种集合的视图,一组key集合,一组value集合,一组key-value集合,map靠keySet(),values(),entrySet()方法实现遍历。

Map的三种方法:

//方法一
public static void byValue(Map<String,Student> map){
    Collection <Student> c=map.values();
    for(Iterator it=c.iterator();it.hasNext();){
        Student s=(Student)it.next();
    }
}
//方法二
public static void byKey(Map<String,Student> map){
    Set<String> key=map.keySet();
    for(Iterator it=key.iterator();it.hasNext();){
        String s=(String)it.next();
        Student value=mat.get(s);
    }
}
//方法三
public static void byEntry(Map<String,Student> map){
    Set<Map.Entry<String,Student>> set=map.entrySet();
    for(Iterator<Map.Entry<String,Student>> it=set.iterator();
    it.hasNext();){
        Map.Entry<String,Student> entry=it.next();
        System.out.println(entry.getKey()+"------->"+entry.getValue());
    }
}

程序说明:

方法一是最常规的遍历方法,使用Map的values()方法可以产生值的类集,再利用Collection的迭代器进行遍历,不过这种方法只能遍历值。

方法二利用KeySet()方法进行遍历,他的优点在于可以根据指定的key值得到对应的values值,灵活性比第一张方法更好。

方法三较复杂,但功能最强。这里使用到了Map.Entry接口。Map.Entry是Map内部定义的一个接口,专门用来保存键值对的内容,其定义为:

public static interface Map.Entry<K,V>

Map.Entry实际上就是把Map中的每一个键值对整体看成一个对象,那么映射集就成了Map.Entry类型数据的一个集合,Map.Entry接口提供了获取键,值,以及设置值等方法,

二、hashMap散列映射

Map必须保持”键“的单一性,和Set必须保持元素值的单一性类似。实际上,HashSet的实现完全是基于HashMap来进行的,在HashSet内部就维护着一个HashMap,其”键“就是要存入的对象,其”值“则是一个常量PRESENT.这样可以确保需要存储的信息只是”键“。而”键“在Map中是不能重复的,这样就保持Set中的元素不重复。

HashSet的所有方法都是通过调用它内部HashMap的方法实现的。以add()方法为例,判断元素是否添加成功,就是判断通过调用put()向Map中存入的键值对是否存在。如果返回值是常量PRESENT,说明键以存在,添加失败,如果返回值为null说明此键是头一次插入,表示添加成功。

综上所述,HashSet与HashMap本质上没有区别,只不过HashSet仅对HashMap键的部分操作,而HashMap比HashSet多出了对值的操作。

hashMap的声明:

public class HashMap<K,V>extends AbstractMap<K,V>implements Map<K,V>,Cloneable,Serializable

构造方法:

HashMap底层就是一个数组,数组中每个元素又是一个链表,同邻接表。向HashMap添加键值对时,首先调用k.hashCode()方法获得其散列码,从而计算出其在数组中的位置,即插入哪一个链表。定位到某个具体链表之后,接下来调用k.equals()方法,和此链表中每个元素比较。如果有相同键的元素,k被视为已经添加过,更新value值,否则将键值对插入链表头。

HashMap在底层将键值对当成一个整体处理,这个整体就是一个Entry对象。HashMap底层采用一个Entry[]数组来保存所有键值对,当需要存储一个Entry对象时,会根据散列算法来决定其在数组中的存储位置,再根据equals()方法决定其再该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表取出该Entry。

和HashSet方法相似,HashMap的对象对应的类也需要实现equals()与hashCode()方法。

三、其他Map

1.TreeMap

TreeMap同HashSet与HashMap关系类似。TreeSet也是利用TreeMap实现的。TreeMap和TreeSet的特性基本相同,也是基于红黑树数据结构实现。查看键或键值对时,他们会被排序(其次序有Comparable或Comparator决定)。TreeMap的特点在于,得到的结果是经过排序的。TreeMap是唯一的带有subMap()方法的Map,他可以返回一个子树。

2.LinkedHashMap是HashMap的子类,再HashMap的基础上将每一个键值对用双向链表串联起来。类似于HashMap,迭代遍历他时,取得键值对的顺序是其插入顺序,或者是最近最少使用(LRU)的次序,LinkedHashMap只比HashMap慢一点。而在迭代访问时反而更快,因为他使用链表维护内部次序,保存了键值对的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的键值对肯定是先插入的。也可以在构造时采用带参数的构造方法,按照应用进行排序。LinkedHashMap在遍历时一般比HashMap慢。不过有种情况例外,当HashMap容量很大,实际数据较少时,HashMap遍历起来可能会比LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际速度有关,和容量无关,而HashMap的遍历速度和他的容量有关。

3.WeakHashMap中使用的对象允许被释放,这是为解决特殊问题设计的。如果没有Map之外的引用指向某个键,则此键可以被垃圾回收器回收。

4.IdentifyHashMap是使用==代替equals()对键做比较的HashMap,转为解决特殊问题而设计。

5.Hashtable与HashMap类似,它继承自Dictionary类,不同的是,它不允许键值对的键(或值)为空。它支持线程的同步,及任意时刻只有一个线程能写Hashtable,因此也导致了Hashtable在写入时会比较慢。

一般情况下,用的最多的是HashMap,里面存入的键值对在取出时时随机的,他根据键的HashCode存储数据,根据键可以直接获取它的值,具有很快的访问速度。在Map中插入,删除和定位元素,HashMap是最好的选择。TreeMap取出来的时排序后的键值对。如果要按自然顺序或自定义顺序遍历键,那么TreeMap效果比HashMap好。LinkedHashMap是HashMap的一个子类。如果需要元素按照插入顺序1排列,同时有较快的查找速度,那么LinkedHashMap可以满足要求。LinkedHashMap可以用于一些比较特殊的情况,比如在构造“连接池”是要求其管理的资源按顺序排列,同时可以快速查询其中的特定资源可用性,这时LinkedHashMap结构比较合适。

总的来说,HashMap,TreeMap,LinkedHashMap结构的区别,就像HashSet,TreeSet,和LinkedHashSet之间的区别,只不过在Set中其顺序是相对与元素,在Map中其顺序是相对于元素的键。

#容器的Collection工具类

java.util.Collection类包含很多有用的方法。

1.Collection.sort()方法

根据元素的自然顺序对指定List按升序进行排序。列表中的所有元素必须实现Comparable接口,或者使用指定比较器Comparator进行特定排序。

与此相反的是shuffle()方法,打乱List中的对象顺序。

2.binarySearch()二分法查找

使用二分查找算法在排好序的List中查找指定元素并返回元素位置。

3.reverse()反转

使用该方法将List中元素按插入顺序的逆序排列。

4.fill()填充

fill()将List中所有元素替换为指定元素。

5.copy()复制。

copy()方法有两个参数,第一个是目标List,第二个是源List,将源List的元素复制到目标List,覆盖其内容。

6.min(),max()返回最值元素。

更多操作查阅API文档。

#容器的选择

完整的容器继承关系如下:

1.List的选择。

一方面,在ArrayList中进行随机访问(即调用get()和set()方法)是高效的,但同样的操作对LinkedList却是一个不小的开销,因为LinkedList只支持顺序存储,LinkedList的按索引存取实际上是用若干次顺序存储实现的。另一方面,在列表中部进行插入和删除操作对LinkedList来说比ArrayList高效。通常做法是先选择使用ArrayList作为默认List,以后若发现由于大量的插入删除造成性能降低,再考虑换成LinkedList。

2.Set和Map的选择。

HashSet在增删查改这类数据处理上的效率无与伦比,而且性能与元素的多少关系不大。这导致大多数情况选择HashSet。但是TreeSet中的元素按顺序排列,在需要元素有序的情况下还是会用到,而且由于平衡二叉树的特点,其查找速度也是比较块的,大概为log(n),比List快,但比不上HashSet。因此如果想使用TreeSet,可以先构造HashSet,再从HashSet构造TreeSet,这样比较高效。LinkedHashSet在要求有高效存取性能,同时要求数据有序的情况下比较适用。

TreeSet相对于List,有较优的添加速度,但存取速度不高。有时可以不把它作为Set使用,而作为创建顺序列表的一种途径。红黑树的本质决定它内部的元素总是顺序的,不必特意进行排序。一旦创建并赋值了一个TreeSet,就可以调用它的toArray()方法产生包含其自身所有元素的一个有序数组。随后,可以用方法Arrays.binarySearch()快速查找排序好的数组中的内容。当然,只有在HashSet不适用的时候,才会采用这种做法,因为HashSet的速度是最快的。

Map的选择和Set类似。

#Java泛型的拓展。

首先明确,java泛型是强类型检测的,泛型类型的子类型互不相关,例如Apple是Fruit的子类,但List<Apple>却不是List<Fruit>的子类。然而,有些时候,我们却希望能够向普通类那样使泛型类型也具有面向对象的特性如:向下转型一个泛型对象,向上转型一个泛型对象。

由于泛型子类互不相关,原本有继承关系的对象不能像普通类那样向上转型。

List<Apple> apples0=new ArrayList<Apple>();
List<Fruit> fruits0=new ArrayList<Fruit>();
fruits0=apples0;             //无法赋值,不可向上转型
apples0=(List<Apple>)fruits0;           //无法赋值,也不可以强制向下转型。

为了使泛型类型具有面向对象的继承关系,java引入了通配符的概念。

一、无界通配符“?”

为了使泛型的子类型仍然具有相关性,也就是在使用了泛型后依然能够保持继承关系,可以使用无界通配符“?”

List<?> apples=new ArrayList<Apple>();
List<?> fruits=new ArrayList<Fruit>();
List<?> cats=new ArrayList<Cat>(){};
fruits=apples;
apples=fruits;
cats=fruits;
fruits.add(new Apple());             //无法赋值
apples.add(new Apple());            //无法赋值

程序说明:使用通配符后,可以实现向上转型或向下转型,即可以将持有Apple的List对象赋值给持有Fruit的List引用,反之亦可。然而第六行代码显示,因为使用了无界通配符,泛型限制功能被削弱了,甚至可以将一个持有Fruit类型的List对象赋值给一个持有Cat类型的List引用,而Fruit与Cat没有继承关系。为什么会出现这种情况呢?因为List<?>在功能上等价于List<?extends Object>,这意味着编译器在运行时会将泛型擦除成Object上界,即List<? extends Object>能够接受Object类型本身或Object子类的一个特定类型。既然如此,编译器就不会检查List中容纳的到底是什么类型的对象。

同时,初始化后,List<?>只能容纳初始化后的那一种特定类型,是一个同构集合,List<Object>可以容纳任意Object类型或Object子类的对象,是一个异构集合。

二、通配符上界“? extends T"

以上无界通配符的例子中,Fruit与Cat并没有继承关系,但是用List<?>仍然可以把一个List<Fruit>的对象赋值给List<Cat>类型的引用,这并不是我们期望的。

使用通配符上界”? extends T"使List<Apple>类型的对象只能赋值给与之类型相关的引用。

List  <Apple> apples=new ArrayList<Apple>();
List <? extends Fruit> fruits=new ArrayList<Fruit>();
List<Cat> cats=new ArrayList<Cat>();
fruits=apples;
fruits=cats;         //无法赋值
fruits.add(new Apple());      //无法赋值
Fruit fruit=fruits.get(0);     //ok
apples=fruits;          //无法赋值
apples=(List<Apple>)fruits;

程序说明:程序第二行的"? extends Fruit“表示可以接受的类型的上界为Fruit类型,即只能接受Fruit类或Fruit的子类。例如程序第四行List<?>类型的引用自动向上转型地接受了一个List<Apple>类型的对象,编译通过。有了”?extends T",就确保了只能接受T或T的子类(向上转型),Cat不是Fruit的子类,因此当前程序的第五行代码无法通过编译。程序第六行不能通过编译,是因为“?extendsT”通配符告诉编译器在处理一个类型T或T的子类型,但是不知道这个子类型究竟是什么;由于没法确定类型,为了保证类型安全,java不允许在fruits中再添加任何类型的数据,除了null。第七行通过,说明List<? extends T>是一个只读的List。第八行出错,第九行通过,说明通过强制类型转换(直接复制的自动向下转型方式不可行),可以先将先前向上转型为List<?extends Fruit>类型的对象重新强制向下转型为List<Apple>类型的对象(但具有一定安全风险)

实际上,“?extends T”的机制本质上实现的是泛型的自动向上转型,他大多时候用在方法传参上,而不仅仅是转型为一个不能扩充的List对象。假设有以下类继承关系:一个抽象类Shape包含了一个抽象方法draw(),非抽象类Circle和Rectangle都继承了Shape,并且实现了draw()方法。现在有一个Canvas类的drawAll()方法想要画出所有继承和实现了Shape类的图形。程序清单:

abstract class Shape{
    public abstract void draw();
}
class Circle extends Shape{
    public void draw(){.....};
}
class Rectangle extends Shape{
    public void draw(){.....};
}
class Canvas {
    //Canvas中的drawAll()方法想画出所有形状,假设这些形状都已存放在一个List中,
    public void drawAll(List<Shape> shapes){
        for(Shape s :shape){
            s.draw();
        }
    }
}

程序第十四行,泛型规则导致drawAll()方法只能接受持有Shape对象的List,虽然我们想要drawAll方法能够接受持有Circle和Rectangle对象的List,即接受任何一个持有shape类型或其子类的对象的List。但遗憾的是普通泛型T是强制类型检测的,只能接受单一类型。此时<?extends T>可以发挥作用:对程序做如下修改:

public void drawAll(List <? extends Shape>shapes){

    for(Shape s:shapes){

        s.draw();

}}

修改后,drawAll()方法可以接受Shape的子类型的List。

三、通配符下界:“?super T”

先看例子:

List <Fruit> fruits=new ArrayList<Fruit>();
List <? super Apple> apples=new ArrayList<Apple>();
List <Cat> cats=new ArrayList<Cat>();
apples=fruits;
apples=cats;  //无法赋值
apples.add(new Apple()); //OK
apples.add(new Fruit());  //无法赋值
Apple apple=apples.get(0);  //无法赋值

程序说明:"? super Apple"表示可以接受·的类型的下界为Apple类型,即只能接受Apple类转型型或Apple的父类型,本质上实现的是泛型的自动向下转型,如程序的第三行是一个泛型自动向下转型的过程 。同样的,因为编译器不知道其所持有的数据具体是什么类型,但是可以确定的是其一定是Apple类型或Apple类型的父类型,所以指定了“?super Apple”类型的容器可以添加Apple子类,既可以向List <? super Apple>中添加Apple类型或Apple的子类型,但不可以添加Apple的父类型。例如第六七行。

有下界限制的通配符“? super T”使得持有T类型或者其子类型对象的容器是可写入的,但是由于编译器不知道到底往容器里加入的是何种类型的对象,只能保证是个Object类型的对象,也就是没办法有效地自动或取对象的类型,(除非进行强制类型转换,但同样具有一定的安全风险),例如程序第八行。

List<? super T>无法自动获取所存数据类型的特点使它看起来作用有限,但实际上同List<? extends T>类似,List<? super T>在方法传参中被大量使用。例如在对象的比较中,类Fruit实现了Comparable接口,即Fruit类的对象集合是可以排序的,但是我们希望所有继承Fruit类的子类对象集合也能够排序,如何通过泛型方式达到目的呢?

程序清单:

public static <T> List<T> higherPrice(List <T> A){
    Collections.sort(A);   //编译出错
    return A;
}

注:pulic static <T> List<T> 方法名(){}   //这是未指定List内对象类型的返回List对象方法的定义

public static List<类名> 方法名(){}  //这是指定List内对象类型的返回List对象方法的定义

程序说明,以上通过普通的泛型实现的程序,显然不能实现我们的目标(期望任何实现了Comparable的接口的类的子类对象集合也能够进行排序)。程序第二行代码编译错误,因为T是一个未知类型,方法higherPrice()并没有明确限定类型T需实现Comparable接口,但是sort方法需要实现Comparable接口,而Java本身不进行泛型查找绑定,所以用简单泛型T不能够实现我们的目标。那么若明确限定类型T需实现Comparable接口又会怎么样呢?

程序清单:

public static <T extends Comparable <T>> List<T> higherPrice(List <T> A){
    Collection.sort(A);
    return A;
}
List <Fruit> Fruits=higherPrice(listFruits);  //listFruits为持有Fruit类型的List对象,且Fruit类实现了Comparable<Fruit>接口,ok
List<Apple> Apples=higherPrice(listApples);  //listApples为持有Apple类型的List对象,但Apple类实现的是Comparale<Fruit>接口,而不是Comparable<Apple>接口,编译出错。

程序说明:

第五行编译通过,第六行编译出错。<T extends Comparable<T>>放在方法的返回值之前,限定了调用此方法的对象必须实现Comparable接口,并且指定了实现Comparable接口的对象是T类型的。Fruit是实现了Comparable<Fruit>接口,第五行编译通过。Apple是Fruit的子类,即子类Apple借助父类间接实现了Comparable接口,但是Comparable<T>限定了listApples传参时,实现Comparable接口的必须是Comparable<Apple>形式。而Apple父类实现的是Comparable<Fruit>形式,因此第六行编译错误。那么,如果我们想要Apple等子类在不重复实现Comparable接口的情况下也能对对象集合进行排序,就要借助“? super T"。

public static <T extends Comparable<? super T>> List<T> higherPrice(List <T> A){
    Collections.sort(A);
    return A;
}
List<Fruit>Fruits=higherPrice(listFruits);  //listFruits为持有Fruit类型的list对象,ok
List<Apple>Apples=higherPrice(listApples); //listApples为持有Apple类型的list对象,ok

程序说明:将 <T extends Comparable <T>> 替换为 <T extends Comparable<? super T>>,这限定了调用该方法的对象需实现Comparable接口,并且限定了实现Comparable接口的类型为T类型本身或T的父类型。在这种限制条件下,Fruit实现了接口Comparable,则Apple等Fruit的子类便能直接继承Fruit的Comparable方法并进行对象集合的排序了。

猜你喜欢

转载自blog.csdn.net/liugf2115/article/details/86364604