Java泛型

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。


一. 为什么需要泛型?

如下代码所示,我们创建一个list,并向其中加入两个字符串类型的值,随后加入Integer值。此时编译器不会报错,因为此时List默认的类型是Object,随后我们忘记之前加入了Integer数据,之后我们取出元素转为String数据,即 1 处。编译时不会出错,但运行时,会出现“java.lang.ClassCastException”异常。因此,导致此类错误编码过程中不易发现。

public class GenericTest {
    public static void main(String[] args){
        List list=new ArrayList();
        list.add("string1");
        list.add("string2");
        list.add(3);
        for (int i = 0; i <list.size() ; i++) {
            String currentData=(String)list.get(i); // 1 
            System.out.println(i+": "+currentData);
        }
    }
}

运行结果:

0: string1
1: string2
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
	at other.GenericTest.main(GenericTest.java:18)

Process finished with exit code 1

因此,我们发现,将对象放入List时,List是记不住对象的类型,该对象编译时为Object型,但运行时仍为对象本身类型。所以在1处运行时会报异常“java.lang.ClassCastException”异常。因此,导致此类错误编码过程中不易发现。

那么有什么办法可以使集合能够记住集合内元素各类型,且能够达到只要编译时不出现问题,运行时就不会出现“java.lang.ClassCastException”异常呢?答案就是使用泛型。

二. 泛型

泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)

下面例子采用泛型

public class GenericTest {
    public static void main(String[] args) {
        /**
         List list=new ArrayList();
         list.add("string1");
         list.add("string2");
         list.add(3);*/
        List<String> list = new ArrayList<>();
        list.add("string1");
        list.add("string2");
        //list.add(3);// 1 提示编译错误
        for (int i = 0; i < list.size(); i++) {
            String currentData = list.get(i);// 2
            System.out.println(i + ": " + currentData);
        }
    }
}

采用泛型写法后,在//1处想加入一个Integer类型的对象时会出现编译错误,通过List<String>,直接限定了list集合中只能含有String类型的元素,从而在//2处无须进行强制类型转换,因为此时,集合能够记住元素的类型信息,编译器已经能够确认它是String类型了。

我们来看看List接口的具体定义:


public interface List<E> extends Collection<E> {
    int size();

    boolean isEmpty();

    boolean contains(Object o);

    Iterator<E> iterator();


    Object[] toArray();

    <T> T[] toArray(T[] a);

    boolean add(E e);

    boolean remove(Object o);

    boolean containsAll(Collection<?> c);

    boolean addAll(Collection<? extends E> c);

    boolean addAll(int index, Collection<? extends E> c);

    boolean removeAll(Collection<?> c);

    boolean retainAll(Collection<?> c);

    default void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final ListIterator<E> li = this.listIterator();
        while (li.hasNext()) {
            li.set(operator.apply(li.next()));
        }
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }

    void clear();

    boolean equals(Object o);

    int hashCode();

    E get(int index);

    E set(int index, E element);

    void add(int index, E element);

    E remove(int index);

    int indexOf(Object o);

    int lastIndexOf(Object o);

    ListIterator<E> listIterator();

    ListIterator<E> listIterator(int index);

    List<E> subList(int fromIndex, int toIndex);

    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.ORDERED);
    }
}

可以看到,在list接口中采用泛型化定义后,<E>中的E表示类型形参,可以接收具体的类型实参,并且此接口定义中,凡是出现E的地方均表示相同的接受自外部的类型实参。

三. 泛型接口、泛型类和泛型方法

1、泛型类

  • 定义:泛型类的声明和非泛型类的声明类似,除了在类名后面增加类型参数声明部分。
  • 我们常见的如T、E、K、V等形式的参数常用于表示泛型形参,由于接收来自外部使用时候传入的类型实参。
  • 泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

定义一个泛型类:

通过<T> 声明告诉JVM返回值定义一个泛型 T这里的T只是个占位符的效果,26个字母随便写哪个字母都可以,但一定要是和< >里面相同的字母,这里使用T。

public class GenericTest {
    public static void main(String[] args) {
        Box<String> name = new Box<>();
        name.add("memory");
        Box<Integer> age = new Box<>();
        age.add(24);
        System.out.println("name: " + name.get());
        System.out.println("age: " + age.get());
    }
}

class Box<T> {
    private T data;

    public T get() {
        return data;
    }

    public void add(T data) {
        this.data = data;
    }
}

编译以上代码,运行结果是:

name: memory
age: 24

Process finished with exit code 0

2、泛型擦除

对于传入的不同类型实参,生成的类对象实例的类型是否一样?

public class GenericTest {
    public static void main(String[] args) {
        Box<String> name = new Box<>();
        name.add("memory");
        Box<Integer> age = new Box<>();
        age.add(24);
        System.out.println("name类的类型:"+name.getClass());
        System.out.println("age类的类型:"+age.getClass());
        System.out.println(name.getClass()==age.getClass());
    }
}

编译以上代码,运行结果:

name类的类型:class other.泛型.Box
age类的类型:class other.泛型.Box
true

Process finished with exit code 0

