Collection实现类、迭代器、泛型

1.Collection实现类

1.1.集合的由来

        先说说集合的由来,假如现在有一个需求:存储80个学生的一门成绩。

那么我们有以下存储方式:
        1.变量:如果存取内容比较多,需要一个一个声明,声明80个变量,每个变量存储一个学生成绩
                  int i1=100;
                  int i2=30;
                       .....(显然非常麻烦,我们也不会去用)
        2.数组:相对于变量,可以一次性声明多个变量,而我们必须要提前预估容量(数组的长度),才能声明数组,如果已经存满了80个学生成绩,又来20个学生成绩,利用扩容机制(类似StringBuilder扩容原理)。重新开辟长度为100数组 new int[100],将原来的80个学生成绩拷贝到新数组中,再把新来的20个成绩也拷贝到新数组中.(这种方式显然比上一种好得多)
        3.集合(Collection)容器:方便开发者使用,集合是JDK提供的,别人写好的,我们关心的角度不再是数组的开辟容量以及扩容,我们关心的焦点 增(添加元素) 删(删除元素) 改(修改元素) 查(获取元素/判断元素)。

        那么集合是如何对元素进行存储和增删改查等操作呢,下面来详细看看。

1.2.集合的主要继承体系

        还有就是要知道集合的主要继承体系如下图:

        当然集合的继承之间的关系肯定没上图这么简单,不一定是直接继承,也有可能是间接继承,但最重要的是掌握上图中的主要继承体系中的接口和类的实现。

        既然Collection接口是集合中的顶层接口,那么它中定义的所有功能子类都可以使用。查阅API中描述的Collection接口。Collection 层次结构中的根接口。Collection 表示一组对象,这些对象也称为 collection 的元素。一些 collection 允许有重复的元素,而另一些则不允许。一些 collection 是有序的,而另一些则是无序的。


1.3.Collection体系通用方法

        继续查阅API ,发现Collection接口中很多集合的操作方法,那么这些方法都具体能做什么呢?这里我们不关心具体创建的Collection中的那个子类对象,这里重点演示的是Collection接口中的方法。
        创建集合的格式:
        Collection 变量名 = new ArrayList();//父类引用指向子类对象
添加&获取功能:

        boolean add(Object e):向集合容器中添加元素
        int size():获取存储的元素个数
        c.clear():清空集合中的元素

        回想学过的获取长度或元素个数的:数组: arr.length
                                                                  字符串: str.length()
                                                                  StringBuilder: sb.length()

public class CollectionDemo01 {
    public static void main(String[] args) {
        Collection c=new ArrayList();
        c.add("张三");
        c.add("李四");
        c.add("王五");
        System.out.println(c);
        System.out.println(c.size());
        c.clear();//清空集合中的元素
        System.out.println(c);
    }
}

     由于Colection是接口,因此我们在这里实现它的子类,即父接口指向子类对象,也就是多态的思想。在存储元素的时候,集合中,我们一般一个集合只存储一种类型的元素,是为了防止后期处理数据混乱。

        运行结果:

集合的判断功能:
        boolean contains(Object o):判断集合中是否包含指定元素,如果包含就返回true,否则就返回false
        原理:
                底层会拿着要判定的元素与集合中的元素利用equals()方法一一对比
                如果在对比的过程中equals()方法返回true => 代表该集合包含要判定的元素 =>contains方法返回true,如果与集合中所有元素利用equals()对比均返回false=>代表该集合不包含要判定的元素=>contains方法返回false。首先简单看一下contains方法的使用:

public class CollectionDemo02 {
    public static void main(String[] args) {
        Collection c=new ArrayList();
        c.add(345);
        c.add(123);
        c.add(408);
        System.out.println(c.contains(345));//true  依然遵循contains方法的原理
        System.out.println(c.contains(321));//false
    }
}

        那么为了验证底层用的是equals方法进行比较的,首先我们建一个Person类:

public class Person{
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

