java数据结构与算法第二课——泛型

 

目录

一:泛型的定义

二:引出泛型

三:泛型

3.1泛型的语法

3.2擦除机制(了解)

3.3泛型的上界

3.3.1泛型的上界的定义 

3.3.2泛型的上界的语法

3.4泛型的静态方法

四:通配符

4.1引出

4.2通配符及其上下界

五:包装类

 5.1装箱

5.2拆箱

5.3一道有趣的题目


一:泛型的定义

        一般的类和方法,只能使用具体的类型: 要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。

        泛型是在JDK1.5引入的新的语法,通俗讲,泛型:就是适用于许多许多的类型。从代码上讲,就是对类型实现了参数化。

二:引出泛型

之前我们已经学过数组,在一个数组中,只能存放指定类型的元素。例如:

int[ ] array1 = new int[10];               //存放整型数据

String[ ] array2 = new Stirng[10];    //存放字符串数据

        基于泛型的定义,我们希望一个数组可以适用许多许多类型。因为Object类是所有类的父类,那么是否可以把数组创建为Object呢?具体示例如下:

class MyArray{
    public Object[] array = new Object[10];

    /**
     * 获取pos下标的值
     *
     * @param pos
     * @return
     */
    public Object getPos(int pos) {
        return array[pos];
    }

    /**
     * 给pos下标放一个元
     */
    public void setPos(int pos, Object val) {
        array[pos] = val;
    }
}
public class TestDemo {

    public static void main(String[] args) {
        MyArray array = new MyArray();
        array.setPos(0,1);
        array.setPos(1,"hello");

    }
}

        就目前为止,这段代码似乎没有什么问题,因为数组的元素类型是Object,所以我可以放任意类型的数据,譬如在0下标的位置放入整型1,在1下标的位置放入字符串“hello”。但当我们想通过getPos()函数获取某个下标的元素时,你会发现:

         编译器报错了!这是因为数组中元素类型不确定,所以当你获取元素时,编译器无法帮你检测你当前想要获取的元素的类型,所以就会报错。而在实际中,我们很少需要把一个整型数据和一个字符串类型的数据放在同一个数组中,这没什么意义。一般情况下,我们仍希望可以自己指定类型,而非同时持有多种类型。

        泛型的作用就在此时体现出来:指定当前的容器,要持有什么类型的对象,然后让编译器去做检查。此时,就需要把类型,作为参数传递。需要什么类型,就传入什么类型。

三:泛型

3.1泛型的语法

class 泛型类名称<类型形参列表> {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> {
}

class 泛型类名称<类型形参列表> extends 继承类/* 这里可以使用类型参数 */ {
// 这里可以使用类型参数
}
class ClassName<T1, T2, ..., Tn> extends ParentClass<T1> {
// 可以只使用部分类型参数
}

        单纯看这些语法可能会让你觉得头大,所以我会对上面的例子进行简单的改写,以帮助我们理解泛型的语法:

package Test;

class MyArray<T>{
    public T[] array = (T[])new Object[10];

    /**
     * 获取pos下标的值
     *
     * @param pos
     * @return
     */
    public T getPos(int pos) {
        return array[pos];
    }

    /**
     * 给pos下标放一个元素
     *
     */
    public void setPos(int pos, T val) {
        array[pos] = val;
    }
}
public class TestDemo {
    public static void main(String[] args) {
        MyArray<Integer> array = new MyArray<Integer>();
        array.setPos(0,1);
        array.setPos(1,2);
        Integer str = array.getPos(1);
        System.out.println(str);

    }
}

运行结果如下:

         也就是说,要将一个类写成“泛性类”,我们需要进行以下操作:

注意:

1.数组中每个元素的类型不再是Object,而变成了T。所以在创建数组,写getPos()和setPos()方法时,都要使用T而不是Object;

2.在创建数组时,要写成public T[] array = (T[])new Object[10],即在右侧new一个Object类型数组并强转为T[]。 即不能直接new泛型类型的数组;

3.类名后的<T> 代表占位符,表示当前类是一个泛型类,【规范】类型形参一般使用一个大写字母表示,常用的名称有:

