美文网首页
泛型与容器

泛型与容器

作者: Vector_Wan | 来源:发表于2019-10-25 17:35 被阅读0次

    一、泛型

    我们先来看看泛型类,和 C++ 中的模板类思想上有很多相似之处,都是使代码能适应于某种不确定的类型,直接看一个泛型类的实例:

    class MyClass01<T> {
        //这个方法中使用的泛型不是自己定义的,而是使用的类上的泛型
        public void print(T t){
            System.out.println(t);
        }
        // 泛型方法:泛型是在方法上自己定义的
        public <E> void show(E e){
            System.out.println(e);
        }
    }
    

    可以在创建 MyClass01 对象的时候指定 T 到底是什么类型(不能是基本数据类型)。这避免了强制类型转换,使得运行时可能出现的错误在提前到编译的时候。比如说如果没有泛型类,那我只能使用某个特定的数据类型,在使用其他类型的时候就不可避免的要类型转换,这就可能出现错误。
    在上面的例子中 <> 里面的东西就是泛型。可以类比函数传参,那么泛型就是类型的形参列表。
    套用一句官方的话来说:泛型的主要作用就是建立一个类型安全的数据结构。就是等下我们会看到的一些容器。
    在上面的例子中有一个泛型方法,创建在方法上的泛型才是真正的调用的时候确定到底是啥类型,一般由传入的参数决定,包含这个方法的类创建对象的时候,并不会确定方法上的类型,在调用的时候才确定。
    顺便提一句,除了泛型类、泛型方法以外,还有泛型接口。
    注意:

    1. 泛型不存在继承,也就是说在确定了泛型时,操作中的相对应的泛型必须一致。不能存在继承或者多态的关系,也就是说 《》中的类型必须一致,不能不同,但是使用泛型的对象依然可以存在继承和多态。
    2. 在泛型类中变量只能调用从 Object 类继承的或重写的方法。这个也可以理解,如果这个方法不是从Object继承过来的,那么可能性就太多了,运行的时候就很容易出现错误,不过这个小地方也给我们编代码带来了一些小障碍。

    下面是一个小李子:

    public class Circle {
        private double radius, area;
    
        Circle(double r){
            radius = r;
            area = r * r * Math.PI;
        }
    
        @Override
        public String toString(){
            return ""+area;
        }
    }
    
    public class Rect {
        private double side1, side2, area;
    
        Rect(double a, double b){
            side1 = a;
            side2 = b;
            area = side1*side2;
        }
    
        @Override
        public String toString() {
            return ""+area;
        }
    }
    
    public class Cone<E> {
        private double heigth;
        private E bottom;
    
        public Cone(E b, double h) {
            bottom = b;
            heigth = h;
        }
    
        public double computeVolume(){
            String s = bottom.toString();
            double area = Double.parseDouble(s);
            return area*heigth;
        }
    }
    
    public class test13_1 {
        public static void main(String[] args){
            Cone<Circle> coneOne = new Cone<Circle>(new Circle(2), 3);
            System.out.println(coneOne.computeVolume());
            Cone<Rect> coneTwo = new Cone<Rect>(new Rect(1,1),2);
            System.out.println(coneTwo.computeVolume());
        }
    }
    

    二、容器

    1. 容器

    首先,容器属于 java.util 包,用之前要先导入。
    容器其实就是一些数据结构,它的作用就是保存很多对象的引用。容器本身就是一个对象,这个对象可以存放任意数量的其他对象,同时提供操作这些对象的方法。

    容器要储存的对象(们)有一些让人棘手的特点:

    • 程序需要根据运行时的某些条件创建对象。
    • 创建对象之前可能不知道需要创建对象 的数量和类型。

    2. 容器的分类

    我们先来看看与容器有关的类族:

    整个容器类库包括两大部分:Collection 和 Map。

    • Collection:独立元素的序列,不同的Collection 满足不同的限制,稍后再详细说。
    • Map:键值对的序列,允许用户通过键来查找对象,比如 HashMap 允许我们使用一个对象来查找另一个对象。

    我们先来看看 Collection 的 一些实现比如 Set 、List、Queue,这些不同的分类实际上就是要求容器有不同的限制。

    3. Set

    这还是个接口,实现了 Set 接口的所有类都可以看成是一个 Set。它的限制是:

    Set 中存放的元素是无序的
    Set 容器中存放的应用指向的对象不能有重复的
    。这就有一个问题了,啥叫重复。先给一个结论,相互 equals 的两个对象就是相等的,所以到底什么叫相等,你说的算。之前说 Object 的时候提到过这个 equals 方法,我们再看一下它的文档:

    文档中对它的实现做了一些要求,需要我们注意:自反性,对称性,传递性,一致性,还有equals(null)返回 false。
    所以一般来说 equals 方法在实现的时候有它自己的一套路子。稍后举例再说。

    常见的 Set 容器有 TreeSet 和 HashSet 。
    我们直接举一些例子说明,里面有一些方法也很好理解。

    package mySet;
    import java.util.*;
    
    public class mySet {
        public static void main (String[]args){
            Set<Integer> numSet = new TreeSet<Integer>();
            numSet.add(new Integer(1));
            numSet.add(new Integer(2));
            numSet.add(new Integer(2));
            System.out.println(numSet.size());
        }
    }
    

    输出的结果为 2 因为不能有重复的元素,这个例子里面有 add 方法,有 size 方法,直接看名字就知道是干什么的,就不多说了。

    注意这个例子中我在实现 Set 接口(实际上是一个泛型接口)的时候传递了一个 Integer 类型的对象,不能直接使用基本数据类型,这是因为泛型的实现只能是对象。

    我们再来看一些其他的例子。如果学过数据结构这些方法都很容易,有疑问的话可以参考官方文档。

    package mySet;
    import java.util.*;
    
    public class mySet {
        public static void main (String[]args){
            Set<Integer> numSet = new TreeSet<Integer>();
            numSet.add(new Integer(1));
            numSet.add(new Integer(2));
            numSet.add(new Integer(2));
            System.out.println(numSet.size()); // 2
            System.out.println(numSet.contains(new Integer(1))); // true
            numSet.clear();
            System.out.println(numSet.isEmpty()); // true
        }
    }
    

    4. List

    List 按照线性顺序排列,每个元素对应一个整数索引,我们可以通过索引访问元素。
    方法差不多直接上例子:

    package test_list;
    import java.util.*;
    
    public class MyList {
        public static void main(String []args) {
            List<Integer> list = new ArrayList<Integer>();
            if (list.isEmpty()) System.out.println("The List is empty."); // The List is empty.
            // 增加
            list.add(new Integer(1));
            list.add(new Integer(2));
            list.add(0, new Integer(3));
            list.add(1,new Integer(4));
            System.out.println(list); // [3, 4, 1, 2]
            // 遍历(查看)
            System.out.println(list.get(0)); // 3
            for (int i = 0; i < list.size();i++) {
                System.out.print(" "+list.get(i)); //  3 4 1 2
            }
    
            // 修改
            list.set(0, new Integer(10));
            System.out.println(list); // [10, 4, 1, 2]
    
            // 删除
                // 根据索引来删除
            list.remove(0);
            System.out.println(list); // [4, 1, 2]
    
                // 根据值来删除
            list.remove(new Integer(4));
            System.out.println(list); // [1, 2]
        }
    }
    

    接下来我们来看一个很有意思的例子:
    我们知道容器就是储存对象(引用)的地方嘛,刚刚是一个系统内置的对象(Integer)我们现在尝试一个自己写的对象。

    package test_list;
    import java.util.*;
    
    public class MyList2 {
        public static void main(String [] args){
            List<Rect> list = new ArrayList<Rect>();
            list.add(new Rect(1,2));
            list.add(new Rect(3,4));
            System.out.println(list); // [矩形1 2, 矩形3 4]
    
            list.remove(new Rect(3,4));
            System.out.println(list); // [矩形1 2, 矩形3 4]
        }
    }
    
    class Rect{
        private int width;
        private int length;
        Rect(int a, int b) {
            width = a;
            length = b;
        }
    
        @Override
        public String toString() {
            return "矩形"+width+" "+length;
        }
    }
    

    嘿,这就有问题啦,remove 不掉。我们再添加一段代码测试一下看看到底怎么回事:

            System.out.println(list.get(1)); // 矩形3 4
            boolean result = list.get(1).equals(new Rect(3,4));
            System.out.println(result); // false
    

    问题就在于,list.get(1) 不等于 new Rect(3,4),对相等的定义来自于 Object 的 equals,我们看一下源码:

    所以默认的 equals 方法实际上是比较两个对象的地址是否相同,显然之前的两个对象地址不同。但是我们之前的那个 Integer 地址也不同呀,为什么就可以正常比较呢,这是因为 integer 作为一个内置类,已经将 equals 方法 override 了。所以我们只要为我们的方法添加自己的 equals 就可以。注意需要满足之前提到的五个性质,复习一遍:自反性,对称性,传递性,一致性,还有equals(null)返回 false。现在我们的逻辑是长一样,宽一样的两个矩形对象相等。

    @Override
    public boolean equals(Object obj) {
        if (obj == null)return false;
        if (obj instanceof Rect){
            return (((Rect) obj).length==this.length && ((Rect) obj).width==this.width);
    //            Rect r = (Rect)obj;
    //            return (r.length==this.length&&r.width==this.width);
        }
        return false;
    }
    

    再运行上面的代码,就可以正常 remove 了;

    队列,map 都差不多,在博客里就不写啦,上面的代码中提到了遍历,但实际上可以更简单,而且这个地方有一些 Java 的编程思想在里面,所以还是单独来写吧,

    5. Collection 的遍历

    Collection 的遍历主要是两种:

    • 使用 Iterator
    • 使用增强的 for 循环

    我们先来看看 List 的遍历:

    package test_list;
    import java.util.*;
    
    
    public class TestIterrator {
        public static void main(String [] args){
            List <Integer> list = new ArrayList <Integer> ();
            list.add(new Integer(5)); //放入最后
            list.add(new Integer(3)); //放入最后
            list.add(0 , new Integer(4)); //放入索引0处
            list.add(1 , new Integer(6)); //放入索引0处
            //开始遍历
            Iterator <Integer> it = list.iterator();
            while(it.hasNext()){
                Integer in = it.next();
                System.out.print(in + " ");
            }
            System.out.println(""); // 4 6 5 3 
        }
    }
    

    使用起来很容易,但是还是要看看底层到底是怎么写的。顺便研究研究为什么要这么写。我们先来看看 Iterator 接口:

    接口要求了必须要有 hasNext 方法,next 方法 ,remove 方法。然后我们实现这个接口的时候是调用了一个 ArrayList 的方法,下面就是这个方法的代码,可以看到是返回了一个内部类 Itr 的对象。

    我们再来看看 Itr 的实现:

    这个内部类实现了Iterator 接口。使用自己的逻辑 override 了接口中的方法。
    到这里就有一个问题,为啥要使用内部类呢?

    假如不使用内部类的话,比如我把ArrayList 的 Iterator 的逻辑写在 listIterator 类里面 ,大概代码要这样写:

    Iterator <Integer> it = new listIterator();
    

    这就要求我们记住每一个容器对应的 Iterator 实现的类名,这太不优雅了,直接 list.iterator() 多舒服!
    还有一个很重要的原因就是可以很大程度上避免容器内部细节的暴露。因为内部类可以访问外部类的成员,所以就省很多事。另外每一种容器的 iterator 实现都不相同,比如 ArrayList 的实现跟 ArrayBlockingQueue 的实现就不同,重用性差,直接写在类里面也会比较清楚。

    另外一种更简单的方法就是使用增强的 for 循环,foreach 循环。

    import java.util.*;
    import java.util.concurrent.ArrayBlockingQueue;
    
    public class TestFor{
        public static void main(String [] args){
                Queue <Integer> q = new ArrayBlockingQueue <Integer> (10);
                q.add(new Integer(5));
                q.add(new Integer(2));
                q.add(new Integer(1));
                q.add(new Integer(3));
                //开始遍历
                for(Integer i : q){
                    System.out.print(i + " ");
                }
                System.out.println("");
            }
    }
    

    跟 Python 里面的 for 循环差不多,这种要比 Iterator 简单一点。

    相关文章

      网友评论

          本文标题:泛型与容器

          本文链接:https://www.haomeiwen.com/subject/zdnkvctx.html