在使用泛型类时,虽然传入不同类型的实参,但在内存上还是同一个类,没有生成真正不同的类型。在逻辑上我们可以理解为不同的泛型类型。

  • Java中的泛型只是作用于代码编译阶段,编译过的class文件不包含任何泛型信息。(在编译过程中对于正确检验泛型结果后,会将泛型的相关信息擦出,泛型信息不会进入到运行时阶段)

2、泛型方法:

你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

下面是定义泛型方法的规则:

所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的<E>)。

每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。

类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。

泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)。并且还要注意的一点是,Java中没有所谓的泛型数组一说。
实例:

public class GenericTest {
    public static void main(String[] args) {
        Integer[] integerArray = new Integer[]{1, 2, 3, 4};
        System.out.println("Integer数组改变前的数据是:");
        printArrray(integerArray);
        changeData(integerArray);
        System.out.println("Integer数组改变后的数据是:");
        printArrray(integerArray);
        Character[] characterArray = new Character[]{'a', 'b', 'c', 'd'};
        System.out.println("Character数组改变前的数据是:");
        printArrray(characterArray);
        changeData(characterArray);
        System.out.println("Character数组改变后的数据是:");
        printArrray(characterArray);
    }

    public static <E> changeData(E[] array) {
        if (array == null || array.length == 0) {
            return;
        }
        for (int i = 0, j = array.length - 1; i < array.length / 2; i++, j--) {
            E temp;
            temp = array[i];
            array[i] = array[j];
            array[j] = temp;
        }
    }

    public static <E> void printArrray(E[] array) {
        if (array == null || array.length == 0) {
            return;
        }
        for (E element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

编译以上代码,程序执行结果是:

Integer数组改变前的数据是:
1 2 3 4 
Integer数组改变后的数据是:
4 3 2 1 
Character数组改变前的数据是:
a b c d 
Character数组改变后的数据是:
d c b a 

Process finished with exit code 0

四. 类型通配符

1. 为什么需要类型通配符

我们看一下例子:

Box<Number>和Box<Integer>实际上都是Box类型,现在需要继续探讨一个问题,那么在逻辑上,类似于Box<Number>和Box<Integer>是否可以看成具有父子关系的泛型类型呢?

为了弄清楚,我们看以下的例子:

public class GenericTest {
    public static void main(String[] args) {
        Box<Number> score= new Box<>(39.3);
        Box<Integer> age=new Box<>(18);
        getData(score);
        /**The method getData(Box<Number>) in the type GenericTest is not applicable for 
           the arguments (Box<Integer>)*/
        //getData(age); // 1
    }

    public static void getData(Box<Number> data){
        System.out.println(data.get());
    }
}

通过提示信息,我们知道在逻辑上Box<Number>不能视为Box<Integer>的父类。因此,我们需要一个在逻辑上可以用来表示同时是Box<Integer>和Box<Number>的父类的一个引用类型,由此,类型通配符应运而生。

2.类型通配符的定义

类型通配符一般是使用?代替具体的类型参数。例如 List<?> 在逻辑上是List<String>,List<Integer> 等所有List<具体类型实参>的父类。

实例:

public class GenericTest {
    public static void main(String[] args) {
        List<String> name=new ArrayList<>();
        List<Integer> age=new ArrayList<>();
        List<Number> number=new ArrayList<>();
        name.add("memory");
        age.add(18);
        number.add(90.7);
        getDataOfList(name);
        getDataOfList(age);
        getDataOfList(number);
    }
    public static void getDataOfList(List<?> data){
        System.out.println("当前数据是:"+data.get(0));
    }
}

程序执行结果:

当前数据是:memory
当前数据是:18
当前数据是:90.7

Process finished with exit code 0

五. 类型通配符上下限

有时候想限制传入到类型参数的类型种类范围。例如:一个操作数字仅允许传入Number和Number子类的实例。这就是有界类型参数的种类。

要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界。

类型通配符上限通过形如Box<? extends Number>形式定义,相对应的,类型通配符下限为Box<? super Number>形式,其含义与类型通配符上限正好相反。

实例:该实例的泛型方法,返回三个可比较对象的最大值。

public class MaximumTest {
    public static void main(String[] args) {
        System.out.print(String.format("%d,%d,%d 中的最大值是:%d\n", 2, 5, 1, getMax(2, 5, 1)));
        System.out.print(String.format("%.1f,%.1f,%.1f 中的最大值是:%.1f\n", 2.2, 5.5, 1.1, getMax(2.2, 5.5, 1.1)));
        System.out.print(String.format("%s,%s,%s 中的最大值是:%s\n", "apple", "orange", "pear", getMax("apple", "orange", "pear")));
        //getMax(new MyObject(1),new MyObject(2),new MyObject(3));//1 编译出错
         
            /**在(//1)处会出现错误,因为getMax()方法中的参数已经限定了
             参数泛型上限为Comparable,所以泛型为String是不在这个范围之内,所以会报错*/
    }

    public static <E extends Comparable<E>> E getMax(E x, E y, E z) {
        E max = x;
        if (y.compareTo(max) > 0) {
            max = y;
        }
        if (z.compareTo(max) > 0) {
            max = z;
        }
        return max;
    }
}

class MyObject {
    int age;

    public MyObject(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

程序执行结果:

2,5,1 中的最大值是:5
2.2,5.5,1.1 中的最大值是:5.5
apple,orange,pear 中的最大值是:pear

Process finished with exit code 0

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值