  • E 表示 Element
  • K 表示 Key
  • V 表示 Value
  • N 表示 Number
  • T 表示 Type
  • S, U, V 等等 - 第二、第三、第四个类型

4.泛型只能接受类,所有的基本数据类型必须使用包装类!即用Integer而不是int;

5.当编译器可以根据上下文推导出类型实参时,可以省略类型实参的填写;

MyArray<Integer> list1 = new MyArray<>(); // 可以推导出实例化需要的类型实参为 Integer

MyArray<String> list2 = new MyArray<>();   // 可以推导出实例化需要的类型实参为 String

6.泛型是将数据类型参数化,进行传递;

7.使用<T> 表示当前类是一个泛型类;

8.泛型目前为止的优点:数据类型参数化,编译时自动进行类型检查和转换。

3.2擦除机制(了解)

        擦除机制就是在编译时,所有的T都被擦除为了Object。通俗讲,就是在编译的过程当中,将所有的T替换为Object的这种机制。

3.3泛型的上界

3.3.1泛型的上界的定义 

        在定义泛型类时,有时需要对传入的类型变量做一定的约束,可以通过类型边界来约束。也就是说,泛型的上界是一种对于传入的类型变量的约束

3.3.2泛型的上界的语法

class 泛型类名称<类型形参 extends 类型边界> {
...
}

具体实例如下:

class Test<E extends Number> {
        
    }

public class TestDemo2 {
    public static void main(String[] args) {
        Test<Number> test = new Test<>();
        Test<Integer> test1 = new Test<Integer>();
        Test<Float> test2 = new Test<Float>();
        Test<Double> test3 = new Test<Double>();
    }
}

        这段代码说明:传入的类型变量必须是Number类及其子类。

        根据java文档显示,我们可以传入的类型包括Byte,Double,Float......

复杂示例:  

public class MyArray<E extends Comparable<E>> {
...
}

E必须是实现了Comparable接口的。 具体示例如下:

package Test;

class Student implements Comparable<Student>{
    public String name;
    public int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        return this.age-o.age;
    }
}

class Alg<T extends Comparable<T>> {
    public T findMax(T[] array) {
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            if(max.compareTo(array[i]) < 0) {
                max = array[i];
            }
        }
        return max;
    }
}



public class TestDemo1 {
    public static void main(String[] args) {
    Alg<Integer> alg = new Alg<Integer>();
    Integer[] array = {1,4,5,21,8,19};
    Integer max = alg.findMax(array);
    System.out.println("该整型数组中最大的元素为:"+max);

    Alg<Student> alg2 = new Alg<Student>();
    Student[] students = new Student[3];
    students[0] = new Student("wukong",18);
    students[1] = new Student("bajie",99);
    students[2] = new Student("shashidi",5);
    Student student = alg2.findMax(students);
    System.out.println("该学生数组中年龄最大的学生为:"+student);
}
}

运行结果如下:

分析:

         我们发现,每次调用findMax()方法时,必须先实例化对象,然后才能调用findMax()方法。那么,有没有可能不实例化对象,直接调用findMax()方法呢?答案是可以的。这就要用到静态方法。

3.4泛型的静态方法

        明确其语法后,我们将“泛型的上界”加入,即对传入的类型变量做一定的约束:

        这时候,我们就可以直接使用类名调用findMax()方法,1而不必先实例化对象了。具体示例如下:

class Alg2 {
    public static<T extends Comparable<T> > T findMax(T[] array) {
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            if(max.compareTo(array[i]) < 0) {
                max = array[i];
            }
        }
        return max;
    }
}



public class TestDemo1 {

    
    public static void main(String[] args) {
        Integer[] array = {1,4,5,21,8,19};
        Integer max = Alg2.findMax(array);
        System.out.println(max);
    }
}

 运行结果如下:

四:通配符