        其次再使用contains方法,比较对象在集合中的存储:

public class CollectionDemo02 {
    public static void main(String[] args) {
        Person p1 = new Person("张三",19);
        Person p2 = new Person("李四",18);
        Collection c=new ArrayList();
        c.add(p1);
        c.add(p2);
        Person p3 = new Person("张三",19);
        System.out.println(c);//输出集合元素
        System.out.println(c.contains(p1));//true
        System.out.println(c.contains(p3));//false
    }
}

        本身输出集合元素会调用toString方法,输出对象的对象也会调用toString方法,在不重写的情况下,默认会打印十六进制哈希值,这里按照重写后的方式输出。而p1和p2都在集合中,p3的存储内容和p1的一样,但并没有加入集合,运行结果:

        如果contains方法仅仅比较存储的内容,那第二行应该返回true,因为存储的内容一样。其实默认的equals方法比较的就是地址值,显然p1和p3两对象的地址值不一样,那么现在重写以一下equals,再看看运行结果:

 //将该代码重写在Person类中
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Person person = (Person) o;

        if (age != person.age) return false;
        return name.equals(person.name);
    }

再看运行结果:


        显然,p1和p3比较后,显示集合中包含p3元素并返回true,比较的就是内容,而不再是地址值。

        boolean isEmpty():判断集合中是否有元素,如果有元素,那么就返回false,否则就返回true
                原理:利用size()判断集合是否为空
                        如果size()==0,说明集合中无元素,isEmpty()返回true
                        如果size()!=0,说明集合中有元素,isEmpty()返回false

public class CollectionDemo02 {
    public static void main(String[] args) {
        Person p1 = new Person();
        Person p2 = new Person();
        Collection c=new ArrayList();
        c.add(p1);
        c.add(p2);
        System.out.println(c.isEmpty());
        c.clear();//清空集合元素
        System.out.println(c.isEmpty());
    }
}

        删除功能:

集合中的移除功能:
        boolean remove(Object o)
                如果移除成功返回true,否则返回false
        原理:
        1.先查找:将要删除的元素与集合中的元素利用equals方法一一对比,一旦equals方法返回true,代表找到该元素,如果与集合中的元素利用equals方法一一对比均返回false,找不到该元素
        2.如果没找到,remove返回false,如果找到,再去删除集合已有元素

public class CollectionDemo03 {
    public static void main(String[] args) {
        Collection c = new ArrayList();
        Person p1 = new Person("张三", 19);
        Person p2 = new Person("李四", 18);
        c.add(p1);
        c.add(p2);
        Person p3 = new Person("张三", 19);
        System.out.println(c.remove(p3));
        System.out.println(c);
    }
}

运行结果:

        上面我们已经重写了equals方法和toString方法,让它来比较内容,所以这里判断的p3存储的信息就是p1,既然匹配到了就直接remove进行移除,再次输出集合元素,就只剩下p2的信息了。

2.迭代器

        java中提供了多种集合,它们在存储元素时,采用的存储方式不同。我们要取出这些集合中的元素,可通过一种通用的获取方式来完成。Collection集合元素的通用获取方式:在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为**迭代**。

        先看看API:

        发现Collection接口继承了Iterable<E>,说明Collection实现的集合是可迭代的,其他如有继承也是可以进行迭代。进入Iterable<E>里面可以看到:

2.1.迭代器使用

        集合通用迭代方式:
        public interface Iterable  {
                Iterator  iterator(); //返回一个迭代器

        }

        public interface Iterator  { // 迭代器的设计中含有三个方法
                boolean hasNext(); //判断容器中元素是否存在
                Object  next();//取出下一个元素
                void remove();//移除当前元素
        }  

        下面先使用迭代器中的方法:

public class CollectionDemo04 {
    public static void main(String[] args) {
        Collection c = new ArrayList();
        c.add("张三");
        c.add("李四");
        //1.先获取一个通用的迭代器对象
        Iterator iterator = c.iterator();
        //2.利用迭代器中方法
        System.out.println(iterator.hasNext());//判断容器中是否存在下一个元素,有就返回true,否则返回false
        System.out.println(iterator.next());//取出下一个元素       //hasNext指针从容器中的第一个元素上方开始移动,所以这里的下一个元素就是第一个元素

        System.out.println(iterator.hasNext());//继续判断下一个元素,此时hasNext指针继续移动到第二个元素
        System.out.println(iterator.next());//取出下一个元素,也就是在第一个元素的基础之上,即第二个元素

        System.out.println(iterator.hasNext());//此时指针指向了第二个元素下方,即指向空,所以返回false
        System.out.println(iterator.next());//因此这里在取出元素的时候就出现没有下一个元素的异常
    }
}

        需要知道,hasNext( )判断方式:

        运行结果:

        判断到第二个元素,再进行判断就会输出false,此时已经没有下一个元素了,再输出就报错:java.util.NoSuchElementException,即没有元素异常。
 

