美文网首页
泛型与容器

泛型与容器

作者: 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 简单一点。

相关文章

  • 泛型与容器

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

  • Java 3 对象容器

    Ref:什么是泛型Java 容器 & 泛型(1):认识容器 3.1 顺序容器 我们首先学习的是顺序容器,即放进容器...

  • 第十五章:泛型

    泛型实现了参数化类型的概念。 简单泛型 容器是出现泛型的重要原因之一。泛型的主要目的之一就是用来指定容器要持有什么...

  • Java自学-集合框架 泛型Generic

    ArrayList上使用泛型 步骤 1 : 泛型 Generic 不指定泛型的容器,可以存放任何类型的元素指定了泛...

  • [GeekBand][C++ STL与泛型编程]第六周笔记

    课堂大纲这周的课程大致上应该是这三个部分 C++模板介绍 泛型编程 容器 概述STL与泛型编程 泛型编程作为一种编...

  • Java 泛型

    泛型类 例如 泛型接口 例如 泛型通配符 泛型方法 类中的泛型方法 泛型方法与可变参数 静态方法与泛型 泛型上下边...

  • 泛型、反射和注解

    泛型可以分为:泛型类,泛型接口和泛型方法 泛型类:容器类可以算的上是最具有重用性的类之一。 先来看一下一个没有泛型...

  • 10泛型算法

    10泛型算法 10.1概述 泛型算法不能改变容器的大小,依赖于元素类型的操作。 10.2初识泛型算法 10.2.1...

  • Java--容器中使用泛型

      容器相关类都定义了泛型,我们在开发和工作中,在使用容器类时都要使用泛型。这样,在容器的存储数据、读取数据时都避...

  • Boolan C++ 笔记四

    泛型编程泛型编程的代表作品STL是一种高效、泛型、可交互操作的软件组件。STL以迭代器(Iterators)和容器...

网友评论

      本文标题:泛型与容器

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