4.1引出

        通配符,顾名思义,即适配各种类型。        

        通配符有什么作用呢?简言之,通配符是用来解决泛型无法协变的问题的,协变指的就是如果Student 是Person 的子类,那么List<Student> 也应该是List<Person> 的子类。但是泛型是不支持这样的父子类关系的。具体实例如下:

         如图所示,Student类是Person类的子类,我们认为父类引用引用子类对象应该是没有任何问题的。然而,当我们将想法付诸实践时,发现这样做不合法,这是因为型不支持这样的父子类关系,这就叫做无法协变。而要解决这个问题,就需要用到通配符。具体实例如下:

package Test;


class Message<T> {
    private T message ;

    public T getMessage() {
        return message;
    }

    public void setMessage(T message) {
        this.message = message;
    }

}

public class TestDemo4 {

    public static void function() {
        Message<String> message = new Message<String>() ;
        message.setMessage("仰天大笑出门去,我辈岂是蓬蒿人!");
        fun(message);
    }

    public static void main(String[] args) {
        Message<Integer> message = new Message<Integer>() ;
        message.setMessage(1);
        fun(message);
    }

    public static void fun(Message<?> temp){
        //temp.setMessage(1);是不能往里面加元素的.
        //Message<?> temp  可以接受多种类型
        System.out.println(temp.getMessage());
    }
}

4.2通配符及其上下界

1、通配符,代表未知类型,代表不关心或无法确定实际操作的类型,一般与容器类配合使用。

public void testV(List<?> list) {}

2、<? extends T>,定义上限,期间只有阅读能力。这种方法表明参数化的类型可能是指定的类型或子类型。 可以接受T及其子类。

//t1要么是Test2,要么是Test2的子类

public void testC(Test1<? extends Test2> t1) {

    Test2 value = t1.getValue();

    System.out.println("testC中的:" + value.getT());

}

3、<? super T>,下限定义,有阅读能力和部分写作能力,子类可以写入父类。这种方法表明参数化的类型可以是指定的类型,也可以是父类。可以接收T及其父类,可以写入T及其子类。

//t1要么是Test5,要么是Test5的父类

public void testB(Test1<? super Test5> t1) {

    //子类代替父类

    Test2 value = (Test2) t1.getValue();

    System.out.println(value.getT());

}

        以上就是通配符及其上界、下界的全部内容了。

五:包装类

在Java中,由于基本类型不是继承自Object,为了在泛型代码中可以支持基本类型,Java给每个基本类型都对应了一个包装类型。

        如图为各基本类型及其对应的包装类。

        今天我们要介绍的内容,主要是装箱和拆箱。

 5.1装箱

public class TestDemo6 {
 
    public static void main(String[] args) {
        int a = 10;
        Integer b = a;//自动装箱
        Integer c = Integer.valueOf(a);//手动装箱
        System.out.println(b);
    }
}

        如何自动装箱?直接将int类型变量赋值给Integer类型的变量。而手动装箱则时调用balueOf()方法。

5.2拆箱

public class TestDemo6 {

    public static void main(String[] args) {
        Integer a = 10;
        int b = a;//自动拆箱
        double d = a;//intValue() 默认是以intValue()拆

        double d2 = a.doubleValue();//手动拆箱

        System.out.println(b);
    }

}

        在拆箱时,a为Integer类型,那么默认会以intValue()进行拆箱;如何想手动拆箱,则要显式调用相应的方法。

5.3一道有趣的题目

        对于拆箱和装箱部分,我们已经了解得差不多了。接下来我会介绍一道非常有趣的题目,其中就包含了“装箱”相关的知识。

        分析代码,为什么会出现这种现象呢?

        在执行这几行代码时,我们知道发生了装箱。而在底层,装箱默认调用的是valueOf()方法。所以要解决这个问题,我们需要查看valueOf()的原码,看看它究竟是怎样实现装箱的。

         为了验证我们的结论,我们采用特殊值法进行验证:

        结论成立!解决方案也很简单,就是使用equals()方法进行比较。(引用类型的比较)

public class TestDemo6 {
    public static void main(String[] args) {
        Integer a = 128;
        Integer b = 128;
        System.out.println(a.equals(b));
    }
}

         运行结果如下:

         这告诉我们,在对引用类型进行比较时,一定要使用equals()方法或compareTo()方法。


本课内容结束!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值