2.2.迭代器遍历

        既然是遍历,就要输出集合中的所有元素,我们打印集合时输出的那不叫遍历,将元素逐个取出才叫遍历。
        这里用for循环和while循环来遍历。

        首先看while循环迭代:

public class CollectionDemo04 {
    public static void main(String[] args) {
        Collection c = new ArrayList();
        c.add("张三");
        c.add("李四");
        c.add("王五");
        Iterator iterator = c.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

结果:

        iterator.hasNext()会经判断后返回一个布尔值,如果为true就继续遍历输出。

        再看for循环迭代:

public class CollectionDemo04 {
    public static void main(String[] args) {
        Collection c = new ArrayList();
        c.add("张三");
        c.add("李四");
        c.add("王五");
        for(Iterator iterator = c.iterator(); iterator.hasNext();){
            System.out.println(iterator.next());
        }
    }
}

        Iterator iterator = c.iterator();这里获取一个迭代器对象,类似于我们int i=0;初始化变量。

iterator.hasNext()依旧在判断条件位置。输出和while的一样。

2.3.并发修改异常

        先看代码:

public class CollectionDemo05 {
    public static void main(String[] args) {
        Collection c = new ArrayList();
        c.add("abc");
        c.add("def");
        c.add("gdk");
        Iterator iterator = c.iterator();
        while (iterator.hasNext()){
            if (iterator.next().equals("def")){
                c.remove(iterator.next());
            }
        }
    }
}

        运行结果:

问题: java.util.ConcurrentModificationException(并发修改异常)
        由于我们在使用迭代器的方法遍历过程中,使用了集合的方法进行 删除/添加(导致集合结构的改变),从而导致并发修改异常。

解决方案:
        如果使用迭代器的方式进行遍历,在遍历过程中需要针对集合添加/删除元素,我们要选用迭代器的方法来完成,从而避免并发修改异常。
        如果使用集合的方式进行遍历,在遍历过程中需要针对集合添加/删除元素,我们要选用集合的方法来完成,从而避免并发修改异常。

        解决后:

        显然就时把c.remove(element);改成iterator.remove();既然用到了迭代器,那删除元素的方式就用迭代器的方式。错误就避免了。
 

2.4.增强for循环

        JDK1.5新特性:增强for,我们日后遍历数组或集合经常使用增强for。

JDK 5新特性:
         增强for循环(for-each):
                适用场景:
                        简化集合和数组的遍历
                        要求集合必须继承或实现 Iterable 接口
        格式:
           for(要遍历的容器中的元素类型 变量名 : 数组/集合){
                 //取出元素
           }
        注意事项:
        1.增强for在遍历的时候无法操作数组的索引
        2.增强for针对集合来说,底层依然使用的是迭代器
        3.注意增强for上元素的类型

看完基本使用,就容易理解了:

public class CollectionDemo06 {
    public static void main(String[] args) {
        //遍历数组
        int index = 0;//索引
        int[] arr = {12, 45, 2, 66};
        for (int element : arr) {
            System.out.print(element+" ");
            index++;
        }
        System.out.println();
        //遍历集合
        Collection c = new ArrayList();
        c.add("张三");
        c.add("李四");
        c.add("王五");
        for (Object element : c) {
            System.out.print(element+" ");
        }
    }
}

        先是用增强for遍历一个数组,由于没索引,所以可以自己加一个index来记录索引,方便后期使用,这里我只解释不再使用。再是使用增强for遍历集合,也是一样。需要注意的是,增强for针对集合来说,底层依然使用的是迭代器,所以要避免并发修改错误,在进行使用增强for移除集合中的元素的时候记得使用迭代器,而不是使用集合的方式直接remove元素。

运行结果:

3.泛型


        JDK 5新特性泛型:广泛的类型
                根据泛型的定义位置,分为三种:
        1.类上的泛型 public class ArrayList<E>
        2.方法上的泛型 <T> T[] toArray(T[] a)
        3.接口上的泛型 public interface Collection<E>

3.1.类上的泛型

          定义在类上泛型
格式:
        class 类名<E,Q,A,...>{//<E,Q,A,...> 泛型变量
           //在类中就可以使用泛型变量
         } 

直接看演示:

public class GenericDemo02<E> {
    public void method01(E e){
        System.out.println(e);
    }
    public static void method02(){

    }
    public static void main(String[] args) {
        GenericDemo02<String> gd01=new GenericDemo02<String>();
        //GenericDemo02<String> gd01 = new GenericDemo02<>();等效于上一行
        gd01.method01("李四");
        GenericDemo02.method02();
        GenericDemo02<Integer> gd02 = new GenericDemo02<>();
        gd02.method01(123);
    }
}

        在声明对象的时候通过<>指定具体类型,例如<String>那么这个String相当于替代掉类上的E,类上的泛型一旦确定为String,类中E的地方,也会被替换成String。那么如果在new对象时用Integer类型,则方法中使用的E也会默认转化为Integer类型。

        总之就是说在创建对象的时候指定泛型变量的类型,该泛型变量就被替换成该类型。

总结:

        1.类上泛型可以在类中直接使用
        2.类上的泛型通过创建该类的对象指定具体类型
        3.如果创建对象时候不指定具体类型,泛型默认会被替换成Object
        4.静态方法不能使用类上泛型,因为静态方法可以通过类名直接调用,无法确定泛型的具体类型

注:集合类上使用泛型

public class GenericDemo03 {
    public static void main(String[] args) {
        Collection<String> c=new ArrayList<>();
        c.add("123");
        c.add("qwe");
        c.add("qhk");
        System.out.println(c.size());
        Collection c2=new ArrayList<String>();  //虽然没有报错,但是无法确定泛型的具体类型,写法错误
    }
}

        集合中一旦确定了元素类型,就只能添加相应类型的元素,否则就报错。

3.2.方法上的泛型


        格式:
             权限修饰符 <T,Q,E,...> 返回值类型 方法名(T t,Q q,...){     

                                                      //<T,Q,E,...> 方法上的泛型声明
                                                      //可以在方法的形参以及方法内使用
            }

public class GenericDemo04 {
    public <T> void method(T t){
        System.out.println(t);
    }
    public static void main(String[] args) {
        GenericDemo04 gd = new GenericDemo04();
        gd.method("String");//String
        gd.method(123);//123
    }
}

        对于gd.method("String");当我们传递一个字符串的时候,此时形参的T被替换为String,替换为:method(String t)。当gd.method(123);我们传递一个整数值的时候,此时形参的T被替换为int对应的包装类,此时替换为:method(Integer t)。

        所以说,方法上的泛型,是当我们为方法传参的时候确定下来的。根据传入参数的类型,方法上的泛型也会跟着变成对应的包装类型。

集合中使用泛型的方法:toArray()

        只要给泛型方法传入什么类型,泛型变量就会被替换为什么类型。

Collection接口中toArray方法:
    public abstract <T> T[] toArray(T[] a):返回一个包含此集合中所有元素的数组(将集合转成数组)

public class GenericDemo05 {
    public static void main(String[] args) {
        ArrayList<String> a = new ArrayList<>();
        a.add("cbv");
        a.add("uiv");
        a.add("cnk");
        String[] strings=new String[a.size()];
        String[] str=a.toArray(strings);
        for (String s : str) {
            System.out.print(s+"  ");//cbv  uiv  cnk  
        }
    }
}

        new一个能存储集合中元素的数组,再用集合对象调用该方法,传入所要返回的数组,下面再用一个for循环输出数组元素。这就是toArray方法基本使用。

3.3.接口上的泛型

        第一种格式:
                接口上的泛型格式:
                 interface 接口名<T,E,Q...>{     //接口上声明的泛型变量,都可以在接口中使用
                }

        首先创建一个父接口Father。

public interface Father<T>{
    void method(T t);
}

        再创建Son类来继承父接口,此时Father后面可以传入指定类型,如传入String类型,则父接口中的T也会自动转化为String类型(包括方法)。也就是:当我们在定义类的时候实现父接口,为父接口传入类型,此时接口上的泛型变量会被替换为该类型,同时在接口中用到该泛型变量的位置都会被替换成该类型。

public class Son implements Father<String> {
    @Override
    public void method(String s) {
        System.out.println(s);
    }
}

        最后创建测试类,使用son调用method方法时,也只能为method方法传入String类型,也就是字符串,传入其他类型均会报错:

public class Demo {
    public static void main(String[] args) {
        Son son = new Son();
        son.method("abc");
    }
}

        第二种格式
                接口上的泛型:
                interface 接口名<T,E,Q,....>{     //接口上声明泛型变量(参数),可以在接口中使用
                 }

                仍使用上面的父接口,再次初始化创建Son类继承该父接口,即:

public class Son<Q> implements Father<Q> {//Q会替换接口上的T,接口中用的T的位置也会被替换成Q
    @Override
    public void method(Q q) {
        System.out.println(q);
    }
}

        当类上定义泛型以及实现的接口也定义泛型,此时泛型被确定的时机: 创建该类对象的时候

也就是:

public class Demo {
    public static void main(String[] args) {
        Son<Integer> son = new Son<>();//定义Son类对象的时候类上的泛型被确定(Integer)
                                      //进而接口上的泛型也确定(Integer)
        son.method(123);
    }
}

        这里Son的对象实现时确定了类上的泛型为Integer,此时,Son类中以及父接口中的泛型都会变成Integer,用son调用method方法时,会提示传入一个整型参数。所以我们说:当类上定义泛型以及实现的接口也定义泛型,此时泛型被确定的时机: 创建该类对象的时候。

3.4.增强for使用泛型
 

        增强for上使用泛型
                for(要遍历的容器中的元素类型 变量名 : 数组/集合){
                     //取出元素
                }

public class Demo {
    public static void main(String[] args) {
        Collection c=new ArrayList();//创建对象的时候没指明实际类型  那么类中的泛型会被替换成Object
        c.add(123);     //Object e=new Integer(123);
        c.add("abc");   //Object e="abc";
        c.add("def");   //Object e="def";
        for (Object o : c) {
            System.out.print(o+" ");//123 abc def
        }
//------------------------------------------------------------------------------------------
        Collection<String> c02=new ArrayList<>();//创建对象的时候指定为String类型  那么类中的泛型会被替换成String
        c02.add("abc");
        c02.add("bf");
        c02.add("as");
        for (String s : c02) {
            System.out.print(s+" ");//abc bf as
        }
    }
}

        第一种c创建对象的时候不指定集合的类型,默认为Object类,那再使用增强for循环的时候,也需要使用Object来遍历。而使用泛型后,第二种c02,在创建对象的时候指定为String类型,那再使用增强fors循环就要使用相应的String类型。

3.5.使用泛型的优点
 

        JDK 5泛型的优点:
                1.使用泛型可以将运行时期可能出现问题(例如:类型转换异常)提升到编译时期(报错时机提前,将错误解决在萌芽阶段)
                2.一定程度上避免强制类型转换,可以直接使用集合中元素类型的特有方法

      使用泛型后,添加指定String类型后,再往集合中添加元素时,只能添加字符串,如果不是字符串类型就会直接编译报错,显然此时c.add(123)这行直接标红报错,可以规范集合中的元素,哪怕在不经意间输错也能及时发现。这也就是泛型的优点之一。

public class Demo02 {
    public static void main(String[] args) {
        Collection<String> c=new ArrayList();
        c.add("abc");
        c.add("def");
        //c.add(123);  // ClassCastException  直接编译报错
        c.add("vbn");
        for (String s : c) {
            System.out.println(s.charAt(0));//获取每个字符串元素的索引为0的字符
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值